Playa - Image Sequence Player
Experimental project: Built to explore Rust's ecosystem and CI/CD patterns while building some cool tools. Production-ready where tested, rough edges expected elsewhere. Open source contributions welcome.
Note on CI/CD (GitHub Actions): we now use a single workflow for both release and dev builds triggered by tags, and a smarter cache warmer. See “CI/CD Workflows” below.

Image sequence player for VFX workflows. Async loading, LRU caching, OpenGL rendering.
Features
- Dual EXR backends: Choose between pure Rust (exrs) for fast builds or OpenEXR C++ for full DWAA/DWAB compression support
- Multi-format support: EXR, PNG, JPEG, TIFF, TGA with fast parallel loading
- HDR pixel precision: Native support for 8-bit, 16-bit half-float, and 32-bit float images
- Drag-and-drop: Drop any image file - automatically detects and loads the entire sequence
- Smart sequence detection: Load one frame (e.g.,
render.0001.exr) - finds all frames automatically - Persistent playlist: Load multiple sequences, auto-saves and restores between sessions
- Color-coded timeline: Visual sequence boundaries with real-time frame load indicators
- Responsive scrubbing: Instant frame navigation - always responsive even during fast scrubbing, cancels stale loads automatically
- Cursor-centered zoom: Mouse wheel zoom centers on cursor position (like Nuke/Houdini)
- Playback controls: Standard transport controls (play/pause, JKL shuttle, loop)
- Viewport controls: Zoom, pan, fit-to-window, 100% pixel-perfect view
- Custom GLSL shaders: Load display shaders from
shaders/directory - LUTs, color transforms, custom effects - Smart memory management: Automatically manages cache size - never runs out of memory
- Settings dialog: Theme switching, font size, preferences (F3)
- Cinema mode: Fullscreen playback with hidden UI
- Persistent settings: Everything saves automatically - window layout, zoom level, shader selection
Installation
Download Pre-built Binaries (Recommended)
Download the latest release for your platform from the Releases page:
Two backends available for each platform:
-
Windows:
playa_x.x.x-exrs-x64-setup.exe- Pure Rust backend (fast, no DWAA/DWAB)playa_x.x.x-openexr-x64-setup.exe- OpenEXR C++ backend (full compression support)- Portable ZIPs:
playa-exrs-x86_64-pc-windows-msvc.zip,playa-openexr-x86_64-pc-windows-msvc.zip
-
Linux:
playa-x.x.x-exrs.AppImage/playa-x.x.x-exrs.deb- Pure Rust backendplaya-x.x.x-openexr.AppImage/playa-x.x.x-openexr.deb- OpenEXR C++ backend- Portable ZIPs:
playa-exrs-x86_64-unknown-linux-gnu.zip,playa-openexr-x86_64-unknown-linux-gnu.zip
-
macOS:
playa-x.x.x-exrs.dmg- Pure Rust backend (code-signed & notarized)playa-x.x.x-openexr.dmg- OpenEXR C++ backend (code-signed & notarized)- Portable ZIPs:
playa-exrs-aarch64-apple-darwin.zip,playa-openexr-aarch64-apple-darwin.zip
macOS Security Note: Applications are code-signed with a Developer ID certificate and notarized by Apple. macOS Gatekeeper will allow the app to run without warnings. Simply double-click the DMG and drag Playa.app to Applications.
Which backend to choose?
- exrs: Faster installation, smaller size, pure Rust. Use if you don't need DWAA/DWAB compression.
- openexr: Full OpenEXR feature support including DWAA/DWAB. Use for maximum compatibility.
Build from Source
Playa supports two EXR backends:
| Backend | Build Command | Dependencies | DWAA/DWAB Support |
|---|---|---|---|
| exrs (default) | cargo build --release |
None (pure Rust) | No |
| OpenEXR (optional) | cargo xtask build --release --openexr |
C++ compiler, CMake | Yes |
Option 1: Default Build (exrs - Pure Rust)
Fast build with no external dependencies. Suitable for most workflows:
# Build with exrs backend (pure Rust, no DLLs)
The compiled binary will be in target/release/playa (or playa.exe on Windows).
Limitations: Cannot load EXR files with DWAA/DWAB compression. Will show helpful error message with build instructions.
Option 2: Full OpenEXR Support (C++ Backend)
Supports all EXR compression formats including DWAA/DWAB:
Prerequisites:
- Rust 1.70+
- C++ compiler and CMake
# Build with OpenEXR backend (full format support)
# Or use the wrapper script
Note: OpenEXR backend compiles C++ libraries (~5-10 minutes first build, then cached).
Using xtask - Project Build Automation
What is xtask?
xtask is an idiomatic Rust pattern for build automation using a workspace helper binary. It provides cross-platform task automation without external dependencies (no Makefiles, no Python, no shell scripts).
Why xtask?
- Cross-platform: Same commands work identically on Windows, Linux, and macOS
- No external tools: Pure Rust, uses project's existing toolchain
- Type-safe: Catch errors at compile time, not runtime
- Self-documenting: Built-in
--helpwith structured command definitions - Integrated: Direct access to project workspace and Cargo metadata
- Maintainable: Refactor-friendly Rust code instead of brittle shell scripts
Quick Start (New Contributors):
# Bootstrap script handles everything
# Shows xtask help and available commands
# Automatically installs missing dependencies (cargo-release, cargo-packager)
# Builds xtask binary if needed
Available Commands:
# Build automation
# Release management
# Platform-specific (Linux only, OpenEXR backend)
What cargo xtask build does:
Without --openexr (default - exrs backend):
- Runs
cargo build [--release]with pure Rust exrs backend - No external dependencies copied (self-contained binary)
With --openexr (OpenEXR C++ backend):
- Linux: Patches OpenEXR headers for GCC 11+ compatibility
- All platforms: Runs
cargo build [--release] --features openexr - All platforms: Copies native libraries (OpenEXR, Imath, zlib) to target directory
- All platforms: Copies shaders from project root
- Linux: Creates necessary symlinks for library loading
Common Workflows:
# Development build (exrs backend - fast, no external deps)
# Development build with full OpenEXR support
# Release build and local install (exrs)
# Release build and local install (OpenEXR)
# Create dev tag and push (triggers CI Build workflow)
# Preview unreleased changelog
# Create PR from dev to main (typical release workflow)
# Create release from main branch (after merging PR)
## CI/CD Workflows
### Complete Workflow
)
)
)
)))
Backend comparison:
-
exrs (default):
- ✅ Pure Rust, fast compilation (~2-3 minutes)
- ✅ No external dependencies
- ❌ No DWAA/DWAB compression support
- Use for: Development, quick iterations
-
openexr (feature flag):
- ✅ Full OpenEXR feature support (DWAA/DWAB/etc)
- ✅ Battle-tested C++ implementation
- ❌ Requires C++ compiler, CMake
- ❌ Slower compilation (~3-4 minutes)
- Use for: Production builds, full compatibility
Development Dependencies
Auto-installed by bootstrap script:
cargo-release- Version bumping and tag creationcargo-packager- Cross-platform installer generation (v0.11.7)
Standard Rust tools (usually pre-installed):
rustup- Rust toolchain managercargo- Rust package managerclippy- Linter (rustup component add clippy)rustfmt- Code formatter (rustup component add rustfmt)
Required for PR workflow:
gh- GitHub CLI (used bycargo xtask pr) - Installation
Optional tools:
git-cliff- Changelog generation (used bycargo xtask changelog)cargo-audit- Security vulnerability scanningcargo-llvm-cov- Code coverage
GitHub Actions CI/CD
Automated builds for Windows, Linux, and macOS with strict cache reuse and optional warm-up.
Workflows
main.yml(Release): tagsv*on main or manual dispatch. Publishes release assets.dev.yml(Build): tagsv*not on main or manual dispatch. Produces dev artifacts.warm-cache.yml(Warm Cache): push tomain/devor manual dispatch. Builds only to warm caches (no packaging/uploads).
Caching Strategy
- Full cache contents:
~/.cargo/registry,~/.cargo/git,~/.cargo/bin, and fulltarget/. - Strict cache key:
playa-${platform}-v1-${arch}-${hash(Cargo.lock)}. - Multi-source restore order: current ref →
refs/heads/main→refs/heads/dev. - Save only if no restore hit (no duplicate uploads when a cache exists).
- Per-platform and per-arch isolation to avoid cross-OS conflicts.
- OpenEXR job runs first, then exrs (to ensure the heavy build creates/updates the cache before the lightweight one uses it).
Tooling cache (cargo-packager):
- Separate cache for the
cargo-packagerbinary, independent ofCargo.lockchanges. - Key:
tools-cargo-packager-0.11.7-${OS}-${ARCH}. - Paths (OS-specific):
~/.cargo/bin/cargo-packager(Unix) or~/.cargo/bin/cargo-packager.exe(Windows); no overlap with project cache. - Restore before install; if not found, install and then save (with a lookup-only check to avoid races).
Tag checks (release vs dev):
- Both
main.ymlanddev.ymlexplicitly fetch remote branches and compare the tag commit ($GITHUB_SHA) againstorigin/main. - Release runs only if the tag commit is contained in
origin/main; dev runs only otherwise.
Why this works without “warmup:
- If a matching cache exists on any of the refs above, it is reused and not re-saved.
- If no cache exists yet, the first run (on any ref) creates it with the exact key.
Ref scoping note:
- GitHub caches are scoped by ref. The warm-cache workflow ensures caches exist under
main/devso future tags can reuse them immediately.
Warm Cache Workflow
- Location:
.github/workflows/warm-cache.yml. - Triggers: push to
mainordev, and manual dispatch (workflow_dispatch). - Matrix: single job uses a platform matrix for
windows,linux,macos. - Behavior: runs builds with
cache_only: trueto populate caches; skips packaging and uploads. - Backends: accepts
backendsinput to control which backends to warm.- Values:
openexr|exrs|both. - Default when manually dispatched:
openexr. - On push to
main/dev: warmsopenexrby default.
- Values:
- Execution order (when
both): OpenEXR builds first, then exrs to avoid cache races and ensure the heavy build populates caches.
Build Times
- Cold cache (first time for a given
Cargo.lock): longer, due to dependency and OpenEXR compilation. - Warm cache: significantly faster; restores dependencies and reuses full
target/.
macOS Code Signing and Notarization
macOS builds are automatically code-signed with a Developer ID certificate and notarized by Apple in CI/CD. This allows users to run the app without Gatekeeper warnings.
For maintainers: Setting up code signing and notarization
-
Create or obtain a Developer ID Application certificate
- Log in to Apple Developer Portal
- Create a "Developer ID Application" certificate (not "Apple Development")
- Download and install the certificate in your Keychain
-
Create App-Specific Password for notarization
- Go to https://appleid.apple.com
- Sign In → Security → App-Specific Passwords
- Generate password (name it "Playa Notarization")
- Save the password - it's shown only once!
-
Export certificate for CI/CD
# Run the helper script (requires password for certificate export)The script will:
- Find your Developer ID Application certificate
- Export it as a password-protected .p12 file
- Convert to base64 for GitHub Secrets
- Optionally upload to GitHub automatically (if
ghCLI is installed)
-
Set up GitHub Secrets
-
Verify secrets are set
Should show all 5 secrets above
How it works in CI:
- Workflow imports the certificate into a temporary keychain
- The identity string defined in
Cargo.toml([package.metadata.packager.macos].signing-identity) must match the certificate cargo-packagersigns the .app bundle with Developer IDcargo-packagerautomatically notarizes the app usingAPPLE_ID,APPLE_PASSWORD, andAPPLE_TEAM_IDenvironment variables- Apple processes notarization (usually takes 1-5 minutes)
- A verification step checks both signature and notarization status
Workflow logs show:
- ✅ Certificate import success with identity name
- ✅ Code signature verification
- ✅ Notarization status (submitted, processing, success)
- ✅ Gatekeeper assessment result
- ⚠️ Clear warnings if signing/notarization fails
Notarization process:
cargo-packagersubmits the signed .app to Apple for notarization- Apple scans for malware and validates the signature
- If approved, Apple issues a notarization ticket
- The ticket is stapled to the .app bundle
- Users can run the app without warnings
Standard Rust Development
# Testing
# Documentation
# Code quality
# Build variants
Linux-Specific Build Notes
Note: These instructions apply only to the OpenEXR C++ backend (--openexr feature). The default exrs backend requires no external dependencies.
OpenEXR GCC 11+ Header Patching:
OpenEXR 3.0.5 headers are missing #include <cstdint>, causing compilation errors with GCC 11+:
error: 'uint64_t' has not been declared
cargo xtask pre automatically patches 3 header files in ~/.cargo/registry/src/:
ImfTiledMisc.hImfDeepTiledInputFile.hImfDeepTiledInputPart.h
The patching is idempotent and version-agnostic - safe to run multiple times.
See: https://github.com/AcademySoftwareFoundation/openexr/issues/1157
Native Libraries (7 Required):
| Library | Purpose |
|---|---|
| OpenEXR Core (4 libs) | EXR reading/writing, utilities, threading, exceptions |
| Imath | Math library |
| Zlib | Compression |
| OpenEXR-C | C API wrapper from openexr-sys |
Library Copy Process (cargo xtask post):
-
Locate libraries compiled by
openexr-sys:- Searches
target/release/build/openexr-sys-*/out/for versioned.sofiles - Example:
libOpenEXR-3_2.so.31.0.0,libImath-3_1.so.29.9.0
- Searches
-
Copy to target directory:
- Destination:
target/release/(next toplayabinary) - Preserves original versioned filenames
- Destination:
-
Create SONAME symlinks:
libOpenEXR-3_2.so -> libOpenEXR-3_2.so.31.0.0libOpenEXRCore-3_2.so -> libOpenEXRCore-3_2.so.31.0.0libOpenEXRUtil-3_2.so -> libOpenEXRUtil-3_2.so.31.0.0libImath-3_1.so -> libImath-3_1.so.29.9.0- Plus OpenEXR-C wrapper lib
Why this is needed:
openexr-sysbuild creates libraries with full SONAME versions- Rust linker expects generic
.sonames without version suffixes - Without symlinks:
error while loading shared libraries: libOpenEXR-3_2.so: cannot open shared object file
RPATH Configuration:
.cargo/config.toml sets RPATH to $ORIGIN, so the executable searches for .so files in its own directory:
[]
= ["-C", "link-arg=-Wl,-rpath,$ORIGIN"]
No LD_LIBRARY_PATH needed! Combined with symlinks from cargo xtask post, the binary is fully self-contained.
Troubleshooting:
Build fails with "uint64_t has not been declared":
Libraries not found when running:
After cargo clean:
Windows-Specific Build Notes
Note: These instructions apply only to the OpenEXR C++ backend (--openexr feature). The default exrs backend requires no external DLLs.
Native Libraries (DLL Management):
Windows requires .dll files alongside the executable. The same 7 OpenEXR/Imath/zlib libraries are needed, just as .dll instead of .so.
Library Copy Process (cargo xtask post):
-
Locate DLLs compiled by
openexr-sys:- Searches
target/release/build/openexr-sys-*/out/bin/for.dllfiles - Example:
OpenEXR-3_2.dll,Imath-3_1.dll,zlib.dll
- Searches
-
Copy to target directory:
- Destination:
target/release/(next toplaya.exe) - Windows DLLs don't use versioned SONAME - simpler than Linux
- Destination:
Why this is needed:
- Windows searches for DLLs in the same directory as the executable
- Without DLLs:
The code execution cannot proceed because OpenEXR-3_2.dll was not found - No PATH modification needed - self-contained binary
No RPATH equivalent:
- Windows automatically searches the executable's directory first
- No special linker flags required (unlike Linux
$ORIGIN)
Usage
Launch
# Start with empty player (drag-and-drop or file dialog)
# Load specific file or sequence
# Use custom config directory
# Enable file logging
Keyboard Shortcuts
Playback Controls:
Space- Play/PauseJ/,/←- Jog backward / decrease speedK/↓- Stop playback / decrease FPSL/./→- Jog forward / increase speed↑- Go to startCtrl+←- Jump to startCtrl+→- Jump to end'/`- Toggle loop
Viewport:
F- Fit to window (auto-fit mode)A/1/Home/H- 100% zoomMouse Wheel- Zoom in/out (center on cursor)Middle Mouse Drag- PanLeft Click + Drag- Scrub timeline
UI:
F1- Toggle help overlayF2- Toggle playlist panelF3- Toggle settings dialogZ- Toggle fullscreen (cinema mode)ESC- Exit fullscreen / QuitQ- QuitCtrl+R- Reset settings to default
Visual Sequence Navigation
The time slider provides visual feedback for multi-sequence playback:
- Color-coded zones: Each loaded sequence is displayed with a unique color on the timeline
- Sequence boundaries: White vertical dividers mark where sequences start/end
- Load indicator bar: Colored blocks below timeline show frame load status:
- Dark gray: Placeholder (not requested)
- Blue: Header only (detected but not loaded)
- Orange: Currently loading
- Green: Fully loaded
- Red: Load error
- Adaptive labels: Sequence names appear on the timeline when space permits
- Instant navigation: Click or drag anywhere on the timeline to jump to that frame
This makes it easy to identify and navigate between different sequences in your playlist at a glance.
Settings Dialog
Press F3 to open the settings dialog with TreeView categories:
UI Category:
- Font Size: Adjust global UI font size (10-18px, default 13px)
- Dark Mode: Toggle between dark and light themes
Settings are automatically persisted to playa.json.
Architecture
Core Components
┌─────────────┐
│ PlayaApp │ Main application (egui/eframe)
└──────┬──────┘
│
├──── Player ───────┐
│ │
│ ┌────▼────┐
│ │ Cache │ LRU cache + async loader + epoch counter
│ └────┬────┘
│ │
│ ┌────▼────────┐
│ │ Sequences │ Pattern-based frame lists
│ └────┬────────┘
│ │
│ ┌────▼────┐
│ │ Frames │ Individual images with status
│ └─────────┘
│
├──── Viewport ────┐
│ │
│ ┌─────▼──────────┐
│ │ ViewportState │ Zoom/pan/fit modes
│ └────────────────┘
│
├──── Scrubber ──── Timeline interaction
│
├──── TimeSlider ── Custom time slider widget + load indicator
│
├──── Shaders ───── OpenGL display shaders
│
└──── Prefs ─────── Settings dialog with TreeView
Module Breakdown
main.rs
Entry point and main application loop. Handles:
- CLI argument parsing
- Window initialization (egui/eframe)
- Event loop and UI rendering
- Keyboard/mouse input routing
- Settings persistence (JSON)
- Global font size application
player.rs
Playback state manager. Controls:
- Play/pause/stop
- Frame navigation (jog, shuttle)
- FPS control with presets
- Loop mode
- Delegates frame access to Cache
cache.rs
Intelligent caching system with multi-threaded architecture:
- LRU eviction: Manages memory budget (default 50% system RAM)
- Epoch counter: Atomic counter for cancelling stale load requests during scrubbing
- Worker pool: 75% of CPU cores for parallel loading
- Load queue: mpsc channel-based task distribution with epoch tagging
- Preload thread: Background spiral loading from current frame
- Sequence management: Multi-sequence playlist support
- Frame status tracking: Provides frame load state for visualization
Caching strategy:
- On-demand loading: Loads frame when accessed
- Spiral preload: Loads frames in order: 0, +1, -1, +2, -2, ...
- Epoch-based cancellation: Workers skip requests with old epoch on scrub/seek
- Memory-aware: Evicts least-recently-used frames when over budget
- Status sync: Updates frame status (Header → Loading → Loaded/Error)
Epoch Counter Pattern:
current_epoch: Arc<AtomicU64>increments on every scrub/seek- Workers check
req.epoch != current_epochand skip stale requests - Prevents wasted work on frames user has already moved past
sequence.rs
Pattern-based frame sequence detection:
- Auto-detects sequences from single file (e.g.,
render.0001.exr→render.*.exr) - Glob pattern matching
- Frame number extraction with padding detection
- Directory scanning for multiple sequences
- Header-only resolution reading (fast)
frame.rs
Individual frame with thread-safe async loading:
- Status states: Placeholder → Header → Loading → Loaded/Error
- Arc<Mutex>: Thread-safe shared ownership
- Format loaders: EXR (OpenEXR), PNG/JPEG/TIFF (image-rs)
- Color conversion: Linear → sRGB for EXR
- Green placeholder: Visible indicator for unloaded frames
- Status API:
frame.status()for load indicator visualization
viewport.rs
Display transformation and interaction:
- Modes: AutoFit (scales to window), Auto100 (1:1 pixels), Manual (user control)
- Zoom: Mouse wheel with cursor-centered scaling
- Pan: Middle-mouse drag
- OpenGL rendering: Custom shader pipeline
scrub.rs
Interactive timeline scrubbing:
- Left-click/drag to navigate frames
- Visual feedback (vertical line + frame number)
- Auto-pauses playback during scrub
- Maps mouse X to frame based on image bounds
- Triggers epoch counter increment for stale request cancellation
timeslider.rs
Custom time slider widget with sequence visualization:
- Color-coded zones: Each sequence rendered with unique color (hash-based)
- Visual dividers: Vertical lines marking sequence boundaries
- Adaptive labels: Sequence names/numbers displayed when space permits
- Load indicator: Colored blocks showing frame status (cached for performance)
- Cache invalidation: Uses
cached_frames_count()to detect when to rebuild - Stateless immediate mode: Fully synchronized with player state
- Interactive: Click/drag to navigate, automatic playhead tracking
- HSV color generation: Stable colors derived from sequence pattern hash
Load Indicator Implementation:
- Queries
cache.get_frame_stats()for all frame statuses - Caches result in
egui::Memorywith version key - Invalidates cache when
cached_frames_count()changes - Draws colored blocks: Dark gray (Placeholder), Blue (Header), Orange (Loading), Green (Loaded), Red (Error)
shaders.rs
OpenGL shader management:
- Built-in shaders (default, checker, etc.)
- Custom shader loading from
shaders/directory - Runtime shader switching
prefs.rs
Settings dialog with TreeView navigation:
- AppSettings struct: Centralizes all user preferences
- SettingsCategory enum: General, UI categories
- TreeView integration: Uses
egui_ltreeviewfor hierarchical navigation - Font size control: Global UI font size (10-18px with live preview)
- Theme toggle: Dark/light mode switching
- Persistence: Selected category and all settings saved to JSON
- Window layout: 700×500 default, resizable with ScrollArea
Data Flow
User Action (drag-drop / file dialog / CLI arg)
│
▼
load_sequence(PathBuf)
│
├──► cache.ingest(paths)
│ │
│ ├──► Sequence::detect() ──► Parse patterns
│ │ Extract frame numbers
│ │ Create Frame objects (status: Header)
│ │
│ └──► append_seq() ──────► Add to cache.sequences
│ Update global frame range
│ Rebuild frame_paths_cache
│
└──► signal_preload() ─────────► Preload thread wakes up
Increments epoch counter
Sends LoadRequests with current epoch
Playback Update Loop
│
▼
player.update()
│
├──► Advance frame based on FPS/direction
│
└──► cache.get_frame(idx)
│
├──► Check LRU cache ───► HIT: update access time, return frame
│
└──► MISS: Send LoadRequest with current epoch
│
▼
Worker threads (75% cores)
│
├──► Check epoch ────► Stale? Skip request
│
├──► frame.load() ─────► Detect format (EXR/PNG/etc)
│ Update status: Loading
│ Load pixels from disk
│ Convert color space
│ Update status: Loaded/Error
│
└──► Send LoadedFrame via channel
│
▼
cache.process_loaded_frames()
│
├──► Ensure space (LRU eviction)
├──► Insert into cache
├──► Update sequence frame reference
└──► Send CacheMessage for UI updates
Scrub/Seek Event
│
▼
├──► Increment epoch counter ────► Cancel all in-flight requests
│
└──► Trigger preload with new epoch
Render Loop
│
▼
UI update
│
├──► Apply global font size from settings
│
├──► Apply theme (dark/light) from settings
│
├──► Get current frame from cache
│
├──► Upload texture to GPU (if frame changed)
│
├──► TimeSlider with load indicator
│ │
│ ├──► Check cached_frames_count()
│ ├──► Rebuild indicator cache if changed
│ └──► Draw colored blocks for each frame
│
└──► ViewportRenderer.render()
│
└──► Apply viewport transform (zoom/pan)
Apply shader
Draw quad with texture
Settings Dialog (F3)
│
▼
├──► TreeView navigation (General / UI)
│
├──► Font size slider ───► Update AppSettings.font_size
│ Apply globally on next frame
│
├──► Dark mode toggle ───► Update AppSettings.dark_mode
│ Switch theme immediately
│
└──► Auto-save to playa.json
Performance Characteristics
- Startup: Instant (lazy loading)
- Sequence detection: Fast (header-only reads, ~1-5ms per file)
- Frame loading: Parallel (75% CPU cores)
- Memory: Self-limiting (50% system RAM, configurable)
- Scrubbing: Responsive (epoch-based cancellation + preloaded cache)
- Playback: Smooth (async loading stays ahead of playback)
- Load indicator: Efficient (cached, O(1) status lookups, rebuilds only on cache changes)
- LRU cache: Optimized (no stale keys in access_order, skips dead entries during eviction)
Configuration
Configuration Files
Playa uses platform-specific configuration directories with flexible override options.
Priority order:
- CLI argument:
--config-dir /custom/path - Environment variable:
PLAYA_CONFIG_DIR=/custom/path - Local folder (backward compatibility): Uses current directory IF any config files already exist
- Platform defaults (new installations):
- Linux:
~/.config/playa/(config),~/.local/share/playa/(data) - macOS:
~/Library/Application Support/playa/ - Windows:
%APPDATA%\playa\
- Linux:
Files:
playa.json- Settings (FPS, theme, viewport, etc.)playa_cache.json- Cache state (sequences, current frame)playa.log- Log file (when--logflag is used)
Examples:
# Use custom directory
# Use environment variable
# Default behavior:
# - Existing users: Uses current directory (if files found)
# - New users: Uses platform-specific location
Settings auto-saved to playa.json:
- FPS
- Loop mode
- Shader selection
- Font size (global UI)
- Dark/light theme
- Viewport state (zoom/pan/mode)
- Playlist (sequence references)
- Window position/size
- Panel widths (playlist)
- Settings dialog state (selected category)
Cache state auto-saved to playa_cache.json for instant restoration on restart.
Technical Stack
- UI: egui 0.33 + eframe
- TreeView: egui_ltreeview 0.6.0 (with persistence feature)
- Graphics: OpenGL via glow + egui_glow
- Image:
- EXR (default): exrs via image 0.25 (pure Rust)
- EXR (optional): openexr 0.11 (C++ bindings,
openexrfeature) - Other formats: image 0.25 (PNG/JPEG/TIFF/TGA/HDR)
- Async: std::thread + crossbeam-channel + mpsc
- Concurrency: AtomicU64 for epoch counter, Arc for shared state
- CLI: clap 4.5
- Logging: env_logger (set
RUST_LOG=debugfor verbose output)
License
See LICENSE file for details.
Contributing
We welcome contributions! Please see our Contributing Guide for details on:
- Commit message conventions (Conventional Commits)
- Development workflow and tools
- Release process
- CI/CD architecture
Need a quick operational overview? Check the Repository Guidelines for a concise map of modules, commands, and review expectations.
See CHANGELOG.md for project history.
Cleaning & CI Utilities
-
cargo xtask wipe(supports-v,--dry-run):- Non-recursively removes executables and shared libraries from
./target,./target/release, and./target/debug. - Also removes packager staging directories:
target/.cargo-packager,target/release/.cargo-packager,target/debug/.cargo-packager. - Does not touch user caches (e.g.,
~/.cargo,%LOCALAPPDATA%).
- Non-recursively removes executables and shared libraries from
-
cargo xtask wipe-wf:- Deletes ALL GitHub Actions workflow runs for this repository using
gh api. - Runs deletions in parallel with a progress bar and prints final stats (deleted/failed).
- Deletes ALL GitHub Actions workflow runs for this repository using