A screensaver that shows Magic: The Gathering illustration art with slow Ken Burns pans and crossfades, on Ubuntu and Windows.
Art comes from Scryfall; images are upscaled once with Real-ESRGAN and cached, so the render loop stays smooth.
python3 -m venv .venv && . .venv/bin/activate
pip install -e .
# one-time: fetch the Real-ESRGAN build that bundles models (~45 MB)
mkdir -p vendor/realesrgan && cd vendor/realesrgan
curl -LO https://github.com/xinntao/Real-ESRGAN/releases/download/v0.2.5.0/realesrgan-ncnn-vulkan-20220424-ubuntu.zip
unzip -o realesrgan-ncnn-vulkan-20220424-ubuntu.zip && rm realesrgan-ncnn-vulkan-20220424-ubuntu.zip
chmod +x realesrgan-ncnn-vulkan && cd ../..
mtgss doctor # verify GPU, upscaler, displays
mtgss index # build the artwork index (~10s)
mtgss prefetch --count 100 # warm the cache (~16s per image)
mtgss run --display 1 # goNote the upscaler binary must come from the xinntao/Real-ESRGAN releases,
not Real-ESRGAN-ncnn-vulkan — only the former bundles the model files.
| Command | Purpose |
|---|---|
mtgss doctor |
Check Vulkan, upscaler, index, cache, displays |
mtgss index |
Build the index from Scryfall bulk data |
mtgss index --rescore-only |
Re-apply curation weights after editing config |
mtgss prefetch --count N |
Pre-produce N images |
mtgss run |
Run the screensaver |
mtgss run takes --display N, --windowed, --slide SECONDS,
--fade SECONDS, --stats, and --no-upscale.
| Key | Action |
|---|---|
f |
Favourite the current artwork (oversampled later, never evicted) |
x |
Block it (never shown again, removed from cache) |
space |
Pause |
→ |
Skip to the next artwork |
i |
Toggle the caption |
| anything else | Exit |
index (sqlite) producer (background) renderer
───────────── ───────────────────── ────────
unique_artwork.jsonl pick candidate (weighted) play queue from ready/
art_tags.jsonl ──► download art_crop ──► SDL2 GPU textures
→ artworks table realesrgan-ncnn-vulkan x4 Ken Burns + crossfade
+ tag weights write ready/ atomically caption overlay
The renderer only consumes images already in ready/. If the cache runs dry it
falls back to a Lanczos upscale rather than stalling on the neural pass, and
JPEG decoding happens on a worker thread so it never lands mid-crossfade.
Scryfall's art crops top out around 626×457 — that is the hard ceiling, and the newer WebP variants are the same pixels in a smaller file. Filling a 4K display therefore means a ~6× upscale.
art_crop images are also scans of printed cards, so they carry halftone
rosette artifacts. Measured on an Intel RPL-P iGPU, 626×457 → 2504×1828:
| Model | Time | Result |
|---|---|---|
| Lanczos (no NN) | instant | soft, and amplifies the halftone |
realesr-animevideov3 |
0.9s | sharpens the halftone into crunchy dots |
realesrgan-x4plus-anime |
5.6s | clean but flattens brushwork, posterised |
realesrgan-x4plus |
16.2s | removes print screen, keeps the paint |
Only x4plus both denoises the scan and preserves painterly texture. The cost is paid once per image, in the background, and cached.
realesrgan-ncnn-vulkan exits 0 even when inference has silently failed.
Under GPU contention — notably running mtgss prefetch while the screensaver is
drawing on the same Intel iGPU — tiles come back empty and the result is a
mostly-black image. Nothing downstream notices: the file is a valid JPEG of the
right dimensions, so it is cached and then redisplayed on every rotation, the
artwork appearing as a narrow strip against black.
Every upscale is therefore verified against its source before being accepted. Both images are reduced to 32×32 and compared; a real upscale changes detail, not overall composition. Measured separation is wide:
| low-frequency difference | |
|---|---|
| Good upscales | 1.4 – 3.6 |
| Corrupt upscales | 61 – 127 |
| Reject threshold | 12 |
A rejected result is retried once (the corruption is transient), then falls back to Lanczos. Nothing that fails validation is ever written to the cache.
To audit images cached before this check existed, or after an interrupted run:
mtgss verify # report
mtgss verify --repair # rebuild the bad onesmtgss doctor reports the corrupt count too.
Avoid running prefetch while the screensaver is displaying — they compete
for the same GPU. Validation makes contention safe rather than silently
destructive, but it still wastes a 16s upscale per retry.
Artwork selection is weighted-random over a precomputed score combining:
- Quality filters — high-res scans only, no digital-only cards, no joke sets
- Aspect preference — wider crops need less cropping on a 16:9 screen; 626×457 loses ~23% of its height, 571×460 old frames lose ~30%
- Art tags from Scryfall's Tagger project (482k taggings over 52,784
illustrations).
no-one— art with no figures — is the single best predictor of something that reads well fullscreen. Close-ups and headshots are demoted. - Favourites and blocks you set with
fandx
Tag weights live in the config file, not in code, because tuning them is the main lever for dialling in what counts as good art:
~/.config/mtg-screensaver/config.toml
Copy config.example.toml there to start. Re-run mtgss index --rescore-only
after editing.
Artwork narrower than max_crop_fraction allows is letterboxed onto a matte of
its own dominant colour rather than cropped harder. Full-art cards, whose crops
come back portrait, always take this path.
--display N is a request. Under GNOME/mutter every SDL placement route
tested — creation-time position, post-map SDL_SetWindowPosition, and
SDL_WINDOWPOS_CENTERED_DISPLAY — was overridden, and the window consistently
opened on whichever monitor had focus. The app asks for genuine fullscreen
(which mutter does honour) and then adapts to whatever surface it is given, so
it always renders correctly; it just may not be the monitor you named.
The renderer also re-reads its drawable size every frame and re-plans the layout if it changes, so a resize or a window manager surprise cannot leave the geometry stale.
GNOME dropped screensaver support, so there is no system hook to register with.
Bind mtgss run to a keyboard shortcut:
Settings → Keyboard → View and Customise Shortcuts → Custom Shortcuts → +
Command:
/path/to/mtg-screensaver/.venv/bin/mtgss run --display 1
To start it automatically on idle instead, misc/mtg-screensaver.desktop can be
launched from a small idle watcher (xidlehook, xss-lock); that is not wired
up yet.
Fetch the Windows Real-ESRGAN build into vendor/realesrgan/, then:
python build_windows.py
This produces dist/mtg-screensaver.scr. Right-click → Install, or copy it to
C:\Windows\System32. It handles /s (run), /c (config) and /p (preview).
Card images and artwork are property of Wizards of the Coast. This is unofficial Fan Content permitted under the Fan Content Policy. Not approved or endorsed by Wizards. Portions of the materials used are property of Wizards of the Coast. ©Wizards of the Coast LLC.
Scryfall's image guidelines require that the artist and source stay identifiable
wherever an art crop is shown, which is why the caption is on by default. i
hides it for a session; it is not a setting.
.venv/bin/python -m pytest tests/ -q