playa 0.1.128

Image sequence player for VFX (EXR, PNG, JPEG, TIFF). Pure Rust with optional OpenEXR support.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
1279
1280
1281
1282
1283
1284
1285
1286
1287
1288
1289
1290
1291
1292
1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
1304
1305
1306
1307
1308
1309
1310
1311
1312
1313
1314
1315
1316
1317
1318
1319
1320
1321
1322
1323
1324
1325
1326
1327
1328
1329
1330
1331
1332
1333
1334
1335
1336
1337
1338
1339
1340
1341
1342
1343
1344
1345
1346
1347
1348
1349
1350
1351
1352
1353
1354
1355
1356
1357
1358
1359
1360
1361
1362
1363
1364
1365
1366
1367
1368
1369
1370
1371
1372
1373
1374
1375
1376
1377
1378
1379
1380
1381
1382
1383
1384
1385
1386
1387
1388
1389
1390
1391
1392
1393
1394
1395
1396
1397
1398
1399
1400
1401
1402
1403
1404
1405
1406
1407
1408
1409
1410
1411
1412
1413
1414
1415
1416
1417
1418
1419
1420
1421
1422
1423
1424
1425
1426
1427
1428
1429
1430
1431
1432
1433
1434
1435
1436
1437
1438
1439
1440
1441
1442
1443
1444
1445
1446
1447
1448
1449
1450
# Playa - Image Sequence Player

[![Release Status](https://github.com/ssoj13/playa/actions/workflows/main.yml/badge.svg?event=push)](https://github.com/ssoj13/playa/actions/workflows/main.yml)
[![Warm Cache Status](https://github.com/ssoj13/playa/actions/workflows/warm-cache.yml/badge.svg?event=push)](https://github.com/ssoj13/playa/actions/workflows/warm-cache.yml)
[![Release](https://img.shields.io/github/v/release/ssoj13/playa)](https://github.com/ssoj13/playa/releases/latest)
[![Downloads](https://img.shields.io/github/downloads/ssoj13/playa/total)](https://github.com/ssoj13/playa/releases)
[![License](https://img.shields.io/github/license/ssoj13/playa)](LICENSE)
[![Lines of Code](https://img.shields.io/endpoint?url=https://ghloc.vercel.app/api/ssoj13/playa/badge?filter=.rs$&style=flat&label=Lines%20of%20Code)](https://github.com/ssoj13/playa)
[![Changelog](https://img.shields.io/badge/changelog-CHANGELOG.md-blue)](CHANGELOG.md)

**Small note**: This is a learning project. I'm really excited to discover the Rust universe and the rise of AI agentic coding techniques to quickly learn a new stack. I perfectly know what I want to build and supposed app architecture, but implementing that alone would be probably not possible within some reasonable timeframe (not within a week, definitely). Well, also now Rust users and open source community now have a half-decent cross-platform image sequence player made of a single binary. I really wanted to express my gratitude towards creators and maintainers of `exrs` and `openexr-rs` crates and of course the rest - Rust is amazing!

Short list of things resolved while building this tool:


![Screenshot](.github/screenshot.png)

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
- **Native Rust Multi-format support**: EXR, PNG, JPEG, TIFF, TGA with fast parallel loading
- **HDR pixel precision**: Support for 8 / 16 / half-float / 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
- **Playback controls**: Standard transport controls (play/pause, JKL shuttle, loop)
- **Viewport controls**: Zoom, pan, fit-to-window, 100% pixel-perfect view, cursor-centered zoom
- **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

## Video Support

Playa now supports video playback alongside image sequences:

**Supported formats**: MP4, MOV, AVI, MKV

**Features**:
- Frame-by-frame video playback with seek support
- Automatic frame count detection
- Cached decoding with worker pool (same as image sequences)
- FFmpeg-based decoding via `playa-ffmpeg` crate

**Usage**:
- Open video file via drag-and-drop or file browser
- Videos appear in playlist with detected frame count
- Scrub timeline to seek through video frames
- All playback controls work identically to image sequences

**Technical details**:
- Videos internally use `@N` suffix notation (e.g., `video.mp4@17` for frame 17)
- Each frame decoded on-demand with YUVโ†’RGBโ†’RGBA conversion
- FFmpeg logging suppressed to avoid console spam
- Playlist serialization preserves video sequences correctly

**Requirements**:
- FFmpeg libraries (auto-detected via vcpkg on Windows)
- `playa-ffmpeg` crate handles all FFmpeg bindings

## Video Encoding

Playa includes built-in video encoding (F7 hotkey) for exporting image sequences and play ranges to video files.

**Features**:
- **F7 hotkey**: Opens encoding dialog with codec/quality settings
- **Play range support**: Encode only selected frames (B/N markers)
- **Hardware acceleration**: NVENC (NVIDIA), QSV (Intel), AMF (AMD)
- **Software codecs**: H.264, H.265, MPEG4
- **Containers**: MP4, MOV
- **Quality modes**: CRF (constant quality) or Bitrate
- **Progress tracking**: Real-time encoding progress with cancel support

**Supported Encoders**:

| Encoder | Type | Platform | Notes |
|---------|------|----------|-------|
| `h264_nvenc` | Hardware | Windows/Linux | NVIDIA GPUs (GTX 600+) |
| `hevc_nvenc` | Hardware | Windows/Linux | NVIDIA GPUs (GTX 900+) |
| `h264_qsv` | Hardware | Windows/Linux | Intel Quick Sync (HD 2000+) |
| `hevc_qsv` | Hardware | Windows/Linux | Intel Quick Sync (Skylake+) |
| `h264_amf` | Hardware | Windows | AMD GPUs |
| `hevc_amf` | Hardware | Windows | AMD GPUs |
| `libx264` | Software | All | CPU-based H.264 |
| `libx265` | Software | All | CPU-based H.265 |
| `mpeg4` | Software | All | Legacy MPEG-4 Part 2 |

**Usage**:
1. Load image sequence or video
2. **(Optional)** Set play range with **B** (begin) and **N** (end) markers
   - Press **B** to mark the start frame
   - Press **N** to mark the end frame
   - Visual indicators appear on the timeline showing the active range
   - Clear markers to encode the entire sequence
3. Press **F7** to open encoding dialog
4. Select codec, quality settings, and output path
5. Click "Encode" - progress shown in real-time with cancel option
6. Output file written to selected location

**Requirements & Limitations**:
- **Resolution consistency**: All frames must have identical width and height
  - Encoder will fail if frame dimensions vary within the sequence
  - Ensure source material has uniform resolution before encoding
- **Play range encoding**: Only frames between B (begin) and N (end) markers are encoded
  - If no markers are set, the entire sequence is encoded
  - Markers are visually indicated on the timeline
  - Frame range is inclusive (both B and N frames are included)

**Technical details**:
- Automatic pixel format conversion (RGB24 โ†’ YUV420P for hardware encoders)
- Uses FFmpeg swscale for color space conversion
- Multi-threaded encoding via background worker thread
- Cancellable operation with atomic flag
- Frame timestamps calculated from sequence frame rate

## Installation

### Recommended: Download Pre-built Installers

**The easiest way** to install Playa - download and run the installer for your platform:

Download the latest release from the [Releases page](https://github.com/ssoj13/playa/releases/latest):

**macOS (recommended: DMG):**
- ๐ŸŽฏ `playa-x.x.x-exrs.dmg` - **Recommended** - Drag to Applications (code-signed & notarized)
- `playa-x.x.x-openexr.dmg` - With DWAA/DWAB compression support (code-signed & notarized)
- Portable: `playa-exrs-aarch64-apple-darwin.zip` (single binary)

**Linux (recommended: AppImage):**
- ๐ŸŽฏ `playa-x.x.x-exrs.AppImage` - **Recommended** - Universal, runs everywhere
- `playa-x.x.x-exrs.deb` - Debian/Ubuntu package
- Portable: `playa-exrs-x86_64-unknown-linux-gnu.zip` (single binary)
- OpenEXR variants: `-openexr.AppImage` / `-openexr.deb` with DWAA/DWAB support

**Windows (choose one):**
- ๐ŸŽฏ `playa-x.x.x-exrs-x64-setup.exe` - **Installer** - System integration
- `playa-x.x.x-exrs-x64.msi` - **MSI** - Enterprise deployments
- `playa-exrs-x86_64-pc-windows-msvc.zip` - **Portable** - Single .exe (no DLLs)
- OpenEXR variants: `-openexr-` prefix - Include DLLs for DWAA/DWAB compression

**macOS Security Note:**
All DMG releases are code-signed with Developer ID and notarized by Apple. No Gatekeeper warnings - just drag to Applications and run.

---

### Alternative: cargo install

Install from crates.io (requires manual FFmpeg setup):

```bash
cargo install playa
```

**โš ๏ธ Requirements:**

1. **vcpkg** must be installed and configured:
   ```bash
   # Windows
   git clone https://github.com/microsoft/vcpkg.git C:\vcpkg
   C:\vcpkg\bootstrap-vcpkg.bat
   setx VCPKG_ROOT "C:\vcpkg"
   setx VCPKGRS_TRIPLET "x64-windows-static-md-release"

   # Linux/macOS
   git clone https://github.com/microsoft/vcpkg.git /usr/local/share/vcpkg
   /usr/local/share/vcpkg/bootstrap-vcpkg.sh
   export VCPKG_ROOT=/usr/local/share/vcpkg
   ```

2. **FFmpeg** with static linking:
   ```bash
   # Windows
   vcpkg install ffmpeg[core,avcodec,avdevice,avfilter,avformat,swresample,swscale,nvcodec]:x64-windows-static-md-release

   # Linux
   export VCPKGRS_TRIPLET=x64-linux-release
   vcpkg install ffmpeg[...]:x64-linux-release

   # macOS
   export VCPKGRS_TRIPLET=arm64-osx-release  # or x64-osx-release for Intel
   vcpkg install ffmpeg[...]:arm64-osx-release
   ```

**See "FFmpeg Setup" section below for complete instructions.**

---

### Build from Source (Development)

**For most users:** Use pre-built installers above or `cargo install`.

**For developers:** Use bootstrap scripts that automatically handle all dependencies and environment setup.

#### Quick Start with Bootstrap

Bootstrap scripts provide the easiest build experience with automatic dependency management:

```bash
# Clone repository
git clone https://github.com/ssoj13/playa.git
cd playa

# Windows
bootstrap.cmd build          # Build with exrs (fast, pure Rust)
bootstrap.cmd build --openexr  # Build with OpenEXR (full DWAA/DWAB support)

# Linux/macOS
./bootstrap.sh build
./bootstrap.sh build --openexr
```

**What bootstrap does:**

1. **Checks Rust installation** - Exits with error if missing
2. **Sets up vcpkg environment variables** automatically:
   - `VCPKG_ROOT` - Points to vcpkg installation
   - `VCPKGRS_TRIPLET` - Platform-specific triplet (e.g., `x64-windows-static-md-release`)
   - `PKG_CONFIG_PATH` - For FFmpeg pkg-config files (Linux/macOS)
3. **Installs dev tools** via cargo-binstall:
   - `cargo-release` - Version bumping and changelog
   - `cargo-packager` - Cross-platform installer generation
4. **Builds xtask** - Project build automation helper
5. **Forwards to xtask** - Handles actual compilation with correct configuration

**Benefits over manual cargo build:**
- โœ… Guaranteed correct FFmpeg linking configuration
- โœ… Same setup as CI/CD builds
- โœ… No manual environment variable setup
- โœ… Handles platform-specific triplets automatically
- โœ… Works identically on Windows, Linux, and macOS

**After bootstrap:** Continue using `bootstrap.{sh|cmd}` or use `cargo xtask` directly.

#### EXR Backend Options

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:

```bash
git clone https://github.com/ssoj13/playa.git
cd playa

# Build with exrs backend (pure Rust, no DLLs)
cargo build --release
```

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.85+ (edition 2024)
- C++ compiler and CMake

```bash
git clone https://github.com/ssoj13/playa.git
cd playa

# Build with OpenEXR backend (full format support)
cargo xtask build --release --openexr
```

**Note:** OpenEXR backend compiles C++ libraries (~5-10 minutes first build, then cached).

### FFmpeg Setup (Video Playback & Encoding)

Playa requires FFmpeg libraries for video support. Install via vcpkg for best compatibility:

#### Windows

```powershell
# Install vcpkg (if not already installed)
git clone https://github.com/microsoft/vcpkg.git C:\vcpkg
C:\vcpkg\bootstrap-vcpkg.bat

# Set environment variables (required for Rust to find FFmpeg)
# Add these permanently to your system environment variables:
setx VCPKG_ROOT "C:\vcpkg"
setx VCPKGRS_TRIPLET "x64-windows-static-md-release"

# Install FFmpeg with static linking and hardware acceleration support
C:\vcpkg\vcpkg install ffmpeg[core,avcodec,avdevice,avfilter,avformat,swresample,swscale,nvcodec]:x64-windows-static-md-release
```

**Important:** The `VCPKGRS_TRIPLET` environment variable tells Rust's vcpkg integration which triplet to use. The `x64-windows-static-md-release` triplet provides static library linkage with optimized release builds, creating self-contained binaries without requiring FFmpeg DLLs at runtime.

**Features explained**:
- `core,avcodec,avformat,swscale,swresample` - Core libraries (required)
- `avdevice,avfilter` - Device input and filtering support
- `nvcodec` - NVIDIA NVENC hardware encoding (GTX 600+)

**Setup Visual Studio environment** (before building):
```cmd
"C:\Program Files\Microsoft Visual Studio\2022\Community\VC\Auxiliary\Build\vcvars64.bat"
```

#### Linux

```bash
# Install vcpkg
git clone https://github.com/microsoft/vcpkg.git /usr/local/share/vcpkg
/usr/local/share/vcpkg/bootstrap-vcpkg.sh

# Set environment variables
export VCPKG_ROOT=/usr/local/share/vcpkg
export VCPKGRS_TRIPLET=x64-linux-release
export PKG_CONFIG_PATH=$VCPKG_ROOT/installed/$VCPKGRS_TRIPLET/lib/pkgconfig

# Install FFmpeg with hardware encoder support
vcpkg install ffmpeg[core,avcodec,avdevice,avfilter,avformat,swresample,swscale,nvcodec]:x64-linux-release
```

**Hardware encoders on Linux**:
- `nvcodec` - NVIDIA NVENC (requires CUDA drivers)

**Alternative: System FFmpeg**
```bash
# Ubuntu/Debian
sudo apt install libavcodec-dev libavformat-dev libavutil-dev libswscale-dev

# Fedora
sudo dnf install ffmpeg-devel

# Arch
sudo pacman -S ffmpeg
```

#### macOS

```bash
# Install vcpkg
git clone https://github.com/microsoft/vcpkg.git /usr/local/share/vcpkg
/usr/local/share/vcpkg/bootstrap-vcpkg.sh

# Set environment variables (automatically detected by bootstrap.sh)
export VCPKG_ROOT=/usr/local/share/vcpkg

# M1/M2 Macs
export VCPKGRS_TRIPLET=arm64-osx-release
export PKG_CONFIG_PATH=$VCPKG_ROOT/installed/arm64-osx-release/lib/pkgconfig
vcpkg install ffmpeg[core,avcodec,avdevice,avfilter,avformat,swresample,swscale]:arm64-osx-release

# Intel Macs
export VCPKGRS_TRIPLET=x64-osx-release
export PKG_CONFIG_PATH=$VCPKG_ROOT/installed/x64-osx-release/lib/pkgconfig
vcpkg install ffmpeg[core,avcodec,avdevice,avfilter,avformat,swresample,swscale]:x64-osx-release
```

**Alternative: Homebrew**
```bash
brew install ffmpeg
```

**Note**: macOS hardware encoding (VideoToolbox) requires system FFmpeg or manual FFmpeg build with `--enable-videotoolbox`.

### Verifying FFmpeg Installation

```bash
# Check FFmpeg availability
pkg-config --modversion libavcodec libavformat libavutil libswscale

# List available encoders (after building playa)
ffmpeg -encoders | grep -E "(nvenc|qsv|amf|264|265)"

# Test encoding (requires playa built)
bootstrap.cmd test    # Windows
./bootstrap.sh test   # Linux/macOS
```

### CI/CD Runner Requirements

GitHub Actions runners need FFmpeg for video support:

**Windows runners**:
- Install vcpkg during workflow
- Cache: `~\vcpkg` and `~\AppData\Local\vcpkg`
- Required features: `ffmpeg[core,avcodec,avformat,avutil,swscale,nvcodec,qsv,vpl]:x64-windows-static-md`

**Linux runners**:
- Install vcpkg during workflow OR use system FFmpeg
- Cache: `~/vcpkg`
- Required features: `ffmpeg[core,avcodec,avformat,avutil,swscale,nvcodec]:x64-linux`

**macOS runners**:
- Use Homebrew FFmpeg (faster than vcpkg)
- Cache: Homebrew bottles
- Command: `brew install ffmpeg`

See `.github/workflows/warm-cache.yml` for reference implementation.

## Quick Start (New Contributors)

**Start here!** Bootstrap scripts handle all dependencies automatically:

### Windows
```cmd
bootstrap.cmd              # Show xtask help
bootstrap.cmd build        # Build with exrs (fast)
bootstrap.cmd build --openexr  # Build with full OpenEXR support
bootstrap.cmd test         # Run encoding integration test
```

### Linux/macOS
```bash
./bootstrap.sh             # Show xtask help
./bootstrap.sh build       # Build with exrs (fast)
./bootstrap.sh build --openexr  # Build with full OpenEXR support
./bootstrap.sh test        # Run encoding integration test
```

**What bootstrap does:**
1. Checks Rust installation (exits with error if missing)
2. Auto-installs dependencies via `cargo-binstall` (faster than `cargo install`):
   - `cargo-release` - Version bumping and changelog generation
   - `cargo-packager` v0.11.7 - Cross-platform installer generation
3. Builds `xtask` binary (project build automation)
4. Forwards all arguments to `cargo xtask`

**After bootstrap:** Use `cargo xtask <command>` directly or continue with `bootstrap.{sh|cmd} <command>`

### Using xtask - Project Build Automation

**Prerequisites:** Run `bootstrap.{sh|cmd}` first (see Quick Start above)

`xtask` is an idiomatic Rust pattern for build automation - a workspace helper binary providing 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
- **Type-safe**: Catch errors at compile time, not runtime
- **Self-documenting**: Built-in `--help` with structured command definitions
- **Pure Rust**: Uses project's existing toolchain, no external tools needed

#### Available Commands

##### ๐Ÿ—๏ธ Build & Development
```bash
cargo xtask build [--release] [--openexr]  # Full build (default: exrs)
cargo xtask post [--release]               # Copy native libraries (OpenEXR only)
cargo xtask verify [--release]             # Verify dependencies present
cargo xtask deploy [--install-dir PATH]    # Install to system
  # Windows: %LOCALAPPDATA%\Programs\playa
  # Linux/macOS: ~/.local/bin/playa
```

##### ๐Ÿงช Testing
```bash
bootstrap.cmd test     # Windows: Run encoding integration test
./bootstrap.sh test    # Linux/macOS: Run encoding integration test
```

**What `bootstrap test` does:**
- Runs `cargo test --release test_encode_placeholder_frames -- --nocapture`
- Creates 100 placeholder frames (640x480, green color)
- Sets play range to frames 10-49 (40 frames)
- Detects available encoder (NVENC/libx264/mpeg4)
- Encodes to `test_encode_output.mp4` in current directory
- Verifies RGB24โ†’YUV420P conversion for hardware encoders
- Shows encoder type, output path, file size, and frame count

**Example output:**
```
๐ŸŽฌ Using NVENC hardware encoder
Play range set: 10..49 (40 frames)
Encoding frames 10..49 to: C:\projects\playa\test_encode_output.mp4
โœ“ Encoding test passed!
  Encoder: h264_nvenc
  Output: C:\projects\playa\test_encode_output.mp4
  Size: 2817 bytes (2.75 KB)
  Frames: 40/40 (play range: 10..49)
```

**Test file location:** `./test_encode_output.mp4` (in project root)

**Additional video tests:**
```bash
# List all video-related tests
cargo test --release -- --list | grep video

# Run specific video test
cargo test --release test_video_decoder_basic -- --nocapture
```

##### ๐Ÿงน Maintenance
```bash
cargo xtask wipe [-v] [--dry-run]    # Remove executables/libs from ./target
cargo xtask wipe-wf                  # Delete ALL GitHub Actions runs (parallel)
```

##### ๐Ÿš€ Release Management
```bash
cargo xtask tag-dev [patch|minor|major]  # Create v0.1.x-dev tag โ†’ trigger Build workflow
cargo xtask tag-rel [patch|minor|major]  # Create v0.1.x tag โ†’ trigger Release workflow
cargo xtask pr [version]                 # Create PR: dev โ†’ main with all commits
cargo xtask changelog                    # Preview unreleased CHANGELOG.md
```

##### ๐Ÿ”ง Platform-Specific
```bash
cargo xtask pre   # Linux only: Patch OpenEXR headers for GCC 11+ compatibility
```

#### What `cargo xtask build` Does

**Without `--openexr` (default - exrs backend):**
1. Runs `cargo build [--release]` with pure Rust exrs backend
2. Self-contained single binary (no dependencies copied)

**With `--openexr` (OpenEXR C++ backend):**
1. **Linux**: Patches OpenEXR headers for GCC 11+ compatibility
2. **All platforms**: Runs `cargo build [--release] --features openexr`
3. **All platforms**: Copies native libraries (OpenEXR, Imath, zlib, openexr-c) to target directory
4. **All platforms**: Copies shaders from project root
5. **Linux**: Creates necessary symlinks for library loading

#### Common Workflows

**Local development (fast):**
```bash
./bootstrap.sh build        # or cargo xtask build
./target/debug/playa
```

**Local development (full OpenEXR):**
```bash
./bootstrap.sh build --openexr
./target/debug/playa
```

**Install to system:**
```bash
cargo xtask build --release --openexr
cargo xtask deploy
# Now available as: playa
```

**Release workflow:**
```bash
# 1. Create PR from dev to main
cargo xtask pr v0.2.0

# 2. Merge PR on GitHub

# 3. Tag release on main
git checkout main && git pull
cargo xtask tag-rel patch

# 4. GitHub Actions builds installers and creates Release
```

## CI/CD Workflows

### Complete Workflow

**1. Development on main branch:**
- Commits to `main` โ†’ push triggers `warm-cache.yml`
- `warm-cache.yml` checks cache age (threshold: 12 hours)
- If cache is stale/missing โ†’ warms cache for all platforms (Windows, Linux, macOS)
- Cache is saved under `refs/heads/main`

**2. Creating a release:**
- Create git tag: `git tag v0.1.109` โ†’ `git push origin v0.1.109`
- Triggers `release.yml` โ†’ verifies tag is on `main` branch
- Runs builds for all platforms via `_build-platform.yml`
- **Cache is read from main** (automatic fallback via `actions/cache@v4`)
- For macOS: imports Developer ID certificate, signs `.app`
- Builds installers: `.msi` (Windows), `.deb`/`.AppImage` (Linux), `.dmg`/`.app.tar.gz` (macOS)
- Creates GitHub Release with artifacts

**3. Manual cache warming:**
- Actions โ†’ Warm Cache โ†’ Run workflow
- Choose backends: `openexr`, `exrs`, or `both`

**Cache strategy:**
- Cache is created **only on main**
- Tags **read** cache from main (don't create their own)
- No duplication, no isolation between tags

**macOS code signing:**
- Certificate: Developer ID Application (stored in GitHub Secrets)
- Workflow imports into temporary keychain
- `cargo-packager` uses `signing-identity` from `Cargo.toml`
- Verification: logs show `โœ… App is signed with Developer ID`

### Technical Details

**Release Workflow:**
- Trigger: pushing a tag matching `v*` or manual run
- Behavior:
  - If tag points to commit on `main` โ†’ release path (publishes GitHub Release)
  - If tag not on `main` โ†’ dev path (builds artifacts without publishing)
- Manual run supports `build_type: auto | release | dev`

**Warm Cache Workflow:**
- Trigger: push to `main` or manual dispatch
- Gate: only executes automatically from `main` branch
- Cooldown: skips if successful run happened within last 12 hours
- Manual run ignores cooldown and always executes
- Backends: `openexr` (default), `exrs`, or `both`

**macOS Packaging:**
- Pre-packaging cleanup: detaches stale `/Volumes/Playa` mount, removes leftover `*.dmg`
- Retries up to 3 times with short delay to avoid `hdiutil: create failed - Resource busy`

**Permissions:**
- Unified workflow configured with `contents: write` for publishing releases

### Static FFmpeg Linking Strategy

All CI builds use static FFmpeg linking via custom vcpkg triplets for portable, self-contained binaries:

**Platform-specific triplets:**

| Platform | Triplet | Configuration | Benefits |
|----------|---------|---------------|----------|
| **Windows** | `x64-windows-static-md-release` | Static libraries + dynamic CRT | No FFmpeg DLLs required, smaller installer |
| **macOS** | `arm64-osx-release` / `x64-osx-release` | Static FFmpeg | Universal binary support, portable `.app` |
| **Linux** | `x64-linux-release` | Static FFmpeg where possible | Reduces runtime dependencies |

**Key advantages:**
- **Portability**: Binaries work without installing FFmpeg separately
- **Version consistency**: Bundled FFmpeg version guaranteed to work
- **Reduced installer size**: No need to package separate FFmpeg DLLs
- **Faster CI builds**: vcpkg FFmpeg cache reduces build time from ~20 minutes to ~30 seconds

**Technical implementation:**
1. Custom vcpkg triplets are created **before** cache check
2. FFmpeg is installed with triplet-specific configuration
3. `VCPKGRS_TRIPLET` environment variable guides Rust's vcpkg integration
4. Cache includes FFmpeg binaries, headers, and pkg-config files
5. Subsequent builds reuse cached FFmpeg (cache key includes triplet name)

**Cache optimization:**
- **Cache paths**: `vcpkg/installed`, `vcpkg/buildtrees`, `vcpkg/downloads`, `vcpkg/packages`
- **Cache keys**: Include OS, triplet, and FFmpeg feature set
- **Hit rate**: ~95% on subsequent builds (assuming no dependency updates)
- **Storage**: ~500MB per platform (compressed)

### Cargo Features

Playa uses Cargo features to provide flexible EXR backend selection:

| Feature | Default | Description | Use Case |
|---------|---------|-------------|----------|
| (none) | โœ… Yes | Pure Rust `exrs` backend | Fast builds, no external dependencies |
| `openexr` | โŒ No | C++ OpenEXR backend via `openexr-rs` | Full DWAA/DWAB compression support |

**Build commands:**
```bash
# Default (exrs backend)
cargo build --release

# OpenEXR backend (full compression support)
cargo build --release --features openexr

# Using xtask (handles dependencies automatically)
cargo xtask build              # exrs backend
cargo xtask build --openexr    # OpenEXR backend
```

**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 creation
- `cargo-packager` - Cross-platform installer generation (v0.11.7)

**Standard Rust tools (usually pre-installed):**
- `rustup` - Rust toolchain manager
- `cargo` - Rust package manager
- `clippy` - Linter (`rustup component add clippy`)
- `rustfmt` - Code formatter (`rustup component add rustfmt`)

**Required for PR workflow:**
- `gh` - GitHub CLI (used by `cargo xtask pr`) - [Installation](https://cli.github.com/)

**Optional tools:**
- `git-cliff` - Changelog generation (used by `cargo xtask changelog`)
- `cargo-audit` - Security vulnerability scanning
- `cargo-llvm-cov` - Code coverage

### Standard Rust Development

```bash
# Testing
cargo test                           # Run all unit tests
cargo test --release                 # Run tests in release mode

# Documentation
cargo doc --open                     # Generate and open rustdoc documentation
cargo doc --no-deps --open           # Only document this crate

# Code quality
cargo clippy                         # Run linter
cargo clippy -- -D warnings          # Treat warnings as errors
cargo fmt                            # Format code
cargo fmt -- --check                 # Check formatting without modifying

# Build variants
cargo build                          # Debug build
cargo build --release                # Release build (optimized)
cargo clean                          # Clean build artifacts
```

#### 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.h`
- `ImfDeepTiledInputFile.h`
- `ImfDeepTiledInputPart.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`):**

1. **Locate libraries** compiled by `openexr-sys`:
   - Searches `target/release/build/openexr-sys-*/out/` for versioned `.so` files
   - Example: `libOpenEXR-3_2.so.31.0.0`, `libImath-3_1.so.29.9.0`

2. **Copy to target directory**:
   - Destination: `target/release/` (next to `playa` binary)
   - Preserves original versioned filenames

3. **Create SONAME symlinks**:
   - `libOpenEXR-3_2.so -> libOpenEXR-3_2.so.31.0.0`
   - `libOpenEXRCore-3_2.so -> libOpenEXRCore-3_2.so.31.0.0`
   - `libOpenEXRUtil-3_2.so -> libOpenEXRUtil-3_2.so.31.0.0`
   - `libImath-3_1.so -> libImath-3_1.so.29.9.0`
   - Plus OpenEXR-C wrapper lib

**Why this is needed:**
- `openexr-sys` build creates libraries with full SONAME versions
- Rust linker expects generic `.so` names 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:
```toml
[target.x86_64-unknown-linux-gnu]
rustflags = ["-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":
```bash
cargo xtask pre
cargo build --release
```

Libraries not found when running:
```bash
cargo xtask verify --release
cargo xtask post --release  # If missing
```

After `cargo clean`:
```bash
cargo xtask build --release  # Re-patches automatically
```

#### 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`):**

1. **Locate DLLs** compiled by `openexr-sys`:
   - Searches `target/release/build/openexr-sys-*/out/bin/` for `.dll` files
   - Example: `OpenEXR-3_2.dll`, `Imath-3_1.dll`, `zlib.dll`

2. **Copy to target directory**:
   - Destination: `target/release/` (next to `playa.exe`)
   - Windows DLLs don't use versioned SONAME - simpler than Linux

**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`)

## macOS Code Signing & Notarization

### For Users

All macOS DMG releases are **code-signed** with Developer ID and **notarized** by Apple:
- โœ… No Gatekeeper warnings
- โœ… No "unidentified developer" dialogs
- โœ… Double-click DMG โ†’ drag to Applications โ†’ works immediately

### For Maintainers: CI/CD Setup

**How it works in CI (`_build-backend.yml`):**

**1. Certificate Import**
- Decodes `APPLE_CERTIFICATE` secret (base64 .p12 file)
- Creates temporary keychain
- Imports Developer ID Application certificate
- Unlocks keychain for build process

**2. Signing** (automatic via `cargo-packager`)
- Reads `signing-identity` from `Cargo.toml`:
  ```toml
  [package.metadata.packager.macos]
  signing-identity = "Developer ID Application: Name (TEAM_ID)"
  ```
- Signs all executables and frameworks in `.app` bundle
- Verifies signature with `codesign -dv`

**3. Notarization** (automatic via `cargo-packager`)
- Requires environment variables:
  - `APPLE_ID` - Apple ID email
  - `APPLE_PASSWORD` - App-specific password (NOT iCloud password!)
  - `APPLE_TEAM_ID` - Team ID from Developer Portal
- Submits signed `.app` to Apple notarization service
- Waits for approval (~1-5 minutes)
- Staples notarization ticket to DMG

**4. Verification Logs Show:**
```
โœ… Certificate imported: Developer ID Application: Name (TEAM_ID)
โœ… App signed successfully
โœ… Notarization submitted (request ID: ...)
โœ… Notarization approved
โœ… Ticket stapled to DMG
```

**Setting Up Secrets (One-Time):**

Run helper script:
```bash
./apple_cert.sh  # Exports certificate and uploads to GitHub Secrets
```

Or manually:
```bash
gh secret set APPLE_CERTIFICATE          # Base64 .p12 file
gh secret set APPLE_CERTIFICATE_PASSWORD # Certificate password
gh secret set APPLE_ID                   # your-email@example.com
gh secret set APPLE_PASSWORD             # App-specific password (NOT iCloud!)
gh secret set APPLE_TEAM_ID              # Y8PQ7YASU9
```

**Certificate Details:**
- Type: "Developer ID Application" (NOT "Apple Development")
- Source: [Apple Developer Portal](https://developer.apple.com/account/resources/certificates/list)
- App-specific password: https://appleid.apple.com โ†’ Security โ†’ App-Specific Passwords

**Workflow Skip Behavior:**
- If `APPLE_CERTIFICATE` secret is empty โ†’ adhoc signature (for testing)
- If any notarization secret missing โ†’ builds but skips notarization

## Configuration

### Configuration Files

Playa uses platform-specific configuration directories with flexible override options.

**Priority order:**
1. **CLI argument**: `--config-dir /custom/path`
2. **Environment variable**: `PLAYA_CONFIG_DIR=/custom/path`
3. **Local folder** (backward compatibility): Uses current directory IF any config files already exist
4. **Platform defaults** (new installations):
   - **Linux**: `~/.config/playa/` (config), `~/.local/share/playa/` (data)
   - **macOS**: `~/Library/Application Support/playa/`
   - **Windows**: `%APPDATA%\playa\`

**Files:**
- `playa.json` - Settings (FPS, theme, viewport, etc.)
- `playa_cache.json` - Cache state (sequences, current frame)
- `playa.log` - Log file (when `--log` flag is used)

**Examples:**
```bash
# Use custom directory
playa --config-dir ~/.playa

# Use environment variable
export PLAYA_CONFIG_DIR=~/my-playa-config
playa

# Default behavior:
# - Existing users: Uses current directory (if files found)
# - New users: Uses platform-specific location
playa
```

**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.

## Usage

### Launch

#### Basic Usage
```bash
# Start with empty player (drag-and-drop or file dialog)
playa

# Load specific file or sequence
playa path/to/image.0001.exr

# Load multiple files (detects sequences automatically)
playa file1.exr file2.exr
```

#### Command-Line Arguments

**File Loading:**
```bash
# Load single file (positional argument)
playa image.0001.exr

# Load multiple files (detects sequences for each)
playa -f seq1.0001.exr -f seq2.0001.exr

# Load saved playlist
playa -p playlist.json

# Combine files and playlist (loaded in command-line order)
playa image.exr -f seq1.exr -f seq2.exr -p playlist.json
```

**Playback Control:**
```bash
# Start in fullscreen (cinema mode)
playa -F image.exr

# Set starting frame (0-based)
playa --frame 100 image.exr

# Auto-start playback
playa -a image.exr

# Disable looping
playa -o 0 image.exr

# Set play range (work area)
playa --start 10 --end 50 image.exr
playa --range 10 50 image.exr        # Shorthand
```

**Configuration:**
```bash
# Use custom config directory
playa --config-dir ~/.playa

# Override memory budget (percentage of system RAM)
playa --mem 75

# Set worker thread count
playa --workers 8
```

**Logging:**
```bash
# Enable file logging (default: playa.log)
playa --log

# Log to custom file
playa --log custom.log

# Increase verbosity (default: warn)
playa -v              # Info level
playa -vv             # Debug level
playa -vvv            # Trace level (maximum detail)
```

**Full Example:**
```bash
# Load sequence, start at frame 50, auto-play in fullscreen with debug logging
playa -f render.0001.exr --frame 50 -a -F --range 0 100 -vv --log
```

**Help:**
```bash
# Show all available options
playa --help

# Show version
playa --version
```

**Note:** When starting without any arguments, help text is automatically printed to console before launching the GUI.

### Keyboard Shortcuts

**Playback Controls:**
- `Space` / `K` / `โ†‘` - Play/Pause (unified control)
- `J` / `,` / `โ†` - Jog backward (starts playback, increases speed if already playing)
- `L` / `.` / `โ†’` - Jog forward (starts playback, increases speed if already playing)
- `โ†“` - Decrease play speed (only when playing)
- `1` / `Home` - Jump to start
- `2` / `End` - Jump to end
- `Ctrl+โ†` - Jump to start
- `Ctrl+โ†’` - Jump to end
- `[` - Jump to previous sequence start
- `]` - Jump to next sequence start
- `'` / `` ` `` - Toggle loop

**FPS Control:**
- `-` - Decrease base FPS (persistent setting)
- `=` / `+` - Increase base FPS (persistent setting)
- Base FPS steps through presets: 1, 2, 4, 8, 12, 24, 30, 60, 120, 240
- Play speed (J/L) resets to base FPS on stop

**Viewport:**
- `F` - Fit to window (auto-fit mode)
- `A` / `H` - 100% zoom
- `Mouse Wheel` - Zoom in/out (center on cursor)
- `Middle Mouse Drag` - Pan
- `Left Click + Drag` - Scrub timeline

**Play Range (Work Area):**
- `B` - Set play range start (begin marker)
- `N` - Set play range end (end marker)
- `Ctrl+B` - Reset play range to full sequence
- Used for:
  - Loop playback within selected range
  - Encoding only selected frames (F7)
  - Timeline highlighting

**UI:**
- `F1` - Toggle help overlay
- `F2` - Toggle playlist panel
- `F3` - Toggle settings dialog
- `F7` - Open video encoding dialog
- `Z` - Toggle fullscreen (cinema mode)
- `ESC` - Exit fullscreen / Quit
- `Q` - Quit
- `Ctrl+R` - Reset settings to default
- `Backspace` - Toggle frame numbers on timeline (shows global range, sequence starts, play range)

### 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:**
1. On-demand loading: Loads frame when accessed
2. Spiral preload: Loads frames in order: 0, +1, -1, +2, -2, ...
3. Epoch-based cancellation: Workers skip requests with old epoch on scrub/seek
4. Memory-aware: Evicts least-recently-used frames when over budget
5. 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_epoch` and 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<FrameData>>**: 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::Memory` with 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_ltreeview` for 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)

## 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, `openexr` feature)
  - **Other formats**: image 0.25 (PNG/JPEG/TIFF/TGA/HDR)
- **Async**: std::thread + crossbeam-channel + mpsc
- **Concurrency**: AtomicU64 for epoch counter, Arc<Mutex> for shared state
- **CLI**: clap 4.5
- **Logging**: env_logger (set `RUST_LOG=debug` for verbose output)

## AI Dev experiment:

This project heavily relies on AI agents: Claude Code and Codex.
Without them development time could span months instead of a single week (still, pretty intensive).

**Human-designed architecture:**
- System design and component boundaries
- Performance targets and trade-offs
- UX workflows and user experience
- Security model and threat boundaries
- Release strategy and versioning

**AI-implemented components:**
- โœ… Build automation (`xtask` workspace - 11 commands, cross-platform)
- โœ… CI/CD workflows (cache warming API, branch detection, unified release)
- โœ… Bootstrap scripts (dependency management, error handling)
- โœ… Installer packaging (NSIS, MSI, DMG, DEB, AppImage)
- โœ… Apple signing pipeline (Developer ID, notarization, keychain management)
- โœ… Documentation (architecture diagrams, data flow, comprehensive README)

**Reality check:** AI agents make plenty of mistakes - wrong API usage, platform-specific bugs, over-engineered solutions. Human catches these through testing and directs corrections. Iteration is fast because agents are like instant encyclopaedia.

### What Works Well

**Speed:** Implement in minutes what would take days manually  
**Breadth:** Cross-platform knowledge (Windows/Linux/macOS quirks) instantly available  
**Consistency:** Code style, documentation, commit messages uniform across project  
**Tirelessness:** Agents iterate without frustration, test edge cases without boredom  

### What's not

**Logic:** "AI" is a great trickster.  
It can execute the task perfectly to your description, working completely incorrect and/or unexpected way.


## Contributing

I'm not looking for contributors, but if you think you can add some useful feature - be my guest.
Fork it, clone it, improve it, PR if you want.
Here's the [Contributing Guide](CONTRIBUTING.md) for details on:
- Commit message conventions (Conventional Commits)
- Development workflow and tools
- Release process
- CI/CD architecture


## Acknowledgements
Cool Halloween Cat app icon is taken from this cute [Flaticon icon pack by Yasashii std](http://flaticon.com/packs/halloween-18020037)  

See [CHANGELOG.md](CHANGELOG.md) for project history.