235 Commits

Author SHA1 Message Date
4096e9c26a reload has a bounded number of shader compilers
Some checks failed
CI / React UI Build (push) Successful in 11s
CI / Native Windows Build And Tests (push) Failing after 2m10s
CI / Windows Release Package (push) Has been skipped
2026-05-21 17:33:19 +10:00
f9aac85e5f reload no longer can disrupt the renderer 2026-05-21 17:30:09 +10:00
5cf1a09e75 Texture composition for text no longer on the render thread
Some checks failed
CI / React UI Build (push) Successful in 10s
CI / Native Windows Build And Tests (push) Failing after 2m9s
CI / Windows Release Package (push) Has been skipped
2026-05-21 17:25:28 +10:00
3fc78d5bb8 Docs update
Some checks failed
CI / React UI Build (push) Successful in 11s
CI / Native Windows Build And Tests (push) Failing after 2m11s
CI / Windows Release Package (push) Has been skipped
2026-05-21 17:14:29 +10:00
5c46eaf18a More changes
Some checks failed
CI / React UI Build (push) Successful in 11s
CI / Native Windows Build And Tests (push) Failing after 2m12s
CI / Windows Release Package (push) Has been skipped
2026-05-21 16:51:00 +10:00
d68cf9b1a0 Update RenderCadenceCompositorRuntimeLayerModelTests.cpp 2026-05-21 16:16:06 +10:00
bda9a9dc22 runtime read of json layer state 2026-05-21 16:08:46 +10:00
c2de2c3738 Hot reload
Some checks failed
CI / React UI Build (push) Successful in 11s
CI / Native Windows Build And Tests (push) Failing after 2m3s
CI / Windows Release Package (push) Has been skipped
2026-05-21 15:58:23 +10:00
dc247ab58d Golden rules change
Some checks failed
CI / React UI Build (push) Successful in 10s
CI / Native Windows Build And Tests (push) Failing after 2m6s
CI / Windows Release Package (push) Has been skipped
2026-05-21 15:45:13 +10:00
df0a77ef01 Text rendering
Some checks failed
CI / React UI Build (push) Successful in 40s
CI / Native Windows Build And Tests (push) Failing after 2m28s
CI / Windows Release Package (push) Has been skipped
2026-05-21 15:39:37 +10:00
09efe2d6a0 Changed defaults 2026-05-21 15:08:36 +10:00
a9eeed30cf Text fix 2026-05-20 17:04:03 +10:00
e43ac21b2f INtial text again 2026-05-20 16:26:36 +10:00
081364e764 Font builder
Some checks failed
CI / React UI Build (push) Successful in 10s
CI / Native Windows Build And Tests (push) Failing after 2m10s
CI / Windows Release Package (push) Has been skipped
2026-05-20 15:49:29 +10:00
f589b1e1fe preview window flipped
All checks were successful
CI / React UI Build (push) Successful in 11s
CI / Native Windows Build And Tests (push) Successful in 2m3s
CI / Windows Release Package (push) Has been skipped
2026-05-20 15:03:02 +10:00
7e17315e74 Preview windows changes 2026-05-20 14:47:45 +10:00
bfaa3f5e0e optional preview frame
All checks were successful
CI / React UI Build (push) Successful in 11s
CI / Native Windows Build And Tests (push) Successful in 2m23s
CI / Windows Release Package (push) Has been skipped
2026-05-20 14:37:24 +10:00
1d4eb7a34c Folder fixes
All checks were successful
CI / React UI Build (push) Successful in 37s
CI / Native Windows Build And Tests (push) Successful in 2m18s
CI / Windows Release Package (push) Successful in 2m0s
2026-05-18 14:57:25 +10:00
f461a05c65 Clean up
Some checks failed
CI / Native Windows Build And Tests (push) Failing after 16s
CI / React UI Build (push) Successful in 38s
CI / Windows Release Package (push) Has been skipped
2026-05-18 14:19:29 +10:00
Aiden
3ffb562ff7 docs update
All checks were successful
CI / React UI Build (push) Successful in 11s
CI / Native Windows Build And Tests (push) Successful in 3m14s
CI / Windows Release Package (push) Successful in 3m11s
2026-05-13 01:06:20 +10:00
Aiden
c2d548499c Timing is finally good
All checks were successful
CI / React UI Build (push) Successful in 11s
CI / Native Windows Build And Tests (push) Successful in 2m57s
CI / Windows Release Package (push) Has been skipped
2026-05-13 00:58:32 +10:00
Aiden
6a0340d1b4 proactive realignment 2026-05-13 00:28:11 +10:00
Aiden
5c1fc2a6cf telemetry and timing updates
All checks were successful
CI / React UI Build (push) Successful in 10s
CI / Native Windows Build And Tests (push) Successful in 2m58s
CI / Windows Release Package (push) Has been skipped
2026-05-13 00:21:28 +10:00
Aiden
d411453f80 timing refactor
All checks were successful
CI / React UI Build (push) Successful in 10s
CI / Native Windows Build And Tests (push) Successful in 3m1s
CI / Windows Release Package (push) Successful in 3m20s
2026-05-12 23:39:57 +10:00
Aiden
4a049a557a Render timing 2026-05-12 22:18:27 +10:00
Aiden
13586c611a Start up settle 2026-05-12 22:04:46 +10:00
Aiden
3a83d9617f Clock updates 2026-05-12 21:44:26 +10:00
Aiden
5c66cfdc64 Input telemetry
All checks were successful
CI / React UI Build (push) Successful in 11s
CI / Native Windows Build And Tests (push) Successful in 2m56s
CI / Windows Release Package (push) Has been skipped
2026-05-12 21:13:22 +10:00
Aiden
d72272b5a8 2 frame buffer
All checks were successful
CI / React UI Build (push) Successful in 11s
CI / Native Windows Build And Tests (push) Successful in 2m59s
CI / Windows Release Package (push) Has been skipped
2026-05-12 21:08:02 +10:00
Aiden
c25ae7b25b input buffer
Some checks failed
CI / React UI Build (push) Successful in 11s
CI / Windows Release Package (push) Has been cancelled
CI / Native Windows Build And Tests (push) Has been cancelled
2026-05-12 21:05:42 +10:00
Aiden
a39be6fb20 Alignment
All checks were successful
CI / React UI Build (push) Successful in 10s
CI / Native Windows Build And Tests (push) Successful in 2m59s
CI / Windows Release Package (push) Has been skipped
2026-05-12 20:38:26 +10:00
Aiden
0a1fe440d9 docs pass 2026-05-12 20:32:32 +10:00
Aiden
3e45bba54b Update InputFrameMailbox.cpp
All checks were successful
CI / React UI Build (push) Successful in 11s
CI / Native Windows Build And Tests (push) Successful in 2m59s
CI / Windows Release Package (push) Has been skipped
2026-05-12 20:30:19 +10:00
Aiden
fd4b70ec9c Input GPU decoding
All checks were successful
CI / React UI Build (push) Successful in 11s
CI / Native Windows Build And Tests (push) Successful in 3m4s
CI / Windows Release Package (push) Has been skipped
2026-05-12 20:26:03 +10:00
Aiden
ce28904891 input testing
All checks were successful
CI / React UI Build (push) Successful in 11s
CI / Native Windows Build And Tests (push) Successful in 3m2s
CI / Windows Release Package (push) Has been skipped
2026-05-12 20:06:23 +10:00
Aiden
2c5e925b97 Video input fallback
All checks were successful
CI / React UI Build (push) Successful in 11s
CI / Native Windows Build And Tests (push) Successful in 3m7s
CI / Windows Release Package (push) Has been skipped
2026-05-12 18:53:46 +10:00
Aiden
957c0be05a Draw video if everything bypassed 2026-05-12 18:41:48 +10:00
Aiden
0a8b335048 INput
All checks were successful
CI / React UI Build (push) Successful in 10s
CI / Native Windows Build And Tests (push) Successful in 2m59s
CI / Windows Release Package (push) Has been skipped
2026-05-12 18:39:08 +10:00
Aiden
6e32941675 Fixed trigger
All checks were successful
CI / React UI Build (push) Successful in 10s
CI / Native Windows Build And Tests (push) Successful in 2m57s
CI / Windows Release Package (push) Has been skipped
2026-05-12 18:11:43 +10:00
Aiden
5fb4607d8c Clean up
All checks were successful
CI / React UI Build (push) Successful in 10s
CI / Native Windows Build And Tests (push) Successful in 2m55s
CI / Windows Release Package (push) Has been skipped
2026-05-12 18:03:54 +10:00
Aiden
f43b6f6519 shader control
All checks were successful
CI / React UI Build (push) Successful in 11s
CI / Native Windows Build And Tests (push) Successful in 3m3s
CI / Windows Release Package (push) Has been skipped
2026-05-12 17:52:55 +10:00
Aiden
dfd49fd0e3 Multipass shaders 2026-05-12 17:08:35 +10:00
Aiden
1429b2e660 Update shader
All checks were successful
CI / React UI Build (push) Successful in 11s
CI / Native Windows Build And Tests (push) Successful in 3m0s
CI / Windows Release Package (push) Has been skipped
2026-05-12 16:52:15 +10:00
Aiden
02b221f481 restructure 2026-05-12 15:47:59 +10:00
Aiden
6a33bd02ab Websocket split
All checks were successful
CI / React UI Build (push) Successful in 10s
CI / Native Windows Build And Tests (push) Successful in 3m0s
CI / Windows Release Package (push) Has been skipped
2026-05-12 15:36:02 +10:00
Aiden
da7e1a93f6 Websockets
All checks were successful
CI / React UI Build (push) Successful in 11s
CI / Native Windows Build And Tests (push) Successful in 2m58s
CI / Windows Release Package (push) Has been skipped
2026-05-12 15:32:01 +10:00
Aiden
334693f28c Render udpates 2026-05-12 15:26:02 +10:00
Aiden
c5fd8e72b4 non-blocking http
All checks were successful
CI / React UI Build (push) Successful in 11s
CI / Native Windows Build And Tests (push) Successful in 2m58s
CI / Windows Release Package (push) Has been skipped
2026-05-12 15:05:54 +10:00
Aiden
95b4a54326 Seperation 2026-05-12 14:57:18 +10:00
Aiden
d07ea1f63a Render changes
All checks were successful
CI / React UI Build (push) Successful in 11s
CI / Native Windows Build And Tests (push) Successful in 2m59s
CI / Windows Release Package (push) Has been skipped
2026-05-12 14:36:36 +10:00
Aiden
1ddcf5d621 More http post end points filled
All checks were successful
CI / React UI Build (push) Successful in 10s
CI / Native Windows Build And Tests (push) Successful in 3m1s
CI / Windows Release Package (push) Has been skipped
2026-05-12 14:23:53 +10:00
Aiden
38d729b346 CI update
All checks were successful
CI / React UI Build (push) Successful in 11s
CI / Native Windows Build And Tests (push) Successful in 2m56s
CI / Windows Release Package (push) Has been skipped
2026-05-12 13:55:10 +10:00
Aiden
4b62627479 removed render thread touching
Some checks failed
CI / React UI Build (push) Successful in 11s
CI / Windows Release Package (push) Has been cancelled
CI / Native Windows Build And Tests (push) Has been cancelled
2026-05-12 13:52:04 +10:00
Aiden
430cf0733d end point adjsutments
Some checks failed
CI / React UI Build (push) Successful in 11s
CI / Windows Release Package (push) Has been cancelled
CI / Native Windows Build And Tests (push) Has been cancelled
2026-05-12 13:50:32 +10:00
Aiden
b44504500a Ui serving
All checks were successful
CI / React UI Build (push) Successful in 11s
CI / Native Windows Build And Tests (push) Successful in 2m55s
CI / Windows Release Package (push) Successful in 3m21s
2026-05-12 13:25:34 +10:00
Aiden
bc690e2a87 Clean up pass
All checks were successful
CI / React UI Build (push) Successful in 11s
CI / Native Windows Build And Tests (push) Successful in 2m55s
CI / Windows Release Package (push) Successful in 3m14s
2026-05-12 13:14:52 +10:00
Aiden
9938a6cc26 http
All checks were successful
CI / React UI Build (push) Successful in 11s
CI / Native Windows Build And Tests (push) Successful in 2m54s
CI / Windows Release Package (push) Successful in 3m2s
2026-05-12 12:38:54 +10:00
Aiden
79f7ac6c86 Json telemetry
All checks were successful
CI / React UI Build (push) Successful in 11s
CI / Native Windows Build And Tests (push) Successful in 2m52s
CI / Windows Release Package (push) Successful in 3m19s
2026-05-12 12:13:21 +10:00
Aiden
44b198b14d logging
All checks were successful
CI / React UI Build (push) Successful in 38s
CI / Native Windows Build And Tests (push) Successful in 3m12s
CI / Windows Release Package (push) Successful in 3m7s
2026-05-12 11:58:29 +10:00
Aiden
511b67c9bc New rules based order
All checks were successful
CI / React UI Build (push) Successful in 11s
CI / Native Windows Build And Tests (push) Successful in 2m53s
CI / Windows Release Package (push) Successful in 3m18s
2026-05-12 02:35:15 +10:00
Aiden
c0d7e84495 Shader ownership change
All checks were successful
CI / React UI Build (push) Successful in 11s
CI / Native Windows Build And Tests (push) Successful in 2m52s
CI / Windows Release Package (push) Successful in 2m59s
2026-05-12 02:15:03 +10:00
Aiden
4ea829af85 Shader test past
Some checks failed
CI / React UI Build (push) Successful in 11s
CI / Native Windows Build And Tests (push) Successful in 2m52s
CI / Windows Release Package (push) Has been cancelled
2026-05-12 02:08:48 +10:00
Aiden
e0ca548ef5 V2 working
All checks were successful
CI / React UI Build (push) Successful in 11s
CI / Native Windows Build And Tests (push) Successful in 2m54s
CI / Windows Release Package (push) Successful in 3m14s
2026-05-12 01:59:02 +10:00
Aiden
2531d871e8 Doc cleanup
All checks were successful
CI / React UI Build (push) Successful in 10s
CI / Native Windows Build And Tests (push) Successful in 2m49s
CI / Windows Release Package (push) Successful in 3m8s
2026-05-12 01:37:20 +10:00
Aiden
709d3d3fa4 Test works
All checks were successful
CI / React UI Build (push) Successful in 11s
CI / Native Windows Build And Tests (push) Successful in 2m53s
CI / Windows Release Package (push) Successful in 3m1s
2026-05-12 01:30:30 +10:00
Aiden
ea31d0ca13 Clean 2026-05-12 01:21:42 +10:00
Aiden
f1f4e3421b Frame timing
All checks were successful
CI / React UI Build (push) Successful in 10s
CI / Native Windows Build And Tests (push) Successful in 2m53s
CI / Windows Release Package (push) Successful in 3m6s
2026-05-12 01:08:32 +10:00
Aiden
ac729dc2b9 Stage 1 rewrite 2026-05-12 00:52:33 +10:00
Aiden
bf23cd880a faliure
Some checks failed
CI / React UI Build (push) Successful in 10s
CI / Native Windows Build And Tests (push) Failing after 2m52s
CI / Windows Release Package (push) Has been skipped
2026-05-12 00:35:01 +10:00
Aiden
9e3412712c Improvement
All checks were successful
CI / React UI Build (push) Successful in 11s
CI / Native Windows Build And Tests (push) Successful in 2m52s
CI / Windows Release Package (push) Successful in 3m0s
2026-05-12 00:00:23 +10:00
Aiden
a434a88108 Performance chasing
All checks were successful
CI / React UI Build (push) Successful in 10s
CI / Native Windows Build And Tests (push) Successful in 2m51s
CI / Windows Release Package (push) Successful in 2m55s
2026-05-11 23:10:45 +10:00
Aiden
c5cead6003 Phase 7.5 step 2
All checks were successful
CI / React UI Build (push) Successful in 10s
CI / Native Windows Build And Tests (push) Successful in 2m45s
CI / Windows Release Package (push) Successful in 2m52s
2026-05-11 21:36:17 +10:00
Aiden
f8adbbe0fe Phase 7.5 timing logs
Some checks failed
CI / React UI Build (push) Successful in 11s
CI / Native Windows Build And Tests (push) Successful in 2m45s
CI / Windows Release Package (push) Has been cancelled
2026-05-11 21:32:40 +10:00
Aiden
0a7954e879 Phase 7 done
All checks were successful
CI / React UI Build (push) Successful in 10s
CI / Native Windows Build And Tests (push) Successful in 2m47s
CI / Windows Release Package (push) Successful in 3m2s
2026-05-11 21:15:51 +10:00
Aiden
f288455709 Phase 7
All checks were successful
CI / React UI Build (push) Successful in 11s
CI / Native Windows Build And Tests (push) Successful in 2m47s
CI / Windows Release Package (push) Successful in 3m2s
2026-05-11 21:05:11 +10:00
Aiden
50d5880835 Step 3 2026-05-11 20:49:36 +10:00
Aiden
52eaf16a8c Phase 7 step 2
All checks were successful
CI / React UI Build (push) Successful in 11s
CI / Native Windows Build And Tests (push) Successful in 2m44s
CI / Windows Release Package (push) Successful in 2m57s
2026-05-11 20:45:58 +10:00
Aiden
6b0638336a Phase 7 step 1
All checks were successful
CI / React UI Build (push) Successful in 10s
CI / Native Windows Build And Tests (push) Successful in 2m47s
CI / Windows Release Package (push) Successful in 2m53s
2026-05-11 20:39:01 +10:00
Aiden
0da6ad6802 Docs update
All checks were successful
CI / React UI Build (push) Successful in 11s
CI / Native Windows Build And Tests (push) Successful in 2m44s
CI / Windows Release Package (push) Successful in 2m47s
2026-05-11 20:14:10 +10:00
Aiden
dd3cd6b66c Clean up
Some checks failed
CI / React UI Build (push) Successful in 10s
CI / Windows Release Package (push) Has been cancelled
CI / Native Windows Build And Tests (push) Has been cancelled
2026-05-11 20:11:20 +10:00
Aiden
1d08dec5fe step 6
Some checks failed
CI / React UI Build (push) Successful in 10s
CI / Native Windows Build And Tests (push) Successful in 2m44s
CI / Windows Release Package (push) Has been cancelled
2026-05-11 20:06:14 +10:00
Aiden
0d57920bc1 step 5 2026-05-11 20:02:26 +10:00
Aiden
1629dbc77a step 4 2026-05-11 19:58:14 +10:00
Aiden
205c90e52e Step 3 2026-05-11 19:53:31 +10:00
Aiden
ab38bfad24 step 2
All checks were successful
CI / React UI Build (push) Successful in 11s
CI / Native Windows Build And Tests (push) Successful in 2m41s
CI / Windows Release Package (push) Successful in 2m46s
2026-05-11 19:49:05 +10:00
Aiden
68503256dc Phase 6 step 1
All checks were successful
CI / React UI Build (push) Successful in 10s
CI / Native Windows Build And Tests (push) Successful in 2m40s
CI / Windows Release Package (push) Successful in 2m47s
2026-05-11 19:44:35 +10:00
Aiden
a91cc91a21 Clean up shape
All checks were successful
CI / React UI Build (push) Successful in 10s
CI / Native Windows Build And Tests (push) Successful in 2m41s
CI / Windows Release Package (push) Successful in 2m48s
2026-05-11 19:37:44 +10:00
Aiden
a530325fa1 Organisation
Some checks failed
CI / React UI Build (push) Successful in 10s
CI / Native Windows Build And Tests (push) Successful in 2m42s
CI / Windows Release Package (push) Has been cancelled
2026-05-11 19:31:06 +10:00
Aiden
d332dceb5b Step 6
Some checks failed
CI / React UI Build (push) Successful in 11s
CI / Native Windows Build And Tests (push) Successful in 2m43s
CI / Windows Release Package (push) Has been cancelled
2026-05-11 19:25:29 +10:00
Aiden
79855d788c Step 5 storng option
Some checks failed
CI / React UI Build (push) Successful in 10s
CI / Native Windows Build And Tests (push) Successful in 2m41s
CI / Windows Release Package (push) Has been cancelled
2026-05-11 19:20:23 +10:00
Aiden
ff10b66d1d Phase 5 step 5
Some checks failed
CI / React UI Build (push) Successful in 10s
CI / Native Windows Build And Tests (push) Successful in 2m42s
CI / Windows Release Package (push) Has been cancelled
2026-05-11 19:14:59 +10:00
Aiden
fdcc38c6ae Step 4 2026-05-11 19:09:01 +10:00
Aiden
718e4dcadd step 3
All checks were successful
CI / React UI Build (push) Successful in 11s
CI / Native Windows Build And Tests (push) Successful in 2m40s
CI / Windows Release Package (push) Successful in 2m44s
2026-05-11 19:05:29 +10:00
Aiden
7740fe209c Phase 5 step 2
All checks were successful
CI / React UI Build (push) Successful in 10s
CI / Native Windows Build And Tests (push) Successful in 2m39s
CI / Windows Release Package (push) Successful in 2m54s
2026-05-11 18:56:39 +10:00
Aiden
77590f4a62 Phase 5 step 1
Some checks failed
CI / React UI Build (push) Successful in 11s
CI / Windows Release Package (push) Has been cancelled
CI / Native Windows Build And Tests (push) Has been cancelled
2026-05-11 18:53:59 +10:00
Aiden
e8a3805fff Doc update again
Some checks failed
CI / React UI Build (push) Successful in 10s
CI / Native Windows Build And Tests (push) Successful in 2m40s
CI / Windows Release Package (push) Has been cancelled
2026-05-11 18:48:55 +10:00
Aiden
99fd903144 docs update
Some checks failed
CI / React UI Build (push) Successful in 11s
CI / Native Windows Build And Tests (push) Successful in 2m40s
CI / Windows Release Package (push) Has been cancelled
2026-05-11 18:45:03 +10:00
Aiden
761df3b2d0 Phase 4 complete
All checks were successful
CI / React UI Build (push) Successful in 11s
CI / Native Windows Build And Tests (push) Successful in 2m39s
CI / Windows Release Package (push) Successful in 2m45s
2026-05-11 18:39:02 +10:00
Aiden
f141d20026 Sanity pass
All checks were successful
CI / React UI Build (push) Successful in 10s
CI / Native Windows Build And Tests (push) Successful in 2m42s
CI / Windows Release Package (push) Successful in 2m45s
2026-05-11 18:31:36 +10:00
Aiden
bfc32c4a1e Phase 4
Some checks failed
CI / React UI Build (push) Successful in 11s
CI / Native Windows Build And Tests (push) Successful in 2m43s
CI / Windows Release Package (push) Has been cancelled
2026-05-11 18:25:47 +10:00
Aiden
20476bdf63 Step 3
All checks were successful
CI / React UI Build (push) Successful in 11s
CI / Native Windows Build And Tests (push) Successful in 2m40s
CI / Windows Release Package (push) Successful in 2m46s
2026-05-11 17:41:59 +10:00
Aiden
0ec5a4cfed Phase 4 step 2a
All checks were successful
CI / React UI Build (push) Successful in 11s
CI / Native Windows Build And Tests (push) Successful in 2m36s
CI / Windows Release Package (push) Successful in 2m42s
2026-05-11 17:26:24 +10:00
Aiden
539fcd3351 Phase 4 step 1
Some checks failed
CI / React UI Build (push) Successful in 11s
CI / Native Windows Build And Tests (push) Successful in 2m35s
CI / Windows Release Package (push) Has been cancelled
2026-05-11 17:21:27 +10:00
Aiden
ebc10a9925 docs update
Some checks failed
CI / React UI Build (push) Successful in 10s
CI / Native Windows Build And Tests (push) Successful in 2m36s
CI / Windows Release Package (push) Has been cancelled
2026-05-11 17:16:39 +10:00
Aiden
e5c5920ccd Launch debug change
Some checks failed
CI / React UI Build (push) Successful in 11s
CI / Native Windows Build And Tests (push) Successful in 2m35s
CI / Windows Release Package (push) Has been cancelled
2026-05-11 17:11:20 +10:00
Aiden
3b641dd07a render thread
Some checks failed
CI / React UI Build (push) Successful in 11s
CI / Windows Release Package (push) Has been cancelled
CI / Native Windows Build And Tests (push) Has been cancelled
2026-05-11 17:07:45 +10:00
Aiden
e00e2574ed render engine updates
Some checks failed
CI / React UI Build (push) Successful in 11s
CI / Windows Release Package (push) Has been cancelled
CI / Native Windows Build And Tests (push) Has been cancelled
2026-05-11 17:06:42 +10:00
Aiden
e459155d51 Clean up pass
All checks were successful
CI / React UI Build (push) Successful in 10s
CI / Native Windows Build And Tests (push) Successful in 2m35s
CI / Windows Release Package (push) Successful in 2m38s
2026-05-11 16:52:53 +10:00
Aiden
06f3dd4942 Phase 3 refactor in progress
Some checks failed
CI / React UI Build (push) Successful in 10s
CI / Native Windows Build And Tests (push) Successful in 2m33s
CI / Windows Release Package (push) Has been cancelled
2026-05-11 16:48:52 +10:00
Aiden
0808171677 GROUND WORK PHASE 3
All checks were successful
CI / React UI Build (push) Successful in 11s
CI / Native Windows Build And Tests (push) Successful in 2m32s
CI / Windows Release Package (push) Successful in 2m34s
2026-05-11 16:32:51 +10:00
Aiden
00b6ad4c36 Phase 3 docs
All checks were successful
CI / React UI Build (push) Successful in 10s
CI / Native Windows Build And Tests (push) Successful in 2m27s
CI / Windows Release Package (push) Successful in 2m46s
2026-05-11 16:24:52 +10:00
Aiden
d4f6a4a268 phase 2 progress
All checks were successful
CI / React UI Build (push) Successful in 11s
CI / Native Windows Build And Tests (push) Successful in 2m26s
CI / Windows Release Package (push) Successful in 2m30s
2026-05-11 16:18:34 +10:00
Aiden
6e600be112 Tests
All checks were successful
CI / React UI Build (push) Successful in 10s
CI / Native Windows Build And Tests (push) Successful in 2m26s
CI / Windows Release Package (push) Successful in 2m28s
2026-05-11 15:59:29 +10:00
Aiden
a9b08f7f27 dispatch event intergration
All checks were successful
CI / React UI Build (push) Successful in 11s
CI / Native Windows Build And Tests (push) Successful in 2m28s
CI / Windows Release Package (push) Successful in 2m24s
2026-05-11 15:42:14 +10:00
Aiden
ccfc0237fd RuntimeStateBroadcastRequested
All checks were successful
CI / React UI Build (push) Successful in 10s
CI / Native Windows Build And Tests (push) Successful in 2m21s
CI / Windows Release Package (push) Successful in 2m23s
2026-05-11 15:19:43 +10:00
Aiden
b3705d96cc event dispatcher
Some checks failed
CI / React UI Build (push) Successful in 11s
CI / Native Windows Build And Tests (push) Successful in 2m37s
CI / Windows Release Package (push) Has been cancelled
2026-05-11 15:15:42 +10:00
Aiden
5503ce85a9 phase 2
All checks were successful
CI / React UI Build (push) Successful in 11s
CI / Native Windows Build And Tests (push) Successful in 2m17s
CI / Windows Release Package (push) Successful in 2m27s
2026-05-11 02:35:31 +10:00
Aiden
41677b71ec Finished phase 1
Some checks failed
CI / React UI Build (push) Successful in 10s
CI / Native Windows Build And Tests (push) Successful in 2m18s
CI / Windows Release Package (push) Has been cancelled
2026-05-11 02:32:13 +10:00
Aiden
9cbb5d8004 phase 1 runtime complete
All checks were successful
CI / React UI Build (push) Successful in 11s
CI / Native Windows Build And Tests (push) Successful in 2m46s
CI / Windows Release Package (push) Successful in 2m59s
2026-05-11 02:23:01 +10:00
Aiden
cbf1b541dc re organisation
All checks were successful
CI / React UI Build (push) Successful in 11s
CI / Native Windows Build And Tests (push) Successful in 2m47s
CI / Windows Release Package (push) Successful in 2m52s
2026-05-11 02:11:51 +10:00
Aiden
5cbdbd6813 Pass 3
Some checks failed
CI / React UI Build (push) Successful in 11s
CI / Native Windows Build And Tests (push) Successful in 2m48s
CI / Windows Release Package (push) Has been cancelled
2026-05-11 02:06:17 +10:00
Aiden
b2369c418b pass 2
All checks were successful
CI / React UI Build (push) Successful in 10s
CI / Native Windows Build And Tests (push) Successful in 2m40s
CI / Windows Release Package (push) Successful in 2m39s
2026-05-11 01:29:44 +10:00
Aiden
c4883d3413 pass 1
All checks were successful
CI / React UI Build (push) Successful in 11s
CI / Native Windows Build And Tests (push) Successful in 2m39s
CI / Windows Release Package (push) Successful in 2m45s
2026-05-11 01:12:24 +10:00
Aiden
53e78890a8 more
All checks were successful
CI / React UI Build (push) Successful in 11s
CI / Native Windows Build And Tests (push) Successful in 2m37s
CI / Windows Release Package (push) Successful in 2m40s
2026-05-11 00:55:19 +10:00
Aiden
36b398ea95 mini cleanup
All checks were successful
CI / React UI Build (push) Successful in 11s
CI / Native Windows Build And Tests (push) Successful in 2m38s
CI / Windows Release Package (push) Successful in 2m41s
2026-05-11 00:41:51 +10:00
Aiden
ba4643dfa3 further phase 1
Some checks failed
CI / React UI Build (push) Successful in 11s
CI / Native Windows Build And Tests (push) Successful in 2m39s
CI / Windows Release Package (push) Has been cancelled
2026-05-11 00:38:49 +10:00
Aiden
27dbb55f7b OSC seperation
All checks were successful
CI / React UI Build (push) Successful in 11s
CI / Native Windows Build And Tests (push) Successful in 2m36s
CI / Windows Release Package (push) Successful in 2m48s
2026-05-11 00:26:59 +10:00
Aiden
f6b26bf28b runtime udates
Some checks failed
CI / React UI Build (push) Successful in 11s
CI / Native Windows Build And Tests (push) Successful in 2m39s
CI / Windows Release Package (push) Has been cancelled
2026-05-11 00:22:55 +10:00
Aiden
861593123d Runtime snapshot provider changes
Some checks failed
CI / React UI Build (push) Successful in 10s
CI / Native Windows Build And Tests (push) Successful in 2m38s
CI / Windows Release Package (push) Has been cancelled
2026-05-11 00:18:01 +10:00
Aiden
34c145e80b Health
All checks were successful
CI / React UI Build (push) Successful in 10s
CI / Native Windows Build And Tests (push) Successful in 2m35s
CI / Windows Release Package (push) Successful in 2m46s
2026-05-11 00:08:12 +10:00
Aiden
a24cdc0630 Osc orchestration
All checks were successful
CI / React UI Build (push) Successful in 11s
CI / Native Windows Build And Tests (push) Successful in 2m38s
CI / Windows Release Package (push) Successful in 2m42s
2026-05-10 23:59:48 +10:00
Aiden
120f899b0d docs
Some checks failed
CI / React UI Build (push) Successful in 11s
CI / Windows Release Package (push) Has been cancelled
CI / Native Windows Build And Tests (push) Has been cancelled
2026-05-10 23:57:05 +10:00
Aiden
41075bbc61 more seperation 2026-05-10 23:53:27 +10:00
Aiden
7f0f60c0e3 ore untangling
All checks were successful
CI / React UI Build (push) Successful in 11s
CI / Native Windows Build And Tests (push) Successful in 2m38s
CI / Windows Release Package (push) Successful in 2m36s
2026-05-10 23:31:45 +10:00
Aiden
739231d5a1 Phase 1 clean-up and separation of concerns
All checks were successful
CI / React UI Build (push) Successful in 10s
CI / Native Windows Build And Tests (push) Successful in 2m34s
CI / Windows Release Package (push) Successful in 2m42s
2026-05-10 23:21:13 +10:00
Aiden
3629227aa9 intial phase 1 subsytem split
All checks were successful
CI / React UI Build (push) Successful in 11s
CI / Native Windows Build And Tests (push) Successful in 2m31s
CI / Windows Release Package (push) Successful in 2m32s
2026-05-10 23:03:15 +10:00
Aiden
618831d578 Phase 1 goals
All checks were successful
CI / React UI Build (push) Successful in 11s
CI / Native Windows Build And Tests (push) Successful in 2m26s
CI / Windows Release Package (push) Successful in 2m32s
2026-05-10 22:36:34 +10:00
Aiden
c38c22834d Preroll udpate
All checks were successful
CI / React UI Build (push) Successful in 11s
CI / Native Windows Build And Tests (push) Successful in 2m29s
CI / Windows Release Package (push) Successful in 2m30s
2026-05-10 22:30:47 +10:00
Aiden
c8a4bd4c7b adjustments to control and stack saving
All checks were successful
CI / React UI Build (push) Successful in 11s
CI / Native Windows Build And Tests (push) Successful in 2m28s
CI / Windows Release Package (push) Successful in 2m44s
2026-05-10 22:10:54 +10:00
Aiden
46129a6044 UI fix
All checks were successful
CI / React UI Build (push) Successful in 10s
CI / Native Windows Build And Tests (push) Successful in 2m26s
CI / Windows Release Package (push) Successful in 2m28s
2026-05-10 21:27:13 +10:00
Aiden
8fcb51d140 example data store
All checks were successful
CI / React UI Build (push) Successful in 11s
CI / Native Windows Build And Tests (push) Successful in 2m24s
CI / Windows Release Package (push) Successful in 2m34s
2026-05-10 21:11:17 +10:00
Aiden
944773c248 added new layer input pass
All checks were successful
CI / React UI Build (push) Successful in 11s
CI / Native Windows Build And Tests (push) Successful in 2m25s
CI / Windows Release Package (push) Successful in 2m28s
2026-05-10 21:00:34 +10:00
Aiden
7777cfc194 data storage
All checks were successful
CI / React UI Build (push) Successful in 11s
CI / Native Windows Build And Tests (push) Successful in 2m23s
CI / Windows Release Package (push) Successful in 2m46s
2026-05-10 20:39:28 +10:00
Aiden
198639ae3f OSC sync back
All checks were successful
CI / React UI Build (push) Successful in 11s
CI / Native Windows Build And Tests (push) Successful in 2m22s
CI / Windows Release Package (push) Successful in 2m29s
2026-05-10 18:58:26 +10:00
Aiden
d7ca42b51b OSC fixes
All checks were successful
CI / React UI Build (push) Successful in 11s
CI / Native Windows Build And Tests (push) Successful in 2m22s
CI / Windows Release Package (push) Successful in 2m43s
2026-05-10 18:37:30 +10:00
Aiden
f11d531e0c OSC bind address
All checks were successful
CI / React UI Build (push) Successful in 11s
CI / Native Windows Build And Tests (push) Successful in 2m41s
CI / Windows Release Package (push) Successful in 2m30s
2026-05-10 17:23:28 +10:00
Aiden
a3635b5d31 Revert "preview changes"
All checks were successful
CI / React UI Build (push) Successful in 11s
CI / Native Windows Build And Tests (push) Successful in 2m23s
CI / Windows Release Package (push) Successful in 2m28s
This reverts commit 98f5cbe309.
2026-05-09 16:47:45 +10:00
Aiden
bc9aa6fbad Revert "Video backend"
This reverts commit 4ffbb97abf.
2026-05-09 16:47:43 +10:00
Aiden
0c16665610 Revert "Decklink separation"
Some checks failed
CI / Windows Release Package (push) Has been cancelled
CI / React UI Build (push) Has been cancelled
CI / Native Windows Build And Tests (push) Has been cancelled
This reverts commit 46f2f1ece5.
2026-05-09 16:47:33 +10:00
Aiden
46f2f1ece5 Decklink separation 2026-05-09 14:42:11 +10:00
Aiden
4ffbb97abf Video backend
All checks were successful
CI / React UI Build (push) Successful in 11s
CI / Native Windows Build And Tests (push) Successful in 2m43s
CI / Windows Release Package (push) Successful in 2m54s
2026-05-09 14:15:49 +10:00
Aiden
98f5cbe309 preview changes
All checks were successful
CI / React UI Build (push) Successful in 11s
CI / Native Windows Build And Tests (push) Successful in 2m23s
CI / Windows Release Package (push) Successful in 2m41s
2026-05-09 13:53:00 +10:00
Aiden
93d856b3b6 CPU optimisations
Some checks failed
CI / React UI Build (push) Successful in 37s
CI / Windows Release Package (push) Has been cancelled
CI / Native Windows Build And Tests (push) Has been cancelled
2026-05-09 13:50:27 +10:00
6ea6971dd6 more shaders and updates/changes
All checks were successful
CI / React UI Build (push) Successful in 10s
CI / Native Windows Build And Tests (push) Successful in 2m22s
CI / Windows Release Package (push) Successful in 2m35s
2026-05-08 20:32:19 +10:00
163d70e9bd Annotations
All checks were successful
CI / React UI Build (push) Successful in 11s
CI / Native Windows Build And Tests (push) Successful in 2m22s
CI / Windows Release Package (push) Successful in 2m28s
2026-05-08 20:01:22 +10:00
8afef5065a Update README.md
All checks were successful
CI / React UI Build (push) Successful in 11s
CI / Native Windows Build And Tests (push) Successful in 2m20s
CI / Windows Release Package (push) Successful in 2m34s
2026-05-08 19:14:31 +10:00
27bf2ae45c doc updates
All checks were successful
CI / React UI Build (push) Successful in 11s
CI / Native Windows Build And Tests (push) Successful in 2m22s
CI / Windows Release Package (push) Successful in 2m27s
2026-05-08 18:49:27 +10:00
1ea44ba3ae fix typo
All checks were successful
CI / React UI Build (push) Successful in 11s
CI / Native Windows Build And Tests (push) Successful in 2m19s
CI / Windows Release Package (push) Successful in 2m31s
2026-05-08 18:43:48 +10:00
0af9a72937 removed redundant code
Some checks failed
CI / React UI Build (push) Successful in 10s
CI / Native Windows Build And Tests (push) Successful in 2m22s
CI / Windows Release Package (push) Has been cancelled
2026-05-08 18:40:56 +10:00
d650cac857 control layout updates
All checks were successful
CI / React UI Build (push) Successful in 10s
CI / Native Windows Build And Tests (push) Successful in 2m20s
CI / Windows Release Package (push) Successful in 2m28s
2026-05-08 18:28:28 +10:00
a0cc86f189 description updates
All checks were successful
CI / React UI Build (push) Successful in 10s
CI / Native Windows Build And Tests (push) Successful in 2m20s
CI / Windows Release Package (push) Successful in 2m28s
2026-05-08 18:11:26 +10:00
f322abf79a updates
Some checks failed
CI / React UI Build (push) Successful in 11s
CI / Native Windows Build And Tests (push) Successful in 2m22s
CI / Windows Release Package (push) Has been cancelled
2026-05-08 18:07:45 +10:00
eede6938cb Update multipass shader test
All checks were successful
CI / React UI Build (push) Successful in 11s
CI / Native Windows Build And Tests (push) Successful in 2m17s
CI / Windows Release Package (push) Successful in 2m27s
2026-05-08 17:41:53 +10:00
ad24a20fdb Multi pass test
Some checks failed
CI / React UI Build (push) Successful in 10s
CI / Windows Release Package (push) Has been cancelled
CI / Native Windows Build And Tests (push) Has been cancelled
2026-05-08 17:40:09 +10:00
5ae43513a7 annotations
Some checks failed
CI / React UI Build (push) Successful in 11s
CI / Native Windows Build And Tests (push) Successful in 2m16s
CI / Windows Release Package (push) Has been cancelled
2026-05-08 17:35:48 +10:00
cc23e73d51 Removed uneeded code 2026-05-08 17:33:57 +10:00
f85abef237 Multi pass
All checks were successful
CI / React UI Build (push) Successful in 10s
CI / Native Windows Build And Tests (push) Successful in 2m16s
CI / Windows Release Package (push) Successful in 2m28s
2026-05-08 17:28:48 +10:00
596d370f43 Add manifest support for pass declarations
All checks were successful
CI / React UI Build (push) Successful in 11s
CI / Native Windows Build And Tests (push) Successful in 2m17s
CI / Windows Release Package (push) Successful in 2m28s
2026-05-08 17:19:30 +10:00
87cb55b80b Layer program split
All checks were successful
CI / React UI Build (push) Successful in 11s
CI / Native Windows Build And Tests (push) Successful in 2m14s
CI / Windows Release Package (push) Successful in 2m26s
2026-05-08 17:10:29 +10:00
f458eb0130 Texture binding 2026-05-08 17:04:28 +10:00
7d8f9a39d1 render target pool
All checks were successful
CI / React UI Build (push) Successful in 11s
CI / Native Windows Build And Tests (push) Successful in 2m15s
CI / Windows Release Package (push) Successful in 2m31s
2026-05-08 16:59:43 +10:00
5b6e30ad13 Render class 2026-05-08 16:55:16 +10:00
07a5c91427 shader validation checks
All checks were successful
CI / React UI Build (push) Successful in 10s
CI / Native Windows Build And Tests (push) Successful in 2m16s
CI / Windows Release Package (push) Successful in 2m27s
2026-05-08 16:46:03 +10:00
53b980913b docs update 2026-05-08 16:42:23 +10:00
4e2ac4a091 re organisation
All checks were successful
CI / React UI Build (push) Successful in 11s
CI / Native Windows Build And Tests (push) Successful in 1m42s
CI / Windows Release Package (push) Successful in 2m31s
2026-05-08 16:38:47 +10:00
3eb5bb5de3 Splitting out rendering 2026-05-08 16:33:55 +10:00
ebbc11bb34 Decklink abstraction
All checks were successful
CI / React UI Build (push) Successful in 11s
CI / Native Windows Build And Tests (push) Successful in 1m41s
CI / Windows Release Package (push) Successful in 2m20s
2026-05-08 16:27:40 +10:00
6d5a606107 Greenscreen adjsutments
All checks were successful
CI / React UI Build (push) Successful in 10s
CI / Native Windows Build And Tests (push) Successful in 1m35s
CI / Windows Release Package (push) Successful in 2m22s
2026-05-08 16:11:43 +10:00
0831e18c2d Updated shader and fixed PNG output
All checks were successful
CI / React UI Build (push) Successful in 10s
CI / Native Windows Build And Tests (push) Successful in 2m15s
CI / Windows Release Package (push) Successful in 2m10s
2026-05-08 15:52:58 +10:00
05d0bcbedd PNG writer
All checks were successful
CI / React UI Build (push) Successful in 11s
CI / Native Windows Build And Tests (push) Successful in 1m35s
CI / Windows Release Package (push) Successful in 2m17s
2026-05-08 15:33:40 +10:00
6ea70d9497 shader adjustment
All checks were successful
CI / React UI Build (push) Successful in 11s
CI / Native Windows Build And Tests (push) Successful in 1m33s
CI / Windows Release Package (push) Successful in 2m11s
2026-05-08 15:12:48 +10:00
bc536bd751 Control ui adjsutments
All checks were successful
CI / React UI Build (push) Successful in 11s
CI / Native Windows Build And Tests (push) Successful in 1m54s
CI / Windows Release Package (push) Successful in 2m9s
2026-05-08 13:54:02 +10:00
7035cde8c8 added random seed
All checks were successful
CI / React UI Build (push) Successful in 11s
CI / Native Windows Build And Tests (push) Successful in 1m33s
CI / Windows Release Package (push) Successful in 2m11s
2026-05-08 13:38:27 +10:00
5eff189bbf random float
Some checks failed
CI / React UI Build (push) Successful in 11s
CI / Native Windows Build And Tests (push) Successful in 1m45s
CI / Windows Release Package (push) Has been cancelled
2026-05-08 13:35:15 +10:00
c9fed70a60 16bit processing
All checks were successful
CI / React UI Build (push) Successful in 38s
CI / Native Windows Build And Tests (push) Successful in 1m48s
CI / Windows Release Package (push) Successful in 2m16s
2026-05-08 13:27:41 +10:00
fb9122ecdc Update README.md
All checks were successful
CI / React UI Build (push) Successful in 11s
CI / Native Windows Build And Tests (push) Successful in 1m33s
CI / Windows Release Package (push) Successful in 2m4s
2026-05-07 06:54:59 +00:00
bff27c42a7 Update README.md
All checks were successful
CI / React UI Build (push) Successful in 16s
CI / Native Windows Build And Tests (push) Successful in 1m45s
CI / Windows Release Package (push) Successful in 2m28s
2026-05-07 06:11:53 +00:00
cea435b609 shader tweak for LUT application
All checks were successful
CI / React UI Build (push) Successful in 11s
CI / Native Windows Build And Tests (push) Successful in 1m33s
CI / Windows Release Package (push) Successful in 2m21s
2026-05-06 16:53:54 +10:00
f9ea2d6900 LUT interpolation
All checks were successful
CI / React UI Build (push) Successful in 11s
CI / Native Windows Build And Tests (push) Successful in 1m47s
CI / Windows Release Package (push) Successful in 2m31s
2026-05-06 16:44:39 +10:00
96e7e66b0d Install step
All checks were successful
CI / React UI Build (push) Successful in 11s
CI / Native Windows Build And Tests (push) Successful in 1m35s
CI / Windows Release Package (push) Successful in 2m6s
2026-05-06 14:51:19 +10:00
e5221b329f Added xyla shader
Some checks failed
CI / React UI Build (push) Successful in 11s
CI / Windows Release Package (push) Has been cancelled
CI / Native Windows Build And Tests (push) Has been cancelled
2026-05-06 14:50:00 +10:00
70be7312b8 Timing and saftey pass
All checks were successful
CI / React UI Build (push) Successful in 11s
CI / Native Windows Build And Tests (push) Successful in 1m33s
CI / Windows Release Package (push) Successful in 2m23s
2026-05-06 14:35:41 +10:00
b2f4d6677c Footer
All checks were successful
CI / React UI Build (push) Successful in 11s
CI / Native Windows Build And Tests (push) Successful in 1m33s
CI / Windows Release Package (push) Successful in 2m14s
2026-05-06 14:15:57 +10:00
08e039aebe Shader compile thread seperation
All checks were successful
CI / React UI Build (push) Successful in 11s
CI / Native Windows Build And Tests (push) Successful in 1m31s
CI / Windows Release Package (push) Successful in 2m6s
2026-05-06 14:11:18 +10:00
6502344d0a Added trigger
All checks were successful
CI / React UI Build (push) Successful in 11s
CI / Native Windows Build And Tests (push) Successful in 1m33s
CI / Windows Release Package (push) Successful in 2m4s
2026-05-06 14:01:23 +10:00
e59677c212 Typography improvements
All checks were successful
CI / React UI Build (push) Successful in 11s
CI / Native Windows Build And Tests (push) Successful in 1m31s
CI / Windows Release Package (push) Successful in 2m20s
2026-05-06 13:16:02 +10:00
3dc7af6fc0 control
All checks were successful
CI / React UI Build (push) Successful in 11s
CI / Native Windows Build And Tests (push) Successful in 1m32s
CI / Windows Release Package (push) Successful in 2m4s
2026-05-06 13:03:35 +10:00
ef829bf3ef Added control interface
All checks were successful
CI / React UI Build (push) Successful in 11s
CI / Native Windows Build And Tests (push) Successful in 1m32s
CI / Windows Release Package (push) Successful in 2m20s
2026-05-06 12:55:36 +10:00
ff1b7519a0 Added bad shader warning instead of hard fail
Some checks failed
CI / React UI Build (push) Successful in 11s
CI / Native Windows Build And Tests (push) Successful in 1m32s
CI / Windows Release Package (push) Failing after 2m15s
2026-05-06 12:44:22 +10:00
414ef62479 Added clock time
Some checks failed
CI / React UI Build (push) Successful in 11s
CI / Native Windows Build And Tests (push) Successful in 1m32s
CI / Windows Release Package (push) Failing after 2m7s
2026-05-06 12:38:23 +10:00
d2cf852eb2 Update ci.yml
Some checks failed
CI / React UI Build (push) Successful in 10s
CI / Native Windows Build And Tests (push) Successful in 1m30s
CI / Windows Release Package (push) Failing after 2m29s
2026-05-06 12:15:34 +10:00
73e0af5d2e Update ci.yml
Some checks failed
CI / React UI Build (push) Successful in 11s
CI / Native Windows Build And Tests (push) Failing after 17s
CI / Windows Release Package (push) Has been skipped
2026-05-06 12:12:08 +10:00
99e8fb4681 Updated runner
Some checks failed
CI / React UI Build (push) Successful in 11s
CI / Native Windows Build And Tests (push) Has been cancelled
CI / Windows Release Package (push) Has been cancelled
2026-05-06 12:11:51 +10:00
a58f8aaf43 Start up procedures
Some checks failed
CI / React UI Build (push) Successful in 10s
CI / Native Windows Build And Tests (push) Failing after 19s
CI / Windows Release Package (push) Has been skipped
2026-05-06 11:56:02 +10:00
515f58b848 Video format refactor
Some checks failed
CI / Native Windows Build And Tests (push) Failing after 4s
CI / React UI Build (push) Successful in 11s
CI / Windows Release Package (push) Has been skipped
2026-05-06 11:51:08 +10:00
02a8a64360 com updates
Some checks failed
CI / Native Windows Build And Tests (push) Failing after 4s
CI / React UI Build (push) Successful in 11s
CI / Windows Release Package (push) Has been skipped
2026-05-06 11:41:27 +10:00
a526887ff6 temporal manifest tests
Some checks failed
CI / Native Windows Build And Tests (push) Has been cancelled
CI / React UI Build (push) Has been cancelled
CI / Windows Release Package (push) Has been cancelled
2026-05-06 11:34:53 +10:00
d2ac369fdc Pacing problems
Some checks failed
CI / Native Windows Build And Tests (push) Has been cancelled
CI / React UI Build (push) Has been cancelled
CI / Windows Release Package (push) Has been cancelled
2026-05-06 11:31:48 +10:00
2317a80ce5 stutter fix
Some checks failed
CI / Native Windows Build And Tests (push) Has been cancelled
CI / React UI Build (push) Has been cancelled
CI / Windows Release Package (push) Has been cancelled
2026-05-06 11:23:40 +10:00
3cb8d3cfad added tests
Some checks failed
CI / Native Windows Build And Tests (push) Has been cancelled
CI / React UI Build (push) Has been cancelled
CI / Windows Release Package (push) Has been cancelled
2026-05-06 11:09:15 +10:00
8b9e2916df Shader ownership
Some checks failed
CI / Native Windows Build And Tests (push) Has been cancelled
CI / React UI Build (push) Has been cancelled
CI / Windows Release Package (push) Has been cancelled
2026-05-06 11:03:16 +10:00
bbbc678c83 Simplify ownership/lifetime
Some checks failed
CI / Native Windows Build And Tests (push) Has been cancelled
CI / React UI Build (push) Has been cancelled
CI / Windows Release Package (push) Has been cancelled
2026-05-06 10:57:59 +10:00
1b67777c4a Extract frame transfer callbacks
Some checks failed
CI / Native Windows Build And Tests (push) Has been cancelled
CI / React UI Build (push) Has been cancelled
CI / Windows Release Package (push) Has been cancelled
2026-05-06 10:53:53 +10:00
5fd24b3f06 Hide renderer internals
Some checks failed
CI / Native Windows Build And Tests (push) Has been cancelled
CI / React UI Build (push) Has been cancelled
CI / Windows Release Package (push) Has been cancelled
2026-05-06 10:48:50 +10:00
35f5a024fd Decklink helper
Some checks failed
CI / Native Windows Build And Tests (push) Has been cancelled
CI / React UI Build (push) Has been cancelled
CI / Windows Release Package (push) Has been cancelled
2026-05-06 10:44:55 +10:00
6918306336 decklink separation
Some checks failed
CI / Native Windows Build And Tests (push) Has been cancelled
CI / React UI Build (push) Has been cancelled
CI / Windows Release Package (push) Has been cancelled
2026-05-06 10:31:21 +10:00
8ec87685b8 Shader clean up
Some checks failed
CI / Native Windows Build And Tests (push) Has been cancelled
CI / React UI Build (push) Has been cancelled
CI / Windows Release Package (push) Has been cancelled
2026-05-06 10:26:38 +10:00
8c8028dd1f Separate history
Some checks failed
CI / Native Windows Build And Tests (push) Has been cancelled
CI / React UI Build (push) Has been cancelled
CI / Windows Release Package (push) Has been cancelled
2026-05-06 10:14:55 +10:00
9e480db31c Further refactor
Some checks failed
CI / Native Windows Build And Tests (push) Has been cancelled
CI / React UI Build (push) Has been cancelled
CI / Windows Release Package (push) Has been cancelled
2026-05-06 09:31:44 +10:00
0bfffa6552 Refactor
Some checks failed
CI / Native Windows Build And Tests (push) Has been cancelled
CI / React UI Build (push) Has been cancelled
CI / Windows Release Package (push) Has been cancelled
2026-05-06 09:28:46 +10:00
437199f3f0 Additional shaders
Some checks failed
CI / Native Windows Build And Tests (push) Has been cancelled
CI / React UI Build (push) Has been cancelled
CI / Windows Release Package (push) Has been cancelled
2026-05-06 00:23:20 +10:00
cf31c91831 Merge pull request 'Text-and-Fonts' (#1) from Text-and-Fonts into main
Some checks failed
CI / Native Windows Build And Tests (push) Has been cancelled
CI / React UI Build (push) Has been cancelled
CI / Windows Release Package (push) Has been cancelled
Reviewed-on: #1
2026-05-05 13:57:23 +00:00
7e4ab5cbd8 V1 text, needs improvements
Some checks failed
CI / Native Windows Build And Tests (pull_request) Failing after 18s
CI / React UI Build (pull_request) Has been cancelled
CI / Windows Release Package (pull_request) Has been cancelled
2026-05-05 23:57:02 +10:00
6ce09c0e9c making text pretty 2026-05-05 23:51:02 +10:00
62c3ded1f8 Font working 2026-05-05 23:47:08 +10:00
3e8b472f74 Initial font work 2026-05-05 23:18:50 +10:00
fd0ebb8d40 Update README.md
Some checks failed
CI / Native Windows Build And Tests (push) Has been cancelled
CI / React UI Build (push) Has been cancelled
CI / Windows Release Package (push) Has been cancelled
2026-05-05 22:56:56 +10:00
fcdc5bac6e Update README.md
Some checks failed
CI / Native Windows Build And Tests (push) Has been cancelled
CI / React UI Build (push) Has been cancelled
CI / Windows Release Package (push) Has been cancelled
2026-05-05 22:52:53 +10:00
fecc936a14 Input optional
Some checks failed
CI / Native Windows Build And Tests (push) Has been cancelled
CI / React UI Build (push) Has been cancelled
CI / Windows Release Package (push) Has been cancelled
2026-05-05 22:52:41 +10:00
536f65bf88 Todo
Some checks failed
CI / Native Windows Build And Tests (push) Has been cancelled
CI / React UI Build (push) Has been cancelled
CI / Windows Release Package (push) Has been cancelled
2026-05-05 22:50:46 +10:00
ce5905373a Added new shaders
Some checks failed
CI / Native Windows Build And Tests (push) Has been cancelled
CI / React UI Build (push) Has been cancelled
CI / Windows Release Package (push) Has been cancelled
2026-05-05 22:36:52 +10:00
119e49aec1 Updated build steps
Some checks failed
CI / Native Windows Build And Tests (push) Has been cancelled
CI / React UI Build (push) Has been cancelled
CI / Windows Release Package (push) Has been cancelled
2026-05-05 21:39:33 +10:00
1cde845a77 Add lciense
Some checks failed
CI / Native Windows Build And Tests (push) Has been cancelled
CI / React UI Build (push) Has been cancelled
CI / Windows Release Package (push) Has been cancelled
2026-05-05 21:14:43 +10:00
74789b43f6 Docs update
Some checks failed
CI / Native Windows Build And Tests (push) Has been cancelled
CI / React UI Build (push) Has been cancelled
CI / Windows Release Package (push) Has been cancelled
2026-05-05 20:58:13 +10:00
be315111ea UI updates and preroll buffer to 8 frames
Some checks failed
CI / Native Windows Build And Tests (push) Has been cancelled
CI / React UI Build (push) Has been cancelled
CI / Windows Release Package (push) Has been cancelled
2026-05-05 20:56:53 +10:00
298 changed files with 73565 additions and 11334 deletions

View File

@@ -7,32 +7,68 @@ on:
- master
- develop
pull_request:
schedule:
# Nightly build at 14:00 UTC, roughly midnight in Australia/Sydney.
- cron: "0 14 * * *"
workflow_dispatch:
jobs:
native-windows:
name: Native Windows Build And Tests
runs-on: windows-latest
runs-on: windows-2022
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Verify Visual Studio ATL
shell: powershell
run: |
$atlHeaders = @(Get-ChildItem -Path "${env:ProgramFiles(x86)}\Microsoft Visual Studio\2022\BuildTools\VC\Tools\MSVC" -Filter atlbase.h -Recurse -ErrorAction SilentlyContinue)
if ($atlHeaders.Count -eq 0) {
Write-Error "Visual Studio Build Tools is missing ATL. Install the 'C++ ATL for latest v143 build tools (x86 & x64)' component, component ID Microsoft.VisualStudio.Component.VC.ATL, then restart the runner service."
exit 1
}
Write-Host "Found ATL header: $($atlHeaders[0].FullName)"
- name: Configure Debug
shell: powershell
run: cmake --preset vs2022-x64-debug
run: |
$slangRoot = "${{ vars.SLANG_ROOT }}"
if ([string]::IsNullOrWhiteSpace($slangRoot)) {
$slangRoot = $env:SLANG_ROOT
}
if ([string]::IsNullOrWhiteSpace($slangRoot)) {
$slangRoot = Join-Path $PWD "3rdParty\slang-2026.8-windows-x86_64"
}
$requiredFiles = @(
(Join-Path $slangRoot "bin\slangc.exe"),
(Join-Path $slangRoot "bin\slang-compiler.dll"),
(Join-Path $slangRoot "bin\slang-glslang.dll"),
(Join-Path $slangRoot "LICENSE")
)
$missingFiles = @($requiredFiles | Where-Object { -not (Test-Path -LiteralPath $_) })
if ($missingFiles.Count -gt 0) {
Write-Error "Missing native third-party dependencies. Set Gitea repository variable SLANG_ROOT, or pre-populate the repo-local 3rdParty folder on the Windows runner. Missing: $($missingFiles -join ', ')"
exit 1
}
Write-Host "Using SLANG_ROOT=$slangRoot"
cmake --preset vs2022-x64-debug -DSLANG_ROOT="$slangRoot"
- name: Build Debug
shell: powershell
run: cmake --build --preset build-debug
run: cmake --build --preset build-debug --parallel
- name: Run Native Tests
- name: Run Native Tests And Shader Validation
shell: powershell
run: cmake --build --preset build-debug --target RUN_TESTS
run: cmake --build --preset build-debug --target RUN_TESTS --parallel
ui-ubuntu:
name: React UI Build
runs-on: nubuntu-latest
runs-on: ubuntu-latest
steps:
- name: Checkout
@@ -48,7 +84,8 @@ jobs:
package-windows:
name: Windows Release Package
runs-on: windows-latest
runs-on: windows-2022
if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch'
needs:
- native-windows
- ui-ubuntu
@@ -57,6 +94,16 @@ jobs:
- name: Checkout
uses: actions/checkout@v4
- name: Verify Visual Studio ATL
shell: powershell
run: |
$atlHeaders = @(Get-ChildItem -Path "${env:ProgramFiles(x86)}\Microsoft Visual Studio\2022\BuildTools\VC\Tools\MSVC" -Filter atlbase.h -Recurse -ErrorAction SilentlyContinue)
if ($atlHeaders.Count -eq 0) {
Write-Error "Visual Studio Build Tools is missing ATL. Install the 'C++ ATL for latest v143 build tools (x86 & x64)' component, component ID Microsoft.VisualStudio.Component.VC.ATL, then restart the runner service."
exit 1
}
Write-Host "Found ATL header: $($atlHeaders[0].FullName)"
- name: Build UI
shell: powershell
working-directory: ui
@@ -66,11 +113,34 @@ jobs:
- name: Configure Release
shell: powershell
run: cmake --preset vs2022-x64-release
run: |
$slangRoot = "${{ vars.SLANG_ROOT }}"
if ([string]::IsNullOrWhiteSpace($slangRoot)) {
$slangRoot = $env:SLANG_ROOT
}
if ([string]::IsNullOrWhiteSpace($slangRoot)) {
$slangRoot = Join-Path $PWD "3rdParty\slang-2026.8-windows-x86_64"
}
$requiredFiles = @(
(Join-Path $slangRoot "bin\slangc.exe"),
(Join-Path $slangRoot "bin\slang-compiler.dll"),
(Join-Path $slangRoot "bin\slang-glslang.dll"),
(Join-Path $slangRoot "LICENSE")
)
$missingFiles = @($requiredFiles | Where-Object { -not (Test-Path -LiteralPath $_) })
if ($missingFiles.Count -gt 0) {
Write-Error "Missing native third-party dependencies. Set Gitea repository variable SLANG_ROOT, or pre-populate the repo-local 3rdParty folder on the Windows runner. Missing: $($missingFiles -join ', ')"
exit 1
}
Write-Host "Using SLANG_ROOT=$slangRoot"
cmake --preset vs2022-x64-release -DSLANG_ROOT="$slangRoot"
- name: Build Release
shell: powershell
run: cmake --build --preset build-release
run: cmake --build --preset build-release --parallel
- name: Install Runtime Package
shell: powershell
@@ -81,7 +151,8 @@ jobs:
run: Compress-Archive -Path dist/VideoShader/* -DestinationPath dist/VideoShader.zip -Force
- name: Upload Runtime Package
uses: actions/upload-artifact@v4
# Gitea/GHES-compatible runners do not support the v4 artifact backend yet.
uses: actions/upload-artifact@v3
with:
name: VideoShader-windows-release
path: dist/VideoShader.zip

15
.vscode/launch.json vendored
View File

@@ -2,16 +2,21 @@
"version": "0.2.0",
"configurations": [
{
"name": "Debug LoopThroughWithOpenGLCompositing",
"name": "Debug RenderCadenceCompositor",
"type": "cppvsdbg",
"request": "launch",
"program": "${workspaceFolder}\\build\\vs2022-x64-debug\\Debug\\LoopThroughWithOpenGLCompositing.exe",
"program": "${workspaceFolder}\\build\\vs2022-x64-debug\\Debug\\RenderCadenceCompositor.exe",
"args": [],
"stopAtEntry": false,
"cwd": "${workspaceFolder}\\build\\vs2022-x64-debug\\Debug",
"cwd": "${workspaceFolder}",
"environment": [],
"console": "internalConsole",
"preLaunchTask": "Build LoopThroughWithOpenGLCompositing Debug x64"
"console": "externalTerminal",
"symbolSearchPath": "${workspaceFolder}\\build\\vs2022-x64-debug\\Debug",
"requireExactSource": true,
"logging": {
"moduleLoad": true
},
"preLaunchTask": "Build RenderCadenceCompositor Debug x64"
}
]
}

46
.vscode/tasks.json vendored
View File

@@ -2,42 +2,72 @@
"version": "2.0.0",
"tasks": [
{
"label": "Build LoopThroughWithOpenGLCompositing Debug x64",
"label": "Configure Debug x64",
"type": "process",
"command": "C:\\Program Files\\Microsoft Visual Studio\\2022\\Community\\Common7\\IDE\\CommonExtensions\\Microsoft\\CMake\\CMake\\bin\\cmake.exe",
"command": "cmake",
"args": [
"--preset",
"vs2022-x64-debug"
],
"problemMatcher": "$msCompile"
},
{
"label": "Build RenderCadenceCompositor Debug x64",
"type": "process",
"command": "cmake",
"args": [
"--build",
"${workspaceFolder}\\build\\vs2022-x64-debug",
"--config",
"Debug",
"--target",
"LoopThroughWithOpenGLCompositing"
"RenderCadenceCompositor",
"--parallel"
],
"group": {
"kind": "build",
"isDefault": true
},
"dependsOn": "Configure Debug x64",
"problemMatcher": "$msCompile"
},
{
"label": "Build LoopThroughWithOpenGLCompositing Release x64",
"label": "Build RenderCadenceCompositor Release x64",
"type": "process",
"command": "C:\\Program Files\\Microsoft Visual Studio\\2022\\Community\\Common7\\IDE\\CommonExtensions\\Microsoft\\CMake\\CMake\\bin\\cmake.exe",
"command": "cmake",
"args": [
"--build",
"${workspaceFolder}\\build\\vs2022-x64-release",
"--config",
"Release",
"--target",
"LoopThroughWithOpenGLCompositing"
"RenderCadenceCompositor",
"--parallel"
],
"group": "build",
"problemMatcher": "$msCompile"
},
{
"label": "Clean LoopThroughWithOpenGLCompositing Debug x64",
"label": "Run Native Tests Debug x64",
"type": "process",
"command": "C:\\Program Files\\Microsoft Visual Studio\\2022\\Community\\Common7\\IDE\\CommonExtensions\\Microsoft\\CMake\\CMake\\bin\\cmake.exe",
"command": "cmake",
"args": [
"--build",
"${workspaceFolder}\\build\\vs2022-x64-debug",
"--config",
"Debug",
"--target",
"RUN_TESTS",
"--parallel"
],
"group": "test",
"dependsOn": "Configure Debug x64",
"problemMatcher": "$msCompile"
},
{
"label": "Clean Debug x64",
"type": "process",
"command": "cmake",
"args": [
"--build",
"${workspaceFolder}\\build\\vs2022-x64-debug",

View File

@@ -1,180 +1,191 @@
cmake_minimum_required(VERSION 3.24)
project(video_shader LANGUAGES C CXX RC)
project(video_shader LANGUAGES C CXX)
include(CTest)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)
set(APP_DIR "${CMAKE_CURRENT_SOURCE_DIR}/apps/LoopThroughWithOpenGLCompositing")
set(GPUDIRECT_DIR "${CMAKE_CURRENT_SOURCE_DIR}/3rdParty/Blackmagic DeckLink SDK 16.0/Win/Samples/NVIDIA_GPUDirect" CACHE PATH "Path to the NVIDIA_GPUDirect sample directory from the Blackmagic DeckLink SDK")
set(SRC_DIR "${CMAKE_CURRENT_SOURCE_DIR}/src")
set(TEST_DIR "${CMAKE_CURRENT_SOURCE_DIR}/tests")
set(SLANG_ROOT "${CMAKE_CURRENT_SOURCE_DIR}/3rdParty/slang-2026.8-windows-x86_64" CACHE PATH "Path to a Slang binary release containing bin/slangc.exe")
set(MSDF_ATLAS_GEN_ROOT "${CMAKE_CURRENT_SOURCE_DIR}/3rdParty/msdf-atlas-gen" CACHE PATH "Path to msdf-atlas-gen binary release")
if(NOT EXISTS "${APP_DIR}/LoopThroughWithOpenGLCompositing.cpp")
message(FATAL_ERROR "Imported app sources were not found under ${APP_DIR}")
set(VIDEO_SHADER_INCLUDE_DIRS
"${SRC_DIR}"
"${SRC_DIR}/app"
"${SRC_DIR}/control"
"${SRC_DIR}/control/http"
"${SRC_DIR}/frames"
"${SRC_DIR}/json"
"${SRC_DIR}/logging"
"${SRC_DIR}/platform"
"${SRC_DIR}/preview"
"${SRC_DIR}/render"
"${SRC_DIR}/render/readback"
"${SRC_DIR}/render/runtime"
"${SRC_DIR}/runtime"
"${SRC_DIR}/shader"
"${SRC_DIR}/telemetry"
"${SRC_DIR}/video"
)
function(video_shader_target_defaults target)
target_include_directories(${target} PRIVATE ${VIDEO_SHADER_INCLUDE_DIRS})
target_compile_definitions(${target} PRIVATE _UNICODE UNICODE)
if(MSVC)
target_compile_options(${target} PRIVATE /W3)
endif()
endfunction()
function(video_shader_files_exist out_var)
set(missing_files)
foreach(file IN LISTS ARGN)
if(NOT EXISTS "${file}")
list(APPEND missing_files "${file}")
endif()
endforeach()
set(${out_var} "${missing_files}" PARENT_SCOPE)
endfunction()
function(add_video_shader_test name)
add_executable(${name} ${ARGN})
video_shader_target_defaults(${name})
add_test(NAME ${name} COMMAND ${name})
endfunction()
set(SLANG_RUNTIME_FILES
"${SLANG_ROOT}/bin/slangc.exe"
"${SLANG_ROOT}/bin/slang-compiler.dll"
"${SLANG_ROOT}/bin/slang-glslang.dll"
)
set(SLANG_LICENSE_FILE "${SLANG_ROOT}/LICENSE")
set(MSDF_ATLAS_GEN_EXE_FILE "${MSDF_ATLAS_GEN_ROOT}/msdf-atlas-gen.exe")
file(GLOB MSDF_ATLAS_GEN_DLL_FILES CONFIGURE_DEPENDS
"${MSDF_ATLAS_GEN_ROOT}/*.dll"
)
set(MSDF_ATLAS_GEN_RUNTIME_FILES
"${MSDF_ATLAS_GEN_EXE_FILE}"
${MSDF_ATLAS_GEN_DLL_FILES}
)
set(MSDF_ATLAS_GEN_LICENSE_FILE "${MSDF_ATLAS_GEN_ROOT}/LICENSE.txt")
set(MSDF_ATLAS_GEN_README_FILE "${MSDF_ATLAS_GEN_ROOT}/README.md")
set(RENDER_CADENCE_APP_REQUIRED_FILES
"${SRC_DIR}/RenderCadenceCompositor.cpp"
"${SRC_DIR}/video/DeckLinkAPI_i.c"
"${SRC_DIR}/video/DeckLinkAPI_h.h"
"${SRC_DIR}/video/DeckLinkDisplayMode.cpp"
"${SRC_DIR}/video/DeckLinkDisplayMode.h"
"${SRC_DIR}/video/DeckLinkSession.cpp"
"${SRC_DIR}/video/DeckLinkSession.h"
"${SRC_DIR}/video/DeckLinkVideoIOFormat.cpp"
"${SRC_DIR}/video/DeckLinkVideoIOFormat.h"
"${SRC_DIR}/video/VideoIOFormat.cpp"
"${SRC_DIR}/video/VideoIOFormat.h"
"${SRC_DIR}/video/VideoIOTypes.h"
"${SRC_DIR}/render/GLExtensions.cpp"
"${SRC_DIR}/render/GLExtensions.h"
"${SRC_DIR}/render/Std140Buffer.h"
"${SRC_DIR}/runtime/RuntimeJson.cpp"
"${SRC_DIR}/runtime/RuntimeJson.h"
"${SRC_DIR}/runtime/RuntimeParameterUtils.cpp"
"${SRC_DIR}/runtime/RuntimeParameterUtils.h"
"${SRC_DIR}/shader/ShaderCompiler.cpp"
"${SRC_DIR}/shader/ShaderCompiler.h"
"${SRC_DIR}/shader/ShaderPackageRegistry.cpp"
"${SRC_DIR}/shader/ShaderPackageRegistry.h"
"${SRC_DIR}/shader/ShaderTypes.h"
)
video_shader_files_exist(RENDER_CADENCE_APP_MISSING_FILES ${RENDER_CADENCE_APP_REQUIRED_FILES})
if(RENDER_CADENCE_APP_MISSING_FILES)
message(STATUS "RenderCadenceCompositor target skipped; source reorganization has not provided these legacy shared files:")
foreach(missing_file IN LISTS RENDER_CADENCE_APP_MISSING_FILES)
message(STATUS " ${missing_file}")
endforeach()
else()
file(GLOB_RECURSE RENDER_CADENCE_APP_SOURCES CONFIGURE_DEPENDS
"${SRC_DIR}/*.c"
"${SRC_DIR}/*.cpp"
"${SRC_DIR}/*.h"
)
list(REMOVE_ITEM RENDER_CADENCE_APP_SOURCES
"${SRC_DIR}/video/VideoBackend.cpp"
"${SRC_DIR}/video/VideoBackend.h"
)
add_executable(RenderCadenceCompositor ${RENDER_CADENCE_APP_SOURCES})
video_shader_target_defaults(RenderCadenceCompositor)
target_link_libraries(RenderCadenceCompositor PRIVATE
opengl32
Ole32
Windowscodecs
Ws2_32
)
source_group(TREE "${SRC_DIR}" FILES ${RENDER_CADENCE_APP_SOURCES})
endif()
if(NOT EXISTS "${GPUDIRECT_DIR}/lib/x64/dvp.lib")
message(FATAL_ERROR "NVIDIA GPUDirect library not found under ${GPUDIRECT_DIR}")
if(BUILD_TESTING)
add_subdirectory(tests)
endif()
set(APP_SOURCES
"${APP_DIR}/AudioSupport.cpp"
"${APP_DIR}/AudioSupport.h"
"${APP_DIR}/ControlServer.cpp"
"${APP_DIR}/ControlServer.h"
"${APP_DIR}/DeckLinkAPI_i.c"
"${APP_DIR}/GLExtensions.cpp"
"${APP_DIR}/GLExtensions.h"
"${APP_DIR}/LoopThroughWithOpenGLCompositing.cpp"
"${APP_DIR}/LoopThroughWithOpenGLCompositing.h"
"${APP_DIR}/LoopThroughWithOpenGLCompositing.rc"
"${APP_DIR}/NativeHandles.h"
"${APP_DIR}/NativeSockets.h"
"${APP_DIR}/OpenGLComposite.cpp"
"${APP_DIR}/OpenGLComposite.h"
"${APP_DIR}/OscServer.cpp"
"${APP_DIR}/OscServer.h"
"${APP_DIR}/resource.h"
"${APP_DIR}/RuntimeHost.cpp"
"${APP_DIR}/RuntimeHost.h"
"${APP_DIR}/RuntimeJson.cpp"
"${APP_DIR}/RuntimeJson.h"
"${APP_DIR}/RuntimeParameterUtils.cpp"
"${APP_DIR}/RuntimeParameterUtils.h"
"${APP_DIR}/ShaderCompiler.cpp"
"${APP_DIR}/ShaderCompiler.h"
"${APP_DIR}/ShaderPackageRegistry.cpp"
"${APP_DIR}/ShaderPackageRegistry.h"
"${APP_DIR}/ShaderTypes.h"
"${APP_DIR}/stdafx.cpp"
"${APP_DIR}/stdafx.h"
"${APP_DIR}/targetver.h"
"${APP_DIR}/VideoFrameTransfer.cpp"
"${APP_DIR}/VideoFrameTransfer.h"
)
add_executable(LoopThroughWithOpenGLCompositing WIN32 ${APP_SOURCES})
target_include_directories(LoopThroughWithOpenGLCompositing PRIVATE
"${APP_DIR}"
"${GPUDIRECT_DIR}/include"
)
target_link_directories(LoopThroughWithOpenGLCompositing PRIVATE
"${GPUDIRECT_DIR}/lib/x64"
)
target_link_libraries(LoopThroughWithOpenGLCompositing PRIVATE
dvp.lib
opengl32
glu32
Ws2_32
Crypt32
Advapi32
)
target_compile_definitions(LoopThroughWithOpenGLCompositing PRIVATE
_UNICODE
UNICODE
)
if(MSVC)
target_compile_options(LoopThroughWithOpenGLCompositing PRIVATE /W3)
if(TARGET RenderCadenceCompositor)
install(TARGETS RenderCadenceCompositor
RUNTIME DESTINATION "."
)
endif()
add_executable(RuntimeJsonTests
"${APP_DIR}/RuntimeJson.cpp"
"${CMAKE_CURRENT_SOURCE_DIR}/tests/RuntimeJsonTests.cpp"
)
foreach(slang_runtime_file IN LISTS SLANG_RUNTIME_FILES)
if(EXISTS "${slang_runtime_file}")
install(FILES "${slang_runtime_file}"
DESTINATION "3rdParty/slang/bin"
)
else()
message(STATUS "Slang runtime file not found and will not be installed: ${slang_runtime_file}")
endif()
endforeach()
target_include_directories(RuntimeJsonTests PRIVATE
"${APP_DIR}"
)
foreach(msdf_runtime_file IN LISTS MSDF_ATLAS_GEN_RUNTIME_FILES)
if(EXISTS "${msdf_runtime_file}")
install(FILES "${msdf_runtime_file}"
DESTINATION "3rdParty/msdf-atlas-gen"
)
else()
message(STATUS "msdf-atlas-gen runtime file not found and will not be installed: ${msdf_runtime_file}")
endif()
endforeach()
if(MSVC)
target_compile_options(RuntimeJsonTests PRIVATE /W3)
if(EXISTS "${MSDF_ATLAS_GEN_LICENSE_FILE}")
install(FILES "${MSDF_ATLAS_GEN_LICENSE_FILE}"
DESTINATION "third_party_notices"
RENAME "MSDF_ATLAS_GEN_LICENSE.txt"
)
else()
message(STATUS "msdf-atlas-gen license file not found: ${MSDF_ATLAS_GEN_LICENSE_FILE}")
endif()
enable_testing()
add_test(NAME RuntimeJsonTests COMMAND RuntimeJsonTests)
add_executable(RuntimeParameterUtilsTests
"${APP_DIR}/RuntimeJson.cpp"
"${APP_DIR}/RuntimeParameterUtils.cpp"
"${CMAKE_CURRENT_SOURCE_DIR}/tests/RuntimeParameterUtilsTests.cpp"
)
target_include_directories(RuntimeParameterUtilsTests PRIVATE
"${APP_DIR}"
)
if(MSVC)
target_compile_options(RuntimeParameterUtilsTests PRIVATE /W3)
if(EXISTS "${MSDF_ATLAS_GEN_README_FILE}")
install(FILES "${MSDF_ATLAS_GEN_README_FILE}"
DESTINATION "third_party_notices"
RENAME "MSDF_ATLAS_GEN_README.md"
)
else()
message(STATUS "msdf-atlas-gen readme file not found: ${MSDF_ATLAS_GEN_README_FILE}")
endif()
add_test(NAME RuntimeParameterUtilsTests COMMAND RuntimeParameterUtilsTests)
add_executable(ShaderPackageRegistryTests
"${APP_DIR}/RuntimeJson.cpp"
"${APP_DIR}/ShaderPackageRegistry.cpp"
"${CMAKE_CURRENT_SOURCE_DIR}/tests/ShaderPackageRegistryTests.cpp"
)
target_include_directories(ShaderPackageRegistryTests PRIVATE
"${APP_DIR}"
)
if(MSVC)
target_compile_options(ShaderPackageRegistryTests PRIVATE /W3)
if(EXISTS "${SLANG_LICENSE_FILE}")
install(FILES "${SLANG_LICENSE_FILE}"
DESTINATION "third_party_notices"
RENAME "SLANG_LICENSE.txt"
)
else()
message(STATUS "Slang license file not found and will not be installed: ${SLANG_LICENSE_FILE}")
endif()
add_test(NAME ShaderPackageRegistryTests COMMAND ShaderPackageRegistryTests)
add_executable(OscServerTests
"${APP_DIR}/OscServer.cpp"
"${CMAKE_CURRENT_SOURCE_DIR}/tests/OscServerTests.cpp"
)
target_include_directories(OscServerTests PRIVATE
"${APP_DIR}"
)
target_link_libraries(OscServerTests PRIVATE
Ws2_32
)
if(MSVC)
target_compile_options(OscServerTests PRIVATE /W3)
endif()
add_test(NAME OscServerTests COMMAND OscServerTests)
add_executable(AudioSupportTests
"${APP_DIR}/AudioSupport.cpp"
"${CMAKE_CURRENT_SOURCE_DIR}/tests/AudioSupportTests.cpp"
)
target_include_directories(AudioSupportTests PRIVATE
"${APP_DIR}"
)
if(MSVC)
target_compile_options(AudioSupportTests PRIVATE /W3)
endif()
add_test(NAME AudioSupportTests COMMAND AudioSupportTests)
add_custom_command(TARGET LoopThroughWithOpenGLCompositing POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy_if_different
"${GPUDIRECT_DIR}/bin/x64/dvp.dll"
"$<TARGET_FILE_DIR:LoopThroughWithOpenGLCompositing>/dvp.dll"
)
install(TARGETS LoopThroughWithOpenGLCompositing
RUNTIME DESTINATION "."
)
install(FILES "${GPUDIRECT_DIR}/bin/x64/dvp.dll"
install(FILES "${CMAKE_CURRENT_SOURCE_DIR}/shaders/SHADER_CONTRACT.md"
DESTINATION "."
)
@@ -203,5 +214,3 @@ install(DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/docs/"
DESTINATION "docs"
OPTIONAL
)
source_group(TREE "${APP_DIR}" FILES ${APP_SOURCES})

674
LICENSE Normal file
View File

@@ -0,0 +1,674 @@
GNU GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The GNU General Public License is a free, copyleft license for
software and other kinds of works.
The licenses for most software and other practical works are designed
to take away your freedom to share and change the works. By contrast,
the GNU General Public License is intended to guarantee your freedom to
share and change all versions of a program--to make sure it remains free
software for all its users. We, the Free Software Foundation, use the
GNU General Public License for most of our software; it applies also to
any other work released this way by its authors. You can apply it to
your programs, too.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
them if you wish), that you receive source code or can get it if you
want it, that you can change the software or use pieces of it in new
free programs, and that you know you can do these things.
To protect your rights, we need to prevent others from denying you
these rights or asking you to surrender the rights. Therefore, you have
certain responsibilities if you distribute copies of the software, or if
you modify it: responsibilities to respect the freedom of others.
For example, if you distribute copies of such a program, whether
gratis or for a fee, you must pass on to the recipients the same
freedoms that you received. You must make sure that they, too, receive
or can get the source code. And you must show them these terms so they
know their rights.
Developers that use the GNU GPL protect your rights with two steps:
(1) assert copyright on the software, and (2) offer you this License
giving you legal permission to copy, distribute and/or modify it.
For the developers' and authors' protection, the GPL clearly explains
that there is no warranty for this free software. For both users' and
authors' sake, the GPL requires that modified versions be marked as
changed, so that their problems will not be attributed erroneously to
authors of previous versions.
Some devices are designed to deny users access to install or run
modified versions of the software inside them, although the manufacturer
can do so. This is fundamentally incompatible with the aim of
protecting users' freedom to change the software. The systematic
pattern of such abuse occurs in the area of products for individuals to
use, which is precisely where it is most unacceptable. Therefore, we
have designed this version of the GPL to prohibit the practice for those
products. If such problems arise substantially in other domains, we
stand ready to extend this provision to those domains in future versions
of the GPL, as needed to protect the freedom of users.
Finally, every program is threatened constantly by software patents.
States should not allow patents to restrict development and use of
software on general-purpose computers, but in those that do, we wish to
avoid the special danger that patents applied to a free program could
make it effectively proprietary. To prevent this, the GPL assures that
patents cannot be used to render the program non-free.
The precise terms and conditions for copying, distribution and
modification follow.
TERMS AND CONDITIONS
0. Definitions.
"This License" refers to version 3 of the GNU General Public License.
"Copyright" also means copyright-like laws that apply to other kinds of
works, such as semiconductor masks.
"The Program" refers to any copyrightable work licensed under this
License. Each licensee is addressed as "you". "Licensees" and
"recipients" may be individuals or organizations.
To "modify" a work means to copy from or adapt all or part of the work
in a fashion requiring copyright permission, other than the making of an
exact copy. The resulting work is called a "modified version" of the
earlier work or a work "based on" the earlier work.
A "covered work" means either the unmodified Program or a work based
on the Program.
To "propagate" a work means to do anything with it that, without
permission, would make you directly or secondarily liable for
infringement under applicable copyright law, except executing it on a
computer or modifying a private copy. Propagation includes copying,
distribution (with or without modification), making available to the
public, and in some countries other activities as well.
To "convey" a work means any kind of propagation that enables other
parties to make or receive copies. Mere interaction with a user through
a computer network, with no transfer of a copy, is not conveying.
An interactive user interface displays "Appropriate Legal Notices"
to the extent that it includes a convenient and prominently visible
feature that (1) displays an appropriate copyright notice, and (2)
tells the user that there is no warranty for the work (except to the
extent that warranties are provided), that licensees may convey the
work under this License, and how to view a copy of this License. If
the interface presents a list of user commands or options, such as a
menu, a prominent item in the list meets this criterion.
1. Source Code.
The "source code" for a work means the preferred form of the work
for making modifications to it. "Object code" means any non-source
form of a work.
A "Standard Interface" means an interface that either is an official
standard defined by a recognized standards body, or, in the case of
interfaces specified for a particular programming language, one that
is widely used among developers working in that language.
The "System Libraries" of an executable work include anything, other
than the work as a whole, that (a) is included in the normal form of
packaging a Major Component, but which is not part of that Major
Component, and (b) serves only to enable use of the work with that
Major Component, or to implement a Standard Interface for which an
implementation is available to the public in source code form. A
"Major Component", in this context, means a major essential component
(kernel, window system, and so on) of the specific operating system
(if any) on which the executable work runs, or a compiler used to
produce the work, or an object code interpreter used to run it.
The "Corresponding Source" for a work in object code form means all
the source code needed to generate, install, and (for an executable
work) run the object code and to modify the work, including scripts to
control those activities. However, it does not include the work's
System Libraries, or general-purpose tools or generally available free
programs which are used unmodified in performing those activities but
which are not part of the work. For example, Corresponding Source
includes interface definition files associated with source files for
the work, and the source code for shared libraries and dynamically
linked subprograms that the work is specifically designed to require,
such as by intimate data communication or control flow between those
subprograms and other parts of the work.
The Corresponding Source need not include anything that users
can regenerate automatically from other parts of the Corresponding
Source.
The Corresponding Source for a work in source code form is that
same work.
2. Basic Permissions.
All rights granted under this License are granted for the term of
copyright on the Program, and are irrevocable provided the stated
conditions are met. This License explicitly affirms your unlimited
permission to run the unmodified Program. The output from running a
covered work is covered by this License only if the output, given its
content, constitutes a covered work. This License acknowledges your
rights of fair use or other equivalent, as provided by copyright law.
You may make, run and propagate covered works that you do not
convey, without conditions so long as your license otherwise remains
in force. You may convey covered works to others for the sole purpose
of having them make modifications exclusively for you, or provide you
with facilities for running those works, provided that you comply with
the terms of this License in conveying all material for which you do
not control copyright. Those thus making or running the covered works
for you must do so exclusively on your behalf, under your direction
and control, on terms that prohibit them from making any copies of
your copyrighted material outside their relationship with you.
Conveying under any other circumstances is permitted solely under
the conditions stated below. Sublicensing is not allowed; section 10
makes it unnecessary.
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
No covered work shall be deemed part of an effective technological
measure under any applicable law fulfilling obligations under article
11 of the WIPO copyright treaty adopted on 20 December 1996, or
similar laws prohibiting or restricting circumvention of such
measures.
When you convey a covered work, you waive any legal power to forbid
circumvention of technological measures to the extent such circumvention
is effected by exercising rights under this License with respect to
the covered work, and you disclaim any intention to limit operation or
modification of the work as a means of enforcing, against the work's
users, your or third parties' legal rights to forbid circumvention of
technological measures.
4. Conveying Verbatim Copies.
You may convey verbatim copies of the Program's source code as you
receive it, in any medium, provided that you conspicuously and
appropriately publish on each copy an appropriate copyright notice;
keep intact all notices stating that this License and any
non-permissive terms added in accord with section 7 apply to the code;
keep intact all notices of the absence of any warranty; and give all
recipients a copy of this License along with the Program.
You may charge any price or no price for each copy that you convey,
and you may offer support or warranty protection for a fee.
5. Conveying Modified Source Versions.
You may convey a work based on the Program, or the modifications to
produce it from the Program, in the form of source code under the
terms of section 4, provided that you also meet all of these conditions:
a) The work must carry prominent notices stating that you modified
it, and giving a relevant date.
b) The work must carry prominent notices stating that it is
released under this License and any conditions added under section
7. This requirement modifies the requirement in section 4 to
"keep intact all notices".
c) You must license the entire work, as a whole, under this
License to anyone who comes into possession of a copy. This
License will therefore apply, along with any applicable section 7
additional terms, to the whole of the work, and all its parts,
regardless of how they are packaged. This License gives no
permission to license the work in any other way, but it does not
invalidate such permission if you have separately received it.
d) If the work has interactive user interfaces, each must display
Appropriate Legal Notices; however, if the Program has interactive
interfaces that do not display Appropriate Legal Notices, your
work need not make them do so.
A compilation of a covered work with other separate and independent
works, which are not by their nature extensions of the covered work,
and which are not combined with it such as to form a larger program,
in or on a volume of a storage or distribution medium, is called an
"aggregate" if the compilation and its resulting copyright are not
used to limit the access or legal rights of the compilation's users
beyond what the individual works permit. Inclusion of a covered work
in an aggregate does not cause this License to apply to the other
parts of the aggregate.
6. Conveying Non-Source Forms.
You may convey a covered work in object code form under the terms
of sections 4 and 5, provided that you also convey the
machine-readable Corresponding Source under the terms of this License,
in one of these ways:
a) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by the
Corresponding Source fixed on a durable physical medium
customarily used for software interchange.
b) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by a
written offer, valid for at least three years and valid for as
long as you offer spare parts or customer support for that product
model, to give anyone who possesses the object code either (1) a
copy of the Corresponding Source for all the software in the
product that is covered by this License, on a durable physical
medium customarily used for software interchange, for a price no
more than your reasonable cost of physically performing this
conveying of source, or (2) access to copy the
Corresponding Source from a network server at no charge.
c) Convey individual copies of the object code with a copy of the
written offer to provide the Corresponding Source. This
alternative is allowed only occasionally and noncommercially, and
only if you received the object code with such an offer, in accord
with subsection 6b.
d) Convey the object code by offering access from a designated
place (gratis or for a charge), and offer equivalent access to the
Corresponding Source in the same way through the same place at no
further charge. You need not require recipients to copy the
Corresponding Source along with the object code. If the place to
copy the object code is a network server, the Corresponding Source
may be on a different server (operated by you or a third party)
that supports equivalent copying facilities, provided you maintain
clear directions next to the object code saying where to find the
Corresponding Source. Regardless of what server hosts the
Corresponding Source, you remain obligated to ensure that it is
available for as long as needed to satisfy these requirements.
e) Convey the object code using peer-to-peer transmission, provided
you inform other peers where the object code and Corresponding
Source of the work are being offered to the general public at no
charge under subsection 6d.
A separable portion of the object code, whose source code is excluded
from the Corresponding Source as a System Library, need not be
included in conveying the object code work.
A "User Product" is either (1) a "consumer product", which means any
tangible personal property which is normally used for personal, family,
or household purposes, or (2) anything designed or sold for incorporation
into a dwelling. In determining whether a product is a consumer product,
doubtful cases shall be resolved in favor of coverage. For a particular
product received by a particular user, "normally used" refers to a
typical or common use of that class of product, regardless of the status
of the particular user or of the way in which the particular user
actually uses, or expects or is expected to use, the product. A product
is a consumer product regardless of whether the product has substantial
commercial, industrial or non-consumer uses, unless such uses represent
the only significant mode of use of the product.
"Installation Information" for a User Product means any methods,
procedures, authorization keys, or other information required to install
and execute modified versions of a covered work in that User Product from
a modified version of its Corresponding Source. The information must
suffice to ensure that the continued functioning of the modified object
code is in no case prevented or interfered with solely because
modification has been made.
If you convey an object code work under this section in, or with, or
specifically for use in, a User Product, and the conveying occurs as
part of a transaction in which the right of possession and use of the
User Product is transferred to the recipient in perpetuity or for a
fixed term (regardless of how the transaction is characterized), the
Corresponding Source conveyed under this section must be accompanied
by the Installation Information. But this requirement does not apply
if neither you nor any third party retains the ability to install
modified object code on the User Product (for example, the work has
been installed in ROM).
The requirement to provide Installation Information does not include a
requirement to continue to provide support service, warranty, or updates
for a work that has been modified or installed by the recipient, or for
the User Product in which it has been modified or installed. Access to a
network may be denied when the modification itself materially and
adversely affects the operation of the network or violates the rules and
protocols for communication across the network.
Corresponding Source conveyed, and Installation Information provided,
in accord with this section must be in a format that is publicly
documented (and with an implementation available to the public in
source code form), and must require no special password or key for
unpacking, reading or copying.
7. Additional Terms.
"Additional permissions" are terms that supplement the terms of this
License by making exceptions from one or more of its conditions.
Additional permissions that are applicable to the entire Program shall
be treated as though they were included in this License, to the extent
that they are valid under applicable law. If additional permissions
apply only to part of the Program, that part may be used separately
under those permissions, but the entire Program remains governed by
this License without regard to the additional permissions.
When you convey a copy of a covered work, you may at your option
remove any additional permissions from that copy, or from any part of
it. (Additional permissions may be written to require their own
removal in certain cases when you modify the work.) You may place
additional permissions on material, added by you to a covered work,
for which you have or can give appropriate copyright permission.
Notwithstanding any other provision of this License, for material you
add to a covered work, you may (if authorized by the copyright holders of
that material) supplement the terms of this License with terms:
a) Disclaiming warranty or limiting liability differently from the
terms of sections 15 and 16 of this License; or
b) Requiring preservation of specified reasonable legal notices or
author attributions in that material or in the Appropriate Legal
Notices displayed by works containing it; or
c) Prohibiting misrepresentation of the origin of that material, or
requiring that modified versions of such material be marked in
reasonable ways as different from the original version; or
d) Limiting the use for publicity purposes of names of licensors or
authors of the material; or
e) Declining to grant rights under trademark law for use of some
trade names, trademarks, or service marks; or
f) Requiring indemnification of licensors and authors of that
material by anyone who conveys the material (or modified versions of
it) with contractual assumptions of liability to the recipient, for
any liability that these contractual assumptions directly impose on
those licensors and authors.
All other non-permissive additional terms are considered "further
restrictions" within the meaning of section 10. If the Program as you
received it, or any part of it, contains a notice stating that it is
governed by this License along with a term that is a further
restriction, you may remove that term. If a license document contains
a further restriction but permits relicensing or conveying under this
License, you may add to a covered work material governed by the terms
of that license document, provided that the further restriction does
not survive such relicensing or conveying.
If you add terms to a covered work in accord with this section, you
must place, in the relevant source files, a statement of the
additional terms that apply to those files, or a notice indicating
where to find the applicable terms.
Additional terms, permissive or non-permissive, may be stated in the
form of a separately written license, or stated as exceptions;
the above requirements apply either way.
8. Termination.
You may not propagate or modify a covered work except as expressly
provided under this License. Any attempt otherwise to propagate or
modify it is void, and will automatically terminate your rights under
this License (including any patent licenses granted under the third
paragraph of section 11).
However, if you cease all violation of this License, then your
license from a particular copyright holder is reinstated (a)
provisionally, unless and until the copyright holder explicitly and
finally terminates your license, and (b) permanently, if the copyright
holder fails to notify you of the violation by some reasonable means
prior to 60 days after the cessation.
Moreover, your license from a particular copyright holder is
reinstated permanently if the copyright holder notifies you of the
violation by some reasonable means, this is the first time you have
received notice of violation of this License (for any work) from that
copyright holder, and you cure the violation prior to 30 days after
your receipt of the notice.
Termination of your rights under this section does not terminate the
licenses of parties who have received copies or rights from you under
this License. If your rights have been terminated and not permanently
reinstated, you do not qualify to receive new licenses for the same
material under section 10.
9. Acceptance Not Required for Having Copies.
You are not required to accept this License in order to receive or
run a copy of the Program. Ancillary propagation of a covered work
occurring solely as a consequence of using peer-to-peer transmission
to receive a copy likewise does not require acceptance. However,
nothing other than this License grants you permission to propagate or
modify any covered work. These actions infringe copyright if you do
not accept this License. Therefore, by modifying or propagating a
covered work, you indicate your acceptance of this License to do so.
10. Automatic Licensing of Downstream Recipients.
Each time you convey a covered work, the recipient automatically
receives a license from the original licensors, to run, modify and
propagate that work, subject to this License. You are not responsible
for enforcing compliance by third parties with this License.
An "entity transaction" is a transaction transferring control of an
organization, or substantially all assets of one, or subdividing an
organization, or merging organizations. If propagation of a covered
work results from an entity transaction, each party to that
transaction who receives a copy of the work also receives whatever
licenses to the work the party's predecessor in interest had or could
give under the previous paragraph, plus a right to possession of the
Corresponding Source of the work from the predecessor in interest, if
the predecessor has it or can get it with reasonable efforts.
You may not impose any further restrictions on the exercise of the
rights granted or affirmed under this License. For example, you may
not impose a license fee, royalty, or other charge for exercise of
rights granted under this License, and you may not initiate litigation
(including a cross-claim or counterclaim in a lawsuit) alleging that
any patent claim is infringed by making, using, selling, offering for
sale, or importing the Program or any portion of it.
11. Patents.
A "contributor" is a copyright holder who authorizes use under this
License of the Program or a work on which the Program is based. The
work thus licensed is called the contributor's "contributor version".
A contributor's "essential patent claims" are all patent claims
owned or controlled by the contributor, whether already acquired or
hereafter acquired, that would be infringed by some manner, permitted
by this License, of making, using, or selling its contributor version,
but do not include claims that would be infringed only as a
consequence of further modification of the contributor version. For
purposes of this definition, "control" includes the right to grant
patent sublicenses in a manner consistent with the requirements of
this License.
Each contributor grants you a non-exclusive, worldwide, royalty-free
patent license under the contributor's essential patent claims, to
make, use, sell, offer for sale, import and otherwise run, modify and
propagate the contents of its contributor version.
In the following three paragraphs, a "patent license" is any express
agreement or commitment, however denominated, not to enforce a patent
(such as an express permission to practice a patent or covenant not to
sue for patent infringement). To "grant" such a patent license to a
party means to make such an agreement or commitment not to enforce a
patent against the party.
If you convey a covered work, knowingly relying on a patent license,
and the Corresponding Source of the work is not available for anyone
to copy, free of charge and under the terms of this License, through a
publicly available network server or other readily accessible means,
then you must either (1) cause the Corresponding Source to be so
available, or (2) arrange to deprive yourself of the benefit of the
patent license for this particular work, or (3) arrange, in a manner
consistent with the requirements of this License, to extend the patent
license to downstream recipients. "Knowingly relying" means you have
actual knowledge that, but for the patent license, your conveying the
covered work in a country, or your recipient's use of the covered work
in a country, would infringe one or more identifiable patents in that
country that you have reason to believe are valid.
If, pursuant to or in connection with a single transaction or
arrangement, you convey, or propagate by procuring conveyance of, a
covered work, and grant a patent license to some of the parties
receiving the covered work authorizing them to use, propagate, modify
or convey a specific copy of the covered work, then the patent license
you grant is automatically extended to all recipients of the covered
work and works based on it.
A patent license is "discriminatory" if it does not include within
the scope of its coverage, prohibits the exercise of, or is
conditioned on the non-exercise of one or more of the rights that are
specifically granted under this License. You may not convey a covered
work if you are a party to an arrangement with a third party that is
in the business of distributing software, under which you make payment
to the third party based on the extent of your activity of conveying
the work, and under which the third party grants, to any of the
parties who would receive the covered work from you, a discriminatory
patent license (a) in connection with copies of the covered work
conveyed by you (or copies made from those copies), or (b) primarily
for and in connection with specific products or compilations that
contain the covered work, unless you entered into that arrangement,
or that patent license was granted, prior to 28 March 2007.
Nothing in this License shall be construed as excluding or limiting
any implied license or other defenses to infringement that may
otherwise be available to you under applicable patent law.
12. No Surrender of Others' Freedom.
If conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot convey a
covered work so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you may
not convey it at all. For example, if you agree to terms that obligate you
to collect a royalty for further conveying from those to whom you convey
the Program, the only way you could satisfy both those terms and this
License would be to refrain entirely from conveying the Program.
13. Use with the GNU Affero General Public License.
Notwithstanding any other provision of this License, you have
permission to link or combine any covered work with a work licensed
under version 3 of the GNU Affero General Public License into a single
combined work, and to convey the resulting work. The terms of this
License will continue to apply to the part which is the covered work,
but the special requirements of the GNU Affero General Public License,
section 13, concerning interaction through a network will apply to the
combination as such.
14. Revised Versions of this License.
The Free Software Foundation may publish revised and/or new versions of
the GNU General Public License from time to time. Such new versions will
be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the
Program specifies that a certain numbered version of the GNU General
Public License "or any later version" applies to it, you have the
option of following the terms and conditions either of that numbered
version or of any later version published by the Free Software
Foundation. If the Program does not specify a version number of the
GNU General Public License, you may choose any version ever published
by the Free Software Foundation.
If the Program specifies that a proxy can decide which future
versions of the GNU General Public License can be used, that proxy's
public statement of acceptance of a version permanently authorizes you
to choose that version for the Program.
Later license versions may give you additional or different
permissions. However, no additional obligations are imposed on any
author or copyright holder as a result of your choosing to follow a
later version.
15. Disclaimer of Warranty.
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
16. Limitation of Liability.
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
SUCH DAMAGES.
17. Interpretation of Sections 15 and 16.
If the disclaimer of warranty and limitation of liability provided
above cannot be given local legal effect according to their terms,
reviewing courts shall apply local law that most closely approximates
an absolute waiver of all civil liability in connection with the
Program, unless a warranty or assumption of liability accompanies a
copy of the Program in return for a fee.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
state the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
<one line to give the program's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
Also add information on how to contact you by electronic and paper mail.
If the program does terminal interaction, make it output a short
notice like this when it starts in an interactive mode:
<program> Copyright (C) <year> <name of author>
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it
under certain conditions; type `show c' for details.
The hypothetical commands `show w' and `show c' should show the appropriate
parts of the General Public License. Of course, your program's commands
might be different; for a GUI interface, you would use an "about box".
You should also get your employer (if you work as a programmer) or school,
if any, to sign a "copyright disclaimer" for the program, if necessary.
For more information on this, and how to apply and follow the GNU GPL, see
<https://www.gnu.org/licenses/>.
The GNU General Public License does not permit incorporating your program
into proprietary programs. If your program is a subroutine library, you
may consider it more useful to permit linking proprietary applications with
the library. If this is what you want to do, use the GNU Lesser General
Public License instead of this License. But first, please read
<https://www.gnu.org/licenses/why-not-lgpl.html>.

View File

@@ -36,7 +36,7 @@
"preArgs": "",
"typeTags": "",
"decimals": 2,
"target": "127.0.0.1:9000",
"target": "192.168.1.46:9000",
"ignoreDefaults": false,
"bypass": false,
"onCreate": "",
@@ -53,8 +53,8 @@
"visible": true,
"interaction": true,
"comments": "XY control for Fisheye Reproject pan and tilt.",
"width": 420,
"height": 420,
"width": 460,
"height": 250,
"expand": false,
"colorText": "auto",
"colorWidget": "auto",
@@ -70,14 +70,14 @@
"css": "",
"pips": true,
"snap": false,
"spring": false,
"spring": true,
"rangeX": {
"min": -60,
"max": 60
"min": -1,
"max": 1
},
"rangeY": {
"min": 45,
"max": -45
"min": 1,
"max": -1
},
"logScaleX": false,
"logScaleY": false,
@@ -94,13 +94,13 @@
"address": "/VideoShaderToys/fisheye-reproject/xy",
"preArgs": "",
"typeTags": "",
"decimals": "2f",
"target": "127.0.0.1:9000",
"decimals": "3f",
"target": "192.168.1.46:9000",
"ignoreDefaults": false,
"bypass": true,
"onCreate": "",
"onValue": "var pan = Array.isArray(value) ? Number(value[0]) : 0;\nvar tilt = Array.isArray(value) ? Number(value[1]) : 0;\nsend('127.0.0.1:9000', '/VideoShaderToys/fisheye-reproject/panDegrees', {type: 'f', value: pan});\nsend('127.0.0.1:9000', '/VideoShaderToys/fisheye-reproject/tiltDegrees', {type: 'f', value: tilt});",
"onTouch": "",
"onCreate": "var state = globalThis.__fisheyePanTiltStick = globalThis.__fisheyePanTiltStick || {};\nstate.target = '192.168.1.46:9000';\nstate.panAddress = '/VideoShaderToys/fisheye-reproject/panDegrees';\nstate.tiltAddress = '/VideoShaderToys/fisheye-reproject/tiltDegrees';\nstate.minPan = -60;\nstate.maxPan = 60;\nstate.minTilt = -45;\nstate.maxTilt = 45;\nstate.pan = 0;\nstate.tilt = 0;\nstate.stickX = 0;\nstate.stickY = 0;\nstate.tickMs = 16;\nstate.stepPan = 0.75;\nstate.stepTilt = 0.75;\nstate.deadzone = 0.14;\nstate.applyCurve = function(input) {\n var amount = Math.abs(input);\n if (amount <= state.deadzone) {\n return 0;\n }\n var normalized = (amount - state.deadzone) / (1 - state.deadzone);\n var softened = normalized * normalized * (3 - (2 * normalized));\n return (input < 0 ? -1 : 1) * softened;\n};\nif (state.timer) {\n clearInterval(state.timer);\n state.timer = null;\n}",
"onValue": "var state = globalThis.__fisheyePanTiltStick = globalThis.__fisheyePanTiltStick || {};\nvar stickX = Array.isArray(value) ? Number(value[0]) : 0;\nvar stickY = Array.isArray(value) ? Number(value[1]) : 0;\nstate.stickX = isFinite(stickX) ? state.applyCurve(stickX) : 0;\nstate.stickY = isFinite(stickY) ? state.applyCurve(stickY) : 0;",
"onTouch": "var state = globalThis.__fisheyePanTiltStick = globalThis.__fisheyePanTiltStick || {};\nif (value) {\n if (!state.timer) {\n state.timer = setInterval(function() {\n if (!state.stickX && !state.stickY) {\n return;\n }\n state.pan = Math.max(state.minPan, Math.min(state.maxPan, state.pan + (state.stickX * state.stepPan)));\n state.tilt = Math.max(state.minTilt, Math.min(state.maxTilt, state.tilt + (state.stickY * state.stepTilt)));\n send(state.target, state.panAddress, {type: 'f', value: state.pan});\n send(state.target, state.tiltAddress, {type: 'f', value: state.tilt});\n }, state.tickMs);\n }\n} else {\n state.stickX = 0;\n state.stickY = 0;\n if (state.timer) {\n clearInterval(state.timer);\n state.timer = null;\n }\n}",
"pointSize": 20,
"ephemeral": false,
"label": "",
@@ -121,7 +121,7 @@
"interaction": true,
"comments": "",
"width": 90,
"height": 420,
"height": 250,
"expand": false,
"colorText": "auto",
"colorWidget": "auto",
@@ -144,90 +144,29 @@
"gradient": [],
"snap": false,
"touchZone": "all",
"spring": false,
"spring": true,
"doubleTap": false,
"range": {
"min": 100,
"max": 10
"min": -1,
"max": 1
},
"logScale": false,
"sensitivity": 1,
"steps": "",
"origin": "auto",
"value": "",
"default": 90,
"value": 0,
"default": 0,
"linkId": "",
"address": "/VideoShaderToys/fisheye-reproject/virtualFovDegrees",
"preArgs": "",
"typeTags": "",
"decimals": 2,
"target": "127.0.0.1:9000",
"ignoreDefaults": false,
"bypass": false,
"onCreate": "",
"onValue": "",
"onTouch": ""
},
{
"type": "xy",
"top": 700,
"left": 190,
"lock": false,
"id": "Pan Pad",
"visible": true,
"interaction": true,
"comments": "",
"width": "auto",
"height": "auto",
"expand": false,
"colorText": "auto",
"colorWidget": "auto",
"colorStroke": "auto",
"colorFill": "auto",
"alphaStroke": "auto",
"alphaFillOff": "auto",
"alphaFillOn": "auto",
"lineWidth": "auto",
"borderRadius": "auto",
"padding": "auto",
"html": "",
"css": "",
"pointSize": 20,
"ephemeral": false,
"pips": true,
"label": "",
"snap": false,
"spring": false,
"rangeX": {
"min": -1,
"max": 1
},
"rangeY": {
"min": -1,
"max": 1
},
"logScaleX": false,
"logScaleY": false,
"stepsX": false,
"stepsY": false,
"clipX": "",
"clipY": "",
"axisLock": "",
"doubleTap": false,
"sensitivity": 1,
"value": "",
"default": "",
"linkId": "",
"address": "/VideoShaderToys/video-transform/pan",
"preArgs": "",
"typeTags": "",
"decimals": 2,
"target": "",
"decimals": "3f",
"target": "192.168.1.46:9000",
"ignoreDefaults": false,
"bypass": true,
"onCreate": "",
"onValue": "var x = Array.isArray(value) ? Number(value[0]) : 0;\nvar y = Array.isArray(value) ? Number(value[1]) : 0;\nsend('127.0.0.1:9000', '/VideoShaderToys/video-transform/pan', {type: 'f', value: x}, {type: 'f', value: y});",
"onTouch": ""
"onCreate": "var state = globalThis.__fisheyeFovStick = globalThis.__fisheyeFovStick || {};\nstate.target = '192.168.1.46:9000';\nstate.address = '/VideoShaderToys/fisheye-reproject/virtualFovDegrees';\nstate.minFov = 10;\nstate.maxFov = 100;\nstate.fov = 90;\nstate.stick = 0;\nstate.tickMs = 16;\nstate.stepFov = 0.6;\nstate.deadzone = 0.14;\nstate.applyCurve = function(input) {\n var amount = Math.abs(input);\n if (amount <= state.deadzone) {\n return 0;\n }\n var normalized = (amount - state.deadzone) / (1 - state.deadzone);\n var softened = normalized * normalized * (3 - (2 * normalized));\n return (input < 0 ? -1 : 1) * softened;\n};\nif (state.timer) {\n clearInterval(state.timer);\n state.timer = null;\n}",
"onValue": "var state = globalThis.__fisheyeFovStick = globalThis.__fisheyeFovStick || {};\nvar stick = Number(value);\nstate.stick = isFinite(stick) ? state.applyCurve(stick) : 0;",
"onTouch": "var state = globalThis.__fisheyeFovStick = globalThis.__fisheyeFovStick || {};\nif (value) {\n if (!state.timer) {\n state.timer = setInterval(function() {\n if (!state.stick) {\n return;\n }\n state.fov = Math.max(state.minFov, Math.min(state.maxFov, state.fov - (state.stick * state.stepFov)));\n send(state.target, state.address, {type: 'f', value: state.fov});\n }, state.tickMs);\n }\n} else {\n state.stick = 0;\n if (state.timer) {\n clearInterval(state.timer);\n state.timer = null;\n }\n}"
}
],
"tabs": []

146
README.md
View File

@@ -1,12 +1,12 @@
# Video Shader
Native video shader host with an OpenGL/DeckLink render path, Slang shader packages, and a local React control UI.
Native video shader host with an OpenGL render path, pluggable video I/O boundary, DeckLink backend, Slang shader packages, and a local React control UI.
The app loads shader packages from `shaders/`, compiles Slang to GLSL at runtime, renders a configurable layer stack, and exposes a browser-based control surface over a local HTTP/WebSocket server.
The app loads shader packages from `shaders/`, compiles Slang to GLSL at runtime, renders a configurable layer stack, and exposes a browser-based control surface over a local HTTP/WebSocket server. Shader compilation is prepared off the frame path where possible, then committed on the render thread so editing shader files does not block video output for the whole compile.
## Repository Layout
- `apps/LoopThroughWithOpenGLCompositing/`: native C++ host app.
- `src/`: native C++ host app.
- `shaders/`: shader packages, each with `shader.json` and `shader.slang`.
- `ui/`: Vite/React control UI.
- `config/runtime-host.json`: runtime configuration.
@@ -15,26 +15,41 @@ The app loads shader packages from `shaders/`, compiles Slang to GLSL at runtime
- `tests/`: focused native tests for pure runtime logic.
- `.gitea/workflows/ci.yml`: Gitea Actions CI for Windows native tests and Ubuntu UI build.
Native app internals are grouped by boundary:
- `app/`: startup/shutdown orchestration and runtime layer controller.
- `control/`: HTTP/WebSocket server, command parsing, and runtime-state JSON presentation.
- `frames/`: system-memory frame exchange and input mailbox handoff.
- `render/`: render thread, readback, runtime render scene, and shared-context shader program preparation.
- `runtime/`: shader catalog support, layer model, Slang build bridge, font atlas build, and runtime-state persistence.
- `shader/`: shader package parsing and Slang compilation helpers.
- `video/`: DeckLink input/output edges, format helpers, and scheduling.
## Requirements
- Windows with Visual Studio 2022 C++ tooling.
- CMake 3.24 or newer.
- Node.js and npm for the control UI.
- Blackmagic DeckLink SDK 16.0 with the NVIDIA GPUDirect sample files available locally.
- Slang compiler available under the repo/tooling paths expected by the runtime, or otherwise discoverable by the existing app setup.
- Blackmagic Desktop Video drivers and a DeckLink device for the current production video I/O backend.
- Slang binary release with `slangc.exe`, `slang-compiler.dll`, `slang-glslang.dll`, and `LICENSE`.
- `msdf-atlas-gen` Windows binary release with `msdf-atlas-gen.exe`, `LICENSE.txt`, and any adjacent runtime DLLs for font atlas generation.
The Blackmagic/GPUDirect SDK should not be committed to this repository. `CMakeLists.txt` exposes `GPUDIRECT_DIR` as a cache path so local machines and CI runners can point at their installed SDK location.
Default expected SDK path:
Default expected Slang path:
```text
3rdParty/Blackmagic DeckLink SDK 16.0/Win/Samples/NVIDIA_GPUDirect
3rdParty/slang-2026.8-windows-x86_64
```
Default expected `msdf-atlas-gen` path:
```text
3rdParty/msdf-atlas-gen
```
Override example:
```powershell
cmake --preset vs2022-x64-debug -DGPUDIRECT_DIR="D:/SDKs/Blackmagic DeckLink SDK 16.0/Win/Samples/NVIDIA_GPUDirect"
cmake --preset vs2022-x64-debug -DSLANG_ROOT="D:/SDKs/slang-2026.8-windows-x86_64"
```
## Build
@@ -43,7 +58,7 @@ Configure and build the native app:
```powershell
cmake --preset vs2022-x64-debug
cmake --build --preset build-debug
cmake --build --preset build-debug --parallel
```
Build the React control UI:
@@ -56,6 +71,12 @@ npm run build
The native app serves `ui/dist` when it exists, otherwise it falls back to the source UI directory during development.
The control UI provides:
- A searchable shader library for adding layers.
- Compact parameter rows with inline descriptions and intended OSC route copy controls.
- Manual shader reload.
## Package
Build the UI, build the native Release target, then install into a portable runtime folder:
@@ -66,7 +87,7 @@ npm ci
npm run build
cd ..
cmake --preset vs2022-x64-release
cmake --build --preset build-release
cmake --build --preset build-release --parallel
cmake --install build/vs2022-x64-release --config Release --prefix dist/VideoShader
```
@@ -74,15 +95,21 @@ The package folder will contain:
```text
dist/VideoShader/
LoopThroughWithOpenGLCompositing.exe
dvp.dll
RenderCadenceCompositor.exe
config/
shaders/
3rdParty/slang/bin/
3rdParty/msdf-atlas-gen/
ui/dist/
docs/
SHADER_CONTRACT.md
runtime/templates/
third_party_notices/
```
You can run `LoopThroughWithOpenGLCompositing.exe` directly from that folder. In packaged mode, the app resolves `config/`, `shaders/`, `ui/dist/`, and `runtime/templates/` relative to the exe folder. In development mode, it still falls back to repo-root discovery.
You can run `RenderCadenceCompositor.exe` directly from that folder. In packaged mode, the app resolves `config/`, `shaders/`, `3rdParty/slang/bin/slangc.exe`, `3rdParty/msdf-atlas-gen/msdf-atlas-gen.exe`, `ui/dist/`, and `runtime/templates/` relative to the exe folder. In development mode, it still falls back to repo-root discovery.
The install step copies only the Slang runtime files required by the shader compiler (`slangc.exe`, `slang-compiler.dll`, and `slang-glslang.dll`) plus `third_party_notices/SLANG_LICENSE.txt`. It also copies `msdf-atlas-gen.exe`, any adjacent `msdf-atlas-gen` DLLs, and the `third_party_notices/MSDF_ATLAS_GEN_LICENSE.txt` and `third_party_notices/MSDF_ATLAS_GEN_README.md` notice files. It does not copy full third-party release folders.
Create a zip for distribution:
@@ -95,7 +122,7 @@ Compress-Archive -Path dist/VideoShader/* -DestinationPath dist/VideoShader.zip
Run native tests:
```powershell
cmake --build --preset build-debug --target RUN_TESTS
cmake --build --preset build-debug --target RUN_TESTS --parallel
```
Run the UI production build check:
@@ -109,7 +136,9 @@ Current native test coverage includes:
- JSON parsing and serialization.
- Parameter normalization and preset filename safety.
- Shader manifest parsing and package registry scanning.
- Shader manifest parsing, temporal manifest validation, and package registry scanning.
- Video I/O format helpers, v210/Ay10 row-byte math, v210 pack/unpack math, playout scheduler timing, and fake backend contract coverage.
- Slang validation for every available shader package.
## Runtime Configuration
@@ -119,24 +148,20 @@ Current native test coverage includes:
{
"shaderLibrary": "shaders",
"serverPort": 8080,
"oscBindAddress": "127.0.0.1",
"oscPort": 9000,
"oscSmoothing": 0.18,
"inputVideoFormat": "1080p",
"inputFrameRate": "59.94",
"outputVideoFormat": "1080p",
"outputFrameRate": "59.94",
"autoReload": true,
"maxTemporalHistoryFrames": 12,
"audioEnabled": true,
"audioChannelCount": 2,
"audioSampleRate": 48000,
"audioDelayMode": "matchVideoPreroll",
"enableExternalKeying": true
}
```
`inputVideoFormat`/`inputFrameRate` select the DeckLink capture mode. `outputVideoFormat`/`outputFrameRate` select the playout mode. The shader stack runs at input resolution and the final rendered frame is scaled once into the configured output mode. Common examples include `720p`/`50`, `720p`/`59.94`, `1080i`/`50`, `1080i`/`59.94`, `1080p`/`25`, `1080p`/`50`, `1080p`/`59.94`, and `2160p`/`59.94`, depending on card support.
`audioEnabled` enables embedded stereo 48 kHz PCM pass-through. Audio is delayed to match the scheduled video preroll and the synchronized level/spectrum data is exposed to shaders.
`inputVideoFormat`/`inputFrameRate` select the video capture mode. `outputVideoFormat`/`outputFrameRate` select the playout mode. With the current DeckLink backend, supported modes depend on the installed card and driver. The shader stack runs at input resolution and the final rendered frame is scaled once into the configured output mode. Common examples include `720p`/`50`, `720p`/`59.94`, `1080i`/`50`, `1080i`/`59.94`, `1080p`/`25`, `1080p`/`50`, `1080p`/`59.94`, and `2160p`/`59.94`.
Legacy `videoFormat` and `frameRate` keys are still accepted and apply to both input and output unless the explicit input/output keys are present.
@@ -146,6 +171,14 @@ The control UI is available at:
http://127.0.0.1:<serverPort>
```
## Runtime State
The current layer stack is autosaved to `runtime/runtime_state.json` whenever durable UI/API layer changes are accepted: add/remove, shader assignment, bypass state, ordering, parameter updates, parameter reset, and reload compatibility refreshes. Saves are debounced and written on a background worker, with a final flush during shutdown.
On startup, the host tries to reload `runtime/runtime_state.json` before compiling the stack. Valid saved layers are rebuilt in saved order, with shader id, bypass state, and parameter values restored. Missing shader packages are skipped, invalid saved parameter values fall back to shader defaults, and if the file is missing or unusable the app falls back to the configured default shader.
Manual stack preset and screenshot routes are still present in the UI/OpenAPI surface, but they are not implemented by the current native command path yet. `runtime_state.json` is the supported latest-working-state mechanism for now.
## Control API
The local REST control API is documented as an OpenAPI/Swagger spec:
@@ -167,17 +200,31 @@ A Swagger UI page is available at:
http://127.0.0.1:<serverPort>/docs
```
Use those docs to inspect the `/api/state`, layer control, stack preset, and reload endpoints. Live state updates are also sent over the `/ws` WebSocket.
Use those docs to inspect the `/api/state`, layer control, and reload endpoints. Live state updates are also sent over the `/ws` WebSocket.
The control UI has a **Reload shaders** button. It rescans `shaders/`, re-reads manifests, refreshes shader availability/errors, updates active layer parameter definitions from changed manifests, and queues recompilation for every catalog-valid layer in the active stack. Missing shader packages are marked failed, and the previous working render stack remains active where possible until replacement builds commit successfully.
Each parameter row still exposes the intended OSC route in the UI, but OSC ingress is not wired in the current native host.
The control UI currently still shows preset and screenshot controls from the intended route surface. Those endpoints return an unimplemented action result in the native host until their backend paths are wired.
The planned screenshot output directory is:
```text
runtime/screenshots/
```
## OSC Control
The native host also listens for local OSC parameter control on the configured `oscPort`:
OSC fields are present in `config/runtime-host.json` and `/api/state` for compatibility with the intended control surface, but the current native host does not start an OSC listener yet.
The intended route shape is:
```text
/VideoShaderToys/{LayerNameOrID}/{ParameterNameOrID}
```
For example, `/VideoShaderToys/VHS/intensity` updates the `intensity` parameter on the first matching `VHS` layer. The listener accepts float, integer, string, and boolean OSC values, and validates them through the same shader parameter path as the REST API. See `docs/OSC_CONTROL.md` for details.
For now, use the REST layer parameter endpoints or the control UI for live parameter changes. Future OSC-driven parameter changes should stay out of autosave unless an explicit persistence policy is added.
## Shader Packages
@@ -187,9 +234,11 @@ Each shader package lives under:
shaders/<id>/
shader.json
shader.slang
optional-extra-pass.slang
optional-font-or-texture-assets
```
See `SHADER_CONTRACT.md` for the manifest schema, parameter types, texture assets, temporal history support, and the Slang entry point contract.
See `SHADER_CONTRACT.md` for the manifest schema, parameter types, texture assets, font/text assets, temporal history support, optional render-pass declarations, and the Slang entry point contract. `shaders/text-overlay/` is the reference live text package and bundles Roboto Regular with its OFL license. Broken shader packages are shown as unavailable in the selector with their error text instead of preventing the app from launching.
## Generated Files
@@ -198,8 +247,9 @@ Runtime-generated files are intentionally ignored:
- `runtime/shader_cache/active_shader_wrapper.slang`
- `runtime/shader_cache/active_shader.raw.frag`
- `runtime/shader_cache/active_shader.frag`
- `runtime/runtime_state.json`
- `runtime/stack_presets/*.json`
- `runtime/runtime_state.json` autosaved latest stack and parameter state.
- `runtime/stack_presets/*.json` planned manual preset output; preset routes are not implemented in the native host yet.
- `runtime/screenshots/*.png` planned screenshot output; screenshot capture is not implemented in the native host yet.
Only `runtime/templates/` and `runtime/README.md` are tracked.
@@ -207,7 +257,39 @@ Only `runtime/templates/` and `runtime/README.md` are tracked.
The Gitea workflow expects two act runners:
- `windows-latest`: builds the native app and runs native tests.
- `windows-2022`: builds the native app and runs native tests.
- `ubuntu-latest`: installs UI dependencies and runs the Vite build.
If your Windows runner stores the Blackmagic SDK outside the repo, configure `GPUDIRECT_DIR` in the runner environment or adjust the workflow configure command to pass `-DGPUDIRECT_DIR=...`.
The Windows jobs validate native third-party dependencies before configuring CMake. Because `3rdParty/` is ignored, configure this path on the runner or in a Gitea repository variable:
- `SLANG_ROOT`: path to the Slang binary release folder containing `bin/slangc.exe`.
- `MSDF_ATLAS_GEN_ROOT`: path to the `msdf-atlas-gen` binary release folder containing `msdf-atlas-gen.exe`.
The Windows runner also needs the Visual Studio ATL component installed. In Visual Studio Build Tools 2022, add `C++ ATL for latest v143 build tools (x86 & x64)`, component ID `Microsoft.VisualStudio.Component.VC.ATL`.
Example runner paths:
```text
D:\SDKs\slang-2026.8-windows-x86_64
D:\SDKs\msdf-atlas-gen
```
If `SLANG_ROOT` or `MSDF_ATLAS_GEN_ROOT` is not set, the workflow falls back to the repo-local defaults under `3rdParty/`.
## Still Todo
- Audio.
- Genlock.
- Logs.
- Add more video I/O backends now that the DeckLink path is behind `videoio/`.
- Support a separate sound shader `.slang` file in shader packages. (https://www.shadertoy.com/view/XsBXWt)
- Add WebView2 for an embedded native control surface.
- MSDF typography rasterisation
- More shader-library organisation and filtering as the built-in library grows.
- Optional linear-light compositing mode.
- compute shaders or a small 1x1 or nx1 RGBA16f render target for arbitrary data storage
- allow shaders to read other shaders data store based on name? or output over OSC
- Mipmapping for shader-declared textures
- Anotate included shaders
- allow 3 vector exposed controls
- add nearest sampling to the extra shader pass

View File

@@ -1,435 +0,0 @@
# Shader Package Contract
This document explains how to create shaders for the Video Shader runtime.
Each shader is a small package under `shaders/<id>/`:
```text
shaders/my-effect/
shader.json
shader.slang
optional-texture.png
```
The runtime reads `shader.json`, generates a Slang wrapper from `runtime/templates/shader_wrapper.slang.in`, includes your `shader.slang`, compiles the result to GLSL, and exposes the shader in the local control UI.
## Quick Start
Create a folder:
```text
shaders/my-effect/
```
Add `shader.json`:
```json
{
"id": "my-effect",
"name": "My Effect",
"description": "A simple starter shader.",
"category": "Custom",
"entryPoint": "shadeVideo",
"parameters": [
{
"id": "strength",
"label": "Strength",
"type": "float",
"default": 0.5,
"min": 0.0,
"max": 1.0,
"step": 0.01
}
]
}
```
Add `shader.slang`:
```slang
float4 shadeVideo(ShaderContext context)
{
float4 color = context.sourceColor;
color.rgb = lerp(color.rgb, 1.0 - color.rgb, strength);
return saturate(color);
}
```
With `autoReload` enabled in `config/runtime-host.json`, edits to shader source, manifests, and declared texture assets are picked up automatically.
## Manifest Fields
`shader.json` is the runtime-facing description of the shader.
Required fields:
- `id`: package ID used by state/presets. Hyphenated names are OK here, for example `my-effect`.
- `name`: display name in the UI.
- `parameters`: array of exposed controls. Use `[]` if there are no user parameters.
Optional fields:
- `description`: display/help text for the shader library.
- `category`: UI grouping label.
- `entryPoint`: Slang function to call. Defaults to `shadeVideo`.
- `textures`: texture assets to load and expose as samplers.
- `temporal`: history-buffer requirements.
Shader-visible identifiers must be valid Slang-style identifiers:
- `entryPoint`
- parameter `id`
- texture `id`
Use letters, numbers, and underscores only, and start with a letter or underscore. For example, `logoTexture` is valid; `logo-texture` is not valid as a shader-visible texture ID.
## Slang Entry Point
Your shader file must implement the manifest `entryPoint`.
Default:
```slang
float4 shadeVideo(ShaderContext context)
{
return context.sourceColor;
}
```
The runtime owns the real fragment shader entry point. Your function is called from the wrapper, and the runtime handles final bypass/mix behavior:
```slang
return lerp(context.sourceColor, effectedColor, mixValue);
```
That means:
- Return the fully effected color from your function.
- Respect alpha if your shader produces an overlay or sprite.
- The runtime will blend your result with the source according to `mixAmount` and bypass state.
## ShaderContext
Your entry point receives:
```slang
struct ShaderContext
{
float2 uv;
float4 sourceColor;
float2 inputResolution;
float2 outputResolution;
float time;
float frameCount;
float mixAmount;
float bypass;
int sourceHistoryLength;
int temporalHistoryLength;
float2 audioRms;
float2 audioPeak;
float audioMonoRms;
float audioMonoPeak;
float4 audioBands;
};
```
Fields:
- `uv`: normalized texture coordinates, usually `0..1`.
- `sourceColor`: decoded RGBA source video at `uv`.
- `inputResolution`: decoded input video resolution in pixels.
- `outputResolution`: shader render resolution in pixels. The current pipeline renders the shader stack at input resolution, then scales the final frame to the configured DeckLink output mode.
- `time`: elapsed runtime time in seconds.
- `frameCount`: incrementing frame counter.
- `mixAmount`: runtime mix amount.
- `bypass`: `1.0` when the layer is bypassed, otherwise `0.0`.
- `sourceHistoryLength`: number of usable source-history frames currently available.
- `temporalHistoryLength`: number of usable temporal frames currently available for this layer.
- `audioRms`: left/right RMS level for the audio block synchronized with the rendered output frame.
- `audioPeak`: left/right peak level for the same synchronized audio block.
- `audioMonoRms`: mono RMS level derived from left/right.
- `audioMonoPeak`: mono peak level derived from left/right.
- `audioBands`: four smoothed, normalized low-to-high frequency bands.
## Helper Functions
The wrapper provides:
```slang
float4 sampleVideo(float2 uv);
float4 sampleSourceHistory(int framesAgo, float2 uv);
float4 sampleTemporalHistory(int framesAgo, float2 uv);
float4 sampleAudioWaveform(float x);
float4 sampleAudioSpectrum(float x);
```
`sampleVideo` samples the live decoded source video.
`sampleSourceHistory` samples previous decoded source frames. `framesAgo` is clamped into the available range. If no history is available, it falls back to `sampleVideo`.
`sampleTemporalHistory` samples previous pre-layer input frames for temporal shaders that request `preLayerInput` history. `framesAgo` is clamped into the available range. If no temporal history is available, it falls back to `sampleVideo`.
`sampleAudioWaveform` samples the current synchronized audio waveform texture. `x` is normalized `0..1`; returned waveform channels are encoded from `-1..1` into `0..1`.
`sampleAudioSpectrum` samples the current synchronized audio spectrum texture. Values are normalized `0..1`.
Example:
```slang
float4 shadeVideo(ShaderContext context)
{
float4 previous = sampleSourceHistory(1, context.uv);
return lerp(context.sourceColor, previous, 0.35);
}
```
## Parameters
Manifest parameters are exposed to Slang as global values with the same `id`.
Supported types:
| Manifest type | Slang type | JSON value |
| --- | --- | --- |
| `float` | `float` | number |
| `vec2` | `float2` | `[x, y]` |
| `color` | `float4` | `[r, g, b, a]` |
| `bool` | `bool` | `true` or `false` |
| `enum` | `int` | selected option index |
Float example:
```json
{
"id": "brightness",
"label": "Brightness",
"type": "float",
"default": 1.0,
"min": 0.0,
"max": 2.0,
"step": 0.01
}
```
```slang
color.rgb *= brightness;
```
Vector example:
```json
{
"id": "offset",
"label": "Offset",
"type": "vec2",
"default": [0.0, 0.0],
"min": [-0.2, -0.2],
"max": [0.2, 0.2],
"step": [0.001, 0.001]
}
```
```slang
float2 uv = clamp(context.uv + offset, float2(0.0), float2(1.0));
```
Color example:
```json
{
"id": "tint",
"label": "Tint",
"type": "color",
"default": [1.0, 1.0, 1.0, 1.0]
}
```
```slang
color *= tint;
```
Boolean example:
```json
{
"id": "invert",
"label": "Invert",
"type": "bool",
"default": false
}
```
```slang
if (invert)
color.rgb = 1.0 - color.rgb;
```
Enum example:
```json
{
"id": "mode",
"label": "Mode",
"type": "enum",
"default": "normal",
"options": [
{ "value": "normal", "label": "Normal" },
{ "value": "luma", "label": "Luma" },
{ "value": "posterize", "label": "Posterize" }
]
}
```
Enums are stored in presets/state by their string `value`, but exposed to Slang as a zero-based integer index in option order:
```slang
if (mode == 1)
{
float luma = dot(color.rgb, float3(0.2126, 0.7152, 0.0722));
color.rgb = float3(luma);
}
else if (mode == 2)
{
color.rgb = floor(color.rgb * 4.0) / 4.0;
}
```
Parameter validation:
- Float values are clamped to `min`/`max` if provided.
- `vec2` must have exactly 2 numbers.
- `color` must have exactly 4 numbers.
- Enum defaults must match one of the declared option values.
- Non-finite numeric values are rejected.
## Texture Assets
Declare texture assets in the manifest:
```json
{
"textures": [
{
"id": "logoTexture",
"path": "logo.png"
}
]
}
```
Rules:
- `id` must be a valid shader identifier.
- `path` is relative to the shader package directory.
- The file must exist when the manifest is loaded.
- Texture asset changes trigger shader reload.
Texture IDs become `Sampler2D<float4>` globals:
```slang
float4 logo = logoTexture.Sample(logoUv);
```
For sprite or overlay shaders, return premultiplied-looking output if you want clean composition:
```slang
float alpha = logo.a;
return float4(logo.rgb * alpha, alpha);
```
See `shaders/dvd-bounce/` for a complete texture-driven example.
## Temporal Shaders
Temporal shaders can request access to previous frames.
Manifest example:
```json
{
"temporal": {
"enabled": true,
"historySource": "preLayerInput",
"historyLength": 12
}
}
```
Supported `historySource` values:
- `source`: decoded source-video history from previous frames.
- `preLayerInput`: history of the input arriving at this layer before the shader runs.
`historyLength` is the requested frame count. The runtime clamps it by `maxTemporalHistoryFrames` in `config/runtime-host.json`.
Temporal history resets when:
- layers are added, removed, or reordered
- a layer bypass state changes
- a layer changes shader
- a shader is reloaded or recompiled
- render dimensions change
Use the available history lengths to avoid assuming history is ready on the first frame:
```slang
float4 shadeVideo(ShaderContext context)
{
if (context.temporalHistoryLength <= 0)
return context.sourceColor;
float4 oldFrame = sampleTemporalHistory(3, context.uv);
return lerp(context.sourceColor, oldFrame, 0.4);
}
```
See `shaders/temporal-ghost-trail/` and `shaders/temporal-low-fps/` for examples.
## Coordinate And Color Notes
- `uv` is normalized.
- Use `context.outputResolution` for pixel-sized effects.
- Use `context.inputResolution` when sampling source video by input pixel size.
- `sourceColor` and `sampleVideo` return RGBA values in normalized `0..1` range.
- Prefer `saturate(color)` or explicit `clamp` before returning if your math can overshoot.
Pixel-size example:
```slang
float2 pixel = 1.0 / max(context.outputResolution, float2(1.0));
float4 right = sampleVideo(context.uv + float2(pixel.x, 0.0));
```
## Reload And Generated Files
When a shader compiles, the runtime writes generated files under `runtime/shader_cache/`:
- `active_shader_wrapper.slang`
- `active_shader.raw.frag`
- `active_shader.frag`
These files are ignored by git and are useful for debugging compiler output. If a shader fails to compile, inspect the wrapper first; it shows the exact generated Slang code including your included shader.
## Common Pitfalls
- Do not use hyphens in parameter IDs, texture IDs, or entry point names.
- Do not declare your own `ShaderContext`, `GlobalParams`, `sampleVideo`, `sampleSourceHistory`, or `sampleTemporalHistory`.
- Do not write a `[shader("fragment")]` entry point in `shader.slang`; the runtime provides it.
- Remember enum globals are integer indexes, not strings.
- Declare every texture in `shader.json`; undeclared texture samplers will not be bound.
- Keep temporal history requests modest. They consume texture units and memory and are capped by runtime config.
- If a parameter appears in the UI but not in Slang, the shader may still compile, but the control has no effect.
- If a Slang name collides with a generated global, rename your parameter or local symbol.
## Minimal Package Checklist
Before committing a new shader package:
- `shader.json` is valid JSON.
- `id` is unique across `shaders/`.
- `entryPoint`, parameter IDs, and texture IDs are valid identifiers.
- `shader.slang` implements the configured entry point.
- Texture files referenced by `textures` exist.
- Enum defaults are present in their `options`.
- Temporal shaders handle short or empty history gracefully.
- The app can reload and compile the shader without errors.

View File

@@ -1,206 +0,0 @@
#include "AudioSupport.h"
#include <algorithm>
#include <cmath>
#include <iterator>
#include <limits>
namespace
{
constexpr float kInt32ToFloat = 1.0f / 2147483648.0f;
constexpr std::size_t kAnalysisWindowSamples = 1024;
constexpr std::size_t kMaxBufferedAudioFrames = kAudioSampleRate * 10;
float Clamp01(float value)
{
return std::max(0.0f, std::min(1.0f, value));
}
float SampleToFloat(int32_t sample)
{
return std::max(-1.0f, std::min(1.0f, static_cast<float>(sample) * kInt32ToFloat));
}
float GoertzelMagnitude(const std::vector<float>& samples, float frequency)
{
if (samples.empty())
return 0.0f;
const double omega = 2.0 * 3.14159265358979323846 * static_cast<double>(frequency) / static_cast<double>(kAudioSampleRate);
const double coefficient = 2.0 * std::cos(omega);
double q0 = 0.0;
double q1 = 0.0;
double q2 = 0.0;
for (float sample : samples)
{
q0 = coefficient * q1 - q2 + static_cast<double>(sample);
q2 = q1;
q1 = q0;
}
const double power = q1 * q1 + q2 * q2 - coefficient * q1 * q2;
return static_cast<float>(std::sqrt(std::max(0.0, power)) / static_cast<double>(samples.size()));
}
}
uint64_t AudioSampleTimeForVideoFrame(uint64_t videoFrameIndex, uint64_t frameDuration, uint64_t frameTimescale, uint64_t audioSampleRate)
{
if (frameTimescale == 0)
return 0;
const uint64_t numerator = videoFrameIndex * frameDuration * audioSampleRate;
return (numerator + frameTimescale / 2) / frameTimescale;
}
unsigned AudioSamplesForVideoFrame(uint64_t videoFrameIndex, uint64_t frameDuration, uint64_t frameTimescale, uint64_t audioSampleRate)
{
const uint64_t start = AudioSampleTimeForVideoFrame(videoFrameIndex, frameDuration, frameTimescale, audioSampleRate);
const uint64_t end = AudioSampleTimeForVideoFrame(videoFrameIndex + 1, frameDuration, frameTimescale, audioSampleRate);
return static_cast<unsigned>(end > start ? end - start : 0);
}
void AudioDelayBuffer::Reset(unsigned delaySampleFrames)
{
std::lock_guard<std::mutex> lock(mMutex);
mSamples.clear();
mSamples.resize(static_cast<std::size_t>(delaySampleFrames) * kAudioChannelCount, 0);
mUnderrunCount = 0;
}
void AudioDelayBuffer::PushInterleaved(const int32_t* samples, std::size_t sampleFrameCount)
{
if (!samples || sampleFrameCount == 0)
return;
std::lock_guard<std::mutex> lock(mMutex);
const std::size_t sampleCount = sampleFrameCount * kAudioChannelCount;
for (std::size_t index = 0; index < sampleCount; ++index)
mSamples.push_back(samples[index]);
const std::size_t maxSamples = kMaxBufferedAudioFrames * kAudioChannelCount;
while (mSamples.size() > maxSamples)
mSamples.pop_front();
}
AudioFrameBlock AudioDelayBuffer::Pop(std::size_t sampleFrameCount, bool& underrun)
{
AudioFrameBlock block;
block.interleavedSamples.resize(sampleFrameCount * kAudioChannelCount, 0);
std::lock_guard<std::mutex> lock(mMutex);
const std::size_t requestedSamples = sampleFrameCount * kAudioChannelCount;
underrun = mSamples.size() < requestedSamples;
if (underrun)
++mUnderrunCount;
const std::size_t availableSamples = std::min(requestedSamples, mSamples.size());
for (std::size_t index = 0; index < availableSamples; ++index)
{
block.interleavedSamples[index] = mSamples.front();
mSamples.pop_front();
}
return block;
}
unsigned AudioDelayBuffer::BufferedSampleFrames() const
{
std::lock_guard<std::mutex> lock(mMutex);
return static_cast<unsigned>(mSamples.size() / kAudioChannelCount);
}
uint64_t AudioDelayBuffer::UnderrunCount() const
{
std::lock_guard<std::mutex> lock(mMutex);
return mUnderrunCount;
}
void AudioAnalyzer::Reset()
{
mMonoHistory.clear();
mSmoothedBands = { 0.0f, 0.0f, 0.0f, 0.0f };
mCurrent = AudioAnalysisSnapshot();
}
AudioAnalysisSnapshot AudioAnalyzer::Analyze(const AudioFrameBlock& block)
{
AudioAnalysisSnapshot next;
double sumSquares[2] = { 0.0, 0.0 };
float peak[2] = { 0.0f, 0.0f };
double monoSumSquares = 0.0;
float monoPeak = 0.0f;
const std::size_t frames = block.frameCount();
for (std::size_t frame = 0; frame < frames; ++frame)
{
const float left = SampleToFloat(block.interleavedSamples[frame * 2]);
const float right = SampleToFloat(block.interleavedSamples[frame * 2 + 1]);
const float mono = (left + right) * 0.5f;
sumSquares[0] += static_cast<double>(left) * left;
sumSquares[1] += static_cast<double>(right) * right;
peak[0] = std::max(peak[0], std::abs(left));
peak[1] = std::max(peak[1], std::abs(right));
monoSumSquares += static_cast<double>(mono) * mono;
monoPeak = std::max(monoPeak, std::abs(mono));
mMonoHistory.push_back(mono);
while (mMonoHistory.size() > kAnalysisWindowSamples)
mMonoHistory.pop_front();
}
if (frames > 0)
{
next.rms[0] = static_cast<float>(std::sqrt(sumSquares[0] / static_cast<double>(frames)));
next.rms[1] = static_cast<float>(std::sqrt(sumSquares[1] / static_cast<double>(frames)));
next.peak[0] = peak[0];
next.peak[1] = peak[1];
next.monoRms = static_cast<float>(std::sqrt(monoSumSquares / static_cast<double>(frames)));
next.monoPeak = monoPeak;
}
std::vector<float> window(mMonoHistory.begin(), mMonoHistory.end());
const float bandFrequencies[4] = { 90.0f, 300.0f, 1200.0f, 5000.0f };
for (std::size_t band = 0; band < next.bands.size(); ++band)
{
const float raw = Clamp01(GoertzelMagnitude(window, bandFrequencies[band]) * 8.0f);
const float smoothing = raw > mSmoothedBands[band] ? 0.45f : 0.12f;
mSmoothedBands[band] = mSmoothedBands[band] + (raw - mSmoothedBands[band]) * smoothing;
next.bands[band] = Clamp01(mSmoothedBands[band]);
}
for (unsigned x = 0; x < kAudioTextureWidth; ++x)
{
float mono = 0.0f;
if (!mMonoHistory.empty())
{
const std::size_t historyIndex = static_cast<std::size_t>(
(static_cast<uint64_t>(x) * static_cast<uint64_t>(mMonoHistory.size())) / kAudioTextureWidth);
auto it = mMonoHistory.begin();
std::advance(it, std::min(historyIndex, mMonoHistory.size() - 1));
mono = *it;
}
const std::size_t waveformOffset = x * 4;
next.texture[waveformOffset + 0] = mono * 0.5f + 0.5f;
next.texture[waveformOffset + 1] = next.texture[waveformOffset + 0];
next.texture[waveformOffset + 2] = next.monoRms;
next.texture[waveformOffset + 3] = 1.0f;
const float bandPosition = static_cast<float>(x) / static_cast<float>(kAudioTextureWidth - 1);
const float scaled = bandPosition * static_cast<float>(next.bands.size() - 1);
const unsigned bandA = static_cast<unsigned>(std::floor(scaled));
const unsigned bandB = std::min<unsigned>(bandA + 1, static_cast<unsigned>(next.bands.size() - 1));
const float t = scaled - static_cast<float>(bandA);
const float spectrum = next.bands[bandA] * (1.0f - t) + next.bands[bandB] * t;
const std::size_t spectrumOffset = (kAudioTextureWidth + x) * 4;
next.texture[spectrumOffset + 0] = spectrum;
next.texture[spectrumOffset + 1] = next.bands[0];
next.texture[spectrumOffset + 2] = next.bands[1];
next.texture[spectrumOffset + 3] = next.bands[2];
}
mCurrent = next;
return mCurrent;
}

View File

@@ -1,71 +0,0 @@
#pragma once
#include <array>
#include <cstdint>
#include <deque>
#include <mutex>
#include <vector>
constexpr unsigned kAudioSampleRate = 48000;
constexpr unsigned kAudioChannelCount = 2;
constexpr unsigned kAudioTextureWidth = 64;
constexpr unsigned kAudioTextureHeight = 2;
struct AudioFrameBlock
{
std::vector<int32_t> interleavedSamples;
std::size_t frameCount() const
{
return interleavedSamples.size() / kAudioChannelCount;
}
};
struct AudioAnalysisSnapshot
{
std::array<float, 2> rms = { 0.0f, 0.0f };
std::array<float, 2> peak = { 0.0f, 0.0f };
float monoRms = 0.0f;
float monoPeak = 0.0f;
std::array<float, 4> bands = { 0.0f, 0.0f, 0.0f, 0.0f };
std::array<float, kAudioTextureWidth * kAudioTextureHeight * 4> texture = {};
};
struct AudioStatusSnapshot
{
bool enabled = false;
unsigned bufferedSampleFrames = 0;
uint64_t underrunCount = 0;
AudioAnalysisSnapshot analysis;
};
class AudioDelayBuffer
{
public:
void Reset(unsigned delaySampleFrames);
void PushInterleaved(const int32_t* samples, std::size_t sampleFrameCount);
AudioFrameBlock Pop(std::size_t sampleFrameCount, bool& underrun);
unsigned BufferedSampleFrames() const;
uint64_t UnderrunCount() const;
private:
mutable std::mutex mMutex;
std::deque<int32_t> mSamples;
uint64_t mUnderrunCount = 0;
};
class AudioAnalyzer
{
public:
void Reset();
AudioAnalysisSnapshot Analyze(const AudioFrameBlock& block);
const AudioAnalysisSnapshot& Current() const { return mCurrent; }
private:
std::deque<float> mMonoHistory;
std::array<float, 4> mSmoothedBands = { 0.0f, 0.0f, 0.0f, 0.0f };
AudioAnalysisSnapshot mCurrent;
};
uint64_t AudioSampleTimeForVideoFrame(uint64_t videoFrameIndex, uint64_t frameDuration, uint64_t frameTimescale, uint64_t audioSampleRate = kAudioSampleRate);
unsigned AudioSamplesForVideoFrame(uint64_t videoFrameIndex, uint64_t frameDuration, uint64_t frameTimescale, uint64_t audioSampleRate = kAudioSampleRate);

View File

@@ -1,616 +0,0 @@
#include "stdafx.h"
#include "ControlServer.h"
#include "RuntimeJson.h"
#include <Wincrypt.h>
#include <ws2tcpip.h>
#include <algorithm>
#include <fstream>
#include <sstream>
#pragma comment(lib, "Ws2_32.lib")
#pragma comment(lib, "Crypt32.lib")
#pragma comment(lib, "Advapi32.lib")
namespace
{
bool InitializeWinsock(std::string& error)
{
WSADATA wsaData = {};
int result = WSAStartup(MAKEWORD(2, 2), &wsaData);
if (result != 0)
{
error = "WSAStartup failed.";
return false;
}
return true;
}
std::string ToLower(std::string text)
{
std::transform(text.begin(), text.end(), text.begin(),
[](unsigned char ch) { return static_cast<char>(std::tolower(ch)); });
return text;
}
bool IsSafeUiPath(const std::filesystem::path& relativePath)
{
for (const std::filesystem::path& part : relativePath)
{
if (part == "..")
return false;
}
return !relativePath.empty();
}
std::string GuessContentType(const std::filesystem::path& assetPath)
{
const std::string extension = ToLower(assetPath.extension().string());
if (extension == ".js" || extension == ".mjs")
return "text/javascript";
if (extension == ".css")
return "text/css";
if (extension == ".json")
return "application/json";
if (extension == ".yaml" || extension == ".yml")
return "application/yaml";
if (extension == ".svg")
return "image/svg+xml";
if (extension == ".png")
return "image/png";
if (extension == ".jpg" || extension == ".jpeg")
return "image/jpeg";
if (extension == ".ico")
return "image/x-icon";
if (extension == ".map")
return "application/json";
if (extension == ".md")
return "text/markdown";
return "text/html";
}
}
ControlServer::ControlServer()
: mPort(0), mRunning(false)
{
}
ControlServer::~ControlServer()
{
Stop();
}
bool ControlServer::Start(const std::filesystem::path& uiRoot, const std::filesystem::path& docsRoot, unsigned short preferredPort, const Callbacks& callbacks, std::string& error)
{
mUiRoot = uiRoot;
mDocsRoot = docsRoot;
mCallbacks = callbacks;
if (!InitializeWinsock(error))
return false;
mListenSocket.reset(socket(AF_INET, SOCK_STREAM, IPPROTO_TCP));
if (!mListenSocket.valid())
{
error = "Could not create listening socket.";
return false;
}
u_long nonBlocking = 1;
ioctlsocket(mListenSocket.get(), FIONBIO, &nonBlocking);
sockaddr_in address = {};
address.sin_family = AF_INET;
address.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
bool bound = false;
for (unsigned short offset = 0; offset < 20; ++offset)
{
address.sin_port = htons(static_cast<u_short>(preferredPort + offset));
if (bind(mListenSocket.get(), reinterpret_cast<sockaddr*>(&address), sizeof(address)) == 0)
{
mPort = preferredPort + offset;
bound = true;
break;
}
}
if (!bound)
{
error = "Could not bind the local control server to any port in the preferred range.";
mListenSocket.reset();
return false;
}
if (listen(mListenSocket.get(), SOMAXCONN) != 0)
{
error = "Could not start listening on the local control server socket.";
mListenSocket.reset();
return false;
}
mRunning = true;
mThread = std::thread(&ControlServer::ServerLoop, this);
return true;
}
void ControlServer::Stop()
{
const bool wasActive = mRunning || mListenSocket.valid() || mThread.joinable();
mRunning = false;
{
std::lock_guard<std::mutex> lock(mMutex);
for (ClientConnection& client : mClients)
client.socket.reset();
mClients.clear();
}
mListenSocket.reset();
if (mThread.joinable())
mThread.join();
if (wasActive)
WSACleanup();
}
void ControlServer::BroadcastState()
{
std::lock_guard<std::mutex> lock(mMutex);
BroadcastStateLocked();
}
void ControlServer::ServerLoop()
{
while (mRunning)
{
TryAcceptClient();
Sleep(25);
}
}
bool ControlServer::HandleHttpClient(UniqueSocket clientSocket)
{
std::string request;
char buffer[8192];
int received = recv(clientSocket.get(), buffer, sizeof(buffer), 0);
if (received <= 0)
return false;
request.assign(buffer, buffer + received);
return HandleHttpRequest(std::move(clientSocket), request);
}
bool ControlServer::TryAcceptClient()
{
sockaddr_in clientAddress = {};
int addressSize = sizeof(clientAddress);
UniqueSocket clientSocket(accept(mListenSocket.get(), reinterpret_cast<sockaddr*>(&clientAddress), &addressSize));
if (!clientSocket.valid())
return false;
return HandleHttpClient(std::move(clientSocket));
}
bool ControlServer::SendHttpResponse(SOCKET clientSocket, const std::string& status, const std::string& contentType, const std::string& body)
{
std::ostringstream response;
response << "HTTP/1.1 " << status << "\r\n";
response << "Content-Type: " << contentType << "\r\n";
response << "Content-Length: " << body.size() << "\r\n";
response << "Connection: close\r\n\r\n";
response << body;
const std::string payload = response.str();
return send(clientSocket, payload.c_str(), static_cast<int>(payload.size()), 0) == static_cast<int>(payload.size());
}
bool ControlServer::SendHttpResponse(SOCKET clientSocket, const HttpResponse& response)
{
return SendHttpResponse(clientSocket, response.status, response.contentType, response.body);
}
bool ControlServer::HandleHttpRequest(UniqueSocket clientSocket, const std::string& request)
{
HttpRequest httpRequest;
if (!ParseHttpRequest(request, httpRequest))
{
SendHttpResponse(clientSocket.get(), "400 Bad Request", "text/plain", "Bad Request");
return true;
}
if (ToLower(GetHeaderValue(httpRequest, "Upgrade")) == "websocket")
return HandleWebSocketUpgrade(std::move(clientSocket), httpRequest);
const HttpResponse response = RouteHttpRequest(httpRequest);
SendHttpResponse(clientSocket.get(), response);
if (response.broadcastState)
BroadcastState();
return true;
}
ControlServer::HttpResponse ControlServer::RouteHttpRequest(const HttpRequest& request)
{
if (request.method == "GET")
return ServeGetRequest(request);
if (request.method == "POST")
return HandleApiPost(request);
return { "404 Not Found", "text/plain", "Not Found" };
}
ControlServer::HttpResponse ControlServer::ServeGetRequest(const HttpRequest& request) const
{
if (request.path == "/" || request.path == "/index.html")
return ServeUiAsset("index.html");
if (request.path == "/api/state")
return { "200 OK", "application/json", mCallbacks.getStateJson ? mCallbacks.getStateJson() : "{}" };
if (request.path == "/openapi.yaml" || request.path == "/docs/openapi.yaml")
return ServeOpenApiSpec();
if (request.path == "/docs" || request.path == "/docs/")
return ServeSwaggerDocs();
const std::string docsPrefix = "/docs/";
if (request.path.rfind(docsPrefix, 0) == 0)
return ServeDocsAsset(request.path.substr(docsPrefix.size()));
if (request.path.size() > 1)
{
const HttpResponse assetResponse = ServeUiAsset(request.path.substr(1));
if (!assetResponse.body.empty())
return assetResponse;
}
return { "404 Not Found", "text/plain", "Not Found" };
}
ControlServer::HttpResponse ControlServer::ServeUiAsset(const std::string& relativePath) const
{
std::string contentType;
const std::string body = LoadUiAsset(relativePath, contentType);
return body.empty()
? HttpResponse{ "404 Not Found", "text/plain", "Not Found" }
: HttpResponse{ "200 OK", contentType, body };
}
ControlServer::HttpResponse ControlServer::ServeDocsAsset(const std::string& relativePath) const
{
const std::filesystem::path sanitizedPath = std::filesystem::path(relativePath).lexically_normal();
if (!IsSafeUiPath(sanitizedPath))
return { "404 Not Found", "text/plain", "Not Found" };
const std::filesystem::path docsPath = mDocsRoot / sanitizedPath;
const std::string body = LoadTextFile(docsPath);
return body.empty()
? HttpResponse{ "404 Not Found", "text/plain", "Not Found" }
: HttpResponse{ "200 OK", GuessContentType(docsPath), body };
}
ControlServer::HttpResponse ControlServer::ServeOpenApiSpec() const
{
const std::filesystem::path specPath = mDocsRoot / "openapi.yaml";
const std::string body = LoadTextFile(specPath);
return body.empty()
? HttpResponse{ "404 Not Found", "text/plain", "OpenAPI spec not found" }
: HttpResponse{ "200 OK", GuessContentType(specPath), body };
}
ControlServer::HttpResponse ControlServer::ServeSwaggerDocs() const
{
std::ostringstream html;
html << "<!doctype html>\n"
<< "<html lang=\"en\">\n"
<< "<head>\n"
<< " <meta charset=\"utf-8\">\n"
<< " <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n"
<< " <title>Video Shader Toys API Docs</title>\n"
<< " <link rel=\"stylesheet\" href=\"https://unpkg.com/swagger-ui-dist@5/swagger-ui.css\">\n"
<< "</head>\n"
<< "<body>\n"
<< " <div id=\"swagger-ui\"></div>\n"
<< " <script src=\"https://unpkg.com/swagger-ui-dist@5/swagger-ui-bundle.js\"></script>\n"
<< " <script>SwaggerUIBundle({url:'/docs/openapi.yaml',dom_id:'#swagger-ui'});</script>\n"
<< "</body>\n"
<< "</html>\n";
return { "200 OK", "text/html", html.str() };
}
ControlServer::HttpResponse ControlServer::HandleApiPost(const HttpRequest& request)
{
JsonValue root;
std::string parseError;
if (!ParseJson(request.body, root, parseError))
return { "400 Bad Request", "application/json", BuildJsonResponse(false, parseError) };
std::string actionError;
const bool success = InvokePostRoute(request.path, root, actionError);
return {
success ? "200 OK" : "400 Bad Request",
"application/json",
BuildJsonResponse(success, actionError),
success
};
}
bool ControlServer::InvokePostRoute(const std::string& path, const JsonValue& root, std::string& actionError)
{
using PostHandler = std::function<bool(const JsonValue&, std::string&)>;
const std::map<std::string, PostHandler> postRoutes =
{
{ "/api/layers/add", [this](const JsonValue& json, std::string& error)
{
const JsonValue* shaderId = json.find("shaderId");
return shaderId && mCallbacks.addLayer && mCallbacks.addLayer(shaderId->asString(), error);
}
},
{ "/api/layers/remove", [this](const JsonValue& json, std::string& error)
{
const JsonValue* layerId = json.find("layerId");
return layerId && mCallbacks.removeLayer && mCallbacks.removeLayer(layerId->asString(), error);
}
},
{ "/api/layers/move", [this](const JsonValue& json, std::string& error)
{
const JsonValue* layerId = json.find("layerId");
const JsonValue* direction = json.find("direction");
return layerId && direction && mCallbacks.moveLayer &&
mCallbacks.moveLayer(layerId->asString(), static_cast<int>(direction->asNumber()), error);
}
},
{ "/api/layers/reorder", [this](const JsonValue& json, std::string& error)
{
const JsonValue* layerId = json.find("layerId");
const JsonValue* targetIndex = json.find("targetIndex");
return layerId && targetIndex && mCallbacks.moveLayerToIndex &&
mCallbacks.moveLayerToIndex(layerId->asString(), static_cast<std::size_t>(targetIndex->asNumber()), error);
}
},
{ "/api/layers/set-bypass", [this](const JsonValue& json, std::string& error)
{
const JsonValue* layerId = json.find("layerId");
const JsonValue* bypass = json.find("bypass");
return layerId && bypass && mCallbacks.setLayerBypass &&
mCallbacks.setLayerBypass(layerId->asString(), bypass->asBoolean(), error);
}
},
{ "/api/layers/set-shader", [this](const JsonValue& json, std::string& error)
{
const JsonValue* layerId = json.find("layerId");
const JsonValue* shaderId = json.find("shaderId");
return layerId && shaderId && mCallbacks.setLayerShader &&
mCallbacks.setLayerShader(layerId->asString(), shaderId->asString(), error);
}
},
{ "/api/layers/update-parameter", [this](const JsonValue& json, std::string& error)
{
const JsonValue* layerId = json.find("layerId");
const JsonValue* parameterId = json.find("parameterId");
const JsonValue* value = json.find("value");
return layerId && parameterId && value && mCallbacks.updateLayerParameter &&
mCallbacks.updateLayerParameter(layerId->asString(), parameterId->asString(), SerializeJson(*value, false), error);
}
},
{ "/api/layers/reset-parameters", [this](const JsonValue& json, std::string& error)
{
const JsonValue* layerId = json.find("layerId");
return layerId && mCallbacks.resetLayerParameters &&
mCallbacks.resetLayerParameters(layerId->asString(), error);
}
},
{ "/api/stack-presets/save", [this](const JsonValue& json, std::string& error)
{
const JsonValue* presetName = json.find("presetName");
return presetName && mCallbacks.saveStackPreset &&
mCallbacks.saveStackPreset(presetName->asString(), error);
}
},
{ "/api/stack-presets/load", [this](const JsonValue& json, std::string& error)
{
const JsonValue* presetName = json.find("presetName");
return presetName && mCallbacks.loadStackPreset &&
mCallbacks.loadStackPreset(presetName->asString(), error);
}
},
{ "/api/reload", [this](const JsonValue&, std::string& error)
{
return mCallbacks.reloadShader && mCallbacks.reloadShader(error);
}
}
};
const auto route = postRoutes.find(path);
return route != postRoutes.end() && route->second(root, actionError);
}
bool ControlServer::HandleWebSocketUpgrade(UniqueSocket clientSocket, const HttpRequest& request)
{
const std::string clientKey = GetHeaderValue(request, "Sec-WebSocket-Key");
if (clientKey.empty())
{
SendHttpResponse(clientSocket.get(), "400 Bad Request", "text/plain", "Missing Sec-WebSocket-Key");
return true;
}
std::ostringstream response;
response << "HTTP/1.1 101 Switching Protocols\r\n";
response << "Upgrade: websocket\r\n";
response << "Connection: Upgrade\r\n";
response << "Sec-WebSocket-Accept: " << ComputeWebSocketAcceptKey(clientKey) << "\r\n\r\n";
const std::string payload = response.str();
send(clientSocket.get(), payload.c_str(), static_cast<int>(payload.size()), 0);
{
std::lock_guard<std::mutex> lock(mMutex);
ClientConnection client;
client.socket.reset(clientSocket.release());
client.websocket = true;
mClients.push_back(std::move(client));
BroadcastStateLocked();
}
return true;
}
bool ControlServer::SendWebSocketText(SOCKET clientSocket, const std::string& payload)
{
std::string frame;
frame.push_back(static_cast<char>(0x81));
if (payload.size() <= 125)
{
frame.push_back(static_cast<char>(payload.size()));
}
else if (payload.size() <= 65535)
{
frame.push_back(126);
frame.push_back(static_cast<char>((payload.size() >> 8) & 0xFF));
frame.push_back(static_cast<char>(payload.size() & 0xFF));
}
else
{
frame.push_back(127);
for (int shift = 56; shift >= 0; shift -= 8)
frame.push_back(static_cast<char>((payload.size() >> shift) & 0xFF));
}
frame.append(payload);
return send(clientSocket, frame.data(), static_cast<int>(frame.size()), 0) == static_cast<int>(frame.size());
}
void ControlServer::BroadcastStateLocked()
{
const std::string stateMessage = mCallbacks.getStateJson ? mCallbacks.getStateJson() : "{}";
for (auto it = mClients.begin(); it != mClients.end();)
{
if (!SendWebSocketText(it->socket.get(), stateMessage))
{
it = mClients.erase(it);
}
else
{
++it;
}
}
}
std::string ControlServer::LoadUiAsset(const std::string& relativePath, std::string& contentType) const
{
const std::filesystem::path sanitizedPath = std::filesystem::path(relativePath).lexically_normal();
if (!IsSafeUiPath(sanitizedPath))
return std::string();
const std::filesystem::path assetPath = mUiRoot / sanitizedPath;
contentType = GuessContentType(assetPath);
return LoadTextFile(assetPath);
}
std::string ControlServer::LoadTextFile(const std::filesystem::path& path) const
{
std::ifstream input(path, std::ios::binary);
if (!input)
return std::string();
std::ostringstream buffer;
buffer << input.rdbuf();
return buffer.str();
}
std::string ControlServer::BuildJsonResponse(bool success, const std::string& error) const
{
JsonValue response = JsonValue::MakeObject();
response.set("ok", JsonValue(success));
if (!error.empty())
response.set("error", JsonValue(error));
return SerializeJson(response, false);
}
std::string ControlServer::Base64Encode(const unsigned char* data, DWORD dataLength)
{
DWORD outputLength = 0;
CryptBinaryToStringA(data, dataLength, CRYPT_STRING_BASE64 | CRYPT_STRING_NOCRLF, NULL, &outputLength);
std::string encoded(outputLength, '\0');
CryptBinaryToStringA(data, dataLength, CRYPT_STRING_BASE64 | CRYPT_STRING_NOCRLF, &encoded[0], &outputLength);
if (!encoded.empty() && encoded.back() == '\0')
encoded.pop_back();
return encoded;
}
std::string ControlServer::ComputeWebSocketAcceptKey(const std::string& clientKey)
{
const std::string combined = clientKey + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
HCRYPTPROV provider = 0;
HCRYPTHASH hash = 0;
BYTE digest[20] = {};
DWORD digestLength = sizeof(digest);
CryptAcquireContext(&provider, NULL, NULL, PROV_RSA_FULL, CRYPT_VERIFYCONTEXT);
CryptCreateHash(provider, CALG_SHA1, 0, 0, &hash);
CryptHashData(hash, reinterpret_cast<const BYTE*>(combined.data()), static_cast<DWORD>(combined.size()), 0);
CryptGetHashParam(hash, HP_HASHVAL, digest, &digestLength, 0);
if (hash)
CryptDestroyHash(hash);
if (provider)
CryptReleaseContext(provider, 0);
return Base64Encode(digest, digestLength);
}
std::string ControlServer::GetHeaderValue(const HttpRequest& request, const std::string& headerName)
{
const auto header = request.headers.find(ToLower(headerName));
return header == request.headers.end() ? std::string() : header->second;
}
bool ControlServer::ParseHttpRequest(const std::string& rawRequest, HttpRequest& request)
{
const std::size_t requestLineEnd = rawRequest.find("\r\n");
if (requestLineEnd == std::string::npos)
return false;
const std::string requestLine = rawRequest.substr(0, requestLineEnd);
const std::size_t methodEnd = requestLine.find(' ');
if (methodEnd == std::string::npos)
return false;
const std::size_t pathEnd = requestLine.find(' ', methodEnd + 1);
if (pathEnd == std::string::npos)
return false;
request.method = requestLine.substr(0, methodEnd);
request.path = requestLine.substr(methodEnd + 1, pathEnd - methodEnd - 1);
request.headers.clear();
const std::size_t headersStart = requestLineEnd + 2;
const std::size_t bodySeparator = rawRequest.find("\r\n\r\n", headersStart);
const std::size_t headersEnd = bodySeparator == std::string::npos ? rawRequest.size() : bodySeparator;
for (std::size_t lineStart = headersStart; lineStart < headersEnd;)
{
const std::size_t lineEnd = rawRequest.find("\r\n", lineStart);
const std::size_t currentLineEnd = lineEnd == std::string::npos ? headersEnd : std::min(lineEnd, headersEnd);
const std::string line = rawRequest.substr(lineStart, currentLineEnd - lineStart);
const std::size_t separator = line.find(':');
if (separator != std::string::npos)
{
const std::string key = ToLower(line.substr(0, separator));
std::string value = line.substr(separator + 1);
const std::size_t first = value.find_first_not_of(" \t");
const std::size_t last = value.find_last_not_of(" \t");
request.headers[key] = first == std::string::npos ? std::string() : value.substr(first, last - first + 1);
}
if (lineEnd == std::string::npos || lineEnd >= headersEnd)
break;
lineStart = lineEnd + 2;
}
request.body = bodySeparator == std::string::npos ? std::string() : rawRequest.substr(bodySeparator + 4);
return !request.method.empty() && !request.path.empty();
}

View File

@@ -1,104 +0,0 @@
#pragma once
#include "NativeSockets.h"
#include <winsock2.h>
#include <atomic>
#include <filesystem>
#include <functional>
#include <map>
#include <mutex>
#include <string>
#include <thread>
#include <vector>
class JsonValue;
class ControlServer
{
public:
struct Callbacks
{
std::function<std::string()> getStateJson;
std::function<bool(const std::string&, std::string&)> addLayer;
std::function<bool(const std::string&, std::string&)> removeLayer;
std::function<bool(const std::string&, int, std::string&)> moveLayer;
std::function<bool(const std::string&, std::size_t, std::string&)> moveLayerToIndex;
std::function<bool(const std::string&, bool, std::string&)> setLayerBypass;
std::function<bool(const std::string&, const std::string&, std::string&)> setLayerShader;
std::function<bool(const std::string&, const std::string&, const std::string&, std::string&)> updateLayerParameter;
std::function<bool(const std::string&, std::string&)> resetLayerParameters;
std::function<bool(const std::string&, std::string&)> saveStackPreset;
std::function<bool(const std::string&, std::string&)> loadStackPreset;
std::function<bool(std::string&)> reloadShader;
};
ControlServer();
~ControlServer();
bool Start(const std::filesystem::path& uiRoot, const std::filesystem::path& docsRoot, unsigned short preferredPort, const Callbacks& callbacks, std::string& error);
void Stop();
void BroadcastState();
unsigned short GetPort() const { return mPort; }
private:
struct ClientConnection
{
UniqueSocket socket;
bool websocket = false;
};
struct HttpRequest
{
std::string method;
std::string path;
std::map<std::string, std::string> headers;
std::string body;
};
struct HttpResponse
{
std::string status;
std::string contentType;
std::string body;
bool broadcastState = false;
};
void ServerLoop();
bool HandleHttpClient(UniqueSocket clientSocket);
bool TryAcceptClient();
bool SendHttpResponse(SOCKET clientSocket, const HttpResponse& response);
bool SendHttpResponse(SOCKET clientSocket, const std::string& status, const std::string& contentType, const std::string& body);
bool HandleHttpRequest(UniqueSocket clientSocket, const std::string& request);
bool HandleWebSocketUpgrade(UniqueSocket clientSocket, const HttpRequest& request);
HttpResponse RouteHttpRequest(const HttpRequest& request);
HttpResponse ServeGetRequest(const HttpRequest& request) const;
HttpResponse ServeUiAsset(const std::string& relativePath) const;
HttpResponse ServeDocsAsset(const std::string& relativePath) const;
HttpResponse ServeOpenApiSpec() const;
HttpResponse ServeSwaggerDocs() const;
HttpResponse HandleApiPost(const HttpRequest& request);
bool InvokePostRoute(const std::string& path, const JsonValue& root, std::string& actionError);
bool SendWebSocketText(SOCKET clientSocket, const std::string& payload);
void BroadcastStateLocked();
std::string LoadUiAsset(const std::string& relativePath, std::string& contentType) const;
std::string LoadTextFile(const std::filesystem::path& path) const;
std::string BuildJsonResponse(bool success, const std::string& error = std::string()) const;
static std::string Base64Encode(const unsigned char* data, DWORD dataLength);
static std::string ComputeWebSocketAcceptKey(const std::string& clientKey);
static std::string GetHeaderValue(const HttpRequest& request, const std::string& headerName);
static bool ParseHttpRequest(const std::string& rawRequest, HttpRequest& request);
private:
std::filesystem::path mUiRoot;
std::filesystem::path mDocsRoot;
Callbacks mCallbacks;
UniqueSocket mListenSocket;
unsigned short mPort;
std::thread mThread;
std::atomic<bool> mRunning;
mutable std::mutex mMutex;
std::vector<ClientConnection> mClients;
};

View File

@@ -1,365 +0,0 @@
/* -LICENSE-START-
** Copyright (c) 2012 Blackmagic Design
**
** Permission is hereby granted, free of charge, to any person or organization
** obtaining a copy of the software and accompanying documentation (the
** "Software") to use, reproduce, display, distribute, sub-license, execute,
** and transmit the Software, and to prepare derivative works of the Software,
** and to permit third-parties to whom the Software is furnished to do so, in
** accordance with:
**
** (1) if the Software is obtained from Blackmagic Design, the End User License
** Agreement for the Software Development Kit ("EULA") available at
** https://www.blackmagicdesign.com/EULA/DeckLinkSDK; or
**
** (2) if the Software is obtained from any third party, such licensing terms
** as notified by that third party,
**
** and all subject to the following:
**
** (3) the copyright notices in the Software and this entire statement,
** including the above license grant, this restriction and the following
** disclaimer, must be included in all copies of the Software, in whole or in
** part, and all derivative works of the Software, unless such copies or
** derivative works are solely in the form of machine-executable object code
** generated by a source language processor.
**
** (4) THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
** OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
** FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT
** SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE
** FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE,
** ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
** DEALINGS IN THE SOFTWARE.
**
** A copy of the Software is available free of charge at
** https://www.blackmagicdesign.com/desktopvideo_sdk under the EULA.
**
** -LICENSE-END-
*/
//
// LoopThroughWithOpenGLCompositing.cpp
// LoopThroughWithOpenGLCompositing
//
#include "stdafx.h"
#include "resource.h"
#include "OpenGLComposite.h"
#ifndef WGL_CONTEXT_MAJOR_VERSION_ARB
#define WGL_CONTEXT_MAJOR_VERSION_ARB 0x2091
#endif
#ifndef WGL_CONTEXT_MINOR_VERSION_ARB
#define WGL_CONTEXT_MINOR_VERSION_ARB 0x2092
#endif
#ifndef WGL_CONTEXT_PROFILE_MASK_ARB
#define WGL_CONTEXT_PROFILE_MASK_ARB 0x9126
#endif
#ifndef WGL_CONTEXT_CORE_PROFILE_BIT_ARB
#define WGL_CONTEXT_CORE_PROFILE_BIT_ARB 0x00000001
#endif
#define MAX_LOADSTRING 100
// Declaration for Window procedure
LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam);
typedef HGLRC (WINAPI* PFNWGLCREATECONTEXTATTRIBSARBPROC)(HDC hdc, HGLRC hShareContext, const int* attribList);
void ShowUnhandledExceptionMessage(const char* prefix)
{
try
{
throw;
}
catch (const std::exception& exception)
{
std::string message = std::string(prefix) + "\n\n" + exception.what();
MessageBoxA(NULL, message.c_str(), "Unhandled exception", MB_OK | MB_ICONERROR);
}
catch (...)
{
MessageBoxA(NULL, prefix, "Unhandled exception", MB_OK | MB_ICONERROR);
}
}
// Select the pixel format for a given device context
void SetDCPixelFormat(HDC hDC)
{
int nPixelFormat;
static PIXELFORMATDESCRIPTOR pfd = {
sizeof(PIXELFORMATDESCRIPTOR), // Size of this structure
1, // Version of this structure
PFD_DRAW_TO_WINDOW | // Draw to Window (not to bitmap)
PFD_SUPPORT_OPENGL | // Support OpenGL calls in window
PFD_DOUBLEBUFFER, // Double buffered mode
PFD_TYPE_RGBA, // RGBA Color mode
32, // Want 32 bit color
0,0,0,0,0,0, // Not used to select mode
0,0, // Not used to select mode
0,0,0,0,0, // Not used to select mode
16, // Size of depth buffer
0, // Not used
0, // Not used
0, // Not used
0, // Not used
0,0,0 }; // Not used
// Choose a pixel format that best matches that described in pfd
nPixelFormat = ChoosePixelFormat(hDC, &pfd);
// Set the pixel format for the device context
SetPixelFormat(hDC, nPixelFormat, &pfd);
}
HGLRC CreateModernOpenGLContext(HDC hDC)
{
PFNWGLCREATECONTEXTATTRIBSARBPROC wglCreateContextAttribsARB =
reinterpret_cast<PFNWGLCREATECONTEXTATTRIBSARBPROC>(wglGetProcAddress("wglCreateContextAttribsARB"));
if (!wglCreateContextAttribsARB)
return NULL;
const int versionCandidates[][2] =
{
{ 4, 5 },
{ 4, 3 },
{ 3, 3 }
};
for (const auto& version : versionCandidates)
{
const int attribs[] =
{
WGL_CONTEXT_MAJOR_VERSION_ARB, version[0],
WGL_CONTEXT_MINOR_VERSION_ARB, version[1],
WGL_CONTEXT_PROFILE_MASK_ARB, WGL_CONTEXT_CORE_PROFILE_BIT_ARB,
0
};
HGLRC modernContext = wglCreateContextAttribsARB(hDC, 0, attribs);
if (modernContext != NULL)
return modernContext;
}
return NULL;
}
int APIENTRY WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow)
{
MSG msg; // Windows message structure
WNDCLASS wc; // Windows class structure
HWND hWnd; // Storeage for window handle
TCHAR szTitle[MAX_LOADSTRING]; // The title bar text
TCHAR szWindowClass[MAX_LOADSTRING]; // the main window class name
// Initialize global strings
LoadString(hInstance, IDS_APP_TITLE, szTitle, MAX_LOADSTRING);
LoadString(hInstance, IDC_OPENGLOUTPUT, szWindowClass, MAX_LOADSTRING);
// Register Window style
wc.style = CS_HREDRAW | CS_VREDRAW | CS_OWNDC;
wc.lpfnWndProc = (WNDPROC) WndProc;
wc.cbClsExtra = 0;
wc.cbWndExtra = 0;
wc.hInstance = hInstance;
wc.hIcon = NULL;
wc.hCursor = LoadCursor(NULL, IDC_ARROW);
// No need for background brush for OpenGL window
wc.hbrBackground = NULL;
wc.lpszMenuName = NULL;
wc.lpszClassName = szWindowClass;
// Register the window class
if (RegisterClass(&wc) == 0)
return FALSE;
// Create the main application window
hWnd = CreateWindow(szWindowClass, szTitle, WS_OVERLAPPEDWINDOW | WS_CLIPCHILDREN | WS_CLIPSIBLINGS,
CW_USEDEFAULT, 0, 250, 250, NULL, NULL, hInstance, NULL);
// If window was not created, quit
if (hWnd == NULL)
return FALSE;
// Display the window
ShowWindow(hWnd,SW_SHOW);
UpdateWindow(hWnd);
// Process application messages until the application closes
while (GetMessage(&msg, NULL, 0, 0))
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}
return (int)msg.wParam;
}
// Window procedure, handles all messages for this program
LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
static HGLRC hRC = NULL; // Permenant Rendering context
static HDC hDC = NULL; // Private GDI Device context
static OpenGLComposite* pOpenGLComposite = NULL;
static bool sInteractiveResize = false;
switch (message)
{
// Window creation, setup for OpenGL context
case WM_CREATE:
{
try
{
// Store the device context
hDC = GetDC(hWnd);
// Select the pixel format
SetDCPixelFormat(hDC);
// Create the rendering context and make it current
hRC = wglCreateContext(hDC);
wglMakeCurrent(hDC, hRC);
HGLRC modernRC = CreateModernOpenGLContext(hDC);
if (modernRC == NULL)
{
MessageBox(NULL, _T("This application requires an OpenGL 3.3+ core profile context."), _T("OpenGL initialization Error."), MB_OK);
PostMessage(hWnd, WM_CLOSE, 0, 0);
break;
}
wglMakeCurrent(NULL, NULL);
wglDeleteContext(hRC);
hRC = modernRC;
wglMakeCurrent(hDC, hRC);
// Initialize COM
HRESULT result;
result = CoInitialize(NULL);
if (FAILED(result))
{
MessageBox(NULL, _T("Initialization of COM failed."), _T("Application initialization Error."),MB_OK);
PostMessage(hWnd, WM_CLOSE, 0, 0);
break;
}
// Setup OpenGL and DeckLink capture and playout object
pOpenGLComposite = new OpenGLComposite(hWnd, hDC, hRC);
if (pOpenGLComposite->InitDeckLink())
{
wglMakeCurrent( NULL, NULL );
if (pOpenGLComposite->Start())
break; // success
}
// Failed to initialize - cleanup
delete pOpenGLComposite;
pOpenGLComposite = NULL;
PostMessage(hWnd, WM_CLOSE, 0, 0);
break;
}
catch (...)
{
ShowUnhandledExceptionMessage("Startup failed while creating the OpenGL/DeckLink runtime.");
PostMessage(hWnd, WM_CLOSE, 0, 0);
break;
}
}
case WM_DESTROY:
try
{
if (pOpenGLComposite)
{
pOpenGLComposite->Stop();
delete pOpenGLComposite;
}
}
catch (...)
{
ShowUnhandledExceptionMessage("Shutdown failed while tearing down the OpenGL/DeckLink runtime.");
}
// Deselect the current rendering context and delete it
wglMakeCurrent(hDC, NULL);
wglDeleteContext(hRC);
// Tell the application to terminate after the window is gone
PostQuitMessage(0);
break;
case WM_ENTERSIZEMOVE:
sInteractiveResize = true;
break;
case WM_EXITSIZEMOVE:
sInteractiveResize = false;
if (pOpenGLComposite)
{
RECT clientRect = {};
if (GetClientRect(hWnd, &clientRect))
pOpenGLComposite->resizeGL(static_cast<WORD>(clientRect.right - clientRect.left), static_cast<WORD>(clientRect.bottom - clientRect.top));
}
InvalidateRect(hWnd, NULL, FALSE);
break;
case WM_SIZE:
try
{
if (pOpenGLComposite)
pOpenGLComposite->resizeGL(LOWORD(lParam), HIWORD(lParam));
}
catch (...)
{
ShowUnhandledExceptionMessage("Resize failed inside the OpenGL runtime.");
}
break;
case WM_ERASEBKGND:
return 1;
case WM_PAINT:
try
{
PAINTSTRUCT paint = {};
BeginPaint(hWnd, &paint);
EndPaint(hWnd, &paint);
if (!sInteractiveResize && pOpenGLComposite)
{
wglMakeCurrent(hDC, hRC);
pOpenGLComposite->paintGL();
wglMakeCurrent( NULL, NULL );
}
}
catch (...)
{
wglMakeCurrent( NULL, NULL );
ShowUnhandledExceptionMessage("Paint failed inside the OpenGL runtime.");
}
break;
case WM_KEYDOWN:
try
{
if (pOpenGLComposite && (wParam == 'R' || wParam == 'r'))
{
pOpenGLComposite->ReloadShader();
InvalidateRect(hWnd, NULL, FALSE);
}
}
catch (...)
{
ShowUnhandledExceptionMessage("Shader reload failed inside the OpenGL runtime.");
}
break;
default:
return (DefWindowProc(hWnd, message, wParam, lParam));
}
return (0L);
}

View File

@@ -1,47 +0,0 @@
/* -LICENSE-START-
** Copyright (c) 2012 Blackmagic Design
**
** Permission is hereby granted, free of charge, to any person or organization
** obtaining a copy of the software and accompanying documentation (the
** "Software") to use, reproduce, display, distribute, sub-license, execute,
** and transmit the Software, and to prepare derivative works of the Software,
** and to permit third-parties to whom the Software is furnished to do so, in
** accordance with:
**
** (1) if the Software is obtained from Blackmagic Design, the End User License
** Agreement for the Software Development Kit ("EULA") available at
** https://www.blackmagicdesign.com/EULA/DeckLinkSDK; or
**
** (2) if the Software is obtained from any third party, such licensing terms
** as notified by that third party,
**
** and all subject to the following:
**
** (3) the copyright notices in the Software and this entire statement,
** including the above license grant, this restriction and the following
** disclaimer, must be included in all copies of the Software, in whole or in
** part, and all derivative works of the Software, unless such copies or
** derivative works are solely in the form of machine-executable object code
** generated by a source language processor.
**
** (4) THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
** OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
** FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT
** SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE
** FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE,
** ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
** DEALINGS IN THE SOFTWARE.
**
** A copy of the Software is available free of charge at
** https://www.blackmagicdesign.com/desktopvideo_sdk under the EULA.
**
** -LICENSE-END-
*/
//
// LoopThroughWithOpenGLCompositing.h
// LoopThroughWithOpenGLCompositing
//
#pragma once
#include "resource.h"

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

View File

@@ -1,95 +0,0 @@
// Microsoft Visual C++ generated resource script.
//
#include "resource.h"
#define APSTUDIO_READONLY_SYMBOLS
/////////////////////////////////////////////////////////////////////////////
//
// Generated from the TEXTINCLUDE 2 resource.
//
#ifndef APSTUDIO_INVOKED
#include "targetver.h"
#endif
#define APSTUDIO_HIDDEN_SYMBOLS
#include "windows.h"
#undef APSTUDIO_HIDDEN_SYMBOLS
/////////////////////////////////////////////////////////////////////////////
#undef APSTUDIO_READONLY_SYMBOLS
/////////////////////////////////////////////////////////////////////////////
// English (U.S.) resources
#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU)
#ifdef _WIN32
LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US
#pragma code_page(1252)
#endif //_WIN32
/////////////////////////////////////////////////////////////////////////////
//
// Icon
//
// Icon with lowest ID value placed first to ensure application icon
// remains consistent on all systems.
IDI_OPENGLOUTPUT ICON "LoopThroughWithOpenGLCompositing.ico"
IDI_SMALL ICON "small.ico"
#ifdef APSTUDIO_INVOKED
/////////////////////////////////////////////////////////////////////////////
//
// TEXTINCLUDE
//
1 TEXTINCLUDE
BEGIN
"resource.h\0"
END
2 TEXTINCLUDE
BEGIN
"#ifndef APSTUDIO_INVOKED\r\n"
"#include ""targetver.h""\r\n"
"#endif\r\n"
"#define APSTUDIO_HIDDEN_SYMBOLS\r\n"
"#include ""windows.h""\r\n"
"#undef APSTUDIO_HIDDEN_SYMBOLS\r\n"
"\0"
END
3 TEXTINCLUDE
BEGIN
"\r\n"
"\0"
END
#endif // APSTUDIO_INVOKED
/////////////////////////////////////////////////////////////////////////////
//
// String Table
//
STRINGTABLE
BEGIN
IDS_APP_TITLE "Video Shader Toys"
IDC_OPENGLOUTPUT "OPENGLOUTPUT"
END
#endif // English (U.S.) resources
/////////////////////////////////////////////////////////////////////////////
#ifndef APSTUDIO_INVOKED
/////////////////////////////////////////////////////////////////////////////
//
// Generated from the TEXTINCLUDE 3 resource.
//
/////////////////////////////////////////////////////////////////////////////
#endif // not APSTUDIO_INVOKED

View File

@@ -1,28 +0,0 @@
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio 2013
VisualStudioVersion = 12.0.21005.1
MinimumVisualStudioVersion = 10.0.40219.1
Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "LoopThroughWithOpenGLCompositing", "LoopThroughWithOpenGLCompositing.vcxproj", "{92C79085-CA51-4008-95DB-5403D2E19885}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Win32 = Debug|Win32
Debug|x64 = Debug|x64
Release|Win32 = Release|Win32
Release|x64 = Release|x64
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{92C79085-CA51-4008-95DB-5403D2E19885}.Debug|Win32.ActiveCfg = Debug|Win32
{92C79085-CA51-4008-95DB-5403D2E19885}.Debug|Win32.Build.0 = Debug|Win32
{92C79085-CA51-4008-95DB-5403D2E19885}.Debug|x64.ActiveCfg = Debug|x64
{92C79085-CA51-4008-95DB-5403D2E19885}.Debug|x64.Build.0 = Debug|x64
{92C79085-CA51-4008-95DB-5403D2E19885}.Release|Win32.ActiveCfg = Release|Win32
{92C79085-CA51-4008-95DB-5403D2E19885}.Release|Win32.Build.0 = Release|Win32
{92C79085-CA51-4008-95DB-5403D2E19885}.Release|x64.ActiveCfg = Release|x64
{92C79085-CA51-4008-95DB-5403D2E19885}.Release|x64.Build.0 = Release|x64
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
EndGlobal

View File

@@ -1,232 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<Project DefaultTargets="Build" ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup Label="ProjectConfigurations">
<ProjectConfiguration Include="Debug|Win32">
<Configuration>Debug</Configuration>
<Platform>Win32</Platform>
</ProjectConfiguration>
<ProjectConfiguration Include="Debug|x64">
<Configuration>Debug</Configuration>
<Platform>x64</Platform>
</ProjectConfiguration>
<ProjectConfiguration Include="Release|Win32">
<Configuration>Release</Configuration>
<Platform>Win32</Platform>
</ProjectConfiguration>
<ProjectConfiguration Include="Release|x64">
<Configuration>Release</Configuration>
<Platform>x64</Platform>
</ProjectConfiguration>
</ItemGroup>
<PropertyGroup Label="Globals">
<ProjectGuid>{92C79085-CA51-4008-95DB-5403D2E19885}</ProjectGuid>
<RootNamespace>LoopThroughWithOpenGLCompositing</RootNamespace>
<Keyword>Win32Proj</Keyword>
<WindowsTargetPlatformVersion>10.0.26100.0</WindowsTargetPlatformVersion>
</PropertyGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|Win32'" Label="Configuration">
<ConfigurationType>Application</ConfigurationType>
<PlatformToolset>v143</PlatformToolset>
<CharacterSet>MultiByte</CharacterSet>
<WholeProgramOptimization>true</WholeProgramOptimization>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'" Label="Configuration">
<ConfigurationType>Application</ConfigurationType>
<PlatformToolset>v143</PlatformToolset>
<CharacterSet>MultiByte</CharacterSet>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'" Label="Configuration">
<ConfigurationType>Application</ConfigurationType>
<PlatformToolset>v143</PlatformToolset>
<CharacterSet>MultiByte</CharacterSet>
<WholeProgramOptimization>true</WholeProgramOptimization>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'" Label="Configuration">
<ConfigurationType>Application</ConfigurationType>
<PlatformToolset>v143</PlatformToolset>
<CharacterSet>MultiByte</CharacterSet>
</PropertyGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.props" />
<ImportGroup Label="ExtensionSettings">
</ImportGroup>
<ImportGroup Condition="'$(Configuration)|$(Platform)'=='Release|Win32'" Label="PropertySheets">
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
</ImportGroup>
<ImportGroup Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'" Label="PropertySheets">
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
</ImportGroup>
<ImportGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'" Label="PropertySheets">
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
</ImportGroup>
<ImportGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'" Label="PropertySheets">
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
</ImportGroup>
<PropertyGroup Label="UserMacros" />
<PropertyGroup>
<_ProjectFileVersion>12.0.21005.1</_ProjectFileVersion>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">
<OutDir>$(SolutionDir)$(Configuration)\</OutDir>
<IntDir>$(Configuration)\</IntDir>
<LinkIncremental>true</LinkIncremental>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
<OutDir>$(SolutionDir)$(Platform)\$(Configuration)\</OutDir>
<IntDir>$(Platform)\$(Configuration)\</IntDir>
<LinkIncremental>true</LinkIncremental>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">
<OutDir>$(SolutionDir)$(Configuration)\</OutDir>
<IntDir>$(Configuration)\</IntDir>
<LinkIncremental>false</LinkIncremental>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
<OutDir>$(SolutionDir)$(Platform)\$(Configuration)\</OutDir>
<IntDir>$(Platform)\$(Configuration)\</IntDir>
<LinkIncremental>false</LinkIncremental>
</PropertyGroup>
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">
<ClCompile>
<Optimization>Disabled</Optimization>
<AdditionalIncludeDirectories>..\..\3rdParty\Blackmagic DeckLink SDK 16.0\Win\Samples\NVIDIA_GPUDirect\include;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
<PreprocessorDefinitions>WIN32;_DEBUG;_WINDOWS;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<BasicRuntimeChecks>EnableFastChecks</BasicRuntimeChecks>
<RuntimeLibrary>MultiThreadedDebugDLL</RuntimeLibrary>
<PrecompiledHeader />
<WarningLevel>Level3</WarningLevel>
<DebugInformationFormat>EditAndContinue</DebugInformationFormat>
<LanguageStandard>stdcpp17</LanguageStandard>
</ClCompile>
<Link>
<AdditionalDependencies>dvp.lib;opengl32.lib;Glu32.lib;%(AdditionalDependencies)</AdditionalDependencies>
<AdditionalLibraryDirectories>..\..\3rdParty\Blackmagic DeckLink SDK 16.0\Win\Samples\NVIDIA_GPUDirect\lib\win32;%(AdditionalLibraryDirectories)</AdditionalLibraryDirectories>
<GenerateDebugInformation>true</GenerateDebugInformation>
<SubSystem>Windows</SubSystem>
<TargetMachine>MachineX86</TargetMachine>
</Link>
<PostBuildEvent>
<Command>copy /y "..\..\3rdParty\Blackmagic DeckLink SDK 16.0\Win\Samples\NVIDIA_GPUDirect\bin\$(Platform)\dvp.dll" "$(TargetDir)"</Command>
</PostBuildEvent>
</ItemDefinitionGroup>
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
<Midl>
<TargetEnvironment>X64</TargetEnvironment>
</Midl>
<ClCompile>
<Optimization>Disabled</Optimization>
<AdditionalIncludeDirectories>..\..\3rdParty\Blackmagic DeckLink SDK 16.0\Win\Samples\NVIDIA_GPUDirect\include;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
<PreprocessorDefinitions>WIN32;_DEBUG;_WINDOWS;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<BasicRuntimeChecks>EnableFastChecks</BasicRuntimeChecks>
<RuntimeLibrary>MultiThreadedDebugDLL</RuntimeLibrary>
<PrecompiledHeader />
<WarningLevel>Level3</WarningLevel>
<DebugInformationFormat>ProgramDatabase</DebugInformationFormat>
<LanguageStandard>stdcpp17</LanguageStandard>
</ClCompile>
<Link>
<AdditionalDependencies>dvp.lib;opengl32.lib;Glu32.lib;%(AdditionalDependencies)</AdditionalDependencies>
<AdditionalLibraryDirectories>..\..\3rdParty\Blackmagic DeckLink SDK 16.0\Win\Samples\NVIDIA_GPUDirect\lib\x64;%(AdditionalLibraryDirectories)</AdditionalLibraryDirectories>
<GenerateDebugInformation>true</GenerateDebugInformation>
<SubSystem>Windows</SubSystem>
<TargetMachine>MachineX64</TargetMachine>
</Link>
<PostBuildEvent>
<Command>copy /y "..\..\3rdParty\Blackmagic DeckLink SDK 16.0\Win\Samples\NVIDIA_GPUDirect\bin\$(Platform)\dvp.dll" "$(TargetDir)"</Command>
</PostBuildEvent>
</ItemDefinitionGroup>
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">
<ClCompile>
<Optimization>MaxSpeed</Optimization>
<IntrinsicFunctions>true</IntrinsicFunctions>
<AdditionalIncludeDirectories>..\..\3rdParty\Blackmagic DeckLink SDK 16.0\Win\Samples\NVIDIA_GPUDirect\include;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
<PreprocessorDefinitions>WIN32;NDEBUG;_WINDOWS;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<RuntimeLibrary>MultiThreadedDLL</RuntimeLibrary>
<FunctionLevelLinking>true</FunctionLevelLinking>
<PrecompiledHeader />
<WarningLevel>Level3</WarningLevel>
<DebugInformationFormat>ProgramDatabase</DebugInformationFormat>
<LanguageStandard>stdcpp17</LanguageStandard>
</ClCompile>
<Link>
<AdditionalDependencies>dvp.lib;opengl32.lib;Glu32.lib;%(AdditionalDependencies)</AdditionalDependencies>
<AdditionalLibraryDirectories>..\..\3rdParty\Blackmagic DeckLink SDK 16.0\Win\Samples\NVIDIA_GPUDirect\lib\win32;%(AdditionalLibraryDirectories)</AdditionalLibraryDirectories>
<GenerateDebugInformation>true</GenerateDebugInformation>
<SubSystem>Windows</SubSystem>
<OptimizeReferences>true</OptimizeReferences>
<EnableCOMDATFolding>true</EnableCOMDATFolding>
<TargetMachine>MachineX86</TargetMachine>
</Link>
<PostBuildEvent>
<Message>Copy nececssary DLLs to target directory</Message>
<Command>copy /y "..\..\3rdParty\Blackmagic DeckLink SDK 16.0\Win\Samples\NVIDIA_GPUDirect\bin\$(Platform)\dvp.dll" "$(TargetDir)"</Command>
</PostBuildEvent>
</ItemDefinitionGroup>
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
<Midl>
<TargetEnvironment>X64</TargetEnvironment>
</Midl>
<ClCompile>
<Optimization>MaxSpeed</Optimization>
<IntrinsicFunctions>true</IntrinsicFunctions>
<AdditionalIncludeDirectories>..\..\3rdParty\Blackmagic DeckLink SDK 16.0\Win\Samples\NVIDIA_GPUDirect\include;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
<PreprocessorDefinitions>WIN32;NDEBUG;_WINDOWS;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<RuntimeLibrary>MultiThreadedDLL</RuntimeLibrary>
<FunctionLevelLinking>true</FunctionLevelLinking>
<PrecompiledHeader />
<WarningLevel>Level3</WarningLevel>
<DebugInformationFormat>ProgramDatabase</DebugInformationFormat>
<LanguageStandard>stdcpp17</LanguageStandard>
</ClCompile>
<Link>
<AdditionalDependencies>dvp.lib;opengl32.lib;Glu32.lib;%(AdditionalDependencies)</AdditionalDependencies>
<AdditionalLibraryDirectories>..\..\3rdParty\Blackmagic DeckLink SDK 16.0\Win\Samples\NVIDIA_GPUDirect\lib\x64;%(AdditionalLibraryDirectories)</AdditionalLibraryDirectories>
<GenerateDebugInformation>true</GenerateDebugInformation>
<SubSystem>Windows</SubSystem>
<OptimizeReferences>true</OptimizeReferences>
<EnableCOMDATFolding>true</EnableCOMDATFolding>
<TargetMachine>MachineX64</TargetMachine>
</Link>
<PostBuildEvent>
<Command>copy /y "..\..\3rdParty\Blackmagic DeckLink SDK 16.0\Win\Samples\NVIDIA_GPUDirect\bin\$(Platform)\dvp.dll" "$(TargetDir)"</Command>
</PostBuildEvent>
</ItemDefinitionGroup>
<ItemGroup>
<ClCompile Include="GLExtensions.cpp" />
<ClCompile Include="LoopThroughWithOpenGLCompositing.cpp" />
<ClCompile Include="OpenGLComposite.cpp" />
<ClCompile Include="stdafx.cpp">
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">Create</PrecompiledHeader>
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">Create</PrecompiledHeader>
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">Create</PrecompiledHeader>
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|x64'">Create</PrecompiledHeader>
</ClCompile>
<ClCompile Include="VideoFrameTransfer.cpp" />
<ClCompile Include="DeckLinkAPI_i.c" />
</ItemGroup>
<ItemGroup>
<ClInclude Include="GLExtensions.h" />
<ClInclude Include="LoopThroughWithOpenGLCompositing.h" />
<ClInclude Include="OpenGLComposite.h" />
<ClInclude Include="resource.h" />
<ClInclude Include="stdafx.h" />
<ClInclude Include="targetver.h" />
<ClInclude Include="VideoFrameTransfer.h" />
</ItemGroup>
<ItemGroup>
<Image Include="LoopThroughWithOpenGLCompositing.ico" />
<Image Include="small.ico" />
</ItemGroup>
<ItemGroup>
<None Include="video_effect.slang" />
</ItemGroup>
<ItemGroup>
<ResourceCompile Include="LoopThroughWithOpenGLCompositing.rc" />
</ItemGroup>
<ItemGroup>
<Midl Include="..\..\3rdParty\Blackmagic DeckLink SDK 16.0\Win\include\DeckLinkAPI.idl" />
</ItemGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
<ImportGroup Label="ExtensionTargets">
</ImportGroup>
</Project>

View File

@@ -1,86 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup>
<Filter Include="Source Files">
<UniqueIdentifier>{4FC737F1-C7A5-4376-A066-2A32D752A2FF}</UniqueIdentifier>
<Extensions>cpp;c;cc;cxx;def;odl;idl;hpj;bat;asm;asmx</Extensions>
</Filter>
<Filter Include="Header Files">
<UniqueIdentifier>{93995380-89BD-4b04-88EB-625FBE52EBFB}</UniqueIdentifier>
<Extensions>h;hpp;hxx;hm;inl;inc;xsd</Extensions>
</Filter>
<Filter Include="Resource Files">
<UniqueIdentifier>{67DA6AB6-F800-4c08-8B7A-83BB121AAD01}</UniqueIdentifier>
<Extensions>rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav</Extensions>
</Filter>
<Filter Include="DeckLink API">
<UniqueIdentifier>{1eab21d6-58f8-49e0-929b-8a4482e04756}</UniqueIdentifier>
</Filter>
</ItemGroup>
<ItemGroup>
<ClCompile Include="GLExtensions.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="LoopThroughWithOpenGLCompositing.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="OpenGLComposite.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="stdafx.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="VideoFrameTransfer.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="DeckLinkAPI_i.c">
<Filter>DeckLink API</Filter>
</ClCompile>
</ItemGroup>
<ItemGroup>
<ClInclude Include="GLExtensions.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="LoopThroughWithOpenGLCompositing.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="OpenGLComposite.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="resource.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="stdafx.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="targetver.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="VideoFrameTransfer.h">
<Filter>Header Files</Filter>
</ClInclude>
</ItemGroup>
<ItemGroup>
<Image Include="LoopThroughWithOpenGLCompositing.ico">
<Filter>Resource Files</Filter>
</Image>
<Image Include="small.ico">
<Filter>Resource Files</Filter>
</Image>
</ItemGroup>
<ItemGroup>
<ResourceCompile Include="LoopThroughWithOpenGLCompositing.rc">
<Filter>Resource Files</Filter>
</ResourceCompile>
</ItemGroup>
<ItemGroup>
<Midl Include="..\..\include\DeckLinkAPI.idl">
<Filter>DeckLink API</Filter>
</Midl>
</ItemGroup>
<ItemGroup>
<None Include="video_effect.slang">
<Filter>Resource Files</Filter>
</None>
</ItemGroup>
</Project>

File diff suppressed because it is too large Load Diff

View File

@@ -1,407 +0,0 @@
/* -LICENSE-START-
** Copyright (c) 2012 Blackmagic Design
**
** Permission is hereby granted, free of charge, to any person or organization
** obtaining a copy of the software and accompanying documentation (the
** "Software") to use, reproduce, display, distribute, sub-license, execute,
** and transmit the Software, and to prepare derivative works of the Software,
** and to permit third-parties to whom the Software is furnished to do so, in
** accordance with:
**
** (1) if the Software is obtained from Blackmagic Design, the End User License
** Agreement for the Software Development Kit ("EULA") available at
** https://www.blackmagicdesign.com/EULA/DeckLinkSDK; or
**
** (2) if the Software is obtained from any third party, such licensing terms
** as notified by that third party,
**
** and all subject to the following:
**
** (3) the copyright notices in the Software and this entire statement,
** including the above license grant, this restriction and the following
** disclaimer, must be included in all copies of the Software, in whole or in
** part, and all derivative works of the Software, unless such copies or
** derivative works are solely in the form of machine-executable object code
** generated by a source language processor.
**
** (4) THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
** OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
** FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT
** SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE
** FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE,
** ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
** DEALINGS IN THE SOFTWARE.
**
** A copy of the Software is available free of charge at
** https://www.blackmagicdesign.com/desktopvideo_sdk under the EULA.
**
** -LICENSE-END-
*/
#ifndef __OPENGL_COMPOSITE_H__
#define __OPENGL_COMPOSITE_H__
#include <windows.h>
#include <process.h>
#include <tchar.h>
#include <gl/gl.h>
#include <gl/glu.h>
#include <objbase.h>
#include <atlbase.h>
#include <comutil.h>
#include "DeckLinkAPI_h.h"
#include "AudioSupport.h"
#include "VideoFrameTransfer.h"
#include "RuntimeHost.h"
#include <atomic>
#include <condition_variable>
#include <cstdint>
#include <functional>
#include <map>
#include <memory>
#include <mutex>
#include <thread>
#include <vector>
#include <deque>
class PlayoutDelegate;
class CaptureDelegate;
class PinnedMemoryAllocator;
class ControlServer;
class OscServer;
class OpenGLComposite
{
public:
OpenGLComposite(HWND hWnd, HDC hDC, HGLRC hRC);
~OpenGLComposite();
bool InitDeckLink();
bool Start();
bool Stop();
bool ReloadShader();
std::string GetRuntimeStateJson() const;
bool AddLayer(const std::string& shaderId, std::string& error);
bool RemoveLayer(const std::string& layerId, std::string& error);
bool MoveLayer(const std::string& layerId, int direction, std::string& error);
bool MoveLayerToIndex(const std::string& layerId, std::size_t targetIndex, std::string& error);
bool SetLayerBypass(const std::string& layerId, bool bypassed, std::string& error);
bool SetLayerShader(const std::string& layerId, const std::string& shaderId, std::string& error);
bool UpdateLayerParameterJson(const std::string& layerId, const std::string& parameterId, const std::string& valueJson, std::string& error);
bool UpdateLayerParameterByControlKeyJson(const std::string& layerKey, const std::string& parameterKey, const std::string& valueJson, std::string& error);
bool ResetLayerParameters(const std::string& layerId, std::string& error);
bool SaveStackPreset(const std::string& presetName, std::string& error);
bool LoadStackPreset(const std::string& presetName, std::string& error);
void resizeGL(WORD width, WORD height);
void paintGL();
void VideoFrameArrived(IDeckLinkVideoInputFrame* inputFrame, bool hasNoInputSource);
void AudioPacketArrived(IDeckLinkAudioInputPacket* audioPacket);
HRESULT RenderAudioSamples(BOOL preroll);
HRESULT ScheduleAudioToWaterLevel();
void AudioSchedulingLoop();
void PlayoutFrameCompleted(IDeckLinkVideoFrame* completedFrame, BMDOutputFrameCompletionResult result);
private:
void resizeWindow(int width, int height);
bool CheckOpenGLExtensions();
CaptureDelegate* mCaptureDelegate;
PlayoutDelegate* mPlayoutDelegate;
HWND hGLWnd;
HDC hGLDC;
HGLRC hGLRC;
CRITICAL_SECTION pMutex;
// DeckLink
IDeckLinkInput* mDLInput;
IDeckLinkOutput* mDLOutput;
IDeckLinkConfiguration* mDLInputConfiguration;
IDeckLinkKeyer* mDLKeyer;
std::deque<IDeckLinkMutableVideoFrame*> mDLOutputVideoFrameQueue;
PinnedMemoryAllocator* mPlayoutAllocator;
BMDTimeValue mFrameDuration;
BMDTimeScale mFrameTimescale;
unsigned mTotalPlayoutFrames;
uint64_t mAudioOutputSampleTime;
unsigned mInputFrameWidth;
unsigned mInputFrameHeight;
unsigned mOutputFrameWidth;
unsigned mOutputFrameHeight;
std::string mInputDisplayModeName;
std::string mOutputDisplayModeName;
bool mHasNoInputSource;
std::string mDeckLinkOutputModelName;
bool mDeckLinkSupportsInternalKeying;
bool mDeckLinkSupportsExternalKeying;
bool mDeckLinkKeyerInterfaceAvailable;
bool mDeckLinkExternalKeyingActive;
std::string mDeckLinkStatusMessage;
// OpenGL data
bool mFastTransferExtensionAvailable;
GLuint mCaptureTexture;
GLuint mDecodedTexture;
GLuint mLayerTempTexture;
GLuint mFBOTexture;
GLuint mOutputTexture;
GLuint mAudioDataTexture;
GLuint mUnpinnedTextureBuffer;
GLuint mDecodeFrameBuf;
GLuint mLayerTempFrameBuf;
GLuint mIdFrameBuf;
GLuint mOutputFrameBuf;
GLuint mIdColorBuf;
GLuint mIdDepthBuf;
GLuint mFullscreenVAO;
GLuint mGlobalParamsUBO;
GLuint mDecodeProgram;
GLuint mDecodeVertexShader;
GLuint mDecodeFragmentShader;
GLsizeiptr mGlobalParamsUBOSize;
int mViewWidth;
int mViewHeight;
std::unique_ptr<RuntimeHost> mRuntimeHost;
std::unique_ptr<ControlServer> mControlServer;
std::unique_ptr<OscServer> mOscServer;
bool mAudioEnabled;
bool mAudioOutputEnabled;
bool mAudioScheduleEnabled;
bool mAudioPrerollEnabled;
bool mAudioScheduleSilence;
bool mAudioScheduleTone;
bool mAudioPrerolling;
std::atomic<bool> mAudioSchedulerRunning;
std::atomic<bool> mPlayoutCallbackActive;
std::thread mAudioSchedulerThread;
std::mutex mAudioStateMutex;
std::mutex mAudioAnalyzerMutex;
AudioAnalyzer mAudioAnalyzer;
AudioAnalysisSnapshot mAudioAnalysis;
struct TimestampedAudioPacket
{
AudioFrameBlock block;
std::vector<int32_t> scheduledOutputSamples;
BMDTimeValue streamTime = 0;
};
std::deque<TimestampedAudioPacket> mAudioPacketQueue;
std::deque<TimestampedAudioPacket> mScheduledAudioPacketRetainQueue;
std::deque<int32_t> mAudioSampleQueue;
std::condition_variable mAudioPacketQueued;
unsigned mQueuedAudioSampleFrames = 0;
uint64_t mAudioUnderrunCount = 0;
uint64_t mAudioToneSampleIndex = 0;
bool mHasFirstAudioPacketTime = false;
BMDTimeValue mFirstAudioPacketTime = 0;
struct LayerProgram
{
struct TextureBinding
{
std::string samplerName;
std::filesystem::path sourcePath;
GLuint texture = 0;
};
std::string layerId;
std::string shaderId;
GLuint program = 0;
GLuint vertexShader = 0;
GLuint fragmentShader = 0;
std::vector<TextureBinding> textureBindings;
};
std::vector<LayerProgram> mLayerPrograms;
struct HistorySlot
{
GLuint texture = 0;
GLuint framebuffer = 0;
};
struct HistoryRing
{
std::vector<HistorySlot> slots;
std::size_t nextWriteIndex = 0;
std::size_t filledCount = 0;
unsigned effectiveLength = 0;
TemporalHistorySource historySource = TemporalHistorySource::None;
};
HistoryRing mSourceHistoryRing;
std::map<std::string, HistoryRing> mPreLayerHistoryByLayerId;
bool mTemporalHistoryNeedsReset;
bool InitOpenGLState();
bool compileLayerPrograms(int errorMessageSize, char* errorMessage);
bool compileSingleLayerProgram(const RuntimeRenderState& state, LayerProgram& layerProgram, int errorMessageSize, char* errorMessage);
bool compileDecodeShader(int errorMessageSize, char* errorMessage);
void destroyLayerPrograms();
void destroySingleLayerProgram(LayerProgram& layerProgram);
void destroyDecodeShaderProgram();
void renderDecodePass();
void renderShaderProgram(GLuint sourceTexture, GLuint destinationFrameBuffer, const LayerProgram& layerProgram, const RuntimeRenderState& state);
bool loadTextureAsset(const ShaderTextureAsset& textureAsset, GLuint& textureId, std::string& error);
void bindLayerTextureAssets(const LayerProgram& layerProgram);
void renderEffect();
bool PollRuntimeChanges();
void broadcastRuntimeState();
void initializeAudioDelay();
BMDTimeValue delayedAudioStreamTime() const;
void updateAudioDataTexture(const AudioAnalysisSnapshot& analysis);
void updateAudioStatus();
bool updateGlobalParamsBuffer(const RuntimeRenderState& state, unsigned availableSourceHistoryLength, unsigned availableTemporalHistoryLength);
bool validateTemporalTextureUnitBudget(const std::vector<RuntimeRenderState>& layerStates, std::string& error) const;
bool ensureTemporalHistoryResources(const std::vector<RuntimeRenderState>& layerStates, std::string& error);
bool createHistoryRing(HistoryRing& ring, unsigned effectiveLength, TemporalHistorySource historySource, std::string& error);
void destroyHistoryRing(HistoryRing& ring);
void destroyTemporalHistoryResources();
void resetTemporalHistoryState();
void pushFramebufferToHistoryRing(GLuint sourceFramebuffer, HistoryRing& ring);
void bindHistorySamplers(const RuntimeRenderState& state, GLuint currentSourceTexture);
GLuint resolveHistoryTexture(const HistoryRing& ring, GLuint fallbackTexture, std::size_t framesAgo) const;
unsigned sourceHistoryAvailableCount() const;
unsigned temporalHistoryAvailableCountForLayer(const std::string& layerId) const;
};
////////////////////////////////////////////
// PinnedMemoryAllocator
////////////////////////////////////////////
class PinnedMemoryAllocator : public IDeckLinkVideoBufferAllocator
{
public:
PinnedMemoryAllocator(HDC hdc, HGLRC hglrc, VideoFrameTransfer::Direction direction, unsigned cacheSize, unsigned bufferSize);
virtual ~PinnedMemoryAllocator();
bool transferFrame(void* address, GLuint gpuTexture);
void waitForTransferComplete(void* address);
unsigned bufferSize() { return mBufferSize; }
// IUnknown methods
virtual HRESULT STDMETHODCALLTYPE QueryInterface(REFIID iid, LPVOID *ppv) override;
virtual ULONG STDMETHODCALLTYPE AddRef(void) override;
virtual ULONG STDMETHODCALLTYPE Release(void) override;
// IDeckLinkVideoBufferAllocator methods
virtual HRESULT STDMETHODCALLTYPE AllocateVideoBuffer (IDeckLinkVideoBuffer** allocatedBuffer) override;
private:
void unPinAddress(void* address);
private:
HDC mHGLDC;
HGLRC mHGLRC;
std::atomic<ULONG> mRefCount;
VideoFrameTransfer::Direction mDirection;
std::map<void*, VideoFrameTransfer*> mFrameTransfer;
unsigned mBufferSize;
std::vector<void*> mFrameCache;
unsigned mFrameCacheSize;
};
////////////////////////////////////////////
// InputAllocatorPool
////////////////////////////////////////////
class InputAllocatorPool : public IDeckLinkVideoBufferAllocatorProvider
{
public:
InputAllocatorPool(HDC hdc, HGLRC hglrc);
// IUnknown interface
ULONG STDMETHODCALLTYPE AddRef() override;
ULONG STDMETHODCALLTYPE Release() override;
HRESULT STDMETHODCALLTYPE QueryInterface(REFIID iid, void** ppv) override;
// IDeckLinkVideoBufferAllocatorProvider interface
HRESULT STDMETHODCALLTYPE GetVideoBufferAllocator(
/* [in] */ unsigned int bufferSize,
/* [in] */ unsigned int width,
/* [in] */ unsigned int height,
/* [in] */ unsigned int rowBytes,
/* [in] */ BMDPixelFormat pixelFormat,
/* [out] */ IDeckLinkVideoBufferAllocator **allocator) override;
private:
std::atomic<ULONG> mRefCount;
std::map<unsigned int, CComPtr<PinnedMemoryAllocator> > mAllocatorBySize;
HDC mHDC;
HGLRC mHGLRC;
};
////////////////////////////////////////////
// DeckLinkVideoBuffer
////////////////////////////////////////////
class DeckLinkVideoBuffer : public IDeckLinkVideoBuffer
{
public:
explicit DeckLinkVideoBuffer(std::shared_ptr<void>& buffer, PinnedMemoryAllocator* parent);
virtual ~DeckLinkVideoBuffer() = default;
// IUnknown interface
virtual HRESULT STDMETHODCALLTYPE QueryInterface(REFIID riid, void** ppvObject) override;
virtual ULONG STDMETHODCALLTYPE AddRef(void) override;
virtual ULONG STDMETHODCALLTYPE Release(void) override;
// IDeckLinkVideoBuffer interface
virtual HRESULT STDMETHODCALLTYPE GetBytes(void** buffer) override;
virtual HRESULT STDMETHODCALLTYPE GetSize(uint64_t* size) override;
virtual HRESULT STDMETHODCALLTYPE StartAccess(BMDBufferAccessFlags flags) override;
virtual HRESULT STDMETHODCALLTYPE EndAccess(BMDBufferAccessFlags flags) override;
private:
CComPtr<PinnedMemoryAllocator> mParentAllocator; // Dual-purpose: allocator owns mem this points to, and to access transferFrame() via a QueryInterface
std::atomic<ULONG> mRefCount;
std::shared_ptr<void> mBuffer;
};
////////////////////////////////////////////
// Capture Delegate Class
////////////////////////////////////////////
class CaptureDelegate : public IDeckLinkInputCallback
{
OpenGLComposite* m_pOwner;
LONG mRefCount;
public:
CaptureDelegate (OpenGLComposite* pOwner);
// IUnknown needs only a dummy implementation
virtual HRESULT STDMETHODCALLTYPE QueryInterface (REFIID iid, LPVOID *ppv);
virtual ULONG STDMETHODCALLTYPE AddRef ();
virtual ULONG STDMETHODCALLTYPE Release ();
virtual HRESULT STDMETHODCALLTYPE VideoInputFrameArrived (IDeckLinkVideoInputFrame *videoFrame, IDeckLinkAudioInputPacket *audioPacket);
virtual HRESULT STDMETHODCALLTYPE VideoInputFormatChanged (BMDVideoInputFormatChangedEvents notificationEvents, IDeckLinkDisplayMode *newDisplayMode, BMDDetectedVideoInputFormatFlags detectedSignalFlags);
};
////////////////////////////////////////////
// Render Delegate Class
////////////////////////////////////////////
class PlayoutDelegate : public IDeckLinkVideoOutputCallback, public IDeckLinkAudioOutputCallback
{
OpenGLComposite* m_pOwner;
LONG mRefCount;
public:
PlayoutDelegate (OpenGLComposite* pOwner);
// IUnknown needs only a dummy implementation
virtual HRESULT STDMETHODCALLTYPE QueryInterface (REFIID iid, LPVOID *ppv);
virtual ULONG STDMETHODCALLTYPE AddRef ();
virtual ULONG STDMETHODCALLTYPE Release ();
virtual HRESULT STDMETHODCALLTYPE ScheduledFrameCompleted (IDeckLinkVideoFrame* completedFrame, BMDOutputFrameCompletionResult result);
virtual HRESULT STDMETHODCALLTYPE ScheduledPlaybackHasStopped ();
virtual HRESULT STDMETHODCALLTYPE RenderAudioSamples (BOOL preroll);
};
#endif // __OPENGL_COMPOSITE_H__

View File

@@ -1,314 +0,0 @@
#include "stdafx.h"
#include "OscServer.h"
#include <ws2tcpip.h>
#include <array>
#include <cstring>
#include <iomanip>
#include <sstream>
#include <vector>
#pragma comment(lib, "Ws2_32.lib")
namespace
{
bool InitializeWinsock(std::string& error)
{
WSADATA wsaData = {};
const int result = WSAStartup(MAKEWORD(2, 2), &wsaData);
if (result != 0)
{
error = "WSAStartup failed.";
return false;
}
return true;
}
std::vector<std::string> SplitAddress(const std::string& address)
{
std::vector<std::string> parts;
std::size_t start = !address.empty() && address[0] == '/' ? 1 : 0;
while (start <= address.size())
{
const std::size_t slash = address.find('/', start);
const std::size_t end = slash == std::string::npos ? address.size() : slash;
if (end > start)
parts.push_back(address.substr(start, end - start));
if (slash == std::string::npos)
break;
start = slash + 1;
}
return parts;
}
}
OscServer::OscServer()
: mPort(0), mRunning(false)
{
}
OscServer::~OscServer()
{
Stop();
}
bool OscServer::Start(unsigned short port, const Callbacks& callbacks, std::string& error)
{
if (port == 0)
return true;
mCallbacks = callbacks;
mPort = port;
if (!InitializeWinsock(error))
return false;
mSocket.reset(socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP));
if (!mSocket.valid())
{
error = "Could not create OSC UDP socket.";
return false;
}
DWORD timeoutMilliseconds = 100;
setsockopt(mSocket.get(), SOL_SOCKET, SO_RCVTIMEO, reinterpret_cast<const char*>(&timeoutMilliseconds), sizeof(timeoutMilliseconds));
sockaddr_in address = {};
address.sin_family = AF_INET;
address.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
address.sin_port = htons(static_cast<u_short>(port));
if (bind(mSocket.get(), reinterpret_cast<sockaddr*>(&address), sizeof(address)) != 0)
{
error = "Could not bind OSC listener to UDP port " + std::to_string(port) + ".";
mSocket.reset();
return false;
}
mRunning = true;
mThread = std::thread(&OscServer::ServerLoop, this);
return true;
}
void OscServer::Stop()
{
mRunning = false;
mSocket.reset();
if (mThread.joinable())
mThread.join();
}
void OscServer::ServerLoop()
{
std::array<char, 4096> buffer = {};
while (mRunning)
{
sockaddr_in sender = {};
int senderLength = sizeof(sender);
const int byteCount = recvfrom(mSocket.get(), buffer.data(), static_cast<int>(buffer.size()), 0,
reinterpret_cast<sockaddr*>(&sender), &senderLength);
if (byteCount <= 0)
continue;
OscMessage message;
std::string error;
if (DecodeMessage(buffer.data(), byteCount, message, error))
{
if (!DispatchMessage(message, error) && !error.empty())
OutputDebugStringA(("OSC dispatch failed: " + error + "\n").c_str());
}
else if (!error.empty())
{
OutputDebugStringA(("OSC decode failed: " + error + "\n").c_str());
}
}
}
bool OscServer::DecodeMessage(const char* data, int byteCount, OscMessage& message, std::string& error) const
{
int offset = 0;
if (!ReadPaddedString(data, byteCount, offset, message.address) || message.address.empty() || message.address[0] != '/')
{
error = "Invalid OSC address.";
return false;
}
std::string typeTags;
if (!ReadPaddedString(data, byteCount, offset, typeTags) || typeTags.empty() || typeTags[0] != ',')
{
error = "Invalid OSC type tag string.";
return false;
}
if (typeTags.size() < 2)
{
error = "OSC message has no parameter value.";
return false;
}
std::vector<std::string> values;
for (std::size_t index = 1; index < typeTags.size(); ++index)
{
std::string valueJson;
if (!DecodeArgument(data, byteCount, offset, typeTags[index], valueJson))
{
error = "Unsupported or malformed OSC value type.";
return false;
}
values.push_back(valueJson);
}
if (values.size() == 1)
{
message.valueJson = values.front();
return true;
}
std::ostringstream arrayJson;
arrayJson << "[";
for (std::size_t index = 0; index < values.size(); ++index)
{
if (index > 0)
arrayJson << ",";
arrayJson << values[index];
}
arrayJson << "]";
message.valueJson = arrayJson.str();
return true;
}
bool OscServer::DispatchMessage(const OscMessage& message, std::string& error) const
{
const std::vector<std::string> parts = SplitAddress(message.address);
if (parts.size() != 3 || parts[0] != "VideoShaderToys")
{
error = "Unsupported OSC address: " + message.address;
return false;
}
return mCallbacks.updateParameter &&
mCallbacks.updateParameter(parts[1], parts[2], message.valueJson, error);
}
bool OscServer::DecodeArgument(const char* data, int byteCount, int& offset, char valueType, std::string& valueJson)
{
if (valueType == 'f')
{
double value = 0.0;
if (!ReadFloat32(data, byteCount, offset, value))
return false;
std::ostringstream stream;
stream << std::setprecision(9) << value;
valueJson = stream.str();
return true;
}
if (valueType == 'd')
{
double value = 0.0;
if (!ReadFloat64(data, byteCount, offset, value))
return false;
std::ostringstream stream;
stream << std::setprecision(17) << value;
valueJson = stream.str();
return true;
}
if (valueType == 'i')
{
int value = 0;
if (!ReadInt32(data, byteCount, offset, value))
return false;
valueJson = std::to_string(value);
return true;
}
if (valueType == 's')
{
std::string value;
if (!ReadPaddedString(data, byteCount, offset, value))
return false;
valueJson = BuildJsonString(value);
return true;
}
if (valueType == 'T' || valueType == 'F')
{
valueJson = valueType == 'T' ? "true" : "false";
return true;
}
return false;
}
bool OscServer::ReadPaddedString(const char* data, int byteCount, int& offset, std::string& value)
{
if (offset < 0 || offset >= byteCount)
return false;
const int start = offset;
while (offset < byteCount && data[offset] != '\0')
++offset;
if (offset >= byteCount)
return false;
value.assign(data + start, data + offset);
++offset;
while (offset % 4 != 0)
++offset;
return offset <= byteCount;
}
bool OscServer::ReadInt32(const char* data, int byteCount, int& offset, int& value)
{
if (offset + 4 > byteCount)
return false;
const unsigned char* bytes = reinterpret_cast<const unsigned char*>(data + offset);
value = static_cast<int>((bytes[0] << 24) | (bytes[1] << 16) | (bytes[2] << 8) | bytes[3]);
offset += 4;
return true;
}
bool OscServer::ReadFloat32(const char* data, int byteCount, int& offset, double& value)
{
int bits = 0;
if (!ReadInt32(data, byteCount, offset, bits))
return false;
float floatValue = 0.0f;
const unsigned int unsignedBits = static_cast<unsigned int>(bits);
std::memcpy(&floatValue, &unsignedBits, sizeof(floatValue));
value = static_cast<double>(floatValue);
return true;
}
bool OscServer::ReadFloat64(const char* data, int byteCount, int& offset, double& value)
{
if (offset + 8 > byteCount)
return false;
const unsigned char* bytes = reinterpret_cast<const unsigned char*>(data + offset);
uint64_t bits = 0;
for (int index = 0; index < 8; ++index)
bits = (bits << 8) | static_cast<uint64_t>(bytes[index]);
std::memcpy(&value, &bits, sizeof(value));
offset += 8;
return true;
}
std::string OscServer::BuildJsonString(const std::string& value)
{
std::ostringstream stream;
stream << '"';
for (char ch : value)
{
if (ch == '"' || ch == '\\')
stream << '\\';
stream << ch;
}
stream << '"';
return stream.str();
}

View File

@@ -1,52 +0,0 @@
#pragma once
#include "NativeSockets.h"
#include <winsock2.h>
#include <atomic>
#include <functional>
#include <string>
#include <thread>
class OscServer
{
public:
struct Callbacks
{
std::function<bool(const std::string&, const std::string&, const std::string&, std::string&)> updateParameter;
};
OscServer();
~OscServer();
bool Start(unsigned short port, const Callbacks& callbacks, std::string& error);
void Stop();
unsigned short GetPort() const { return mPort; }
private:
friend struct OscServerTestAccess;
struct OscMessage
{
std::string address;
std::string valueJson;
};
void ServerLoop();
bool DecodeMessage(const char* data, int byteCount, OscMessage& message, std::string& error) const;
bool DispatchMessage(const OscMessage& message, std::string& error) const;
static bool DecodeArgument(const char* data, int byteCount, int& offset, char valueType, std::string& valueJson);
static bool ReadPaddedString(const char* data, int byteCount, int& offset, std::string& value);
static bool ReadInt32(const char* data, int byteCount, int& offset, int& value);
static bool ReadFloat32(const char* data, int byteCount, int& offset, double& value);
static bool ReadFloat64(const char* data, int byteCount, int& offset, double& value);
static std::string BuildJsonString(const std::string& value);
Callbacks mCallbacks;
UniqueSocket mSocket;
unsigned short mPort;
std::thread mThread;
std::atomic<bool> mRunning;
};

File diff suppressed because it is too large Load Diff

View File

@@ -1,176 +0,0 @@
#pragma once
#include "RuntimeJson.h"
#include "ShaderTypes.h"
#include <chrono>
#include <filesystem>
#include <map>
#include <mutex>
#include <string>
#include <vector>
class RuntimeHost
{
public:
RuntimeHost();
bool Initialize(std::string& error);
bool PollFileChanges(bool& registryChanged, bool& reloadRequested, std::string& error);
bool ManualReloadRequested();
void ClearReloadRequest();
bool AddLayer(const std::string& shaderId, std::string& error);
bool RemoveLayer(const std::string& layerId, std::string& error);
bool MoveLayer(const std::string& layerId, int direction, std::string& error);
bool MoveLayerToIndex(const std::string& layerId, std::size_t targetIndex, std::string& error);
bool SetLayerBypass(const std::string& layerId, bool bypassed, std::string& error);
bool SetLayerShader(const std::string& layerId, const std::string& shaderId, std::string& error);
bool UpdateLayerParameter(const std::string& layerId, const std::string& parameterId, const JsonValue& newValue, std::string& error);
bool UpdateLayerParameterByControlKey(const std::string& layerKey, const std::string& parameterKey, const JsonValue& newValue, std::string& error);
bool ResetLayerParameters(const std::string& layerId, std::string& error);
bool SaveStackPreset(const std::string& presetName, std::string& error) const;
bool LoadStackPreset(const std::string& presetName, std::string& error);
void SetCompileStatus(bool succeeded, const std::string& message);
void SetSignalStatus(bool hasSignal, unsigned width, unsigned height, const std::string& modeName);
void SetDeckLinkOutputStatus(const std::string& modelName, bool supportsInternalKeying, bool supportsExternalKeying,
bool keyerInterfaceAvailable, bool externalKeyingRequested, bool externalKeyingActive, const std::string& statusMessage);
void SetPerformanceStats(double frameBudgetMilliseconds, double renderMilliseconds);
void SetAudioStatus(const AudioStatusSnapshot& status);
void AdvanceFrame();
bool BuildLayerFragmentShaderSource(const std::string& layerId, std::string& fragmentShaderSource, std::string& error);
std::vector<RuntimeRenderState> GetLayerRenderStates(unsigned outputWidth, unsigned outputHeight) const;
std::string BuildStateJson() const;
const std::filesystem::path& GetRepoRoot() const { return mRepoRoot; }
const std::filesystem::path& GetUiRoot() const { return mUiRoot; }
const std::filesystem::path& GetDocsRoot() const { return mDocsRoot; }
const std::filesystem::path& GetRuntimeRoot() const { return mRuntimeRoot; }
unsigned short GetServerPort() const { return mServerPort; }
unsigned short GetOscPort() const { return mConfig.oscPort; }
unsigned GetMaxTemporalHistoryFrames() const { return mConfig.maxTemporalHistoryFrames; }
bool ExternalKeyingEnabled() const { return mConfig.enableExternalKeying; }
bool AudioEnabled() const { return mConfig.audioEnabled; }
bool AudioOutputEnabled() const { return mConfig.audioOutputEnabled; }
bool AudioScheduleEnabled() const { return mConfig.audioScheduleEnabled; }
bool AudioPrerollEnabled() const { return mConfig.audioPrerollEnabled; }
bool AudioScheduleSilence() const { return mConfig.audioScheduleSilence; }
bool AudioScheduleTone() const { return mConfig.audioScheduleTone; }
unsigned AudioChannelCount() const { return mConfig.audioChannelCount; }
unsigned AudioSampleRate() const { return mConfig.audioSampleRate; }
const std::string& GetInputVideoFormat() const { return mConfig.inputVideoFormat; }
const std::string& GetInputFrameRate() const { return mConfig.inputFrameRate; }
const std::string& GetOutputVideoFormat() const { return mConfig.outputVideoFormat; }
const std::string& GetOutputFrameRate() const { return mConfig.outputFrameRate; }
void SetServerPort(unsigned short port);
bool AutoReloadEnabled() const { return mAutoReloadEnabled; }
private:
struct AppConfig
{
std::string shaderLibrary = "shaders";
unsigned short serverPort = 8080;
unsigned short oscPort = 9000;
bool autoReload = true;
unsigned maxTemporalHistoryFrames = 4;
bool enableExternalKeying = false;
bool audioEnabled = true;
bool audioOutputEnabled = true;
bool audioScheduleEnabled = true;
bool audioPrerollEnabled = true;
bool audioScheduleSilence = false;
bool audioScheduleTone = false;
unsigned audioChannelCount = kAudioChannelCount;
unsigned audioSampleRate = kAudioSampleRate;
std::string audioDelayMode = "matchVideoPreroll";
std::string inputVideoFormat = "1080p";
std::string inputFrameRate = "59.94";
std::string outputVideoFormat = "1080p";
std::string outputFrameRate = "59.94";
};
struct DeckLinkOutputStatus
{
std::string modelName;
bool supportsInternalKeying = false;
bool supportsExternalKeying = false;
bool keyerInterfaceAvailable = false;
bool externalKeyingRequested = false;
bool externalKeyingActive = false;
std::string statusMessage;
};
struct LayerPersistentState
{
std::string id;
std::string shaderId;
bool bypass = false;
std::map<std::string, ShaderParameterValue> parameterValues;
};
struct PersistentState
{
std::vector<LayerPersistentState> layers;
};
bool LoadConfig(std::string& error);
bool LoadPersistentState(std::string& error);
bool SavePersistentState(std::string& error) const;
bool ScanShaderPackages(std::string& error);
bool ParseShaderManifest(const std::filesystem::path& manifestPath, ShaderPackage& shaderPackage, std::string& error) const;
bool NormalizeAndValidateValue(const ShaderParameterDefinition& definition, const JsonValue& value, ShaderParameterValue& normalizedValue, std::string& error) const;
ShaderParameterValue DefaultValueForDefinition(const ShaderParameterDefinition& definition) const;
void EnsureLayerDefaultsLocked(LayerPersistentState& layerState, const ShaderPackage& shaderPackage) const;
std::string ReadTextFile(const std::filesystem::path& path, std::string& error) const;
bool WriteTextFile(const std::filesystem::path& path, const std::string& contents, std::string& error) const;
bool ResolvePaths(std::string& error);
JsonValue BuildStateValue() const;
JsonValue SerializeLayerStackLocked() const;
bool DeserializeLayerStackLocked(const JsonValue& layersValue, std::vector<LayerPersistentState>& layers, std::string& error);
std::vector<std::string> GetStackPresetNamesLocked() const;
std::string MakeSafePresetFileStem(const std::string& presetName) const;
JsonValue SerializeParameterValue(const ShaderParameterDefinition& definition, const ShaderParameterValue& value) const;
std::string TemporalHistorySourceToString(TemporalHistorySource source) const;
LayerPersistentState* FindLayerById(const std::string& layerId);
const LayerPersistentState* FindLayerById(const std::string& layerId) const;
std::string GenerateLayerId();
private:
mutable std::mutex mMutex;
AppConfig mConfig;
PersistentState mPersistentState;
std::filesystem::path mRepoRoot;
std::filesystem::path mUiRoot;
std::filesystem::path mDocsRoot;
std::filesystem::path mShaderRoot;
std::filesystem::path mRuntimeRoot;
std::filesystem::path mPresetRoot;
std::filesystem::path mRuntimeStatePath;
std::filesystem::path mConfigPath;
std::filesystem::path mWrapperPath;
std::filesystem::path mGeneratedGlslPath;
std::filesystem::path mPatchedGlslPath;
std::map<std::string, ShaderPackage> mPackagesById;
std::vector<std::string> mPackageOrder;
bool mReloadRequested;
bool mCompileSucceeded;
std::string mCompileMessage;
bool mHasSignal;
unsigned mSignalWidth;
unsigned mSignalHeight;
std::string mSignalModeName;
double mFrameBudgetMilliseconds;
double mRenderMilliseconds;
double mSmoothedRenderMilliseconds;
DeckLinkOutputStatus mDeckLinkOutputStatus;
AudioStatusSnapshot mAudioStatus;
unsigned short mServerPort;
bool mAutoReloadEnabled;
std::chrono::steady_clock::time_point mStartTime;
std::chrono::steady_clock::time_point mLastScanTime;
uint64_t mFrameCounter;
uint64_t mNextLayerId;
};

View File

@@ -1,377 +0,0 @@
/* -LICENSE-START-
** Copyright (c) 2012 Blackmagic Design
**
** Permission is hereby granted, free of charge, to any person or organization
** obtaining a copy of the software and accompanying documentation (the
** "Software") to use, reproduce, display, distribute, sub-license, execute,
** and transmit the Software, and to prepare derivative works of the Software,
** and to permit third-parties to whom the Software is furnished to do so, in
** accordance with:
**
** (1) if the Software is obtained from Blackmagic Design, the End User License
** Agreement for the Software Development Kit ("EULA") available at
** https://www.blackmagicdesign.com/EULA/DeckLinkSDK; or
**
** (2) if the Software is obtained from any third party, such licensing terms
** as notified by that third party,
**
** and all subject to the following:
**
** (3) the copyright notices in the Software and this entire statement,
** including the above license grant, this restriction and the following
** disclaimer, must be included in all copies of the Software, in whole or in
** part, and all derivative works of the Software, unless such copies or
** derivative works are solely in the form of machine-executable object code
** generated by a source language processor.
**
** (4) THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
** OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
** FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT
** SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE
** FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE,
** ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
** DEALINGS IN THE SOFTWARE.
**
** A copy of the Software is available free of charge at
** https://www.blackmagicdesign.com/desktopvideo_sdk under the EULA.
**
** -LICENSE-END-
*/
#include "VideoFrameTransfer.h"
#include "NativeHandles.h"
#define DVP_CHECK(cmd) { \
DVPStatus hr = (cmd); \
if (DVP_STATUS_OK != hr) { \
OutputDebugStringA( #cmd " failed\n" ); \
ExitProcess(hr); \
} \
}
// Initialise static members
bool VideoFrameTransfer::mInitialized = false;
bool VideoFrameTransfer::mUseDvp = false;
unsigned VideoFrameTransfer::mWidth = 0;
unsigned VideoFrameTransfer::mHeight = 0;
GLuint VideoFrameTransfer::mCaptureTexture = 0;
// NVIDIA specific static members
DVPBufferHandle VideoFrameTransfer::mDvpCaptureTextureHandle = 0;
DVPBufferHandle VideoFrameTransfer::mDvpPlaybackTextureHandle = 0;
uint32_t VideoFrameTransfer::mBufferAddrAlignment = 0;
uint32_t VideoFrameTransfer::mBufferGpuStrideAlignment = 0;
uint32_t VideoFrameTransfer::mSemaphoreAddrAlignment = 0;
uint32_t VideoFrameTransfer::mSemaphoreAllocSize = 0;
uint32_t VideoFrameTransfer::mSemaphorePayloadOffset = 0;
uint32_t VideoFrameTransfer::mSemaphorePayloadSize = 0;
bool VideoFrameTransfer::isNvidiaDvpAvailable()
{
// Look for supported graphics boards
const GLubyte* renderer = glGetString(GL_RENDERER);
if (renderer == NULL)
return false;
bool hasDvp = (strstr((char*)renderer, "Quadro") != NULL);
return hasDvp;
}
bool VideoFrameTransfer::isAMDPinnedMemoryAvailable()
{
// GL_AMD_pinned_memory presence indicates GL_EXTERNAL_VIRTUAL_MEMORY_BUFFER_AMD buffer target is supported
const GLubyte* strExt = glGetString(GL_EXTENSIONS);
if (strExt == NULL)
{
// In a core profile context GL_EXTENSIONS is no longer queryable via glGetString().
// Treat this as "extension unavailable" for now; the fast-transfer path is optional.
return false;
}
bool hasAMDPinned = (strstr((char*)strExt, "GL_AMD_pinned_memory") != NULL);
return hasAMDPinned;
}
bool VideoFrameTransfer::checkFastMemoryTransferAvailable()
{
return (isNvidiaDvpAvailable() || isAMDPinnedMemoryAvailable());
}
bool VideoFrameTransfer::initialize(unsigned width, unsigned height, GLuint captureTexture, GLuint playbackTexture)
{
if (mInitialized)
return false;
bool hasDvp = isNvidiaDvpAvailable();
bool hasAMDPinned = isAMDPinnedMemoryAvailable();
if (!hasDvp && !hasAMDPinned)
return false;
mUseDvp = hasDvp;
mWidth = width;
mHeight = height;
mCaptureTexture = captureTexture;
if (! initializeMemoryLocking(mWidth * mHeight * 4)) // BGRA uses 4 bytes per pixel
return false;
if (mUseDvp)
{
// DVP initialisation
DVP_CHECK(dvpInitGLContext(DVP_DEVICE_FLAGS_SHARE_APP_CONTEXT));
DVP_CHECK(dvpGetRequiredConstantsGLCtx( &mBufferAddrAlignment, &mBufferGpuStrideAlignment,
&mSemaphoreAddrAlignment, &mSemaphoreAllocSize,
&mSemaphorePayloadOffset, &mSemaphorePayloadSize));
// Register textures with DVP
DVP_CHECK(dvpCreateGPUTextureGL(captureTexture, &mDvpCaptureTextureHandle));
DVP_CHECK(dvpCreateGPUTextureGL(playbackTexture, &mDvpPlaybackTextureHandle));
}
mInitialized = true;
return true;
}
bool VideoFrameTransfer::initializeMemoryLocking(unsigned memSize)
{
// Increase the process working set size to allow pinning of memory.
static SIZE_T dwMin = 0, dwMax = 0;
UniqueHandle processHandle(OpenProcess(PROCESS_QUERY_INFORMATION | PROCESS_SET_QUOTA, FALSE, GetCurrentProcessId()));
if (!processHandle.valid())
return false;
// Retrieve the working set size of the process.
if (!dwMin && !GetProcessWorkingSetSize(processHandle.get(), &dwMin, &dwMax))
return false;
// Allow for 80 frames to be locked
BOOL res = SetProcessWorkingSetSize(processHandle.get(), memSize * 80 + dwMin, memSize * 80 + (dwMax-dwMin));
if (!res)
return false;
return true;
}
// SyncInfo sets up a semaphore which is shared between the GPU and CPU and used to
// synchronise access to DVP buffers.
struct SyncInfo
{
SyncInfo(uint32_t semaphoreAllocSize, uint32_t semaphoreAddrAlignment);
~SyncInfo();
volatile uint32_t* mSem;
volatile uint32_t mReleaseValue;
volatile uint32_t mAcquireValue;
DVPSyncObjectHandle mDvpSync;
};
SyncInfo::SyncInfo(uint32_t semaphoreAllocSize, uint32_t semaphoreAddrAlignment)
{
mSem = (uint32_t*)_aligned_malloc(semaphoreAllocSize, semaphoreAddrAlignment);
// Initialise
mSem[0] = 0;
mReleaseValue = 0;
mAcquireValue = 0;
// Setup DVP sync object and import it
DVPSyncObjectDesc syncObjectDesc;
syncObjectDesc.externalClientWaitFunc = NULL;
syncObjectDesc.sem = (uint32_t*)mSem;
DVP_CHECK(dvpImportSyncObject(&syncObjectDesc, &mDvpSync));
}
SyncInfo::~SyncInfo()
{
DVP_CHECK(dvpFreeSyncObject(mDvpSync));
_aligned_free((void*)mSem);
}
VideoFrameTransfer::VideoFrameTransfer(unsigned long memSize, void* address, Direction direction) :
mBuffer(address),
mMemSize(memSize),
mDirection(direction),
mExtSync(NULL),
mGpuSync(NULL),
mDvpSysMemHandle(0),
mBufferHandle(0)
{
if (mUseDvp)
{
// Pin the memory
if (! VirtualLock(mBuffer, mMemSize))
throw std::runtime_error("Error pinning memory with VirtualLock");
// Create necessary sysmem and gpu sync objects
mExtSync = new SyncInfo(mSemaphoreAllocSize, mSemaphoreAddrAlignment);
mGpuSync = new SyncInfo(mSemaphoreAllocSize, mSemaphoreAddrAlignment);
// Register system memory buffers with DVP
DVPSysmemBufferDesc sysMemBuffersDesc;
sysMemBuffersDesc.width = mWidth;
sysMemBuffersDesc.height = mHeight;
sysMemBuffersDesc.stride = mWidth * 4;
sysMemBuffersDesc.format = DVP_BGRA;
sysMemBuffersDesc.type = DVP_UNSIGNED_BYTE;
sysMemBuffersDesc.size = mMemSize;
sysMemBuffersDesc.bufAddr = mBuffer;
if (mDirection == CPUtoGPU)
{
// A UYVY 4:2:2 frame is transferred to the GPU, rather than RGB 4:4:4, so width is halved
sysMemBuffersDesc.width /= 2;
sysMemBuffersDesc.stride /= 2;
}
DVP_CHECK(dvpCreateBuffer(&sysMemBuffersDesc, &mDvpSysMemHandle));
DVP_CHECK(dvpBindToGLCtx(mDvpSysMemHandle));
}
else
{
// Create an OpenGL buffer handle to use for pinned memory
GLuint bufferHandle;
glGenBuffers(1, &bufferHandle);
// Pin memory by binding buffer to special AMD target.
glBindBuffer(GL_EXTERNAL_VIRTUAL_MEMORY_BUFFER_AMD, bufferHandle);
// glBufferData() sets up the address so any OpenGL operation on this buffer will use system memory directly
// (assumes address is aligned to 4k boundary).
glBufferData(GL_EXTERNAL_VIRTUAL_MEMORY_BUFFER_AMD, mMemSize, address, GL_STREAM_DRAW);
GLenum result = glGetError();
if (result != GL_NO_ERROR)
{
throw std::runtime_error("Error pinning memory with glBufferData(GL_EXTERNAL_VIRTUAL_MEMORY_BUFFER_AMD, ...)");
}
glBindBuffer(GL_EXTERNAL_VIRTUAL_MEMORY_BUFFER_AMD, 0); // Unbind buffer to target
mBufferHandle = bufferHandle;
}
}
VideoFrameTransfer::~VideoFrameTransfer()
{
if (mUseDvp)
{
DVP_CHECK(dvpUnbindFromGLCtx(mDvpSysMemHandle));
DVP_CHECK(dvpDestroyBuffer(mDvpSysMemHandle));
delete mExtSync;
delete mGpuSync;
VirtualUnlock(mBuffer, mMemSize);
}
else
{
// The buffer is un-pinned by the GPU when the buffer is deleted
glDeleteBuffers(1, &mBufferHandle);
}
}
bool VideoFrameTransfer::performFrameTransfer()
{
if (mUseDvp)
{
// NVIDIA DVP transfers
DVPStatus status;
mGpuSync->mReleaseValue++;
dvpBegin();
if (mDirection == CPUtoGPU)
{
// Copy from system memory to GPU texture
dvpMapBufferWaitDVP(mDvpCaptureTextureHandle);
status = dvpMemcpyLined( mDvpSysMemHandle, mExtSync->mDvpSync, mExtSync->mAcquireValue, DVP_TIMEOUT_IGNORED,
mDvpCaptureTextureHandle, mGpuSync->mDvpSync, mGpuSync->mReleaseValue, 0, mHeight);
dvpMapBufferEndDVP(mDvpCaptureTextureHandle);
}
else
{
// Copy from GPU texture to system memory
dvpMapBufferWaitDVP(mDvpPlaybackTextureHandle);
status = dvpMemcpyLined( mDvpPlaybackTextureHandle, mExtSync->mDvpSync, mExtSync->mReleaseValue, DVP_TIMEOUT_IGNORED,
mDvpSysMemHandle, mGpuSync->mDvpSync, mGpuSync->mReleaseValue, 0, mHeight);
dvpMapBufferEndDVP(mDvpPlaybackTextureHandle);
}
dvpEnd();
return (status == DVP_STATUS_OK);
}
else
{
// AMD pinned memory transfers
if (mDirection == CPUtoGPU)
{
glEnable(GL_TEXTURE_2D);
// Use a pinned buffer for the GL_PIXEL_UNPACK_BUFFER target
glBindBuffer(GL_PIXEL_UNPACK_BUFFER, mBufferHandle);
glBindTexture(GL_TEXTURE_2D, mCaptureTexture);
// NULL for last arg indicates use current GL_PIXEL_UNPACK_BUFFER target as texture data
glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, mWidth/2, mHeight, GL_BGRA, GL_UNSIGNED_INT_8_8_8_8_REV, NULL);
// Ensure pinned texture has been transferred to GPU before we draw with it
GLsync fence = glFenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE, 0);
glClientWaitSync(fence, GL_SYNC_FLUSH_COMMANDS_BIT, 40 * 1000 * 1000); // timeout in nanosec
glDeleteSync(fence);
glBindTexture(GL_TEXTURE_2D, 0);
glBindBuffer(GL_PIXEL_UNPACK_BUFFER, 0);
glDisable(GL_TEXTURE_2D);
}
else
{
// Use a PIXEL PACK BUFFER to read back pixels
glBindBuffer(GL_PIXEL_PACK_BUFFER, mBufferHandle);
glReadPixels(0, 0, mWidth, mHeight, GL_BGRA, GL_UNSIGNED_INT_8_8_8_8_REV, NULL);
// Ensure GPU has processed all commands in the pipeline up to this point, before memory is read by the CPU
GLsync fence = glFenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE, 0);
glClientWaitSync(fence, GL_SYNC_FLUSH_COMMANDS_BIT, 40 * 1000 * 1000); // timeout in nanosec
glDeleteSync(fence);
}
return (glGetError() == GL_NO_ERROR);
}
}
void VideoFrameTransfer::waitForTransferComplete()
{
if (!mUseDvp)
return;
// Block until buffer has completely transferred between GPU and CPU buffer
dvpBegin();
dvpSyncObjClientWaitComplete(mGpuSync->mDvpSync, DVP_TIMEOUT_IGNORED);
dvpEnd();
}
void VideoFrameTransfer::beginTextureInUse(Direction direction)
{
if (!mUseDvp)
return;
if (direction == CPUtoGPU)
dvpMapBufferWaitAPI(mDvpCaptureTextureHandle);
else
dvpMapBufferWaitAPI(mDvpPlaybackTextureHandle);
}
void VideoFrameTransfer::endTextureInUse(Direction direction)
{
if (!mUseDvp)
return;
if (direction == CPUtoGPU)
dvpMapBufferEndAPI(mDvpCaptureTextureHandle);
else
dvpMapBufferEndAPI(mDvpPlaybackTextureHandle);
}

View File

@@ -1,109 +0,0 @@
/* -LICENSE-START-
** Copyright (c) 2012 Blackmagic Design
**
** Permission is hereby granted, free of charge, to any person or organization
** obtaining a copy of the software and accompanying documentation (the
** "Software") to use, reproduce, display, distribute, sub-license, execute,
** and transmit the Software, and to prepare derivative works of the Software,
** and to permit third-parties to whom the Software is furnished to do so, in
** accordance with:
**
** (1) if the Software is obtained from Blackmagic Design, the End User License
** Agreement for the Software Development Kit ("EULA") available at
** https://www.blackmagicdesign.com/EULA/DeckLinkSDK; or
**
** (2) if the Software is obtained from any third party, such licensing terms
** as notified by that third party,
**
** and all subject to the following:
**
** (3) the copyright notices in the Software and this entire statement,
** including the above license grant, this restriction and the following
** disclaimer, must be included in all copies of the Software, in whole or in
** part, and all derivative works of the Software, unless such copies or
** derivative works are solely in the form of machine-executable object code
** generated by a source language processor.
**
** (4) THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
** OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
** FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT
** SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE
** FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE,
** ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
** DEALINGS IN THE SOFTWARE.
**
** A copy of the Software is available free of charge at
** https://www.blackmagicdesign.com/desktopvideo_sdk under the EULA.
**
** -LICENSE-END-
*/
#ifndef __VIDEO_FRAME_TRANSFER_H__
#define __VIDEO_FRAME_TRANSFER_H__
#include "GLExtensions.h"
#include <stdexcept>
#include <map>
// NVIDIA GPU Direct For Video with OpenGL requires the following two headers.
// See the NVIDIA website to check if your graphics card is supported.
#include <DVPAPI.h>
#include <dvpapi_gl.h>
struct SyncInfo;
// Class for performing efficient frame memory transfers between the CPU and GPU,
// using NVIDIA and AMD extensions.
class VideoFrameTransfer
{
public:
enum Direction
{
CPUtoGPU,
GPUtoCPU
};
VideoFrameTransfer(unsigned long memSize, void* address, Direction direction);
~VideoFrameTransfer();
static bool checkFastMemoryTransferAvailable();
static bool initialize(unsigned width, unsigned height, GLuint captureTexture, GLuint playbackTexture);
static void beginTextureInUse(Direction direction);
static void endTextureInUse(Direction direction);
bool performFrameTransfer();
void waitForTransferComplete();
private:
static bool isNvidiaDvpAvailable();
static bool isAMDPinnedMemoryAvailable();
static bool initializeMemoryLocking(unsigned memSize);
void* mBuffer;
unsigned long mMemSize;
Direction mDirection;
static bool mInitialized;
static bool mUseDvp;
static unsigned mWidth;
static unsigned mHeight;
static GLuint mCaptureTexture;
// NVIDIA GPU Direct for Video support
SyncInfo* mExtSync;
SyncInfo* mGpuSync;
DVPBufferHandle mDvpSysMemHandle;
static DVPBufferHandle mDvpCaptureTextureHandle;
static DVPBufferHandle mDvpPlaybackTextureHandle;
static uint32_t mBufferAddrAlignment;
static uint32_t mBufferGpuStrideAlignment;
static uint32_t mSemaphoreAddrAlignment;
static uint32_t mSemaphoreAllocSize;
static uint32_t mSemaphorePayloadOffset;
static uint32_t mSemaphorePayloadSize;
// GPU buffer bound to the target GL_EXTERNAL_VIRTUAL_MEMORY_BUFFER_AMD for pinned memory
GLuint mBufferHandle;
};
#endif

View File

@@ -1,24 +0,0 @@
//{{NO_DEPENDENCIES}}
// Microsoft Visual C++ generated include file.
// Used by LoopThroughWithOpenGLCompositing.rc
//
#define IDC_MYICON 2
#define IDD_OPENGLOUTPUT_DIALOG 102
#define IDS_APP_TITLE 103
#define IDI_OPENGLOUTPUT 107
#define IDI_SMALL 108
#define IDC_OPENGLOUTPUT 109
#define IDR_MAINFRAME 128
#define IDC_STATIC -1
// Next default values for new objects
//
#ifdef APSTUDIO_INVOKED
#ifndef APSTUDIO_READONLY_SYMBOLS
#define _APS_NO_MFC 1
#define _APS_NEXT_RESOURCE_VALUE 129
#define _APS_NEXT_COMMAND_VALUE 32771
#define _APS_NEXT_CONTROL_VALUE 1000
#define _APS_NEXT_SYMED_VALUE 110
#endif
#endif

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

View File

@@ -1,46 +0,0 @@
/* -LICENSE-START-
** Copyright (c) 2012 Blackmagic Design
**
** Permission is hereby granted, free of charge, to any person or organization
** obtaining a copy of the software and accompanying documentation (the
** "Software") to use, reproduce, display, distribute, sub-license, execute,
** and transmit the Software, and to prepare derivative works of the Software,
** and to permit third-parties to whom the Software is furnished to do so, in
** accordance with:
**
** (1) if the Software is obtained from Blackmagic Design, the End User License
** Agreement for the Software Development Kit ("EULA") available at
** https://www.blackmagicdesign.com/EULA/DeckLinkSDK; or
**
** (2) if the Software is obtained from any third party, such licensing terms
** as notified by that third party,
**
** and all subject to the following:
**
** (3) the copyright notices in the Software and this entire statement,
** including the above license grant, this restriction and the following
** disclaimer, must be included in all copies of the Software, in whole or in
** part, and all derivative works of the Software, unless such copies or
** derivative works are solely in the form of machine-executable object code
** generated by a source language processor.
**
** (4) THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
** OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
** FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT
** SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE
** FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE,
** ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
** DEALINGS IN THE SOFTWARE.
**
** A copy of the Software is available free of charge at
** https://www.blackmagicdesign.com/desktopvideo_sdk under the EULA.
**
** -LICENSE-END-
*/
//
// stdafx.cpp : source file that includes just the standard includes
// LoopThroughWithOpenGLCompositing.pch will be the pre-compiled header
// stdafx.obj will contain the pre-compiled type information
#include "stdafx.h"

View File

@@ -1,65 +0,0 @@
/* -LICENSE-START-
** Copyright (c) 2012 Blackmagic Design
**
** Permission is hereby granted, free of charge, to any person or organization
** obtaining a copy of the software and accompanying documentation (the
** "Software") to use, reproduce, display, distribute, sub-license, execute,
** and transmit the Software, and to prepare derivative works of the Software,
** and to permit third-parties to whom the Software is furnished to do so, in
** accordance with:
**
** (1) if the Software is obtained from Blackmagic Design, the End User License
** Agreement for the Software Development Kit ("EULA") available at
** https://www.blackmagicdesign.com/EULA/DeckLinkSDK; or
**
** (2) if the Software is obtained from any third party, such licensing terms
** as notified by that third party,
**
** and all subject to the following:
**
** (3) the copyright notices in the Software and this entire statement,
** including the above license grant, this restriction and the following
** disclaimer, must be included in all copies of the Software, in whole or in
** part, and all derivative works of the Software, unless such copies or
** derivative works are solely in the form of machine-executable object code
** generated by a source language processor.
**
** (4) THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
** OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
** FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT
** SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE
** FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE,
** ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
** DEALINGS IN THE SOFTWARE.
**
** A copy of the Software is available free of charge at
** https://www.blackmagicdesign.com/desktopvideo_sdk under the EULA.
**
** -LICENSE-END-
*/
//
// stdafx.h : include file for standard system include files,
// or project specific include files that are used frequently, but
// are changed infrequently
//
#pragma once
#include "targetver.h"
#ifndef NOMINMAX
#define NOMINMAX
#endif
#define WIN32_LEAN_AND_MEAN // Exclude rarely-used stuff from Windows headers
// Windows Header Files:
#include <winsock2.h>
#include <ws2tcpip.h>
#include <windows.h>
// C RunTime Header Files
#include <stdlib.h>
#include <malloc.h>
#include <memory.h>
#include <tchar.h>

View File

@@ -1,64 +0,0 @@
/* -LICENSE-START-
** Copyright (c) 2012 Blackmagic Design
**
** Permission is hereby granted, free of charge, to any person or organization
** obtaining a copy of the software and accompanying documentation (the
** "Software") to use, reproduce, display, distribute, sub-license, execute,
** and transmit the Software, and to prepare derivative works of the Software,
** and to permit third-parties to whom the Software is furnished to do so, in
** accordance with:
**
** (1) if the Software is obtained from Blackmagic Design, the End User License
** Agreement for the Software Development Kit ("EULA") available at
** https://www.blackmagicdesign.com/EULA/DeckLinkSDK; or
**
** (2) if the Software is obtained from any third party, such licensing terms
** as notified by that third party,
**
** and all subject to the following:
**
** (3) the copyright notices in the Software and this entire statement,
** including the above license grant, this restriction and the following
** disclaimer, must be included in all copies of the Software, in whole or in
** part, and all derivative works of the Software, unless such copies or
** derivative works are solely in the form of machine-executable object code
** generated by a source language processor.
**
** (4) THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
** OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
** FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT
** SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE
** FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE,
** ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
** DEALINGS IN THE SOFTWARE.
**
** A copy of the Software is available free of charge at
** https://www.blackmagicdesign.com/desktopvideo_sdk under the EULA.
**
** -LICENSE-END-
*/
//
#pragma once
// The following macros define the minimum required platform. The minimum required platform
// is the earliest version of Windows, Internet Explorer etc. that has the necessary features to run
// your application. The macros work by enabling all features available on platform versions up to and
// including the version specified.
// Modify the following defines if you have to target a platform prior to the ones specified below.
// Refer to MSDN for the latest info on corresponding values for different platforms.
#ifndef WINVER // Specifies that the minimum required platform is Windows Vista.
#define WINVER 0x0600 // Change this to the appropriate value to target other versions of Windows.
#endif
#ifndef _WIN32_WINNT // Specifies that the minimum required platform is Windows Vista.
#define _WIN32_WINNT 0x0600 // Change this to the appropriate value to target other versions of Windows.
#endif
#ifndef _WIN32_WINDOWS // Specifies that the minimum required platform is Windows 98.
#define _WIN32_WINDOWS 0x0410 // Change this to the appropriate value to target Windows Me or later.
#endif
#ifndef _WIN32_IE // Specifies that the minimum required platform is Internet Explorer 7.0.
#define _WIN32_IE 0x0700 // Change this to the appropriate value to target other versions of IE.
#endif

View File

@@ -1,21 +1,16 @@
{
"shaderLibrary": "shaders",
"serverPort": 8080,
"oscBindAddress": "0.0.0.0",
"oscPort": 9000,
"oscSmoothing": 0.18,
"inputVideoFormat": "1080p",
"inputFrameRate": "59.94",
"outputVideoFormat": "1080p",
"outputFrameRate": "59.94",
"autoReload": true,
"maxTemporalHistoryFrames": 12,
"audioEnabled": true,
"audioOutputEnabled": true,
"audioScheduleEnabled": true,
"audioPrerollEnabled": true,
"audioScheduleSilence": false,
"audioScheduleTone": false,
"audioChannelCount": 2,
"audioSampleRate": 48000,
"audioDelayMode": "matchVideoPreroll",
"previewEnabled": true,
"previewFps": 59.94,
"enableExternalKeying": true
}

View File

@@ -0,0 +1,725 @@
# Architecture Resilience Review
This note summarizes the main architectural improvements that would make the app more resilient during live use, especially around timing isolation, failure isolation, and recoverability.
Phase checklist:
- [x] Define subsystem boundaries and target architecture
- [x] Introduce an internal event model
- [x] Split `RuntimeHost`
- [x] Finish live-state and service-facing coordination
- [x] Make the render thread the sole GL owner
- [x] Refactor live state layering into an explicit composition model
- [x] Move persistence onto a background snapshot writer
- [x] Make DeckLink/backend lifecycle explicit with a state machine
- [ ] Make playout timing proactive and deadline-aware
- [ ] Add structured health, telemetry, and operational reporting
Checklist note:
- The checked Phase 1 item means the subsystem vocabulary, dependency direction, state categories, design package, and runtime implementation foothold are in place.
- The checked Phase 2 item means the internal event model substrate is complete enough for later phases: the typed event vocabulary, app-owned dispatcher, coalesced event pump, reload bridge events, production bridges, and pure event tests are in place. Remaining items in [PHASE_2_INTERNAL_EVENT_MODEL_DESIGN.md](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/docs/PHASE_2_INTERNAL_EVENT_MODEL_DESIGN.md) are narrow follow-ups, mainly completion/failure observations and later replacement of the runtime-store poll fallback with real file-watch events.
- The checked Phase 3 item means the render-facing state path now has named live-state, composition, frame-state, resolver, and service-bridge boundaries. `OpenGLComposite::renderEffect()` is reduced to runtime work, frame input construction, and frame rendering.
- The checked Phase 4 item means normal runtime GL work is now owned by a dedicated `RenderEngine` render thread. Input upload, output render, preview, screenshot capture, render-local resets, and shader application enter through render-thread queue/request paths instead of caller-thread context borrowing. The remaining output timing risk is callback-coupled synchronous output production, which is intentionally tracked for the later DeckLink/backend lifecycle and playout-queue work.
- The checked Phase 5 item means persisted, committed/session, transient automation, and render-local state are explicitly named. `CommittedLiveState` physically owns current session layer state, `RuntimeLiveState` owns transient OSC overlays, `RenderStateComposer` consumes a layered input contract, and reset/reload/preset overlay invalidation is centralized and covered by non-GL tests.
- It does not mean the whole app is fully extracted. Backend lifecycle/playout queue policy and richer telemetry continue through later phases.
## Timing Review
The recent OSC work removed several control-path stalls, but the app still has a few deeper timing characteristics that matter for live resilience:
- output playout is still effectively render-on-demand from the DeckLink completion callback
- output buffering and preroll are now larger, but the buffering model is still static and only loosely related to actual render cost
- GPU readback is partly asynchronous, but the fallback path still returns to synchronous readback on any miss
- preview presentation is best-effort and render-thread queued, but still shares the same render-thread budget as playout
- background service timing is partially event-driven; runtime-store scanning still uses a bounded compatibility poll fallback
Those points are important because they affect not just average performance, but how the app behaves under brief spikes, device jitter, or load bursts.
## Key Findings
### 1. The original runtime host carried too many responsibilities
The original `RuntimeHost` acted as:
- config store
- persistent state store
- live parameter/state authority
- shader package registry owner
- status/telemetry sink
- control mutation entrypoint
That makes it a single contention and failure domain. It is also why OSC and render timing issues repeatedly surfaced around shared state access.
Relevant code:
- `RuntimeHost.h`
Recommended direction:
- split persisted config/state from live render-facing state
- separate status/telemetry updates from control mutation paths
- make render consume snapshots rather than sharing a large mutable authority object
### 2. OpenGL ownership has moved to the render thread
Phase 4 removed normal runtime dependence on the old shared GL `CRITICAL_SECTION`. `RenderEngine` now owns a dedicated render thread and binds the GL context there for normal input upload, output rendering, preview presentation, screenshot capture, shader application, and render-local reset work.
Relevant code:
- [RenderEngine.cpp](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/gl/RenderEngine.cpp:36)
- [OpenGLVideoIOBridge.cpp](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/gl/pipeline/OpenGLVideoIOBridge.cpp:11)
- [OpenGLComposite.cpp](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/gl/composite/OpenGLComposite.cpp:168)
This removes cross-thread GL context borrowing as the central correctness model. The remaining timing risk is that output frame production is still synchronous from the DeckLink completion path, so a render/readback spike can still reduce playout headroom.
Recommended direction:
- keep the render thread as the sole GL owner
- replace synchronous output request/response with a bounded producer/consumer playout queue
- keep preview and screenshot subordinate to output deadline pressure
### 3. Control flow is spread across polling and shared-memory patterns
`RuntimeServices` currently mixes:
- file polling
- deferred OSC commit handling
- control service orchestration
OSC ingest, overlay application, and host sync are distributed across several components.
Relevant code:
- [RuntimeServices.h](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/control/RuntimeServices.h:26)
- [RuntimeServices.cpp](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/control/RuntimeServices.cpp:178)
Recommended direction:
- introduce a small internal event pipeline or message bus
- use typed events for OSC, reloads, persistence requests, and status changes
- make timing ownership explicit per subsystem
Example event types:
- `OscParameterTargeted`
- `RenderOverlaySettled`
- `PersistStateRequested`
- `ShaderReloadRequested`
- `DeckLinkStatusChanged`
### 4. Error handling is still heavily UI-coupled
Failures are often surfaced via `MessageBoxA`, while background services mainly log with `OutputDebugStringA`.
Relevant code:
- [OpenGLComposite.cpp](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/gl/composite/OpenGLComposite.cpp:314)
- [DeckLinkSession.cpp](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/videoio/decklink/DeckLinkSession.cpp:478)
- [RuntimeServices.cpp](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/control/RuntimeServices.cpp:205)
This is not ideal for a live system where modal dialogs and silent debug logging are both poor operational behavior.
Recommended direction:
- introduce structured in-app error reporting
- define severity levels and counters
- prefer degraded runtime states over modal failure handling where possible
- add a rolling log file for operational troubleshooting
### 5. Live OSC overlay and persisted state now have an explicit layering model
Phase 5 formalized the previous hand-managed reconciliation between:
- base persisted state owned by `RuntimeStore` serialization/preset IO
- committed session state owned by `CommittedLiveState`
- transient OSC overlay state owned by `RuntimeLiveState`
- render-local temporal, feedback, preview, screenshot, and playout state owned by `RenderEngine`
Relevant code:
- [CommittedLiveState.h](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/runtime/live/CommittedLiveState.h:1)
- [RuntimeLiveState.h](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/runtime/live/RuntimeLiveState.h:1)
- [RenderStateComposer.h](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/runtime/live/RenderStateComposer.h:1)
- [RuntimeStateLayerModel.h](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/runtime/live/RuntimeStateLayerModel.h:1)
Current direction:
- render resolves values with a named composition rule:
- `final = base + committed + transient`
- settled OSC commits are session-only by default and do not request persistence unless policy explicitly opts in
- reset, reload, preset load, and shader compatibility changes prune or clear transient overlays at the live-state boundary
- render-local temporal and feedback resources remain outside the parameter layering model
### 6. DeckLink lifecycle could be modeled more explicitly
`DeckLinkSession` has a number of imperative calls, but startup, preroll, running, degraded, and stopped are not represented as an explicit state machine.
Relevant code:
- [DeckLinkSession.h](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/videoio/decklink/DeckLinkSession.h:17)
Recommended direction:
- introduce explicit session states
- define allowed transitions
- centralize recovery behavior
- make shutdown ordering and degraded-mode behavior more predictable
Timing-specific additions:
- separate "device callback received" from "render the next output frame" so output cadence is not driven directly by the completion callback thread
- make playout headroom configurable and adaptive instead of using a fixed compile-time preroll count
- track an explicit backend health state such as `running-steady`, `catching-up`, `late`, and `dropping`
Relevant timing code:
- [OpenGLVideoIOBridge.cpp](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/gl/pipeline/OpenGLVideoIOBridge.cpp:86)
- [DeckLinkSession.cpp](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/videoio/decklink/DeckLinkSession.cpp:420)
- [DeckLinkSession.cpp](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/videoio/decklink/DeckLinkSession.cpp:487)
- [VideoPlayoutScheduler.cpp](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/videoio/VideoPlayoutScheduler.cpp:26)
Why this matters:
- the output completion path currently requests a scheduled render through `OpenGLVideoIOBridge::RenderScheduledFrame()`, which asks the render thread to render/read back synchronously and then schedules the next frame in one callback-driven flow.
- `VideoPlayoutScheduler::AccountForCompletionResult()` currently reacts to both late and dropped frames by blindly advancing the schedule index by `2`, which is simple but not especially robust.
- `kPrerollFrameCount` is now `12`, but `DeckLinkSession::ConfigureOutput()` still creates a fixed pool of `10` mutable output frames. That mismatch suggests the buffering model is not being sized from one coherent source of truth.
Recommended direction:
- move playout to a producer/consumer model where a render worker fills output buffers ahead of the DeckLink callback
- define buffer-pool sizing from one policy object, for example: preroll depth, minimum spare buffers, and allowed catch-up depth
- replace fixed "skip two frames" recovery with measured lag accounting based on actual scheduled-versus-completed position
- expose playout latency as a runtime setting or policy, rather than burying it in a constant
### 6a. The current playout timing model is still callback-coupled
The app now has more headroom, but the next output frame is still produced directly in the scheduled-frame completion callback path.
Relevant code:
- [OpenGLVideoIOBridge.cpp](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/gl/pipeline/OpenGLVideoIOBridge.cpp:86)
- [DeckLinkFrameTransfer.cpp](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/videoio/decklink/DeckLinkFrameTransfer.cpp:53)
That means the completion callback is currently responsible for:
- frame pacing accounting
- acquiring the next output buffer
- requesting render-thread output production
- waiting for render/readback completion
- performing output readback
- scheduling the next frame
This works when the app is comfortably within budget, but it makes deadline misses much harder to absorb gracefully.
Recommended direction:
- make the DeckLink callback a lightweight notifier
- have a dedicated playout worker or render worker keep an ahead-of-time queue of ready output frames
- treat callback time as control-plane time, not render time
### 6b. A producer/consumer playout model would be a better long-term fit
The stronger architecture for this app is:
- a render scheduler or dedicated render thread runs at the configured video cadence
- rendering produces completed output frames ahead of need
- those frames are placed into a bounded queue or ring buffer
- the DeckLink side consumes already-prepared frames when callbacks indicate they are needed
That is a better fit than callback-driven rendering because it separates:
- render timing
- GL ownership
- output-device timing
- latency policy
In that model:
- render is the producer
- DeckLink is the timing consumer
- the queue between them becomes the main place to manage latency versus resilience
Why this is preferable:
- brief callback jitter is less likely to become a visible dropped frame
- render spikes can be absorbed by queue headroom instead of immediately missing output deadlines
- latency becomes an explicit policy choice rather than an incidental side effect of callback timing
- queue depth, underruns, stale-frame reuse, and catch-up behavior become measurable and tunable
Recommended direction:
- move toward a bounded producer/consumer playout queue
- make queue depth and target headroom runtime policy, not compile-time constants
- define explicit underrun behavior, for example:
- reuse newest completed frame
- reuse last scheduled frame
- output black or degraded frame
- keep DeckLink callbacks limited to dequeue/schedule/accounting work wherever possible
### 7. Persistence should be more asynchronous and debounced
Status: addressed by Phase 6.
Relevant current `RenderCadenceCompositor` code:
- `src/app/RuntimeLayerControllerControls.cpp`
- `src/runtime/RuntimeStatePersistence.cpp`
- `src/runtime/RuntimeStatePersistence.h`
Runtime-state persistence now flows from accepted durable layer-stack mutations into a debounced background writer. The layer controller owns the current snapshot source, while `RuntimeStatePersistenceWriter` owns serialization, temp-file replacement, coalescing, result reporting, and shutdown flushing.
The remaining architecture concern is broader persistence policy, not direct mutation-path disk writes:
- whether preset saves should stay synchronous
- whether runtime config writes should share the persistence writer
- whether failed writes should retry automatically or wait for the next request
This improves both resilience and timing safety.
### 8. Telemetry is useful, but still too coarse
The app already records render timing and playout pacing, which is a good foundation.
Relevant code:
- [OpenGLRenderPipeline.cpp](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/gl/pipeline/OpenGLRenderPipeline.cpp:24)
- [OpenGLVideoIOBridge.cpp](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/gl/pipeline/OpenGLVideoIOBridge.cpp:24)
Recommended direction:
Add lightweight tracing for:
- input callback latency
- input upload skip count
- render-thread request latency
- render queue depth
- render time
- pass build/compile latency
- readback time
- output scheduling lag
- output queue depth
- preroll depth versus spare-buffer depth
- preview present cost and skipped-preview count
- control queue depth
- runtime state lock contention
That would make future tuning and failure diagnosis much easier.
Timing-specific observations from the current code:
- render time is captured as one total number in [OpenGLRenderPipeline.cpp](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/gl/pipeline/OpenGLRenderPipeline.cpp:24), but not split into draw, pack, readback wait, readback copy, or preview present
- frame pacing stats are recorded in [OpenGLVideoIOBridge.cpp](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/gl/pipeline/OpenGLVideoIOBridge.cpp:17), but there is no explicit visibility into how much queued playout headroom remains
- input uploads are intentionally skipped when the GL bridge is busy in [OpenGLVideoIOBridge.cpp](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/gl/pipeline/OpenGLVideoIOBridge.cpp:60), but the app does not currently surface how often that is happening
### 8a. Preview and playout are still too close together
The desktop preview is rate-limited, but still presented from inside the render pipeline path.
Relevant code:
- [OpenGLRenderPipeline.cpp](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/gl/pipeline/OpenGLRenderPipeline.cpp:54)
- [OpenGLComposite.cpp](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/gl/composite/OpenGLComposite.cpp:235)
This means preview presentation can still consume time on the same path that is trying to meet output deadlines.
Recommended direction:
- treat preview as best-effort and entirely subordinate to playout
- move preview present to a separate presentation schedule fed from the latest completed render
- record preview skips and preview present cost independently from playout timing
### 8b. Readback is improved, but still not fully deadline-safe
The async readback path is a good step, but the miss path still falls back to synchronous `glReadPixels()` and then flushes the async pipeline.
Relevant code:
- [OpenGLRenderPipeline.cpp](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/gl/pipeline/OpenGLRenderPipeline.cpp:150)
- [OpenGLRenderPipeline.cpp](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/gl/pipeline/OpenGLRenderPipeline.cpp:228)
That means a single late GPU fence can push the app back onto the most timing-sensitive path exactly when it is already under pressure.
Recommended direction:
- increase readback instrumentation before changing policy again
- consider deeper readback buffering or a true stale-frame reuse policy instead of immediate synchronous fallback
- separate "freshest possible frame" policy from "never miss output deadline" policy and make that tradeoff explicit
### 8c. Background control and file-watch timing are partially event-driven
`ControlServices::PollLoop()` now uses a condition-variable wakeup for queued OSC commit work and a fallback timer for compatibility polling. That removes the old fixed `25 x Sleep(10)` cadence as the default OSC commit timing model, but file-watch/runtime-store refresh work still relies on a compatibility poll path.
Relevant code:
- [ControlServices.cpp](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/control/ControlServices.cpp:217)
That is acceptable as transitional non-critical background work. The Phase 2 bridge now publishes typed reload/file-change events when changes are detected; a later file-watch implementation can replace scanning as the source.
Recommended direction:
- replace runtime-store scanning with true file-watch events when practical
- isolate truly background work from latency-sensitive control reconciliation
- add separate metrics for queue age, not just queue depth
## Phased Roadmap
This roadmap is ordered by architectural dependency rather than by “quick wins.” The goal is to move the app toward clearer ownership boundaries and safer live behavior without doing later work on top of foundations that are likely to change again.
### Phase 1. Define subsystem boundaries and target architecture
Before changing major internals, formalize the target responsibilities for each major part of the app.
Status:
- Design deliverable: complete.
- Runtime implementation foothold: complete.
- Target boundary extraction: not complete across the whole app; remaining work is tracked by later phases, especially the event model, render ownership, live-state layering, backend lifecycle, telemetry, and persistence work.
Target split:
- `RuntimeStore`
- persisted config
- persisted layer stack
- preset persistence
- `RuntimeCoordinator`
- mutation validation and classification
- committed-live versus transient policy
- snapshot and persistence requests
- `RuntimeSnapshotProvider`
- render-facing immutable or near-immutable snapshots
- parameter values prepared for the render path
- `ControlServices`
- OSC ingress
- web control ingress
- reload/file-watch requests
- commit/persist requests
- `RenderEngine`
- sole owner of live GL rendering
- sole consumer of render snapshots plus transient overlays
- `VideoBackend`
- DeckLink input/output lifecycle
- pacing and scheduling
- `HealthTelemetry`
- logging
- counters
- timing traces
- degraded-state reporting
Why this phase comes first:
- it prevents later refactors from reintroducing responsibility overlap
- it gives names to the seams the later phases will build around
- it reduces the risk of replacing one monolith with several poorly-defined ones
Suggested deliverables:
- a short architecture diagram
- a responsibility table for each subsystem
- a list of allowed dependencies between subsystems
- a dedicated Phase 1 design note:
- [PHASE_1_SUBSYSTEM_BOUNDARIES_DESIGN.md](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/docs/PHASE_1_SUBSYSTEM_BOUNDARIES_DESIGN.md)
- a subsystem design bundle index:
- [docs/subsystems/README.md](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/docs/subsystems/README.md)
Current implementation note:
The repo now has concrete runtime classes, folders, read models, and subsystem tests for the Phase 1 names. These classes are the runtime foothold for later phases; app-wide extraction still continues around eventing, render ownership, backend lifecycle, persistence, and telemetry.
### Phase 2. Introduce an internal event model
Once subsystem boundaries are defined, introduce a typed event pipeline between them. This should happen before large state splits so the app has a stable coordination model.
Dedicated design note:
- [PHASE_2_INTERNAL_EVENT_MODEL_DESIGN.md](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/docs/PHASE_2_INTERNAL_EVENT_MODEL_DESIGN.md)
Example event families:
- control events
- `OscParameterTargeted`
- `UiParameterCommitted`
- `TriggerFired`
- runtime events
- `ShaderReloadRequested`
- `PackagesRescanned`
- `PersistStateRequested`
- render events
- `OverlayApplied`
- `OverlaySettled`
- `SnapshotPublished`
- backend events
- `InputSignalChanged`
- `OutputLateFrameDetected`
- `OutputDroppedFrameDetected`
- health events
- `SubsystemWarningRaised`
- `SubsystemRecovered`
Why this phase comes second:
- it provides a migration path away from direct cross-calls
- it makes ownership explicit before data structures are split apart
- it lets you move one subsystem at a time without losing coordination
Suggested outcome:
- the app stops relying on “shared object plus mutex plus polling” as the default coordination pattern
### Phase 3. Finish live-state and service-facing coordination
After the event model exists, finish separating live committed state and service-facing coordination from the runtime facades.
Dedicated design note:
- [PHASE_3_LIVE_STATE_SERVICE_COORDINATION_DESIGN.md](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/docs/PHASE_3_LIVE_STATE_SERVICE_COORDINATION_DESIGN.md)
Recommended split:
- `RuntimeStore`
- owns config and saved layer data
- handles serialization/deserialization
- does not sit on the live render path
- `RuntimeCoordinator`
- resolves control actions
- validates mutations
- publishes new snapshots
- bridges events between services and render
- `RuntimeSnapshotProvider`
- publishes immutable render snapshots
- avoids large shared mutable structures on the render path
Why this phase comes before render-thread isolation:
- render isolation is easier when the render thread consumes clean snapshots instead of a large mutable host object
- otherwise the GL refactor still drags along too much shared state complexity
Primary design rule:
- render should read snapshots
- persistence should write stored state
- services should request mutations through the coordinator
### Phase 4. Make the render thread the sole GL owner
With state and coordination cleaner, move to a dedicated render-thread model.
Dedicated design note:
- [PHASE_4_RENDER_THREAD_OWNERSHIP_DESIGN.md](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/docs/PHASE_4_RENDER_THREAD_OWNERSHIP_DESIGN.md)
Status:
- complete for GL ownership
- remaining playout-headroom work is tracked under Phase 7/backend lifecycle
Target behavior:
- one thread owns the GL context
- input callbacks never perform GL work directly
- output callbacks never perform GL work directly
- preview presentation, texture upload, render passes, readback, and output pack work are all issued by the render thread
Other threads should only:
- enqueue new video frames
- enqueue control updates
- enqueue backend events
- consume produced output buffers
Why this phase comes here:
- it is much safer once state access and control coordination are no longer centered on one shared runtime object
- it avoids coupling the render-thread refactor to storage and service refactors at the same time
Expected benefits:
- less cross-thread GL contention
- easier timing reasoning
- much lower risk of callback-driven stalls
- a clearer foundation for future GPU pipeline work
### Phase 5. Refactor live state layering into an explicit composition model
Once rendering and snapshots are isolated, formalize how final parameter values are derived.
Dedicated design note:
- [PHASE_5_LIVE_STATE_LAYERING_DESIGN.md](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/docs/PHASE_5_LIVE_STATE_LAYERING_DESIGN.md)
Status:
- complete for the current architecture
- `RuntimeStateLayerModel` names the state categories
- `CommittedLiveState` physically owns committed/session layer state
- `RenderStateComposer` consumes `LayeredRenderStateInput`
- `RuntimeLiveState` owns transient overlay smoothing, generation, commit settlement, and compatibility pruning
- settled OSC commits update session state without requesting persistence by default
Render should derive final values from a clear composition rule such as:
- `final = base + committed + transient`
Why this phase follows render isolation:
- once render owns snapshot consumption, it becomes much easier to cleanly evaluate layered state without touching persistence or control services
- it turns the current OSC overlay behavior into a first-class model instead of an implementation detail
Expected benefits:
- fewer one-off sync rules
- clearer behavior for OSC, UI changes, and automation
- easier future expansion to presets, cues, or timed transitions
### Phase 6. Move persistence onto a background snapshot writer
Status: complete. Runtime-state persistence is now a background concern rather than a synchronous side effect of mutations.
Dedicated design note:
- [PHASE_6_BACKGROUND_PERSISTENCE_DESIGN.md](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/docs/PHASE_6_BACKGROUND_PERSISTENCE_DESIGN.md)
Implemented behavior:
- mutations update authoritative in-memory stored state
- persistence requests are queued
- disk writes are debounced and coalesced
- writes use temp-file replacement where practical
- shutdown flush behavior is explicit and tested
Why this phase comes after state splitting:
- otherwise persistence logic will need to be rewritten twice
- it should operate on the new `RuntimeStore` model, not on a mixed-responsibility runtime object
Expected benefits:
- less timing interference
- better corruption resistance
- cleaner restart/recovery semantics
### Phase 7. Make DeckLink/backend lifecycle explicit with a state machine
Once the render and state layers are cleaner, refactor the video backend into an explicit lifecycle model.
Dedicated design note:
- [PHASE_7_BACKEND_LIFECYCLE_PLAYOUT_DESIGN.md](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/docs/PHASE_7_BACKEND_LIFECYCLE_PLAYOUT_DESIGN.md)
Suggested states:
- uninitialized
- devices-discovered
- configured
- prerolling
- running
- degraded
- stopping
- stopped
- failed
Why this phase belongs here:
- the backend should integrate with the new event model
- degraded/recovery behavior will be easier once rendering and state coordination are already more deterministic
Expected benefits:
- safer startup/shutdown ordering
- clearer recovery behavior
- easier handling of missing input, dropped frames, or reconfiguration
- a clearer place to own playout headroom policy, output queue sizing, and late-frame recovery behavior
### Phase 7.5. Make playout timing proactive and deadline-aware
Phase 7 made backend lifecycle, ready-frame queueing, measured recovery, and backend playout health visible. The remaining timing-specific work is to make output production proactive instead of demand-filled by completion pressure.
Dedicated design note:
- [PHASE_7_5_PROACTIVE_PLAYOUT_TIMING_DESIGN.md](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/docs/PHASE_7_5_PROACTIVE_PLAYOUT_TIMING_DESIGN.md)
Expected benefits:
- output frames are produced ahead based on queue pressure or cadence
- DeckLink completion handling normally consumes already-ready frames
- preview and synchronous readback fallback become explicitly subordinate to playout deadlines
- queue depth, readback misses, preview skips, and render timing explain why headroom drains
### Phase 8. Add structured health, telemetry, and operational reporting
This phase should happen after the main ownership changes so the telemetry can reflect the final architecture instead of a transient one.
Dedicated design note:
- [PHASE_8_HEALTH_TELEMETRY_DESIGN.md](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/docs/PHASE_8_HEALTH_TELEMETRY_DESIGN.md)
Recommended coverage:
- render queue depth
- input callback latency
- input upload skip count
- output scheduling lag
- output queue depth and spare-buffer depth
- readback timing
- readback fence wait timing
- synchronous readback fallback count
- preview present timing and skipped-preview count
- snapshot publish frequency
- persistence queue depth
- event queue depth
- backend state transitions
- warning/error counters per subsystem
Also replace modal-only error handling with:
- structured in-app health state
- severity-based logging
- rolling log files
- operator-visible degraded-state messages
Why this phase comes last:
- it should instrument the architecture you intend to keep
- otherwise instrumentation work gets invalidated by the refactor
## Recommended Execution Order
If this is approached as a serious architecture program rather than opportunistic cleanup, the recommended order is:
1. Define subsystem boundaries and target architecture.
2. Introduce the internal event model.
3. Finish runtime live-state/service coordination.
4. Make the render thread the sole GL owner.
5. Formalize live state layering and composition.
6. Move persistence to a background snapshot writer.
7. Refactor DeckLink/backend lifecycle into an explicit state machine.
8. Make playout timing proactive and deadline-aware.
9. Add structured telemetry, health reporting, and operational diagnostics.
## Why This Order Makes Sense
This order tries to avoid doing foundational work twice.
- The event model comes before major subsystem extraction so coordination patterns stabilize early.
- runtime state ownership is split before render isolation so the render thread does not inherit a monolithic state model.
- Live state layering is formalized only after render ownership is clearer.
- Persistence moved after the state model split so it could target the durable snapshot model rather than an older mixed-responsibility runtime object.
- Telemetry is intentionally late so it instruments the architecture that survives the refactor.
## Short Version
The app is in a much better place than it was before the OSC timing work. The shared-GL ownership risk has now been addressed by Phase 4; the main remaining live-resilience risk is output playout headroom because DeckLink callbacks still synchronously request render-thread output production. The most sensible path forward is:
1. define boundaries
2. establish an event model
3. split state ownership
4. isolate rendering
5. formalize layered live state
6. complete background persistence
7. explicit backend lifecycle
8. proactive playout timing
9. health and telemetry
That sequence gives each later phase a cleaner foundation than the current app has today.

View File

@@ -1,406 +0,0 @@
# Audio / SDI Tearing Investigation
Date: 2026-05-05
## Problem
After adding DeckLink audio pass-through, the SDI output intermittently shows a torn/corrupted frame. The preview window does not show the artifact.
Observed artifact:
- Bottom portion of the SDI image can show an offset mix of current/previous frame.
- Looks like a frame-buffer or output-transfer issue rather than shader rendering.
- Occurs even with all shaders bypassed.
- Main branch is known good with no tearing.
Later tests also showed audio tearing/stutter when non-silent audio was scheduled.
## Known Good Baseline
- `main` branch has no SDI tearing.
- Current branch with `audioEnabled: false` ran for several minutes with no visible tearing.
This strongly suggests the issue is tied to DeckLink audio output/scheduling rather than the shader stack.
## SDK References Checked
### `InputLoopThrough`
Location:
`3rdParty/Blackmagic DeckLink SDK 16.0/Win/Samples/InputLoopThrough`
Findings:
- This is the SDK loop-through sample that keeps audio.
- It preserves DeckLink audio packet timestamps using `GetPacketTime(..., m_frameTimescale)`.
- It schedules audio packets with `ScheduleAudioSamples(..., packetTime, m_frameTimescale, ...)`.
- It uses 16-channel 32-bit embedded audio by default.
- It has separate scheduling threads for video/audio.
- It waits for both video and audio preroll before `StartScheduledPlayback`.
### `LoopThroughWithOpenGLCompositing`
Location:
`3rdParty/Blackmagic DeckLink SDK 16.0/Win/Samples/LoopThroughWithOpenGLCompositing`
Findings:
- This sample is the base for this app.
- It ignores `IDeckLinkAudioInputPacket`.
- It does not demonstrate audio pass-through.
### `SignalGenerator`
Location:
`3rdParty/Blackmagic DeckLink SDK 16.0/Win/Samples/SignalGenerator`
Findings:
- Uses `RenderAudioSamples()` callback to top up audio when DeckLink requests samples.
- Uses `GetBufferedAudioSampleFrameCount()` and a water level before scheduling more audio.
## Tests Tried And Results
### 1. Initial audio pass-through with FIFO and sample-time accumulator
Implementation:
- Copied incoming audio into a stereo FIFO.
- Scheduled audio with a generated `mNextAudioSampleFrame` clock in 48 kHz timescale.
- Matched delay to video preroll.
Result:
- Audio eventually worked.
- SDI video tearing appeared.
Conclusion:
- Basic audio output path triggered SDI instability.
### 2. Reworked audio toward SDK `InputLoopThrough` packet-timestamp model
Implementation:
- Preserved incoming packet time via `GetPacketTime(..., mFrameTimescale)`.
- Queued timestamped audio packets.
- Scheduled packets with `ScheduleAudioSamples(..., packet.streamTime, mFrameTimescale, ...)`.
Result:
- Tearing persisted.
Conclusion:
- Simply matching SDK timestamp domain did not fix the issue.
### 3. Restored video callback closer to `main`
Implementation:
- Removed extra `glFinish()` calls.
- Restored preview/readback ordering closer to `main`.
- Re-enabled fast transfer path after earlier tests disabled it.
- Removed audio texture upload from video playout callback.
- Removed audio analysis and audio locks from video playout callback.
- Removed DeckLink scheduling mutex around `ScheduleVideoFrame`.
Result:
- Tearing frequency seemed reduced at one point, but tearing persisted.
Conclusion:
- Extra work in the playout callback may have made timing worse, but was not the root cause.
### 4. Disabled audio completely
Config:
```json
"audioEnabled": false
```
Result:
- Ran for several minutes with no visible tearing.
Conclusion:
- The tearing is tied to audio being enabled.
### 5. Enabled audio input/analysis but disabled DeckLink audio output
Config:
```json
"audioEnabled": true,
"audioOutputEnabled": false
```
Result:
- No tearing appeared.
Conclusion:
- DeckLink audio input and CPU analysis are not the trigger.
- The problem is on the DeckLink audio output side.
### 6. Enabled DeckLink audio output but disabled scheduling
Config:
```json
"audioEnabled": true,
"audioOutputEnabled": true,
"audioScheduleEnabled": false
```
Result:
- No video tearing.
- Slight stutter appeared.
Conclusion:
- `EnableAudioOutput()` alone did not produce the tearing.
- Stutter was likely from enabling an audio output stream without feeding it samples.
### 7. Enabled audio scheduling but skipped audio preroll
Config:
```json
"audioEnabled": true,
"audioOutputEnabled": true,
"audioScheduleEnabled": true,
"audioPrerollEnabled": false
```
Result:
- Video tearing returned.
- Stutter also present.
Conclusion:
- `BeginAudioPreroll()` / `EndAudioPreroll()` are not required to trigger the tear.
- `ScheduleAudioSamples()` is strongly implicated.
### 8. Retained scheduled audio packet memory after `ScheduleAudioSamples`
Implementation:
- Kept scheduled packet buffers alive in a retain queue after scheduling.
- Avoided passing DeckLink pointers to vectors that immediately went out of scope.
Result:
- Video tearing and stutter persisted.
Conclusion:
- Buffer lifetime after `ScheduleAudioSamples()` was not the root cause.
### 9. Added audio water-level cap
Implementation:
- Restored SDK-style `GetBufferedAudioSampleFrameCount()` check.
- Only scheduled more audio if DeckLink buffer was below the target water level.
Result:
- Stutter was reduced.
- Video tearing persisted.
Conclusion:
- Overscheduling contributed to stutter/timing pressure.
- It did not explain the tearing.
### 10. Removed standalone audio scheduler thread
Implementation:
- Stopped starting the dedicated audio scheduler thread.
- Audio top-up occurred from input packet arrival and `RenderAudioSamples()` callback.
Result:
- No meaningful change.
Conclusion:
- The polling thread itself was not the cause.
### 11. Switched from timestamped audio output to continuous audio output
Implementation:
- Changed audio output to `bmdAudioOutputStreamContinuous`.
- Scheduled audio using a monotonic 48 kHz sample clock.
Result:
- Video tearing and stutter persisted.
Conclusion:
- The issue was not specific to timestamped output mode.
### 12. Rendered into the actual `completedFrame`
Implementation:
- Changed `PlayoutFrameCompleted()` to reuse the exact `completedFrame` passed by DeckLink rather than rotating an independent output-frame queue.
Result:
- No change.
Conclusion:
- The app was probably not overwriting a still-in-use frame from its output queue.
### 13. Scheduled generated silence instead of captured audio
Config:
```json
"audioScheduleSilence": true
```
Result:
- Occasional stutter.
- No video tearing.
Conclusion:
- Scheduling audio buffers itself can be stable if the audio data is zero.
- Non-zero audio data appears to be important.
### 14. Flattened captured audio into PCM FIFO and scheduled fixed chunks
Implementation:
- Captured packets were flattened into a PCM FIFO.
- DeckLink received fixed 10 ms chunks rather than original packet boundaries.
- Missing audio was padded with silence.
Result:
- Video tearing returned.
- Audio stutter/tearing returned.
Conclusion:
- Packet boundaries/timestamps were not the whole cause.
- Non-zero captured audio data still triggered instability.
### 15. Scheduled generated 440 Hz tone
Config:
```json
"audioScheduleTone": true
```
Result:
- Video tearing occurred.
- Tone/audio also tore.
Conclusion:
- The issue is not specific to captured input data.
- Non-zero scheduled audio, even generated tone, triggers the problem.
### 16. Changed DeckLink output to 16 embedded audio channels
Implementation:
- Enabled DeckLink audio output with 16 channels instead of 2.
- Mapped stereo to channels 1/2.
- Filled channels 3-16 with silence.
Result:
- Video tearing and audio tearing still occurred.
Conclusion:
- The issue is not simply caused by 2-channel embedded audio output.
### 17. Used DeckLink-owned output video frames with audio enabled
Implementation:
- When audio output is enabled:
- disabled fast transfer path
- created output frames with `CreateVideoFrame()`
- avoided `CreateVideoFrameWithBuffer()` and the custom pinned playout allocator
Result:
- Video tearing and audio tearing still occurred.
Conclusion:
- The custom pinned output video buffers are likely not the root cause.
## Current Strong Conclusions
- Shader stack is not the cause.
- Preview/render output is not showing the issue, so the artifact is SDI/output-side.
- DeckLink audio input is not the cause.
- DeckLink audio output enabled but unscheduled does not cause tearing.
- `ScheduleAudioSamples()` with zero/silent buffers does not cause tearing.
- `ScheduleAudioSamples()` with non-zero audio causes both video tearing and audio tearing.
- The problem persists across:
- timestamped audio output
- continuous audio output
- captured audio
- generated tone
- 2-channel output
- 16-channel embedded output
- app-owned/pinned output video buffers
- DeckLink-owned output video frames
## Current Hypothesis
The issue appears to be a DeckLink output interaction where non-zero embedded audio samples disturb SDI video/audio output in this apps scheduling model.
Since silence is stable but tone is not, the next likely areas to investigate are:
- Audio sample format/range/endian expectations.
- Whether DeckLink expects 32-bit audio samples to be in a different effective range than we are providing.
- Whether the scheduled audio buffer layout for the selected hardware/output mode differs from our assumptions.
- Whether the selected output mode/keyer/SDI configuration has constraints when non-zero embedded audio is present.
- Whether the SDK sample behaves correctly on the same hardware with a generated tone and same video mode.
## Suggested Next Tests
1. Schedule very low amplitude non-zero audio, e.g. constant `1`, then `256`, then a very quiet sine.
2. Try 16-bit audio output instead of 32-bit if supported.
3. Try `bmdAudioOutputStreamContinuousDontResample`.
4. Disable external keying and test with non-zero audio.
5. Build/run the SDK `SignalGenerator` or `InputLoopThrough` sample on the same DeckLink device, video mode, and SDI output path with non-zero embedded audio.
6. Add instrumentation for DeckLink status/errors around scheduled video/audio completion.
7. Confirm Desktop Video setup panel audio/SDI settings for the selected output.
## Current Config At Time Of Note
```json
"audioEnabled": true,
"audioOutputEnabled": true,
"audioScheduleEnabled": true,
"audioPrerollEnabled": true,
"audioScheduleSilence": false,
"audioScheduleTone": false
```

View File

@@ -0,0 +1,175 @@
# Current System Architecture
This document describes how the current `RenderCadenceCompositor` app works.
The implementation is cadence-first: the render/output path owns frame timing, while shader compilation, HTTP control, preview, persistence, and video I/O edges stay outside the render cadence loop wherever possible. The guardrails in [Render Cadence Golden Rules](RENDER_CADENCE_GOLDEN_RULES.md) are the operating contract.
## Application Shape
The app is a native C++ OpenGL compositor with:
- optional DeckLink input
- optional DeckLink scheduled output
- a render-thread-owned OpenGL context
- runtime Slang shader packages from `shaders/`
- a configurable active layer stack
- a local HTTP/WebSocket control server
- optional Win32/GDI preview from system-memory output frames
- background runtime-state persistence
- cadence telemetry and log output
Primary source areas:
- `src/app`: startup/shutdown orchestration, config loading, runtime layer controller
- `src/render`: render thread, readback, runtime GL scene, shared-context shader prepare worker
- `src/frames`: system-memory frame exchange
- `src/video`: DeckLink input/output edges and scheduling
- `src/runtime`: shader catalog support, layer model, Slang build bridge, font atlas build, runtime-state persistence
- `src/control`: HTTP routing, command parsing, OpenAPI state JSON
- `src/preview`: optional non-consuming preview window
- `src/telemetry` and `src/logging`: runtime observation and logging
## Startup
Startup broadly proceeds as:
1. Load `config/runtime-host.json` through `AppConfigProvider`, then apply CLI overrides.
2. Load the supported shader catalog from the configured `shaderLibrary`.
3. Start the runtime-state persistence writer.
4. Try to restore `runtime/runtime_state.json`.
5. If restore fails or no usable state exists, create one layer from the configured default shader.
6. Start the render thread.
7. Queue background Slang builds for every pending active layer.
8. Build a small completed-frame reserve.
9. Start optional preview, optional DeckLink output, telemetry, and HTTP control.
The runtime-state restore is intentionally app/control side work. The render thread does not read JSON, inspect the shader library, or decide what to compile.
## Runtime Layer State
`RuntimeLayerController` owns the app-side layer model and coordinates:
- supported shader catalog loading
- layer add/remove/reorder/bypass/shader assignment
- parameter update/reset
- startup restore
- reload reconciliation
- background Slang builds
- render-layer publication
- runtime-state persistence requests
`RuntimeLayerModel` owns the in-memory active stack:
- layer id
- shader id and display name
- build state and message
- bypass state
- manifest parameter definitions
- current parameter values
- render-ready artifacts
The current durable runtime state is stored in `runtime/runtime_state.json`. It contains the active stack order, shader ids, bypass flags, and parameter values. On startup, valid saved layers are restored in order. Missing shader packages are skipped, invalid saved parameter values fall back to manifest defaults, and a missing or unusable file falls back to the configured default shader.
Manual stack preset routes are present in the UI/OpenAPI surface but are not implemented in the current native command path yet. `runtime_state.json` is the supported latest-working-state mechanism.
## Persistence
`RuntimeStatePersistenceWriter` performs debounced background writes to `runtime/runtime_state.json`.
Durable UI/API mutations request persistence after they are accepted:
- add/remove layer
- reorder layer
- set bypass
- set shader
- update parameter
- reset parameters
- reload compatibility refresh
The mutation path snapshots the current layer model and hands serialized state to the writer. File IO happens on the persistence worker, not on the render thread or cadence path. Shutdown flushes the latest pending snapshot.
OSC-driven changes are intentionally not part of this autosave path yet.
## Shader Reload
`POST /api/reload` and the control UI reload button:
- rescan `shaders/`
- re-read manifests
- rebuild the supported shader catalog
- update active layer metadata and parameter definitions from changed manifests
- preserve compatible parameter values
- default new or incompatible parameter values
- queue recompilation for every catalog-valid layer in the active stack
Reload does not compile every package in the shader library. A package is compiled when it is part of the active layer stack. If an active layer references a shader that no longer exists, that layer is marked failed and skipped. Existing render output remains active where possible until replacement builds are ready.
`autoReload` is still exposed in config/state for compatibility, but automatic file watching is not currently wired.
## Render Ownership
The render thread owns the app OpenGL context during normal operation.
The render path consumes published render-layer snapshots. It does not:
- parse JSON
- scan files
- launch Slang
- run font atlas generation
- perform persistence
- handle HTTP or OSC
- call DeckLink discovery/setup APIs
When a runtime shader build completes, the app publishes a render-layer artifact. The render thread-owned runtime scene diffs the snapshot and queues changed pass programs to the shared-context prepare worker. The render thread swaps in an already-prepared render plan at a frame boundary.
## Video And Preview
DeckLink input and output are optional edges.
Input captures BGRA8 directly where possible, or raw UYVY8 into `InputFrameMailbox` for render-thread GPU decode. The input edge does not call GL, render, preview, screenshot, shader, or output scheduling code.
Output consumes completed system-memory frames from `SystemFrameExchange` and schedules them to DeckLink. If DeckLink output is unavailable, the app continues running render cadence, control, preview, telemetry, and logging.
`PreviewWindowThread` is optional and uses a non-consuming system-memory tap. It paints with Win32/GDI on its own thread and skips preview ticks instead of blocking the frame exchange.
Screenshot routes are present in the UI/OpenAPI surface but are not implemented in the current native command path yet.
## Control Surface
The HTTP server runs on its own thread. It serves:
- UI assets
- OpenAPI/Swagger docs
- `GET /api/state`
- `/ws` state updates
- layer mutation POST routes
- `/api/reload`
Known but not implemented in the current native command path:
- `/api/layers/move`
- `/api/stack-presets/save`
- `/api/stack-presets/load`
- `/api/screenshot`
Unsupported routes return an action response with `ok: false`.
## Tests
Native tests cover the main non-GL contracts:
- JSON parsing/serialization
- runtime parameter normalization
- shader package registry and Slang validation
- supported shader catalog
- runtime layer restore/reload behavior
- runtime-state persistence writer
- HTTP command parsing
- frame exchange and input mailbox behavior
- video format and scheduling helpers
Run:
```powershell
cmake --build --preset build-debug --target RUN_TESTS --parallel
```

View File

@@ -0,0 +1,414 @@
# DeckLink / OpenGL Lessons Learned
This document summarizes the practical lessons from the Phase 3-7.7 refactor work, especially the DeckLink playout timing experiments.
It is intentionally broader than the phase design docs. The goal is to preserve what we now know about the system so future architecture choices start from evidence instead of rediscovering the same constraints.
## High-Level Lesson
The application is not just a renderer with a video output attached.
It is a real-time playout system with several independent clocks:
- the selected output cadence, for example 59.94 fps
- the GPU render/readback timeline
- the DeckLink scheduled playback clock
- the Windows thread scheduler
- the input capture callback cadence
- the preview/window message loop
- the runtime/control update cadence
Stable playback depends on assigning one owner to each timing domain and keeping those domains loosely coupled.
## What Worked
### Named State Contracts Helped
`RenderFrameInput` and `RenderFrameState` made the render path easier to reason about.
Before that, frame rendering depended on scattered choices about snapshots, cache state, layer state, input source state, and runtime service state. Naming the frame contract made it possible to move logic out of `RenderEngine` and toward explicit frame construction.
Lesson:
- keep frame inputs explicit
- keep render-frame state immutable for the duration of a frame
- avoid making the renderer ask global systems which state it should use mid-frame
### Render-Thread Ownership Helped
Moving GL work behind a render-thread boundary reduced wrong-thread GL access risk and made ownership clearer.
The current render thread is still shared by output render, input upload, preview, screenshot, resize, and reset work, so it is not yet a pure output cadence thread. But the ownership direction is right.
Lesson:
- GL context ownership should be explicit
- public methods should enqueue or request work
- render-thread methods should own GL bodies
- synchronous calls should be reserved for places that genuinely need a result
### Background Persistence Was Worth It
Moving persistence away from hot render/control paths reduced incidental latency risk and made state writes easier to reason about.
Lesson:
- runtime/control persistence should not sit on output render timing
- shutdown flushing is fine, steady-state blocking is not
### Lifecycle State Was Worth It
The backend lifecycle model gave us better failure and shutdown vocabulary.
This became important once startup stopped being a single `Start()` call and became:
- prepare output schedule
- start render cadence
- warm up real frames
- start input streams
- start scheduled playback
Lesson:
- playout startup needs phases
- degradation should be explicit
- shutdown order should be deliberate and testable
## What Did Not Work
### Completion-Driven Rendering Was Too Fragile
Rendering on or near DeckLink completion can average the target frame rate, but it leaves no headroom.
When the callback asks for a frame just-in-time, any small delay in render, readback, scheduling, or Windows wake timing becomes visible as a buffer dip or stutter.
Lesson:
- DeckLink completion should release scheduled resources and wake scheduling
- it should not render
- it should not decide visual fallback policy in steady state
### Black Fallback Hid The Real Timing Problem
Scheduling black on app-ready underrun made the pipeline appear to keep moving while producing visible black flicker.
It also made diagnosis harder because DeckLink could have scheduled frames while the app visibly failed.
Lesson:
- black is a startup/error/degraded-state policy, not normal steady-state recovery
- steady-state underruns should be measured as timing failures
### Synthetic Schedule Lead Was Misleading
The synthetic scheduled/completed index could report a large buffer while DeckLink still showed low actual device buffer depth.
Real DeckLink `GetBufferedVideoFrameCount()` telemetry was necessary to separate:
- app-owned scheduled slots
- synthetic schedule lead
- actual hardware/device buffer depth
Lesson:
- measure actual device buffer depth
- keep synthetic counters only as diagnostics
- do not infer device health from internal stream indexes alone
### Schedule Cursor Recovery Must Be Conservative
The DeckLink schedule cursor should normally advance as a continuous stream timeline. Continuously realigning the next scheduled stream time to the sampled playback cursor can create its own timing fault: output may look like low FPS even when render and scheduling counters average 59.94/60 fps.
What worked better:
- use the exact DeckLink frame duration for the render cadence
- keep healthy scheduling on a continuous stream cursor
- measure schedule lead from DeckLink playback time versus the next schedule time
- realign only after real pressure, such as a late/drop report or dangerously low measured lead
- re-arm proactive realignment only after lead has recovered
Lesson:
- schedule recovery is an output-edge safety valve, not a per-frame timing policy
- if recovery increments continuously, the recovery path has become the problem
- include schedule lead and realignment count in telemetry/logs so drift is visible before guessing
### More Buffer Is Not Automatically Smoother
Increasing DeckLink scheduled frames sometimes made the reported device buffer look healthier while visible motion still stuttered.
The problem was not only "how many frames are scheduled"; it was also whether the scheduled frames represented a stable render cadence.
Lesson:
- buffer depth absorbs jitter, but it cannot fix bad cadence ownership
- a full buffer of poorly timed or repeated frames can still look wrong
### Speed-Up Catch-Up Was The Wrong Instinct
Letting the producer sprint to refill the buffer created new timing artifacts.
The render side should behave like a stable game/render loop: render at the selected cadence, record lateness, and only skip ticks when render/GPU work itself overruns.
Lesson:
- the render thread should not render faster because DeckLink is empty
- buffer drain is a failure signal, not a sprint signal
- warmup should fill buffers before playback starts
## GPU Readback Lessons
### The Original Readback Path Was The Major Collapse
Early Phase 7.5 telemetry showed `glReadPixels(..., nullptr)` into the PBO costing roughly 8-14 ms on representative samples. That was enough to collapse ready depth and cause long freezes.
Direct synchronous readback was worse on the sampled machine.
Cached-output mode, while visually invalid for live output, immediately recovered timing. That proved ongoing GPU-to-CPU transfer was the major cost in that version of the path.
Lesson:
- isolate readback cost from render cost
- use intentionally invalid cached-output experiments when diagnosing throughput
- do not assume async PBO is actually cheap on every format/driver path
### BGRA8 Packing Changed The Problem
Changing the output path so readback matched the DeckLink BGRA8 format made `asyncQueueReadPixelsMs` drop dramatically in sampled runs.
Long pauses disappeared and the remaining issue became short stutters/cadence gaps.
Lesson:
- output/readback format matters
- avoid format conversions on the readback path when possible
- BGRA8 is a good current format target for experiments
- v210/YUV packing can be deferred until cadence is stable
### DeckLink SDK Fast Transfer Was Not Available On The Test GPU
The SDK OpenGL fast-transfer path depends on hardware/extension support that was not present on the RTX 4060 Ti test machine:
- NVIDIA DVP path was gated around Quadro-style support
- `GL_AMD_pinned_memory` was not exposed
Lesson:
- SDK fast-transfer samples are useful references but not a universal fix
- unsupported fast-transfer code should not be central to the architecture
- the default path must work with ordinary consumer GPUs
## DeckLink Lessons
### DeckLink Wants Scheduled System-Memory Frames
Using `CreateVideoFrameWithBuffer()` lets DeckLink schedule frames backed by our system-memory slots.
That is the right ownership model for this app:
- render/readback writes into a slot
- DeckLink schedules a frame that references that slot
- the slot is protected until DeckLink completion
Lesson:
- system-memory slots are the contract between render and playout
- scheduled slots must not be recycled early
- completed-but-unscheduled slots should form a bounded FIFO reserve for playout
### Startup Needs Real Preroll
Starting scheduled playback before real rendered frames exist creates avoidable startup fragility.
The better startup shape is:
- prepare the DeckLink schedule
- start render cadence
- render warmup frames at normal cadence
- schedule those frames as preroll
- start DeckLink scheduled playback
Lesson:
- do not use black preroll as the normal startup path
- do not render faster during warmup
- if warmup cannot fill in a bounded time, fail/degrade visibly
## Buffering Lessons
### There Are Two Different Buffers
The app has at least two important frame stores:
- system-memory completed FIFO reserve frames
- DeckLink scheduled/device buffer
They have different ownership rules.
Completed-but-unscheduled frames should be a bounded FIFO reserve for playout. If that reserve overflows, dropping the oldest completed frame is an app-side reserve policy and should be counted separately from DeckLink dropped frames.
Scheduled frames are not disposable because DeckLink may still read them.
Lesson:
- completed frames waiting for playout are a bounded FIFO reserve
- scheduled frames are owned by DeckLink until completion
- keep metrics for both
### Consume-Before-Render Is The Wrong Model For Completed Frames
If the render cadence waits for completed frames to be consumed, DeckLink timing can indirectly slow the renderer.
That couples the clocks again.
Lesson:
- render cadence should keep rendering at selected cadence
- render acquire should not evict completed frames that are waiting for playout
- if the completed reserve overflows, drop/count the oldest unscheduled completed frame
- only scheduled/in-flight saturation should prevent rendering to a safe slot
## Render Thread Lessons
### The Current Render Thread Is Still Shared
The GL render thread currently handles:
- output rendering
- input upload
- preview present
- screenshot capture
- render reset commands
- shader/resource operations
Output render can therefore be delayed by queued or inline work.
Lesson:
- "one GL thread" is not the same as "one output cadence thread"
- output render should become the highest-priority GL operation
- non-output GL work needs budgets, coalescing, or deferral
### Input Upload Is A Suspect Timing Coupling
Output render currently processes input upload work immediately before rendering the output frame.
That keeps input fresh but can steal time from the exact frame we are trying to render on cadence.
Lesson:
- measure input upload count and time immediately before output render
- test policies such as `one_before_output` or `skip_before_output`
- prefer latest-input semantics over draining every pending upload
### CPU Input Conversion Can Be Worse Than Input Copy
When DeckLink input only exposed UYVY8 on the test machine, an initial CPU UYVY-to-BGRA conversion in the input callback measured around a full-frame budget on sampled runs and reduced input cadence dramatically.
Moving the input edge to raw UYVY8 capture changed the ownership:
- DeckLink callback copies raw supported input bytes into `InputFrameMailbox`
- the mailbox keeps latest-frame semantics and uses a contiguous copy when row strides match
- the render thread uploads/decodes UYVY8 into the shader-visible `gVideoInput` texture
- runtime shaders continue to see decoded input, not packed capture bytes
Lesson:
- keep input callbacks as capture/copy edges
- keep GL decode/upload in the render-owned path
- measure input copy, upload, and decode separately
- do not hide expensive format conversion inside the DeckLink callback
### Preview And Screenshot Must Stay Secondary
Preview is useful, but DeckLink output is the real-time path.
Screenshot and preview share GL resources and can block or queue work on the same render thread.
Lesson:
- preview should be skipped when output is under pressure
- screenshot capture should be treated as disruptive unless proven otherwise
- forced preview/screenshot should be visible in telemetry
## Telemetry Lessons
The useful telemetry has been the telemetry that separates domains:
- output render queue wait
- render/draw time
- readback queue time
- readback fence/map/copy time
- app ready/completed queue depth
- system-memory free/rendering/completed/scheduled counts
- actual DeckLink buffered-frame count
- DeckLink schedule-call time/failures
- late/drop completion counts
Lesson:
- averages are not enough
- timing spikes matter more than steady low values
- count ownership states, not just queue depth
- keep experiment logs short and evidence-based
## Current Architectural Direction
The current direction is still sound:
```text
Render cadence loop
renders at selected output cadence
writes completed system-memory frames into a bounded FIFO reserve
never sprints to refill DeckLink
Frame store
owns free / rendering / completed / scheduled slots
recycles unscheduled completed frames when needed
protects scheduled frames until completion
DeckLink playout scheduler
consumes completed frames
tops up actual device buffer
never renders
Completion callback
releases scheduled slots
records completion result
wakes scheduler
```
## Rewrite Lesson
A full restart is not obviously the right next move.
The current repo now contains:
- working runtime/control architecture
- useful phase docs
- non-GL tests around key state machines
- real telemetry
- a clearer understanding of DeckLink and OpenGL timing
The better next step is likely a contained "V2 spine" inside the current app:
- harden the render cadence loop
- harden the frame store
- separate DeckLink scheduling
- demote preview/screenshot/input upload below output cadence
- delete old compatibility branches as they become unnecessary
A full rewrite becomes attractive only if the current GL ownership model cannot be made deterministic without excessive surgery, or if the project switches rendering API.
## Practical Rules Going Forward
- One timing authority per domain.
- Render cadence is time-driven, not completion-driven.
- DeckLink scheduling is device-buffer-driven, not render-driven.
- Completion callbacks release and report; they do not render.
- System-memory completed frames are a bounded FIFO reserve.
- Scheduled frames are protected until DeckLink completion.
- Startup uses real rendered warmup/preroll.
- Black fallback is degraded/error behavior, not steady-state behavior.
- Output render has priority over preview, screenshot, and bulk input upload.
- Measure before adding recovery branches.

View File

@@ -0,0 +1,580 @@
# New Render Cadence App Plan
Status: historical implementation plan. `apps/RenderCadenceCompositor` now exists; use [apps/RenderCadenceCompositor/README.md](../apps/RenderCadenceCompositor/README.md) and [Render Cadence Golden Rules](RENDER_CADENCE_GOLDEN_RULES.md) as the current implementation contract.
This plan describes a new application folder that rebuilds the output path from the proven `DeckLinkRenderCadenceProbe` architecture, but as a maintainable app foundation rather than a monolithic probe file.
The first goal is not to port the current compositor feature set. The first goal is to reproduce the probe's smooth 59.94/60 fps DeckLink output with clean module boundaries, tests where possible, and a structure that can later accept the shader/runtime/control systems without compromising timing.
## Working Name
Suggested folder:
```text
apps/RenderCadenceCompositor
```
Suggested executable:
```text
RenderCadenceCompositor
```
The existing app remains intact:
```text
apps/LoopThroughWithOpenGLCompositing
```
The probe remains the control sample:
```text
apps/DeckLinkRenderCadenceProbe
```
## Design Principle
The app is built around one spine:
```text
Render cadence thread
-> owns GL context
-> renders at selected frame cadence
-> performs async BGRA8 readback
-> publishes completed system-memory frames
System frame exchange
-> owns Free / Rendering / Completed / Scheduled slots
-> bounded FIFO reserve for completed unscheduled frames
-> protects scheduled frames until DeckLink completion
DeckLink output thread
-> consumes completed frames
-> schedules to target buffer depth
-> releases scheduled frames on completion
-> never renders
```
Everything else must fit around that spine.
## Non-Negotiable Rules
- The render thread owns its GL context from initialization to shutdown.
- The render thread is driven by selected render cadence, not DeckLink demand.
- DeckLink scheduling never calls render code.
- Completion callbacks never render.
- No synchronous render request exists in the output path.
- Preview, screenshot, input upload, shader rebuild, and runtime control cannot run ahead of a due output frame.
- Completed unscheduled frames are a bounded FIFO reserve; overflow drops are counted separately from DeckLink drops.
- Scheduled frames are protected until DeckLink completion.
- Startup warms up real rendered frames before scheduled playback starts.
## Borrow From The Probe
Keep these behaviors from `DeckLinkRenderCadenceProbe`:
- hidden OpenGL context owned by the render thread
- simple render loop with `nextRenderTime`
- BGRA8 render target
- PBO ring readback
- non-blocking fence polling with zero timeout
- system-memory slots with `Free`, `Rendering`, `Completed`, `Scheduled`
- preserve completed frames waiting for playout; drop/count the oldest completed frame only if the bounded reserve overflows
- DeckLink playout thread only schedules completed frames
- warmup completed frames before `StartScheduledPlayback()`
- one-line-per-second timing telemetry
## Do Not Borrow Directly
The probe is deliberately compact. Do not carry over these probe limitations into the new app:
- one huge `.cpp` file
- hard-coded output mode as permanent behavior
- render pattern, frame store, PBO logic, DeckLink playout, COM setup, and telemetry mixed together
- no reusable interfaces
- no unit-testable non-GL core
## Proposed Folder Structure
```text
apps/RenderCadenceCompositor/
README.md
RenderCadenceCompositor.cpp
app/
RenderCadenceApp.cpp
RenderCadenceApp.h
AppConfig.cpp
AppConfig.h
AppConfigProvider.cpp
AppConfigProvider.h
control/
HttpControlServer.cpp
HttpControlServer.h
RuntimeStateJson.h
platform/
ComInit.cpp
ComInit.h
HiddenGlWindow.cpp
HiddenGlWindow.h
Win32Console.cpp
Win32Console.h
render/
RenderThread.cpp
RenderThread.h
RenderCadenceClock.cpp
RenderCadenceClock.h
SimpleMotionRenderer.cpp
SimpleMotionRenderer.h
Bgra8ReadbackPipeline.cpp
Bgra8ReadbackPipeline.h
PboReadbackRing.cpp
PboReadbackRing.h
frames/
SystemFrameExchange.cpp
SystemFrameExchange.h
SystemFrameTypes.h
video/
DeckLinkOutput.cpp
DeckLinkOutput.h
DeckLinkOutputThread.cpp
DeckLinkOutputThread.h
telemetry/
CadenceTelemetry.cpp
CadenceTelemetry.h
CadenceTelemetryJson.h
TelemetryHealthMonitor.h
logging/
Logger.cpp
Logger.h
json/
JsonWriter.cpp
JsonWriter.h
```
The new app can reuse selected existing source files from the current app at first:
- `videoio/decklink/DeckLinkSession.*`
- `videoio/decklink/DeckLinkDisplayMode.*`
- `videoio/decklink/DeckLinkVideoIOFormat.*`
- `videoio/decklink/DeckLinkFrameTransfer.*`
- `videoio/VideoIOFormat.*`
- `videoio/VideoIOTypes.h`
- `videoio/VideoPlayoutScheduler.*`
- `gl/renderer/GLExtensions.*`
Longer term, shared code should move into common libraries, but the first version can link these files directly to avoid a big build-system refactor.
## Module Responsibilities
### `RenderCadenceApp`
Owns top-level startup/shutdown sequencing.
Responsibilities:
- initialize COM
- discover/select DeckLink output
- create frame exchange
- start render thread
- wait for completed-frame warmup
- start DeckLink output thread
- wait for scheduled buffer warmup
- start DeckLink scheduled playback
- start telemetry printer
- stop in reverse order
It should not contain OpenGL drawing code, frame slot policy, or DeckLink scheduling loops.
### `AppConfig`
Owns runtime settings for the initial app.
Initial settings:
- output mode preference
- output width/height validation
- frame buffer capacity
- PBO depth
- warmup completed-frame count
- target DeckLink scheduled depth
- telemetry interval
Initial values should match the successful probe:
```text
systemFrameSlots = 12
pboDepth = 6
warmupFrames = 4
targetDeckLinkBufferedFrames = 4
pixelFormat = BGRA8
```
### `HiddenGlWindow`
Owns hidden Win32 window, device context, and OpenGL context creation.
Responsibilities:
- create hidden window with `CS_OWNDC`
- choose/set pixel format
- create `HGLRC`
- expose `MakeCurrent()` and `ClearCurrent()`
- destroy context/window safely
Only `RenderThread` should call `MakeCurrent()` after startup.
### `RenderThread`
Owns the render loop and GL context for its full lifetime.
Responsibilities:
- create/bind hidden GL context
- resolve GL extensions
- initialize renderer/readback pipeline
- run cadence loop
- render one frame when due
- queue PBO readback
- consume completed PBOs into `SystemFrameExchange`
- record telemetry
- destroy GL resources on the render thread
It must not:
- wait for DeckLink
- schedule DeckLink frames
- block on a system frame slot if only completed unscheduled frames can be dropped
- accept arbitrary GL tasks ahead of output frames
### `RenderCadenceClock`
Small, testable cadence helper.
Responsibilities:
- track target frame duration
- return whether a render is due
- compute sleep duration
- detect overrun/skipped ticks
- never speed up to fill buffers
This should be unit tested without GL.
### `SimpleMotionRenderer`
First renderer only.
Responsibilities:
- render obvious smooth motion and color changes
- produce BGRA8-compatible framebuffer content
- make dropped/repeated frames visually obvious
This intentionally avoids shader-package/runtime complexity.
### `Bgra8ReadbackPipeline`
Owns output framebuffer and BGRA8 readback orchestration.
Responsibilities:
- configure render target dimensions
- render into an RGBA8/BGRA-compatible texture
- coordinate `PboReadbackRing`
- publish completed frames into `SystemFrameExchange`
### `PboReadbackRing`
Owns PBO/fence state.
Responsibilities:
- queue readback into the next free PBO slot
- poll completed fences with zero timeout
- map/copy completed PBOs into provided system-memory slots
- count PBO misses
- clean up fences/PBOs on render thread
This is GL-backed, but the state model should be small and easy to reason about.
### `SystemFrameExchange`
The central handoff between render and video.
Responsibilities:
- own system-memory frame buffers
- track slot states: `Free`, `Rendering`, `Completed`, `Scheduled`
- provide `AcquireForRender()`
- provide `PublishCompleted()`
- provide `ConsumeCompletedForSchedule()`
- provide `ReleaseScheduledByBytes()`
- drop oldest completed unscheduled frame when render needs a slot
- expose metrics
This should be unit tested heavily.
### `DeckLinkOutput`
Thin wrapper around `DeckLinkSession` for output-only use.
Responsibilities:
- discover/select output mode
- configure output callback
- prepare output schedule
- schedule app-owned system-memory frames
- start scheduled playback
- stop/release resources
- expose actual DeckLink buffered count
No input support in the first version.
### `DeckLinkOutputThread`
Owns playout scheduling loop.
Responsibilities:
- keep scheduled depth near target
- consume completed frames from `SystemFrameExchange`
- schedule them through `DeckLinkOutput`
- release frame if scheduling fails
- sleep briefly when scheduled buffer is full or no completed frame exists
It must not render.
### `CadenceTelemetry`
Owns counters, not policy.
Initial counters:
- rendered frames
- completed readback frames
- scheduled frames
- completion count
- completed-frame drops
- acquire misses
- schedule underruns
- PBO queue misses
- DeckLink late count
- DeckLink dropped count
- free/rendering/completed/scheduled slot counts
- actual DeckLink buffered frames
### `TelemetryHealthMonitor`
Samples cadence telemetry once per interval and logs only health events.
Normal telemetry is available through the HTTP state endpoint. The console should not receive a healthy once-per-second cadence line.
Health events:
- warning when DeckLink late/dropped-frame counters increase
- warning when schedule failures increase
- error when app/DeckLink output buffering is starved
## Startup Sequence
Target first-version startup:
```text
main
-> load AppConfig through AppConfigProvider
-> initialize COM
-> create SystemFrameExchange
-> start RenderThread
-> wait for completed frame warmup
-> optionally discover/select/configure DeckLink output
-> if DeckLink is available:
-> start DeckLinkOutputThread
-> wait for scheduled depth warmup
-> DeckLinkOutput start scheduled playback
-> if DeckLink is unavailable:
-> continue without video output
-> start TelemetryHealthMonitor
-> start HttpControlServer
-> wait for Enter
```
Shutdown:
```text
stop HttpControlServer
stop TelemetryHealthMonitor
stop DeckLinkOutputThread
DeckLinkOutput stop playback
stop RenderThread
DeckLinkOutput release resources
release COM
```
## First Milestone: Modular Probe Equivalent
This is the only goal for the initial implementation.
Feature set:
- console app
- output-only DeckLink
- no input
- hidden GL context
- simple motion renderer
- BGRA8 only
- PBO async readback
- bounded FIFO system-memory frame exchange
- warmup before playback
- one-line telemetry
Acceptance:
- visible DeckLink output is smooth
- `renderFps` near selected cadence
- `scheduleFps` near selected cadence
- scheduled count/decklink buffered count stable around 4
- no continuous late/drop count
- no continuous PBO misses
- behavior matches or exceeds `DeckLinkRenderCadenceProbe`
## Second Milestone: Testable Core
Before porting compositor features, add tests for non-GL/non-DeckLink pieces.
Test targets:
- `SystemFrameExchangeTests`
- `RenderCadenceClockTests`
- `CadenceTelemetryTests`
Important cases:
- slot lifecycle transitions
- scheduled slots are protected
- completed unscheduled frames can be dropped
- stale handles/generations are rejected
- cadence does not speed up to refill buffers
- cadence records overrun/skipped ticks
## Third Milestone: Replace Simple Renderer With Render Interface
Add an interface around frame rendering:
```text
IRenderScene
-> InitializeGl()
-> RenderFrame(frameIndex, time)
-> ShutdownGl()
```
The first implementation remains `SimpleMotionRenderer`.
This creates the insertion point for shader-package rendering later without changing timing/scheduling.
## Fourth Milestone: Begin Porting Current App Features
Port only after the modular probe equivalent is stable.
Suggested order:
1. shader package compile/load
2. render pass/layer stack drawing
3. runtime snapshot input to renderer
4. live state overlays
5. control services
6. persistence/runtime store
7. preview from system-memory frames
8. screenshot from system-memory frames
9. input capture via CPU latest-frame mailbox
Each port must preserve the rule that the render thread cadence is primary.
## What Not To Port Early
Do not port these until the output spine is proven:
- DeckLink input
- preview GL presentation
- screenshot GL readback
- HTTP/OSC control services
- shader hot reload
- persistence
- runtime state JSON/open API
- complex telemetry/event dispatch
These are useful, but they are exactly the kinds of features that can accidentally reintroduce timing coupling.
## Build Plan
Initial CMake can follow the probe pattern:
```cmake
set(RENDER_CADENCE_APP_DIR "${CMAKE_CURRENT_SOURCE_DIR}/apps/RenderCadenceCompositor")
add_executable(RenderCadenceCompositor
# selected shared DeckLink/video/gl support files
# new modular app files
)
```
Later, shared source should be split into libraries:
```text
video_shader_decklink
video_shader_videoio
video_shader_gl_support
render_cadence_core
```
Avoid doing that library split before the first modular app works.
## VS Code Launch
Add a separate launch profile:
```text
Debug RenderCadenceCompositor
```
Run it as a console app so telemetry remains visible.
## Documentation
Add:
```text
apps/RenderCadenceCompositor/README.md
```
The README should record:
- intended architecture
- build/run instructions
- expected telemetry
- test result notes
- differences from the old app
- differences from the probe
## Success Criteria Before Porting More Features
Do not start feature porting until the new app can run with:
- stable smooth DeckLink output
- stable target scheduled depth
- stable actual DeckLink buffered count
- no regular visible freezes
- no steady PBO misses
- no steadily increasing late/dropped completions
- focus/minimize changes do not affect output cadence
- clean shutdown without hangs
This gives us a clean foundation. Once this is true, every feature added later has to prove it does not damage the spine.

View File

@@ -1,6 +1,6 @@
# OSC Control
Video Shader Toys can listen for local OSC messages and map them onto shader layer parameters.
This is the intended OSC control contract, but OSC ingress is not wired in the current `RenderCadenceCompositor` native host yet. The config fields and UI copy buttons are present for compatibility; use the REST layer parameter endpoints or the control UI for live parameter changes today.
## Configuration
@@ -8,11 +8,13 @@ Set the UDP port in `config/runtime-host.json`:
```json
{
"oscPort": 9000
"oscBindAddress": "127.0.0.1",
"oscPort": 9000,
"oscSmoothing": 0.18
}
```
Set `oscPort` to `0` to disable the OSC listener.
When OSC ingress is implemented, `oscPort: 0` should disable the OSC listener, `oscBindAddress: "127.0.0.1"` should keep OSC local to the host, and `oscBindAddress: "0.0.0.0"` should listen on all IPv4 interfaces. `oscSmoothing` is reserved for a per-frame easing amount on numeric OSC controls.
## Address Pattern
@@ -47,6 +49,8 @@ Matching is exact first. If that fails, names are compared in a simplified form
If multiple layers use the same shader package ID or display name, the first matching layer in the stack is controlled. Use the internal layer ID shown in the UI when you need to target one duplicate layer precisely.
In the control UI, each parameter row has a small **OSC** button. Clicking it copies that parameter's exact OSC address to the clipboard, which is the safest way to target controls with long names or duplicate shader layers.
## Values
The listener accepts these OSC argument types:
@@ -59,17 +63,27 @@ The listener accepts these OSC argument types:
Single-argument messages become scalar JSON values. Multi-argument messages become JSON arrays, which lets OSC drive `vec2` and `color` parameters.
OSC updates are coalesced by target route and applied once per render tick, so rapid controller motion does not force one runtime mutation, disk write, and UI push per incoming UDP packet. Numeric OSC controls can also be slightly smoothed with `oscSmoothing`.
Examples:
```text
/VideoShaderToys/fisheye-reproject/panDegrees 45.0
/VideoShaderToys/fisheye-reproject/fisheyeModel "equisolid"
/VideoShaderToys/video-transform/pan 0.25 -0.5
/VideoShaderToys/composition-guides/lineColor 1.0 0.8 0.1 1.0
/VideoShaderToys/safe-area-guides/lineColor 1.0 0.8 0.1 1.0
```
Values are validated with the same shader parameter rules used by the REST API. Invalid values or unknown addresses are ignored and reported to the native debug output.
OSC-driven parameter changes are not autosaved to `runtime/runtime_state.json`. Stack edits made through the UI and preset operations still persist as before. Smoothing only applies to numeric controls such as floats, `vec2`, and `color`; booleans, enums, text, and triggers stay immediate.
For `trigger` parameters, the OSC value is treated as a pulse. A simple integer or boolean message is enough:
```text
/VideoShaderToys/trigger-flash/flash 1
```
## Open Stage Control
For simple scalar controls, set the widget address and target directly:
@@ -106,10 +120,21 @@ send('127.0.0.1:9000', '/VideoShaderToys/fisheye-reproject/tiltDegrees', {type:
## Network
The listener binds to localhost only:
By default the listener binds to localhost only:
```text
127.0.0.1:<oscPort>
```
This keeps the control surface local to the machine running Video Shader Toys.
To accept OSC from other machines on the network, set:
```json
{
"oscBindAddress": "0.0.0.0",
"oscPort": 9000
}
```
That listens on all IPv4 interfaces, so make sure your firewall and network are configured appropriately.

View File

@@ -0,0 +1,160 @@
# Render Cadence Golden Rules
These are the non-negotiable rules for the new render-cadence architecture.
They exist because the old app drifted into a place where DeckLink timing, render work, shader build work, state coordination, readback, and recovery behavior all influenced each other. The new app should stay boring, explicit, and easy to reason about.
## 1. The Render Thread Owns Its GL Context
Only the render thread may bind and use its primary OpenGL context.
Allowed on the render thread:
- GL resource creation and destruction for resources it owns
- GL shader/program swap from an already-prepared GL program
- drawing the next frame
- async readback queueing and completion polling
- publishing completed system-memory frames
Not allowed on the render thread:
- Slang compiler invocation
- manifest scanning/parsing
- filesystem discovery
- image/font/LUT decoding
- persistence
- network/API/OSC handling
- DeckLink scheduling
- blocking console logging
- config file discovery or parsing
If GL preparation happens off-thread, use an explicit shared-context GL prepare thread. Do not smuggle non-render work back into the cadence loop.
## 2. Render Cadence Does Not Chase Buffers
The render thread runs at the selected render cadence.
It must not speed up to fill a DeckLink/system-memory buffer, and it must not slow down because a consumer is late. If the GPU is genuinely overloaded, record that as render overrun telemetry.
Buffers absorb timing differences. They do not control render cadence.
## 3. Video I/O Never Renders
DeckLink output consumes already-rendered system-memory frames.
The output/scheduling side may:
- schedule completed frames
- release frames after DeckLink completion
- report late/dropped/schedule telemetry
- record app-side poll misses
- conservatively realign the DeckLink schedule cursor after measured timing pressure
It must not:
- render fallback frames
- invoke GL
- compile shaders
- block the render cadence waiting for DeckLink
- continuously rewrite healthy scheduled timestamps
If no completed frame is available, record the miss and keep the ownership boundary intact.
DeckLink input is also an edge, not a renderer. It may capture/copy the latest supported CPU frame into an input mailbox and update input telemetry, but it must not call GL, schedule output, compile shaders, or drive render cadence. Format decode that requires GL belongs to the render-owned input upload path.
## 4. Runtime Build Work Produces Artifacts
Runtime shader work is split into two phases:
1. CPU/build phase outside the render thread
2. shared-context GL preparation outside the render thread where practical
3. GL program swap on the render thread
The CPU/build phase may parse manifests, invoke Slang, validate package shape, and prepare CPU-side data.
The render thread receives completed render-layer artifacts, asks the shared-context prepare worker to compile/link changed GL programs, and only swaps in prepared programs at a frame boundary. A failed artifact or failed GL preparation must not disturb the current renderer.
The display/render layer model is app-owned. It may track requested shaders, build state, display metadata, and render-ready artifacts, but it must not perform GL work or drive render cadence directly.
## 5. No Hidden Blocking In The Cadence Path
The render loop must not do work with unbounded or OS-dependent latency.
Examples to avoid:
- file reads
- directory scans
- image decoding
- process launches
- waits on worker threads
- blocking locks around slow code
- synchronous GPU readback waits
- console I/O
Short mutex use for exchanging small already-prepared objects is acceptable. Holding a lock while doing heavy work is not.
## 6. System Memory Frames Are A Handoff, Not A Render Driver
The system-memory frame exchange stores completed frames as a bounded FIFO reserve and protects frames scheduled to DeckLink.
Render acquire must not evict completed frames that are waiting for playout, and it must never force the render thread to wait for the output side to consume a frame.
If the completed reserve overflows, the exchange may drop the oldest completed, unscheduled frame and record `completedDrops`. That is an app-side reserve drop, not a DeckLink dropped frame.
## 7. Startup Uses Warmup, Not Burst Rendering
DeckLink playback starts only after the render thread has produced enough real frames for preroll.
Warmup should happen at normal render cadence. Do not temporarily accelerate the renderer to fill buffers.
## 8. Telemetry Must Name Ownership Clearly
Counters should say which subsystem had the event.
Good examples:
- `renderFps`
- `scheduleFps`
- `completedPollMisses`
- `scheduleFailures`
- `decklinkBuffered`
- `deckLinkScheduleLeadFrames`
- `deckLinkScheduleRealignments`
- `inputCaptureFps`
- `inputSubmitMs`
- `inputUploadMs`
- `inputConvertMs`
- `shaderCommitted`
- `shaderFailures`
Avoid ambiguous names like `underrun` unless it is clear whether it means app-ready underrun, DeckLink buffered-frame underrun, render overrun, or schedule failure.
## 9. Keep Files Small And Role-Based
A file should have one clear reason to change.
Preferred boundaries:
- app orchestration
- render cadence/thread ownership
- GL rendering
- runtime artifact build/bridge
- app-owned display/render layer model
- parameter packing
- system-memory frame exchange
- DeckLink output scheduling
- telemetry
- local control/API edge
- config loading
- JSON presentation/serialization
- logging
If a file starts coordinating multiple subsystems and doing detailed work for each of them, split it before it becomes the new old app.
## 10. Prefer Explicit Unsupported States
If a feature needs storage, timing behavior, or ownership we have not designed yet, reject it clearly.
For example, in the current new app it is better to reject texture/LUT/text/temporal/feedback shaders than to quietly load files or allocate history state on the render thread.
Unsupported is healthy when it protects the architecture.

View File

@@ -0,0 +1,448 @@
# Render Thread Ownership Plan
This plan describes how to make the main compositor behave like the successful `DeckLinkRenderCadenceProbe`: one render cadence owner, one GL context owner, no unrelated work able to interrupt output frame production.
The goal is not just "all GL calls happen on one thread". The current app mostly does that during runtime already. The real goal is:
- the output render thread owns its GL context for its whole lifetime
- output cadence is driven by the render thread, not by DeckLink completion timing
- non-output GL work cannot sit ahead of output frames
- callers cannot block the render thread while waiting for synchronous answers
- DeckLink scheduling consumes completed system-memory frames and never causes rendering
## Current Risk Points
The current main app still has several ways to interrupt output cadence.
### Shared GL Executor
`RenderEngine` owns the GL context during runtime, but it acts as a general task executor.
The same queue/path can run:
- output frame render
- input upload
- preview present
- screenshot capture
- render resets
- shader/program commits
- resource resize
- state clearing
That means output frames are not guaranteed to be the next GL work item at the selected frame time.
### Synchronous Output Render Request
`VideoBackend` drives output production from its output producer thread, then calls:
```text
VideoBackend
-> OpenGLVideoIOBridge::RenderScheduledFrame
-> RenderEngine::RequestOutputFrame
-> TryInvokeOnRenderThread
```
That makes output production a request/response interaction. The producer waits for the render thread, and the render thread is still shared with other work.
### Input Upload Shares Output Context
DeckLink input capture currently flows into:
```text
VideoBackend::HandleInputFrame
-> OpenGLVideoIOBridge::UploadInputFrame
-> RenderEngine::QueueInputFrame
-> render thread upload
```
Even with coalescing, input upload can consume render-thread time and GPU bandwidth directly before output rendering.
### Preview And Screenshot Share Output Context
Preview and screenshot are lower-priority features, but today they still execute on the render thread.
Preview is best-effort at the caller side, but once queued it can still occupy the same context. Screenshot capture can be more expensive because it performs readback and CPU-side image preparation.
### Startup Context Ownership Is Transitional
The Win32 startup path creates and binds the GL context before `RenderEngine::StartRenderThread()`.
That is acceptable as a transitional state, but the final model should make context ownership explicit:
- bootstrap thread creates the window/context
- bootstrap thread releases it
- render thread binds it
- only render thread initializes GL resources
- only render thread destroys GL resources
### Render Callback Re-enters App State
`OpenGLRenderPipeline::RenderFrame()` calls a callback into `OpenGLComposite::renderEffect()`.
That callback builds `RenderFrameInput`, resolves frame state, drains runtime live state, and then calls back into `RenderEngine` to draw the prepared frame.
This works, but it means the output render path still reaches up into app/runtime code at frame time.
## Target Runtime Shape
The main app should match this ownership model:
```text
runtime/control threads
-> publish snapshots, live overlays, reset requests, shader-build results
-> never call GL
render cadence thread
-> sole owner of output GL context
-> wakes at selected render cadence
-> samples latest render input/state
-> renders one frame
-> queues async readback/copies completed readback into system-memory slot
-> publishes completed frame to bounded FIFO output reserve
video output thread
-> consumes completed system-memory frames
-> schedules DeckLink frames to target buffer depth
-> processes completion results
-> never calls GL
optional input upload path
-> writes latest input frame into CPU-side latest-frame buffer
-> render thread imports/uploads at a controlled point in its frame
preview/screenshot path
-> consumes already-rendered output/system-memory frame when possible
-> never interrupts output render cadence
```
## Non-Negotiable Rules
- The render thread never waits for DeckLink.
- DeckLink callbacks never render.
- Runtime/control threads never directly execute GL.
- Preview and screenshot never execute ahead of output frames.
- Input upload is never a separate urgent GL task ahead of output render.
- Shader/resource commits are applied only at a frame boundary.
- Telemetry on the hot path must be lock-light or try-lock only.
- The render thread cadence does not speed up to refill buffers.
- If output work overruns, the render thread records the overrun and resumes the selected cadence policy.
## Implementation Plan
### 1. Add Thread/Context Ownership Guards
Add explicit render-thread ownership checks around all GL entry points.
Deliverables:
- `RenderEngine` exposes `IsOnRenderThread()` for assertions/tests.
- GL-facing classes get debug-only owner checks where practical.
- wrong-thread GL access becomes a counted telemetry warning, not just `OutputDebugStringA`.
- tests cover that public request methods do not execute GL directly.
Acceptance:
- every `RenderEngine` public method is classified as either request-only, lifecycle-only, or render-thread-only.
- render-thread-only methods are private or guarded.
- no normal runtime caller can accidentally invoke GL work inline.
### 2. Move GL Initialization Fully Onto The Render Thread
Start the render thread before compiling shaders and initializing GL resources.
Current startup does:
```text
InitOpenGLState()
-> CompileDecodeShader
-> CompileOutputPackShader
-> InitializeResources
-> CompileLayerPrograms
StartRenderThread()
```
Move toward:
```text
create context on Win32 thread
release context on Win32 thread
StartRenderThread()
render thread binds context
render thread initializes extensions, shaders, resources
```
Deliverables:
- a single `RenderEngine::StartAndInitialize(RenderInitializationConfig)` path.
- GL extension resolution happens on the render thread.
- shader/resource initialization is a render-thread startup phase.
- `RenderEngine` destructor only destroys resources on the render thread.
Acceptance:
- after `StartRenderThread()`, no non-render thread binds or uses the app GL context.
- shutdown order is deterministic: stop video output, stop render cadence, destroy GL resources, release context.
### 3. Replace Synchronous Output Render Requests With Render-Owned Cadence
Move output cadence out of `VideoBackend` and into the render system.
Current:
```text
VideoBackend output producer
-> cadence tick
-> acquire output slot
-> synchronous render-thread request
```
Target:
```text
RenderEngine output cadence loop
-> cadence tick
-> acquire/free output slot through a non-blocking frame-sink interface
-> render frame
-> publish completed frame
```
Deliverables:
- introduce `RenderedFrameSink` or similar interface owned by video output.
- render thread pulls/claims a free system-memory slot without waiting.
- if no free slot exists, render thread drops/recycles the oldest unscheduled completed frame or records backpressure without blocking.
- remove `RenderEngine::RequestOutputFrame()` from the steady-state output path.
Acceptance:
- output rendering continues even if DeckLink completion is delayed.
- no `std::future` wait exists in the output cadence path.
- `VideoBackend` no longer owns the producer render loop; it owns scheduling/completion only.
### 4. Make The Render Thread A Frame Loop, Not A Task Queue
Keep a command mailbox, but process it only at safe frame-boundary points.
Frame loop:
```text
while running:
wait until next render timestamp
apply bounded frame-boundary commands
sample latest frame input/state
upload latest input frame if enabled and budget allows
render output frame
queue/consume readback
publish completed frame
record timings
```
Command classes:
- frame-boundary commands: reset temporal history, reset shader feedback, commit prepared shader programs
- background/low-priority commands: preview, screenshot, diagnostic readback
- non-GL commands: state publication, telemetry, persistence
Deliverables:
- replace FIFO render task queue with a priority/mailbox model.
- output cadence is the loop's main clock.
- commands have budget classes and max work per frame.
- long commands are deferred rather than blocking the current output tick.
Acceptance:
- preview/screenshot cannot run immediately before a due output frame.
- reset/shader work is applied between frames and measured.
- output render starts within a small jitter window when the GPU is not overrun.
### 5. Move Input Capture To A CPU Latest-Frame Buffer
Input capture should not enqueue independent GL upload tasks.
Target:
```text
DeckLink input callback
-> copy/coalesce latest CPU input frame
-> return quickly
render thread frame boundary
-> if input version changed, upload latest frame
-> render using last successfully uploaded input texture
```
Deliverables:
- introduce `InputFrameMailbox` with latest-frame semantics.
- remove `RenderEngine::QueueInputFrame()` from the callback path.
- render thread owns the upload moment.
- if upload would exceed budget, render thread can reuse the previous input texture and record an input-upload skip.
Acceptance:
- input capture enabled does not create arbitrary render-thread tasks.
- output cadence remains stable when input frames arrive.
- telemetry separates input-frame arrival, upload count, upload skips, and upload cost.
### 6. Move Preview To A Consumer Path
Preview should consume the latest completed output image instead of asking the output GL context to present.
Options:
- CPU preview from latest system-memory output frame.
- a separate preview GL context fed asynchronously from completed frames.
- a low-priority render-thread blit only when output has measurable slack.
Recommended first step:
- use latest system-memory BGRA8 output for the window preview.
Deliverables:
- preview reads from latest completed/scheduled output frame copy.
- `TryPresentPreview()` no longer queues GL work on the output render thread.
- preview FPS throttling remains caller-side.
Acceptance:
- forcing preview cannot delay output rendering.
- minimizing/focusing the window does not affect output cadence.
### 7. Move Screenshot To Completed Frame Capture
Screenshot should capture from the latest completed output frame unless an explicit "exact render capture" mode is requested.
Deliverables:
- screenshot request reads the latest system-memory output frame.
- PNG write remains async.
- optional diagnostic exact-GL screenshot is disabled during live output or explicitly marked disruptive.
Acceptance:
- screenshot request does not call `glReadPixels` on the output render context during steady-state playout.
### 8. Make Shader Commits Frame-Boundary Work
Prepared shader builds are CPU/background work; GL program commit is still GL work.
Deliverables:
- shader build queue produces `PreparedShaderBuild`.
- render thread sees latest pending prepared build at a frame boundary.
- commit is applied only between frames.
- expensive commits can temporarily enter a measured "render reconfigure" state.
Acceptance:
- shader commits do not interleave midway through output render.
- output timing telemetry records commit duration separately from normal render duration.
### 9. Split Output Scheduling From Rendering Completely
`VideoBackend` should become a playout/scheduling owner, not a render producer.
Target:
```text
RenderEngine
-> produces completed frames at render cadence
VideoBackend
-> schedules completed frames up to target DeckLink depth
-> processes completions
-> releases scheduled slots
```
Deliverables:
- `VideoBackend` owns `SystemOutputFramePool`, or a new `SystemFrameExchange` owns it between render/video.
- render thread publishes completed frames into the exchange.
- video output thread schedules from the exchange.
- no render calls exist in completion handling or scheduling paths.
Acceptance:
- DeckLink buffer depth changes cannot directly cause render-thread wakeups except through non-blocking availability signals.
- render cadence can be tested without DeckLink by using a fake frame sink.
- video scheduling can be tested without GL by using synthetic frames.
### 10. Preserve The Probe As The Reference Contract
The `DeckLinkRenderCadenceProbe` is now the control sample.
Deliverables:
- document which main-app components correspond to the probe components.
- add a small regression checklist:
- render FPS near target
- schedule FPS near target
- DeckLink buffered frames stable
- no late/drop frames
- no PBO misses or readback stalls
- focus/minimize does not change output cadence
Acceptance:
- after each migration step, compare the main app telemetry against the probe's known-good behavior.
## Suggested Order Of Work
1. Add ownership guards and classify render methods.
2. Move GL initialization/destruction fully onto the render thread.
3. Introduce a render-owned cadence loop behind a feature flag.
4. Add a frame-sink/exchange interface between render and video.
5. Move output production from `VideoBackend` to the render cadence loop.
6. Convert input upload to latest-frame mailbox semantics.
7. Move preview to completed-frame consumption.
8. Move screenshot to completed-frame capture.
9. Convert shader commits/resets to frame-boundary mailbox commands.
10. Remove old synchronous output render request path.
## Feature Flags During Migration
Use flags only to keep testing safe, not as long-term compatibility layers.
Suggested flags:
```text
VST_RENDER_CADENCE_OWNER=render_thread
VST_DISABLE_INPUT_CAPTURE=1
VST_PREVIEW_SOURCE=system_frame
VST_SCREENSHOT_SOURCE=system_frame
```
Remove each flag once the new behavior is proven and becomes the only supported path.
## Telemetry Needed
Add or preserve counters for:
- render tick jitter
- render tick overrun
- output render duration
- GL command mailbox depth by class
- frame-boundary command duration
- input upload duration and skips
- readback queue/consume duration
- completed system-memory frame depth
- scheduled DeckLink frame depth
- DeckLink actual buffered frames
- preview frames consumed
- screenshot requests served from system memory
The key metric is whether output render starts on time. Buffer depth alone is not enough; a full buffer can still contain stale or repeated frames.
## Completion Definition
This work is complete when:
- the output render thread owns the app GL context from initialization through shutdown
- output rendering is driven by the render thread's selected frame cadence
- no non-output task can run ahead of a due output frame
- `VideoBackend` never asks the render thread to render synchronously
- DeckLink scheduling consumes already completed system-memory frames
- input upload, preview, screenshot, shader commits, and resets are all frame-boundary, mailbox, or consumer-side operations
- main-app telemetry approaches the cadence probe behavior under the same output mode

View File

@@ -6,24 +6,167 @@ info:
REST API exposed by the local Video Shader Toys control server.
The API is intended for local control tools and the bundled React UI. All mutating
endpoints return a small action result object. Successful mutating requests also
broadcast the latest runtime state over the `/ws` WebSocket.
endpoints return a small action result object.
WebSocket state streaming is not described by OpenAPI; connect to `ws://127.0.0.1:{port}/ws`
to receive full runtime state JSON messages whenever state changes.
RenderCadenceCompositor serves `/api/state` for snapshots and `/ws` for local
WebSocket state updates consumed by the bundled control UI.
servers:
- url: http://127.0.0.1:8080
description: Default local control server
tags:
- name: State
description: Runtime state and status.
- name: Static
description: Bundled control UI and static assets served by the local host.
- name: Docs
description: OpenAPI and Swagger UI documentation served by the local host.
- name: Layers
description: Layer stack control.
- name: Stack Presets
description: Save and recall layer stack presets.
description: Planned save/recall layer stack presets. The current native host exposes the routes but returns an unimplemented action result.
- name: Runtime
description: Runtime actions.
paths:
/:
get:
tags: [Static]
summary: Serve the bundled control UI
description: Returns the built React control UI `index.html` from `ui/dist`.
operationId: getControlUiRoot
responses:
"200":
description: Control UI HTML.
content:
text/html:
schema:
type: string
"404":
description: UI bundle was not found.
content:
text/plain:
schema:
type: string
/index.html:
get:
tags: [Static]
summary: Serve the bundled control UI index file
description: Returns the built React control UI `index.html` from `ui/dist`.
operationId: getControlUiIndex
responses:
"200":
description: Control UI HTML.
content:
text/html:
schema:
type: string
"404":
description: UI bundle was not found.
content:
text/plain:
schema:
type: string
/assets/{assetPath}:
get:
tags: [Static]
summary: Serve a bundled control UI asset
description: Serves files from `ui/dist/assets`. The server rejects unsafe relative paths and guesses the content type from the file extension.
operationId: getControlUiAsset
parameters:
- name: assetPath
in: path
required: true
description: Relative asset path below `ui/dist/assets`.
schema:
type: string
responses:
"200":
description: Static asset.
content:
text/javascript:
schema:
type: string
text/css:
schema:
type: string
image/svg+xml:
schema:
type: string
image/png:
schema:
type: string
format: binary
text/plain:
schema:
type: string
"404":
description: Asset was not found or the path was unsafe.
content:
text/plain:
schema:
type: string
/docs:
get:
tags: [Docs]
summary: Serve Swagger UI
description: Returns a small Swagger UI page pointed at `/docs/openapi.yaml`.
operationId: getSwaggerUi
responses:
"200":
description: Swagger UI HTML.
content:
text/html:
schema:
type: string
/docs/:
get:
tags: [Docs]
summary: Serve Swagger UI
description: Alias for `/docs`.
operationId: getSwaggerUiWithTrailingSlash
responses:
"200":
description: Swagger UI HTML.
content:
text/html:
schema:
type: string
/docs/openapi.yaml:
get:
tags: [Docs]
summary: Serve the OpenAPI document
operationId: getOpenApiDocumentFromDocs
responses:
"200":
description: OpenAPI YAML document.
content:
application/yaml:
schema:
type: string
"404":
description: OpenAPI document was not found.
content:
text/plain:
schema:
type: string
/openapi.yaml:
get:
tags: [Docs]
summary: Serve the OpenAPI document
description: Alias for `/docs/openapi.yaml`.
operationId: getOpenApiDocument
responses:
"200":
description: OpenAPI YAML document.
content:
application/yaml:
schema:
type: string
"404":
description: OpenAPI document was not found.
content:
text/plain:
schema:
type: string
/api/state:
get:
tags: [State]
@@ -36,6 +179,24 @@ paths:
application/json:
schema:
$ref: "#/components/schemas/RuntimeState"
/ws:
get:
tags: [State]
summary: Stream runtime state over WebSocket
description: |
Upgrades to a WebSocket connection. The server sends JSON runtime-state
snapshots using the same shape as `GET /api/state` whenever the serialized
state changes.
operationId: streamRuntimeState
responses:
"101":
description: WebSocket protocol upgrade accepted.
"400":
description: The request was not a valid WebSocket upgrade.
content:
text/plain:
schema:
type: string
/api/layers/add:
post:
tags: [Layers]
@@ -72,7 +233,7 @@ paths:
post:
tags: [Layers]
summary: Move a layer by direction
description: Moves a layer up or down by one or more positions, depending on the signed direction value.
description: Planned relative move route. The current native command path supports `/api/layers/reorder`; this route currently returns an unimplemented action result.
operationId: moveLayer
requestBody:
required: true
@@ -168,7 +329,8 @@ paths:
/api/stack-presets/save:
post:
tags: [Stack Presets]
summary: Save the current layer stack as a preset
summary: Planned preset save route
description: Planned preset route. The current native command path currently returns an unimplemented action result; latest-working-state autosave uses `runtime/runtime_state.json`.
operationId: saveStackPreset
requestBody:
required: true
@@ -184,7 +346,8 @@ paths:
/api/stack-presets/load:
post:
tags: [Stack Presets]
summary: Load a layer stack preset
summary: Planned preset load route
description: Planned preset route. The current native command path currently returns an unimplemented action result; latest-working-state startup restore uses `runtime/runtime_state.json`.
operationId: loadStackPreset
requestBody:
required: true
@@ -201,6 +364,7 @@ paths:
post:
tags: [Runtime]
summary: Reload shaders
description: Rescans the shader library, re-reads manifests, refreshes shader availability/errors, reconciles active layer parameter definitions, and queues recompilation for every catalog-valid layer in the active stack. It does not compile every shader package in the library; packages are compiled when they are active layers. If a rebuild fails, the previous working render stack remains active where possible.
operationId: reloadShaders
requestBody:
required: false
@@ -214,6 +378,24 @@ paths:
$ref: "#/components/responses/ActionOk"
"400":
$ref: "#/components/responses/ActionError"
/api/screenshot:
post:
tags: [Runtime]
summary: Planned screenshot route
description: Planned screenshot route. The current native command path currently returns an unimplemented action result.
operationId: queueScreenshot
requestBody:
required: false
content:
application/json:
schema:
type: object
additionalProperties: false
responses:
"200":
$ref: "#/components/responses/ActionOk"
"400":
$ref: "#/components/responses/ActionError"
components:
responses:
ActionOk:
@@ -340,8 +522,14 @@ components:
$ref: "#/components/schemas/VideoStatus"
decklink:
$ref: "#/components/schemas/DeckLinkStatus"
videoIO:
$ref: "#/components/schemas/VideoIOStatus"
performance:
$ref: "#/components/schemas/PerformanceStatus"
backendPlayout:
$ref: "#/components/schemas/BackendPlayoutStatus"
runtimeEvents:
$ref: "#/components/schemas/RuntimeEventStatus"
shaders:
type: array
items:
@@ -361,10 +549,16 @@ components:
type: number
oscPort:
type: number
oscBindAddress:
type: string
oscSmoothing:
type: number
autoReload:
type: boolean
maxTemporalHistoryFrames:
type: number
previewFps:
type: number
enableExternalKeying:
type: boolean
inputVideoFormat:
@@ -397,6 +591,8 @@ components:
type: string
DeckLinkStatus:
type: object
deprecated: true
description: Legacy DeckLink-specific status object. Prefer `videoIO` for new clients.
properties:
modelName:
type: string
@@ -412,6 +608,26 @@ components:
type: boolean
statusMessage:
type: string
VideoIOStatus:
type: object
properties:
backend:
type: string
example: decklink
modelName:
type: string
supportsInternalKeying:
type: boolean
supportsExternalKeying:
type: boolean
keyerInterfaceAvailable:
type: boolean
externalKeyingRequested:
type: boolean
externalKeyingActive:
type: boolean
statusMessage:
type: string
PerformanceStatus:
type: object
properties:
@@ -419,10 +635,280 @@ components:
type: number
renderMs:
type: number
description: Alias of cadence.renderFrameMs for clients that read the top-level performance object.
smoothedRenderMs:
type: number
budgetUsedPercent:
type: number
description: Alias of cadence.renderFrameBudgetUsedPercent for clients that read the top-level performance object.
completionIntervalMs:
type: number
smoothedCompletionIntervalMs:
type: number
maxCompletionIntervalMs:
type: number
lateFrameCount:
type: number
droppedFrameCount:
type: number
flushedFrameCount:
type: number
cadence:
$ref: "#/components/schemas/CadenceTelemetry"
CadenceTelemetry:
type: object
properties:
clockOverruns:
type: number
description: Render cadence overruns where the render thread was late enough to skip one or more frame intervals.
clockSkippedFrames:
type: number
description: Total render cadence frame intervals skipped instead of catch-up rendering.
clockOveruns:
type: number
deprecated: true
description: Deprecated misspelled alias for clockOverruns.
clockSkipped:
type: number
deprecated: true
description: Deprecated alias for clockSkippedFrames.
renderFrameMs:
type: number
description: Most recent render-thread frame draw duration in milliseconds, excluding completed-readback copy and readback queue work.
renderFrameBudgetUsedPercent:
type: number
description: Most recent render-thread frame draw duration as a percentage of the selected frame budget.
renderFrameMaxMs:
type: number
description: Maximum observed render-thread frame draw duration in milliseconds for this process.
readbackQueueMs:
type: number
description: Most recent duration spent queueing BGRA8 async PBO readback after rendering.
completedReadbackCopyMs:
type: number
description: Most recent duration spent mapping and copying a completed BGRA8 readback into system-memory frame storage.
completedDrops:
type: number
description: Number of completed unscheduled system-memory frames dropped so render could reuse the slot.
acquireMisses:
type: number
description: Number of times render/readback could not acquire a writable system-memory frame slot.
inputFramesReceived:
type: number
inputFramesDropped:
type: number
inputConsumeMisses:
type: number
description: Render ticks where no ready input frame was available to upload.
inputUploadMisses:
type: number
description: Input texture upload attempts that reused the previous GL input texture.
inputReadyFrames:
type: number
description: Ready input frames currently queued in the input mailbox.
inputReadingFrames:
type: number
description: Input frames currently protected while render uploads them.
inputLatestAgeMs:
type: number
inputUploadMs:
type: number
inputCaptureFps:
type: number
inputConvertMs:
type: number
inputSubmitMs:
type: number
inputCaptureFormat:
type: string
deckLinkScheduleLeadAvailable:
type: boolean
description: Whether DeckLink playback stream-time lead telemetry is currently available.
deckLinkScheduleLeadFrames:
type: number
nullable: true
description: Estimated number of frame intervals between the next app schedule timestamp and the DeckLink playback frame index.
deckLinkPlaybackFrameIndex:
type: number
description: DeckLink playback stream time converted to frame index at the configured output cadence.
deckLinkNextScheduleFrameIndex:
type: number
description: Next frame index the app scheduler will assign to a DeckLink output frame.
deckLinkPlaybackStreamTime:
type: number
description: Raw DeckLink scheduled playback stream time in the output mode time scale.
deckLinkScheduleRealignments:
type: number
description: Count of schedule-cursor recovery realignments triggered by DeckLink late/drop pressure.
BackendPlayoutStatus:
type: object
properties:
lifecycleState:
type: string
example: running
degraded:
type: boolean
statusMessage:
type: string
lateFrameCount:
type: number
droppedFrameCount:
type: number
flushedFrameCount:
type: number
readyQueue:
$ref: "#/components/schemas/BackendReadyQueueStatus"
outputRender:
$ref: "#/components/schemas/BackendOutputRenderStatus"
recovery:
$ref: "#/components/schemas/BackendPlayoutRecoveryStatus"
BackendReadyQueueStatus:
type: object
properties:
depth:
type: number
description: Current number of ready output frames.
capacity:
type: number
description: Maximum ready output frames currently allowed.
minDepth:
type: number
description: Minimum observed ready queue depth since backend worker start.
maxDepth:
type: number
description: Maximum observed ready queue depth since backend worker start.
zeroDepthCount:
type: number
description: Number of observed samples where the ready queue was empty.
pushedCount:
type: number
poppedCount:
type: number
droppedCount:
type: number
underrunCount:
type: number
BackendOutputRenderStatus:
type: object
properties:
renderMs:
type: number
description: Most recent output render duration in milliseconds.
smoothedRenderMs:
type: number
description: Smoothed output render duration in milliseconds.
maxRenderMs:
type: number
description: Maximum observed output render duration in milliseconds.
acquireFrameMs:
type: number
description: Time spent acquiring a writable backend output frame in milliseconds.
renderRequestMs:
type: number
description: Time spent executing the render-thread output frame request in milliseconds.
endAccessMs:
type: number
description: Time spent ending write access to the backend output frame in milliseconds.
queueWaitMs:
type: number
description: Time the output render request spent waiting for the render thread in milliseconds.
drawMs:
type: number
description: Time spent drawing, blitting, packing, and flushing the output frame in milliseconds.
fenceWaitMs:
type: number
description: Time spent waiting for the async readback fence in milliseconds.
mapMs:
type: number
description: Time spent mapping the async readback pixel buffer in milliseconds.
readbackCopyMs:
type: number
description: Time spent copying async readback bytes into the backend output frame in milliseconds.
cachedCopyMs:
type: number
description: Time spent copying the cached output frame when async readback is not ready in milliseconds.
asyncQueueMs:
type: number
description: Time spent queueing the next async readback in milliseconds.
asyncQueueBufferMs:
type: number
description: Time spent orphaning or allocating the async readback pixel buffer in milliseconds.
asyncQueueSetupMs:
type: number
description: Time spent applying readback pixel-store, framebuffer, and pixel-pack-buffer state in milliseconds.
asyncQueueReadPixelsMs:
type: number
description: Time spent issuing glReadPixels for the async readback in milliseconds.
asyncQueueFenceMs:
type: number
description: Time spent creating the async readback fence in milliseconds.
syncReadMs:
type: number
description: Time spent in bootstrap synchronous readback in milliseconds.
asyncReadbackMissCount:
type: number
description: Count of output render requests where async readback was not ready.
cachedFallbackCount:
type: number
description: Count of output render requests served from the cached output frame.
syncFallbackCount:
type: number
description: Count of output render requests that used bootstrap synchronous readback.
BackendPlayoutRecoveryStatus:
type: object
properties:
completionResult:
type: string
enum: [Completed, DisplayedLate, Dropped, Flushed, Unknown]
completedFrameIndex:
type: number
scheduledFrameIndex:
type: number
scheduledLeadFrames:
type: number
measuredLagFrames:
type: number
catchUpFrames:
type: number
lateStreak:
type: number
dropStreak:
type: number
RuntimeEventStatus:
type: object
properties:
queue:
$ref: "#/components/schemas/RuntimeEventQueueStatus"
dispatch:
$ref: "#/components/schemas/RuntimeEventDispatchStatus"
RuntimeEventQueueStatus:
type: object
properties:
name:
type: string
depth:
type: number
capacity:
type: number
droppedCount:
type: number
oldestEventAgeMs:
type: number
RuntimeEventDispatchStatus:
type: object
properties:
dispatchCallCount:
type: number
dispatchedEventCount:
type: number
handlerInvocationCount:
type: number
handlerFailureCount:
type: number
lastDispatchDurationMs:
type: number
maxDispatchDurationMs:
type: number
ShaderSummary:
type: object
properties:
@@ -434,8 +920,16 @@ components:
type: string
category:
type: string
available:
type: boolean
description: False when the shader package exists but failed manifest or compile validation.
error:
type: string
description: Error text for unavailable shader packages.
temporal:
$ref: "#/components/schemas/TemporalState"
feedback:
$ref: "#/components/schemas/FeedbackState"
TemporalState:
type: object
properties:
@@ -448,6 +942,13 @@ components:
type: number
effectiveHistoryLength:
type: number
FeedbackState:
type: object
properties:
enabled:
type: boolean
writePass:
type: string
LayerState:
type: object
properties:
@@ -472,9 +973,21 @@ components:
type: string
label:
type: string
description:
type: string
description: Short helper text shown under the parameter label in the control UI.
type:
type: string
enum: [float, vec2, color, bool, enum]
enum: [float, vec2, color, bool, enum, text, trigger]
defaultValue:
description: Default parameter value from the shader manifest.
oneOf:
- type: number
- type: boolean
- type: string
- type: array
items:
type: number
min:
type: array
items:
@@ -491,6 +1004,12 @@ components:
type: array
items:
$ref: "#/components/schemas/ParameterOption"
maxLength:
type: number
description: Maximum length for text parameters.
font:
type: string
description: Font asset id used by text parameters, when declared.
value:
description: Current parameter value.
oneOf:

View File

@@ -0,0 +1,589 @@
# ControlServices Subsystem Design
This document expands the `ControlServices` subsystem described in [PHASE_1_SUBSYSTEM_BOUNDARIES_DESIGN.md](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/docs/PHASE_1_SUBSYSTEM_BOUNDARIES_DESIGN.md). It defines the target role of `ControlServices` as the ingress boundary for non-render control sources and the normalization layer that turns external input into typed internal actions.
The intent here is to make `ControlServices` explicit enough that later phases can extract it from the current `RuntimeServices` / `ControlServer` / `OscServer` mix without inventing new boundaries ad hoc.
## Purpose
`ControlServices` is the subsystem that accepts external control traffic and turns it into safe, typed, low-cost input for the rest of the app.
In the target architecture, `ControlServices` should:
- own ingress for OSC, HTTP/REST-style control routes, WebSocket session management, and file-watch/reload signals
- normalize transport-specific payloads into typed internal actions or events
- apply ingress-local buffering, coalescing, deduplication, and rate limiting where useful
- expose service timing and health observations to `HealthTelemetry`
- forward normalized actions into `RuntimeCoordinator`
It should not:
- decide persistence policy
- mutate persisted state directly
- build render snapshots
- own render-local overlay state
- own device timing or playout policy
This subsystem is intentionally narrow in authority and broad in transport coverage.
## Why This Subsystem Exists
Today the app already has a recognizable control-services slice, but it is spread across several classes:
- `RuntimeServices` hosts control server startup, OSC queues, deferred OSC commits, and file-watch polling:
- [RuntimeServices.h](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/control/RuntimeServices.h:26)
- [RuntimeServices.cpp](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/control/RuntimeServices.cpp:24)
- `ControlServer` owns HTTP, WebSocket upgrade, static asset serving, and direct callback-based route dispatch:
- [ControlServer.h](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/control/ControlServer.h:15)
- [ControlServer.cpp](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/control/ControlServer.cpp:88)
- `OscServer` owns UDP socket receive, OSC decode, and parameter callback dispatch:
- [OscServer.h](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/control/OscServer.h:11)
- [OscServer.cpp](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/control/OscServer.cpp:58)
The current shape works, but it mixes:
- transport handling
- action normalization
- direct callback dispatch
- coarse background polling
- transient queue ownership
- UI broadcast behavior
- partial runtime mutation coordination
That overlap is exactly what Phase 1 is trying to remove.
## Design Goals
`ControlServices` should optimize for:
- low-latency ingress without forcing immediate whole-app work
- clear transport boundaries
- deterministic normalization of external input
- isolation of service-specific timing concerns
- easy replacement of polling flows with typed events
- no direct knowledge of render-local implementation details
- safe behavior under bursty traffic such as high-rate OSC
## Subsystem Responsibilities
`ControlServices` owns the following concerns.
### 1. Transport Ingress
It accepts input from external control-facing sources such as:
- OSC/UDP parameter control
- HTTP API requests from the native control UI or external clients
- WebSocket connection lifecycle for state consumers
- file-watch triggers and manual reload requests
- future automation ingress such as MIDI, serial, or remote control bridges
The key rule is that transport-specific details stop here.
### 2. Action Normalization
Every ingress path should be converted into a typed internal action or event before it touches runtime policy.
Examples:
- OSC `/layer/param` traffic becomes `AutomationTargetReceived`
- `POST /api/layers/add` becomes `LayerAddRequested`
- `POST /api/reload` becomes `ShaderReloadRequested`
- file-watch changes become `RegistryChangedDetected` or `ReloadRequested`
The rest of the app should not need to know whether an action came from UDP, HTTP, the embedded UI, or a background watcher.
### 3. Ingress-Local Buffering and Coalescing
`ControlServices` may maintain short-lived queues or coalesced maps when that is the correct place to absorb bursty input.
Examples:
- latest-value coalescing per OSC route
- pending reload edge detection
- bounded outbound state-broadcast requests
- short-lived delivery queues for already-classified follow-up work, as long as commit and persistence policy still belong to `RuntimeCoordinator`
This state is ingress-local and must not become a substitute for committed runtime state.
### 4. WebSocket Session Management
The subsystem owns connection lifecycle for clients that observe runtime state, but it does not own the authoritative runtime model.
It is responsible for:
- accepting WebSocket upgrades
- tracking connected clients
- forwarding serialized state snapshots or health payloads produced elsewhere
- applying broadcast throttling or collapse policies when necessary
It is not responsible for deciding what the authoritative state is.
### 5. File-Watch and Reload Ingress
The subsystem should own the detection side of registry/file changes and reload requests.
It may:
- observe filesystem changes
- debounce bursts of related file events
- translate those changes into typed reload actions
It should not directly trigger render rebuilds or mutate shader/package state itself.
### 6. Service Health and Timing Reporting
`ControlServices` should emit operational signals into `HealthTelemetry`, including:
- OSC packet rate
- OSC decode failures
- queue depth / coalesced route count
- dropped or collapsed ingress events
- HTTP error counts
- WebSocket connection count
- reload request frequency
- file-watch failures
- service-thread startup/shutdown errors
## Explicit Non-Responsibilities
The following must stay outside `ControlServices` in the target design.
### Persistence Decisions
The subsystem may report that an input requested a state change, but it should not decide whether that change is persisted.
That belongs to `RuntimeCoordinator`, with `RuntimeStore` and the later persistence writer carrying out durable writes when policy requests them.
### Render Snapshot Publication
`ControlServices` must not publish render-facing snapshots or poke render-local structures directly.
### Render-Local Overlay Ownership
Live OSC automation overlays belong to the live-state/render preparation boundary (`RuntimeLiveState` today). Temporal state, shader feedback, output staging, and other render-only transient state belong to `RenderEngine`.
`ControlServices` may ingest and coalesce automation targets, but it should not own how those targets are composed, committed, persisted, or applied inside the render domain.
### Hardware Timing or Playout Recovery
Device scheduling, queue headroom, and callback recovery belong to `VideoBackend`, not the control ingress path.
## Ingress Boundary Model
The clean boundary for `ControlServices` is:
- external transport in
- typed action/event out
That implies three layers inside the subsystem.
### Transport Adapters
These are protocol-facing components.
Examples:
- `OscIngress`
- `HttpControlIngress`
- `WebSocketSessionHost`
- `FileWatchIngress`
Responsibilities:
- socket/file watcher lifecycle
- protocol decoding
- request framing
- transport-level validation
- low-level authentication or origin checks later if added
### Normalization Layer
This layer translates decoded transport input into typed actions.
Responsibilities:
- route parsing
- payload type normalization
- parameter name/key resolution where that is purely syntactic
- conversion from transport-specific errors into typed ingress errors
This layer should not perform deep runtime mutation policy.
### Service Coordination Shell
This shell owns:
- startup/shutdown ordering for ingress services
- shared ingress-local queues
- service-thread lifecycle
- handing normalized actions to `RuntimeCoordinator`
- handing outbound snapshot payloads to WebSocket clients
This shell is the spiritual successor to the hosting part of current `RuntimeServices`, but with a much narrower responsibility set.
## Service Timing Concerns
`ControlServices` is the correct place to isolate transport-level timing concerns that should not leak into whole-app state policy.
### OSC Timing
Current behavior already points in the right direction:
- OSC receive is on its own thread in `OscServer`
- latest values are coalesced by route in `RuntimeServices`
- updates are applied once per render tick rather than per packet
Relevant code:
- [OscServer.cpp](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/control/OscServer.cpp:95)
- [RuntimeServices.cpp](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/control/RuntimeServices.cpp:65)
- [RuntimeServices.cpp](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/control/RuntimeServices.cpp:82)
Target rule:
- network receive and decode stay inside `ControlServices`
- coalescing policy stays inside `ControlServices`
- classification of the resulting action belongs to `RuntimeCoordinator`
- render-local application belongs to `RenderEngine`
This keeps high-rate ingress cheap without giving the service layer authority over render behavior or committed-state policy.
### HTTP / UI Timing
HTTP control requests are operator-facing and usually low-rate, but the UI can still generate bursts through slider drags or repeated parameter edits.
`ControlServices` should:
- normalize each request into a typed action
- allow collapse/throttle policies for purely observational outbound state pushes
- avoid synchronous full-state serialization on every ingress event where possible
It should not decide whether a request results in immediate, deferred, transient, or persisted mutation. That is a coordinator concern.
### WebSocket Broadcast Timing
Outbound state streaming is control-plane behavior, not core runtime ownership.
Current code already distinguishes immediate and requested broadcasts:
- [ControlServer.cpp](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/control/ControlServer.cpp:163)
- [ControlServer.cpp](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/control/ControlServer.cpp:170)
Target rule:
- `ControlServices` may own broadcast scheduling and collapse policy
- the source state payload should come from snapshot/telemetry producers, not from service-owned mutable state
### File-Watch Timing
Current file-watch and deferred OSC commit work run on a coarse poll loop:
- [RuntimeServices.cpp](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/control/RuntimeServices.cpp:194)
This is one of the cleaner migration opportunities in the whole app.
Target rule:
- file-watch detection belongs in `ControlServices`
- coarse polling should eventually be replaced with either event-driven watching or a narrower, typed background loop
- detected changes should be debounced and surfaced as typed reload-related actions
### Service Backpressure
`ControlServices` needs explicit backpressure rules for high-rate sources.
Recommended policies:
- coalesce latest-value automation by route
- bound per-service queues
- count and report dropped/coalesced events
- prefer collapsing observation work before collapsing operator mutations
- never let service queues become hidden durable state
## Interfaces
These are suggested target-facing interfaces, not final class signatures.
### Subsystem Shell
Possible top-level responsibilities:
- `Start(...)`
- `Stop()`
- `PublishStateSnapshot(...)`
- `PublishHealthSnapshot(...)`
- `DrainNormalizedActions(...)`
The shell should feel like a host for ingress adapters plus a normalization/buffering boundary.
### OSC Ingress
Possible responsibilities:
- `StartOscIngress(...)`
- `StopOscIngress()`
- `ConfigureOscBinding(...)`
- `EnqueueDecodedOscMessage(...)`
- `DrainCoalescedAutomationTargets(...)`
### HTTP / Web Control Ingress
Possible responsibilities:
- `StartHttpIngress(...)`
- `StopHttpIngress()`
- `HandleHttpRequest(...)`
- `HandleWebSocketUpgrade(...)`
- `QueueStateBroadcastRequest()`
### File-Watch Ingress
Possible responsibilities:
- `StartFileWatchIngress(...)`
- `StopFileWatchIngress()`
- `PollOrConsumeFileEvents(...)`
- `DrainReloadSignals(...)`
### Normalized Action Types
These should likely become shared event/action definitions in Phase 2, but `ControlServices` should be designed around them now.
Examples:
- `LayerAddRequested`
- `LayerRemovedRequested`
- `LayerReorderedRequested`
- `LayerBypassSetRequested`
- `LayerShaderSetRequested`
- `ParameterSetRequested`
- `LayerResetRequested`
- `StackPresetSaveRequested`
- `StackPresetLoadRequested`
- `ShaderReloadRequested`
- `ScreenshotRequested`
- `AutomationTargetReceived`
- `RegistryChangeDetected`
## Data Ownership Inside The Subsystem
`ControlServices` is allowed to own ingress-local ephemeral state.
Examples:
- connected WebSocket client list
- pending broadcast flag
- coalesced OSC route map
- outstanding decoded-but-undrained action queue
- file-watch debounce state
- transport error counters before publication to telemetry
It should not own:
- authoritative layer stack state
- committed parameter values
- render snapshots
- playout queue state
- shader feedback or render overlays
The rule is simple:
- if the state exists only to absorb or forward external input, it can live here
- if the state defines how the app should behave over time, it belongs elsewhere
## Outbound Boundaries
`ControlServices` talks outward in only a few approved directions.
### To `RuntimeCoordinator`
Primary outbound path.
It sends:
- normalized mutation requests
- automation targets
- reload requests
- stack preset requests
It does not send:
- transport-specific objects such as raw sockets or OSC packet structures
- render-facing state objects
### To `HealthTelemetry`
Observation-only relationship.
It sends:
- counters
- warnings
- timing samples
- service health transitions
It should not use `HealthTelemetry` as a hidden control path.
### From Snapshot / Telemetry Producers To Web Clients
`ControlServices` may deliver serialized outbound payloads to WebSocket clients, but the authoritative payload contents should be produced by the owning subsystems.
That means a later design may look like:
- `RuntimeSnapshotProvider` provides render-facing snapshot payloads or a runtime-state projection derived from those published snapshots
- `RuntimeCoordinator` or a later runtime-read-model helper provides control-plane runtime summaries when the UI needs more than raw render state
- `HealthTelemetry` provides health payloads
- `ControlServices` delivers them to connected observers
## Current Code Mapping
This section maps the current implementation onto the target subsystem.
### Current `RuntimeServices`
Should split into:
- `ControlServices` shell
- temporary compatibility adapter into `RuntimeCoordinator`
- removal of any direct runtime-state mutation responsibilities over time
Likely keep under `ControlServices`:
- service startup/shutdown
- OSC update coalescing
- Web control hosting shell
- file-watch ingress hosting
Should move out later:
- legacy direct runtime polling dependency
- deferred OSC commit behavior that has since moved behind coordinator-facing outcomes
- any remaining direct state-broadcast decisions tied to runtime internals
### Current `ControlServer`
Should become primarily:
- HTTP ingress adapter
- WebSocket session host
- static asset/doc host if that remains embedded
The callback table in current code:
- [ControlServer.h](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/control/ControlServer.h:18)
is a useful migration aid, but long-term it should evolve from callback-per-action toward typed action emission.
### Current `OscServer`
Should remain transport-focused.
Its clean long-term responsibilities are:
- UDP socket lifecycle
- OSC frame decode
- syntactic route extraction
- emitting decoded automation payloads into the `ControlServices` shell
It should not own any runtime state semantics beyond ingress decoding.
## Migration Plan
The safest migration is incremental.
### Step 1. Name The Boundary Explicitly
Create and use the `ControlServices` name in docs and future interfaces before moving all logic.
This document is part of that step.
### Step 2. Convert Callback Thinking Into Action Thinking
Without changing all runtime code at once, introduce typed action/event shapes for the major ingress paths.
The goal is for transports to emit actions, even if temporary adapters still call into existing code.
### Step 3. Extract Service Hosting From `OpenGLComposite`
`OpenGLComposite` currently owns `RuntimeServices` startup and consumption:
- [OpenGLComposite.cpp](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/gl/composite/OpenGLComposite.cpp:312)
- [OpenGLComposite.cpp](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/gl/composite/OpenGLComposite.cpp:723)
That should move toward a composition root or subsystem host arrangement where render is no longer the owner of control ingress.
### Step 4. Remove Direct Runtime Mutation Dependency
Previous polling and deferred OSC commit work directly against runtime storage:
- [RuntimeServices.cpp](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/control/RuntimeServices.cpp:194)
That has been routed through coordinator-facing actions; later phases should replace the remaining polling shape with event-driven flows.
### Step 5. Split Out Observation Delivery
WebSocket outbound delivery can stay in `ControlServices`, but serialization ownership should move toward the owning subsystems so the service layer stops assembling authoritative state itself.
## Risks
### Risk 1. Recreating `RuntimeHost` Coupling Under A New Name
If `ControlServices` is allowed to keep direct knowledge of runtime mutation internals, it will become a renamed version of the same coupling problem.
Mitigation:
- keep the boundary strict
- route mutations through coordinator interfaces
- treat any direct runtime mutation calls as migration-only compatibility
### Risk 2. Service Queues Becoming Hidden State Authority
Latest-value OSC maps and reload debounce flags are appropriate here. Full committed runtime state is not.
Mitigation:
- define ingress-local versus authoritative state explicitly
- bound queues
- publish queue metrics into telemetry
### Risk 3. WebSocket Broadcast Path Reintroducing Heavy Synchronous Work
If `ControlServices` becomes the place where whole runtime state is rebuilt or serialized on every input, it will recreate timing stalls.
Mitigation:
- broadcast snapshots produced elsewhere
- collapse redundant outbound requests
- track serialization/broadcast timing in telemetry
### Risk 4. Polling Surviving Too Long As Architecture
Some polling may remain during migration, but it should not become the permanent contract.
Mitigation:
- isolate polling behind ingress interfaces
- make replacement with event-driven flows a planned Phase 2/3 outcome
## Open Questions
- Should the embedded static UI/docs hosting stay inside `ControlServices`, or move to a thinner app-shell concern while control APIs remain in `ControlServices`?
- Should outbound state for WebSocket clients be one combined payload or separate runtime and health channels?
- How much route/key resolution should happen in `ControlServices` versus `RuntimeCoordinator`?
- Should any deferred automation-settle delivery remain in `ControlServices`, or should all commit/settle policy move entirely into coordinator/render ownership once the live-state model is formalized?
- When file watching is modernized, should reload classification live entirely in `ControlServices`, or should it emit a lower-level `FilesChanged` event and let `RuntimeCoordinator` decide reload semantics?
- Will future non-OSC automation sources reuse the same `AutomationTargetReceived` path, or need source-specific typed actions for policy reasons?
## Short Version
`ControlServices` should become the app's clean ingress boundary:
- transport handling stays here
- input normalization stays here
- ingress-local buffering stays here
- mutation policy does not
- authoritative runtime state does not
- render-local transient state does not
If later phases keep that line sharp, the app gains a control layer that is fast, testable, and timing-aware without becoming another shared state authority.

View File

@@ -0,0 +1,647 @@
# HealthTelemetry Subsystem Design
This document expands the `HealthTelemetry` subsystem introduced in [PHASE_1_SUBSYSTEM_BOUNDARIES_DESIGN.md](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/docs/PHASE_1_SUBSYSTEM_BOUNDARIES_DESIGN.md).
`HealthTelemetry` is the subsystem that owns operational visibility for the app. Its purpose is to gather health state, warnings, counters, logs, and timing observations from the other subsystems and publish them in a structured way without becoming a second control plane.
Before the Phase 1 runtime split, those responsibilities were fragmented across `RuntimeHost` status setters, ad hoc `OutputDebugStringA` calls, callback-local warnings, and UI-facing runtime-state payloads. The result was that the app could often detect problems, but did not yet have one clear place that answered:
- what is healthy right now
- what is degraded right now
- what has recently gone wrong
- which subsystem is under pressure
- how timing behavior is trending over time
`HealthTelemetry` is the target boundary that should answer those questions.
## Why This Subsystem Exists
The codebase already contains meaningful health and timing signals, but some are still spread through unrelated ownership domains:
- previous `RuntimeHost` status fields stored signal and timing status:
- `RuntimeHost.h`
- `RuntimeHost.cpp`
- `RuntimeHost.cpp`
- `RuntimeHost.cpp`
- render and bridge code historically reported timing by writing back into `RuntimeHost`:
- [OpenGLRenderPipeline.cpp](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/gl/pipeline/OpenGLRenderPipeline.cpp:50)
- [OpenGLVideoIOBridge.cpp](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/gl/pipeline/OpenGLVideoIOBridge.cpp:49)
- backend warning paths still log directly:
- [DeckLinkFrameTransfer.cpp](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/videoio/decklink/DeckLinkFrameTransfer.cpp:84)
- [DeckLinkSession.cpp](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/videoio/decklink/DeckLinkSession.cpp:305)
- control ingress failures still log directly:
- [OscServer.cpp](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/control/OscServer.cpp:142)
- [RuntimeServices.cpp](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/control/RuntimeServices.cpp:100)
This creates several recurring problems:
- health information shares storage and lock scope with runtime state
- warnings are not consistently classified by subsystem or severity
- timing data is hard to compare across render, control, and backend paths
- UI connection state and operational state are too closely coupled
- logging is mostly text-first instead of structured-first
- recovery behavior is hard to audit because the app does not retain a coherent health snapshot
`HealthTelemetry` exists so timing and health concerns have one subsystem whose only job is observation and reporting, instead of drifting back into runtime storage, callback-local logging, or UI payload assembly.
## Design Goals
`HealthTelemetry` should optimize for:
- one authoritative home for operational visibility
- structured health state per subsystem
- timing and counter recording that does not require a UI to be connected
- low-friction reporting from render, backend, coordinator, and services
- explicit degraded-mode reporting instead of only raw text logs
- support for live operator summaries and deeper engineering diagnostics
- minimal risk of telemetry writes becoming a render or callback bottleneck
## Responsibilities
`HealthTelemetry` owns structured operational visibility.
Primary responsibilities:
- accept timing samples from major subsystems
- accept counter deltas and point-in-time gauges
- accept warning, error, and degraded-state transitions
- collect subsystem-scoped health state
- collect operator-visible summary state
- collect structured log entries
- build stable health snapshots for UI, diagnostics, and later persistence/export if desired
- retain recent history needed for short-term troubleshooting
- classify observations by subsystem, severity, and category
Secondary responsibilities that still fit here:
- smoothing or rolling-window summaries for timing metrics
- mapping raw subsystem observations into operator-facing health summaries
- deduplicating repeated warnings
- tracking warning open/clear lifecycles
- providing bounded in-memory history for recent logs and warning transitions
## Explicit Non-Responsibilities
`HealthTelemetry` should not become a behavior owner.
It does not own:
- layer stack truth
- persistence policy
- render scheduling
- DeckLink scheduling
- OSC buffering or routing
- reload coordination
- shader compilation
- recovery actions themselves
It also should not decide:
- whether render should skip a frame
- whether VideoBackend should increase queue depth
- whether RuntimeCoordinator should reject a mutation
- whether ControlServices should drop or coalesce ingress traffic
Those decisions belong to the subsystem being observed. `HealthTelemetry` may describe that a subsystem is degraded, but it must not quietly become the mechanism that tells the app how to react.
## Ownership Boundaries
`HealthTelemetry` owns the following state categories.
### Structured Log State
Examples:
- subsystem name
- severity
- category
- timestamp
- message
- optional structured fields such as layer id, preset name, queue depth, or shader id
This replaces the idea that `OutputDebugStringA` text is itself the main diagnostic product.
### Warning And Error State
Examples:
- active warning set
- warning occurrence counts
- first-seen and last-seen timestamps
- clear timestamps
- subsystem-scoped degraded flags
This is the durable in-memory operational state that should answer "what is currently wrong?" even if no UI was connected when the warning was raised.
### Timing State
Examples:
- render duration
- frame budget
- playout completion interval
- smoothed completion interval
- queue depth
- input upload skip count
- async readback fallback count
- control ingress lag or queue depth
- snapshot publication cost
This state should be organized as time-series-like rolling telemetry, not as a grab bag of unrelated `double` fields mixed into the runtime store.
### Health Snapshot State
Examples:
- current subsystem health summaries
- current operator-facing overall health summary
- most recent warning list
- recent counters and timing summaries
- "degraded but still running" status
This is the material that `ControlServices` or a diagnostics endpoint may later publish.
## State Model
The subsystem should model health and telemetry in a way that supports both machine-friendly and operator-friendly views.
Suggested conceptual model:
- `TelemetryLogEntry`
- `TelemetryWarningRecord`
- `TelemetryCounterState`
- `TelemetryGaugeState`
- `TelemetryTimingSeries`
- `SubsystemHealthState`
- `HealthSnapshot`
Important distinction:
- raw observations are append/update operations
- health snapshots are derived read models
That distinction matters because the system should be able to retain richer recent telemetry internally than what is necessarily sent to the UI on every refresh.
## Subsystem Health Domains
`HealthTelemetry` should track health by subsystem rather than as one flat status blob.
At minimum, Phase 1 should assume domains for:
- `RuntimeStore`
- `RuntimeCoordinator`
- `RuntimeSnapshotProvider`
- `ControlServices`
- `RenderEngine`
- `VideoBackend`
Optional cross-cutting domain:
- `ApplicationShell`
Each domain should be able to express states such as:
- `Healthy`
- `Warning`
- `Degraded`
- `Error`
- `Unavailable`
The exact enum can change, but the design should preserve the idea that each subsystem reports into its own health lane first, and only then is an overall status derived.
## Logging Boundaries
Logging belongs here, but logging should be structured-first.
Expected inputs:
- subsystem-scoped debug information
- warning and error messages
- recovery events
- notable state transitions
- significant operator actions that matter for diagnostics
Expected design rules:
- textual messages are still useful, but they should be wrapped in a structured log entry
- repeated transient failures should be rate-limited or deduplicated at the telemetry layer where possible
- log storage should be bounded in memory
- UI publication should read from health/log snapshots, not scrape stdout/debug output
Examples of current direct log paths that should eventually move behind `HealthTelemetry`:
- OSC decode/dispatch failures
- screenshot write failures
- DeckLink fallback warnings
- late/dropped frame warnings
## Metrics And Timing Boundaries
Timing and metrics should also move here, but their ownership line matters.
`HealthTelemetry` should own:
- metric collection interfaces
- rolling summaries
- recent history buffers
- warning thresholds if the app later chooses to define them declaratively
- operator-facing derived summaries
The producing subsystem should still own:
- the meaning of the measurement
- when it is sampled
- whether it triggers local mitigation
Examples:
- `RenderEngine` owns when render duration is sampled
- `VideoBackend` owns when queue depth or playout lateness is sampled
- `ControlServices` owns when ingress backlog is sampled
- `RuntimeSnapshotProvider` owns when snapshot publish/build timing is sampled
`HealthTelemetry` should not invent those timings by inference. It records them when producers report them.
## Proposed Interfaces
These are target-shape interfaces, not final signatures.
### Write/Record Interface
Core write-side operations could look like:
```cpp
enum class TelemetrySeverity;
enum class TelemetrySubsystem;
struct TelemetryLogEntry;
struct TelemetryWarning;
struct TelemetryTimingSample;
struct TelemetryCounterDelta;
struct TelemetryGaugeUpdate;
class IHealthTelemetry
{
public:
virtual void AppendLogEntry(const TelemetryLogEntry& entry) = 0;
virtual void RaiseWarning(const TelemetryWarning& warning) = 0;
virtual void ClearWarning(std::string_view warningKey) = 0;
virtual void RecordTimingSample(const TelemetryTimingSample& sample) = 0;
virtual void RecordCounterDelta(const TelemetryCounterDelta& delta) = 0;
virtual void RecordGauge(const TelemetryGaugeUpdate& gauge) = 0;
virtual void ReportSubsystemState(TelemetrySubsystem subsystem,
SubsystemHealthState state) = 0;
};
```
The key is that every subsystem should be able to publish observations without also needing to know how UI payloads, rolling summaries, or log retention are implemented.
### Read Interface
Expected read-side operations:
- `BuildHealthSnapshot()`
- `GetSubsystemHealth(...)`
- `GetRecentLogs(...)`
- `GetActiveWarnings()`
- `GetRecentTimingSummary(...)`
Design notes:
- the read interface should return stable snapshots or read models
- UI/websocket publication should consume those snapshots through `ControlServices`
- read-side access should not require direct knowledge of internal ring buffers or lock layout
## Producer Expectations By Subsystem
The parent Phase 1 design already allows multiple subsystems to publish into telemetry. This section makes that concrete.
### From `RuntimeCoordinator`
Expected observations:
- mutation rejected
- reload requested
- preset apply failed
- transient state cleared due to compatibility rules
- policy-driven degraded notices such as repeated invalid external control input
### From `RuntimeSnapshotProvider`
Expected observations:
- snapshot publication duration
- snapshot build failure
- snapshot version churn metrics
- repeated publish retries or stale-snapshot conditions
### From `ControlServices`
Expected observations:
- OSC decode failures
- websocket broadcast failures
- REST/control transport errors
- ingress queue depth
- coalescing/drop counts
- file-watch reload request activity
### From `RenderEngine`
Expected observations:
- frame render duration
- upload duration
- readback duration
- fallback to synchronous readback
- preview present timing
- render-local state resets caused by reload or incompatibility
### From `VideoBackend`
Expected observations:
- current playout queue depth
- system-memory playout frame counts by state: free, ready, and scheduled
- system-memory playout underrun, repeat, and drop counters
- system-memory frame age at schedule and completion time
- input signal state
- late frames
- dropped frames
- backend mode changes
- fallback from 10-bit to 8-bit input
- output-only black-frame mode
## Current Code Mapping
The current codebase already contains several telemetry responsibilities that should migrate here.
### Previous `RuntimeHost` Status Setters
These were the clearest initial migration candidates:
- `SetSignalStatus(...)`
- `TrySetSignalStatus(...)`
- `SetPerformanceStats(...)`
- `TrySetPerformanceStats(...)`
- `SetFramePacingStats(...)`
- `TrySetFramePacingStats(...)`
See:
- `RuntimeHost.h`
- `RuntimeHost.cpp`
- `RuntimeHost.cpp`
- `RuntimeHost.cpp`
In the target architecture, this kind of state should not sit on the same object that owns persistent layer truth.
### Render Timing Production
Current render timing is produced in:
- [OpenGLRenderPipeline.cpp](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/gl/pipeline/OpenGLRenderPipeline.cpp:50)
That timing sample should conceptually become:
- `RenderEngine -> HealthTelemetry::RecordTimingSample(...)`
not the old pattern:
- `RenderEngine -> RuntimeHost::TrySetPerformanceStats(...)`
### Playout And Signal Status Production
Current signal and frame pacing updates are produced in:
- [OpenGLVideoIOBridge.cpp](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/gl/pipeline/OpenGLVideoIOBridge.cpp:49)
- [OpenGLVideoIOBridge.cpp](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/gl/pipeline/OpenGLVideoIOBridge.cpp:61)
These should eventually become structured `VideoBackend` observations instead of bridge-to-host status writes.
### Direct Warning And Log Paths
Current examples:
- late/dropped frame warnings:
- [DeckLinkFrameTransfer.cpp](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/videoio/decklink/DeckLinkFrameTransfer.cpp:84)
- backend fallback warnings:
- [DeckLinkSession.cpp](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/videoio/decklink/DeckLinkSession.cpp:305)
- [DeckLinkSession.cpp](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/videoio/decklink/DeckLinkSession.cpp:320)
- OSC errors:
- [OscServer.cpp](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/control/OscServer.cpp:142)
- [RuntimeServices.cpp](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/control/RuntimeServices.cpp:100)
All of these are clear migration candidates for `AppendLogEntry(...)`, `RaiseWarning(...)`, or counter/timing updates.
## Health Snapshot Contract
`HealthTelemetry` should expose one coherent health snapshot that other publication layers can consume.
That snapshot should be able to answer, at minimum:
- what the overall app health is
- whether input signal is present
- whether playout is healthy, degraded, or underrunning
- whether render timing is within budget
- what active warnings exist
- what recent notable events occurred
- what the current subsystem-specific states are
The important boundary is:
- `HealthTelemetry` builds the health snapshot
- `ControlServices` may publish it
- UI consumes it
That avoids rebuilding health summaries ad hoc in UI-facing runtime state serializers.
## Concurrency Expectations
This subsystem will likely receive updates from multiple threads:
- control ingress threads
- render thread
- backend callback threads
- coordinator/service threads
So the design should assume:
- low-contention write paths
- bounded memory
- no long-held global mutex that callbacks and render both depend on
Phase 1 does not require lock-free implementation, but it does require the architecture to avoid recreating the old problem where health writes share the same lock as durable state and render-facing concerns.
Practical expectations:
- per-domain aggregation or lightweight internal locking is acceptable
- read snapshots should be cheap and stable
- callback paths should record telemetry cheaply and return
## Migration Plan From Current Code
The safest migration path is to peel telemetry responsibilities away from the existing classes incrementally.
### Step 1: Introduce The `HealthTelemetry` Interface
Create a small interface and health model types first.
Initial responsibilities:
- append structured logs
- record timing samples
- record counter deltas
- raise and clear warnings
- build a read-only health snapshot
The first implementation can still be backed by simple in-memory structures.
### Step 2: Keep New Observations Off Runtime Storage
Route new health-style work into `HealthTelemetry` instead of adding more status fields to runtime storage.
This prevents the old status surface from growing during migration.
### Step 3: Replace Legacy Status Setters With Telemetry Producers
Refactor:
- render timing writes
- signal status writes
- playout pacing writes
so they publish structured observations instead of mutating store-adjacent fields.
### Step 4: Replace Direct `OutputDebugStringA` Warning Paths
Wrap common warning/error cases in telemetry producers.
This includes:
- OSC decode/dispatch failures
- DeckLink late/dropped frame notifications
- backend fallback notices
- screenshot write failures
Direct debug output can remain as a sink of telemetry if desired, but not as the primary source of truth.
### Step 5: Publish Health Snapshot Through UI/Diagnostics Paths
Once the snapshot format exists, let `ControlServices` publish health summaries and recent warnings explicitly rather than depending on the runtime-state payload alone.
## Risks
### 1. Telemetry becomes a hidden behavior controller
If warning states start being used as the indirect way subsystems tell each other what to do, the subsystem boundary will fail.
Guardrail:
- telemetry observes and reports
- it does not coordinate or command
### 2. Logging stays string-only
If the subsystem only centralizes text logging without structure, later diagnostics will still be difficult.
Guardrail:
- severity, subsystem, category, and optional fields should be first-class
### 3. Timing writes become too expensive
If every sample requires heavy locking or snapshot rebuilds, render and callback timing could regress.
Guardrail:
- cheap recording path
- derived summaries built separately from hot-path writes
### 4. Health snapshot duplicates runtime truth
If health snapshots start storing copies of durable runtime state, the subsystem boundary will blur again.
Guardrail:
- health snapshots summarize operational state
- they do not become a second runtime store
### 5. Warning severity semantics drift by subsystem
If each subsystem invents its own meaning for warning/degraded/error, operator visibility becomes noisy and inconsistent.
Guardrail:
- define shared severity and health-state vocabulary early
## Open Questions
### 1. Should debug-output sinks remain enabled by default?
Current recommendation:
- yes, as a sink fed by structured telemetry entries, not as the source of truth
### 2. How much timing history should be retained in memory?
Current recommendation:
- enough for short-term live troubleshooting and UI summaries
- not an unbounded time-series archive
### 3. Should operator-facing health and engineering diagnostics use the same snapshot?
Current recommendation:
- share one core telemetry model
- allow separate derived views for concise operator summaries versus deeper engineering detail
### 4. Where should threshold policy live if the app later formalizes warnings like "render over budget"?
Current recommendation:
- telemetry may evaluate declared thresholds
- subsystem owners still own mitigation behavior
### 5. Should input signal presence remain part of runtime state or move fully into telemetry?
Current recommendation:
- treat it as operational health state under `VideoBackend` reporting into telemetry
- avoid keeping it as a core durable runtime-store concern
## Success Criteria For This Subsystem
`HealthTelemetry` can be considered well-defined once the codebase can say, without ambiguity:
- all major subsystems have one place to publish timing, warnings, and counters
- health and timing state no longer share ownership with durable runtime state
- the UI can consume a stable health snapshot without scraping unrelated runtime fields
- direct debug-string warning paths are being retired or wrapped behind structured telemetry
- degraded-but-running conditions are visible as first-class state
## Short Version
`HealthTelemetry` is the subsystem that should answer:
- what is healthy right now
- what is degraded right now
- what recent warnings and errors occurred
- how render, control, and playout timing are behaving
It should:
- collect structured logs
- collect warnings and counters
- collect timing samples and gauges
- build stable health snapshots for publication
It should not:
- own core runtime truth
- decide app behavior
- coordinate recovery actions
- become a replacement for the render or backend policy layers
If this boundary holds, later phases can keep moving toward a much more diagnosable live system without putting timing and warning state back into runtime storage.

44
docs/subsystems/README.md Normal file
View File

@@ -0,0 +1,44 @@
# Subsystem Notes Index
The current, phase-free architecture summary is:
- [Current System Architecture](../CURRENT_SYSTEM_ARCHITECTURE.md)
Start there when you want to understand how the application works now.
This directory contains deeper notes for individual subsystem boundaries. These notes were originally written during the phased architecture work, so some files may still mention migration steps or target-state language. Treat them as companion notes, not as the source of truth when they disagree with the current architecture summary.
## Recommended Reading Order
1. [Current System Architecture](../CURRENT_SYSTEM_ARCHITECTURE.md)
2. [RuntimeStore](RuntimeStore.md)
3. [RuntimeCoordinator](RuntimeCoordinator.md)
4. [RuntimeSnapshotProvider](RuntimeSnapshotProvider.md)
5. [ControlServices](ControlServices.md)
6. [RenderEngine](RenderEngine.md)
7. [VideoBackend](VideoBackend.md)
8. [HealthTelemetry](HealthTelemetry.md)
That order follows the current ownership story:
- durable state first
- mutation and publication next
- control ingress after that
- render ownership and video timing next
- operational visibility last
## Subsystem Notes
- [RuntimeStore](RuntimeStore.md): durable runtime-state facade over layer-stack, config, package-catalog, presentation, and persistence boundaries.
- [RuntimeCoordinator](RuntimeCoordinator.md): mutation validation, state classification, reset/reload policy, and publication/persistence requests.
- [RuntimeSnapshotProvider](RuntimeSnapshotProvider.md): render-facing snapshot publication boundary backed by explicit render snapshot building/versioning.
- [ControlServices](ControlServices.md): OSC, HTTP/WebSocket, and file-watch ingress plus normalization and service-local buffering.
- [RenderEngine](RenderEngine.md): GL ownership boundary, render-local transient state, preview, and playout-ready frame production.
- [VideoBackend](VideoBackend.md): device lifecycle, input/output pacing, buffer policy, and producer/consumer playout behavior.
- [HealthTelemetry](HealthTelemetry.md): logs, warnings, counters, timing traces, and subsystem health snapshots.
## Historical Documents
The `docs/PHASE_*` files and experiment logs record how the architecture evolved. They are useful when you need rationale, investigation history, or rejected paths, but they are no longer arranged as the main feature split for the app.
For current implementation work, use [Current System Architecture](../CURRENT_SYSTEM_ARCHITECTURE.md) as the entry point and only dip into the phase documents when you need context for why a subsystem ended up this way.

View File

@@ -0,0 +1,486 @@
# RenderEngine Subsystem Design
This document expands the `RenderEngine` portion of [PHASE_1_SUBSYSTEM_BOUNDARIES_DESIGN.md](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/docs/PHASE_1_SUBSYSTEM_BOUNDARIES_DESIGN.md). It defines the target ownership, boundaries, and migration shape for the rendering subsystem so later phases can move GL work out of today's mixed orchestration paths without inventing new boundaries on the fly.
The intent here is not to force a one-step rewrite. It is to make the target render boundary explicit enough that later work on events, live-state layering, sole-owner GL threading, and backend decoupling all land in the same place.
## Purpose
`RenderEngine` is the live frame-production subsystem.
It owns:
- GL context ownership in the target architecture
- render loop cadence and render task execution
- shader program and render-pass execution once build outputs are available
- capture texture upload scheduling once frames are accepted for render
- temporal history resources
- shader feedback resources
- render-local transient overlays
- preview-ready frame production
- playout-ready frame production
- render-local reset and rebuild behavior
It does not own:
- persisted runtime state
- high-level mutation policy
- OSC/UI ingress
- device discovery or callback policy
- playout queue policy
- operator-visible health policy beyond publishing observations
In the Phase 1 terminology, `RenderEngine` consumes snapshots plus render-local transient state and produces completed visual frames plus timing signals.
## Why This Subsystem Needs A Sharp Boundary
The current rendering path is split across several classes:
- [OpenGLComposite.cpp](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/gl/composite/OpenGLComposite.cpp:86) constructs the renderer, render pipeline, shader programs, runtime services, and video bridge in one owner.
- [OpenGLRenderPipeline.cpp](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/gl/pipeline/OpenGLRenderPipeline.cpp:31) performs pass execution, pack/readback, preview paint, and performance stat publication.
- [OpenGLVideoIOBridge.cpp](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/gl/pipeline/OpenGLVideoIOBridge.cpp:58) accepts capture frames and still performs render work from the playout completion callback path.
- `RenderFrameStateResolver` and `RenderStateComposer` now keep frame-state selection and live value composition outside GL drawing, while `RenderEngine` still owns the current GL resource and draw path.
That split is workable today, but it creates architectural pressure:
- render and playout timing are still callback-coupled.
- preview and playout are produced in the same immediate path.
- render-local transient state now has clearer Phase 3/5 boundaries, but output production is still synchronously requested by the backend completion path.
- it is difficult to test render behavior separately from app bootstrap and hardware integration.
`RenderEngine` exists to absorb that responsibility into one subsystem with one direction of ownership. Phase 4 has completed the GL ownership part of this target: normal runtime GL work now enters through the `RenderEngine` render thread.
## Responsibilities
### 1. Sole GL Ownership
In the target design, `RenderEngine` should be the only subsystem that performs long-lived GL work.
That includes:
- context binding and release policy
- framebuffer and texture lifetime
- shader program binding and draw execution
- upload/readback buffer lifetime
- preview blit or present paths
- render-local resource reset on rebuild or video-format changes
This is the most important boundary. Other subsystems may request work or provide data, but they should not directly perform GL commands.
### 2. Snapshot Consumption
`RenderEngine` should consume immutable or near-immutable render snapshots from `RuntimeSnapshotProvider`.
It is responsible for:
- detecting snapshot version changes
- rebuilding or re-binding render-local resources when the snapshot changes
- resolving render-pass execution from snapshot contents
- separating structural snapshot changes from transient overlay changes
It should not inspect mutable runtime store objects directly.
### 3. Frame Production
`RenderEngine` should produce completed frames for two consumers:
- preview presentation
- `VideoBackend` playout consumption
Those outputs may share most of their render work, but they are not equal-priority outputs. The subsystem rule from Phase 1 should be preserved:
- playout is the primary timing-sensitive output
- preview is subordinate and best-effort
### 4. Render-Local Transient State
`RenderEngine` owns transient visual state that affects output but is not persisted truth.
Examples:
- temporal history textures
- feedback ping-pong buffers
- render-local OSC/live overlay state
- queued input frames accepted for upload
- cached readback frames
- preview-only presentation state
- in-flight rebuild generations
This state should remain render-local even when it influences visible output.
Phase 5's `RuntimeStateLayerModel` explicitly keeps temporal history, feedback state, accepted input frames, staged output frames, preview staging, and screenshot/readback staging in the render-local category. These are deliberately outside the persisted/committed/transient-automation parameter composition rule.
`RuntimeLiveState` now owns transient automation invalidation for render-facing compatibility. It can clear overlays for a target layer/control key and prunes overlays that no longer resolve to the current layer and parameter definitions before applying them to a frame. This keeps shader reload, preset load, and layer removal behavior local to the live-state/composition boundary instead of scattering it through GL drawing code.
Render snapshots now flow through a named `CommittedLiveStateReadModel`, so render-facing committed state is distinct from durable storage and physically owned by `CommittedLiveState`.
### 5. Shader Build Application
Compilation itself may eventually move into a separate build service, but once shader build outputs exist, `RenderEngine` owns:
- program creation/link usage
- pass graph application
- sampler/texture binding layout application
- resource reallocation required by shader shape changes
- safe invalidation of old render-local feedback/history resources
### 6. Render Timing Publication
`RenderEngine` should publish observations to `HealthTelemetry` such as:
- frame render duration
- upload duration
- pass execution duration
- pack/readback duration
- preview present timing
- rebuild stalls
- dropped/skipped input uploads
- output frame production latency
It should publish them, not own the health policy built from them.
## Non-Responsibilities
The target boundary should remain explicit about what does not belong here.
`RenderEngine` should not:
- decide whether a parameter mutation is persisted
- normalize OSC/UI actions
- choose device modes
- own DeckLink callback behavior
- own playout headroom policy
- perform stack preset serialization
- broadcast UI state
- treat telemetry as a control plane
Those rules matter because the current codebase often solves timing issues by letting the render path reach sideways into nearby systems.
## GL Ownership Model
## Current Rule
One subsystem owns GL. `RenderEngine` now starts a dedicated render thread, binds the existing GL context on that thread for normal runtime work, and routes input upload, output render, preview presentation, screenshot capture, shader application, and render-local reset work through render-thread requests.
The render thread should:
- create or adopt the GL context
- execute all frame production work
- perform accepted texture uploads
- execute all pass graphs
- manage async readback and output packing
- manage feedback/history resets and reallocations
Other threads should interact with the subsystem through queues, snapshots, and completion signals, not by borrowing the GL context.
## Remaining Timing State
GL ownership is no longer shared across callback-driven and UI entrypoints:
- input upload is requested through [OpenGLVideoIOBridge::UploadInputFrame()](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/gl/pipeline/OpenGLVideoIOBridge.cpp:11)
- playout-triggered render is requested through [OpenGLVideoIOBridge::RenderScheduledFrame()](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/gl/pipeline/OpenGLVideoIOBridge.cpp:18)
- render-pass execution occurs in [OpenGLRenderPipeline::RenderFrame()](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/gl/pipeline/OpenGLRenderPipeline.cpp:31)
- preview and screenshot paths enter `RenderEngine` queue/request methods
The remaining timing issue is not shared GL ownership; it is the transitional synchronous output request/response path. The DeckLink completion callback still waits while the render thread produces an output frame, fills the DeckLink buffer, and then schedules the next frame.
## Migration Direction
The next target path should be:
1. input callback enqueues frame payloads or references
2. render thread accepts the latest usable input frame
3. render thread performs uploads on its own cadence
4. render thread produces completed output frames ahead of backend demand
5. backend callbacks only dequeue and schedule pre-rendered frames
Phase 4 completed the part that removes callback-thread GL ownership. Phase 7 should complete the producer/consumer playout part.
## Render Loop Boundaries
`RenderEngine` should own a render loop with explicit phases. A good target shape is:
1. drain render-side commands and accepted service events
2. swap to the latest published snapshot if needed
3. apply render-local transient overlays
4. accept or coalesce latest input frame for upload
5. perform required uploads
6. execute pass graph
7. update temporal and feedback resources
8. pack and stage output frame(s)
9. publish preview-ready image if due
10. publish playout-ready frame(s) to `VideoBackend`
11. emit timing and health samples
The important property is that preview, playout preparation, feedback maintenance, and upload execution all happen under one render-owned cadence rather than as ad hoc side effects of unrelated callbacks.
## Snapshot And Overlay Interaction
`RenderEngine` should treat snapshots and overlays as different layers of state.
### Snapshot Inputs
Snapshots should provide:
- layer stack structure
- shader/package selections
- validated committed parameter values
- pass graph definitions
- resource requirements derived from runtime state
### Render-Local Overlay Inputs
Overlays should provide:
- active automation targets
- smoothed transient parameter overrides
- temporary visual state that should not persist back into the store
- queued reset/rebuild invalidations for render-local resources
### Resolution Rule
The render-side resolution order should be:
1. snapshot committed state forms the baseline
2. render-local transient overlays are applied on top
3. feedback/history resources influence shading as render-local inputs
4. completed frame is produced without mutating the underlying snapshot
This is especially important after the OSC work already moved toward render-local overlays. Phase 1 should keep that direction: render consumes committed truth plus transient live overlays, but render does not become the owner of persisted truth.
## Preview And Playout Relationship
Preview should be a subordinate consumer of render results, not a peer that can disturb playout timing.
### Target Rule
- playout deadlines come first
- preview is best-effort
- preview cadence may be reduced independently
- preview failure must not stall output frame production
### Current State
Today preview still hangs off the render pipeline path through `mPaint()` in [OpenGLRenderPipeline::RenderFrame()](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/gl/pipeline/OpenGLRenderPipeline.cpp:54). That keeps preview close enough to the playout path that it is still part of the same timing surface.
### Target Shape
`RenderEngine` should internally distinguish:
- playout-ready frame production
- preview presentation or preview-copy publication
Possible later implementations:
- playout frame and preview frame share one composite render, but preview present is decoupled and rate-limited
- render publishes a preview texture handle or CPU-side preview image to a preview presenter
- preview updates are skipped under load without affecting playout queue fill
The exact implementation can change later, but the subsystem contract should already assume preview is subordinate.
## Interaction With `RuntimeSnapshotProvider`
`RenderEngine` should depend on `RuntimeSnapshotProvider`, not on `RuntimeStore`.
Expected interactions:
- query latest snapshot version
- consume latest stable snapshot
- detect structural versus parameter-only changes
- request no mutation back into the snapshot provider during render
Expected non-interactions:
- no direct persistence reads/writes
- no raw store mutation
- no direct service ingress handling
This is one of the main Phase 1 guardrails, because the current code often achieves convenience by letting render reach back into runtime-owned mutable objects.
## Interaction With `VideoBackend`
The target dependency direction stays:
- `VideoBackend -> RenderEngine`
That means:
- backend requests or consumes ready frames
- backend reports output timing/completion events
- render does not own output device policy
`RenderEngine` should expose frame-production and queue-facing interfaces, while `VideoBackend` owns:
- device callback handling
- output scheduling policy
- buffer pool policy
- backend state transitions
In later phases, this should evolve toward a producer/consumer queue where:
- render produces completed frames ahead of demand
- backend consumes already-produced frames
- callbacks drive dequeue/schedule/accounting only
## Current Code Mapping
The following current responsibilities should converge into `RenderEngine`.
### From `OpenGLComposite`
- render-local overlay management
- render-facing rebuild application
- screenshot-related render execution hooks
- render bootstrap ownership currently mixed with app bootstrap
### From `OpenGLRenderPipeline`
- frame render orchestration
- output pack conversion
- async readback state
- output frame caching
- preview-ready signal publication
### From `OpenGLVideoIOBridge`
- GL texture upload execution should move under render ownership
- playout callback render work should move out of the callback path
### Remains Outside `RenderEngine`
- device callback registration
- playout scheduling policy
- signal/device status lifecycle
- runtime mutation policy
## Suggested Internal Components
This document does not require final class names, but `RenderEngine` will likely be easier to evolve if it is not one monolithic replacement for `OpenGLComposite`.
Reasonable internal pieces could include:
- `RenderLoopController`
- `RenderSnapshotConsumer`
- `RenderOverlayState`
- `RenderInputQueue`
- `RenderPassExecutor`
- `RenderHistoryManager`
- `RenderOutputStager`
- `PreviewPresenter`
Those are internal implementation helpers. They should not become new cross-cutting subsystem boundaries by themselves.
## Public Interface Shape
Aligned with the Phase 1 design, `RenderEngine` should eventually expose operations in this family:
- `StartRenderLoop(...)`
- `StopRenderLoop()`
- `ConsumeSnapshot(...)`
- `EnqueueInputFrame(...)`
- `ApplyOverlayUpdate(...)`
- `RequestRenderLocalReset(...)`
- `HandleRebuildOutputs(...)`
- `TryProduceOutputFrame(...)`
- `GetPreviewFrame(...)`
- `ReportRenderState()`
Interface goals:
- calls are explicit about whether they mutate render-local state or request frame production
- no caller needs direct GL access
- preview and playout are exposed as outputs, not as reasons for callers to enter the render path
## Migration Plan From Current Code
### Step 1. Name The Boundary
Treat `OpenGLRenderPipeline` plus the render portions of `OpenGLComposite` and `OpenGLVideoIOBridge` as conceptually belonging to `RenderEngine`, even before physical extraction is complete.
### Step 2. Stop New Render Work From Escaping
As new features are added, keep:
- feedback buffers
- temporal history
- render-local overlays
- preview state
inside render-owned code paths instead of putting them back into runtime storage or service layers.
### Step 3. Isolate Snapshot Consumption
Introduce snapshot-facing APIs so render no longer depends on broad runtime-state access for frame production.
Current status: Phase 3 introduced `RenderFrameInput`, `RenderFrameState`, and `RenderFrameStateResolver`, so frame-state selection is named and no longer lives inside GL drawing. Phase 4 built on that contract and moved normal runtime GL ownership onto the render thread.
### Step 4. Move Uploads Onto Render Ownership
Input callbacks should enqueue or hand off frame data; render executes the upload.
### Step 5. Break Callback-Driven Rendering
Move from "render in playout completion callback" to "render ahead and let backend consume ready frames."
### Step 6. Decouple Preview Cadence
Make preview a best-effort presentation path with its own skip/rate-limit policy.
### Step 7. Narrow `OpenGLComposite`
After the above, `OpenGLComposite` should collapse toward a composition root and legacy adapter rather than remaining the owner of render behavior.
## Risks
### Latency Risk
Moving to queue-based frame production can accidentally increase latency if headroom is allowed to grow without policy. `RenderEngine` should therefore expose queue-friendly production, but `VideoBackend` must still own explicit latency/headroom policy.
### Resource Churn Risk
Snapshot changes, shader rebuilds, and video-format changes can cause expensive reallocation of:
- feedback surfaces
- history buffers
- output pack resources
- readback buffers
The subsystem needs clear structural-change boundaries so parameter-only changes do not trigger broad resource churn.
### Preview Coupling Risk
If preview remains too close to the render/playout path, it can continue to steal budget from output production even after the rest of the subsystem is cleaned up.
### Readback Deadline Risk
The current async readback path still falls back to synchronous reads when the deadline is missed. That behavior may remain necessary, but `RenderEngine` should treat it as a degraded-path metric, not as an invisible normal case.
### Overlay Complexity Risk
Render-local overlays are powerful, but they can become a hidden second state model if not kept clearly subordinate to committed snapshot state.
## Open Questions
- Should preview become a separate presenter helper inside `RenderEngine`, or remain a subordinate callback/output sink?
- Where should screenshot capture live long-term: inside `RenderEngine`, or in a small render consumer layered on top of it?
- Should shader compilation outputs be delivered to render as whole-framegraph rebuild packages, or incrementally by layer/pass?
- How should input frame ownership work under load: newest-only, bounded queue, or policy selected by backend mode?
- Should render expose one playout-ready frame at a time, or a bounded ring the backend drains directly?
- What exact distinction should the snapshot provider publish between structural changes and parameter-only changes so render rebuilds stay cheap?
## Phase 1 Exit Criteria For `RenderEngine`
For Phase 1, this subsystem design is sufficiently defined once the project agrees that:
- render is the sole long-term owner of GL work
- render consumes snapshots, not mutable runtime store objects
- preview is subordinate to playout
- feedback/history/overlays are render-local transient state
- backend callbacks should converge toward dequeue/schedule behavior rather than direct rendering
- current render responsibilities in `OpenGLComposite`, `OpenGLRenderPipeline`, and `OpenGLVideoIOBridge` are expected to migrate under this subsystem
## Short Version
`RenderEngine` should become the subsystem that owns live GPU execution and nothing else.
It consumes committed snapshots plus render-local overlays, owns the full GL lifecycle, produces preview and playout-ready frames, and publishes timing observations. It should not own persistence, control ingress, or hardware scheduling policy. If later phases hold to that line, timing work and render-state work can get cleaner without reintroducing the same cross-thread coupling in a different form.

View File

@@ -0,0 +1,564 @@
# RuntimeCoordinator Design Note
This document defines the target design for the `RuntimeCoordinator` subsystem introduced in [PHASE_1_SUBSYSTEM_BOUNDARIES_DESIGN.md](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/docs/PHASE_1_SUBSYSTEM_BOUNDARIES_DESIGN.md).
`RuntimeCoordinator` is the mutation and policy layer for the app. Its job is to accept already-normalized actions from ingress systems, decide whether those actions are valid, classify how they should affect durable and live state, and trigger downstream publication or persistence work without taking ownership of rendering, device callbacks, or disk serialization details.
## Why This Subsystem Exists
Before the Phase 1 runtime split, the app's mutation path was split across several places:
- `RuntimeHost` performed validation, mutation, persistence, render-state invalidation, and some status updates:
- `RuntimeHost.h`
- `RuntimeHost.cpp`
- `OpenGLComposite` currently acts like an orchestration shell and a mutation coordinator at the same time:
- [OpenGLCompositeRuntimeControls.cpp](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/gl/composite/OpenGLCompositeRuntimeControls.cpp:1)
- `RuntimeServices` still owns some deferred control flow around OSC commit and polling:
- [RuntimeServices.h](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/control/RuntimeServices.h:46)
That overlap makes several kinds of regressions more likely:
- persistence policy leaks into control handlers
- render invalidation rules are spread across UI and non-UI paths
- transient automation behavior is hard to reason about
- reload behavior is partly a render concern and partly a runtime concern
- future event-model work has no single policy owner to target
`RuntimeCoordinator` exists to centralize those decisions without becoming a new monolith.
## Core Responsibilities
`RuntimeCoordinator` should own the following responsibilities.
### 1. Mutation intake after normalization
`RuntimeCoordinator` accepts typed, already-parsed actions from `ControlServices` or composition-root adapters. Examples:
- add/remove/move layer
- change shader on a layer
- change a parameter value
- reset a layer
- save or load a stack preset
- request a shader/package reload
- apply a transient automation target
- commit or clear transient overlay state
The coordinator should not parse JSON, decode OSC payloads, or inspect HTTP payload syntax. That belongs to ingress systems.
### 2. Validation and policy decisions
The coordinator validates whether a requested mutation is allowed and decides how it should behave.
Examples:
- whether a layer id exists
- whether a shader id is valid
- whether a parameter exists on the targeted shader
- whether a value is within the definition's allowed range or enum set
- whether a trigger should update committed state, transient state, or both
- whether a structural change should preserve compatible transient state such as feedback buffers
This is the policy surface that used to be spread between `RuntimeHost` methods such as:
- `AddLayer(...)`
- `SetLayerShader(...)`
- `UpdateLayerParameter(...)`
- `UpdateLayerParameterByControlKey(...)`
- `ApplyOscTargetByControlKey(...)`
- `ResetLayerParameters(...)`
See `RuntimeHost.h`.
### 3. State classification
The coordinator decides which state category a mutation affects:
- persisted state
- committed live state
- transient live overlay state
- health/timing state only
The design rule is that classification belongs here, not in the ingress layer and not in render code.
Phase 5 codifies the shared vocabulary for this classification in `RuntimeStateLayerModel`. Current committed session parameter values and layer bypass state are committed-live/session state owned by `CommittedLiveState`; runtime compile/reload flags are coordination state rather than durable store truth.
### 4. Snapshot publication requests
When a mutation changes render-facing state, the coordinator asks `RuntimeSnapshotProvider` to publish a new snapshot or mark one dirty for publication.
The coordinator does not build render snapshots itself.
### 5. Persistence requests
When a mutation changes durable state, the coordinator asks `RuntimeStore` to record the new authoritative state and, when applicable, request persistence through the store's write path.
The coordinator does not serialize files directly.
### 6. Cross-subsystem consistency policy
The coordinator is where "what else must happen if this changes?" lives.
Examples:
- a layer add/remove/move may require:
- store mutation
- snapshot republish
- compatibility-preserving render-state reset policy
- optional UI-state notification via later event-model work
- a stack preset load may require:
- replacement of committed layer stack state
- invalidation of transient overlay state that no longer maps cleanly
- snapshot republish
- deferred persistence request
- an automation target may require:
- transient overlay update only
- no persistence write
- optional later commit into committed live state if policy says so
## Explicit Non-Responsibilities
`RuntimeCoordinator` should explicitly not own the following.
### Not a persistence engine
It does not:
- read or write files
- decide file formats
- own preset storage layout
- perform debounced disk flushing logic
Those belong in `RuntimeStore` and later persistence helpers.
### Not a render engine
It does not:
- own GL objects
- perform shader compilation
- reset temporal history textures directly
- build render passes
- hold frame queues
It may request policy outcomes that cause render-local resets, but render performs the work.
### Not a hardware/backend owner
It does not:
- configure DeckLink
- react directly to device callbacks
- schedule playout
- own input signal callbacks
### Not an ingress transport layer
It does not:
- parse OSC wire messages
- host websockets
- own HTTP handlers
- own polling loops
### Not a health reporting sink
It can emit mutation outcomes and warnings to `HealthTelemetry`, but it should not own counters, logs, or dashboards.
## Mutation Policy
The coordinator should use a small number of policy classes of mutation behavior rather than ad hoc per-call decisions.
### Durable mutation
Updates authoritative state that should survive beyond the current session flow.
Examples:
- add/remove/move layer
- change selected shader on a layer
- update a parameter via UI or API
- load a stack preset
- reset a layer to defaults
Expected coordinator behavior:
1. validate the request
2. normalize the target and value if needed
3. update committed/durable state via `RuntimeStore`
4. request snapshot publication
5. request persistence according to policy
### Live committed mutation
Updates committed current-session state that should be treated as true until changed again, but may not need synchronous persistence.
Examples:
- a UI action that changes a parameter repeatedly while dragging
- a manual operator bypass toggle during live use
Expected coordinator behavior:
1. update committed live state
2. request snapshot publication
3. decide whether persistence should happen immediately, be debounced, or be deferred
### Transient overlay mutation
Affects output but should not masquerade as stored truth.
Examples:
- active OSC automation target
- short-lived trigger-driven visual automation state
Expected coordinator behavior:
1. validate the route and target parameter
2. classify the action as transient
3. update overlay state through the appropriate owner boundary
4. avoid persistence unless a separate commit policy is invoked
### Coordination-only mutation
A request that mainly exists to trigger a flow rather than edit value state.
Examples:
- request reload
- request publish-now
- request clear transient state on reset/rebuild
## Interaction With State Categories
This section restates the Phase 1 state model specifically from the coordinator's perspective.
### Persisted state
`RuntimeCoordinator` does not own persisted state, but it decides when persisted state should change.
Typical interaction:
- validate request
- call into `RuntimeStore`
- receive success/failure
- request persistence if policy says this mutation should be durable
### Committed live state
This is the coordinator's primary logical domain.
The coordinator is the policy owner of:
- current layer stack composition
- current selected shaders
- current bypass flags
- current operator-authored parameter values
`CommittedLiveState` is the physical owner for this current-session layer state. `RuntimeStore` persists or skips disk writes according to coordinator policy and remains the compatibility facade for existing mutation call shapes.
### Transient live overlay state
The coordinator defines the rules for transient state, but should not become the long-term storage owner for render-local transient data.
The expected split is:
- coordinator owns policy
- `ControlServices` may own short ingress-side queues and coalescing buffers
- `RenderEngine` owns render-local transient application state
- `VideoBackend` owns playout and device transient state
For OSC specifically, the coordinator should eventually decide:
- whether an automation change is transient-only
- whether it should later commit into committed live state
- what reset/reload actions invalidate it
Phase 5 sets the default settled OSC policy to session-only. `CommitOscParameterByControlKey(...)` updates committed session state through the store with persistence disabled, publishes ordinary mutation/state-change observations, and does not request a persistence write unless a future explicit policy opts into durable OSC commits.
The committed-live concept now has a physical owner, `CommittedLiveState`, plus a named read model, `CommittedLiveStateReadModel`. The coordinator remains the owner of whether a mutation should be durable or session-only, while `RuntimeStore` persists or skips disk writes according to that policy.
### Health and timing state
The coordinator may emit events like:
- mutation rejected
- reload requested
- preset load succeeded/failed
- transient state cleared because structure changed
But those are observations into `HealthTelemetry`, not coordinator-owned data.
## Proposed Interfaces
These are target-shape interfaces, not final signatures.
### Input-facing API
Core mutation entrypoints could look like:
```cpp
struct RuntimeMutationRequest;
struct RuntimeMutationResult;
struct ReloadRequest;
struct OverlayCommitRequest;
class RuntimeCoordinator
{
public:
RuntimeMutationResult ApplyMutation(const RuntimeMutationRequest& request);
RuntimeMutationResult ApplyAutomationTarget(const RuntimeMutationRequest& request);
RuntimeMutationResult ResetLayer(const std::string& layerId);
RuntimeMutationResult RequestReload(const ReloadRequest& request);
RuntimeMutationResult CommitOverlayState(const OverlayCommitRequest& request);
RuntimeMutationResult ClearTransientStateForScope(const RuntimeResetScope& scope);
};
```
The important point is not the exact names. It is that ingress systems send typed requests into one policy owner.
### Downstream collaborators
The coordinator likely needs collaborators conceptually equivalent to:
- `IRuntimeStore`
- `IRuntimeSnapshotProvider`
- `IHealthTelemetry`
- compatibility adapters only where older call shapes still need to be supported during migration
### Mutation result shape
A useful result structure should carry more than success/failure. It should support policy-driven downstream behavior without re-deriving the decision elsewhere.
Suggested fields:
- `accepted`
- `errorMessage`
- `stateChanged`
- `persistedStateChanged`
- `committedLiveStateChanged`
- `transientStateChanged`
- `snapshotPublicationRequired`
- `persistenceRequested`
- `renderResetScope`
- `telemetryNotes`
This prevents callers from guessing whether they need to reload, publish, or persist.
## Current Code Mapping
The current app does not have a separate coordinator class, but several existing code paths are clearly doing coordinator work.
### `OpenGLCompositeRuntimeControls.cpp`
Methods like:
- `AddLayer(...)`
- `RemoveLayer(...)`
- `MoveLayer(...)`
- `SetLayerBypass(...)`
- `SetLayerShader(...)`
- `UpdateLayerParameterJson(...)`
- `ResetLayerParameters(...)`
- `SaveStackPreset(...)`
- `LoadStackPreset(...)`
currently do this pattern:
1. call a host/store mutation directly
2. decide whether to call `ReloadShader(...)`
3. call `broadcastRuntimeState()`
See [OpenGLCompositeRuntimeControls.cpp](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/gl/composite/OpenGLCompositeRuntimeControls.cpp:1).
That "call host, then decide reload/broadcast policy" logic is a direct candidate for migration into `RuntimeCoordinator`.
### Previous `RuntimeHost`
`RuntimeHost` previously combined:
- mutation validation
- state mutation
- value normalization
- persistence writes
- render-state dirty marking
Examples from the old `RuntimeHost.cpp`:
- `AddLayer(...)`
- `SetLayerShader(...)`
- `UpdateLayerParameter(...)`
- `UpdateLayerParameterByControlKey(...)`
- `ApplyOscTargetByControlKey(...)`
- `ResetLayerParameters(...)`
- `LoadStackPreset(...)`
The target design is not to move all implementation in one step. It is to peel policy and orchestration decisions away first.
### `RuntimeServices`
Current OSC-specific flow in `RuntimeServices` includes:
- queueing updates
- applying pending updates
- queueing commits
- consuming completed commits
- clearing OSC state
See [RuntimeServices.h](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/control/RuntimeServices.h:46).
The coordinator should eventually own the rules for when these updates are transient, when they commit, and what reset/reload does to them, while `ControlServices` keeps only the ingress mechanics.
## Recommended Internal Model
The coordinator should remain small enough to reason about. A good target is to split its internal logic into policy-focused helpers rather than letting one class become another `RuntimeHost`.
Possible internal helper concepts:
- `LayerMutationPolicy`
- `ParameterMutationPolicy`
- `PresetMutationPolicy`
- `ReloadPolicy`
- `OverlayPolicy`
That can still be presented as one subsystem to the rest of the app, while keeping the implementation testable.
## Snapshot Publication Contract
The coordinator should never force callers to know whether a snapshot must be rebuilt. That policy should be owned here.
Examples:
- parameter changes require snapshot publication
- layer reorder requires snapshot publication
- shader swap requires snapshot publication and render-local rebuild work
- stack preset load requires snapshot publication and likely broader transient-state invalidation
- pure health/status changes do not require snapshot publication
This contract matters because current call sites often use coarse actions like `ReloadShader()` after structural edits. The coordinator should return a more precise outcome than "reload or not."
## Reload and Reset Policy
Reload and reset behavior has been a recurring source of edge cases in the current app, especially with shader feedback, temporal history, and OSC overlay state.
The coordinator should define explicit reset scopes such as:
- parameter-values-only reset
- committed-live-state reset for a layer
- transient-overlay reset for a layer
- render-local-history reset for a layer
- whole-stack structural reset
- reload-induced compatibility reset
That allows later phases to stop encoding reset behavior implicitly in UI handlers or render rebuild code.
Phase 5 has made this more concrete for OSC overlays: coordinator results now carry a named transient OSC invalidation request, with layer-scoped invalidation used for layer removal and manual parameter reset. The render/live-state owner still decides compatibility details, but callers no longer infer transient reset behavior from a generic boolean.
## Migration Plan From Current Code
The coordinator should be introduced incrementally.
### Step 1. Define request and result types
Introduce typed mutation request/result objects without changing most internals yet.
### Step 2. Wrap direct runtime mutations behind coordinator entrypoints
The first implementation could still delegate heavily into existing runtime mutation paths, but the call sites should stop deciding policy on their own.
For example, instead of:
1. `OpenGLComposite::AddLayer()`
2. direct layer-add mutation
3. `ReloadShader(true)`
4. `broadcastRuntimeState()`
the flow becomes:
1. `OpenGLComposite` or `ControlServices` creates a typed request
2. `RuntimeCoordinator::ApplyMutation(...)`
3. coordinator returns a result describing snapshot, reset, and persistence needs
4. composition root dispatches those downstream effects
### Step 3. Move validation and classification out of direct mutation helpers
Once coordinator entrypoints are stable, pull up:
- mutation classification
- reset/reload policy
- transient-versus-durable decisions
while leaving raw store operations in place.
### Step 4. Split storage and snapshot collaborators
Only after the coordinator is clearly owning policy should storage and snapshot responsibilities be split into real target subsystems.
## Key Risks
### Risk 1. Coordinator becomes a new god object
If the coordinator starts owning persistence details, status counters, or render reset mechanics directly, it will just recreate the current problem under a new name.
Mitigation:
- keep collaborators explicit
- keep request/result types narrow
- avoid direct dependencies on render or backend internals
### Risk 2. Call sites bypass coordinator during migration
If new code bypasses `RuntimeCoordinator` for convenience, the architecture will fork into two policy systems.
Mitigation:
- treat the coordinator as the required entrypoint for new non-render mutations
- add compatibility adapters rather than parallel mutation paths
### Risk 3. Too much policy stays implicit in return conventions
If callers still infer policy from "which method was called," the coordinator will not actually clarify the system.
Mitigation:
- return explicit mutation outcomes
- define reset and publication scopes as named concepts
### Risk 4. Transient-state ownership remains fuzzy
OSC overlay behavior, feedback invalidation, and reload compatibility can easily blur subsystem boundaries again.
Mitigation:
- coordinator owns classification rules
- subsystem owners retain storage ownership
- reset scopes are explicit
## Open Questions
- Should preset load/save stay synchronous through early migration, or should the coordinator always treat them as policy requests whose persistence effects may complete later?
- Should reload requests be modeled as a dedicated mutation class distinct from ordinary control mutations from the start?
- How much normalization of parameter values should remain in store-side helpers versus moving into coordinator policy helpers?
- Should transient overlay commit policy be global, or parameter-definition-driven for specific shader controls?
- What is the minimal reset-scope vocabulary needed to avoid hard-coding reload behavior in `RenderEngine` later?
## Short Version
`RuntimeCoordinator` is where the app decides what a valid change means.
It should:
- accept typed mutations from ingress systems
- validate and classify them
- update durable and committed state through `RuntimeStore`
- request render-facing publication through `RuntimeSnapshotProvider`
- request persistence when policy requires it
- define reset, reload, and transient-overlay rules
It should not:
- parse transport payloads
- own GL work
- own device callbacks
- write files directly
- become a replacement monolith for every kind of state

View File

@@ -0,0 +1,476 @@
# RuntimeSnapshotProvider Subsystem Design
This document expands the `RuntimeSnapshotProvider` subsystem from [PHASE_1_SUBSYSTEM_BOUNDARIES_DESIGN.md](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/docs/PHASE_1_SUBSYSTEM_BOUNDARIES_DESIGN.md) into a concrete subsystem design.
The goal of `RuntimeSnapshotProvider` is to separate render-facing state publication from both runtime mutation policy and durable storage. In the target architecture, render should consume published snapshots rather than reaching into `RuntimeStore` or lock-protected live objects directly.
## Purpose
`RuntimeSnapshotProvider` is the boundary between runtime-owned state and render-consumable state.
It exists to solve three problems that Phase 1 pulled apart:
- render state was built directly out of `RuntimeHost` under a shared mutex
- render read and refreshed partially mutable cached layer state in more than one place
- state publication, state versioning, and dynamic frame-field refresh need explicit ownership
Before the Phase 1 runtime split, the closest behavior lived in:
- `RuntimeHost::GetLayerRenderStates(...)`
- `RuntimeHost::TryGetLayerRenderStates(...)`
- `RuntimeHost::TryRefreshCachedLayerStates(...)`
- `RuntimeHost::RefreshDynamicRenderStateFields(...)`
- `RuntimeHost::BuildLayerRenderStatesLocked(...)`
- the render-side cache usage in [OpenGLComposite.cpp](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/gl/composite/OpenGLComposite.cpp:589)
`RuntimeSnapshotProvider` has absorbed that responsibility in a cleaner and more publish-oriented way.
## Responsibilities
`RuntimeSnapshotProvider` is responsible for:
- publishing stable, versioned snapshots that can be consumed without large shared mutable locks
- giving `RenderEngine` a cheap read path for the latest committed snapshot
- making snapshot invalidation and publication rules explicit
`RenderSnapshotBuilder` is responsible for:
- building render-facing snapshots from the committed-live read model and package/runtime metadata supplied by `RuntimeStore`
- separating structural snapshot changes from dynamic frame fields
- translating runtime layer state into render-ready layer descriptors
- attaching immutable or near-immutable shader/package-derived data needed by render
- maintaining render snapshot version counters and frame advancement
It is not responsible for:
- deciding whether a mutation is valid
- classifying a change as transient versus durable
- directly accepting OSC/UI/file-watch requests
- disk persistence
- GL resource allocation
- shader compilation execution
- render-local transient overlays such as live OSC overlay state, temporal history textures, or feedback textures
## Design Principles
### Render consumes published state, not store internals
The render side should never need to walk `RuntimeStore` structures directly or perform per-frame reconstruction under the store lock.
### Structural data and dynamic frame fields are different classes of data
The layer stack, shader ids, parameter definitions, texture assets, font assets, feedback declarations, and temporal requirements change relatively infrequently. Frame count, wall time, UTC time, and similar values change every frame.
`RuntimeSnapshotProvider` should publish structural snapshots and provide a separate mechanism for frame-local dynamic enrichment, rather than rebuilding everything for every frame.
### Snapshot reads should be cheap and explicit
The render side should be able to say:
- give me the latest published snapshot
- tell me whether the structural snapshot version changed
- apply dynamic frame fields for this frame
without having to infer cache validity from multiple host-owned counters and fallback lock behavior.
### Published shape should be stable
The shape of render-facing layer state should remain consistent across phases even if the underlying store or coordination model changes.
## Snapshot Inputs
`RenderSnapshotBuilder` should build from a read-oriented runtime view, not from direct mutation calls. `RuntimeSnapshotProvider` should consume the builder's output and own publication/cache behavior.
That view now includes:
- committed live layer state from `CommittedLiveStateReadModel`
- package and manifest metadata supplied through `RuntimeStore`
- durable runtime configuration needed to describe render-facing dimensions and defaults
The important Phase 1 rule is not "the provider always reads one specific object." It is:
- the builder consumes read-oriented committed runtime state
- the provider consumes builder-published render snapshot data
- the provider does not own mutation policy
- render consumes the provider's published output instead of reaching back into whichever runtime object currently stores the truth
## Snapshot Model
The subsystem should publish a render snapshot object rather than loose vectors and ad hoc version getters.
Suggested top-level shape:
```cpp
struct RuntimeRenderSnapshot
{
uint64_t snapshotVersion = 0;
uint64_t structureVersion = 0;
uint64_t parameterVersion = 0;
uint64_t packageVersion = 0;
uint64_t publicationSequence = 0;
unsigned inputWidth = 0;
unsigned inputHeight = 0;
unsigned outputWidth = 0;
unsigned outputHeight = 0;
std::vector<RuntimeRenderLayerSnapshot> layers;
};
```
Suggested per-layer shape:
```cpp
struct RuntimeRenderLayerSnapshot
{
std::string layerId;
std::string shaderId;
std::string shaderName;
double mixAmount = 1.0;
double bypass = 0.0;
std::vector<ShaderParameterDefinition> parameterDefinitions;
std::map<std::string, ShaderParameterValue> parameterValues;
std::vector<ShaderTextureAsset> textureAssets;
std::vector<ShaderFontAsset> fontAssets;
bool isTemporal = false;
TemporalHistorySource temporalHistorySource = TemporalHistorySource::None;
unsigned requestedTemporalHistoryLength = 0;
unsigned effectiveTemporalHistoryLength = 0;
FeedbackSettings feedback;
};
```
This is intentionally close to todays [RuntimeRenderState](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/shader/ShaderTypes.h:134), but split so dynamic fields are not embedded in the published structural snapshot.
Suggested per-frame dynamic supplement:
```cpp
struct RuntimeRenderFrameContext
{
double timeSeconds = 0.0;
double utcTimeSeconds = 0.0;
double utcOffsetSeconds = 0.0;
double startupRandom = 0.0;
double frameCount = 0.0;
};
```
`RenderEngine` can combine `RuntimeRenderSnapshot` and `RuntimeRenderFrameContext` into its final frame-local render input without forcing snapshot republish every frame.
## Publication Rules
The provider should publish a new structural snapshot when any render-relevant structural or committed-live field changes, including:
- layer add/remove/reorder
- shader id change on a layer
- layer bypass change
- parameter value change that is part of committed live state
- shader package metadata refresh that changes parameter definitions, assets, temporal declarations, or feedback declarations
- input or output dimensions that change render-facing layer interpretation
- stack preset load that changes any render-facing state
The provider should not publish a new structural snapshot just because:
- time advanced by one frame
- frame count increased
- preview cadence changed
- render-local transient overlay state changed
- temporal history or feedback textures changed
- device playout queue state changed
That distinction matters because the current model effectively mixes structural publication with frame-local refresh and lock-driven fallback logic.
## Versioning Model
The provider should own explicit version domains rather than exposing only host-wide counters.
Recommended version domains:
- `structureVersion`
- changes when the layer graph or shader/package-derived structure changes
- `parameterVersion`
- changes when committed parameter or bypass values change
- `packageVersion`
- changes when shader manifests or package-derived metadata relevant to render changes
- `snapshotVersion`
- a composed version for consumers that only need a single fast invalidation key
- `publicationSequence`
- monotonic sequence number for diagnostics and telemetry
Recommended rules:
- `snapshotVersion` changes whenever any render-visible aspect of the structural snapshot changes
- `structureVersion` should not change for pure parameter edits
- `parameterVersion` should not change for time-only updates
- dynamic frame context should not require any version change
This makes later cache policy much cleaner:
- shader rebuild decisions can key off structure/package changes
- parameter buffer refresh can key off parameter changes
- frame-local updates can ignore snapshot publication entirely
## Snapshot Read Rules
The target read contract for `RenderEngine` should be:
1. acquire the latest published snapshot atomically or under a very small provider-owned read lock
2. compare relevant versions with the render-side cached state
3. if unchanged, reuse render-local compiled/cached resources
4. if changed, rebuild only the portions implied by the changed version domains
5. attach the current `RuntimeRenderFrameContext` for the frame being rendered
Important rule:
- `RenderEngine` should never partially mutate the provider's published snapshot in place.
The old `TryRefreshCachedLayerStates(...)` host path is gone. The remaining dynamic refresh is explicit: `RuntimeSnapshotProvider::RefreshDynamicRenderStateFields(...)` updates frame-local fields on render-owned copies, while published snapshot structure and committed parameter data stay behind the provider boundary.
## Render-Facing Data Shape Rules
The published snapshot should contain exactly the data render needs to interpret a layer, but not render-local execution artifacts.
Include:
- layer identity
- shader identity and display name
- parameter definitions
- committed parameter values
- bypass and mix flags needed for layer evaluation
- texture and font asset declarations
- temporal settings
- feedback settings
- input/output dimensions when they affect shader configuration or resource interpretation
Do not include:
- GL object ids
- framebuffer handles
- compiled shader programs
- live texture bindings resolved to hardware units
- temporal history texture state
- feedback buffer contents
- queued OSC overlays
- queued input frames
- preview frame caches
- DeckLink buffer handles
This line is important because current `RuntimeRenderState` is close to render-ready data, but the subsystem contract should stop before actual device or GL execution artifacts.
## Proposed Public Interface
Suggested interface shape:
```cpp
class IRuntimeSnapshotProvider
{
public:
virtual ~IRuntimeSnapshotProvider() = default;
virtual RuntimeRenderSnapshot BuildSnapshot(
const RuntimeStoreView& storeView,
const SnapshotBuildOptions& options) const = 0;
virtual void PublishSnapshot(RuntimeRenderSnapshot snapshot) = 0;
virtual std::shared_ptr<const RuntimeRenderSnapshot> GetLatestSnapshot() const = 0;
virtual uint64_t GetSnapshotVersion() const = 0;
virtual RuntimeRenderFrameContext BuildFrameContext() const = 0;
};
```
Likely supporting methods:
- `BuildLayerSnapshot(...)`
- `BuildFrameContext(...)`
- `ComputeSnapshotVersion(...)`
- `DidStructureChange(...)`
- `DidParametersChange(...)`
- `PublishIfChanged(...)`
Notes:
- `GetLatestSnapshot()` should ideally return a shared immutable snapshot pointer or equivalent stable handle
- `BuildFrameContext()` may remain provider-owned or later move behind a clock/timing helper if that subsystem becomes more explicit
- publication should be initiated by `RuntimeCoordinator`, not by render
## Relationship to Other Subsystems
### `RuntimeStore`
`RenderSnapshotBuilder` depends on store-owned durable metadata and the committed-live read model exposed through store-facing read APIs. `RuntimeSnapshotProvider` depends on the builder rather than reaching into store internals directly.
Committed session layer state now lives in `CommittedLiveState`; `RuntimeStore` remains the facade that combines that read model with package metadata and persistence-owned data for snapshot publication.
Neither the builder nor provider should mutate the store directly.
### `RuntimeCoordinator`
`RuntimeCoordinator` decides when a mutation requires snapshot republish.
The provider should not reclassify policy. It should only:
- build
- compare
- publish
based on the change request it is asked to materialize.
### `RenderEngine`
`RenderEngine` is the main consumer.
It should:
- read the latest published snapshot
- treat that snapshot as immutable
- derive render-local artifacts from it
- keep frame-local overlays and history outside the provider
### `HealthTelemetry`
The provider should emit:
- snapshot publication counts
- snapshot build duration
- version bump reason categories
- publication suppression counts when no effective change occurred
- warning states if snapshot build repeatedly fails
This is especially important while migrating away from the current lock/fallback model.
## Current Code Mapping
The current runtime path is:
1. get latest published snapshot from provider
2. compare snapshot versions produced by `RenderSnapshotBuilder`
3. rebuild through `RenderSnapshotBuilder` only if needed
4. apply render-local overlay state
5. attach frame context
That replaced the old mixed lock/cache/fallback flow that lived around [OpenGLComposite.cpp](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/gl/composite/OpenGLComposite.cpp:589).
`RenderSnapshotBuilder` now owns:
- layer render-state construction
- render-facing translation of committed live state plus package metadata
- explicit version composition for render-visible state
- dynamic frame-field refresh for render-owned copies
`RuntimeSnapshotProvider` now owns:
- published snapshot cache ownership
- version matching for already-published snapshots
- publication events and snapshot publish observations
## Migration Plan
### Step 1: Introduce provider types without changing behavior
- define `RuntimeRenderSnapshot`, `RuntimeRenderLayerSnapshot`, and `RuntimeRenderFrameContext`
- initially implement provider methods as thin wrappers over existing behavior
- completed: replace the temporary `RuntimeHost` backing source with `RenderSnapshotBuilder`
### Step 2: Route render reads through the provider
- replace direct host/store layer-state reads with provider snapshot reads
- preserve current version behavior first, even if internally bridged to existing counters
### Step 3: Separate structural publication from frame context
- stop rebuilding structural layer state just to refresh time and frame values
- let render request frame context separately each frame
### Step 4: Remove mutable snapshot refresh paths
- completed: retire the old `TryRefreshCachedLayerStates(...)` host path
- publish new snapshots for committed parameter changes instead of mutating published snapshot structure in place
### Step 5: Move publication triggering fully behind `RuntimeCoordinator`
- no render-driven snapshot rebuilding
- coordinator requests publication after successful committed mutations and reloads
## Risks
### Risk: snapshot copies become expensive
Publishing whole snapshots on every parameter commit could be expensive if the layer stack grows.
Mitigation:
- use immutable shared snapshots with replace-on-publish semantics
- consider per-layer structural sharing later if real profiles justify it
- avoid republishing for frame-local time-only changes
### Risk: unclear boundary between committed state and transient overlay state
If overlays are accidentally folded into the published snapshot, the provider will recreate the coupling that the subsystem split is supposed to remove.
Mitigation:
- keep overlays render-local or coordinator-owned transient state
- document that snapshots represent committed render-facing truth, not in-flight automation state
### Risk: version domains are under-specified
If version rules are not crisp, render may still over-rebuild or miss needed updates.
Mitigation:
- make version bump reasons explicit
- log version-domain changes during migration
- add tests around parameter-only, structure-only, and package-only changes
### Risk: snapshot publication is treated as a background convenience rather than a core contract
If code keeps reaching around the provider into the store, the architecture will remain half-split.
Mitigation:
- treat provider publication as the only supported render-facing state publication path
- convert direct host/store render-state methods into adapters, then remove them
## Testing Strategy
The provider should be testable without GL or hardware.
Recommended tests:
- snapshot build from a sample layer stack
- parameter-only mutation increments `parameterVersion` but not `structureVersion`
- layer reorder increments `structureVersion`
- shader manifest change increments `packageVersion`
- frame context changes over time without forcing `snapshotVersion` changes
- repeated publish with no effective change suppresses unnecessary version bumps
- feedback and temporal declarations are preserved correctly in published layer snapshots
## Open Questions
- Should output dimensions live inside the top-level snapshot only, or also be copied into each layer snapshot for compatibility with current code paths?
- Should package-derived compile-ready pass source metadata eventually be published by this provider, or remain a separate build artifact pipeline?
- Is `BuildFrameContext()` part of the provider long-term, or should timing/clock publication become its own helper owned adjacent to `HealthTelemetry`?
- Do parameter-only changes always require full snapshot republish, or should later phases add more granular per-layer publication handles?
- Should the provider own input signal dimensions directly, or should those come from a backend-published runtime environment view supplied during build?
## Completion Criteria For This Subsystem
`RuntimeSnapshotProvider` can be considered architecturally in place once:
- render no longer reads `RuntimeStore` or legacy host render state directly
- render consumes published snapshot handles rather than rebuilding layer vectors from host state
- dynamic frame fields are supplied separately from structural snapshot publication
- snapshot version domains are explicit and observable
- transient overlays remain outside the published snapshot contract
## Short Version
`RuntimeSnapshotProvider` should become the single place that turns committed runtime state into render-consumable published snapshots.
Its contract is:
- build from store-owned state
- publish immutable or near-immutable render snapshots; the current implementation keeps the last matching versioned snapshot in `RuntimeSnapshotProvider`
- version them explicitly
- keep frame-local timing separate
- give render a cheap, lock-light read path
If that boundary is held, later phases can isolate render timing and decouple playout without inventing a second render-state authority.

View File

@@ -0,0 +1,590 @@
# RuntimeStore Subsystem Design
This document expands the `RuntimeStore` portion of [PHASE_1_SUBSYSTEM_BOUNDARIES_DESIGN.md](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/docs/PHASE_1_SUBSYSTEM_BOUNDARIES_DESIGN.md) into a subsystem-specific design note.
The purpose of `RuntimeStore` is to give the Phase 1 target architecture one clear home for durable runtime data. Before the Phase 1 runtime split, that responsibility was spread through `RuntimeHost`, where persistence, mutation entrypoints, render-state building, shader metadata access, and status reporting all shared the same object and lock domain. `RuntimeStore` is the design boundary that separates "what the app knows and saves" from "how the app decides to mutate it" and "how rendering consumes it."
## Role In The Phase 1 Architecture
Within the Phase 1 subsystem model, `RuntimeStore` is the durable data authority.
It exists to answer questions like:
- what runtime configuration is currently loaded
- what the saved layer stack structure is
- what the saved parameter values are
- what stack presets exist and what they contain
- what package and manifest metadata is available for validation and snapshot building
It should not answer questions like:
- should this control mutation be allowed
- should this OSC value be treated as transient or persisted
- how should the render thread consume state
- when should output frames be scheduled
- what warnings should be shown to the operator
That policy belongs elsewhere:
- mutation policy: `RuntimeCoordinator`
- render-facing publication: `RuntimeSnapshotProvider`
- hardware timing: `VideoBackend`
- operational visibility: `HealthTelemetry`
## Design Goals
`RuntimeStore` should optimize for:
- explicit ownership of durable runtime data
- predictable disk-backed load and save behavior
- minimal knowledge of GL, callbacks, or live playout timing
- stable read models for validation and snapshot building
- a clean seam for introducing debounced or asynchronous persistence later
- testability without GPU or DeckLink dependencies
## Responsibilities
`RuntimeStore` owns persisted and operator-authored state.
Primary responsibilities:
- load runtime host configuration from disk
- load saved runtime state from disk
- save runtime state snapshots to disk
- own the stored layer stack model
- own persisted parameter values and bypass flags
- own stack preset serialization and deserialization
- own package/manifest metadata needed across renders and reloads
- expose query/read APIs over stored state
- expose write APIs for coordinator-approved durable mutations
- normalize or repair stored data at load boundaries when necessary
Secondary responsibilities that still fit here:
- path resolution for runtime state and preset files
- preset name normalization/file-stem safety
- compatibility handling for older saved-state schemas
- default seeding of initial persistent state when no saved runtime exists
## Non-Responsibilities
`RuntimeStore` must not become a general convenience layer again.
It does not own:
- render-thread timing
- GL objects or resource lifetime
- shader compilation orchestration
- render-local transient state such as temporal history, feedback buffers, preview caches, or playout queues
- OSC smoothing, coalescing, or overlay application
- websocket broadcast policy
- REST or OSC ingress handling
- device callbacks, queue-depth policy, or preroll policy
- app-wide health aggregation
It also should not directly decide:
- whether a mutation is valid in policy terms
- whether a change should persist immediately, eventually, or not at all
- when a new render snapshot should be published
- whether a reload should be treated as config-only, package-only, or render-affecting
Those are coordinator concerns, not store concerns.
## State Ownership
`RuntimeStore` should own the following state categories.
Phase 5 names this boundary in code through `RuntimeStateLayerModel`: persisted layer stack data, saved parameter values, and stack presets are classified as base persisted state. Operator/session values are owned by `CommittedLiveState`; their mutation policy is committed-live policy owned by the coordinator, not durable-store policy by default.
Phase 5 also adds `CommittedLiveState` as the physical owner of current session/operator layer state and `CommittedLiveStateReadModel` as the named read boundary for render snapshot publication. `RuntimeStore` still owns file IO, config, package metadata, preset persistence, and persistence requests, but it delegates current-session layer mutations to `CommittedLiveState`.
### Runtime Configuration
Examples:
- server/control ports
- OSC bind address
- OSC smoothing defaults
- runtime paths and directory configuration
- any host-side configuration loaded from `config/runtime-host.json`
This data is durable, file-backed, and not inherently render-local.
### Persistent Layer Stack State
Examples:
- ordered layer list
- stable layer ids
- selected shader id per layer
- bypass state
- persisted parameter values
This is the stored "official" layer model, not a render-thread working copy.
### Stack Presets
Examples:
- preset names
- serialized saved layer stacks under `runtime/stack_presets`
Preset files are durable artifacts and should remain in the store domain even if later phases add async writing.
### Shader/Package Metadata Needed As Durable Reference Data
Examples:
- discovered shader package manifests
- parameter definitions used for validation/default restoration
- manifest-level capability metadata such as temporal history and feedback declarations
- package ordering that should survive across reloads
Important distinction:
- manifest and package metadata belongs here
- render-ready compiled programs and GPU resources do not
### Load-Time Compatibility/Repair State
Examples:
- schema version adaptation
- default value filling for missing parameters
- removal or migration of layers that reference missing packages
- preset compatibility cleanup
This should be treated as store hygiene during ingest, not runtime mutation policy.
## Data Model Boundaries
`RuntimeStore` should present data in durable-model terms rather than live-render terms.
Core model groupings:
- `RuntimeConfigModel`
- `PersistentLayerStackModel`
- `LayerStoredState`
- `StoredParameterValue`
- `StackPresetModel`
- `ShaderPackageCatalog` or equivalent durable package registry view
The exact C++ types may differ from these names, but the boundary should hold:
- store models describe durable intent
- snapshot models describe render consumption
That means `RuntimeStore` should not expose render-optimized structures such as `RuntimeRenderState` directly as its primary interface.
## Interface Shape
The Phase 1 architecture doc already sketches the high-level interface. This section expands it.
### Load / Save Interface
Expected responsibilities:
- `LoadConfig()`
- `LoadPersistentState()`
- `BuildPersistentStateSnapshot(...)`
- `RequestPersistence(...)`
- `LoadStackPreset(...)`
- `SaveStackPreset(...)`
- `GetStackPresetNames()`
Design notes:
- `Load*` operations should parse and normalize external file content into durable in-memory models.
- `Save*` operations should serialize durable models without needing render or control subsystem context.
- debounce/background writing wraps these operations rather than redefining store ownership
### Read Interface
Expected responsibilities:
- `GetRuntimeConfig()`
- `GetStoredLayerStack()`
- `FindStoredLayer(...)`
- `GetShaderPackageCatalog()`
- `GetStackPresetNames()`
- `BuildPersistenceSnapshot()` or equivalent stable serialization input
Design notes:
- read APIs should support coordinator validation and snapshot building
- read APIs should avoid exposing raw mutable internals across subsystem boundaries
- stable read snapshots from the store are fine; render snapshots are still the snapshot provider's job
### Write Interface
Expected responsibilities:
- `SetStoredLayerStack(...)`
- `ReplaceStoredLayer(...)`
- `SetStoredParameterValue(...)`
- `SetStoredBypassState(...)`
- `SetStoredShaderSelection(...)`
- `ReplaceShaderPackageCatalog(...)`
Design notes:
- writes should assume the coordinator already decided the mutation is allowed
- store APIs may still enforce structural invariants and shape correctness
- writes should not contain ingress-specific policy like OSC smoothing or UI throttling
### Normalization / Validation-Support Interface
Expected responsibilities:
- `NormalizeLoadedState(...)`
- `EnsureStoredDefaults(...)`
- `MakeSafePresetFileStem(...)`
- package lookup helpers for parameter-definition queries
Design notes:
- lightweight structure and schema validation belongs here
- policy validation belongs in the coordinator
- render compatibility translation belongs in the snapshot provider
## Concurrency Expectations
`RuntimeStore` should be designed as a shared data authority, but not as the app's global lock for everything.
Phase 1 design expectations:
- coordinator-driven writes may still be synchronized internally
- read APIs should be safe for coordinator and snapshot-provider use
- render should not directly take a large mutable store lock in the target architecture
This implies:
- `RuntimeStore` may keep an internal mutex during migration
- that mutex should protect durable models only
- render-facing consumers should eventually read via `RuntimeSnapshotProvider`, not by reaching into the store
One of the main goals here is avoiding the old situation where runtime lock scope effectively mixed:
- persistent state
- status reporting
- render-state caches
- timing stats
- reload flags
`RuntimeStore` should sharply narrow that scope.
## Dependency Rules
Per the Phase 1 subsystem design, `RuntimeStore` should sit low in the dependency graph.
Allowed inbound dependencies:
- `RuntimeCoordinator -> RuntimeStore`
- `RenderSnapshotBuilder -> RuntimeStore`
- temporary migration shims from `ControlServices` only where explicitly tolerated
Allowed outbound dependencies:
- file/serialization helpers
- package manifest parsing helpers
- pure utility types
Not allowed:
- `RuntimeStore -> RenderEngine`
- `RuntimeStore -> VideoBackend`
- `RuntimeStore -> ControlServices`
- `RuntimeStore -> HealthTelemetry` for behavior control
The store may emit errors or return result objects, but it should not coordinate the rest of the system directly.
## Current Code Mapping
Before the Phase 1 runtime split, `RuntimeHost` contained many responsibilities that needed to move into `RuntimeStore` or adjacent runtime collaborators.
Previous code paths:
- config load:
- `RuntimeHost.cpp`
- persistent state load:
- `RuntimeHost.cpp`
- persistent state save:
- `RuntimeHost.cpp`
- preset save/load:
- `RuntimeHost.cpp`
- `RuntimeHost.cpp`
- state serialization helpers:
- `RuntimeHost.cpp`
- `RuntimeHost.cpp`
- `RuntimeHost.cpp`
- path and file helpers:
- `RuntimeHost.cpp`
- `RuntimeHost.cpp`
- `RuntimeHost.cpp`
Durable-state mutation entrypoints that previously lived on `RuntimeHost` but conceptually split between coordinator and store:
- layer stack edits:
- `AddLayer`
- `RemoveLayer`
- `MoveLayer`
- `MoveLayerToIndex`
- committed-state edits:
- `SetLayerBypass`
- `SetLayerShader`
- `UpdateLayerParameter`
- `ResetLayerParameters`
The target split should be:
- validation/policy/orchestration -> `RuntimeCoordinator`
- durable state write application -> `RuntimeStore`
Methods that were intentionally not moved into `RuntimeStore` because they belong under other runtime subsystems:
- render-state building and caching:
- `GetLayerRenderStates`
- `TryRefreshCachedLayerStates`
- `BuildLayerRenderStatesLocked`
- status/timing reporting:
- `SetSignalStatus`
- `SetPerformanceStats`
- `SetFramePacingStats`
- `AdvanceFrame`
- live reload flags/polling shell:
- `PollFileChanges`
- `ManualReloadRequested`
- `ClearReloadRequest`
Those belong under other target subsystems.
## Proposed Internal Subcomponents
`RuntimeStore` does not need to be one monolithic class forever. A practical internal shape would be:
- `RuntimeConfigStore`
- runtime host config load and resolved paths
The current codebase has completed this part of the split: `RuntimeConfigStore` owns config parsing, path resolution, configured ports/formats, runtime roots, and shader compiler paths, while `RuntimeStore` exposes compatibility-shaped delegates for existing callers.
- `CommittedLiveState`
- current committed/session layer stack and parameter values
- layer CRUD/reorder and shader selection for the running session
- committed-live read model for snapshot publication
- `LayerStackStore`
- backing layer stack mechanics used by committed-live state
- layer CRUD/reorder and shader selection helpers
- stack preset value serialization/load helpers
- `RuntimeStatePresenter` / `RuntimeStateJson`
- runtime-state JSON assembly
- layer-stack presentation serialization
- `RenderSnapshotBuilder`
- render-state assembly and parameter refresh
- dynamic frame-field refresh and render snapshot version counters
- `ShaderPackageCatalog`
- durable manifest/package metadata
- shader package scanning, status/order/lookup, and asset/source change comparison
- `PersistenceWriter` helper
- synchronous at first, async/debounced later
The current codebase has completed the committed-live split: `CommittedLiveState` owns current committed/session layer state using `LayerStackStore` backing mechanics. `RuntimeStore` keeps file IO, package metadata, persistence serialization, persistence requests, preset file access, and facade methods for existing callers.
The current codebase has completed the render snapshot split: `RenderSnapshotBuilder` owns render-state assembly, cached parameter refresh, dynamic frame-field refresh, and render snapshot versions. `RuntimeSnapshotProvider` depends on this builder rather than on `RuntimeStore` friendship.
The current codebase has also completed the presentation split: `RuntimeStatePresenter` owns top-level runtime-state JSON assembly, while `RuntimeStateJson` owns the layer-stack and parameter presentation shape used by runtime state clients.
The current codebase has also completed the package split: `ShaderPackageCatalog` owns package scanning and registry comparison, while `RuntimeStore` uses it to keep layer state valid and to build compatibility read models.
These can still be presented through one subsystem façade during migration.
## Persistence Model
The store should treat persistence as durable snapshot management, not incremental side-effect spraying.
Target behavior:
- in-memory durable models are updated first
- serialization snapshots are built from those models
- save requests persist a coherent snapshot
This matters because earlier code called persistent-state saves directly from mutation paths. Phase 6 removed that pressure point: accepted durable mutations now publish persistence requests, and `RuntimeStore::RequestPersistence(...)` builds a coherent snapshot for the background writer.
The Phase 1 design for `RuntimeStore` should therefore assume:
- store ownership of serialization remains
- persistence requests, not mutation methods, are the durable write boundary
Phase 6 added that background snapshot writer underneath this subsystem, while keeping the durable model here.
## Migration Plan From Current Code
The safest migration path is to extract responsibilities by interface, not by big-bang rename.
### Step 1: Introduce The `RuntimeStore` Name And Facade
Create a facade interface for the durable-data parts that used to live in `RuntimeHost`.
Initial likely contents:
- config load/save access
- persistent layer-stack get/set access
- preset load/save access
- package catalog read access
This stage is complete: `RuntimeStore` owns its durable/session backing fields directly rather than wrapping a `RuntimeHost` object.
### Step 2: Move Pure Persistence Helpers First
Low-risk extractions:
- path resolution helpers
- file read/write helpers
- preset enumeration and serialization helpers
- persistent-state serialization/deserialization helpers
These have relatively low coupling to GL and backend timing.
### Step 3: Split Durable Models From Render Cache/Status Fields
Move out or conceptually separate:
- `mPersistentState`
- runtime config fields
- preset roots and runtime roots
- package catalog/order metadata
From fields that should stay elsewhere:
- render-state dirty flags and caches
- status/timing counters
- reload flags
This is one of the most important separations in the whole program.
### Step 4: Route Durable Mutations Through Coordinator-Owned Policy
Once the coordinator exists, `RuntimeStore` write calls should become lower-level and less policy-rich.
Examples:
- `SetStoredParameterValue(...)` rather than `ApplyOscTargetByControlKey(...)`
- `ReplaceStoredLayerStack(...)` rather than `LoadStackPreset(...)` directly mutating every downstream concern
### Step 5: Keep Render Off The Store
As `RuntimeSnapshotProvider` arrives, render should stop reading store internals directly.
That is the moment where `RuntimeStore` becomes a proper durable authority instead of a shared mutable app center.
## Risks
### 1. Recreating `RuntimeHost` Under A New Name
The biggest risk is calling something `RuntimeStore` while leaving policy, status, and render-cache behavior attached.
Guardrail:
- only durable data and store hygiene belong here
### 2. Letting Validation Drift Into Persistence
Store-level shape validation is appropriate. High-level mutation policy is not.
Risk examples:
- store decides whether OSC should persist
- store decides whether a layer reorder should trigger snapshot publication
- store decides whether a reload is render-only or package-affecting
Those are coordinator decisions.
### 3. Overexposing Mutable Internals
If callers keep direct mutable access to the underlying vectors/maps, the subsystem boundary will exist only on paper.
Guardrail:
- prefer controlled write methods and stable read models
### 4. Coupling Package Metadata Too Tightly To Compile Outputs
Package manifest and parameter-definition metadata belongs here. Compiled program state does not.
Guardrail:
- keep compile products and GPU artifacts out of the store
### 5. Using The Store Lock As A Global Synchronization Shortcut
This would recreate timing and contention issues in a new form.
Guardrail:
- store locking protects durable models only
- render synchronization must happen through snapshots, not by sharing the store lock
## Open Questions
### 1. How Much Shader Package Data Should Live Here?
Clear yes:
- manifest metadata
- parameter definitions
- package discovery/order information
Still open:
- whether compile-ready transformed sources belong here or in a later build-focused subsystem
Current recommendation:
- keep only durable reference/package metadata here
### 2. Should Preset Application Be A Store Operation Or A Coordinator Operation?
The file load and preset parse clearly belong here.
The policy question of how a loaded preset affects live state, snapshot publication, overlays, and notifications belongs in the coordinator.
Current recommendation:
- `RuntimeStore` loads preset content
- `RuntimeCoordinator` decides how to apply it
### 3. How Early Should Async Persistence Land?
Phase 1 does not require it, but the store design should not block it.
Current recommendation:
- keep synchronous save semantics initially if needed
- shape the interfaces so a background writer can be introduced without changing subsystem ownership
## Success Criteria For This Subsystem
`RuntimeStore` can be considered well-defined once the codebase can say, without ambiguity:
- all durable runtime config and saved layer data has one authoritative home
- stack presets are owned by that same durable-data subsystem
- render does not depend on store internals directly
- timing/status/reporting state is no longer mixed into the same subsystem
- persistence ownership is clear even before async persistence is introduced
## Short Version
`RuntimeStore` is the subsystem that should answer:
- what durable runtime data exists
- what saved layer stack and parameters exist
- what presets and package metadata exist
- how that durable data is loaded and serialized
It should not answer:
- whether a mutation should happen
- how rendering should consume state
- how hardware pacing should work
- what health warnings should be emitted
If this boundary holds, later phases can continue without recreating the old coupling under a different class name.

View File

@@ -0,0 +1,694 @@
# VideoBackend Subsystem Design
This note defines the target design for the `VideoBackend` subsystem introduced in [PHASE_1_SUBSYSTEM_BOUNDARIES_DESIGN.md](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/docs/PHASE_1_SUBSYSTEM_BOUNDARIES_DESIGN.md).
It focuses on input/output device lifecycle, pacing, buffering, and recovery policy for live video I/O. It does not redefine the whole app architecture. Its job is to make the backend boundary concrete enough that later phases can move current DeckLink and bridge code toward one clear ownership model.
## Purpose
`VideoBackend` is the hardware-facing timing subsystem.
It owns:
- video device discovery and capability inspection
- input and output device configuration
- input callback handling
- output callback handling
- buffer-pool ownership for device-facing frames
- playout headroom policy
- queueing and pacing policy between render and hardware
- input signal presence tracking
- backend lifecycle and degraded-state transitions
It does not own:
- GL contexts
- frame composition
- shader execution
- persistence
- control mutation policy
- render snapshot publication
The core rule is:
- `RenderEngine` produces frames
- `VideoBackend` moves those frames to and from hardware at the right cadence
## Why This Subsystem Exists
Today the boundary between render and hardware pacing is still too blurred.
The main current pressure points are:
- `OpenGLVideoIOBridge` still performs render-facing work inside the output completion callback:
- [OpenGLVideoIOBridge.cpp](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/gl/pipeline/OpenGLVideoIOBridge.cpp:83)
- `DeckLinkSession` owns device setup, mutable output frame pools, and schedule timing in one class:
- [DeckLinkSession.h](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/videoio/decklink/DeckLinkSession.h:13)
- [DeckLinkSession.cpp](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/videoio/decklink/DeckLinkSession.cpp:289)
- the output scheduler currently reacts to late and dropped frames with a fixed skip policy:
- [VideoPlayoutScheduler.cpp](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/videoio/VideoPlayoutScheduler.cpp:26)
- the current output frame pool and preroll depth are not sourced from one policy object:
- `DeckLinkSession::ConfigureOutput()` creates `10` mutable output frames
- `kPrerollFrameCount` is currently `12`
Those overlaps make latency, buffering, and recovery behavior harder to reason about.
## Subsystem Responsibilities
`VideoBackend` should own the following responsibilities explicitly.
### 1. Device Discovery and Capability Reporting
The subsystem should:
- discover available input and output devices
- choose the configured input/output pair
- inspect mode support and pixel-format support
- expose capability facts needed by higher layers
Examples:
- input present or absent
- output present or absent
- model name
- keyer support
- internal/external keying availability
- supported pixel formats for the configured mode
- input/output frame sizes
This work is currently mostly in:
- [DeckLinkSession.cpp](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/videoio/decklink/DeckLinkSession.cpp:76)
### 2. Input Lifecycle and Input Callback Handling
The subsystem should:
- configure input mode and pixel format
- install and own the input callback delegate
- start and stop capture streams
- translate hardware input frames into backend-level input frame events
- track signal-present versus no-input-source conditions
It should not decide how uploaded textures are produced. That belongs to `RenderEngine`.
The backend may expose input frames as:
- borrowed CPU-accessible frame views
- backend-managed input frame objects
- typed input events containing signal state and frame payload metadata
This work is currently split across:
- [DeckLinkSession::ConfigureInput](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/videoio/decklink/DeckLinkSession.cpp:221)
- [CaptureDelegate::VideoInputFrameArrived](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/videoio/decklink/DeckLinkFrameTransfer.cpp:33)
- [OpenGLVideoIOBridge::UploadInputFrame](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/gl/pipeline/OpenGLVideoIOBridge.cpp:11)
### 3. Output Lifecycle and Output Callback Handling
The subsystem should:
- configure output mode and pixel format
- own the output frame pool
- install and own the scheduled-frame completion callback
- start scheduled playback
- stop scheduled playback
- account for completion results such as completed, late, dropped, and flushed
It should not render the next frame in the callback path.
This work is currently split across:
- [DeckLinkSession::ConfigureOutput](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/videoio/decklink/DeckLinkSession.cpp:273)
- [DeckLinkSession::Start](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/videoio/decklink/DeckLinkSession.cpp:358)
- [PlayoutDelegate::ScheduledFrameCompleted](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/videoio/decklink/DeckLinkFrameTransfer.cpp:79)
### 4. Pacing and Scheduling Policy
The subsystem should own:
- target frame duration and timescale
- schedule time generation
- preroll policy
- spare-buffer policy
- queue headroom policy
- late-frame and dropped-frame recovery policy
This is not just a utility detail. It is one of the main timing responsibilities of the subsystem.
The current `VideoPlayoutScheduler` is a useful seed, but it is too small and too implicit to represent the eventual backend policy by itself.
### 5. Device-Facing Buffer Pools
The subsystem should own all device-facing buffers that exist to satisfy the hardware API contract.
Examples:
- mutable output frames created through DeckLink
- any staging buffers required by a future non-DeckLink backend
- reusable CPU frame containers for hardware ingress/egress
The goal is to make buffer depth and lifetime explicit and measurable.
`RenderEngine` may own render surfaces and GPU readback resources. `VideoBackend` owns the buffers required to talk to the hardware or OS video I/O API.
### 6. Backend Health and Degraded State
The subsystem should publish operational state such as:
- running normally
- prerolling
- temporarily late
- dropping frames
- no input signal
- output stopped
- failed to configure
This state should be reported to `HealthTelemetry`, not hidden inside debug logs or modal dialog paths.
## Boundary With Other Subsystems
This subsystem must stay aligned with the Phase 1 dependency rules.
Allowed directions:
- `VideoBackend -> RenderEngine`
- `VideoBackend -> HealthTelemetry`
Not allowed in the target design:
- `VideoBackend -> RuntimeStore`
- `VideoBackend -> RuntimeCoordinator`
- `VideoBackend -> ControlServices`
The important operational boundary is:
- `VideoBackend` may request or consume rendered output frames
- it may not own frame composition policy
That means:
- no shader parameter validation here
- no persistence decisions here
- no direct mutation of runtime state here
## State Owned by VideoBackend
`VideoBackend` should own the following state categories.
### Device Configuration State
Examples:
- selected device handles
- configured input/output formats
- negotiated pixel formats
- keyer configuration
- output model name
- supported keying flags
### Session Lifecycle State
Examples:
- discovered
- configured
- prerolling
- running
- degraded
- stopping
- stopped
- failed
### Input Runtime State
Examples:
- signal present or missing
- last observed input format properties
- input frame counters
- input callback timestamps
- queued capture frames awaiting render ingestion
### Output Runtime State
Examples:
- output queue depth
- free system-memory playout frame count
- ready system-memory playout frame count
- scheduled system-memory playout frame count
- scheduled frame index
- completed frame index
- late frame count
- dropped frame count
- underrun/repeat/drop counters for system-memory playout policy
- frame age at schedule time and completion callback time
- spare buffer count
- current headroom target
### Backend-Owned Transient Buffers
Examples:
- output mutable frame pool
- playout ring buffer entries
- input frame handoff queue
- staging buffers if required by the device API
This is transient live state, not persisted state.
## Target Lifecycle Model
`VideoBackend` should eventually expose an explicit lifecycle state machine rather than relying on scattered imperative calls.
Suggested states:
1. `uninitialized`
2. `discovering`
3. `discovered`
4. `configuring`
5. `configured`
6. `prerolling`
7. `running`
8. `degraded`
9. `stopping`
10. `stopped`
11. `failed`
Suggested transition rules:
- `uninitialized -> discovering`
- `discovering -> discovered | failed`
- `discovered -> configuring | stopped`
- `configuring -> configured | failed`
- `configured -> prerolling | stopped`
- `prerolling -> running | failed | stopping`
- `running -> degraded | stopping | failed`
- `degraded -> running | stopping | failed`
- `stopping -> stopped`
Why this matters:
- startup failure reporting becomes more predictable
- backend recovery can become policy-driven
- telemetry can report backend state directly
- later backends do not need to mimic DeckLink's exact imperative shape
## Target Timing Model
The long-term timing design should be producer/consumer playout.
### Current Model
Today the callback path effectively does this:
1. DeckLink signals completion.
2. The callback path asks for a new output buffer.
3. The callback path requests render-thread output production.
4. The render thread renders the next frame.
5. The render thread reads it back into the output buffer.
6. The callback path schedules the next hardware frame.
That path is visible in:
- [OpenGLVideoIOBridge::RenderScheduledFrame](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/gl/pipeline/OpenGLVideoIOBridge.cpp:18)
This no longer borrows the GL context from the callback thread, but it still couples output timing directly to render-thread work.
### Target Model
The target model should be:
1. `RenderEngine` produces completed output frames at the configured cadence.
2. `RenderEngine` places them into a bounded queue owned or mediated by `VideoBackend`.
3. `VideoBackend` dequeues ready frames when the device needs them.
4. hardware callbacks only:
- record completion results
- release or recycle buffers
- dequeue and schedule the next ready frame
- raise underrun or degraded-state signals if needed
The timing rule becomes:
- render is the producer
- hardware output is the consumer
This gives the app a clear place to manage:
- target latency
- playout headroom
- stale-frame reuse
- underrun behavior
- spare buffer policy
## Input Buffering and Pacing
The input side needs a simpler but still explicit handoff model.
Recommended target behavior:
- hardware callbacks push input frames into a bounded ingress queue
- `RenderEngine` pulls the newest useful input frame when preparing a render
- if the ingress queue overflows, old frames are discarded according to policy
Recommended default policy for live playout:
- prefer recency over completeness
- drop stale capture frames instead of blocking render or output
The current latest-input mailbox behavior is directionally correct for live timing:
- [OpenGLVideoIOBridge::UploadInputFrame](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/gl/pipeline/OpenGLVideoIOBridge.cpp:11)
The next improvement is to make the backend-to-render handoff policy more explicit in telemetry and playout scheduling, rather than treating it as only a render command mailbox detail.
Suggested input metrics:
- input frames received
- no-signal transitions
- input queue depth
- dropped input frames
- oldest queued input age
## Output Buffering and Headroom Policy
Output buffering should be policy-driven from one source of truth.
The target design should define a playout buffering policy object with at least:
- target preroll depth
- minimum spare device buffers
- maximum queued rendered frames
- allowed catch-up depth
- underrun behavior
Example policy fields:
- `targetPrerollFrames`
- `minSpareOutputBuffers`
- `maxReadyFrames`
- `maxCatchUpFrames`
- `reuseLastFrameOnUnderrun`
- `allowAdaptiveHeadroom`
This replaces the current split between:
- fixed mutable frame pool size in `DeckLinkSession::ConfigureOutput()`
- fixed preroll count in `kPrerollFrameCount`
- fixed skip-ahead recovery in `VideoPlayoutScheduler`
## Underrun and Recovery Policy
The backend should define explicit behavior for when no fresh frame is ready at schedule time.
Candidate policies:
1. Reuse the last completed rendered frame.
2. Reuse the last scheduled output frame.
3. Schedule a known black or degraded frame.
4. Temporarily increase headroom if the system is repeatedly catching up.
Which one is correct may differ by operating mode, but the choice should be explicit rather than incidental.
Similarly, completion-result handling should become measured rather than fixed.
The current scheduler does this:
- late or dropped frame -> `mScheduledFrameIndex += 2`
That is a useful emergency simplification, but not a durable backend contract.
The target backend should instead track:
- scheduled frame index
- completed frame index
- backlog depth
- late streaks
- dropped streaks
- current operating headroom
Then recovery can use measured lag, not a hardcoded skip.
## Suggested Public Interface
This is not a final class API. It describes the shape the subsystem should move toward.
### Discovery and Configuration
- `DiscoverDevices(...)`
- `SelectFormats(...)`
- `ConfigureInput(...)`
- `ConfigureOutput(...)`
- `GetCapabilities()`
- `GetBackendState()`
### Lifecycle
- `StartCapture()`
- `StartPlayout()`
- `StopCapture()`
- `StopPlayout()`
- `Shutdown()`
### Input Handoff
- `PollInputFrame(...)` or `TryDequeueInputFrame(...)`
- `ReportInputSignalState(...)`
### Output Handoff
- `QueueRenderedFrame(...)`
- `TryDequeueReadyFrameForSchedule(...)`
- `RecycleCompletedFrame(...)`
### Timing and Recovery
- `SetPlayoutPolicy(...)`
- `AccountForCompletionResult(...)`
- `BuildBackendTimingSnapshot()`
### Health Reporting
- `BuildBackendHealthSnapshot()`
- `GetWarningState()`
## Suggested Internal Components
The subsystem will likely be easier to evolve if its responsibilities are split internally.
Possible internal structure:
### `VideoBackendSession`
Owns:
- high-level lifecycle state
- configuration
- input/output subcomponents
- policy objects
### `InputEndpoint`
Owns:
- input device callback registration
- input frame queue
- signal detection state
### `OutputEndpoint`
Owns:
- output device callback registration
- output device buffer pool
- schedule/dequeue logic
- preroll and output queue management
### `PlayoutPolicy`
Owns:
- preroll target
- spare buffer target
- underrun behavior
- catch-up and lateness rules
### `BackendTimingState`
Owns:
- frame counters
- queue depth snapshots
- late/dropped streaks
- observed intervals
These can remain implementation details in Phase 1, but the design should leave room for them.
## Mapping From Current Code
### Current `DeckLinkSession`
Should mostly migrate into:
- `VideoBackend`
- device discovery
- input configuration
- output configuration
- keyer capability handling
- output frame pool ownership
- lifecycle state handling
Candidates to stay backend-owned:
- `DiscoverDevicesAndModes(...)`
- `SelectPreferredFormats(...)`
- `ConfigureInput(...)`
- `ConfigureOutput(...)`
- `Start()`
- `Stop()`
- `HandleVideoInputFrame(...)`
- `HandlePlayoutFrameCompleted(...)`
### Current `VideoPlayoutScheduler`
Should likely become:
- a backend-owned policy helper or timing component under `VideoBackend`
It is still a backend concern, but it should be expanded beyond a single counter and fixed skip rule.
### Current `OpenGLVideoIOBridge`
Should split between:
- `RenderEngine`
- input texture upload scheduling
- render submission
- readback or output-frame production
- `VideoBackend`
- input ingress queue
- output callback and scheduling policy
- pacing stats
The most important migration is:
- remove render work from `PlayoutFrameCompleted()`
### Previous Runtime Status Updates
Frame pacing and signal status setters that were historically called from the bridge should route through:
- `VideoBackend -> HealthTelemetry`
rather than the old pattern:
- callback/bridge -> `RuntimeHost`
## Migration Plan
The migration should avoid a flag-day rewrite.
### Step 1. Name the backend boundary explicitly
Create a conceptual `VideoBackend` interface around the existing `VideoIODevice`/`DeckLinkSession` shape without moving all logic at once.
### Step 2. Pull timing policy into backend-owned objects
Move:
- completion accounting
- headroom configuration
- frame-pool sizing
- queue depth reporting
behind explicit backend policy types.
This can happen before changing the render thread model.
### Step 3. Separate callback work from render work
Change the output completion path so it stops rendering immediately in the callback chain.
Intermediate step:
- callback records completion and wakes a playout worker
Target step:
- callback only dequeues and schedules already-ready frames
### Step 4. Move input handoff to a bounded queue
Replace direct callback-to-GL upload behavior with:
- backend-owned input queue
- render-owned dequeue/upload policy
### Step 5. Introduce explicit backend lifecycle states
Start surfacing:
- configured
- prerolling
- running
- degraded
- failed
before changing all recovery behavior.
### Step 6. Route backend health to `HealthTelemetry`
Move debug-only warnings and ad hoc status strings toward structured counters and backend snapshots.
## Risks
### Latency Versus Stability Tradeoff
Increasing headroom reduces deadline misses but increases end-to-end latency. The backend must make that tradeoff explicit and configurable enough for live use.
### Hidden Coupling During Migration
The current bridge still mixes backend and render concerns. Partial extraction can accidentally preserve the old coupling under new names if the callback path is not cleaned up deliberately.
### Buffer Ownership Ambiguity
If device-facing buffers and render-facing buffers are not separated clearly, lifetime bugs and timing regressions will remain easy to reintroduce.
### Backend-Specific Assumptions
The first target is still DeckLink-centric. The interface should avoid baking in assumptions that would make alternate backends awkward later.
### Recovery Policy Complexity
A more explicit backend model will surface choices that are currently hidden:
- stale frame reuse
- black-frame fallback
- adaptive headroom
- catch-up rules
That is healthy, but it will require deliberate policy decisions.
## Open Questions
- Should `VideoBackend` own both input and output under one session object long-term, or should it expose distinct input and output endpoints under a shared shell?
- Should queue ownership sit fully inside `VideoBackend`, or should there be a narrow shared frame-exchange interface between `RenderEngine` and `VideoBackend`?
- What should the default underrun policy be for live playout: reuse last frame, reuse newest completed frame, or output black?
- Should adaptive headroom be automatic, operator-configurable, or both?
- At what point should preview timing be treated as a backend concern versus a render concern? The Phase 1 direction says preview is subordinate to render, not owned by the backend, but later timing work may still require explicit coordination.
- How much of the current `VideoIOState` belongs inside `VideoBackend` versus `HealthTelemetry` snapshots?
## Short Version
`VideoBackend` should become the subsystem that owns hardware timing, device lifecycle, buffer policy, and playout recovery.
It should not render frames.
The target direction is:
- `RenderEngine` produces frames ahead of need
- `VideoBackend` consumes and schedules them
- callbacks become lightweight control-plane events
- headroom, queue depth, and recovery become explicit backend policy
- hardware health is reported structurally instead of being inferred from scattered logs and bridge behavior

View File

@@ -14,11 +14,12 @@ Packaged documentation:
Generated files:
- `shader_cache/active_shader_wrapper.slang`: generated Slang wrapper for the active shader/layer.
- `shader_cache/active_shader.raw.frag`: raw GLSL emitted by `slangc`.
- `shader_cache/active_shader.frag`: patched GLSL consumed by the OpenGL path.
- `runtime_state.json`: persisted layer stack and parameter values.
- `stack_presets/*.json`: user-saved layer stack presets.
- `shader_cache/active_shader_wrapper.slang`: generated Slang wrapper for the most recently compiled shader pass.
- `shader_cache/active_shader.raw.frag`: raw GLSL emitted by `slangc` for the most recently compiled pass.
- `shader_cache/active_shader.frag`: patched GLSL consumed by the OpenGL path for the most recently compiled pass.
- `runtime_state.json`: debounced autosave of the latest layer stack, layer order, bypass state, shader assignments, and parameter values. The host reloads this file on startup and falls back to the configured default shader if the file is missing or unusable.
- `stack_presets/*.json`: planned user-saved layer stack presets. Preset routes are present in the API surface but not implemented in the current native host.
- `screenshots/*.png`: planned screenshot output. Screenshot capture is present in the API surface but not implemented in the current native host.
Git policy:

View File

@@ -11,16 +11,15 @@ struct ShaderContext
float2 inputResolution;
float2 outputResolution;
float time;
float utcTimeSeconds;
float utcOffsetSeconds;
float startupRandom;
float frameCount;
float mixAmount;
float bypass;
int sourceHistoryLength;
int temporalHistoryLength;
float2 audioRms;
float2 audioPeak;
float audioMonoRms;
float audioMonoPeak;
float4 audioBands;
int feedbackAvailable;
};
cbuffer GlobalParams
@@ -28,34 +27,29 @@ cbuffer GlobalParams
float gTime;
float2 gInputResolution;
float2 gOutputResolution;
float gUtcTimeSeconds;
float gUtcOffsetSeconds;
float gStartupRandom;
float gFrameCount;
float gMixAmount;
float gBypass;
int gSourceHistoryLength;
int gTemporalHistoryLength;
float2 gAudioRms;
float2 gAudioPeak;
float gAudioMonoRms;
float gAudioMonoPeak;
float4 gAudioBands;
int gFeedbackAvailable;
{{PARAMETER_UNIFORMS}}};
Sampler2D<float4> gVideoInput;
Sampler2D<float4> gAudioData;
{{SOURCE_HISTORY_SAMPLERS}}{{TEMPORAL_HISTORY_SAMPLERS}}{{TEXTURE_SAMPLERS}}
Sampler2D<float4> gLayerInput;
{{SOURCE_HISTORY_SAMPLERS}}{{TEMPORAL_HISTORY_SAMPLERS}}{{FEEDBACK_SAMPLER}}{{TEXTURE_SAMPLERS}}
{{TEXT_SAMPLERS}}
float4 sampleVideo(float2 tc)
{
return gVideoInput.Sample(tc);
}
float4 sampleAudioWaveform(float x)
float4 sampleLayerInput(float2 tc)
{
return gAudioData.Sample(float2(saturate(x), 0.25));
}
float4 sampleAudioSpectrum(float x)
{
return gAudioData.Sample(float2(saturate(x), 0.75));
return gLayerInput.Sample(tc);
}
float4 sampleSourceHistory(int framesAgo, float2 tc)
@@ -88,6 +82,9 @@ float4 sampleTemporalHistory(int framesAgo, float2 tc)
}
}
{{FEEDBACK_HELPER}}
{{TEXT_HELPERS}}
#include "{{USER_SHADER_INCLUDE}}"
[shader("fragment")]
@@ -99,16 +96,15 @@ float4 fragmentMain(FragmentInput input) : SV_Target
context.inputResolution = gInputResolution;
context.outputResolution = gOutputResolution;
context.time = gTime;
context.utcTimeSeconds = gUtcTimeSeconds;
context.utcOffsetSeconds = gUtcOffsetSeconds;
context.startupRandom = gStartupRandom;
context.frameCount = gFrameCount;
context.mixAmount = gMixAmount;
context.bypass = gBypass;
context.sourceHistoryLength = gSourceHistoryLength;
context.temporalHistoryLength = gTemporalHistoryLength;
context.audioRms = gAudioRms;
context.audioPeak = gAudioPeak;
context.audioMonoRms = gAudioMonoRms;
context.audioMonoPeak = gAudioMonoPeak;
context.audioBands = gAudioBands;
context.feedbackAvailable = gFeedbackAvailable;
float4 effectedColor = {{ENTRY_POINT_CALL}};
float mixValue = clamp(gBypass > 0.5 ? 0.0 : gMixAmount, 0.0, 1.0);
return lerp(context.sourceColor, effectedColor, mixValue);

819
shaders/SHADER_CONTRACT.md Normal file
View File

@@ -0,0 +1,819 @@
# Shader Package Contract
This document explains how to create shaders for the Video Shader runtime.
Each shader is a small package under `shaders/<id>/`:
```text
shaders/my-effect/
shader.json
shader.slang
optional-texture.png
```
The runtime reads `shader.json`, generates a Slang wrapper from `runtime/templates/shader_wrapper.slang.in`, includes your `shader.slang`, compiles the result to GLSL, and exposes the shader in the local control UI.
## Quick Start
Create a folder:
```text
shaders/my-effect/
```
Add `shader.json`:
```json
{
"id": "my-effect",
"name": "My Effect",
"description": "A simple starter shader.",
"category": "Custom",
"entryPoint": "shadeVideo",
"parameters": [
{
"id": "strength",
"label": "Strength",
"type": "float",
"default": 0.5,
"min": 0.0,
"max": 1.0,
"step": 0.01
}
]
}
```
Add `shader.slang`:
```slang
float4 shadeVideo(ShaderContext context)
{
float4 color = context.sourceColor;
color.rgb = lerp(color.rgb, 1.0 - color.rgb, strength);
return saturate(color);
}
```
With `autoReload` enabled in `config/runtime-host.json`, edits to shader source, manifests, and declared texture assets are picked up automatically. You can also use **Reload shaders** in the control UI to manually rescan the shader library.
## Guidance For Shaders
When generating a new shader package, prefer matching the existing runtime contract over copying code verbatim from Shadertoy, GLSL sandbox sites, or WebGL demos.
Important rules:
- Generate a complete package: `shaders/<id>/shader.json` and `shaders/<id>/shader.slang`.
- Use `float4 shadeVideo(ShaderContext context)` unless the manifest explicitly sets a different `entryPoint`.
- Do not create `mainImage`, `main`, `fragColor`, `iResolution`, `iTime`, `iChannel0`, or a fragment shader attribute layout. The runtime wrapper provides the real fragment entry point.
- Replace Shadertoy `fragCoord` with `context.uv * context.outputResolution`.
- Replace `iResolution.xy` with `context.outputResolution`.
- Replace `iTime` with `context.time`.
- Replace `iFrame` with `context.frameCount`.
- Replace source-video `iChannel0` sampling with `sampleVideo(uv)` or `context.sourceColor`.
- Use Slang/HLSL names and syntax: `float2`, `float3`, `float4`, `float2x2`, `lerp`, `frac`, `saturate`, and `mul(matrix, vector)`.
- Do not use GLSL-only types/functions such as `vec2`, `vec3`, `vec4`, `mat2`, `mix`, `fract`, `mod`, `texture`, or `mainImage`.
- Keep parameter IDs, texture IDs, font IDs, and function entry points as valid shader identifiers: letters, numbers, and underscores only, starting with a letter or underscore.
- Add only controls that are actually used by the shader.
- Prefer a small number of clear controls with conservative defaults.
- Keep shaders deterministic unless randomness is an explicit feature. For stable process-level variation, use `context.startupRandom`; for per-pixel pseudo-randomness, hash from `uv`, pixel coordinates, `frameCount`, or trigger values.
- If adapting third-party code, include attribution and source URL in the manifest description when the license allows adaptation.
- If the source license is unclear or incompatible, do not add the shader package.
Before finishing, compile-check the shader through the runtime wrapper or launch the app and verify the shader appears without an error in the selector. CI also runs shader validation, so every available package in `shaders/` should compile successfully. Intentionally broken examples should stay visibly marked as broken rather than pretending to be production shaders.
## Manifest Fields
`shader.json` is the runtime-facing description of the shader.
Required fields:
- `id`: package ID used by state/presets. Hyphenated names are OK here, for example `my-effect`.
- `name`: display name in the UI.
- `parameters`: array of exposed controls. Use `[]` if there are no user parameters.
Optional fields:
- `description`: display/help text for the shader library.
- `category`: UI grouping label.
- `entryPoint`: Slang function to call. Defaults to `shadeVideo`.
- `passes`: advanced render-pass declarations. Omit this for normal single-pass shaders.
- `textures`: texture assets to load and expose as samplers.
- `fonts`: packaged font assets for live text parameters.
- `temporal`: history-buffer requirements.
- `feedback`: optional previous-frame shader-local feedback surface.
Parameter objects may also include an optional `description` string. The control UI displays it as one-line helper text with the full text available on hover, so use it for short operational guidance rather than long documentation.
Metadata conventions:
- Keep `name` short, human-facing, and in title case.
- Keep `category` consistent with existing library groups such as `Color`, `Transform`, `Projection`, `Temporal`, `Scopes & Guides`, `Utility`, `Feedback`, and `Calibration`.
- Keep `description` to one clear sentence in present tense that explains what the shader does for an operator.
- Avoid placeholder, joke, or overly implementation-heavy descriptions unless the shader is intentionally a diagnostic or broken example.
Shader-visible identifiers must be valid Slang-style identifiers:
- `entryPoint`
- parameter `id`
- texture `id`
- font `id`
Use letters, numbers, and underscores only, and start with a letter or underscore. For example, `logoTexture` is valid; `logo-texture` is not valid as a shader-visible texture ID.
## Render Passes
Most shaders should omit `passes`. The runtime then creates one implicit pass:
```json
{
"id": "main",
"source": "shader.slang",
"entryPoint": "shadeVideo",
"output": "layerOutput"
}
```
Advanced shaders may declare explicit passes. All passes may live in one `.slang` file by using different `entryPoint` values, or they may be split across multiple source files:
```json
{
"passes": [
{
"id": "blurX",
"source": "blur-x.slang",
"entryPoint": "blurHorizontal",
"inputs": ["layerInput"],
"output": "blurredX"
},
{
"id": "final",
"source": "final.slang",
"entryPoint": "finish",
"inputs": ["blurredX"],
"output": "layerOutput"
}
]
}
```
Pass fields:
- `id`: required pass identifier. It must be a valid shader identifier and unique inside the package.
- `source`: required Slang source path relative to the package directory.
- `entryPoint`: optional Slang function for this pass. Defaults to the package-level `entryPoint`.
- `inputs`: optional list of named inputs. The first input is used as the pass input texture.
- `output`: optional output name. Use `layerOutput` for the final visible layer result.
Pass input names:
- `layerInput`: the input to this layer, before any of its passes run.
- `previousPass`: the previous pass output in this layer. If there is no previous pass, this falls back to `layerInput`.
- Any earlier pass `id` or `output` name from the same layer.
If `inputs` is omitted, the first pass samples `layerInput` and later passes sample `previousPass`.
Single-file multipass example:
```json
{
"passes": [
{
"id": "mask",
"source": "shader.slang",
"entryPoint": "makeMask",
"output": "maskBuffer"
},
{
"id": "final",
"source": "shader.slang",
"entryPoint": "finish",
"inputs": ["maskBuffer"],
"output": "layerOutput"
}
]
}
```
Pass output names:
- `layerOutput`: the final visible output of this layer.
- Any other name creates an intermediate 16-bit float render target that later passes may sample.
If the final declared pass does not explicitly output `layerOutput`, the runtime still treats that final pass as the visible layer output. Existing single-pass shaders are unaffected.
## Feedback Surface
Shaders may opt in to a persistent previous-frame feedback surface:
```json
{
"feedback": {
"enabled": true,
"writePass": "final"
}
}
```
Fields:
- `enabled`: when `true`, the runtime allocates one persistent `RGBA16F` feedback surface for this shader at the current render resolution.
- `writePass`: optional pass `id` whose output should become next frame's feedback surface. If omitted, the runtime uses the final declared pass, or the implicit `main` pass for single-pass shaders.
Behavior:
- all passes may sample the same previous-frame feedback surface
- one designated pass writes the next feedback surface
- feedback is previous-frame state, not same-frame pass chaining
Guardrails:
- Feedback is best suited to image-like state such as trails, masks, luminance fields, decay maps, and shader-local analysis buffers.
- Feedback is not a precise long-term data store. The surface uses `RGBA16F`, so repeated accumulation, exact counters, and tightly packed metadata can drift or clamp over time.
- The feedback surface is currently filtered like an image, not configured as strict texel-addressed storage. If you reserve texels as data slots, sample them carefully and do not assume exact CPU-style array semantics.
- Each feedback-enabled layer allocates two full-resolution feedback textures for ping-pong state. This increases VRAM use and adds one extra full-frame feedback copy per rendered frame.
- In multipass shaders, feedback remains previous-frame state even when a pass also consumes same-frame pass outputs. Do not treat feedback as another same-frame intermediate buffer.
Single-pass example:
```json
{
"id": "feedback-glow",
"name": "Feedback Glow",
"feedback": {
"enabled": true
},
"parameters": []
}
```
Multipass example:
```json
{
"passes": [
{
"id": "analysis",
"source": "shader.slang",
"entryPoint": "analyzeFrame",
"output": "analysisBuffer"
},
{
"id": "final",
"source": "shader.slang",
"entryPoint": "finishFrame",
"inputs": ["analysisBuffer"],
"output": "layerOutput"
}
],
"feedback": {
"enabled": true,
"writePass": "final"
}
}
```
The wrapper exposes:
```slang
float4 sampleFeedback(float2 uv);
```
On the first frame, or after a reset, `sampleFeedback` returns transparent black.
Feedback resets when:
- a layer bypass state changes
- a layer changes shader
- the layer itself is removed
- a shader is reloaded or recompiled
- render dimensions change
- the app restarts
Ordinary stack add/remove/reorder operations on other layers are intended to preserve feedback state for unchanged feedback-enabled layers.
So feedback should be treated as live runtime state, not durable saved state.
## Slang Entry Point
Your shader file must implement the manifest `entryPoint`.
Default:
```slang
float4 shadeVideo(ShaderContext context)
{
return context.sourceColor;
}
```
The runtime owns the real fragment shader entry point. Your function is called from the wrapper, and the runtime handles final bypass/mix behavior:
```slang
return lerp(context.sourceColor, effectedColor, mixValue);
```
That means:
- Return the fully effected color from your function.
- Respect alpha if your shader produces an overlay or sprite.
- The runtime will blend your result with the source according to `mixAmount` and bypass state.
## ShaderContext
Your entry point receives:
```slang
struct ShaderContext
{
float2 uv;
float4 sourceColor;
float2 inputResolution;
float2 outputResolution;
float time;
float utcTimeSeconds;
float utcOffsetSeconds;
float startupRandom;
float frameCount;
float mixAmount;
float bypass;
int sourceHistoryLength;
int temporalHistoryLength;
int feedbackAvailable;
};
```
Fields:
- `uv`: normalized texture coordinates, usually `0..1`.
- `sourceColor`: decoded RGBA source video at `uv`.
- `inputResolution`: decoded input video resolution in pixels.
- `outputResolution`: shader render resolution in pixels. The current pipeline renders the shader stack at input resolution, then scales the final frame to the configured video I/O output mode.
- `time`: elapsed runtime time in seconds.
- `utcTimeSeconds`: current UTC time of day from the host PC clock, expressed as seconds since UTC midnight.
- `utcOffsetSeconds`: host PC local UTC offset in seconds. Add this to `utcTimeSeconds` and wrap to `0..86400` to get local time of day.
- `startupRandom`: random `0..1` value generated once when the host process starts. It stays constant for the lifetime of the app and changes on the next launch.
- `frameCount`: incrementing frame counter.
- `mixAmount`: runtime mix amount.
- `bypass`: `1.0` when the layer is bypassed, otherwise `0.0`.
- `sourceHistoryLength`: number of usable source-history frames currently available.
- `temporalHistoryLength`: number of usable temporal frames currently available for this layer.
- `feedbackAvailable`: `1` when previous-frame feedback exists for this layer, otherwise `0`.
Color/precision notes:
- `context.sourceColor`, `sampleVideo()`, and temporal history samples are display-referred Rec.709-like RGB, not linear-light RGB.
- The current DeckLink backend prefers 10-bit YUV capture and output when the card/mode supports it, with automatic 8-bit fallback. If external keying is enabled, output prefers 10-bit YUVA (`Ay10`) when supported so shader alpha can drive the key signal, then falls back to 8-bit BGRA.
- Internal decoded, layer, composite, output, and temporal render targets are 16-bit floating point, so gradients and LUT work have more headroom than packed byte video I/O formats.
- Do not add extra Rec.709 or linear conversions unless the shader intentionally documents that behavior.
## Helper Functions
The wrapper provides:
```slang
float4 sampleLayerInput(float2 uv);
float4 sampleVideo(float2 uv);
float4 sampleSourceHistory(int framesAgo, float2 uv);
float4 sampleTemporalHistory(int framesAgo, float2 uv);
float4 sampleFeedback(float2 uv);
```
`sampleLayerInput` samples the input arriving at this shader layer before any of the layer's own passes run. If this layer follows another shader, it sees that previous shader's output. If this is the first shader layer, it sees the decoded source image.
`sampleVideo` samples the current pass input texture. In single-pass shaders this is usually the layer input. In multipass shaders it may instead be a named pass output or `previousPass`, depending on the manifest routing for that pass.
`sampleSourceHistory` samples previous decoded source frames. `framesAgo` is clamped into the available range. If no history is available, it falls back to `sampleVideo`.
`sampleTemporalHistory` samples previous pre-layer input frames for temporal shaders that request `preLayerInput` history. `framesAgo` is clamped into the available range. If no temporal history is available, it falls back to `sampleVideo`.
`sampleFeedback` samples the shader-local previous-frame feedback surface. If feedback has not been written yet, it returns transparent black.
Example:
```slang
float4 shadeVideo(ShaderContext context)
{
float4 previous = sampleSourceHistory(1, context.uv);
return lerp(context.sourceColor, previous, 0.35);
}
```
Layer-input example:
```slang
float4 finishPass(ShaderContext context)
{
float3 baseColor = sampleLayerInput(context.uv).rgb;
float3 passResult = context.sourceColor.rgb;
return float4(baseColor + passResult * 0.25, 1.0);
}
```
Feedback example:
```slang
float4 shadeVideo(ShaderContext context)
{
float4 previous = sampleFeedback(context.uv);
float4 current = context.sourceColor;
return lerp(current, previous, 0.2);
}
```
Multipass feedback example:
```slang
float4 analyzeFrame(ShaderContext context)
{
float4 previous = sampleFeedback(context.uv);
float luma = dot(context.sourceColor.rgb, float3(0.2126, 0.7152, 0.0722));
return float4(lerp(previous.rgb, float3(luma), 0.1), 1.0);
}
float4 finishFrame(ShaderContext context)
{
float4 analysis = context.sourceColor;
return float4(analysis.rgb, 1.0);
}
```
In that multipass case:
- `analyzeFrame` reads last frame's feedback
- `finishFrame` receives the same-frame pass output through normal multipass routing
- the `writePass` decides which pass output becomes next frame's feedback
That means:
- use `context.sourceColor` or `sampleVideo()` when you want this pass's routed input
- use `sampleLayerInput()` when you want the pre-pass layer input
- use `sampleFeedback()` when you want previous-frame persistent shader-local state
## Parameters
Manifest parameters are exposed to Slang as global values with the same `id`.
Supported types:
| Manifest type | Slang type | JSON value |
| --- | --- | --- |
| `float` | `float` | number |
| `vec2` | `float2` | `[x, y]` |
| `color` | `float4` | `[r, g, b, a]` |
| `bool` | `bool` | `true` or `false` |
| `enum` | `int` | selected option index |
| `text` | generated texture/helper | string |
| `trigger` | `int <id>`, `float <id>Time` | pulse/count |
Float example:
```json
{
"id": "brightness",
"label": "Brightness",
"type": "float",
"default": 1.0,
"min": 0.0,
"max": 2.0,
"step": 0.01
}
```
```slang
color.rgb *= brightness;
```
Vector example:
```json
{
"id": "offset",
"label": "Offset",
"type": "vec2",
"default": [0.0, 0.0],
"min": [-0.2, -0.2],
"max": [0.2, 0.2],
"step": [0.001, 0.001]
}
```
```slang
float2 uv = clamp(context.uv + offset, float2(0.0), float2(1.0));
```
Color example:
```json
{
"id": "tint",
"label": "Tint",
"type": "color",
"default": [1.0, 1.0, 1.0, 1.0]
}
```
```slang
color *= tint;
```
Boolean example:
```json
{
"id": "invert",
"label": "Invert",
"type": "bool",
"default": false
}
```
```slang
if (invert)
color.rgb = 1.0 - color.rgb;
```
Enum example:
```json
{
"id": "mode",
"label": "Mode",
"type": "enum",
"default": "normal",
"options": [
{ "value": "normal", "label": "Normal" },
{ "value": "luma", "label": "Luma" },
{ "value": "posterize", "label": "Posterize" }
]
}
```
Enums are stored in presets/state by their string `value`, but exposed to Slang as a zero-based integer index in option order:
```slang
if (mode == 1)
{
float luma = dot(color.rgb, float3(0.2126, 0.7152, 0.0722));
color.rgb = float3(luma);
}
else if (mode == 2)
{
color.rgb = floor(color.rgb * 4.0) / 4.0;
}
```
Text example:
```json
{
"fonts": [
{ "id": "inter", "path": "fonts/Inter-Regular.ttf" }
],
"parameters": [
{
"id": "titleText",
"label": "Title",
"type": "text",
"default": "LIVE",
"font": "inter",
"maxLength": 64
}
]
}
```
Text parameters are runtime-owned strings. They are not emitted as uniform values. Instead, the runtime renders the current string into a single-line SDF mask texture and the shader wrapper exposes helpers based on the parameter id:
```slang
float mask = sampleTitleText(textUv);
float4 premultipliedText = drawTitleText(textUv, float4(1.0, 1.0, 1.0, 1.0));
```
Text is currently limited to printable ASCII. `maxLength` defaults to `64` and is clamped to `1..256`. The optional `font` field references a packaged font declared in `fonts`; if no font is specified, the runtime uses its fallback sans-serif renderer.
Trigger example:
```json
{
"id": "flash",
"label": "Flash",
"type": "trigger"
}
```
A trigger appears as a button in the control UI. Pressing it increments the shader-visible integer `flash` and records the runtime time in `flashTime`:
```slang
float age = context.time - flashTime;
float intensity = flash > 0 ? exp(-age * 5.0) : 0.0;
color.rgb += intensity;
```
Triggers are useful for one-shot shader reactions such as flashes, ripples, cuts, or randomized looks. They do not execute arbitrary CPU code; they only update uniforms consumed by the shader.
Parameter validation:
- Float values are clamped to `min`/`max` if provided.
- `vec2` must have exactly 2 numbers.
- `color` must have exactly 4 numbers.
- Enum defaults must match one of the declared option values.
- Text defaults must be strings. Non-printable characters are dropped and values are clamped to `maxLength`.
- Trigger values are incremented by the host when triggered. The shader sees the trigger count and last trigger time.
- Non-finite numeric values are rejected.
## Texture Assets
Declare texture assets in the manifest:
```json
{
"textures": [
{
"id": "logoTexture",
"path": "logo.png"
}
]
}
```
Rules:
- `id` must be a valid shader identifier.
- `path` is relative to the shader package directory.
- The file must exist when the manifest is loaded.
- Texture asset changes trigger shader reload.
Texture IDs become `Sampler2D<float4>` globals:
```slang
float4 logo = logoTexture.Sample(logoUv);
```
For sprite or overlay shaders, return premultiplied-looking output if you want clean composition:
```slang
float alpha = logo.a;
return float4(logo.rgb * alpha, alpha);
```
See `shaders/dvd-bounce/` for a complete texture-driven example.
## Font Assets
Declare packaged font assets in the manifest:
```json
{
"fonts": [
{
"id": "inter",
"path": "fonts/Inter-Regular.ttf"
}
]
}
```
Rules:
- `id` must be a valid shader identifier.
- `path` is relative to the shader package directory.
- The file must exist when the manifest is loaded.
- Font asset changes trigger shader reload.
- V1 text layout is single-line; shaders position and scale the generated text texture themselves.
See `shaders/text-overlay/` for a complete live text example. The sample bundles Roboto Regular and includes its OFL license beside the font file.
## Temporal Shaders
Temporal shaders can request access to previous frames.
Manifest example:
```json
{
"temporal": {
"enabled": true,
"historySource": "preLayerInput",
"historyLength": 12
}
}
```
Supported `historySource` values:
- `source`: decoded source-video history from previous frames.
- `preLayerInput`: history of the input arriving at this layer before the shader runs.
`historyLength` is the requested frame count. The runtime clamps it by `maxTemporalHistoryFrames` in `config/runtime-host.json`.
Temporal history resets when:
- layers are added, removed, or reordered
- a layer bypass state changes
- a layer changes shader
- a shader is reloaded or recompiled
- render dimensions change
Use the available history lengths to avoid assuming history is ready on the first frame:
```slang
float4 shadeVideo(ShaderContext context)
{
if (context.temporalHistoryLength <= 0)
return context.sourceColor;
float4 oldFrame = sampleTemporalHistory(3, context.uv);
return lerp(context.sourceColor, oldFrame, 0.4);
}
```
See `shaders/temporal-ghost-trail/` and `shaders/temporal-low-fps/` for examples.
## Coordinate And Color Notes
- `uv` is normalized.
- Use `context.outputResolution` for pixel-sized effects.
- Use `context.inputResolution` when sampling source video by input pixel size.
- `sourceColor` and `sampleVideo` return RGBA values in normalized `0..1` range.
- Prefer `saturate(color)` or explicit `clamp` before returning if your math can overshoot.
- For generated calibration charts, test patterns, gradients, and exposure ramps, state whether patch values are linear-light, display-referred gamma encoded, Rec.709 encoded, or intentionally artistic.
- For one-stop exposure patches, each patch should normally be `baseLevel * 2^patchIndex` before any display/tone encoding.
- For Rec.709 OETF encoding, use:
```slang
float rec709Oetf(float linearLevel)
{
float value = saturate(linearLevel);
if (value < 0.018)
return 4.5 * value;
return 1.099 * pow(value, 0.45) - 0.099;
}
```
Pixel-size example:
```slang
float2 pixel = 1.0 / max(context.outputResolution, float2(1.0));
float4 right = sampleVideo(context.uv + float2(pixel.x, 0.0));
```
## Animation And Timing Notes
- `context.time` is elapsed runtime time in seconds and is the default animation source for generative shaders.
- `context.frameCount` increments once per rendered output frame and is useful when an effect must be frame-locked.
- Avoid expensive CPU-like timing logic in the shader; animation should usually be a simple function of `context.time`, `context.frameCount`, trigger uniforms, or parameters.
- If a shader appears to judder only while animated, first test whether freezing its time removes the issue. That usually separates animation cadence issues from rendering or transfer issues.
- Do not add custom timer uniforms to the wrapper. Use the fields already in `ShaderContext`.
## Performance Notes
The app has to meet a fixed video frame cadence, so avoid shader code that only looks good in unconstrained browser demos.
Guidelines:
- Keep loops bounded with compile-time constants where possible.
- Avoid very high per-pixel raymarch counts by default. If a heavy loop is needed, expose a quality/steps control with a safe default.
- Prefer early exits only when they are simple; highly divergent branches can be expensive across a full frame.
- Avoid repeated texture sampling in large loops unless the effect really needs it.
- Use `context.outputResolution` carefully. A 1080p frame is over 2 million fragments; a tiny extra loop can become expensive.
- The UI render time may measure CPU command submission rather than true GPU execution time, so visual frame issues can still be GPU-related even when reported render time is small.
- Do not write debug files, allocate resources, or assume CPU-side work can happen from `shader.slang`. Shader code is GPU-only.
## Reload And Generated Files
When a shader compiles, the runtime writes generated files under `runtime/shader_cache/`:
- `active_shader_wrapper.slang`
- `active_shader.raw.frag`
- `active_shader.frag`
These files are ignored by git and are useful for debugging compiler output. If a shader fails to compile, inspect the wrapper first; it shows the exact generated Slang code including your included shader.
For multipass shaders, these files reflect the most recently compiled pass. If a package has several passes, the reported compile error and pass name are usually more useful than assuming the cache contains the first pass.
## Common Pitfalls
- Do not use hyphens in parameter IDs, texture IDs, or entry point names.
- Do not declare your own `ShaderContext`, `GlobalParams`, `sampleVideo`, `sampleSourceHistory`, or `sampleTemporalHistory`.
- Do not write a `[shader("fragment")]` entry point in `shader.slang`; the runtime provides it.
- Remember enum globals are integer indexes, not strings.
- Declare every texture in `shader.json`; undeclared texture samplers will not be bound.
- Declare packaged fonts in `shader.json` when text parameters should use a specific font.
- Keep temporal history requests modest. They consume texture units and memory and are capped by runtime config.
- If a parameter appears in the UI but not in Slang, the shader may still compile, but the control has no effect.
- If a Slang name collides with a generated global, rename your parameter or local symbol.
## Minimal Package Checklist
Before committing a new shader package:
- `shader.json` is valid JSON.
- `id` is unique across `shaders/`.
- `entryPoint`, parameter IDs, and texture IDs are valid identifiers.
- `shader.slang` implements the configured entry point.
- Texture files referenced by `textures` exist.
- Font files referenced by `fonts` exist.
- Enum defaults are present in their `options`.
- Temporal shaders handle short or empty history gracefully.
- The app can reload and compile the shader without errors.

View File

@@ -0,0 +1,85 @@
{
"id": "anamorphic-desqueeze",
"name": "Anamorphic Desqueeze",
"description": "Desqueezes anamorphic footage by 1.3x, 1.33x, 1.5x, or 2x with fit or fill framing.",
"category": "Transform",
"entryPoint": "shadeVideo",
"parameters": [
{
"id": "desqueezeFactor",
"label": "Desqueeze",
"type": "enum",
"default": "x1_33",
"options": [
{
"value": "x1_3",
"label": "1.3x"
},
{
"value": "x1_33",
"label": "1.33x"
},
{
"value": "x1_5",
"label": "1.5x"
},
{
"value": "x2_0",
"label": "2x"
}
],
"description": "Horizontal stretch factor matching the anamorphic lens or adapter."
},
{
"id": "framing",
"label": "Framing",
"type": "enum",
"default": "fit",
"options": [
{
"value": "fit",
"label": "Fit"
},
{
"value": "fill",
"label": "Fill"
}
],
"description": "Fit preserves the whole image; Fill crops to remove borders."
},
{
"id": "pan",
"label": "Pan",
"type": "vec2",
"default": [
0,
0
],
"min": [
-1,
-1
],
"max": [
1,
1
],
"step": [
0.001,
0.001
],
"description": "Reframes the desqueezed image after fit/fill scaling."
},
{
"id": "outsideColor",
"label": "Outside Color",
"type": "color",
"default": [
0,
0,
0,
1
],
"description": "Color used where the remapped image samples outside the source frame."
}
]
}

View File

@@ -0,0 +1,32 @@
float selectedDesqueezeFactor()
{
if (desqueezeFactor == 0)
return 1.3;
if (desqueezeFactor == 1)
return 1.3333333;
if (desqueezeFactor == 2)
return 1.5;
return 2.0;
}
float4 shadeVideo(ShaderContext context)
{
float factor = selectedDesqueezeFactor();
float2 centered = context.uv - 0.5;
if (framing == 0)
{
centered.y *= factor;
}
else
{
centered.x /= factor;
}
float2 sourceUv = centered + 0.5 - pan;
bool inside = sourceUv.x >= 0.0 && sourceUv.x <= 1.0 && sourceUv.y >= 0.0 && sourceUv.y <= 1.0;
if (!inside)
return outsideColor;
return sampleVideo(sourceUv);
}

View File

@@ -1,76 +0,0 @@
{
"id": "audio-vu-meter",
"name": "Audio VU Meter",
"description": "Draws stereo audio level meters from the runtime audio analysis data.",
"category": "Utility",
"entryPoint": "shadeVideo",
"parameters": [
{
"id": "meterPosition",
"label": "Position",
"type": "vec2",
"default": [0.08, 0.82],
"min": [0.0, 0.0],
"max": [1.0, 1.0],
"step": [0.01, 0.01]
},
{
"id": "meterScale",
"label": "Scale",
"type": "float",
"default": 0.35,
"min": 0.1,
"max": 1.0,
"step": 0.01
},
{
"id": "meterOpacity",
"label": "Opacity",
"type": "float",
"default": 0.9,
"min": 0.0,
"max": 1.0,
"step": 0.01
},
{
"id": "noiseGate",
"label": "Noise Gate",
"type": "float",
"default": 0.03,
"min": 0.0,
"max": 0.5,
"step": 0.01
},
{
"id": "meterColor",
"label": "Meter Color",
"type": "color",
"default": [0.2, 1.0, 0.55, 1.0]
},
{
"id": "peakColor",
"label": "Peak Color",
"type": "color",
"default": [1.0, 0.85, 0.2, 1.0]
},
{
"id": "backgroundOpacity",
"label": "Background",
"type": "float",
"default": 0.45,
"min": 0.0,
"max": 1.0,
"step": 0.01
},
{
"id": "orientation",
"label": "Orientation",
"type": "enum",
"default": "horizontal",
"options": [
{ "value": "horizontal", "label": "Horizontal" },
{ "value": "vertical", "label": "Vertical" }
]
}
]
}

View File

@@ -1,59 +0,0 @@
float rectMask(float2 uv, float2 minUv, float2 maxUv)
{
float2 insideMin = step(minUv, uv);
float2 insideMax = step(uv, maxUv);
return insideMin.x * insideMin.y * insideMax.x * insideMax.y;
}
float denoiseLevel(float value)
{
float gate = saturate(noiseGate);
float clean = saturate((value - gate) / max(1.0 - gate, 0.001));
return smoothstep(0.0, 1.0, clean);
}
float4 shadeVideo(ShaderContext context)
{
float4 color = context.sourceColor;
float2 size = orientation == 0 ? float2(meterScale, meterScale * 0.18) : float2(meterScale * 0.18, meterScale);
float2 minUv = clamp(meterPosition, 0.0, 1.0 - size);
float2 local = (context.uv - minUv) / max(size, float2(0.001));
float inside = rectMask(local, float2(0.0), float2(1.0));
if (inside <= 0.0)
return color;
float3 bg = lerp(color.rgb, float3(0.0), saturate(backgroundOpacity));
float leftLevel = denoiseLevel(context.audioRms.x * 2.4);
float rightLevel = denoiseLevel(context.audioRms.y * 2.4);
float leftPeak = denoiseLevel(context.audioPeak.x);
float rightPeak = denoiseLevel(context.audioPeak.y);
float bar = 0.0;
float peak = 0.0;
if (orientation == 0)
{
float leftRow = rectMask(local, float2(0.04, 0.58), float2(0.96, 0.86));
float rightRow = rectMask(local, float2(0.04, 0.14), float2(0.96, 0.42));
float leftFill = rectMask(local, float2(0.04, 0.58), float2(0.04 + 0.92 * leftLevel, 0.86));
float rightFill = rectMask(local, float2(0.04, 0.14), float2(0.04 + 0.92 * rightLevel, 0.42));
float leftPeakLine = rectMask(local, float2(0.04 + 0.92 * leftPeak - 0.006, 0.55), float2(0.04 + 0.92 * leftPeak + 0.006, 0.89));
float rightPeakLine = rectMask(local, float2(0.04 + 0.92 * rightPeak - 0.006, 0.11), float2(0.04 + 0.92 * rightPeak + 0.006, 0.45));
bar = max(leftFill, rightFill);
peak = max(leftPeakLine * leftRow, rightPeakLine * rightRow);
}
else
{
float leftColumn = rectMask(local, float2(0.14, 0.04), float2(0.42, 0.96));
float rightColumn = rectMask(local, float2(0.58, 0.04), float2(0.86, 0.96));
float leftFill = rectMask(local, float2(0.14, 0.04), float2(0.42, 0.04 + 0.92 * leftLevel));
float rightFill = rectMask(local, float2(0.58, 0.04), float2(0.86, 0.04 + 0.92 * rightLevel));
float leftPeakLine = rectMask(local, float2(0.11, 0.04 + 0.92 * leftPeak - 0.006), float2(0.45, 0.04 + 0.92 * leftPeak + 0.006));
float rightPeakLine = rectMask(local, float2(0.55, 0.04 + 0.92 * rightPeak - 0.006), float2(0.89, 0.04 + 0.92 * rightPeak + 0.006));
bar = max(leftFill * leftColumn, rightFill * rightColumn);
peak = max(leftPeakLine, rightPeakLine);
}
float3 metered = lerp(bg, meterColor.rgb, bar * saturate(meterOpacity) * meterColor.a);
metered = lerp(metered, peakColor.rgb, peak * saturate(meterOpacity) * peakColor.a);
return float4(metered, color.a);
}

View File

@@ -0,0 +1,144 @@
{
"id": "balatro-swirl",
"name": "Balatro Swirl",
"description": "Animated painterly swirl background. Original by localthunk (https://www.playbalatro.com), adapted from https://www.shadertoy.com/view/XXtBRr.",
"category": "Generative",
"entryPoint": "shadeVideo",
"parameters": [
{
"id": "spinRotation",
"label": "Spin Rotation",
"type": "float",
"default": -2,
"min": -8,
"max": 8,
"step": 0.05,
"description": "Base rotation applied to the swirl field."
},
{
"id": "spinSpeed",
"label": "Spin Speed",
"type": "float",
"default": 7,
"min": 0,
"max": 20,
"step": 0.1,
"description": "How quickly the swirl pattern rotates."
},
{
"id": "spinAmount",
"label": "Spin Amount",
"type": "float",
"default": 0.25,
"min": 0,
"max": 1,
"step": 0.01,
"description": "Amount of radial twisting in the swirl."
},
{
"id": "spinEase",
"label": "Spin Ease",
"type": "float",
"default": 1,
"min": 0,
"max": 3,
"step": 0.01,
"description": "Changes how strongly the twist falls off from the center."
},
{
"id": "contrast",
"label": "Contrast",
"type": "float",
"default": 3.5,
"min": 0.5,
"max": 8,
"step": 0.05,
"description": "Adjusts separation between dark and bright areas."
},
{
"id": "lighting",
"label": "Lighting",
"type": "float",
"default": 0.4,
"min": 0,
"max": 1.5,
"step": 0.01,
"description": "Strength of the highlight/shadow modulation."
},
{
"id": "offset",
"label": "Offset",
"type": "vec2",
"default": [
0,
0
],
"min": [
-1,
-1
],
"max": [
1,
1
],
"step": [
0.001,
0.001
],
"description": "Moves the generated field in normalized coordinates."
},
{
"id": "colour1",
"label": "Colour 1",
"type": "color",
"default": [
0.871,
0.267,
0.231,
1
],
"description": "Primary warm swirl color."
},
{
"id": "colour2",
"label": "Colour 2",
"type": "color",
"default": [
0,
0.42,
0.706,
1
],
"description": "Secondary cool swirl color."
},
{
"id": "colour3",
"label": "Colour 3",
"type": "color",
"default": [
0.086,
0.137,
0.145,
1
],
"description": "Dark base color in the swirl."
},
{
"id": "isRotate",
"label": "Rotate Field",
"type": "bool",
"default": false,
"description": "Rotates the whole generated field over time."
},
{
"id": "sourceMix",
"label": "Source Mix",
"type": "float",
"default": 0,
"min": 0,
"max": 1,
"step": 0.01,
"description": "Blends the generated effect with the incoming video."
}
]
}

View File

@@ -0,0 +1,54 @@
float4 balatroSwirl(float2 screenSize, float2 screenCoords, float time, float seed)
{
const float pi = 3.14159265359;
float safeScreenLength = max(length(screenSize), 1.0);
float2 seedOffset = float2(sin(seed * 6.2831853), cos(seed * 6.2831853)) * 0.035;
float2 uv = (screenCoords - 0.5 * screenSize) / safeScreenLength - offset - seedOffset;
float uvLength = length(uv);
// First warp: convert to polar space and twist the angle more near the
// center, creating the large spiral motion.
float speed = spinRotation * spinEase * 0.2;
if (isRotate)
speed = time * speed;
speed += 302.2 + seed * 6.2831853;
float newPixelAngle = atan2(uv.y, uv.x) + speed - spinEase * 20.0 * (spinAmount * uvLength + (1.0 - spinAmount));
float2 mid = (screenSize / safeScreenLength) * 0.5;
uv = float2(uvLength * cos(newPixelAngle) + mid.x, uvLength * sin(newPixelAngle) + mid.y) - mid;
uv *= 30.0;
speed = (time + seed * 17.0) * spinSpeed;
float2 uv2 = float2(uv.x + uv.y, uv.x + uv.y);
// Second warp: a short iterative feedback loop turns the spiral into
// painterly bands while preserving a fixed compile-time loop bound.
for (int i = 0; i < 5; ++i)
{
uv2 += float2(sin(max(uv.x, uv.y)), sin(max(uv.x, uv.y))) + uv;
uv += 0.5 * float2(cos(5.1123314 + 0.353 * uv2.y + speed * 0.131121), sin(uv2.x - 0.113 * speed));
float warp = cos(uv.x + uv.y) - sin(uv.x * 0.711 - uv.y);
uv -= float2(warp, warp);
}
float contrastMod = 0.25 * contrast + 0.5 * spinAmount + 1.2;
float paintRes = min(2.0, max(0.0, length(uv) * 0.035 * contrastMod));
float c1p = max(0.0, 1.0 - contrastMod * abs(1.0 - paintRes));
float c2p = max(0.0, 1.0 - contrastMod * abs(paintRes));
float c3p = 1.0 - min(1.0, c1p + c2p);
// Three soft band weights drive the palette; lighting rides on the brightest
// bands so the swirl keeps dimensional highlights.
float light = (lighting - 0.2) * max(c1p * 5.0 - 4.0, 0.0) + lighting * max(c2p * 5.0 - 4.0, 0.0);
float safeContrast = max(contrast, 0.001);
float4 base = (0.3 / safeContrast) * colour1;
float4 paint = colour1 * c1p + colour2 * c2p + float4(c3p * colour3.rgb, c3p * colour1.a);
return base + (1.0 - 0.3 / safeContrast) * paint + float4(light, light, light, light);
}
float4 shadeVideo(ShaderContext context)
{
float2 screenSize = max(context.outputResolution, float2(1.0, 1.0));
float4 swirl = balatroSwirl(screenSize, context.uv * screenSize, context.time, context.startupRandom);
return saturate(lerp(swirl, context.sourceColor, sourceMix));
}

View File

@@ -2,7 +2,7 @@
"id": "black-and-white",
"name": "Black and White",
"description": "A minimal monochrome shader that converts the decoded video input to grayscale.",
"category": "Built-in",
"category": "Color",
"entryPoint": "shadeVideo",
"parameters": []
}

View File

@@ -0,0 +1,16 @@
{
"id": "broken-shader-example",
"name": "Broken Shader Example",
"description": "Intentionally invalid shader package used to verify that bad shaders appear as errors without blocking the app.",
"category": "Diagnostics",
"entryPoint": "shadeVideo",
"parameters": [
{
"id": "badToggle",
"label": "Bad Toggle",
"type": "boolean",
"default": true,
"description": "Intentionally unsupported parameter type used to test shader error handling."
}
]
}

View File

@@ -0,0 +1,4 @@
float4 shadeVideo(ShaderContext context)
{
return context.sourceColor;
}

View File

@@ -2,62 +2,74 @@
"id": "composition-guides",
"name": "Composition Guides",
"description": "Overlays rule-of-thirds guides and a center crosshair for camera alignment and framing.",
"category": "Utility",
"category": "Scopes & Guides",
"entryPoint": "shadeVideo",
"parameters": [
{
"id": "showThirds",
"label": "Rule of Thirds",
"type": "bool",
"default": true
"default": true,
"description": "Shows vertical and horizontal thirds lines."
},
{
"id": "showCrosshair",
"label": "Center Crosshair",
"type": "bool",
"default": true
"default": true,
"description": "Shows a center crosshair for lens/framing alignment."
},
{
"id": "lineColor",
"label": "Line Color",
"type": "color",
"default": [1.0, 1.0, 1.0, 1.0]
"default": [
1,
1,
1,
1
],
"description": "Color used for guide lines and marks."
},
{
"id": "lineOpacity",
"label": "Line Opacity",
"type": "float",
"default": 0.65,
"min": 0.0,
"max": 1.0,
"step": 0.01
"min": 0,
"max": 1,
"step": 0.01,
"description": "Overall visibility of the guide lines."
},
{
"id": "lineThicknessPixels",
"label": "Line Thickness",
"type": "float",
"default": 2.0,
"default": 2,
"min": 0.5,
"max": 12.0,
"step": 0.1
"max": 12,
"step": 0.1,
"description": "Guide line width in output pixels."
},
{
"id": "crosshairSizePixels",
"label": "Crosshair Size",
"type": "float",
"default": 54.0,
"min": 8.0,
"max": 240.0,
"step": 1.0
"default": 54,
"min": 8,
"max": 240,
"step": 1,
"description": "Length of each crosshair arm in output pixels."
},
{
"id": "crosshairGapPixels",
"label": "Crosshair Gap",
"type": "float",
"default": 10.0,
"min": 0.0,
"max": 80.0,
"step": 1.0
"default": 10,
"min": 0,
"max": 80,
"step": 1,
"description": "Empty gap around the exact frame center."
}
]
}

View File

@@ -0,0 +1,102 @@
{
"id": "crt-bulge",
"name": "CRT Bulge",
"description": "Warps the image like convex CRT glass, with optional rounded screen edges and vignette darkening.",
"category": "Distortion",
"entryPoint": "shadeVideo",
"parameters": [
{
"id": "bulgeAmount",
"label": "Bulge",
"type": "float",
"default": -0.04,
"min": -0.5,
"max": 0.8,
"step": 0.01,
"description": "Positive values swell the center outward; negative values pinch it inward."
},
{
"id": "zoom",
"label": "Zoom",
"type": "float",
"default": 1.04,
"min": 0.5,
"max": 2,
"step": 0.01,
"description": "Scales the source before distortion, useful for hiding warped edges."
},
{
"id": "edgeRoundness",
"label": "Edge Roundness",
"type": "float",
"default": 0.08,
"min": 0,
"max": 0.35,
"step": 0.01,
"description": "Rounds the visible screen corners like older CRT glass."
},
{
"id": "edgeFeather",
"label": "Edge Feather",
"type": "float",
"default": 2,
"min": 0,
"max": 24,
"step": 0.1,
"description": "Softens the rounded screen edge in pixels."
},
{
"id": "sourceEdgeFeather",
"label": "Source Edge Feather",
"type": "float",
"default": 1.5,
"min": 0,
"max": 16,
"step": 0.1,
"description": "Antialiases warped source edges when the distortion reveals outside-frame pixels."
},
{
"id": "vignetteAmount",
"label": "Vignette",
"type": "float",
"default": 0.18,
"min": 0,
"max": 1,
"step": 0.01,
"description": "Darkens the glass toward the screen edges."
},
{
"id": "edgeMode",
"label": "Edge Mode",
"type": "enum",
"default": "black",
"options": [
{
"value": "black",
"label": "Black"
},
{
"value": "clamp",
"label": "Clamp"
},
{
"value": "mirror",
"label": "Mirror"
}
],
"description": "Chooses how warped samples outside the source frame are filled."
},
{
"id": "outsideColor",
"label": "Outside Color",
"type": "color",
"default": [
0,
0,
0,
1
],
"description": "Color used outside the curved screen or source frame."
}
]
}

View File

@@ -0,0 +1,71 @@
float mirroredCoordinate(float coordinate)
{
float wrapped = frac(coordinate * 0.5) * 2.0;
return wrapped <= 1.0 ? wrapped : 2.0 - wrapped;
}
float roundedBoxMask(float2 point, float2 halfSize, float radius, float feather)
{
float2 distanceToEdge = abs(point) - (halfSize - radius);
float outsideDistance = length(max(distanceToEdge, float2(0.0, 0.0))) - radius;
float insideDistance = min(max(distanceToEdge.x, distanceToEdge.y), 0.0);
float signedDistance = outsideDistance + insideDistance;
return 1.0 - smoothstep(0.0, max(feather, 0.00001), signedDistance);
}
float sourceBoundsMask(float2 uv, float2 resolution)
{
float2 pixel = 1.0 / max(resolution, float2(1.0, 1.0));
float2 feather = pixel * max(sourceEdgeFeather, 0.0);
float left = smoothstep(0.0, max(feather.x, 0.00001), uv.x);
float right = 1.0 - smoothstep(1.0 - max(feather.x, 0.00001), 1.0, uv.x);
float top = smoothstep(0.0, max(feather.y, 0.00001), uv.y);
float bottom = 1.0 - smoothstep(1.0 - max(feather.y, 0.00001), 1.0, uv.y);
return saturate(left * right * top * bottom);
}
float2 applyBulge(float2 uv, float2 resolution)
{
float2 centered = uv * 2.0 - 1.0;
float aspect = resolution.x / max(resolution.y, 1.0);
float2 aspectCentered = float2(centered.x * aspect, centered.y);
float radiusSq = dot(aspectCentered, aspectCentered);
float amount = clamp(bulgeAmount, -0.95, 0.95);
float scale = 1.0 / max(1.0 + amount * radiusSq, 0.05);
return centered * scale / max(zoom, 0.001) * 0.5 + 0.5;
}
float4 sampleWarped(float2 uv, float2 resolution, out bool insideSource)
{
insideSource = uv.x >= 0.0 && uv.x <= 1.0 && uv.y >= 0.0 && uv.y <= 1.0;
if (edgeMode == 1)
return sampleLayerInput(clamp(uv, 0.0, 1.0));
if (edgeMode == 2)
return sampleLayerInput(float2(mirroredCoordinate(uv.x), mirroredCoordinate(uv.y)));
float edgeMask = sourceBoundsMask(uv, resolution);
float4 color = sampleLayerInput(clamp(uv, 0.0, 1.0));
return lerp(outsideColor, color, edgeMask);
}
float4 shadeVideo(ShaderContext context)
{
float2 resolution = max(context.outputResolution, float2(1.0, 1.0));
float2 sourceUv = applyBulge(context.uv, resolution);
bool insideSource = false;
float4 color = sampleWarped(sourceUv, resolution, insideSource);
float2 centered = context.uv * 2.0 - 1.0;
float feather = max(edgeFeather, 0.0) / min(resolution.x, resolution.y);
float screenMask = roundedBoxMask(centered, float2(1.0, 1.0), saturate(edgeRoundness), feather);
color = lerp(outsideColor, color, screenMask);
float2 aspectCentered = float2(centered.x * resolution.x / max(resolution.y, 1.0), centered.y);
float edgeDistance = saturate(length(aspectCentered) * 0.72);
float vignette = lerp(1.0, 1.0 - saturate(vignetteAmount), smoothstep(0.35, 1.05, edgeDistance));
color.rgb *= vignette;
return saturate(color);
}

View File

@@ -15,36 +15,52 @@
"label": "Amount",
"type": "float",
"default": 0.45,
"min": 0.0,
"max": 1.0,
"step": 0.01
"min": 0,
"max": 1,
"step": 0.01,
"description": "Strength of the blocky temporal smear."
},
{
"id": "blockCount",
"label": "Block Count",
"type": "vec2",
"default": [32.0, 18.0],
"min": [2.0, 2.0],
"max": [160.0, 120.0],
"step": [1.0, 1.0]
"default": [
32,
18
],
"min": [
2,
2
],
"max": [
160,
120
],
"step": [
1,
1
],
"description": "Number of glitch blocks across X and Y."
},
{
"id": "tearAmount",
"label": "Tear",
"type": "float",
"default": 0.18,
"min": 0.0,
"max": 1.0,
"step": 0.01
"min": 0,
"max": 1,
"step": 0.01,
"description": "Horizontal scanline tearing intensity."
},
{
"id": "chromaShift",
"label": "Chroma Shift",
"type": "float",
"default": 1.8,
"min": 0.0,
"max": 12.0,
"step": 0.1
"min": 0,
"max": 12,
"step": 0.1,
"description": "Separate color-channel offset in pixels."
}
]
}

View File

@@ -7,12 +7,14 @@ float4 shadeVideo(ShaderContext context)
{
float2 blocks = max(blockCount, float2(1.0, 1.0));
float2 blockId = floor(context.uv * blocks);
float n = hash21(blockId + floor(context.time * 8.0));
float seed = context.startupRandom * 4096.0;
float frameSeed = floor(context.time * 8.0);
float n = hash21(blockId + float2(frameSeed + seed, frameSeed + seed * 0.17));
float historyFrame = floor(lerp(1.0, 7.0, n));
float rowNoise = hash21(float2(floor(context.uv.y * blocks.y), floor(context.time * 5.0)));
float rowNoise = hash21(float2(floor(context.uv.y * blocks.y) + seed * 0.37, floor(context.time * 5.0) + seed * 0.13));
float tear = (rowNoise * 2.0 - 1.0) * tearAmount * amount * 0.08;
float2 offset = float2(tear, (hash21(blockId + 19.0) * 2.0 - 1.0) * amount * 0.025);
float2 offset = float2(tear, (hash21(blockId + float2(19.0 + seed, 19.0 + seed * 0.31)) * 2.0 - 1.0) * amount * 0.025);
float2 moshedUv = clamp(context.uv + offset, 0.0, 1.0);
float4 previous = sampleTemporalHistory(int(historyFrame), moshedUv);

View File

@@ -2,7 +2,7 @@
"id": "dvd-bounce",
"name": "DVD Bounce",
"description": "A transparent bouncing DVD logo sprite that changes color on each screen hit.",
"category": "Built-in",
"category": "Generative",
"entryPoint": "shadeVideo",
"textures": [
{
@@ -18,7 +18,8 @@
"default": 0.28,
"min": 0.12,
"max": 0.5,
"step": 0.01
"step": 0.01,
"description": "Logo size relative to the frame."
},
{
"id": "bounceSpeed",
@@ -27,34 +28,38 @@
"default": 0.22,
"min": 0.02,
"max": 0.8,
"step": 0.01
"step": 0.01,
"description": "How fast the logo moves between edge hits."
},
{
"id": "edgePadding",
"label": "Edge Padding",
"type": "float",
"default": 0.018,
"min": 0.0,
"min": 0,
"max": 0.08,
"step": 0.001
"step": 0.001,
"description": "Inset distance from the frame edges."
},
{
"id": "glowAmount",
"label": "Glow",
"type": "float",
"default": 0.18,
"min": 0.0,
"min": 0,
"max": 0.75,
"step": 0.01
"step": 0.01,
"description": "Adds a soft colored glow around the logo."
},
{
"id": "baseAlpha",
"label": "Alpha",
"type": "float",
"default": 1.0,
"default": 1,
"min": 0.05,
"max": 1.0,
"step": 0.01
"max": 1,
"step": 0.01,
"description": "Overall opacity of the overlay."
}
]
}

View File

@@ -29,7 +29,8 @@ float4 shadeVideo(ShaderContext context)
float2 velocityPx = float2(
max(20.0, bounceSpeed * minDimension * 1.00),
max(24.0, bounceSpeed * minDimension * 0.77));
float2 motionPx = context.time * velocityPx;
float seed = context.startupRandom;
float2 motionPx = (float2(context.time, context.time) + float2(seed * 32.7, seed * 40.3)) * velocityPx;
float2 centerPx = minCenterPx + float2(
pingPong(motionPx.x, travelPx.x),
pingPong(motionPx.y, travelPx.y));
@@ -38,7 +39,7 @@ float4 shadeVideo(ShaderContext context)
int yHits = int(floor(motionPx.y / max(travelPx.y, 1.0)));
int totalHits = max(0, xHits + yHits);
float hue = frac(0.09 + float(totalHits) * 0.173);
float hue = frac(0.09 + seed * 0.71 + float(totalHits) * 0.173);
float3 badgeColor = hsvToRgb(float3(hue, 0.86, 1.0));
float3 glowColor = hsvToRgb(float3(frac(hue + 0.06), 0.72, 1.0));

115
shaders/ether/shader.json Normal file
View File

@@ -0,0 +1,115 @@
{
"id": "ether",
"name": "Ether",
"description": "Raymarched ether field. Original by nimitz 2014 (twitter: @stormoid), adapted from https://www.shadertoy.com/view/MsjSW3.",
"category": "Generative",
"entryPoint": "shadeVideo",
"parameters": [
{
"id": "speed",
"label": "Speed",
"type": "float",
"default": 1,
"min": 0,
"max": 4,
"step": 0.01,
"description": "Animation speed multiplier; set to 0 to pause motion."
},
{
"id": "depth",
"label": "Depth",
"type": "float",
"default": 2.5,
"min": 0.2,
"max": 8,
"step": 0.01,
"description": "Raymarch depth through the ether volume."
},
{
"id": "density",
"label": "Density",
"type": "float",
"default": 0.7,
"min": 0,
"max": 2,
"step": 0.01,
"description": "Density of the volumetric strands."
},
{
"id": "brightness",
"label": "Brightness",
"type": "float",
"default": 1,
"min": 0,
"max": 3,
"step": 0.01,
"description": "Adjusts the generated effect brightness."
},
{
"id": "contrast",
"label": "Contrast",
"type": "float",
"default": 1,
"min": 0.25,
"max": 3,
"step": 0.01,
"description": "Adjusts separation between dark and bright areas."
},
{
"id": "offset",
"label": "Offset",
"type": "vec2",
"default": [
0.9,
0.5
],
"min": [
0,
0
],
"max": [
2,
2
],
"step": [
0.001,
0.001
],
"description": "Moves the generated field in normalized coordinates."
},
{
"id": "baseColor",
"label": "Base Color",
"type": "color",
"default": [
0.1,
0.3,
0.4,
1
],
"description": "Low-energy color used in the generated field."
},
{
"id": "energyColor",
"label": "Energy Color",
"type": "color",
"default": [
1,
0.5,
0.6,
1
],
"description": "High-energy color used in the generated field."
},
{
"id": "sourceMix",
"label": "Source Mix",
"type": "float",
"default": 0,
"min": 0,
"max": 1,
"step": 0.01,
"description": "Blends the generated effect with the incoming video."
}
]
}

View File

@@ -0,0 +1,41 @@
float2x2 rotation2(float angle)
{
float c = cos(angle);
float s = sin(angle);
return float2x2(c, -s, s, c);
}
float etherMap(float3 p, float time)
{
p.xz = mul(rotation2(time * 0.4), p.xz);
p.xy = mul(rotation2(time * 0.3), p.xy);
float3 q = p * 2.0 + time;
float wave = sin(q.x + sin(q.z + sin(q.y))) * 0.5;
return length(p + float3(sin(time * 0.7), sin(time * 0.7), sin(time * 0.7))) * log(length(p) + 1.0) + wave - 1.0;
}
float4 shadeVideo(ShaderContext context)
{
float2 resolution = max(context.outputResolution, float2(1.0, 1.0));
float2 fragCoord = context.uv * resolution;
float2 p = fragCoord / resolution.y - offset;
float seed = context.startupRandom;
float time = context.time * speed + seed * 41.0;
float3 color = float3(0.0, 0.0, 0.0);
float d = depth;
for (int i = 0; i <= 5; ++i)
{
float3 rayPosition = float3(seed * 0.7 - 0.35, 0.23 - seed * 0.46, 5.0) + normalize(float3(p, -1.0)) * d;
float rz = etherMap(rayPosition, time);
float f = clamp((rz - etherMap(rayPosition + float3(0.1, 0.1, 0.1), time)) * 0.5, -0.1, 1.0);
float3 light = baseColor.rgb + energyColor.rgb * 5.0 * f;
color = color * light + smoothstep(2.5, 0.0, rz) * density * light;
d += min(rz, 1.0);
}
color = pow(max(color * brightness, float3(0.0, 0.0, 0.0)), float3(1.0 / max(contrast, 0.001)));
return saturate(lerp(float4(color, 1.0), context.sourceColor, sourceMix));
}

View File

@@ -2,41 +2,45 @@
"id": "false-color",
"name": "False Color",
"description": "Maps luminance ranges to exposure-assist colors for camera and shader debugging.",
"category": "Utility",
"category": "Color",
"entryPoint": "shadeVideo",
"parameters": [
{
"id": "blendAmount",
"label": "Blend",
"type": "float",
"default": 1.0,
"min": 0.0,
"max": 1.0,
"step": 0.01
"default": 1,
"min": 0,
"max": 1,
"step": 0.01,
"description": "Mix amount for the processed result."
},
{
"id": "showLuma",
"label": "Show Luma",
"type": "bool",
"default": false
"default": false,
"description": "Shows grayscale luminance before applying false-color mapping."
},
{
"id": "lift",
"label": "Lift",
"type": "float",
"default": 0.0,
"default": 0,
"min": -0.25,
"max": 0.25,
"step": 0.001
"step": 0.001,
"description": "Offsets luminance before false-color mapping."
},
{
"id": "gain",
"label": "Gain",
"type": "float",
"default": 1.0,
"default": 1,
"min": 0.25,
"max": 2.0,
"step": 0.01
"max": 2,
"step": 0.01,
"description": "Scales luminance before false-color mapping."
}
]
}

View File

@@ -0,0 +1,66 @@
{
"id": "feedback-data-blocks",
"name": "Feedback Data Blocks",
"description": "Demonstrates coarse shader-local data storage by reserving eight 3x3 feedback cells for sampled colors and one hidden metadata cell for refresh state.",
"category": "Feedback",
"entryPoint": "storeProbeData",
"passes": [
{
"id": "store",
"source": "shader.slang",
"entryPoint": "storeProbeData",
"output": "dataBuffer"
},
{
"id": "display",
"source": "shader.slang",
"entryPoint": "displayProbeData",
"inputs": [
"dataBuffer"
],
"output": "layerOutput"
}
],
"feedback": {
"enabled": true,
"writePass": "store"
},
"parameters": [
{
"id": "refresh",
"label": "Refresh",
"type": "trigger",
"description": "Forces the stored probe colors to resample immediately."
},
{
"id": "refreshSeconds",
"label": "Refresh Seconds",
"type": "float",
"default": 15.0,
"min": 1.0,
"max": 60.0,
"step": 0.1,
"description": "Automatic interval for resampling all stored probe colors."
},
{
"id": "overlayOpacity",
"label": "Overlay Opacity",
"type": "float",
"default": 1.0,
"min": 0.0,
"max": 1.0,
"step": 0.01,
"description": "Strength of the swatch overlay drawn from the stored data cells."
},
{
"id": "swatchSize",
"label": "Swatch Size",
"type": "vec2",
"default": [0.045, 0.055],
"min": [0.02, 0.02],
"max": [0.12, 0.12],
"step": [0.001, 0.001],
"description": "Size of the top-left preview swatches that show the stored cell values."
}
]
}

View File

@@ -0,0 +1,152 @@
static const int kProbeCount = 8;
static const int kMetadataIndex = 8;
float2 probeUvForIndex(int index)
{
if (index == 0)
return float2(0.18, 0.28);
if (index == 1)
return float2(0.39, 0.28);
if (index == 2)
return float2(0.61, 0.28);
if (index == 3)
return float2(0.82, 0.28);
if (index == 4)
return float2(0.18, 0.72);
if (index == 5)
return float2(0.39, 0.72);
if (index == 6)
return float2(0.61, 0.72);
return float2(0.82, 0.72);
}
float2 cellCenterPixelForIndex(int index)
{
return float2(1.0 + float(index) * 3.0, 1.0);
}
float2 cellCenterUvForIndex(ShaderContext context, int index)
{
return (cellCenterPixelForIndex(index) + 0.5) / context.outputResolution;
}
bool pixelIsInsideCell(float2 pixelCoord, int index)
{
float minX = float(index) * 3.0;
float maxX = minX + 3.0;
return pixelCoord.x >= minX && pixelCoord.x < maxX && pixelCoord.y >= 0.0 && pixelCoord.y < 3.0;
}
float4 readStoredCell(ShaderContext context, int index)
{
if (context.feedbackAvailable <= 0)
return float4(0.0, 0.0, 0.0, 0.0);
return sampleFeedback(cellCenterUvForIndex(context, index));
}
bool shouldRefreshStoredData(ShaderContext context)
{
if (context.feedbackAvailable <= 0)
return true;
float4 metadata = readStoredCell(context, kMetadataIndex);
float previousRefreshBucket = metadata.r;
float previousTriggerCount = metadata.g;
float refreshInterval = max(refreshSeconds, 0.001);
float currentRefreshBucket = floor(context.time / refreshInterval);
float currentTriggerCount = float(refresh);
return currentRefreshBucket > previousRefreshBucket + 0.5 || currentTriggerCount > previousTriggerCount + 0.5;
}
float4 metadataValueForFrame(ShaderContext context, bool refreshNow)
{
float refreshInterval = max(refreshSeconds, 0.001);
float currentRefreshBucket = floor(context.time / refreshInterval);
float currentTriggerCount = float(refresh);
if (!refreshNow && context.feedbackAvailable > 0)
return readStoredCell(context, kMetadataIndex);
return float4(currentRefreshBucket, currentTriggerCount, refreshTime, 1.0);
}
float4 storedProbeValueForFrame(ShaderContext context, int index, bool refreshNow)
{
float3 liveColor = sampleLayerInput(probeUvForIndex(index)).rgb;
if (refreshNow || context.feedbackAvailable <= 0)
return float4(liveColor, 1.0);
return readStoredCell(context, index);
}
float4 storeProbeData(ShaderContext context)
{
// Reserve nine 3x3 texel cells along the top edge of the feedback surface:
// eight cells for visible probe colors and one hidden metadata cell that
// tracks the timed refresh bucket and last trigger count.
float2 pixelCoord = floor(context.uv * context.outputResolution);
bool refreshNow = shouldRefreshStoredData(context);
for (int index = 0; index < kProbeCount; ++index)
{
if (pixelIsInsideCell(pixelCoord, index))
return storedProbeValueForFrame(context, index, refreshNow);
}
if (pixelIsInsideCell(pixelCoord, kMetadataIndex))
return metadataValueForFrame(context, refreshNow);
return float4(0.0, 0.0, 0.0, 1.0);
}
float rectMask(float2 uv, float2 minUv, float2 maxUv)
{
if (uv.x < minUv.x || uv.x > maxUv.x)
return 0.0;
if (uv.y < minUv.y || uv.y > maxUv.y)
return 0.0;
return 1.0;
}
float borderMask(float2 uv, float2 minUv, float2 maxUv, float thickness)
{
float outer = rectMask(uv, minUv, maxUv);
float inner = rectMask(uv, minUv + thickness, maxUv - thickness);
return saturate(outer - inner);
}
float4 displayProbeData(ShaderContext context)
{
float3 baseColor = sampleLayerInput(context.uv).rgb;
float3 swatchColor = baseColor;
float swatchMask = 0.0;
float2 panelOrigin = float2(0.03, 0.04);
float2 gap = float2(swatchSize.x + 0.012, swatchSize.y + 0.012);
float borderThickness = min(swatchSize.x, swatchSize.y) * 0.08;
for (int index = 0; index < kProbeCount; ++index)
{
int column = index % 4;
int row = index / 4;
float2 swatchMin = panelOrigin + float2(float(column) * gap.x, float(row) * gap.y);
float2 swatchMax = swatchMin + swatchSize;
float3 storedColor = sampleVideo(cellCenterUvForIndex(context, index)).rgb;
float fill = rectMask(context.uv, swatchMin, swatchMax);
float outline = borderMask(context.uv, swatchMin, swatchMax, borderThickness);
if (fill > 0.5)
{
swatchColor = storedColor;
swatchMask = 1.0;
}
if (outline > 0.5)
{
swatchColor = float3(0.0, 0.0, 0.0);
swatchMask = 1.0;
}
}
float opacity = saturate(overlayOpacity) * swatchMask;
float3 displayColor = lerp(baseColor, swatchColor, opacity);
return float4(saturate(displayColor), 1.0);
}

View File

@@ -0,0 +1,110 @@
{
"id": "feedback-highlight-accumulator",
"name": "Feedback Background Memory",
"description": "Learns a persistent per-pixel background plate in shader-local feedback and compares the live frame against that evolving full-frame state.",
"category": "Feedback",
"entryPoint": "updateBackgroundModel",
"passes": [
{
"id": "background",
"source": "shader.slang",
"entryPoint": "updateBackgroundModel",
"output": "backgroundModel"
},
{
"id": "display",
"source": "shader.slang",
"entryPoint": "displayBackgroundDifference",
"inputs": [
"backgroundModel"
],
"output": "layerOutput"
}
],
"feedback": {
"enabled": true,
"writePass": "background"
},
"parameters": [
{
"id": "learnRate",
"label": "Learn Rate",
"type": "float",
"default": 0.03,
"min": 0.001,
"max": 0.5,
"step": 0.001,
"description": "How quickly the stored background model adapts toward the current frame."
},
{
"id": "differenceThreshold",
"label": "Difference Threshold",
"type": "float",
"default": 0.12,
"min": 0.001,
"max": 1.0,
"step": 0.001,
"description": "Minimum difference between the live frame and stored background before the overlay becomes visible."
},
{
"id": "softness",
"label": "Threshold Softness",
"type": "float",
"default": 0.08,
"min": 0.001,
"max": 0.5,
"step": 0.001,
"description": "Softens the transition around the difference threshold."
},
{
"id": "overlayOpacity",
"label": "Overlay Opacity",
"type": "float",
"default": 0.85,
"min": 0.0,
"max": 1.0,
"step": 0.01,
"description": "Strength of the motion/difference overlay on top of the live image."
},
{
"id": "backgroundMix",
"label": "Background Mix",
"type": "float",
"default": 0.15,
"min": 0.0,
"max": 1.0,
"step": 0.01,
"description": "Amount of the learned background model shown underneath the live source."
},
{
"id": "overlayTint",
"label": "Overlay Tint",
"type": "color",
"default": [
1.0,
0.45,
0.08,
1.0
],
"min": [
0.0,
0.0,
0.0,
0.0
],
"max": [
1.0,
1.0,
1.0,
1.0
],
"step": [
0.01,
0.01,
0.01,
0.01
],
"description": "Tint used for areas that differ from the learned background."
}
]
}

View File

@@ -0,0 +1,39 @@
float luminance(float3 color)
{
return dot(color, float3(0.2126, 0.7152, 0.0722));
}
float4 updateBackgroundModel(ShaderContext context)
{
float3 liveColor = context.sourceColor.rgb;
if (context.feedbackAvailable <= 0)
return float4(liveColor, 1.0);
float3 previousBackground = sampleFeedback(context.uv).rgb;
float rate = saturate(learnRate);
float3 nextBackground = lerp(previousBackground, liveColor, rate);
return float4(saturate(nextBackground), 1.0);
}
float4 displayBackgroundDifference(ShaderContext context)
{
// In the display pass, context.sourceColor is the same-frame background
// model produced by updateBackgroundModel().
float3 backgroundModel = context.sourceColor.rgb;
float3 liveColor = sampleLayerInput(context.uv).rgb;
float3 delta = abs(liveColor - backgroundModel);
float difference = max(delta.r, max(delta.g, delta.b));
float thresholdWidth = max(softness, 0.0001);
float motionMask = smoothstep(
differenceThreshold - thresholdWidth,
differenceThreshold + thresholdWidth,
difference);
float3 baseColor = lerp(liveColor, backgroundModel, saturate(backgroundMix));
float3 overlayColor = overlayTint.rgb * max(luminance(liveColor), 0.15);
float overlayAmount = motionMask * saturate(overlayOpacity) * overlayTint.a;
float3 displayColor = lerp(baseColor, baseColor + overlayColor, overlayAmount);
return float4(saturate(displayColor), 1.0);
}

View File

@@ -0,0 +1,150 @@
{
"id": "fisheye-equirectangular-mirror",
"name": "Fisheye Equirectangular Mirror",
"description": "Unwraps a single width-filled 16:9 fisheye lens into a 360x180 equirectangular map by mirroring the rear hemisphere into the same fisheye source.",
"category": "Projection",
"entryPoint": "shadeVideo",
"parameters": [
{
"id": "lensFovDegrees",
"label": "Lens FOV",
"type": "float",
"default": 190,
"min": 1,
"max": 220,
"step": 0.1,
"description": "Actual fisheye lens field of view in degrees."
},
{
"id": "center",
"label": "Optical Center",
"type": "vec2",
"default": [
0.5,
0.5
],
"min": [
0,
0
],
"max": [
1,
1
],
"step": [
0.001,
0.001
],
"description": "Normalized position in the frame, where 0.5, 0.5 is center."
},
{
"id": "radius",
"label": "Fisheye Radius",
"type": "vec2",
"default": [
0.5,
0.8889
],
"min": [
0.001,
0.001
],
"max": [
2,
2
],
"step": [
0.001,
0.001
],
"description": "Normalized fisheye radius; adjust X/Y when the image is cropped or squeezed."
},
{
"id": "yawDegrees",
"label": "Yaw",
"type": "float",
"default": 0,
"min": -180,
"max": 180,
"step": 0.1,
"description": "Rotates the virtual view horizontally."
},
{
"id": "pitchDegrees",
"label": "Pitch",
"type": "float",
"default": 0,
"min": -120,
"max": 120,
"step": 0.1,
"description": "Rotates the virtual view vertically."
},
{
"id": "rollDegrees",
"label": "Roll",
"type": "float",
"default": 0,
"min": -180,
"max": 180,
"step": 0.1,
"description": "Live roll rotation around the viewing axis."
},
{
"id": "fisheyeModel",
"label": "Fisheye Model",
"type": "enum",
"default": "equidistant",
"options": [
{
"value": "equidistant",
"label": "Equidistant"
},
{
"value": "equisolid",
"label": "Equisolid"
},
{
"value": "stereographic",
"label": "Stereographic"
},
{
"value": "orthographic",
"label": "Orthographic"
}
],
"description": "Projection model used by the physical fisheye lens."
},
{
"id": "edgeFill",
"label": "Edge Fill",
"type": "float",
"default": 0.06,
"min": 0,
"max": 0.3,
"step": 0.001,
"description": "Extends edge samples outward to cover small missing areas."
},
{
"id": "edgeBlur",
"label": "Edge Blur",
"type": "float",
"default": 0.018,
"min": 0,
"max": 0.12,
"step": 0.001,
"description": "Softens the dilated edge fill."
},
{
"id": "outsideColor",
"label": "Outside Color",
"type": "color",
"default": [
0,
0,
0,
1
],
"description": "Color used where the remapped image samples outside the source frame."
}
]
}

View File

@@ -0,0 +1,133 @@
static const float PI = 3.14159265358979323846;
static const float TWO_PI = 6.28318530717958647692;
float radiansFromDegrees(float degrees)
{
return degrees * (PI / 180.0);
}
float3 rotateX(float3 ray, float angle)
{
float s = sin(angle);
float c = cos(angle);
return float3(ray.x, c * ray.y - s * ray.z, s * ray.y + c * ray.z);
}
float3 rotateY(float3 ray, float angle)
{
float s = sin(angle);
float c = cos(angle);
return float3(c * ray.x + s * ray.z, ray.y, -s * ray.x + c * ray.z);
}
float3 rotateZ(float3 ray, float angle)
{
float s = sin(angle);
float c = cos(angle);
return float3(c * ray.x - s * ray.y, s * ray.x + c * ray.y, ray.z);
}
float normalizedFisheyeRadius(float theta, float halfFov)
{
float safeHalfFov = max(halfFov, 0.0001);
// Match common fisheye projection families while keeping the selected FOV
// normalized to the same source-image radius.
if (fisheyeModel == 1)
{
return sin(theta * 0.5) / max(sin(safeHalfFov * 0.5), 0.0001);
}
else if (fisheyeModel == 2)
{
return tan(theta * 0.5) / max(tan(safeHalfFov * 0.5), 0.0001);
}
else if (fisheyeModel == 3)
{
return sin(theta) / max(sin(safeHalfFov), 0.0001);
}
return theta / safeHalfFov;
}
float3 equirectangularRay(float2 uv)
{
// Convert equirectangular UVs into longitude/latitude on the unit sphere.
float longitude = (uv.x - 0.5) * TWO_PI;
float latitude = (0.5 - uv.y) * PI;
float latitudeCos = cos(latitude);
return normalize(float3(
sin(longitude) * latitudeCos,
sin(latitude),
cos(longitude) * latitudeCos
));
}
float sourceUvOutsideDistance(float2 uv)
{
float2 lower = max(-uv, float2(0.0, 0.0));
float2 upper = max(uv - 1.0, float2(0.0, 0.0));
return max(max(lower.x, lower.y), max(upper.x, upper.y));
}
float4 sampleEdgeFilledVideo(float2 sourceUv, ShaderContext context)
{
float outsideDistance = sourceUvOutsideDistance(sourceUv);
if (outsideDistance <= 0.0)
return sampleVideo(sourceUv);
float fillDistance = max(edgeFill, 0.0);
if (outsideDistance > fillDistance)
return outsideColor;
float2 clampedUv = saturate(sourceUv);
float2 inward = clampedUv - sourceUv;
float inwardLength = max(length(inward), 0.000001);
inward /= inwardLength;
// Outside the fisheye image, sample back inward from the nearest edge so the
// fill looks like stretched lens content instead of a hard color plate.
float blurDistance = max(edgeBlur, 0.0);
float4 color = sampleVideo(clampedUv) * 0.32;
color += sampleVideo(saturate(clampedUv + inward * blurDistance * 0.35)) * 0.26;
color += sampleVideo(saturate(clampedUv + inward * blurDistance * 0.75)) * 0.20;
color += sampleVideo(saturate(clampedUv + inward * blurDistance * 1.20)) * 0.14;
color += sampleVideo(saturate(clampedUv + inward * blurDistance * 1.75)) * 0.08;
float edgeFade = smoothstep(fillDistance * 0.78, fillDistance, outsideDistance);
return lerp(color, outsideColor, edgeFade);
}
float4 shadeVideo(ShaderContext context)
{
float3 ray = equirectangularRay(context.uv);
ray = rotateZ(ray, radiansFromDegrees(rollDegrees));
ray = rotateX(ray, radiansFromDegrees(-pitchDegrees));
ray = rotateY(ray, radiansFromDegrees(yawDegrees));
// Mirror the rear hemisphere into the front-facing fisheye image so one
// circular lens source fills both halves of the equirectangular output.
ray.z = abs(ray.z);
ray = normalize(ray);
float halfFov = radiansFromDegrees(clamp(lensFovDegrees, 1.0, 220.0) * 0.5);
float theta = acos(clamp(ray.z, -1.0, 1.0));
if (theta > halfFov)
return outsideColor;
float phi = atan2(ray.y, ray.x);
float fisheyeRadius = normalizedFisheyeRadius(theta, halfFov);
// Project the mirrored sphere ray back into the circular fisheye source.
float2 sourceUv = float2(
center.x + cos(phi) * fisheyeRadius * radius.x,
center.y - sin(phi) * fisheyeRadius * radius.y
);
float2 guard = 0.5 / max(context.inputResolution, float2(1.0, 1.0));
if (edgeFill <= 0.0 && (sourceUv.x < -guard.x || sourceUv.x > 1.0 + guard.x || sourceUv.y < -guard.y || sourceUv.y > 1.0 + guard.y))
return outsideColor;
return sampleEdgeFilledVideo(sourceUv, context);
}

View File

@@ -9,91 +9,145 @@
"id": "lensFovDegrees",
"label": "Lens FOV",
"type": "float",
"default": 190.0,
"min": 1.0,
"max": 220.0,
"step": 0.1
"default": 190,
"min": 1,
"max": 220,
"step": 0.1,
"description": "Actual fisheye lens field of view in degrees."
},
{
"id": "center",
"label": "Optical Center",
"type": "vec2",
"default": [0.5, 0.5],
"min": [0.0, 0.0],
"max": [1.0, 1.0],
"step": [0.001, 0.001]
"default": [
0.5,
0.5
],
"min": [
0,
0
],
"max": [
1,
1
],
"step": [
0.001,
0.001
],
"description": "Normalized position in the frame, where 0.5, 0.5 is center."
},
{
"id": "radius",
"label": "Fisheye Radius",
"type": "vec2",
"default": [0.5, 0.885],
"min": [0.001, 0.001],
"max": [2.0, 2.0],
"step": [0.001, 0.001]
"default": [
0.5,
0.885
],
"min": [
0.001,
0.001
],
"max": [
2,
2
],
"step": [
0.001,
0.001
],
"description": "Normalized fisheye radius; adjust X/Y when the image is cropped or squeezed."
},
{
"id": "sourceEdgeCut",
"label": "Source Edge Cut",
"type": "float",
"default": 0.01,
"min": 0,
"max": 0.2,
"step": 0.001,
"description": "Cuts slightly inward from all four source-frame edges before sampling to hide empty border regions."
},
{
"id": "sourceEdgeFeather",
"label": "Source Edge Feather",
"type": "float",
"default": 0.02,
"min": 0,
"max": 0.2,
"step": 0.001,
"description": "Softens the trimmed source edges into the outside color for easier background blending."
},
{
"id": "virtualFovDegrees",
"label": "Virtual FOV",
"type": "float",
"default": 75.0,
"min": 1.0,
"max": 175.0,
"step": 0.1
"default": 75,
"min": 1,
"max": 175,
"step": 0.1,
"description": "Field of view of the generated virtual camera."
},
{
"id": "basePanDegrees",
"label": "Base Pan",
"type": "float",
"default": 0.0,
"min": -180.0,
"max": 180.0,
"step": 0.1
"default": 0,
"min": -180,
"max": 180,
"step": 0.1,
"description": "Permanent horizontal alignment offset before live pan."
},
{
"id": "baseTiltDegrees",
"label": "Base Tilt",
"type": "float",
"default": 0.0,
"min": -120.0,
"max": 120.0,
"step": 0.1
"default": 0,
"min": -120,
"max": 120,
"step": 0.1,
"description": "Permanent vertical alignment offset before live tilt."
},
{
"id": "baseRollDegrees",
"label": "Base Roll",
"type": "float",
"default": 0.0,
"min": -180.0,
"max": 180.0,
"step": 0.1
"default": 0,
"min": -180,
"max": 180,
"step": 0.1,
"description": "Permanent roll alignment offset before live roll."
},
{
"id": "panDegrees",
"label": "Pan",
"type": "float",
"default": 0.0,
"min": -180.0,
"max": 180.0,
"step": 0.1
"default": 0,
"min": -180,
"max": 180,
"step": 0.1,
"description": "Live horizontal view rotation."
},
{
"id": "tiltDegrees",
"label": "Tilt",
"type": "float",
"default": 0.0,
"min": -120.0,
"max": 120.0,
"step": 0.1
"default": 0,
"min": -120,
"max": 120,
"step": 0.1,
"description": "Live vertical view rotation."
},
{
"id": "rollDegrees",
"label": "Roll",
"type": "float",
"default": 0.0,
"min": -180.0,
"max": 180.0,
"step": 0.1
"default": 0,
"min": -180,
"max": 180,
"step": 0.1,
"description": "Live roll rotation around the viewing axis."
},
{
"id": "fisheyeModel",
@@ -101,11 +155,24 @@
"type": "enum",
"default": "equidistant",
"options": [
{ "value": "equidistant", "label": "Equidistant" },
{ "value": "equisolid", "label": "Equisolid" },
{ "value": "stereographic", "label": "Stereographic" },
{ "value": "orthographic", "label": "Orthographic" }
]
{
"value": "equidistant",
"label": "Equidistant"
},
{
"value": "equisolid",
"label": "Equisolid"
},
{
"value": "stereographic",
"label": "Stereographic"
},
{
"value": "orthographic",
"label": "Orthographic"
}
],
"description": "Projection model used by the physical fisheye lens."
},
{
"id": "outputProjection",
@@ -113,15 +180,28 @@
"type": "enum",
"default": "rectilinear",
"options": [
{ "value": "rectilinear", "label": "Rectilinear" },
{ "value": "cylindrical", "label": "Cylindrical" }
]
{
"value": "rectilinear",
"label": "Rectilinear"
},
{
"value": "cylindrical",
"label": "Cylindrical"
}
],
"description": "Chooses rectilinear perspective or cylindrical reprojection."
},
{
"id": "outsideColor",
"label": "Outside Color",
"type": "color",
"default": [0.0, 0.0, 0.0, 1.0]
"default": [
0,
0,
0,
1
],
"description": "Color used where the remapped image samples outside the source frame."
}
]
}

View File

@@ -43,6 +43,8 @@ float normalizedFisheyeRadius(float theta, float halfFov)
{
float safeHalfFov = max(halfFov, 0.0001);
// Different fisheye lenses map angle to image radius differently. Normalize
// each model by the selected half-FOV so the outer lens edge stays at 1.0.
if (fisheyeModel == 1)
{
return sin(theta * 0.5) / max(sin(safeHalfFov * 0.5), 0.0001);
@@ -59,6 +61,20 @@ float normalizedFisheyeRadius(float theta, float halfFov)
return theta / safeHalfFov;
}
float sourceUvRectMask(float2 uv, float2 inputResolution)
{
float2 pixel = 1.0 / max(inputResolution, float2(1.0, 1.0));
float cut = max(sourceEdgeCut, 0.0);
float feather = max(sourceEdgeFeather, 0.0);
float2 featherSize = max(float2(feather, feather), pixel * 0.5);
float left = smoothstep(cut, cut + featherSize.x, uv.x);
float right = 1.0 - smoothstep(1.0 - cut - featherSize.x, 1.0 - cut, uv.x);
float top = smoothstep(cut, cut + featherSize.y, uv.y);
float bottom = 1.0 - smoothstep(1.0 - cut - featherSize.y, 1.0 - cut, uv.y);
return saturate(left * right * top * bottom);
}
float4 shadeVideo(ShaderContext context)
{
float2 screen = float2(context.uv.x * 2.0 - 1.0, 1.0 - context.uv.y * 2.0);
@@ -67,6 +83,8 @@ float4 shadeVideo(ShaderContext context)
float virtualFov = radiansFromDegrees(clamp(virtualFovDegrees, 1.0, 175.0));
float tanHalfFov = tan(virtualFov * 0.5);
// Build a virtual output-camera ray, then rotate it into the fisheye lens
// coordinate system before asking where that ray lands on the source image.
float3 ray = outputProjection == 1
? buildCylindricalRay(screen, outputAspect, tanHalfFov)
: buildRectilinearRay(screen, outputAspect, tanHalfFov);
@@ -86,6 +104,7 @@ float4 shadeVideo(ShaderContext context)
float phi = atan2(ray.y, ray.x);
float fisheyeRadius = normalizedFisheyeRadius(theta, halfFov);
// Polar lens coordinates become UVs inside the circular fisheye image.
float2 sourceUv = float2(
center.x + cos(phi) * fisheyeRadius * radius.x,
center.y - sin(phi) * fisheyeRadius * radius.y
@@ -94,5 +113,7 @@ float4 shadeVideo(ShaderContext context)
if (sourceUv.x < 0.0 || sourceUv.x > 1.0 || sourceUv.y < 0.0 || sourceUv.y > 1.0)
return outsideColor;
return sampleVideo(sourceUv);
float sourceMask = sourceUvRectMask(sourceUv, context.inputResolution);
float4 sourceColor = sampleVideo(sourceUv);
return saturate(lerp(outsideColor, sourceColor, sourceMask));
}

View File

@@ -1,36 +1,59 @@
{
"id": "gaussian-blur",
"name": "Gaussian Blur",
"description": "Applies a simple Gaussian-style blur to the decoded video input.",
"category": "Built-in",
"description": "Applies a separable two-pass Gaussian-style blur to the decoded video input.",
"category": "Transform",
"entryPoint": "shadeVideo",
"passes": [
{
"id": "horizontal",
"source": "shader.slang",
"entryPoint": "blurHorizontal",
"inputs": [
"layerInput"
],
"output": "blurHorizontal"
},
{
"id": "vertical",
"source": "shader.slang",
"entryPoint": "blurVertical",
"inputs": [
"blurHorizontal"
],
"output": "layerOutput"
}
],
"parameters": [
{
"id": "radius",
"label": "Radius",
"type": "float",
"default": 2.0,
"min": 0.0,
"max": 8.0,
"step": 0.1
"default": 2,
"min": 0,
"max": 8,
"step": 0.1,
"description": "Blur radius in pixels for each separable pass."
},
{
"id": "strength",
"label": "Strength",
"type": "float",
"default": 1.0,
"min": 0.0,
"max": 1.0,
"step": 0.01
"default": 1,
"min": 0,
"max": 1,
"step": 0.01,
"description": "Blends between the original and blurred result."
},
{
"id": "samples",
"label": "Samples",
"type": "float",
"default": 2.0,
"min": 0.0,
"max": 25.0,
"step": 1.0
"default": 2,
"min": 0,
"max": 25,
"step": 1,
"description": "Number of taps per direction; higher values cost more GPU time."
}
]
}

View File

@@ -1,25 +1,22 @@
float4 shadeVideo(ShaderContext context)
float4 gaussianBlurDirection(ShaderContext context, float2 direction)
{
float2 texel = 1.0 / max(context.inputResolution, float2(1.0, 1.0));
float blurRadius = max(radius, 0.0);
float2 sampleStep = texel * blurRadius;
float blurRadius = max(radius, 0.0) * saturate(strength);
float2 sampleStep = texel * blurRadius * direction;
int sampleRadius = int(clamp(samples, 0.0, 8.0) + 0.5);
float4 center = sampleVideo(context.uv);
float4 blur = float4(0.0, 0.0, 0.0, 0.0);
float totalWeight = 0.0;
for (int y = -sampleRadius; y <= sampleRadius; ++y)
for (int x = -sampleRadius; x <= sampleRadius; ++x)
{
for (int x = -sampleRadius; x <= sampleRadius; ++x)
{
float distanceSquared = float(x * x + y * y);
float sigma = max(float(sampleRadius) * 0.5, 0.5);
float weight = exp(-distanceSquared / (2.0 * sigma * sigma));
float2 offset = float2(float(x), float(y)) * sampleStep;
blur += sampleVideo(context.uv + offset) * weight;
totalWeight += weight;
}
float distanceSquared = float(x * x);
float sigma = max(float(sampleRadius) * 0.5, 0.5);
float weight = exp(-distanceSquared / (2.0 * sigma * sigma));
float2 offset = float(x) * sampleStep;
blur += sampleVideo(context.uv + offset) * weight;
totalWeight += weight;
}
if (sampleRadius == 0)
@@ -29,7 +26,20 @@ float4 shadeVideo(ShaderContext context)
}
blur /= max(totalWeight, 0.0001);
float mixValue = saturate(strength);
return lerp(center, blur, mixValue);
return blur;
}
float4 blurHorizontal(ShaderContext context)
{
return gaussianBlurDirection(context, float2(1.0, 0.0));
}
float4 blurVertical(ShaderContext context)
{
return gaussianBlurDirection(context, float2(0.0, 1.0));
}
float4 shadeVideo(ShaderContext context)
{
return blurVertical(context);
}

View File

@@ -1,90 +1,347 @@
{
"id": "greenscreen-key",
"name": "Greenscreen Key",
"description": "Keys out a green screen background and outputs transparent alpha for compositing.",
"category": "Built-in",
"description": "Production-style green/blue screen keyer with matte refinement, despill, edge treatment, and debug views.",
"category": "Keying",
"entryPoint": "shadeVideo",
"passes": [
{
"id": "rawMatte",
"source": "shader.slang",
"entryPoint": "buildRawMatte",
"inputs": [
"layerInput"
],
"output": "rawMatte"
},
{
"id": "refinedMatte",
"source": "shader.slang",
"entryPoint": "refineMatte",
"inputs": [
"rawMatte"
],
"output": "refinedMatte"
},
{
"id": "final",
"source": "shader.slang",
"entryPoint": "applyKey",
"inputs": [
"refinedMatte"
],
"output": "layerOutput"
}
],
"parameters": [
{
"id": "screenColor",
"label": "Screen Color",
"type": "color",
"default": [0.15, 0.85, 0.2, 1.0],
"min": [0.0, 0.0, 0.0, 0.0],
"max": [1.0, 1.0, 1.0, 1.0],
"step": [0.01, 0.01, 0.01, 0.01]
"default": [
0.15,
0.85,
0.2,
1
],
"min": [
0,
0,
0,
0
],
"max": [
1,
1,
1,
1
],
"step": [
0.01,
0.01,
0.01,
0.01
],
"description": "Target screen color to remove; use green or blue depending on the backdrop."
},
{
"id": "threshold",
"label": "Threshold",
"label": "Screen Gain",
"type": "float",
"default": 0.24,
"min": 0.01,
"max": 0.8,
"step": 0.005
"step": 0.005,
"description": "Higher values keep more foreground; lower values remove more screen."
},
{
"id": "softness",
"label": "Softness",
"type": "float",
"default": 0.12,
"default": 0.16,
"min": 0.001,
"max": 0.5,
"step": 0.005
"step": 0.005,
"description": "Feathers the transition between foreground and keyed screen."
},
{
"id": "edgeSoftness",
"label": "Edge Softness",
"id": "screenBalance",
"label": "Screen Balance",
"type": "float",
"default": 0.08,
"min": 0.0,
"max": 0.4,
"step": 0.005
"default": 0.5,
"min": 0,
"max": 1,
"step": 0.005,
"description": "Balances chroma-distance keying against color-direction keying."
},
{
"id": "screenPreBlur",
"label": "Screen PreBlur",
"type": "float",
"default": 1,
"min": 0,
"max": 8,
"step": 0.1,
"description": "Blurs source color before matte generation to reduce noisy edges."
},
{
"id": "erodeDilate",
"label": "Erode/Dilate",
"type": "float",
"default": 0.0,
"default": 0,
"min": -0.3,
"max": 0.3,
"step": 0.005
"step": 0.005,
"description": "Negative erodes the matte; positive expands it."
},
{
"id": "matteBlur",
"label": "Matte Blur",
"type": "float",
"default": 1.25,
"min": 0,
"max": 6,
"step": 0.1,
"description": "Softens the generated matte after keying."
},
{
"id": "matteGamma",
"label": "Matte Gamma",
"type": "float",
"default": 1,
"min": 0.25,
"max": 4,
"step": 0.01,
"description": "Shapes midtone opacity in the matte."
},
{
"id": "matteContrast",
"label": "Matte Contrast",
"type": "float",
"default": 1,
"min": 0.25,
"max": 4,
"step": 0.01,
"description": "Increases or reduces matte separation around 50 percent alpha."
},
{
"id": "blackCleanup",
"label": "Black Cleanup",
"type": "float",
"default": 0,
"min": 0,
"max": 1,
"step": 0.005,
"description": "Pushes semi-transparent dark matte areas toward transparent."
},
{
"id": "whiteCleanup",
"label": "White Cleanup",
"type": "float",
"default": 0,
"min": 0,
"max": 1,
"step": 0.005,
"description": "Pushes semi-transparent light matte areas toward opaque."
},
{
"id": "despill",
"label": "Despill",
"type": "float",
"default": 0.45,
"min": 0.0,
"min": 0,
"max": 1.5,
"step": 0.01
"step": 0.01,
"description": "Removes screen-colored contamination from foreground edges."
},
{
"id": "edgeBoost",
"label": "Edge Boost",
"id": "despillBias",
"label": "Despill Bias",
"type": "float",
"default": 0.08,
"min": -0.2,
"max": 0.3,
"step": 0.005
"default": 0,
"min": -0.5,
"max": 0.5,
"step": 0.005,
"description": "Offsets spill detection when foreground colors are close to the screen color."
},
{
"id": "spillTint",
"label": "Spill Tint",
"type": "color",
"default": [
1,
1,
1,
1
],
"min": [
0,
0,
0,
0
],
"max": [
1,
1,
1,
1
],
"step": [
0.01,
0.01,
0.01,
0.01
],
"description": "Tint used when neutralizing spill."
},
{
"id": "edgeRecover",
"label": "Edge Recover",
"type": "float",
"default": 0.18,
"min": 0,
"max": 1,
"step": 0.005,
"description": "Adds color recovery along semi-transparent matte edges."
},
{
"id": "edgeColor",
"label": "Edge Color",
"type": "color",
"default": [
1,
1,
1,
1
],
"min": [
0,
0,
0,
0
],
"max": [
1,
1,
1,
1
],
"step": [
0.01,
0.01,
0.01,
0.01
],
"description": "Tint applied to recovered edge detail."
},
{
"id": "clipBlack",
"label": "Clip Black",
"type": "float",
"default": 0.0,
"min": 0.0,
"default": 0,
"min": 0,
"max": 0.5,
"step": 0.005
"step": 0.005,
"description": "Matte values below this become transparent."
},
{
"id": "clipWhite",
"label": "Clip White",
"type": "float",
"default": 1.0,
"default": 1,
"min": 0.5,
"max": 1.0,
"step": 0.005
"max": 1,
"step": 0.005,
"description": "Matte values above this become opaque."
},
{
"id": "cropLeft",
"label": "Crop Left",
"description": "Trims the final matte from the left edge as a fraction of frame width.",
"type": "float",
"default": 0,
"min": 0,
"max": 0.5,
"step": 0.001
},
{
"id": "cropRight",
"label": "Crop Right",
"description": "Trims the final matte from the right edge as a fraction of frame width.",
"type": "float",
"default": 0,
"min": 0,
"max": 0.5,
"step": 0.001
},
{
"id": "cropTop",
"label": "Crop Top",
"description": "Trims the final matte from the top edge as a fraction of frame height.",
"type": "float",
"default": 0,
"min": 0,
"max": 0.5,
"step": 0.001
},
{
"id": "cropBottom",
"label": "Crop Bottom",
"description": "Trims the final matte from the bottom edge as a fraction of frame height.",
"type": "float",
"default": 0,
"min": 0,
"max": 0.5,
"step": 0.001
},
{
"id": "viewMode",
"label": "View",
"type": "enum",
"default": "composite",
"options": [
{
"value": "composite",
"label": "Composite"
},
{
"value": "matte",
"label": "Matte"
},
{
"value": "spill",
"label": "Spill"
},
{
"value": "despill",
"label": "Despill"
},
{
"value": "status",
"label": "Status"
}
],
"description": "Debug output mode for inspecting matte, spill, and despill stages."
}
]
}

View File

@@ -9,32 +9,216 @@ float luma709(float3 color)
return dot(color, float3(0.2126, 0.7152, 0.0722));
}
float4 shadeVideo(ShaderContext context)
float2 chroma709(float3 color)
{
float y = luma709(color);
return float2((color.b - y) * 0.5647, (color.r - y) * 0.7132);
}
float3 matteSampleColor(float2 uv, ShaderContext context)
{
float2 pixel = 1.0 / max(context.outputResolution, float2(1.0, 1.0));
float blur = max(screenPreBlur, 0.0);
float3 center = saturate(sampleVideo(saturate(uv)).rgb);
if (blur <= 0.0001)
return center;
// Pre-blur only the color used for screen comparison; the final image keeps
// its original detail and alpha is refined in a later pass.
float2 radius = pixel * blur;
float3 color = center * 0.36;
color += saturate(sampleVideo(saturate(uv + float2(radius.x, 0.0))).rgb) * 0.16;
color += saturate(sampleVideo(saturate(uv - float2(radius.x, 0.0))).rgb) * 0.16;
color += saturate(sampleVideo(saturate(uv + float2(0.0, radius.y))).rgb) * 0.16;
color += saturate(sampleVideo(saturate(uv - float2(0.0, radius.y))).rgb) * 0.16;
return color;
}
float keyDistanceAt(float2 uv, ShaderContext context)
{
float3 color = matteSampleColor(uv, context);
float3 keyColor = saturate(screenColor.rgb);
float chromaDistance = distance(chroma709(color), chroma709(keyColor)) * 2.65;
// Direction distance is less sensitive to brightness, while chroma distance
// follows broadcast-style color difference; screenBalance blends the two.
float directionDistance = length(safeNormalize(max(color, float3(0.0001, 0.0001, 0.0001))) - safeNormalize(max(keyColor, float3(0.0001, 0.0001, 0.0001)))) * 0.55;
return lerp(directionDistance, chromaDistance, saturate(screenBalance));
}
float rawAlphaAt(float2 uv, ShaderContext context)
{
float keyDistance = keyDistanceAt(uv, context);
float matteCenter = threshold + erodeDilate;
float matteFeather = max(softness, 0.0005);
float alpha = smoothstep(matteCenter - matteFeather, matteCenter + matteFeather, keyDistance);
return saturate(alpha);
}
float matteAlphaAt(float2 uv)
{
return saturate(sampleVideo(saturate(uv)).a);
}
float refinedAlphaFromMatte(float2 uv, ShaderContext context)
{
float2 pixel = 1.0 / max(context.outputResolution, float2(1.0, 1.0));
float blur = max(matteBlur, 0.0);
float aaRadius = max(blur, 0.65);
float centerAlpha = matteAlphaAt(uv);
float alpha = centerAlpha * 0.30;
if (aaRadius > 0.0001)
{
// A small fixed kernel smooths edges and collects min/max alpha for
// black/white cleanup without needing dynamic loops or arrays.
float2 radius = pixel * aaRadius;
float2 halfRadius = radius * 0.5;
float alphaMin = centerAlpha;
float alphaMax = centerAlpha;
float sampleAlpha = matteAlphaAt(uv + float2(halfRadius.x, 0.0));
alpha += sampleAlpha * 0.065;
alphaMin = min(alphaMin, sampleAlpha);
alphaMax = max(alphaMax, sampleAlpha);
sampleAlpha = matteAlphaAt(uv - float2(halfRadius.x, 0.0));
alpha += sampleAlpha * 0.065;
alphaMin = min(alphaMin, sampleAlpha);
alphaMax = max(alphaMax, sampleAlpha);
sampleAlpha = matteAlphaAt(uv + float2(0.0, halfRadius.y));
alpha += sampleAlpha * 0.065;
alphaMin = min(alphaMin, sampleAlpha);
alphaMax = max(alphaMax, sampleAlpha);
sampleAlpha = matteAlphaAt(uv - float2(0.0, halfRadius.y));
alpha += sampleAlpha * 0.065;
alphaMin = min(alphaMin, sampleAlpha);
alphaMax = max(alphaMax, sampleAlpha);
sampleAlpha = matteAlphaAt(uv + float2(radius.x, 0.0));
alpha += sampleAlpha * 0.06;
alphaMin = min(alphaMin, sampleAlpha);
alphaMax = max(alphaMax, sampleAlpha);
sampleAlpha = matteAlphaAt(uv - float2(radius.x, 0.0));
alpha += sampleAlpha * 0.06;
alphaMin = min(alphaMin, sampleAlpha);
alphaMax = max(alphaMax, sampleAlpha);
sampleAlpha = matteAlphaAt(uv + float2(0.0, radius.y));
alpha += sampleAlpha * 0.06;
alphaMin = min(alphaMin, sampleAlpha);
alphaMax = max(alphaMax, sampleAlpha);
sampleAlpha = matteAlphaAt(uv - float2(0.0, radius.y));
alpha += sampleAlpha * 0.06;
alphaMin = min(alphaMin, sampleAlpha);
alphaMax = max(alphaMax, sampleAlpha);
sampleAlpha = matteAlphaAt(uv + radius);
alpha += sampleAlpha * 0.05;
alphaMin = min(alphaMin, sampleAlpha);
alphaMax = max(alphaMax, sampleAlpha);
sampleAlpha = matteAlphaAt(uv - radius);
alpha += sampleAlpha * 0.05;
alphaMin = min(alphaMin, sampleAlpha);
alphaMax = max(alphaMax, sampleAlpha);
sampleAlpha = matteAlphaAt(uv + float2(radius.x, -radius.y));
alpha += sampleAlpha * 0.05;
alphaMin = min(alphaMin, sampleAlpha);
alphaMax = max(alphaMax, sampleAlpha);
sampleAlpha = matteAlphaAt(uv + float2(-radius.x, radius.y));
alpha += sampleAlpha * 0.05;
alphaMin = min(alphaMin, sampleAlpha);
alphaMax = max(alphaMax, sampleAlpha);
alpha = lerp(alpha, alphaMin, saturate(blackCleanup));
alpha = lerp(alpha, alphaMax, saturate(whiteCleanup));
}
else
{
alpha = centerAlpha;
}
// Final matte shaping happens after blur/cleanup so clip and contrast affect
// the refined edge rather than the raw screen-distance estimate.
alpha = saturate((alpha - clipBlack) / max(clipWhite - clipBlack, 0.0001));
alpha = saturate((alpha - 0.5) * max(matteContrast, 0.0001) + 0.5);
alpha = pow(max(alpha, 0.0), max(matteGamma, 0.0001));
return saturate(alpha);
}
float spillAmountForColor(float3 color)
{
float3 keyColor = saturate(screenColor.rgb);
// Measure spill as color energy aligned with the screen color minus the
// strongest opposing channel, leaving neutral highlights mostly intact.
float keyComponent = dot(color, safeNormalize(max(keyColor, float3(0.0001, 0.0001, 0.0001))));
float opposingComponent = max(max(color.r * (1.0 - keyColor.r), color.g * (1.0 - keyColor.g)), color.b * (1.0 - keyColor.b));
return saturate(keyComponent - opposingComponent + despillBias);
}
float3 despillColor(float3 color, float alpha)
{
float3 keyColor = safeNormalize(max(screenColor.rgb, float3(0.0001, 0.0001, 0.0001)));
float spill = spillAmountForColor(color) * despill * (1.0 - alpha * 0.35);
float neutral = luma709(color);
float3 neutralized = color - keyColor * spill;
neutralized = max(neutralized, float3(0.0, 0.0, 0.0));
neutralized = lerp(neutralized, float3(neutral, neutral, neutral), spill * 0.18);
neutralized = lerp(neutralized, neutralized * saturate(spillTint.rgb), saturate(spill));
return saturate(neutralized);
}
float cropMaskAt(float2 uv, ShaderContext context)
{
float2 feather = 1.0 / max(context.outputResolution, float2(1.0, 1.0));
float left = smoothstep(saturate(cropLeft), saturate(cropLeft) + feather.x, uv.x);
float right = 1.0 - smoothstep(1.0 - saturate(cropRight) - feather.x, 1.0 - saturate(cropRight), uv.x);
float top = smoothstep(saturate(cropTop), saturate(cropTop) + feather.y, uv.y);
float bottom = 1.0 - smoothstep(1.0 - saturate(cropBottom) - feather.y, 1.0 - saturate(cropBottom), uv.y);
return saturate(left * right * top * bottom);
}
float4 buildRawMatte(ShaderContext context)
{
float4 src = context.sourceColor;
float3 color = saturate(src.rgb);
float alpha = rawAlphaAt(context.uv, context);
return float4(color, alpha);
}
float3 keyColor = safeNormalize(max(screenColor.rgb, float3(0.0001, 0.0001, 0.0001)));
float3 sampleColor = safeNormalize(max(color, float3(0.0001, 0.0001, 0.0001)));
float4 refineMatte(ShaderContext context)
{
float4 raw = sampleVideo(context.uv);
float alpha = refinedAlphaFromMatte(context.uv, context);
return float4(saturate(raw.rgb), alpha);
}
float chromaDistance = length(sampleColor - keyColor);
float matteCenter = threshold - erodeDilate;
float matteFeather = max(softness + edgeSoftness, 0.0005);
float alpha = smoothstep(matteCenter - matteFeather, matteCenter + matteFeather, chromaDistance);
float4 applyKey(ShaderContext context)
{
float4 keyed = sampleVideo(context.uv);
float3 color = saturate(keyed.rgb);
float alpha = saturate(keyed.a);
float spill = spillAmountForColor(color);
float3 despilled = despillColor(color, alpha);
float cropMask = cropMaskAt(context.uv, context);
alpha *= cropMask;
alpha = saturate((alpha - clipBlack) / max(clipWhite - clipBlack, 0.0001));
alpha = saturate(alpha + edgeBoost);
// Edge recovery is strongest around 50% alpha, where fringing usually lives,
// and fades away for solid foreground/background pixels.
float edgeAmount = saturate(1.0 - abs(alpha * 2.0 - 1.0));
despilled = lerp(despilled, despilled * saturate(edgeColor.rgb), edgeAmount * saturate(edgeRecover));
float greenExcess = max(0.0, color.g - max(color.r, color.b));
float spillReduction = greenExcess * despill;
float3 despilled = color;
despilled.g = max(0.0, despilled.g - spillReduction);
float neutral = luma709(despilled);
despilled.rb += spillReduction * 0.25;
despilled = lerp(float3(neutral, neutral, neutral), despilled, 0.92);
if (viewMode == 1)
return float4(alpha, alpha, alpha, 1.0);
if (viewMode == 2)
return float4(spill, spill * 0.55, 0.0, 1.0);
if (viewMode == 3)
return float4(despilled, 1.0);
if (viewMode == 4)
{
float rawAlpha = rawAlphaAt(context.uv, context) * cropMask;
return float4(rawAlpha, alpha, spill, 1.0);
}
float3 premultiplied = saturate(despilled) * alpha;
return float4(premultiplied, alpha);
}
float4 shadeVideo(ShaderContext context)
{
return applyKey(context);
}

View File

@@ -0,0 +1,59 @@
{
"id": "happy-accident",
"name": "Happy Accident",
"description": "Raymarched generative line field. CC0 original 'Clearly a bug' adapted from https://www.shadertoy.com/view/33cGDj.",
"category": "Generative",
"entryPoint": "shadeVideo",
"parameters": [
{
"id": "speed",
"label": "Speed",
"type": "float",
"default": 1,
"min": 0,
"max": 4,
"step": 0.01,
"description": "Animation speed multiplier; set to 0 to pause motion."
},
{
"id": "scale",
"label": "Scale",
"type": "float",
"default": 1,
"min": 0.25,
"max": 3,
"step": 0.01,
"description": "Overall size of the effect in the frame."
},
{
"id": "raySteps",
"label": "Ray Steps",
"type": "float",
"default": 77,
"min": 8,
"max": 77,
"step": 1,
"description": "Raymarch iteration count; higher values increase detail and GPU cost."
},
{
"id": "intensity",
"label": "Intensity",
"type": "float",
"default": 1,
"min": 0.1,
"max": 4,
"step": 0.01,
"description": "Overall brightness of the accumulated raymarched light."
},
{
"id": "sourceMix",
"label": "Source Mix",
"type": "float",
"default": 0,
"min": 0,
"max": 1,
"step": 0.01,
"description": "Blends the generated effect with the incoming video."
}
]
}

View File

@@ -0,0 +1,68 @@
float happyNoise(float2 p)
{
return frac(dot(p, sin(p))) - 0.5;
}
float2x2 rotateAroundZ(float angle)
{
float c = cos(angle);
float s = sin(angle);
return float2x2(c, s, -s, c);
}
float2x2 happyAccidentMatrix(float3 originalPosition, float timeCos)
{
return float2x2(
cos(originalPosition.x),
sin(originalPosition.y),
-sin(originalPosition.z),
timeCos);
}
float4 shadeVideo(ShaderContext context)
{
float2 resolution = max(context.outputResolution, float2(1.0, 1.0));
float2 fragCoord = context.uv * resolution;
float2 normalizedCoord = (fragCoord - 0.5 * resolution) / resolution.y / max(scale, 0.001);
float seed = context.startupRandom;
float time = context.time * speed + seed * 53.0;
float timeCos = cos(0.1 * time);
float3 direction = normalize(float3(normalizedCoord, 1.0));
float3 origin = float3(seed * 0.4 - 0.2, 0.2 - seed * 0.4, time);
float edgeAmount = saturate(length(normalizedCoord) * 0.55);
float z = happyNoise(fragCoord + seed * resolution.yx) * lerp(0.22, 0.08, edgeAmount);
float distanceToSurface = 0.0;
float4 accumulated = float4(0.0, 0.0, 0.0, 0.0);
float clampedSteps = clamp(raySteps, 1.0, 77.0);
// Ray-march a folded procedural field. distanceToSurface advances the ray,
// while inverse-distance accumulation creates the glowing filaments.
for (int i = 0; i < 77; ++i)
{
if (float(i) >= clampedSteps)
break;
z += 0.6 * distanceToSurface;
float3 position = origin + z * direction;
float3 originalPosition = position;
position.xy = mul(rotateAroundZ(2.0 + originalPosition.z), position.xy);
position.xy = mul(happyAccidentMatrix(originalPosition, timeCos), position.xy);
// Color comes from pre-fold space so the palette varies smoothly even as
// the geometry folds into repeated cells.
float colorSeed = 0.5 * originalPosition.z + length(position - originalPosition);
float4 palette = 1.0 + sin(colorSeed + float4(0.0, 4.0, 3.0, 6.0));
palette /= 0.55 + 1.55 * dot(originalPosition.xy, originalPosition.xy);
position = abs(frac(position) - 0.5);
// Distance to a tiny box/cross primitive inside each repeated cell.
distanceToSurface = abs(min(length(position.xy) - 0.125, min(position.x, position.y) + 0.001)) + 0.001;
accumulated += palette.w * palette / distanceToSurface;
}
float4 color = float4(tanh((accumulated.rgb * intensity) / 20000.0), 1.0);
return saturate(lerp(color, context.sourceColor, sourceMix));
}

View File

@@ -0,0 +1,67 @@
{
"id": "lift-gamma-gain",
"name": "Lift Gamma Gain",
"description": "Basic color grading controls for shadows, midtones, highlights, and overall RGB offset.",
"category": "Color",
"entryPoint": "shadeVideo",
"parameters": [
{
"id": "lift",
"label": "Lift",
"type": "color",
"default": [
0.5,
0.5,
0.5,
1
],
"description": "Adds color mostly to shadows."
},
{
"id": "gamma",
"label": "Gamma",
"type": "color",
"default": [
0.5,
0.5,
0.5,
1
],
"description": "Balances midtone color response."
},
{
"id": "gain",
"label": "Gain",
"type": "color",
"default": [
0.5,
0.5,
0.5,
1
],
"description": "Scales highlights and overall channel intensity."
},
{
"id": "offset",
"label": "Offset",
"type": "color",
"default": [
0.5,
0.5,
0.5,
1
],
"description": "Adds a uniform color offset after lift/gamma/gain."
},
{
"id": "strength",
"label": "Strength",
"type": "float",
"default": 1,
"min": 0,
"max": 1,
"step": 0.01,
"description": "Blends the grade with the original image."
}
]
}

View File

@@ -0,0 +1,20 @@
float3 applyLiftGammaGainOffset(float3 color)
{
float3 liftAdjust = (lift.rgb - 0.5) * 0.5;
float3 offsetAdjust = (offset.rgb - 0.5) * 0.5;
float3 gammaAdjust = exp2((gamma.rgb - 0.5) * 2.0);
float3 gainAdjust = exp2((gain.rgb - 0.5) * 2.0);
float3 lifted = color + liftAdjust;
float3 gained = lifted * gainAdjust;
float3 corrected = pow(saturate(gained), 1.0 / max(gammaAdjust, float3(0.001)));
return corrected + offsetAdjust;
}
float4 shadeVideo(ShaderContext context)
{
float4 source = context.sourceColor;
float3 graded = applyLiftGammaGainOffset(source.rgb);
source.rgb = lerp(source.rgb, graded, strength);
return saturate(source);
}

View File

@@ -0,0 +1,62 @@
{
"id": "lut-apply",
"name": "3D LUT Apply",
"description": "Applies the packaged 33-point .cube LUT to the incoming video using tetrahedral interpolation and optional output dithering.",
"category": "Color",
"entryPoint": "shadeVideo",
"textures": [
{
"id": "lutTexture",
"path": "test-lut.cube"
}
],
"parameters": [
{
"id": "lutStrength",
"label": "LUT Strength",
"type": "float",
"default": 1,
"min": 0,
"max": 1,
"step": 0.01,
"description": "Blends between the original image and the LUT result."
},
{
"id": "preExposure",
"label": "Pre Exposure",
"type": "float",
"default": 0,
"min": -4,
"max": 4,
"step": 0.01,
"description": "Exposure offset applied before the LUT lookup."
},
{
"id": "postContrast",
"label": "Post Contrast",
"type": "float",
"default": 1,
"min": 0,
"max": 2,
"step": 0.01,
"description": "Contrast adjustment applied after the LUT lookup."
},
{
"id": "ditherAmount",
"label": "Output Dither",
"type": "float",
"default": 0.5,
"min": 0,
"max": 1,
"step": 0.01,
"description": "Adds subtle output dither to reduce visible banding."
},
{
"id": "clampInput",
"label": "Clamp Input",
"type": "bool",
"default": true,
"description": "Clamps colors to 0-1 before the LUT lookup."
}
]
}

View File

@@ -0,0 +1,84 @@
static const float LUT_SIZE = 33.0;
static const float LUT_LAST_INDEX = 32.0;
float3 sampleLutCell(float3 index)
{
float r = floor(index.r + 0.5);
float g = floor(index.g + 0.5);
float b = floor(index.b + 0.5);
// The 33^3 cube is packed as blue slices laid horizontally, with red across
// each slice and green down the atlas.
float atlasWidth = LUT_SIZE * LUT_SIZE;
float2 lutUv;
lutUv.x = (r + b * LUT_SIZE + 0.5) / atlasWidth;
lutUv.y = (g + 0.5) / LUT_SIZE;
return lutTexture.Sample(lutUv).rgb;
}
float3 applyLut33(float3 color)
{
float3 lutCoord = saturate(color) * LUT_LAST_INDEX;
float3 baseIndex = floor(lutCoord);
float3 nextIndex = min(baseIndex + 1.0, LUT_LAST_INDEX);
float3 blend = lutCoord - baseIndex;
float3 c000 = sampleLutCell(float3(baseIndex.r, baseIndex.g, baseIndex.b));
float3 c100 = sampleLutCell(float3(nextIndex.r, baseIndex.g, baseIndex.b));
float3 c010 = sampleLutCell(float3(baseIndex.r, nextIndex.g, baseIndex.b));
float3 c110 = sampleLutCell(float3(nextIndex.r, nextIndex.g, baseIndex.b));
float3 c001 = sampleLutCell(float3(baseIndex.r, baseIndex.g, nextIndex.b));
float3 c101 = sampleLutCell(float3(nextIndex.r, baseIndex.g, nextIndex.b));
float3 c011 = sampleLutCell(float3(baseIndex.r, nextIndex.g, nextIndex.b));
float3 c111 = sampleLutCell(float3(nextIndex.r, nextIndex.g, nextIndex.b));
// Tetrahedral interpolation chooses one of six paths through the cube.
// This avoids the muddy diagonals that simple trilinear LUT sampling can
// introduce for strong grades.
if (blend.r > blend.g)
{
if (blend.g > blend.b)
return c000 + blend.r * (c100 - c000) + blend.g * (c110 - c100) + blend.b * (c111 - c110);
if (blend.r > blend.b)
return c000 + blend.r * (c100 - c000) + blend.b * (c101 - c100) + blend.g * (c111 - c101);
return c000 + blend.b * (c001 - c000) + blend.r * (c101 - c001) + blend.g * (c111 - c101);
}
if (blend.b > blend.g)
return c000 + blend.b * (c001 - c000) + blend.g * (c011 - c001) + blend.r * (c111 - c011);
if (blend.b > blend.r)
return c000 + blend.g * (c010 - c000) + blend.b * (c011 - c010) + blend.r * (c111 - c011);
return c000 + blend.g * (c010 - c000) + blend.r * (c110 - c010) + blend.b * (c111 - c110);
}
float hash12(float2 value)
{
float3 p = frac(float3(value.xyx) * 0.1031);
p += dot(p, p.yzx + 33.33);
return frac((p.x + p.y) * p.z);
}
float3 outputDither(float2 pixel)
{
// Subtract paired hashes to center the dither around zero, then scale to
// roughly one 8-bit code value.
float r = hash12(pixel + float2(17.0, 31.0)) - hash12(pixel + float2(83.0, 47.0));
float g = hash12(pixel + float2(29.0, 71.0)) - hash12(pixel + float2(53.0, 19.0));
float b = hash12(pixel + float2(61.0, 11.0)) - hash12(pixel + float2(7.0, 97.0));
return float3(r, g, b) / 255.0;
}
float4 shadeVideo(ShaderContext context)
{
float4 source = context.sourceColor;
float3 inputColor = source.rgb * pow(2.0, preExposure);
if (clampInput)
inputColor = saturate(inputColor);
float3 lutColor = applyLut33(inputColor);
float3 graded = lerp(inputColor, lutColor, lutStrength);
graded = (graded - 0.5) * postContrast + 0.5;
graded += outputDither(context.uv * context.outputResolution) * ditherAmount;
return float4(saturate(graded), source.a);
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,49 @@
{
"id": "multipass-test",
"name": "Multipass Test",
"description": "Diagnostic two-pass shader that generates a mask in pass one, then samples that named intermediate in pass two.",
"category": "Utility",
"entryPoint": "shadeVideo",
"passes": [
{
"id": "mask",
"source": "shader.slang",
"entryPoint": "buildMask",
"inputs": [
"layerInput"
],
"output": "generatedMask"
},
{
"id": "final",
"source": "shader.slang",
"entryPoint": "applyMask",
"inputs": [
"generatedMask"
],
"output": "layerOutput"
}
],
"parameters": [
{
"id": "intensity",
"label": "Intensity",
"type": "float",
"default": 1,
"min": 0,
"max": 1,
"step": 0.01,
"description": "Opacity of the second-pass diagnostic overlay."
},
{
"id": "scale",
"label": "Scale",
"type": "float",
"default": 10,
"min": 2,
"max": 32,
"step": 1,
"description": "Size of the generated test pattern."
}
]
}

View File

@@ -0,0 +1,37 @@
float ringMask(float2 uv)
{
float2 centered = uv * 2.0 - 1.0;
float radius = length(centered);
float ring = 1.0 - smoothstep(0.015, 0.035, abs(radius - 0.55));
float cross = 1.0 - smoothstep(0.006, 0.018, min(abs(centered.x), abs(centered.y)));
return saturate(max(ring, cross));
}
float gridMask(float2 uv)
{
float2 cell = abs(frac(uv * max(scale, 1.0)) - 0.5);
float line = 1.0 - smoothstep(0.455, 0.495, max(cell.x, cell.y));
return saturate(line * 0.55);
}
float4 buildMask(ShaderContext context)
{
float mask = saturate(max(ringMask(context.uv), gridMask(context.uv)));
return float4(context.sourceColor.rgb, mask);
}
float4 applyMask(ShaderContext context)
{
float4 generated = sampleVideo(context.uv);
float mask = generated.a;
float checker = step(0.5, frac((context.uv.x + context.uv.y) * max(scale, 1.0)));
float3 testColor = lerp(float3(0.0, 0.75, 1.0), float3(1.0, 0.1, 0.85), checker);
float3 base = generated.rgb;
float3 color = lerp(base, testColor, mask * saturate(intensity));
return float4(color, 1.0);
}
float4 shadeVideo(ShaderContext context)
{
return applyMask(context);
}

View File

@@ -2,32 +2,52 @@
"id": "pixelate",
"name": "Pixelate",
"description": "Reduces the effective X and Y pixel count independently to create a low-resolution blocky image.",
"category": "Utility",
"category": "Transform",
"entryPoint": "shadeVideo",
"parameters": [
{
"id": "pixelCount",
"label": "Pixel Count",
"type": "vec2",
"default": [96.0, 54.0],
"min": [2.0, 2.0],
"max": [1920.0, 1080.0],
"step": [1.0, 1.0]
"default": [
96,
54
],
"min": [
2,
2
],
"max": [
1920,
1080
],
"step": [
1,
1
],
"description": "Number of pixel blocks across X and Y."
},
{
"id": "gridAmount",
"label": "Grid",
"type": "float",
"default": 0.0,
"min": 0.0,
"max": 1.0,
"step": 0.01
"default": 0,
"min": 0,
"max": 1,
"step": 0.01,
"description": "Visibility of the block grid lines."
},
{
"id": "gridColor",
"label": "Grid Color",
"type": "color",
"default": [0.0, 0.0, 0.0, 1.0]
"default": [
0,
0,
0,
1
],
"description": "Color used for the pixel grid."
}
]
}

View File

@@ -2,35 +2,61 @@
"id": "safe-area-guides",
"name": "Safe Area Guides",
"description": "Overlays broadcast action/title safe guides plus optional center marks and aspect matte.",
"category": "Utility",
"category": "Scopes & Guides",
"entryPoint": "shadeVideo",
"parameters": [
{ "id": "showActionSafe", "label": "Action Safe", "type": "bool", "default": true },
{ "id": "showTitleSafe", "label": "Title Safe", "type": "bool", "default": true },
{ "id": "showCenter", "label": "Center Marks", "type": "bool", "default": true },
{
"id": "showActionSafe",
"label": "Action Safe",
"type": "bool",
"default": true,
"description": "Shows the broadcast action-safe rectangle."
},
{
"id": "showTitleSafe",
"label": "Title Safe",
"type": "bool",
"default": true,
"description": "Shows the broadcast title-safe rectangle."
},
{
"id": "showCenter",
"label": "Center Marks",
"type": "bool",
"default": true,
"description": "Shows center marks for alignment."
},
{
"id": "lineColor",
"label": "Line Color",
"type": "color",
"default": [1.0, 1.0, 1.0, 1.0]
"default": [
1,
1,
1,
1
],
"description": "Color used for guide lines and marks."
},
{
"id": "lineOpacity",
"label": "Line Opacity",
"type": "float",
"default": 0.65,
"min": 0.0,
"max": 1.0,
"step": 0.01
"min": 0,
"max": 1,
"step": 0.01,
"description": "Overall visibility of the guide lines."
},
{
"id": "lineThicknessPixels",
"label": "Line Thickness",
"type": "float",
"default": 2.0,
"default": 2,
"min": 0.5,
"max": 12.0,
"step": 0.1
"max": 12,
"step": 0.1,
"description": "Guide line width in output pixels."
},
{
"id": "aspectMode",
@@ -38,20 +64,34 @@
"type": "enum",
"default": "none",
"options": [
{ "value": "none", "label": "None" },
{ "value": "239", "label": "2.39:1" },
{ "value": "185", "label": "1.85:1" },
{ "value": "square", "label": "1:1" }
]
{
"value": "none",
"label": "None"
},
{
"value": "239",
"label": "2.39:1"
},
{
"value": "185",
"label": "1.85:1"
},
{
"value": "square",
"label": "1:1"
}
],
"description": "Adds an optional framing matte for common delivery ratios."
},
{
"id": "matteOpacity",
"label": "Matte Opacity",
"type": "float",
"default": 0.35,
"min": 0.0,
"max": 1.0,
"step": 0.01
"min": 0,
"max": 1,
"step": 0.01,
"description": "Opacity of the aspect-ratio matte outside the active image."
}
]
}

View File

@@ -0,0 +1,111 @@
{
"id": "singularity",
"name": "Singularity",
"description": "Whirling blackhole and accretion disk. Original by XorDev, adapted from https://www.shadertoy.com/view/3csSWB.",
"category": "Generative",
"entryPoint": "shadeVideo",
"parameters": [
{
"id": "speed",
"label": "Speed",
"type": "float",
"default": 1,
"min": 0,
"max": 4,
"step": 0.01,
"description": "Animation speed multiplier; set to 0 to pause motion."
},
{
"id": "scale",
"label": "Scale",
"type": "float",
"default": 0.7,
"min": 0.25,
"max": 1.5,
"step": 0.01,
"description": "Overall size of the effect in the frame."
},
{
"id": "strength",
"label": "Gravity",
"type": "float",
"default": 1,
"min": 0.1,
"max": 3,
"step": 0.01,
"description": "Strength of the lensing/gravity distortion."
},
{
"id": "ringRadius",
"label": "Ring Radius",
"type": "float",
"default": 0.7,
"min": 0.2,
"max": 1.4,
"step": 0.01,
"description": "Radius of the bright accretion ring."
},
{
"id": "tightness",
"label": "Tightness",
"type": "float",
"default": 1.35,
"min": 0.5,
"max": 3,
"step": 0.01,
"description": "Concentration of the ring and spiral detail."
},
{
"id": "brightness",
"label": "Brightness",
"type": "float",
"default": 1,
"min": 0.1,
"max": 4,
"step": 0.01,
"description": "Adjusts the generated effect brightness."
},
{
"id": "colorShift",
"label": "Color Shift",
"type": "float",
"default": 1,
"min": -2,
"max": 2,
"step": 0.01,
"description": "Cycles the generated color palette."
},
{
"id": "center",
"label": "Center",
"type": "vec2",
"default": [
0,
0
],
"min": [
-1,
-1
],
"max": [
1,
1
],
"step": [
0.001,
0.001
],
"description": "Moves the black hole center in normalized coordinates."
},
{
"id": "sourceMix",
"label": "Source Mix",
"type": "float",
"default": 0,
"min": 0,
"max": 1,
"step": 0.01,
"description": "Blends the generated effect with the incoming video."
}
]
}

View File

@@ -0,0 +1,55 @@
float2 singularitySpiral(float2 c, float time, float iterator)
{
float radiusSq = max(dot(c, c), 0.0001);
float angle = 0.5 * log(radiusSq) + time * iterator;
return float2(
c.x * cos(angle + 0.0) + c.y * cos(angle + 11.0),
c.x * cos(angle + 33.0) + c.y * cos(angle + 0.0)) / max(iterator, 0.001);
}
float4 shadeVideo(ShaderContext context)
{
float2 resolution = max(context.outputResolution, float2(1.0, 1.0));
float2 fragCoord = context.uv * resolution;
float safeScale = max(scale, 0.001);
float safeRingRadius = max(ringRadius, 0.001);
float safeTightness = max(tightness, 0.001);
float seed = context.startupRandom;
float time = context.time * speed + seed * 37.0;
float2 p = (fragCoord + fragCoord - resolution) / resolution.y / safeScale;
p -= center + float2(sin(seed * 6.2831853), cos(seed * 6.2831853)) * 0.035;
// Build a skewed coordinate system around an offset "black hole" so the
// waves pinch and stretch instead of staying radially symmetric.
float iterator = 0.2;
float2 diagonal = normalize(float2(-1.0 + seed * 0.5, 1.0 - seed * 0.35));
float2 blackholeCenter = p - iterator * diagonal;
float gravity = iterator * strength / max(dot(blackholeCenter, blackholeCenter), 0.0001);
float2 skew = diagonal / (0.1 + gravity);
float2 c = float2(p.x + p.y, p.x * skew.x + p.y * skew.y);
float2 v = singularitySpiral(c, time, iterator);
float2 waves = float2(0.0001, 0.0001);
// Iterative sine feedback creates the accretion texture; the iterator value
// also damps later steps to keep the pattern stable.
for (; iterator < 9.0; iterator += 1.0)
{
waves += 1.0 + sin(v);
v += 0.7 * sin(v.yx * iterator + time) / iterator + 0.5;
}
float diskRadius = length(sin(v / 0.3) * 0.4 + c * float2(2.0, 4.0));
float disk = 2.0 + diskRadius * diskRadius * (0.25 * safeTightness) - diskRadius;
float centerDarkness = 0.5 + 1.0 / max(dot(c, c), 0.0001);
float rim = 0.025 + abs(length(p) - safeRingRadius) * safeTightness;
// Exponential falloff turns the accumulated wave field into bright rims and
// a darker center without hard thresholds.
float4 redBlueGradient = exp(c.x * float4(0.6, -0.4, -1.0, 0.0) * colorShift);
float4 waveColor = waves.xyyx;
float4 color = 1.0 - exp(-redBlueGradient / max(waveColor, float4(0.0001, 0.0001, 0.0001, 0.0001)) / disk / centerDarkness / rim * brightness);
color.a = 1.0;
return saturate(lerp(color, context.sourceColor, sourceMix));
}

View File

@@ -0,0 +1,8 @@
{
"id": "smpte-color-bars",
"name": "SMPTE Color Bars",
"description": "Generates a procedural SMPTE RP 219-style 16:9 color bar test pattern matching the common Wikimedia 1920x1080 reference layout.",
"category": "Calibration",
"entryPoint": "shadeVideo",
"parameters": []
}

View File

@@ -0,0 +1,90 @@
float3 hexColor(float r, float g, float b)
{
return float3(r, g, b) / 255.0;
}
float3 smpteTop(float x)
{
if (x < 240.0)
return hexColor(102.0, 102.0, 102.0);
if (x < 445.0)
return hexColor(191.0, 191.0, 191.0);
if (x < 651.0)
return hexColor(191.0, 191.0, 0.0);
if (x < 857.0)
return hexColor(0.0, 191.0, 191.0);
if (x < 1063.0)
return hexColor(0.0, 191.0, 0.0);
if (x < 1269.0)
return hexColor(191.0, 0.0, 191.0);
if (x < 1475.0)
return hexColor(191.0, 0.0, 0.0);
if (x < 1680.0)
return hexColor(0.0, 0.0, 191.0);
return hexColor(102.0, 102.0, 102.0);
}
float3 smpteMiddleA(float x)
{
if (x < 240.0)
return hexColor(0.0, 255.0, 255.0);
if (x < 445.0)
return hexColor(0.0, 63.0, 105.0);
if (x < 1680.0)
return hexColor(191.0, 191.0, 191.0);
return hexColor(0.0, 0.0, 255.0);
}
float3 smpteMiddleB(float x)
{
if (x < 240.0)
return hexColor(255.0, 255.0, 0.0);
if (x < 445.0)
return hexColor(65.0, 0.0, 119.0);
if (x < 1475.0)
{
float ramp = saturate((x - 445.0) / (1475.0 - 445.0));
return float3(ramp, ramp, ramp);
}
if (x < 1680.0)
return float3(1.0, 1.0, 1.0);
return hexColor(255.0, 0.0, 0.0);
}
float3 smpteBottom(float x)
{
if (x < 240.0)
return hexColor(38.0, 38.0, 38.0);
if (x < 549.0)
return float3(0.0, 0.0, 0.0);
if (x < 960.0)
return float3(1.0, 1.0, 1.0);
if (x < 1268.0)
return float3(0.0, 0.0, 0.0);
if (x < 1337.0)
return hexColor(5.0, 5.0, 5.0);
if (x < 1405.0)
return float3(0.0, 0.0, 0.0);
if (x < 1474.0)
return hexColor(10.0, 10.0, 10.0);
if (x < 1680.0)
return float3(0.0, 0.0, 0.0);
return hexColor(38.0, 38.0, 38.0);
}
float4 shadeVideo(ShaderContext context)
{
float2 uv = saturate(context.uv);
float2 pixel = float2(uv.x, 1.0 - uv.y) * float2(1920.0, 1080.0);
if (pixel.y < 630.0)
return float4(smpteTop(pixel.x), 1.0);
if (pixel.y < 720.0)
return float4(smpteMiddleA(pixel.x), 1.0);
if (pixel.y < 810.0)
return float4(smpteMiddleB(pixel.x), 1.0);
return float4(smpteBottom(pixel.x), 1.0);
}

View File

@@ -0,0 +1,21 @@
{
"id": "solid-color",
"name": "Solid Color",
"description": "Fills the frame with a single user-selected color.",
"category": "Color",
"entryPoint": "shadeVideo",
"parameters": [
{
"id": "fillColor",
"label": "Fill",
"type": "color",
"default": [
1,
1,
1,
1
],
"description": "Frame fill color; alpha is preserved for key-capable outputs."
}
]
}

Some files were not shown because too many files have changed in this diff Show More