Building NES Wallpaper
I recently released NES Wallpaper, a free and open-source macOS app that turns the desktop into a grid of running NES games. Most of the time, it quietly plays tool-assisted speedruns behind the desktop icons. If you're bored and want to play one of the running games, a keyboard shortcut brings the wallpaper forward and lets you take control of any tile.
The idea came from the UberNES “Nintendo Saver”, a Windows screensaver from 2008. I used it on all of my machines until it completely broke with the release of macOS Sonoma 14. I really wanted to get it working on modern macOS, but since the UberNES project hasn't been updated in more than 10 years and the entire project is closed source (to my knowledge), I figured it would be best to recreate it entirely using an open-source emulator with a larger library of movie files.
Since Aerial already existed, along with plenty of projects for running live wallpapers on the desktop, I figured it should be possible not only to recreate the screensaver but also to get it working as a live wallpaper for macOS.
When it comes to coding, my background is mainly Terraform for Azure and PowerShell for Windows, so I didn't really know where to start with macOS development. I've been using LLMs and AI extensively in my current job to automate tasks and assist with troubleshooting issues, so I figured this would be a good project for building out my own AI coding workflow, seeing just how good these modern models have become, and learning more about macOS app development.
Using AI
This project was heavily assisted by OpenAI GPT-5.6-Sol and Anthropic's Fable. These models wrote around 65% of the code. I've mainly been using OpenAI models because of their price and the generous token limits on their subscription plans, but since many of my colleagues love Fable and talk about how good it is, I thought I would try using it for some of this project.
I have $100 subscriptions for personal use with both providers, and I can say hands down that I'll be canceling the Anthropic subscription after this month. I really liked using Fable. It's a smart model that is good at making the right assumptions, asking excellent questions, staying within the bounds of what you ask it to do, and writing in a way that just makes sense. The downside, though, is the limits on the Anthropic plan. While using Fable and Sol, I noticed that the weekly Fable limit is around six times lower than what I get with Sol. Fable also burns through tokens like crazy, but that's mostly due to Claude Code itself. Anthropic also doesn't allow you to use its subscriptions with other harnesses, so the token-efficiency optimizations I've made in OpenCode can't be used. I'll probably do a write-up on my OpenCode setup in the future and what I've done to drive down token usage in that harness.
My workflow isn't just telling the models to go and do something. I wanted to learn while they were working and understand why they made the decisions they did. It's really easy to let the models make a plan and implement it, but I wouldn't learn anything by letting them do all the work. A lot of the time, I used a skill that had the model explain its decisions, describe how it was going to implement something, and take on more of a teaching role. I didn't expect the models to do as well as they did. About half the time, after I asked one to plan something and it gave me a plan, I was able to think of a different way to do it, confirm that it would work, and then implement it myself with the model's assistance when needed. I'd recommend doing something similar if you're trying to learn something new, as these models can be excellent teachers when you force them to work with you instead of simply doing the work themselves.
All of my dotfiles are available in my dotfiles repository, and I'll be updating it with more of the skills and workflows I used for this project once I've cleaned them up.
The choice of emulator
NES Wallpaper uses the FCEUX emulator core. FCEUX is mature, supports the FM2 movie format, and already provides the hardware accuracy needed by a large collection of games. The core is vendored into the project and wrapped by the native macOS application. The main reason I used FCEUX was TASVideos. It has a huge collection of tool-assisted speedruns, many of which are available as FM2 input movies that anyone can download and watch.
Keeping resource use reasonable
One emulator is inexpensive on a current Mac. A grid of them, spread across multiple high-resolution displays and running continuously, gets expensive quickly. A lot of the work on this project went into making sure NES Wallpaper didn't become the most resource-intensive application on the machine.
Each grid cell runs FCEUX in its own helper process. This keeps the emulator's global state isolated and lets the operating system schedule the tiles across CPU cores. The helpers don't send screenshots through pipes or encode every frame as an image. Instead, each helper publishes into a file-backed, memory-mapped buffer. FCEUX converts the frame directly into the back buffer, atomically flips which buffer is in front, and the app uploads that memory straight into a Metal texture. Removing the extra image conversion and copy from every tile on every frame made a meaningful difference.
The Metal renderer also avoids work whenever it can. It checks the published frame counter before uploading a tile, so unchanged frames never get sent to the GPU again. All of the tiles for a display are then composited in a single render pass. If no tile changed, the window keeps the previous CAMetalLayer contents instead of acquiring and presenting an identical full-screen drawable. Multiple displays mirror the same grid of helper processes, which means plugging in another monitor adds texture uploads and draws, but it does not start a second set of emulators.
NES Wallpaper also includes a Low Power Mode. The NES still emulates every frame at its native NTSC rate so tool-assisted runs, game state, and timing remain deterministic. What changes is the expensive presentation work: every other frame skips color conversion and publication, the display link is capped at 30 frames per second, and the wallpaper renders at logical resolution instead of full Retina resolution. A tile under live control returns to full-rate publication so input still feels responsive.
We also added the ability to pause the background when it isn't visible. NES Wallpaper watches the occlusion state of its windows and pauses the helper processes when every wallpaper window is completely covered or the screen is locked. That was a little tricky because I use applications like Ghostty with a slightly transparent background. I still wanted the games to move underneath a transparent window, while a fully opaque or full-screen application shouldn't. The occlusion change is debounced to avoid repeatedly pausing and resuming while macOS is reordering windows, and a paused helper blocks on a condition instead of waking up at the NES frame rate just to check whether it should still be paused.
These optimizations alone were enough to lower CPU usage from 21% to 4% with six games running. I'm sure there's more we could do here, but I'm happy with its current state.
From wallpaper to screensaver
Once the grid worked on the desktop, a screensaver was a natural extension. The app ships with an optional .saver bundle in its resources. Pressing Install in NES Wallpaper's settings copies that bundle into ~/Library/Screen Savers, after which it can be selected in macOS under System Settings → Wallpaper → Screen Saver. Since it is a real screensaver plugin, it also runs on the lock screen.
The .saver itself is a Swift dynamic library wrapped in the old bundle structure expected by Apple's legacyScreenSaver host. Its principal class is a ScreenSaverView, but it does not launch another set of emulators. Doing that would double the resource usage and introduce a completely separate copy of the library state. Instead, the screensaver is a thin, read-only client of the emulator processes that NES Wallpaper is already running.
The wallpaper app writes an atomic JSON manifest containing the grid dimensions, video size, Low Power Mode state, and the path to each tile's frame file. The screensaver polls that manifest, memory-maps those frame files read-only, and composites them with the same Metal renderer used by the desktop wallpaper. When a game rotates, the manifest changes and the screensaver remaps only that tile. If the app isn't running, the screensaver displays a message explaining why there are no frames instead of failing with a black screen and no context.
Getting this to work on the lock screen required some workarounds. Taking some ideas from Aerial, I worked around the fact that the screensaver is hosted in a sandboxed Apple process and that macOS app-container protections prevent the main app from reading a status file written inside that container, even though both processes belong to the same user. The shared frames therefore live under /Users/Shared/NESWallpaper, one of the few locations the screensaver host can read without requesting access from the user.
The screensaver sends a small heartbeat to the app over a loopback UDP socket while it is actively consuming frames. Normally, NES Wallpaper pauses emulation when the screen locks because the desktop is no longer visible. A current heartbeat tells it that the screensaver is showing those frames and that the emulators need to keep running. When animation stops, the heartbeat stops too, becomes stale after a few seconds, and allows the app to pause again. This prevents a leftover screensaver process from keeping an entire grid of emulators awake forever.
That last part matters because macOS Sonoma and newer versions are known to leave the legacy screensaver host—and sometimes the screensaver instance itself—alive after the user dismisses it. The plugin has to explicitly exit after full-screen playback stops, while avoiding that workaround inside the System Settings preview. It isn't pretty, but it keeps a dismissed screensaver from continuing to burn GPU time in the background.
I'm not sure how long the screensaver portion will last, but I can confirm that it works with the macOS 27 Golden Gate developer beta, and I will continue trying to keep it working for as long as possible.
Non-TAS movies
I've implemented the TASVideos API directly into NES Wallpaper. This allows end users to download TAS movies and watch them in NES Wallpaper.
Something UberNES had was a catalog of normal game movies that could be downloaded and used to show normal gameplay rather than hyper-optimized speedruns. I imagine UberNES's idea was to have something playing like an old attract screen that was easy to take over.
To offer something similar, I've added some of my own FM2 files, along with other FM2 files that I was able to convert from different emulator formats. I have files for most of the games UberNES had, but not all of them. Some of these may not work properly, but I haven't run into any issues in my testing yet.
You can find them on the downloads page. Just download the ZIP file and throw those files into your NES Wallpaper movies folder.
Making the app "official"
This was a big unknown for me, and let me tell you: Apple doesn't make it easy to build and distribute applications for macOS. First, to keep the application from triggering Gatekeeper warnings, you need to pay Apple $100 per year for a developer account. That account gives you access to the Developer ID certificate and notarization service needed to distribute an application outside the App Store without telling every user to bypass their security settings. Apple has made bypassing those warnings even more difficult, as users can no longer simply right-click an application and select Open. You have to go into Settings itself to bypass them.
Even before signing, a successful swift build does not produce a Mac application. It produces executables and libraries. I had to assemble the .app bundle with the correct Contents/MacOS, Contents/Resources, Info.plist, icon assets, licenses, and package metadata. NES Wallpaper's bundle contains the main menu-bar executable, the nes-helper executable used for every emulator tile, and the complete .saver bundle. The build also has to keep the app and screensaver version numbers synchronized and support both the modern asset-catalog icon and the older .icns fallback.
NES Wallpaper is normally an accessory application with no Dock icon, controlled from the menu bar, but it can switch to a regular application when the user asks for a Dock icon. Its wallpaper windows are borderless AppKit windows placed at the desktop window level, joined to every Space, excluded from normal window cycling, and configured to ignore mouse events until takeover mode is activated. The settings interface is written with SwiftUI, while the application lifecycle, menu bar, windows, display links, global shortcut, and most of the system integration still rely on AppKit and other older frameworks.
Even a simple feature such as Launch at Login has a catch. Apple's SMAppService can register the installed .app, but it cannot register the bare SwiftPM executable used during development. The same is true of installing the screensaver because the development executable has no application resources containing the .saver. Features can therefore behave correctly in a packaged build while being unavailable when the exact same program is launched through the normal development workflow.
Then comes code signing. Every piece of executable code has to be signed, and the nesting order matters. We first sign the helper executable, then the embedded screensaver bundle, and finally the outer application bundle. A release signature also needs the hardened runtime and a secure timestamp. After that, the app is copied into a compressed DMG alongside an Applications shortcut, and the disk image itself is signed.
Signing, though, still isn't enough. The DMG has to be uploaded to Apple's notarization service and wait for an Accepted result. That notarization ticket then has to be stapled to the disk image so Gatekeeper can validate it even when the Mac is offline. Finally, the entire result is checked again with codesign, stapler, and spctl. Our release workflow has to import the signing certificate into a temporary keychain, supply separate App Store Connect credentials to the notary service, build everything in the correct order, and only then publish the DMG. The way I'm doing this with the scripts I've created seems to be the best practice, but even with the assistance of LLMs, finding information on how to do it was not easy.
The frustrating part isn't that security exists; signing downloaded applications is a good thing. The frustrating part is how many separate systems, generations of frameworks, special bundle layouts, and undocumented-feeling edge cases have to line up before the application behaves normally. The screensaver still uses a legacy plugin API hosted inside a sandboxed system process. That host doesn't reliably shut down after dismissal. The Screen Saver settings pane moved under Wallpaper in macOS 26, but the old deep link is still the one that opens it. SwiftUI features available in Xcode may be unavailable through the Command Line Tools because the macro plugin is missing. An app launched from the wrong location can also be translocated by macOS and behave differently from the copy in Applications.
I expected the emulation and rendering to be the hard parts. Instead, a surprising amount of the project was convincing macOS that the app, its helper, its embedded screensaver, and its installer were all legitimate pieces of the same application. Apple provides an API for almost every individual part, but turning those parts into something another person can download and run is far more painful than it needs to be.
What now?
I'll continue to update and support NES Wallpaper for as long as I can. Working on these kinds of projects is a lot of fun and a good break from my day job.
Disclaimer
I'm just one guy who wanted to make something cool for myself and learn some stuff along the way, but I figured I'd share it for other people to enjoy as well. If you find an issue with the project, please open an issue on GitHub.
If you're the owner of one of the services used by this application and find the application to be in breach of your license or usage guidelines, please reach out to me immediately at contact@wolff.tech. I'm no expert, but I did my best to respect the licenses and follow the guidelines laid out by the services we use. I'm happy to remove or modify anything that doesn't follow the proper usage guidelines or that I may have misinterpreted.