Backdrop
Backdrop draws the MixOS desktop background. It is its own program, not part of the compositor: it puts one surface on the compositor's background layer for each output, under every window and panel, and draws it with egui. A crash in it never takes the session down.
What it draws today:
- a solid colour per output, or the session theme's colour when no document names one;
- a PNG or JPEG image, with fill, fit, centre, tile or stretch sizing;
- a slideshow, from a folder or an explicit image list, with a half-second crossfade. Each output may have its own wallpaper.
The live folder view remains planned. The probe (backdrop --probe) is the
regression gate for the surfaces, scale and input, and draws its own labels.
The document
backdrop.mx (strict data, schema: 1), in the app's config directory
(AppDirs, config/backdrop.mx) unless --config PATH names another.
schema: 1
default: { colour: "#1e1e2e" }
outputs: {
"DP-1": { image: "/usr/share/wallpapers/hills.jpg", fallback: "#000000" },
}
schemais required and must be1.defaultis one wallpaper; without it the session theme's colour fills every output.- Each wallpaper names exactly one of
colour(#RRGGBB, six hex digits),image(an absolute path) orslideshow(the source and timing below).fallback(#RRGGBB) goes with an image or slideshow: it shows behind transparent pixels and in letterbox bars. Without it the theme colour shows. fitgoes with an image or slideshow. It defaults tofill, which crops equally from opposite edges to cover the output.fitpreserves the aspect ratio with letterbox bars;centrecentres the image at its native pixel size;tilerepeats at its native pixel size from the top left;stretchcovers the output without preserving the aspect ratio. Native sizing honours the output scale even when the retained texture has been downscaled.outputsmaps an output's name (DP-1) to its own wallpaper; an output without one takes the default.- Unknown keys, wrong types, bad colours, relative paths and other shapes are refused, naming the field. A bad document at start stops the app.
Command line: --colour #RRGGBB or --image FILE replaces the document's
default for this run (it is not stored). --service NAME registers the
Bus name (default backdrop).
Slideshows
schema: 1
default: {
slideshow: { folder: "/usr/share/wallpapers", interval_s: 600, shuffle: false },
fallback: "#101820",
fit: "fit",
}
Replace folder with images: ["/usr/share/wallpapers/a.png", "/usr/share/wallpapers/b.jpg"] for an explicit order. Exactly one source is
required, all paths are absolute, and an explicit list must be non-empty.
interval_s defaults to 600 and values below 5 are refused. shuffle defaults
to false. Folder discovery is bounded and non-recursive; PNG and JPEG files
are sorted before scheduling. Discovery and decoding run on the worker.
The folder is discovered at startup or on a set; reapply the document to pick up
folder changes. Repeated paths in a list share one schedule entry.
A slideshow retains its current image while the next decodes. A failed decode keeps that image and reports the error; the failed entry is skipped on the next attempt, which waits for the interval. Entries are tried again on later cycles. A successful advance crossfades for about 0.5 seconds, then releases the previous image and texture. The configured source is preserved when stored. A refused replacement also defers active slideshows for another interval, so their last good image stays visible after the refusal.
Images
- PNG and JPEG only. The file is opened once, without blocking (a FIFO or device is refused), and read through a limit of 32 MiB; anything larger is refused. The format, the header's dimensions and the pixels all come from those same bytes. A header over 16384 pixels a side, or over 32 Mpx, is refused before any pixel buffer exists. The decoder's own allocation cap is 256 MiB.
- Decoding runs on one worker thread, one image at a time. The kept image is downscaled so its longest side is at most 3840 pixels.
- At most four resident image sources per document, counted after command-line overrides. A slideshow counts as one source regardless of its list length; only its selected images are decoded.
- Every image a set names is decoded again, even one already on screen, so a file replaced at the same path is picked up by the next set.
- A set applies only once all its images are decoded. A missing, corrupt,
oversize or unsupported image refuses the whole change: the last applied
backdrop stays on screen, and the reason is in
backdrop.state. Images being kept for a refused or superseded set are dropped at once; in-flight results are discarded on completion.
Memory budgets
Two aggregate budgets, both enforced; the per-image limits above are not totals.
| Budget | Limit | Worst case |
|---|---|---|
| Decoded RGBA held by the controller (applied and candidate images together) | 512 MiB (RESIDENT_BUDGET) | a decode that would take the total over it is refused (IMAGE); the last applied images stay until a whole candidate set applies |
Accounted wallpaper texture bytes, across all outputs, overlap included (TEXTURE_BUDGET) | 512 MiB | four full-size outputs swapping (four old and four new textures) |
Each output keeps its own texture at the decoded size, capped at 3840 pixels
a side. The budget can leave additional outputs on fallback when there is no
room for their textures. An image over the device's texture-size limit keeps
the output's last good image on screen, or shows fallback when there is none,
with the limit reported in that output's error.
A full-size image is 3840 × 3840 × 4 bytes = 56.25 MiB, the largest kept. Four applied plus four candidates is 450 MiB, which fits the 512 MiB resident budget. Textures are accounted as the bytes of the new size reserved while the old is still held, so a swap is counted in full. When bytes are released, live outputs are woken once so one that was starved draws again; nothing wakes when nothing is freed.
- A context owns one image texture, or two during a slideshow crossfade.
Replacing it reserves the new size
while the old is still held, so the overlap is counted; a texture that does
not fit leaves that output on its fallback colour, with the reason in its
error. Removing an output, showing a solid colour, or a change of image releases its bytes. - A texture upload copies the decoded image once (a transient copy of at most one image, on the CPU).
These account for wallpaper data, rather than total process RSS or GPU-driver memory. Compressed input, decoder allocations, RGBA conversion and resizing have their own transient bounds; the renderer also owns resources beyond the wallpaper textures.
The Bus
Service backdrop (or --service). Verbs (registered in
docs/docs/spec/bus/verbs.conf.mix):
| Verb | Class | Body | Reply |
|---|---|---|---|
backdrop.ping | query | {} | {schema, service} |
backdrop.state | query | {} | the state below |
backdrop.set | intent | a complete document (above) | the change, once applied |
backdrop.next | intent | {} | full state, once the next images have decoded and applied |
backdrop.set replaces the whole document; it does not patch it. A valid
set waits for its images, is stored in the document's file (atomically,
through config::atomic), and then applies. Its reply is rc 0 with
{requested, active, config_file}. A refusal is rc 10 with
{error_code, message}:
ARGUMENT: the body is not a valid document (nothing changed);IMAGE: an image did not decode (nothing changed);STORE: the document could not be stored (nothing changed), or it was stored but its durability is not confirmed (applied; the reply says so);SUPERSEDED: a newer set replaced this one before it applied.BUSY: a next arrived while a set or advance was decoding;NON_SLIDESHOW: a next was requested with no applied slideshow;UNKNOWN: the requested verb is not implemented.
backdrop.next advances configured slideshows without changing or storing their
sources. It refuses a configuration with no slideshow. An advance waiting for
decode keeps the current pixels; a newer set can supersede it. Refused advances
are not published as executed commands.
Automatic advances run independently for each configured source and wait for
the active crossfade to finish. A corrupt image on one output does not stop
other outputs' schedules. A manual next advances all slideshows together and
can interrupt their current fade.
backdrop.state answers with no output on screen:
{schema, service, requested, active, pending, error, config, config_file, outputs, fit, slideshows}. fit is the default image's effective fit (or null
for a solid fill); outputs also report their effective fit. Each slideshow
reports its output (null for the default), current_path, index, count,
interval_s, shuffle, pending and fading.
requested and active are revisions: requested numbers every request,
including startup and automatic slideshow advances,
made, and active is the revision of the accepted configuration. pending is
the revision still decoding, if any. error is the last refused change's
reason until another change applies successfully, including an automatic advance.
outputs lists each output drawn so far, with its size, scale, what it
shows (theme, colour, image or fallback), its error, and
presented_revision: the revision that output last drew. Use that field to see
what is on screen; active alone does not mean every output has presented it yet.
Every applied Bus set or next is one run of its command and is published on
backdrop.command.executed with origin bus. The command line's default is
not a run: it is the start state.
Cost
It costs nothing while nothing changes. A static colour or image draws once per output and then sleeps; a set, a decode, a change of output or scale wakes it through egui's repaint request.
The slideshow controller waits on its inbox until the next schedule or fade deadline. It never polls. Repaints run only while a fade is active, and stop when the previous texture is released.
Start-up and failure
- The Bus registration must succeed: if the broker refuses or is unreachable, backdrop exits with the reason rather than running a second, unregistered wallpaper.
- An output that goes away is released (
LayerProgram::removed): its context, its texture and its status.
Known gaps
- The theme's colour is taken from the installed style's panel fill, not a named design token. Naming the desktop token is a follow-up.
- Overlapping sets (one still decoding when the next arrives) are exercised by the controller's unit tests; the Mix gate sends its sets one after another.
- Input limits bound allocations, not filesystem latency. A stalled mount can block the single decode lane until its I/O returns; state, ping, theme and solid-colour controls remain available. Superseded in-flight results are discarded on completion; already queued jobs are withdrawn immediately.
Running it
Run it inside a MixOS session: backdrop, or backdrop --colour #1e1e2e,
or backdrop --image /abs/path.jpg --service backdrop-test.
Source layout
The source is
apps/backdrop:
src/main.rs: command line, the start-up document, the layer loop;src/control.rs: the Bus controller and its one command,backdrop.set;src/program.rs: the layer program, which only draws a published view;src/probe.rs:--probe, the regression probe (JSON lines on stdout);crates/wallpaper: the engine: the document, colour, fit, decode limits and revisions. Pure data, no Bus and no window.
Tests
Building and the cargo tests are not in this tree's checks yet; the cargo
gate runs on a build worker. cargo build -p backdrop enables toolkit's
layer feature.
tests/desktop/wallpaper_gate.mixruns the wallpaper on a private broker and a private nested compd, takes screenshots of a solid colour and a PNG (share/brand's favicon), and checksbackdrop.setandbackdrop.state: a refused image keeps the last good pixels and revision, rapid sets apply in order, a non-document is refused, the accepted document is stored, and the process is idle. Run it from the checkout;--image-fixture PATHnames a different PNG and--shot-out DIRkeeps the PPMs.tests/desktop/backdrop_gate.mixis the--proberegression gate: input, paint and occlusion. It runs the probe on a private nested compd and checks the band, the paint, the screen, click delivery and occlusion:mix tests/desktop/backdrop_gate.mix --bin <compd> --config-file tests/desktop/smoke-settings.json. On a build worker add--software --private-bus --noded <noded>.- Unit tests (
cargo test -p wallpaper,cargo test -p backdrop) cover the document, colour, fit, decoder limits (including an oversized header and a FIFO), revisions, candidate and active images, the texture budget and the verb registry.