open-mpv
I built a Rust / GTK4 media viewer with bounded decode scheduling, generation-checked rendering, byte-budgeted caching, and explicit playback and file-write lifecycles.
- Focus
- Async scheduling · memory bounds · file safety
- Built with
- Rust · GTK4 · glycin · GStreamer
- Status
- Active · installable on Fedora
- Ownership
- Personal project
Engineering focus
The engineering problem is keeping GTK's main loop responsive while storage, image decoders, and video pipelines finish work at different times. A user can change selection, switch folders, or edit a source file before earlier operations complete. I built the scheduler, cache, and presentation rules around that changing state.
- The first-frame scheduler caps active decoding at two jobs, including jobs that are still cancelling.
- The image cache budgets additional decoded media by bytes and entry count; the displayed image has a separate exemption.
- Generation checks and per-path query versions prevent old work from replacing current media or restoring invalid cache entries.
Architecture
-
GTK presentation and background I/O
GTK owns input and presentation. Folder enumeration and subtitle discovery each use one active worker and one replaceable pending request. A storage call already in progress keeps its worker slot until it returns, even after cancellation.
-
Image scheduler and decoded cache
glycin produces decoded images. The scheduler tracks the foreground request and up to two neighbors, shares in-flight work for the same path, and starts foreground work before speculation. A small LRU cache retains decoded neighbors within its configured budgets.
-
Separate video playback state
GStreamer handles streaming rather than populating the image cache. A playback model tracks the current session, seeks, rates, audio tracks, and subtitles. Rendering and transport state follow the video lifecycle instead of borrowing image-cache rules.
Engineering challenges
Cancellation must not create more work
Cancelling a decoder is a request, not proof that it has stopped. Releasing its slot immediately would let rapid navigation accumulate active decoders. I keep the slot until completion, allow at most one uncancelled speculative job, and promote a requested neighbor using the latest foreground token.
Stale results must not re-enter the cache
A slow decode or filesystem metadata query can finish after selection changes, rename, or deletion. Results carry generation or per-path version state. File events invalidate affected cached and pending work; superseded results cannot restore it. Current-image changes coalesce through a 100 ms quiet-period refresh.
Bound extra memory without excluding large images
The current image must remain viewable even if it exceeds the neighbor-cache budget. Additional decoded textures count against checked byte and entry limits; arithmetic overflow counts as over-budget. A zero neighbor budget disables retention, and folder replacement prunes old entries.
Preserve files across an interrupted save
Image edits are staged in a temporary file in the source directory. The save path restores ownership, permissions, and user extended attributes, synchronizes the staged file, replaces the destination, and attempts directory synchronization. Trash Undo uses no-replace rename semantics, so it refuses to overwrite a recreated file.
Playback commands and pipeline observations can disagree
A seek or rate request can be pending while the pipeline still reports earlier state. I keep requested and accepted state separate in a session model, and tie delayed error handling to that session's generation. The controls can then track pending commands without treating them as completed pipeline changes.
Tradeoffs and current limits
The two-job bound limits concurrent work, not the memory required to decode one large file. The displayed image is also exempt from the neighbor-cache budget. Those are deliberate boundaries, so I do not describe the app as having a fixed total memory ceiling.
Fedora 44 Workstation on GNOME, Wayland, and x86-64 is the supported environment. Arch / Omarchy packaging remains experimental. Video support depends on installed codecs and drivers. Cold-start and navigation timings in the requirements are targets, not published benchmark results.
Code and documentation
Read the code and the documents behind this explanation.