201 Commits

Author SHA1 Message Date
dfba5dbc40 added OCIO libary
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 4m0s
2026-06-03 01:16:40 -04:00
659fbef1a5 OCIO and Fullscreen stubs
All checks were successful
CI / React UI Build (push) Successful in 12s
CI / Native Windows Build And Tests (push) Successful in 2m10s
CI / Windows Release Package (push) Has been skipped
2026-06-03 01:08:43 -04:00
8a6bb81a37 Font loading fixes
All checks were successful
CI / React UI Build (push) Successful in 39s
CI / Native Windows Build And Tests (push) Successful in 2m21s
CI / Windows Release Package (push) Has been skipped
2026-06-03 00:45:31 -04:00
b7ce079a26 Fixed duplication
All checks were successful
CI / React UI Build (push) Successful in 39s
CI / Native Windows Build And Tests (push) Successful in 2m23s
CI / Windows Release Package (push) Successful in 3m6s
2026-06-01 06:52:56 -04:00
171b790fa3 Fixed bindings 2026-06-01 06:45:41 -04:00
Aiden
27690c3afa added optional web component UI control
All checks were successful
CI / React UI Build (push) Successful in 12s
CI / Native Windows Build And Tests (push) Successful in 2m20s
CI / Windows Release Package (push) Successful in 2m56s
2026-05-30 22:57:59 +10:00
Aiden
a6d2ee385e Delete FORKING_RENDER_CADENCE_BASE.md
All checks were successful
CI / React UI Build (push) Successful in 11s
CI / Native Windows Build And Tests (push) Successful in 2m34s
CI / Windows Release Package (push) Has been skipped
2026-05-30 21:36:20 +10:00
Aiden
fce3c3c5ae Doc cleanup
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-30 21:35:07 +10:00
Aiden
56883439a6 clean split
All checks were successful
CI / React UI Build (push) Successful in 12s
CI / Native Windows Build And Tests (push) Successful in 2m14s
CI / Windows Release Package (push) Has been skipped
2026-05-30 21:22:03 +10:00
Aiden
fbc2851ccb Slang seperation
All checks were successful
CI / React UI Build (push) Successful in 12s
CI / Native Windows Build And Tests (push) Successful in 2m21s
CI / Windows Release Package (push) Has been skipped
2026-05-30 21:05:30 +10:00
Aiden
067c606092 removed hard coded shader start up
All checks were successful
CI / React UI Build (push) Successful in 11s
CI / Native Windows Build And Tests (push) Successful in 2m27s
CI / Windows Release Package (push) Has been skipped
2026-05-30 20:57:01 +10:00
Aiden
0f3db3ba1b OSC stubs
All checks were successful
CI / React UI Build (push) Successful in 12s
CI / Native Windows Build And Tests (push) Successful in 2m11s
CI / Windows Release Package (push) Has been skipped
2026-05-30 20:42:38 +10:00
Aiden
04e0802ef2 HTTP boundry
All checks were successful
CI / React UI Build (push) Successful in 12s
CI / Native Windows Build And Tests (push) Successful in 2m22s
CI / Windows Release Package (push) Has been skipped
2026-05-30 20:34:52 +10:00
Aiden
aa33d72b6e config UI updates
All checks were successful
CI / React UI Build (push) Successful in 11s
CI / Native Windows Build And Tests (push) Successful in 2m8s
CI / Windows Release Package (push) Has been skipped
2026-05-30 20:20:22 +10:00
Aiden
216a561ede UI fix
All checks were successful
CI / React UI Build (push) Successful in 12s
CI / Native Windows Build And Tests (push) Successful in 2m8s
CI / Windows Release Package (push) Has been skipped
2026-05-30 20:10:57 +10:00
Aiden
2b995ac058 NDI discovery
All checks were successful
CI / React UI Build (push) Successful in 11s
CI / Native Windows Build And Tests (push) Successful in 2m10s
CI / Windows Release Package (push) Has been skipped
2026-05-30 20:03:01 +10:00
Aiden
8ffc011ca0 Added config editor in front end
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) Has been skipped
2026-05-30 19:33:40 +10:00
Aiden
f0f8b080ca UYVY backend
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) Has been skipped
2026-05-30 19:16:16 +10:00
Aiden
d0b1f63524 Font fix and forkable docs
All checks were successful
CI / React UI Build (push) Successful in 12s
CI / Native Windows Build And Tests (push) Successful in 2m52s
CI / Windows Release Package (push) Has been skipped
2026-05-30 15:01:36 +10:00
cfb796756d Update ci.yml
Some checks failed
CI / React UI Build (push) Successful in 11s
CI / Native Windows Build And Tests (push) Failing after 2m46s
CI / Windows Release Package (push) Has been skipped
2026-05-30 14:43:33 +10:00
e88a4a1f3b CI/CD updates
Some checks failed
CI / Native Windows Build And Tests (push) Failing after -34s
CI / React UI Build (push) Successful in 11s
CI / Windows Release Package (push) Has been skipped
2026-05-30 14:41:20 +10:00
4bfabaca88 SDK changes for submodule
Some checks failed
CI / React UI Build (push) Successful in 11s
CI / Native Windows Build And Tests (push) Failing after 1m39s
CI / Windows Release Package (push) Has been skipped
2026-05-30 14:38:49 +10:00
16548d54d3 added 3rd party repo
Some checks failed
CI / React UI Build (push) Successful in 38s
CI / Native Windows Build And Tests (push) Failing after 1m53s
CI / Windows Release Package (push) Has been skipped
2026-05-30 14:32:46 +10:00
b96b32b441 Update README.md
Some checks failed
CI / Native Windows Build And Tests (push) Failing after 1m58s
CI / React UI Build (push) Successful in 11s
CI / Windows Release Package (push) Has been skipped
2026-05-22 17:50:11 +10:00
3c3b1d68ff UI Cleanup
Some checks failed
CI / React UI Build (push) Successful in 11s
CI / Native Windows Build And Tests (push) Failing after 1m44s
CI / Windows Release Package (push) Has been skipped
2026-05-22 17:44:12 +10:00
c35ca8d61c Font fixes
Some checks failed
CI / React UI Build (push) Successful in 10s
CI / Native Windows Build And Tests (push) Failing after 1m50s
CI / Windows Release Package (push) Has been skipped
2026-05-22 17:38:28 +10:00
be9f3b4e8b font changing on the fly 2026-05-22 17:27:29 +10:00
af448c338c docs 2026-05-22 17:24:58 +10:00
283f38dddb Font selector 2026-05-22 17:22:57 +10:00
c5f0a9df0e NDI output flip
Some checks failed
CI / React UI Build (push) Successful in 11s
CI / Native Windows Build And Tests (push) Failing after 1m47s
CI / Windows Release Package (push) Has been skipped
2026-05-22 17:04:27 +10:00
80c6fd2434 NDI cleanup to avoid calling NDI while holding the output mutex
Some checks failed
CI / React UI Build (push) Successful in 10s
CI / Native Windows Build And Tests (push) Failing after 1m46s
CI / Windows Release Package (push) Has been skipped
2026-05-22 16:51:48 +10:00
f8c3c60611 build release check 2026-05-22 16:46:39 +10:00
93aa8fa981 NDI output
Some checks failed
CI / React UI Build (push) Successful in 10s
CI / Native Windows Build And Tests (push) Failing after 1m47s
CI / Windows Release Package (push) Has been skipped
2026-05-22 16:41:24 +10:00
4e6b37304f NDI related tests 2026-05-22 16:34:20 +10:00
cec8b76f61 schema settings 2026-05-22 16:31:49 +10:00
521b3cb09e fall back to black
Some checks failed
CI / React UI Build (push) Successful in 11s
CI / Native Windows Build And Tests (push) Failing after 1m43s
CI / Windows Release Package (push) Has been skipped
2026-05-22 16:29:08 +10:00
3bd6aeb52f NDI input
Some checks failed
CI / React UI Build (push) Successful in 11s
CI / Native Windows Build And Tests (push) Failing after 1m48s
CI / Windows Release Package (push) Has been skipped
2026-05-22 16:25:56 +10:00
e006fcc6ee config cleanup
Some checks failed
CI / React UI Build (push) Successful in 11s
CI / Native Windows Build And Tests (push) Failing after 2m30s
CI / Windows Release Package (push) Has been skipped
2026-05-22 16:05:40 +10:00
2058f94193 generic telemetry 2026-05-22 15:53:56 +10:00
6e8f18e24c Update VideoBackendFactory.cpp 2026-05-22 15:51:44 +10:00
2fdb1741f9 legacy code cleanup
Some checks failed
CI / React UI Build (push) Successful in 10s
CI / Native Windows Build And Tests (push) Failing after 2m31s
CI / Windows Release Package (push) Has been skipped
2026-05-22 15:45:54 +10:00
d831b418d7 Intial cleanup 2026-05-22 15:37:50 +10:00
e6faaee1ca Generic telemetry
Some checks failed
CI / React UI Build (push) Successful in 10s
CI / Native Windows Build And Tests (push) Failing after 2m30s
CI / Windows Release Package (push) Has been skipped
2026-05-22 15:32:55 +10:00
64a6125c3f decklink start up to separate factory 2026-05-22 15:27:46 +10:00
315cbda9d1 replace direct ownership 2026-05-22 15:22:11 +10:00
9787ca5f27 Clean up video IO
Some checks failed
CI / React UI Build (push) Successful in 10s
CI / Native Windows Build And Tests (push) Failing after 2m33s
CI / Windows Release Package (push) Has been skipped
2026-05-22 15:16:35 +10:00
7bf5464fd2 reorganization
Some checks failed
CI / React UI Build (push) Successful in 11s
CI / Native Windows Build And Tests (push) Failing after 2m33s
CI / Windows Release Package (push) Has been skipped
2026-05-22 15:06:26 +10:00
b7e7452567 videoIO seperation 2026-05-22 14:58:42 +10:00
35801601a5 Cmake fixes
Some checks failed
CI / React UI Build (push) Successful in 11s
CI / Native Windows Build And Tests (push) Failing after 2m28s
CI / Windows Release Package (push) Has been skipped
2026-05-22 14:48:27 +10:00
0b6a2300ea Fixes
Some checks failed
CI / React UI Build (push) Successful in 38s
CI / Native Windows Build And Tests (push) Failing after 3m0s
CI / Windows Release Package (push) Has been skipped
2026-05-22 14:43:03 +10:00
084e60cbe0 render thread split 2026-05-21 17:57:29 +10:00
f2ff69fe90 Font builder changes
Some checks failed
CI / React UI Build (push) Successful in 10s
CI / Native Windows Build And Tests (push) Failing after 4m47s
CI / Windows Release Package (push) Has been skipped
2026-05-21 17:52:08 +10:00
4ddb5b6428 split shaders
Some checks failed
CI / React UI Build (push) Successful in 11s
CI / Native Windows Build And Tests (push) Failing after 2m18s
CI / Windows Release Package (push) Has been skipped
2026-05-21 17:48:27 +10:00
108edc096e Separating build queue 2026-05-21 17:43:17 +10:00
269dbd0079 Runtime layer split 2026-05-21 17:40:31 +10:00
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
298 changed files with 28365 additions and 12817 deletions

View File

@@ -7,6 +7,9 @@ on:
- master
- develop
pull_request:
schedule:
# Nightly build at 14:00 UTC, roughly midnight in Australia/Sydney.
- cron: "0 14 * * *"
workflow_dispatch:
jobs:
@@ -15,8 +18,53 @@ jobs:
runs-on: windows-2022
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Manual Checkout With LFS Submodules
shell: powershell
env:
GITEA_PAT: ${{ secrets.TOKEN }}
run: |
function Invoke-Git {
& git @args
if ($LASTEXITCODE -ne 0) {
exit $LASTEXITCODE
}
}
function Add-GiteaTokenToUrl([string]$url) {
if ([string]::IsNullOrWhiteSpace($env:GITEA_PAT)) {
return $url
}
return $url -replace "^https://", "https://aiden:$($env:GITEA_PAT)@"
}
$serverUrl = "${{ gitea.server_url }}".TrimEnd("/")
$repository = "${{ gitea.repository }}"
if ([string]::IsNullOrWhiteSpace($serverUrl)) {
$serverUrl = "https://git.f-40.com"
}
if ([string]::IsNullOrWhiteSpace($repository)) {
$repository = "aiden/video-shader-toys"
}
$repoUrl = Add-GiteaTokenToUrl "$serverUrl/$repository.git"
Invoke-Git init .
Invoke-Git lfs install --local
Invoke-Git remote add origin "$repoUrl"
Invoke-Git fetch --depth=1 origin "${{ gitea.ref }}"
Invoke-Git checkout FETCH_HEAD
Invoke-Git lfs pull origin "${{ gitea.ref }}"
Invoke-Git submodule sync --recursive
$submoduleUrl = Invoke-Git config --file .gitmodules --get submodule.video-io-3rdParty.url
$submoduleUrl = Add-GiteaTokenToUrl "$submoduleUrl"
Invoke-Git config submodule.video-io-3rdParty.url "$submoduleUrl"
Invoke-Git submodule update --init --recursive --depth=1
Invoke-Git -C video-io-3rdParty lfs pull
Write-Host "Parent LFS files:"
Invoke-Git lfs ls-files
Write-Host "Submodule LFS files:"
Invoke-Git -C video-io-3rdParty lfs ls-files
- name: Verify Visual Studio ATL
shell: powershell
@@ -31,37 +79,81 @@ jobs:
- name: Configure Debug
shell: powershell
run: |
$thirdPartyRoot = "${{ vars.THIRD_PARTY_ROOT }}"
if ([string]::IsNullOrWhiteSpace($thirdPartyRoot)) {
$thirdPartyRoot = $env:THIRD_PARTY_ROOT
}
if ([string]::IsNullOrWhiteSpace($thirdPartyRoot)) {
$thirdPartyRoot = Join-Path $PWD "video-io-3rdParty"
}
if (-not (Test-Path -LiteralPath $thirdPartyRoot)) {
$thirdPartyRoot = Join-Path $PWD "3rdParty"
}
$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"
$slangRoot = Join-Path $thirdPartyRoot "slang-2026.8-windows-x86_64"
}
$msdfAtlasGenRoot = "${{ vars.MSDF_ATLAS_GEN_ROOT }}"
if ([string]::IsNullOrWhiteSpace($msdfAtlasGenRoot)) {
$msdfAtlasGenRoot = $env:MSDF_ATLAS_GEN_ROOT
}
if ([string]::IsNullOrWhiteSpace($msdfAtlasGenRoot)) {
$msdfAtlasGenRoot = Join-Path $thirdPartyRoot "msdf-atlas-gen"
}
$ndiSdkRoot = "${{ vars.NDI_SDK_ROOT }}"
if ([string]::IsNullOrWhiteSpace($ndiSdkRoot)) {
$ndiSdkRoot = $env:NDI_SDK_ROOT
}
if ([string]::IsNullOrWhiteSpace($ndiSdkRoot)) {
$ndiSdkRoot = Join-Path $thirdPartyRoot "NDI 6 SDK"
}
$deckLinkSdkRoot = "${{ vars.DECKLINK_SDK_ROOT }}"
if ([string]::IsNullOrWhiteSpace($deckLinkSdkRoot)) {
$deckLinkSdkRoot = $env:DECKLINK_SDK_ROOT
}
if ([string]::IsNullOrWhiteSpace($deckLinkSdkRoot)) {
$deckLinkSdkRoot = Join-Path $thirdPartyRoot "Blackmagic DeckLink SDK 16.0"
}
$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")
(Join-Path $slangRoot "LICENSE"),
(Join-Path $msdfAtlasGenRoot "msdf-atlas-gen.exe"),
(Join-Path $ndiSdkRoot "Include\Processing.NDI.Lib.h"),
(Join-Path $ndiSdkRoot "Lib\x64\Processing.NDI.Lib.x64.lib"),
(Join-Path $ndiSdkRoot "Bin\x64\Processing.NDI.Lib.x64.dll"),
(Join-Path $deckLinkSdkRoot "Win\include\DeckLinkAPI.idl")
)
$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 ', ')"
Write-Error "Missing native third-party dependencies. Initialize the private video-io-3rdParty submodule, set THIRD_PARTY_ROOT, or configure per-SDK repository variables. Missing: $($missingFiles -join ', ')"
exit 1
}
Write-Host "Using THIRD_PARTY_ROOT=$thirdPartyRoot"
Write-Host "Using SLANG_ROOT=$slangRoot"
cmake --preset vs2022-x64-debug -DSLANG_ROOT="$slangRoot"
Write-Host "Using MSDF_ATLAS_GEN_ROOT=$msdfAtlasGenRoot"
Write-Host "Using NDI_SDK_ROOT=$ndiSdkRoot"
Write-Host "Using DECKLINK_SDK_ROOT=$deckLinkSdkRoot"
cmake --preset vs2022-x64-debug -DTHIRD_PARTY_ROOT="$thirdPartyRoot" -DSLANG_ROOT="$slangRoot" -DMSDF_ATLAS_GEN_ROOT="$msdfAtlasGenRoot" -DNDI_SDK_ROOT="$ndiSdkRoot" -DDECKLINK_SDK_ROOT="$deckLinkSdkRoot"
- name: Build Debug
shell: powershell
run: cmake --build --preset build-debug
run: cmake --build --preset build-debug --parallel
- 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
@@ -82,13 +174,59 @@ jobs:
package-windows:
name: Windows Release Package
runs-on: windows-2022
if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch'
needs:
- native-windows
- ui-ubuntu
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Manual Checkout With LFS Submodules
shell: powershell
env:
GITEA_PAT: ${{ secrets.TOKEN }}
run: |
function Invoke-Git {
& git @args
if ($LASTEXITCODE -ne 0) {
exit $LASTEXITCODE
}
}
function Add-GiteaTokenToUrl([string]$url) {
if ([string]::IsNullOrWhiteSpace($env:GITEA_PAT)) {
return $url
}
return $url -replace "^https://", "https://aiden:$($env:GITEA_PAT)@"
}
$serverUrl = "${{ gitea.server_url }}".TrimEnd("/")
$repository = "${{ gitea.repository }}"
if ([string]::IsNullOrWhiteSpace($serverUrl)) {
$serverUrl = "https://git.f-40.com"
}
if ([string]::IsNullOrWhiteSpace($repository)) {
$repository = "aiden/video-shader-toys"
}
$repoUrl = Add-GiteaTokenToUrl "$serverUrl/$repository.git"
Invoke-Git init .
Invoke-Git lfs install --local
Invoke-Git remote add origin "$repoUrl"
Invoke-Git fetch --depth=1 origin "${{ gitea.ref }}"
Invoke-Git checkout FETCH_HEAD
Invoke-Git lfs pull origin "${{ gitea.ref }}"
Invoke-Git submodule sync --recursive
$submoduleUrl = Invoke-Git config --file .gitmodules --get submodule.video-io-3rdParty.url
$submoduleUrl = Add-GiteaTokenToUrl "$submoduleUrl"
Invoke-Git config submodule.video-io-3rdParty.url "$submoduleUrl"
Invoke-Git submodule update --init --recursive --depth=1
Invoke-Git -C video-io-3rdParty lfs pull
Write-Host "Parent LFS files:"
Invoke-Git lfs ls-files
Write-Host "Submodule LFS files:"
Invoke-Git -C video-io-3rdParty lfs ls-files
- name: Verify Visual Studio ATL
shell: powershell
@@ -110,33 +248,77 @@ jobs:
- name: Configure Release
shell: powershell
run: |
$thirdPartyRoot = "${{ vars.THIRD_PARTY_ROOT }}"
if ([string]::IsNullOrWhiteSpace($thirdPartyRoot)) {
$thirdPartyRoot = $env:THIRD_PARTY_ROOT
}
if ([string]::IsNullOrWhiteSpace($thirdPartyRoot)) {
$thirdPartyRoot = Join-Path $PWD "video-io-3rdParty"
}
if (-not (Test-Path -LiteralPath $thirdPartyRoot)) {
$thirdPartyRoot = Join-Path $PWD "3rdParty"
}
$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"
$slangRoot = Join-Path $thirdPartyRoot "slang-2026.8-windows-x86_64"
}
$msdfAtlasGenRoot = "${{ vars.MSDF_ATLAS_GEN_ROOT }}"
if ([string]::IsNullOrWhiteSpace($msdfAtlasGenRoot)) {
$msdfAtlasGenRoot = $env:MSDF_ATLAS_GEN_ROOT
}
if ([string]::IsNullOrWhiteSpace($msdfAtlasGenRoot)) {
$msdfAtlasGenRoot = Join-Path $thirdPartyRoot "msdf-atlas-gen"
}
$ndiSdkRoot = "${{ vars.NDI_SDK_ROOT }}"
if ([string]::IsNullOrWhiteSpace($ndiSdkRoot)) {
$ndiSdkRoot = $env:NDI_SDK_ROOT
}
if ([string]::IsNullOrWhiteSpace($ndiSdkRoot)) {
$ndiSdkRoot = Join-Path $thirdPartyRoot "NDI 6 SDK"
}
$deckLinkSdkRoot = "${{ vars.DECKLINK_SDK_ROOT }}"
if ([string]::IsNullOrWhiteSpace($deckLinkSdkRoot)) {
$deckLinkSdkRoot = $env:DECKLINK_SDK_ROOT
}
if ([string]::IsNullOrWhiteSpace($deckLinkSdkRoot)) {
$deckLinkSdkRoot = Join-Path $thirdPartyRoot "Blackmagic DeckLink SDK 16.0"
}
$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")
(Join-Path $slangRoot "LICENSE"),
(Join-Path $msdfAtlasGenRoot "msdf-atlas-gen.exe"),
(Join-Path $ndiSdkRoot "Include\Processing.NDI.Lib.h"),
(Join-Path $ndiSdkRoot "Lib\x64\Processing.NDI.Lib.x64.lib"),
(Join-Path $ndiSdkRoot "Bin\x64\Processing.NDI.Lib.x64.dll"),
(Join-Path $deckLinkSdkRoot "Win\include\DeckLinkAPI.idl")
)
$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 ', ')"
Write-Error "Missing native third-party dependencies. Initialize the private video-io-3rdParty submodule, set THIRD_PARTY_ROOT, or configure per-SDK repository variables. Missing: $($missingFiles -join ', ')"
exit 1
}
Write-Host "Using THIRD_PARTY_ROOT=$thirdPartyRoot"
Write-Host "Using SLANG_ROOT=$slangRoot"
cmake --preset vs2022-x64-release -DSLANG_ROOT="$slangRoot"
Write-Host "Using MSDF_ATLAS_GEN_ROOT=$msdfAtlasGenRoot"
Write-Host "Using NDI_SDK_ROOT=$ndiSdkRoot"
Write-Host "Using DECKLINK_SDK_ROOT=$deckLinkSdkRoot"
cmake --preset vs2022-x64-release -DTHIRD_PARTY_ROOT="$thirdPartyRoot" -DSLANG_ROOT="$slangRoot" -DMSDF_ATLAS_GEN_ROOT="$msdfAtlasGenRoot" -DNDI_SDK_ROOT="$ndiSdkRoot" -DDECKLINK_SDK_ROOT="$deckLinkSdkRoot"
- name: Build Release
shell: powershell
run: cmake --build --preset build-release
run: cmake --build --preset build-release --parallel
- name: Install Runtime Package
shell: powershell

3
.gitmodules vendored Normal file
View File

@@ -0,0 +1,3 @@
[submodule "video-io-3rdParty"]
path = video-io-3rdParty
url = https://git.f-40.com/aiden/video-io-3rdParty.git

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"
}
]
}

11
.vscode/settings.json vendored Normal file
View File

@@ -0,0 +1,11 @@
{
"json.schemas": [
{
"fileMatch": [
"/config/runtime-host.json",
"/config/runtime-host.*.json"
],
"url": "./config/runtime-host.schema.json"
}
]
}

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,353 +1,316 @@
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(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(SRC_DIR "${CMAKE_CURRENT_SOURCE_DIR}/src")
set(TEST_DIR "${CMAKE_CURRENT_SOURCE_DIR}/tests")
if(NOT EXISTS "${APP_DIR}/LoopThroughWithOpenGLCompositing.cpp")
message(FATAL_ERROR "Imported app sources were not found under ${APP_DIR}")
set(LEGACY_THIRD_PARTY_ROOT "${CMAKE_CURRENT_SOURCE_DIR}/3rdParty")
set(DEFAULT_THIRD_PARTY_ROOT "${CMAKE_CURRENT_SOURCE_DIR}/video-io-3rdParty")
if(NOT EXISTS "${DEFAULT_THIRD_PARTY_ROOT}")
set(DEFAULT_THIRD_PARTY_ROOT "${LEGACY_THIRD_PARTY_ROOT}")
endif()
set(THIRD_PARTY_ROOT "${DEFAULT_THIRD_PARTY_ROOT}" CACHE PATH "Path to the third-party SDK bundle")
if(EXISTS "${DEFAULT_THIRD_PARTY_ROOT}" AND THIRD_PARTY_ROOT STREQUAL LEGACY_THIRD_PARTY_ROOT AND NOT EXISTS "${THIRD_PARTY_ROOT}")
set(THIRD_PARTY_ROOT "${DEFAULT_THIRD_PARTY_ROOT}" CACHE PATH "Path to the third-party SDK bundle" FORCE)
endif()
set(SLANG_ROOT "${THIRD_PARTY_ROOT}/slang-2026.8-windows-x86_64" CACHE PATH "Path to a Slang binary release containing bin/slangc.exe")
set(MSDF_ATLAS_GEN_ROOT "${THIRD_PARTY_ROOT}/msdf-atlas-gen" CACHE PATH "Path to msdf-atlas-gen binary release")
set(NDI_SDK_ROOT "${THIRD_PARTY_ROOT}/NDI 6 SDK" CACHE PATH "Path to the NDI SDK")
set(DECKLINK_SDK_ROOT "${THIRD_PARTY_ROOT}/Blackmagic DeckLink SDK 16.0" CACHE PATH "Path to the Blackmagic DeckLink SDK")
set(LEGACY_SLANG_ROOT "${LEGACY_THIRD_PARTY_ROOT}/slang-2026.8-windows-x86_64")
set(LEGACY_MSDF_ATLAS_GEN_ROOT "${LEGACY_THIRD_PARTY_ROOT}/msdf-atlas-gen")
set(LEGACY_NDI_SDK_ROOT "${LEGACY_THIRD_PARTY_ROOT}/NDI 6 SDK")
set(LEGACY_DECKLINK_SDK_ROOT "${LEGACY_THIRD_PARTY_ROOT}/Blackmagic DeckLink SDK 16.0")
if(EXISTS "${DEFAULT_THIRD_PARTY_ROOT}")
if(SLANG_ROOT STREQUAL LEGACY_SLANG_ROOT AND NOT EXISTS "${SLANG_ROOT}")
set(SLANG_ROOT "${THIRD_PARTY_ROOT}/slang-2026.8-windows-x86_64" CACHE PATH "Path to a Slang binary release containing bin/slangc.exe" FORCE)
endif()
if(MSDF_ATLAS_GEN_ROOT STREQUAL LEGACY_MSDF_ATLAS_GEN_ROOT AND NOT EXISTS "${MSDF_ATLAS_GEN_ROOT}")
set(MSDF_ATLAS_GEN_ROOT "${THIRD_PARTY_ROOT}/msdf-atlas-gen" CACHE PATH "Path to msdf-atlas-gen binary release" FORCE)
endif()
if(NDI_SDK_ROOT STREQUAL LEGACY_NDI_SDK_ROOT AND NOT EXISTS "${NDI_SDK_ROOT}")
set(NDI_SDK_ROOT "${THIRD_PARTY_ROOT}/NDI 6 SDK" CACHE PATH "Path to the NDI SDK" FORCE)
endif()
if(DECKLINK_SDK_ROOT STREQUAL LEGACY_DECKLINK_SDK_ROOT AND NOT EXISTS "${DECKLINK_SDK_ROOT}")
set(DECKLINK_SDK_ROOT "${THIRD_PARTY_ROOT}/Blackmagic DeckLink SDK 16.0" CACHE PATH "Path to the Blackmagic DeckLink SDK" FORCE)
endif()
endif()
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/thread"
"${SRC_DIR}/render/runtime"
"${SRC_DIR}/runtime"
"${SRC_DIR}/runtime/catalog"
"${SRC_DIR}/runtime/layers"
"${SRC_DIR}/runtime/shader"
"${SRC_DIR}/runtime/state"
"${SRC_DIR}/runtime/text"
"${SRC_DIR}/shader"
"${SRC_DIR}/telemetry"
"${SRC_DIR}/video"
"${SRC_DIR}/video/core"
"${SRC_DIR}/video/decklink"
"${SRC_DIR}/video/ndi"
"${SRC_DIR}/video/playout"
)
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(RUNTIME_JSON_SOURCES
"${SRC_DIR}/runtime/state/RuntimeJson.cpp"
)
set(RUNTIME_PARAMETER_SOURCES
${RUNTIME_JSON_SOURCES}
"${SRC_DIR}/runtime/state/RuntimeParameterUtils.cpp"
)
set(RUNTIME_STATE_SOURCES
${RUNTIME_JSON_SOURCES}
"${SRC_DIR}/runtime/state/RuntimeStatePersistence.cpp"
)
set(RUNTIME_LAYER_SOURCES
"${SRC_DIR}/runtime/layers/RuntimeLayerModel.cpp"
"${SRC_DIR}/runtime/layers/RuntimeLayerReload.cpp"
"${SRC_DIR}/runtime/layers/RuntimeLayerSnapshot.cpp"
"${SRC_DIR}/runtime/layers/RuntimeLayerStateRestore.cpp"
)
set(RUNTIME_TEXT_SOURCES
"${SRC_DIR}/runtime/text/FontAtlasBuilder.cpp"
"${SRC_DIR}/runtime/text/FontAtlasImageLoader.cpp"
"${SRC_DIR}/runtime/text/FontAtlasMetadata.cpp"
"${SRC_DIR}/runtime/text/FontAtlasProcess.cpp"
"${SRC_DIR}/runtime/text/RuntimeTextTextureComposer.cpp"
)
set(RUNTIME_CATALOG_SOURCES
"${SRC_DIR}/runtime/catalog/SupportedShaderCatalog.cpp"
)
set(SHADER_MANIFEST_SOURCES
"${SRC_DIR}/shader/ShaderManifestAssets.cpp"
"${SRC_DIR}/shader/ShaderManifestParameters.cpp"
"${SRC_DIR}/shader/ShaderManifestParser.cpp"
"${SRC_DIR}/shader/ShaderPackageRegistry.cpp"
"${SRC_DIR}/shader/ShaderUiPath.cpp"
)
set(VIDEO_MODE_SOURCES
"${SRC_DIR}/video/core/VideoMode.cpp"
)
set(VIDEO_FORMAT_SOURCES
"${SRC_DIR}/video/core/VideoIOFormat.cpp"
)
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(NDI_INCLUDE_DIR "${NDI_SDK_ROOT}/Include")
set(NDI_RUNTIME_DLL "${NDI_SDK_ROOT}/Bin/x64/Processing.NDI.Lib.x64.dll")
set(NDI_IMPORT_LIB "${NDI_SDK_ROOT}/Lib/x64/Processing.NDI.Lib.x64.lib")
set(NDI_LICENSE_FILE "${NDI_SDK_ROOT}/NDI SDK License Agreement.pdf")
set(NDI_NOTICES_FILE "${NDI_SDK_ROOT}/Bin/x64/Processing.NDI.Lib.Licenses.txt")
set(DECKLINK_SDK_IDL_FILE "${DECKLINK_SDK_ROOT}/Win/include/DeckLinkAPI.idl")
set(DECKLINK_SDK_LICENSE_FILE "${DECKLINK_SDK_ROOT}/End User License Agreement.pdf")
foreach(SLANG_RUNTIME_FILE IN LISTS SLANG_RUNTIME_FILES)
if(NOT EXISTS "${SLANG_RUNTIME_FILE}")
message(FATAL_ERROR "Required Slang runtime file not found: ${SLANG_RUNTIME_FILE}")
set(RENDER_CADENCE_APP_REQUIRED_FILES
"${SRC_DIR}/RenderCadenceCompositor.cpp"
"${SRC_DIR}/video/decklink/DeckLinkAPI_i.c"
"${SRC_DIR}/video/decklink/DeckLinkAPI_h.h"
)
video_shader_files_exist(RENDER_CADENCE_APP_MISSING_FILES ${RENDER_CADENCE_APP_REQUIRED_FILES})
if(EXISTS "${DECKLINK_SDK_IDL_FILE}")
message(STATUS "Blackmagic DeckLink SDK found: ${DECKLINK_SDK_ROOT}")
else()
message(STATUS "Blackmagic DeckLink SDK not found at ${DECKLINK_SDK_ROOT}; using checked-in DeckLink API shim files only")
endif()
if(RENDER_CADENCE_APP_MISSING_FILES)
message(STATUS "RenderCadenceCompositor target skipped; required app entry or external DeckLink SDK shim files are missing:")
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"
)
add_executable(RenderCadenceCompositor ${RENDER_CADENCE_APP_SOURCES})
video_shader_target_defaults(RenderCadenceCompositor)
if(EXISTS "${NDI_INCLUDE_DIR}" AND EXISTS "${NDI_IMPORT_LIB}")
target_include_directories(RenderCadenceCompositor PRIVATE "${NDI_INCLUDE_DIR}")
target_link_libraries(RenderCadenceCompositor PRIVATE "${NDI_IMPORT_LIB}")
if(EXISTS "${NDI_RUNTIME_DLL}")
add_custom_command(TARGET RenderCadenceCompositor POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy_if_different
"${NDI_RUNTIME_DLL}"
"$<TARGET_FILE_DIR:RenderCadenceCompositor>/Processing.NDI.Lib.x64.dll"
)
endif()
else()
message(STATUS "NDI SDK headers/import library not found; NDI backends will not build correctly: ${NDI_SDK_ROOT}")
endif()
target_link_libraries(RenderCadenceCompositor PRIVATE
opengl32
Ole32
Windowscodecs
Ws2_32
)
source_group(TREE "${SRC_DIR}" FILES ${RENDER_CADENCE_APP_SOURCES})
endif()
if(BUILD_TESTING)
add_subdirectory(tests)
endif()
if(TARGET RenderCadenceCompositor)
install(TARGETS RenderCadenceCompositor
RUNTIME DESTINATION "."
)
endif()
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()
set(SLANG_LICENSE_FILE "${SLANG_ROOT}/LICENSE")
if(NOT EXISTS "${SLANG_LICENSE_FILE}")
message(FATAL_ERROR "Slang license file not found: ${SLANG_LICENSE_FILE}")
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(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()
set(APP_SOURCES
"${APP_DIR}/videoio/decklink/DeckLinkAPI_i.c"
"${APP_DIR}/control/ControlServer.cpp"
"${APP_DIR}/control/ControlServer.h"
"${APP_DIR}/control/OscServer.cpp"
"${APP_DIR}/control/OscServer.h"
"${APP_DIR}/control/RuntimeControlBridge.cpp"
"${APP_DIR}/control/RuntimeControlBridge.h"
"${APP_DIR}/control/RuntimeServices.cpp"
"${APP_DIR}/control/RuntimeServices.h"
"${APP_DIR}/videoio/decklink/DeckLinkAPI_h.h"
"${APP_DIR}/videoio/decklink/DeckLinkDisplayMode.cpp"
"${APP_DIR}/videoio/decklink/DeckLinkDisplayMode.h"
"${APP_DIR}/videoio/decklink/DeckLinkFrameTransfer.cpp"
"${APP_DIR}/videoio/decklink/DeckLinkFrameTransfer.h"
"${APP_DIR}/videoio/decklink/DeckLinkSession.cpp"
"${APP_DIR}/videoio/decklink/DeckLinkSession.h"
"${APP_DIR}/videoio/decklink/DeckLinkVideoIOFormat.cpp"
"${APP_DIR}/videoio/decklink/DeckLinkVideoIOFormat.h"
"${APP_DIR}/gl/renderer/GLExtensions.cpp"
"${APP_DIR}/gl/renderer/GLExtensions.h"
"${APP_DIR}/gl/shader/GlobalParamsBuffer.cpp"
"${APP_DIR}/gl/shader/GlobalParamsBuffer.h"
"${APP_DIR}/gl/renderer/GlRenderConstants.h"
"${APP_DIR}/gl/renderer/GlScopedObjects.h"
"${APP_DIR}/gl/shader/GlShaderSources.cpp"
"${APP_DIR}/gl/shader/GlShaderSources.h"
"${APP_DIR}/gl/OpenGLComposite.cpp"
"${APP_DIR}/gl/OpenGLComposite.h"
"${APP_DIR}/gl/OpenGLCompositeRuntimeControls.cpp"
"${APP_DIR}/gl/pipeline/OpenGLRenderPass.cpp"
"${APP_DIR}/gl/pipeline/OpenGLRenderPass.h"
"${APP_DIR}/gl/pipeline/OpenGLRenderPipeline.cpp"
"${APP_DIR}/gl/pipeline/OpenGLRenderPipeline.h"
"${APP_DIR}/gl/pipeline/RenderPassDescriptor.h"
"${APP_DIR}/gl/renderer/OpenGLRenderer.cpp"
"${APP_DIR}/gl/renderer/OpenGLRenderer.h"
"${APP_DIR}/gl/renderer/RenderTargetPool.cpp"
"${APP_DIR}/gl/renderer/RenderTargetPool.h"
"${APP_DIR}/gl/pipeline/OpenGLVideoIOBridge.cpp"
"${APP_DIR}/gl/pipeline/OpenGLVideoIOBridge.h"
"${APP_DIR}/gl/shader/OpenGLShaderPrograms.cpp"
"${APP_DIR}/gl/shader/OpenGLShaderPrograms.h"
"${APP_DIR}/gl/pipeline/PngScreenshotWriter.cpp"
"${APP_DIR}/gl/pipeline/PngScreenshotWriter.h"
"${APP_DIR}/gl/shader/ShaderProgramCompiler.cpp"
"${APP_DIR}/gl/shader/ShaderProgramCompiler.h"
"${APP_DIR}/gl/shader/ShaderBuildQueue.cpp"
"${APP_DIR}/gl/shader/ShaderBuildQueue.h"
"${APP_DIR}/gl/shader/ShaderTextureBindings.cpp"
"${APP_DIR}/gl/shader/ShaderTextureBindings.h"
"${APP_DIR}/gl/shader/Std140Buffer.h"
"${APP_DIR}/gl/shader/TextRasterizer.cpp"
"${APP_DIR}/gl/shader/TextRasterizer.h"
"${APP_DIR}/gl/shader/TextureAssetLoader.cpp"
"${APP_DIR}/gl/shader/TextureAssetLoader.h"
"${APP_DIR}/gl/pipeline/TemporalHistoryBuffers.cpp"
"${APP_DIR}/gl/pipeline/TemporalHistoryBuffers.h"
"${APP_DIR}/LoopThroughWithOpenGLCompositing.cpp"
"${APP_DIR}/LoopThroughWithOpenGLCompositing.h"
"${APP_DIR}/LoopThroughWithOpenGLCompositing.rc"
"${APP_DIR}/platform/NativeHandles.h"
"${APP_DIR}/platform/NativeSockets.h"
"${APP_DIR}/resource.h"
"${APP_DIR}/runtime/RuntimeHost.cpp"
"${APP_DIR}/runtime/RuntimeHost.h"
"${APP_DIR}/runtime/RuntimeClock.cpp"
"${APP_DIR}/runtime/RuntimeClock.h"
"${APP_DIR}/runtime/RuntimeJson.cpp"
"${APP_DIR}/runtime/RuntimeJson.h"
"${APP_DIR}/runtime/RuntimeParameterUtils.cpp"
"${APP_DIR}/runtime/RuntimeParameterUtils.h"
"${APP_DIR}/shader/ShaderCompiler.cpp"
"${APP_DIR}/shader/ShaderCompiler.h"
"${APP_DIR}/shader/ShaderPackageRegistry.cpp"
"${APP_DIR}/shader/ShaderPackageRegistry.h"
"${APP_DIR}/shader/ShaderTypes.h"
"${APP_DIR}/stdafx.cpp"
"${APP_DIR}/stdafx.h"
"${APP_DIR}/targetver.h"
"${APP_DIR}/videoio/VideoIOFormat.cpp"
"${APP_DIR}/videoio/VideoIOFormat.h"
"${APP_DIR}/videoio/VideoIOTypes.h"
"${APP_DIR}/videoio/VideoPlayoutScheduler.cpp"
"${APP_DIR}/videoio/VideoPlayoutScheduler.h"
)
add_executable(LoopThroughWithOpenGLCompositing WIN32 ${APP_SOURCES})
target_include_directories(LoopThroughWithOpenGLCompositing PRIVATE
"${APP_DIR}"
"${APP_DIR}/control"
"${APP_DIR}/gl"
"${APP_DIR}/gl/pipeline"
"${APP_DIR}/gl/renderer"
"${APP_DIR}/gl/shader"
"${APP_DIR}/platform"
"${APP_DIR}/runtime"
"${APP_DIR}/shader"
"${APP_DIR}/videoio"
"${APP_DIR}/videoio/decklink"
)
target_link_libraries(LoopThroughWithOpenGLCompositing PRIVATE
opengl32
glu32
Ws2_32
Crypt32
Advapi32
Gdiplus
Ole32
Windowscodecs
)
target_compile_definitions(LoopThroughWithOpenGLCompositing PRIVATE
_UNICODE
UNICODE
)
if(MSVC)
target_compile_options(LoopThroughWithOpenGLCompositing 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_executable(RuntimeJsonTests
"${APP_DIR}/runtime/RuntimeJson.cpp"
"${CMAKE_CURRENT_SOURCE_DIR}/tests/RuntimeJsonTests.cpp"
)
target_include_directories(RuntimeJsonTests PRIVATE
"${APP_DIR}"
"${APP_DIR}/runtime"
)
if(MSVC)
target_compile_options(RuntimeJsonTests PRIVATE /W3)
endif()
enable_testing()
add_test(NAME RuntimeJsonTests COMMAND RuntimeJsonTests)
add_executable(RuntimeClockTests
"${APP_DIR}/runtime/RuntimeClock.cpp"
"${CMAKE_CURRENT_SOURCE_DIR}/tests/RuntimeClockTests.cpp"
)
target_include_directories(RuntimeClockTests PRIVATE
"${APP_DIR}"
"${APP_DIR}/runtime"
)
if(MSVC)
target_compile_options(RuntimeClockTests PRIVATE /W3)
endif()
add_test(NAME RuntimeClockTests COMMAND RuntimeClockTests)
add_executable(RuntimeParameterUtilsTests
"${APP_DIR}/runtime/RuntimeJson.cpp"
"${APP_DIR}/runtime/RuntimeParameterUtils.cpp"
"${CMAKE_CURRENT_SOURCE_DIR}/tests/RuntimeParameterUtilsTests.cpp"
)
target_include_directories(RuntimeParameterUtilsTests PRIVATE
"${APP_DIR}"
"${APP_DIR}/runtime"
"${APP_DIR}/shader"
)
if(MSVC)
target_compile_options(RuntimeParameterUtilsTests PRIVATE /W3)
endif()
add_test(NAME RuntimeParameterUtilsTests COMMAND RuntimeParameterUtilsTests)
add_executable(Std140BufferTests
"${CMAKE_CURRENT_SOURCE_DIR}/tests/Std140BufferTests.cpp"
)
target_include_directories(Std140BufferTests PRIVATE
"${APP_DIR}"
"${APP_DIR}/gl"
"${APP_DIR}/gl/shader"
)
if(MSVC)
target_compile_options(Std140BufferTests PRIVATE /W3)
endif()
add_test(NAME Std140BufferTests COMMAND Std140BufferTests)
add_executable(ShaderPackageRegistryTests
"${APP_DIR}/runtime/RuntimeJson.cpp"
"${APP_DIR}/shader/ShaderPackageRegistry.cpp"
"${CMAKE_CURRENT_SOURCE_DIR}/tests/ShaderPackageRegistryTests.cpp"
)
target_include_directories(ShaderPackageRegistryTests PRIVATE
"${APP_DIR}"
"${APP_DIR}/runtime"
"${APP_DIR}/shader"
)
if(MSVC)
target_compile_options(ShaderPackageRegistryTests PRIVATE /W3)
endif()
add_test(NAME ShaderPackageRegistryTests COMMAND ShaderPackageRegistryTests)
add_executable(ShaderSlangValidationTests
"${APP_DIR}/runtime/RuntimeJson.cpp"
"${APP_DIR}/shader/ShaderCompiler.cpp"
"${APP_DIR}/shader/ShaderPackageRegistry.cpp"
"${CMAKE_CURRENT_SOURCE_DIR}/tests/ShaderSlangValidationTests.cpp"
)
target_include_directories(ShaderSlangValidationTests PRIVATE
"${APP_DIR}"
"${APP_DIR}/platform"
"${APP_DIR}/runtime"
"${APP_DIR}/shader"
)
if(MSVC)
target_compile_options(ShaderSlangValidationTests PRIVATE /W3)
endif()
add_test(NAME ShaderSlangValidationTests COMMAND ShaderSlangValidationTests)
set_tests_properties(ShaderSlangValidationTests PROPERTIES
ENVIRONMENT "SLANG_ROOT=${SLANG_ROOT}"
)
add_executable(OscServerTests
"${APP_DIR}/control/OscServer.cpp"
"${CMAKE_CURRENT_SOURCE_DIR}/tests/OscServerTests.cpp"
)
target_include_directories(OscServerTests PRIVATE
"${APP_DIR}"
"${APP_DIR}/control"
"${APP_DIR}/platform"
)
target_link_libraries(OscServerTests PRIVATE
Ws2_32
)
if(MSVC)
target_compile_options(OscServerTests PRIVATE /W3)
endif()
add_test(NAME OscServerTests COMMAND OscServerTests)
add_executable(VideoIOFormatTests
"${APP_DIR}/videoio/decklink/DeckLinkVideoIOFormat.cpp"
"${APP_DIR}/videoio/VideoIOFormat.cpp"
"${CMAKE_CURRENT_SOURCE_DIR}/tests/VideoIOFormatTests.cpp"
)
target_include_directories(VideoIOFormatTests PRIVATE
"${APP_DIR}"
"${APP_DIR}/videoio"
"${APP_DIR}/videoio/decklink"
)
if(MSVC)
target_compile_options(VideoIOFormatTests PRIVATE /W3)
endif()
add_test(NAME VideoIOFormatTests COMMAND VideoIOFormatTests)
add_executable(VideoPlayoutSchedulerTests
"${APP_DIR}/videoio/VideoPlayoutScheduler.cpp"
"${CMAKE_CURRENT_SOURCE_DIR}/tests/VideoPlayoutSchedulerTests.cpp"
)
target_include_directories(VideoPlayoutSchedulerTests PRIVATE
"${APP_DIR}"
"${APP_DIR}/videoio"
"${APP_DIR}/videoio/decklink"
)
if(MSVC)
target_compile_options(VideoPlayoutSchedulerTests PRIVATE /W3)
endif()
add_test(NAME VideoPlayoutSchedulerTests COMMAND VideoPlayoutSchedulerTests)
add_executable(VideoIODeviceFakeTests
"${APP_DIR}/videoio/VideoIOFormat.cpp"
"${CMAKE_CURRENT_SOURCE_DIR}/tests/VideoIODeviceFakeTests.cpp"
)
target_include_directories(VideoIODeviceFakeTests PRIVATE
"${APP_DIR}"
"${APP_DIR}/videoio"
"${APP_DIR}/videoio/decklink"
)
if(MSVC)
target_compile_options(VideoIODeviceFakeTests PRIVATE /W3)
endif()
add_test(NAME VideoIODeviceFakeTests COMMAND VideoIODeviceFakeTests)
install(TARGETS LoopThroughWithOpenGLCompositing
RUNTIME DESTINATION "."
)
install(FILES ${SLANG_RUNTIME_FILES}
DESTINATION "3rdParty/slang/bin"
)
install(FILES "${SLANG_LICENSE_FILE}"
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()
install(FILES "${CMAKE_CURRENT_SOURCE_DIR}/SHADER_CONTRACT.md"
if(EXISTS "${NDI_RUNTIME_DLL}")
install(FILES "${NDI_RUNTIME_DLL}"
DESTINATION "."
)
else()
message(STATUS "NDI runtime DLL not found and will not be installed: ${NDI_RUNTIME_DLL}")
endif()
if(EXISTS "${NDI_LICENSE_FILE}")
install(FILES "${NDI_LICENSE_FILE}"
DESTINATION "third_party_notices"
RENAME "NDI_SDK_LICENSE_AGREEMENT.pdf"
)
else()
message(STATUS "NDI license file not found: ${NDI_LICENSE_FILE}")
endif()
if(EXISTS "${NDI_NOTICES_FILE}")
install(FILES "${NDI_NOTICES_FILE}"
DESTINATION "third_party_notices"
RENAME "NDI_RUNTIME_LICENSES.txt"
)
else()
message(STATUS "NDI runtime notices file not found: ${NDI_NOTICES_FILE}")
endif()
if(EXISTS "${DECKLINK_SDK_LICENSE_FILE}")
install(FILES "${DECKLINK_SDK_LICENSE_FILE}"
DESTINATION "third_party_notices"
RENAME "BLACKMAGIC_DECKLINK_SDK_EULA.pdf"
)
else()
message(STATUS "Blackmagic DeckLink SDK license file not found: ${DECKLINK_SDK_LICENSE_FILE}")
endif()
install(FILES "${CMAKE_CURRENT_SOURCE_DIR}/shaders/SHADER_CONTRACT.md"
DESTINATION "."
)
@@ -376,5 +339,3 @@ install(DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/docs/"
DESTINATION "docs"
OPTIONAL
)
source_group(TREE "${APP_DIR}" FILES ${APP_SOURCES})

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": []

227
README.md
View File

@@ -1,27 +1,37 @@
# Video Shader
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.
Native video shader host with an OpenGL render path, pluggable video I/O boundary, DeckLink and NDI backends, 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. 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.
- `runtime/templates/`: tracked shader wrapper templates.
- `runtime/`: ignored generated runtime cache/state output. See `runtime/README.md`.
- `tests/`: focused native tests for pure runtime logic.
- `docs/CURRENT_SYSTEM_ARCHITECTURE.md`: current architecture notes for the cadence/video I/O host.
- `docs/RENDER_CADENCE_GOLDEN_RULES.md`: guardrails for changes that touch render cadence, runtime work, and video I/O.
- `.gitea/workflows/ci.yml`: Gitea Actions CI for Windows native tests and Ubuntu UI build.
Native app internals are grouped by boundary:
- `videoio/`: backend-neutral video I/O contracts, formats, and playout timing.
- `videoio/decklink/`: DeckLink-specific device adapter, callbacks, and SDK bindings.
- `gl/renderer/`: low-level OpenGL resources and extension helpers.
- `gl/pipeline/`: frame pipeline, render passes, video I/O bridge, preview/readback, and screenshots.
- `gl/shader/`: shader compilation, texture/text assets, UBO packing, and shader program ownership.
- `app/`: startup/shutdown orchestration, runtime-content controller boundary, config, preview, telemetry, and HTTP hookup.
- `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/core/`: backend-neutral video IO handoff contracts, mode descriptions, pixel formats, and output scheduling thread.
- `video/decklink/`: DeckLink input/output backend.
- `video/ndi/`: NDI input/output backend.
- `video/playout/`: backend-adjacent playout policy, queues, frame pools, and scheduling helpers.
- `video/legacy/`: older backend pipeline pieces kept separate while the new edge model settles.
The runtime shader stack is plugged into the host through `src/app/RuntimeContentController.h`. The checked-in app wires `ShaderRuntimeContentController` into that slot; a fork can provide another runtime-content controller for a larger render engine while keeping the cadence/video I/O shell.
## Requirements
@@ -29,27 +39,80 @@ Native app internals are grouped by boundary:
- CMake 3.24 or newer.
- Node.js and npm for the control UI.
- 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`.
- Blackmagic DeckLink SDK 16.0 for DeckLink SDK reference files and redistribution notices.
- NDI 6 SDK for NDI input/output builds. CMake expects `Include/`, `Lib/x64/Processing.NDI.Lib.x64.lib`, `Bin/x64/Processing.NDI.Lib.x64.dll`, and the NDI license/notice files.
- Slang 2026.8 Windows x86_64 binary release with `bin/slangc.exe`, `bin/slang-compiler.dll`, `bin/slang-glslang.dll`, and `LICENSE`.
- `msdf-atlas-gen` Windows binary release with `msdf-atlas-gen.exe`, `LICENSE.txt`, `README.md`, and any adjacent runtime DLLs for font atlas generation.
Default expected Slang path:
### Third-party SDK bundle
Org members can initialize the private SDK bundle submodule:
```powershell
git submodule update --init --recursive
```
When present, CMake defaults to this private bundle:
```text
video-io-3rdParty/
Blackmagic DeckLink SDK 16.0/
NDI 6 SDK/
slang-2026.8-windows-x86_64/
msdf-atlas-gen/
```
The parent repository is public, but this bundle is private. External builders need to obtain the SDKs from their vendors and place them in an ignored local `3rdParty/` folder with the same layout, or pass explicit CMake paths.
Fallback local Slang path:
```text
3rdParty/slang-2026.8-windows-x86_64
```
Override example:
Fallback local `msdf-atlas-gen` path:
```text
3rdParty/msdf-atlas-gen
```
Fallback local NDI SDK path:
```text
3rdParty/NDI 6 SDK
```
Fallback local Blackmagic DeckLink SDK path:
```text
3rdParty/Blackmagic DeckLink SDK 16.0
```
Single-root override example:
```powershell
cmake --preset vs2022-x64-debug -DSLANG_ROOT="D:/SDKs/slang-2026.8-windows-x86_64"
cmake --preset vs2022-x64-debug -DTHIRD_PARTY_ROOT="D:/SDKs/video-io-3rdParty"
```
Individual override example:
```powershell
cmake --preset vs2022-x64-debug `
-DSLANG_ROOT="D:/SDKs/slang-2026.8-windows-x86_64" `
-DMSDF_ATLAS_GEN_ROOT="D:/SDKs/msdf-atlas-gen" `
-DNDI_SDK_ROOT="D:/SDKs/NDI 6 SDK" `
-DDECKLINK_SDK_ROOT="D:/SDKs/Blackmagic DeckLink SDK 16.0"
```
At runtime, Slang compilation follows the same root order: `SLANG_ROOT`, `THIRD_PARTY_ROOT`, repo `video-io-3rdParty/`, then repo or packaged `3rdParty/`. The packaged layout uses `3rdParty/slang/bin/slangc.exe`; development bundles can use `slang-2026.8-windows-x86_64/bin/slangc.exe`.
## Build
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:
@@ -65,10 +128,11 @@ The native app serves `ui/dist` when it exists, otherwise it falls back to the s
The control UI provides:
- A searchable shader library for adding layers.
- Compact parameter rows with inline descriptions and OSC copy controls.
- Stack save/recall presets.
- Compact parameter rows with inline descriptions and intended OSC route copy controls.
- Shader-declared custom Web Component control panels with default-control fallback.
- Manual shader reload.
- Screenshot capture from the final output render target.
- Host config editing, save, restart request, and NDI input source discovery.
- Compact video I/O and render cadence status.
## Package
@@ -80,7 +144,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
```
@@ -88,10 +152,12 @@ The package folder will contain:
```text
dist/VideoShader/
LoopThroughWithOpenGLCompositing.exe
RenderCadenceCompositor.exe
Processing.NDI.Lib.x64.dll
config/
shaders/
3rdParty/slang/bin/
3rdParty/msdf-atlas-gen/
ui/dist/
docs/
SHADER_CONTRACT.md
@@ -99,9 +165,9 @@ dist/VideoShader/
third_party_notices/
```
You can run `LoopThroughWithOpenGLCompositing.exe` directly from that folder. In packaged mode, the app resolves `config/`, `shaders/`, `3rdParty/slang/bin/slangc.exe`, `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`, `Processing.NDI.Lib.x64.dll`, `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 does not copy the full Slang release folder.
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, `Processing.NDI.Lib.x64.dll`, and the `third_party_notices/MSDF_ATLAS_GEN_LICENSE.txt`, `third_party_notices/MSDF_ATLAS_GEN_README.md`, `third_party_notices/NDI_SDK_LICENSE_AGREEMENT.pdf`, and `third_party_notices/NDI_RUNTIME_LICENSES.txt` notice files. It does not copy full third-party release folders.
Create a zip for distribution:
@@ -114,7 +180,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:
@@ -130,7 +196,6 @@ Current native test coverage includes:
- Parameter normalization and preset filename safety.
- 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.
- OSC packet parsing.
- Slang validation for every available shader package.
## Runtime Configuration
@@ -141,20 +206,39 @@ Current native test coverage includes:
{
"shaderLibrary": "shaders",
"serverPort": 8080,
"oscBindAddress": "127.0.0.1",
"oscPort": 9000,
"inputVideoFormat": "1080p",
"inputFrameRate": "59.94",
"outputVideoFormat": "1080p",
"outputFrameRate": "59.94",
"oscSmoothing": 0.18,
"runtimeShaderId": "",
"input": {
"backend": "decklink",
"device": "default",
"resolution": "1080p",
"frameRate": "59.94"
},
"output": {
"backend": "decklink",
"device": "default",
"resolution": "1080p",
"frameRate": "59.94",
"pixelFormat": "auto",
"keying": {
"external": false,
"alphaRequired": false
}
},
"autoReload": true,
"maxTemporalHistoryFrames": 12,
"enableExternalKeying": true
"previewEnabled": false,
"previewFps": 59.94
}
```
`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`.
`input.backend` and `output.backend` select the concrete video I/O backend. Today the app supports `decklink`, `ndi`, and `none`; future backends such as Spout or file playback can be added behind the same factory boundary. `device` is a backend-neutral selector placeholder: DeckLink still chooses the first compatible device, NDI input uses it as the source selector, and NDI output uses it as the sender name.
Legacy `videoFormat` and `frameRate` keys are still accepted and apply to both input and output unless the explicit input/output keys are present.
`input.resolution`/`input.frameRate` select the video capture mode. `output.resolution`/`output.frameRate` select the playout mode through a backend-neutral mode description; the current DeckLink backend maps that mode to a `BMDDisplayMode` at the DeckLink boundary. Supported modes still 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`.
The checked-in config uses the nested `input` and `output` objects as the supported shape. When `input.backend` is `ndi`, the host-config editor uses NDI discovery to offer source-name suggestions in the `input.device` field while still allowing manual entry. The control UI presents `output.keying.external` and `output.keying.alphaRequired` as one **Output alpha** control; DeckLink maps that to external keying, while NDI only uses it to request an alpha-carrying system-frame format.
The control UI is available at:
@@ -162,11 +246,43 @@ The control UI is available at:
http://127.0.0.1:<serverPort>
```
## Runtime State And Presets
`/api/state` exposes backend-neutral output telemetry in `videoOutput`. Use `videoOutput.enabled`, `videoOutput.backend`, and `videoOutput.scheduleFailures` for portable status. Backend-specific counters live in `videoOutput.backendMetrics`.
The current layer stack is autosaved to `runtime/runtime_state.json` whenever layers, shader assignments, bypass state, ordering, or parameter values change. On startup, the host reloads that file before compiling the stack, so the last working stack should come back automatically.
### Borderless Window / Fullscreen Output Plan
Manual stack presets are still available from the control UI and are saved under `runtime/stack_presets/*.json`. Presets are useful for named looks, while `runtime_state.json` is the latest working state for the local machine.
The preview window is a best-effort diagnostic view and is allowed to drop or reuse frames without disturbing cadence. A borderless window or fullscreen display output should be implemented as a real `output.backend`, separate from preview, so it can target full configured resolution and frame rate.
Rough plan:
- Add a `window` video output backend behind the existing `IVideoOutput` factory, selectable with `"output": { "backend": "window", ... }`.
- Give it its own device/display selection, fullscreen/borderless mode, window position, vsync policy, and optional monitor refresh validation in `runtime-host.json`.
- Consume `SystemFrameExchange` frames like the other output backends, on an output-owned thread, without blocking the render thread.
- Upload/present frames through a dedicated GL/DX window context or shared-context path, with a bounded mailbox so late window presentation never back-pressures render cadence.
- Report backend-neutral telemetry through `videoOutput` and put display/window-specific details under `videoOutput.backendMetrics`.
- Keep the existing preview window as an operator confidence monitor only, not as the full-quality display output path.
### OCIO / Linear 16-Bit Float Pipeline Plan
The render backend should move toward a true linear-light `RGBA16F` pipeline, with OpenColorIO used for explicit color transforms at the video I/O and display edges. Shader packages should be able to assume a stable working space instead of guessing whether input pixels are display-referred, log, Rec.709, HDR, or already linear.
Rough plan:
- Add OCIO as an optional third-party dependency and package its required runtime DLLs, config files, and license notices with Release builds.
- Extend `runtime-host.json` with color settings for input, working, output, and preview/display transforms, including OCIO config path, input color space, working color space, output color space, view, look, exposure, and gamma.
- Convert captured/input frames into the configured linear working space before shader layers sample them.
- Allocate runtime render targets, temporal history, feedback buffers, and intermediate layer buffers as `RGBA16F` by default, with explicit fallbacks for unsupported hardware.
- Keep shader parameter uploads and texture assets explicit about color intent: data textures stay data, image/video textures can opt into OCIO or known transfer conversion.
- Apply the output transform only at backend edges: DeckLink/NDI/window output, screenshots, and preview each get the correct display/output transform for their target.
- Prebuild OCIO GPU shader/lut resources off the render thread and commit prepared resources to the render thread only when ready, following the render cadence golden rules.
- Add test coverage for config parsing, color-pipeline defaults, fallback behavior, and shader/runtime state reporting.
## 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 optional configured startup shader. The checked-in config leaves `runtimeShaderId` empty, so a fresh host uses the simple fallback renderer until layers are added or a saved stack exists.
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
@@ -189,13 +305,15 @@ 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, queues shader compilation, refreshes shader availability/errors, and keeps the previous working shader stack running if a changed shader fails to compile.
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 also includes a small **OSC** button. Clicking it copies that parameter's OSC route to the clipboard.
Each parameter row still exposes the intended OSC route in the UI. The native host has an OSC service stub that reports the configured bind/port in state, but it does not open a UDP listener or dispatch OSC messages yet.
The control UI also has a **Screenshot** button. It queues a capture of the final output render target and writes a PNG under:
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 reserved screenshot output directory is:
```text
runtime/screenshots/
@@ -203,13 +321,15 @@ 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
@@ -223,7 +343,19 @@ shaders/<id>/
optional-font-or-texture-assets
```
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.
See `SHADER_CONTRACT.md` for the manifest schema, parameter types, texture assets, font/text assets, temporal history support, optional render-pass declarations, optional custom UI metadata, 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.
Shader packages can optionally declare a custom control panel:
```json
"ui": {
"type": "webComponent",
"entry": "ui/controls.js",
"tag": "my-shader-controls"
}
```
The host validates that metadata, exposes it in `/api/state`, and serves the module from `/shader-assets/{shaderId}/...`. Custom controls receive the current layer, declared parameters, `setParameter(id, value)`, and `requestReset()`. They still update the same manifest-declared parameters as the default React controls.
## Generated Files
@@ -233,8 +365,8 @@ Runtime-generated files are intentionally ignored:
- `runtime/shader_cache/active_shader.raw.frag`
- `runtime/shader_cache/active_shader.frag`
- `runtime/runtime_state.json` autosaved latest stack and parameter state.
- `runtime/stack_presets/*.json`
- `runtime/screenshots/*.png` screenshots captured from the final output render target.
- `runtime/stack_presets/*.json` reserved manual preset output; preset routes are not implemented in the native host yet.
- `runtime/screenshots/*.png` reserved screenshot output; screenshot capture is not implemented in the native host yet.
Only `runtime/templates/` and `runtime/README.md` are tracked.
@@ -247,7 +379,11 @@ The Gitea workflow expects two act runners:
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:
- `THIRD_PARTY_ROOT`: path to a bundle containing the expected SDK folder layout.
- `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`.
- `NDI_SDK_ROOT`: path to the NDI SDK folder containing `Include/`, `Lib/x64/`, and `Bin/x64/`.
- `DECKLINK_SDK_ROOT`: path to the Blackmagic DeckLink SDK folder containing `Win/include/DeckLinkAPI.idl`.
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`.
@@ -255,19 +391,20 @@ Example runner paths:
```text
D:\SDKs\slang-2026.8-windows-x86_64
D:\SDKs\msdf-atlas-gen
D:\SDKs\NDI 6 SDK
D:\SDKs\Blackmagic DeckLink SDK 16.0
```
If `SLANG_ROOT` is not set, the workflow falls back to the repo-local default under `3rdParty/`.
If these variables are not set, CMake first looks under the private `video-io-3rdParty/` submodule and then falls back to repo-local defaults under ignored `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
@@ -275,3 +412,7 @@ If `SLANG_ROOT` is not set, the workflow falls back to the repo-local default un
- Mipmapping for shader-declared textures
- Anotate included shaders
- allow 3 vector exposed controls
- add nearest sampling to the extra shader pass
- Add Aja input and output (Assuming i can get a hold of an aja card)
- Add bluefish input and output (Assuming again card acess)
- Endpoint to show OSC paths seperatly instead of a part of the control UI

View File

@@ -1,589 +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"
#include <algorithm>
#include <shellapi.h>
#include <string>
#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);
namespace
{
const int kStatusPanelWidth = 680;
const int kStatusPanelHeight = 92;
const int kStatusPadding = 8;
const int kStatusLabelWidth = 58;
const int kStatusButtonWidth = 86;
const int kStatusRowHeight = 24;
const int kStatusGap = 6;
const UINT kCreateStatusStripMessage = WM_APP + 1;
enum StatusControlId
{
kControlUrlEditId = 2001,
kDocsUrlEditId = 2002,
kOscAddressEditId = 2003,
kOpenControlButtonId = 2004,
kOpenDocsButtonId = 2005
};
struct StatusStripControls
{
HWND panel = NULL;
HWND controlLabel = NULL;
HWND controlUrl = NULL;
HWND openControl = NULL;
HWND docsLabel = NULL;
HWND docsUrl = NULL;
HWND openDocs = NULL;
HWND oscLabel = NULL;
HWND oscAddress = NULL;
};
bool StatusStripCreated(const StatusStripControls& controls)
{
return controls.panel != NULL;
}
HWND CreateStatusChild(HWND parent, const char* className, const char* text, DWORD style, DWORD exStyle, int controlId)
{
return CreateWindowExA(
exStyle,
className,
text,
WS_CHILD | WS_VISIBLE | WS_CLIPSIBLINGS | style,
0,
0,
0,
0,
parent,
reinterpret_cast<HMENU>(static_cast<INT_PTR>(controlId)),
reinterpret_cast<HINSTANCE>(GetWindowLongPtr(parent, GWLP_HINSTANCE)),
NULL);
}
void CreateStatusStrip(HWND hWnd, StatusStripControls& controls)
{
controls.panel = CreateStatusChild(hWnd, "STATIC", "", SS_LEFT, WS_EX_CLIENTEDGE, 0);
controls.controlLabel = CreateStatusChild(hWnd, "STATIC", "Control", SS_LEFT, 0, 0);
controls.controlUrl = CreateStatusChild(hWnd, "EDIT", "", ES_AUTOHSCROLL | ES_READONLY | WS_TABSTOP, WS_EX_CLIENTEDGE, kControlUrlEditId);
controls.openControl = CreateStatusChild(hWnd, "BUTTON", "Open", BS_PUSHBUTTON | WS_TABSTOP, 0, kOpenControlButtonId);
controls.docsLabel = CreateStatusChild(hWnd, "STATIC", "Docs", SS_LEFT, 0, 0);
controls.docsUrl = CreateStatusChild(hWnd, "EDIT", "", ES_AUTOHSCROLL | ES_READONLY | WS_TABSTOP, WS_EX_CLIENTEDGE, kDocsUrlEditId);
controls.openDocs = CreateStatusChild(hWnd, "BUTTON", "Open", BS_PUSHBUTTON | WS_TABSTOP, 0, kOpenDocsButtonId);
controls.oscLabel = CreateStatusChild(hWnd, "STATIC", "OSC", SS_LEFT, 0, 0);
controls.oscAddress = CreateStatusChild(hWnd, "EDIT", "", ES_AUTOHSCROLL | ES_READONLY | WS_TABSTOP, WS_EX_CLIENTEDGE, kOscAddressEditId);
HFONT guiFont = reinterpret_cast<HFONT>(GetStockObject(DEFAULT_GUI_FONT));
HWND children[] = {
controls.controlLabel,
controls.controlUrl,
controls.openControl,
controls.docsLabel,
controls.docsUrl,
controls.openDocs,
controls.oscLabel,
controls.oscAddress
};
for (HWND child : children)
{
if (child)
SendMessage(child, WM_SETFONT, reinterpret_cast<WPARAM>(guiFont), TRUE);
}
SetWindowTextA(controls.controlUrl, "Starting control server...");
SetWindowTextA(controls.docsUrl, "Starting API docs...");
SetWindowTextA(controls.oscAddress, "Starting OSC listener...");
}
void RaiseStatusControls(const StatusStripControls& controls)
{
if (!StatusStripCreated(controls))
return;
SetWindowPos(controls.panel, HWND_BOTTOM, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE);
HWND interactiveControls[] = {
controls.controlLabel,
controls.controlUrl,
controls.openControl,
controls.docsLabel,
controls.docsUrl,
controls.openDocs,
controls.oscLabel,
controls.oscAddress
};
for (HWND control : interactiveControls)
{
if (control)
SetWindowPos(control, HWND_TOP, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE);
}
}
void LayoutStatusStrip(HWND hWnd, const StatusStripControls& controls)
{
RECT clientRect = {};
if (!GetClientRect(hWnd, &clientRect) || !controls.panel)
return;
const int clientWidth = static_cast<int>(clientRect.right - clientRect.left);
const int clientHeight = static_cast<int>(clientRect.bottom - clientRect.top);
const int panelWidth = std::max(280, std::min(kStatusPanelWidth, clientWidth - (kStatusPadding * 2)));
const int panelHeight = kStatusPanelHeight;
const int panelLeft = kStatusPadding;
const int panelTop = std::max(kStatusPadding, clientHeight - panelHeight - kStatusPadding);
MoveWindow(controls.panel, panelLeft, panelTop, panelWidth, panelHeight, TRUE);
const int rowX = panelLeft + kStatusPadding;
const int editX = rowX + kStatusLabelWidth + kStatusGap;
const int buttonX = panelLeft + panelWidth - kStatusPadding - kStatusButtonWidth;
const int editWidth = std::max(80, buttonX - editX - kStatusGap);
const int oscWidth = std::max(80, panelLeft + panelWidth - editX - kStatusPadding);
const int row1 = panelTop + kStatusPadding;
const int row2 = row1 + kStatusRowHeight + kStatusGap;
const int row3 = row2 + kStatusRowHeight + kStatusGap;
MoveWindow(controls.controlLabel, rowX, row1 + 3, kStatusLabelWidth, kStatusRowHeight, TRUE);
MoveWindow(controls.controlUrl, editX, row1, editWidth, kStatusRowHeight, TRUE);
MoveWindow(controls.openControl, buttonX, row1, kStatusButtonWidth, kStatusRowHeight, TRUE);
MoveWindow(controls.docsLabel, rowX, row2 + 3, kStatusLabelWidth, kStatusRowHeight, TRUE);
MoveWindow(controls.docsUrl, editX, row2, editWidth, kStatusRowHeight, TRUE);
MoveWindow(controls.openDocs, buttonX, row2, kStatusButtonWidth, kStatusRowHeight, TRUE);
MoveWindow(controls.oscLabel, rowX, row3 + 3, kStatusLabelWidth, kStatusRowHeight, TRUE);
MoveWindow(controls.oscAddress, editX, row3, oscWidth, kStatusRowHeight, TRUE);
RaiseStatusControls(controls);
}
void UpdateStatusStrip(const StatusStripControls& controls, const OpenGLComposite& composite)
{
if (!StatusStripCreated(controls))
return;
SetWindowTextA(controls.controlUrl, composite.GetControlUrl().c_str());
SetWindowTextA(controls.docsUrl, composite.GetDocsUrl().c_str());
SetWindowTextA(controls.oscAddress, composite.GetOscAddress().c_str());
}
void OpenUrl(const char* url)
{
ShellExecuteA(NULL, "open", url, NULL, NULL, SW_SHOWNORMAL);
}
}
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;
static StatusStripControls sStatusStrip;
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())
{
PostMessage(hWnd, kCreateStatusStripMessage, 0, 0);
break; // success
}
MessageBoxA(NULL, "The OpenGL/DeckLink runtime initialized, but playout failed to start. See the previous DeckLink start message for the failing call.", "Startup failed", MB_OK | MB_ICONERROR);
}
else
{
MessageBoxA(NULL, "The OpenGL/DeckLink runtime failed to initialize. See the previous initialization message for the failing call.", "Startup failed", MB_OK | MB_ICONERROR);
}
// 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 kCreateStatusStripMessage:
if (pOpenGLComposite)
{
if (!StatusStripCreated(sStatusStrip))
CreateStatusStrip(hWnd, sStatusStrip);
UpdateStatusStrip(sStatusStrip, *pOpenGLComposite);
LayoutStatusStrip(hWnd, sStatusStrip);
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_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 (StatusStripCreated(sStatusStrip))
LayoutStatusStrip(hWnd, sStatusStrip);
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(true);
wglMakeCurrent( NULL, NULL );
RaiseStatusControls(sStatusStrip);
}
}
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;
case WM_COMMAND:
switch (LOWORD(wParam))
{
case kOpenControlButtonId:
if (pOpenGLComposite)
{
std::string url = pOpenGLComposite->GetControlUrl();
OpenUrl(url.c_str());
}
break;
case kOpenDocsButtonId:
if (pOpenGLComposite)
{
std::string url = pOpenGLComposite->GetDocsUrl();
OpenUrl(url.c_str());
}
break;
default:
return DefWindowProc(hWnd, message, wParam, lParam);
}
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,245 +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>.;control;gl;gl\pipeline;gl\renderer;gl\shader;videoio;videoio\decklink;%(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>opengl32.lib;Glu32.lib;Windowscodecs.lib;Ole32.lib;%(AdditionalDependencies)</AdditionalDependencies>
<GenerateDebugInformation>true</GenerateDebugInformation>
<SubSystem>Windows</SubSystem>
<TargetMachine>MachineX86</TargetMachine>
</Link>
</ItemDefinitionGroup>
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
<Midl>
<TargetEnvironment>X64</TargetEnvironment>
</Midl>
<ClCompile>
<Optimization>Disabled</Optimization>
<AdditionalIncludeDirectories>.;control;gl;gl\pipeline;gl\renderer;gl\shader;videoio;videoio\decklink;%(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>opengl32.lib;Glu32.lib;Windowscodecs.lib;Ole32.lib;%(AdditionalDependencies)</AdditionalDependencies>
<GenerateDebugInformation>true</GenerateDebugInformation>
<SubSystem>Windows</SubSystem>
<TargetMachine>MachineX64</TargetMachine>
</Link>
</ItemDefinitionGroup>
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">
<ClCompile>
<Optimization>MaxSpeed</Optimization>
<IntrinsicFunctions>true</IntrinsicFunctions>
<AdditionalIncludeDirectories>.;control;gl;gl\pipeline;gl\renderer;gl\shader;videoio;videoio\decklink;%(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>opengl32.lib;Glu32.lib;Windowscodecs.lib;Ole32.lib;%(AdditionalDependencies)</AdditionalDependencies>
<GenerateDebugInformation>true</GenerateDebugInformation>
<SubSystem>Windows</SubSystem>
<OptimizeReferences>true</OptimizeReferences>
<EnableCOMDATFolding>true</EnableCOMDATFolding>
<TargetMachine>MachineX86</TargetMachine>
</Link>
</ItemDefinitionGroup>
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
<Midl>
<TargetEnvironment>X64</TargetEnvironment>
</Midl>
<ClCompile>
<Optimization>MaxSpeed</Optimization>
<IntrinsicFunctions>true</IntrinsicFunctions>
<AdditionalIncludeDirectories>.;control;gl;gl\pipeline;gl\renderer;gl\shader;videoio;videoio\decklink;%(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>opengl32.lib;Glu32.lib;Windowscodecs.lib;Ole32.lib;%(AdditionalDependencies)</AdditionalDependencies>
<GenerateDebugInformation>true</GenerateDebugInformation>
<SubSystem>Windows</SubSystem>
<OptimizeReferences>true</OptimizeReferences>
<EnableCOMDATFolding>true</EnableCOMDATFolding>
<TargetMachine>MachineX64</TargetMachine>
</Link>
</ItemDefinitionGroup>
<ItemGroup>
<ClCompile Include="gl\renderer\GLExtensions.cpp" />
<ClCompile Include="LoopThroughWithOpenGLCompositing.cpp" />
<ClCompile Include="gl\OpenGLComposite.cpp" />
<ClCompile Include="gl\pipeline\OpenGLRenderPass.cpp" />
<ClCompile Include="gl\pipeline\OpenGLRenderPipeline.cpp" />
<ClCompile Include="gl\renderer\OpenGLRenderer.cpp" />
<ClCompile Include="gl\renderer\RenderTargetPool.cpp" />
<ClCompile Include="gl\shader\OpenGLShaderPrograms.cpp" />
<ClCompile Include="gl\pipeline\PngScreenshotWriter.cpp" />
<ClCompile Include="gl\shader\ShaderBuildQueue.cpp" />
<ClCompile Include="gl\pipeline\TemporalHistoryBuffers.cpp" />
<ClCompile Include="gl\pipeline\OpenGLVideoIOBridge.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="videoio\decklink\DeckLinkAPI_i.c" />
<ClCompile Include="control\RuntimeServices.cpp" />
<ClCompile Include="videoio\decklink\DeckLinkSession.cpp" />
<ClCompile Include="videoio\decklink\DeckLinkVideoIOFormat.cpp" />
<ClCompile Include="runtime\RuntimeClock.cpp" />
<ClCompile Include="videoio\VideoIOFormat.cpp" />
<ClCompile Include="videoio\VideoPlayoutScheduler.cpp" />
</ItemGroup>
<ItemGroup>
<ClInclude Include="gl\renderer\GLExtensions.h" />
<ClInclude Include="LoopThroughWithOpenGLCompositing.h" />
<ClInclude Include="gl\OpenGLComposite.h" />
<ClInclude Include="gl\pipeline\OpenGLRenderPass.h" />
<ClInclude Include="gl\pipeline\OpenGLRenderPipeline.h" />
<ClInclude Include="gl\pipeline\RenderPassDescriptor.h" />
<ClInclude Include="gl\renderer\OpenGLRenderer.h" />
<ClInclude Include="gl\renderer\RenderTargetPool.h" />
<ClInclude Include="gl\shader\OpenGLShaderPrograms.h" />
<ClInclude Include="gl\pipeline\PngScreenshotWriter.h" />
<ClInclude Include="gl\shader\ShaderBuildQueue.h" />
<ClInclude Include="gl\pipeline\TemporalHistoryBuffers.h" />
<ClInclude Include="gl\pipeline\OpenGLVideoIOBridge.h" />
<ClInclude Include="resource.h" />
<ClInclude Include="stdafx.h" />
<ClInclude Include="targetver.h" />
<ClInclude Include="control\RuntimeServices.h" />
<ClInclude Include="videoio\decklink\DeckLinkSession.h" />
<ClInclude Include="videoio\decklink\DeckLinkVideoIOFormat.h" />
<ClInclude Include="runtime\RuntimeClock.h" />
<ClInclude Include="videoio\VideoIOFormat.h" />
<ClInclude Include="videoio\VideoIOTypes.h" />
<ClInclude Include="videoio\VideoPlayoutScheduler.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,176 +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="gl\renderer\GLExtensions.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="LoopThroughWithOpenGLCompositing.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="gl\OpenGLComposite.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="gl\pipeline\OpenGLRenderPass.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="gl\pipeline\OpenGLRenderPipeline.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="gl\renderer\OpenGLRenderer.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="gl\renderer\RenderTargetPool.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="gl\shader\OpenGLShaderPrograms.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="gl\pipeline\PngScreenshotWriter.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="gl\shader\ShaderBuildQueue.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="gl\pipeline\TemporalHistoryBuffers.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="gl\pipeline\OpenGLVideoIOBridge.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="stdafx.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="videoio\decklink\DeckLinkAPI_i.c">
<Filter>DeckLink API</Filter>
</ClCompile>
<ClCompile Include="control\RuntimeServices.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="videoio\decklink\DeckLinkSession.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="videoio\decklink\DeckLinkVideoIOFormat.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="runtime\RuntimeClock.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="videoio\VideoIOFormat.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="videoio\VideoPlayoutScheduler.cpp">
<Filter>Source Files</Filter>
</ClCompile>
</ItemGroup>
<ItemGroup>
<ClInclude Include="gl\renderer\GLExtensions.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="LoopThroughWithOpenGLCompositing.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="gl\OpenGLComposite.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="gl\pipeline\OpenGLRenderPass.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="gl\pipeline\OpenGLRenderPipeline.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="gl\pipeline\RenderPassDescriptor.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="gl\renderer\OpenGLRenderer.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="gl\renderer\RenderTargetPool.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="gl\shader\OpenGLShaderPrograms.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="gl\pipeline\PngScreenshotWriter.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="gl\shader\ShaderBuildQueue.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="gl\pipeline\TemporalHistoryBuffers.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="gl\pipeline\OpenGLVideoIOBridge.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="control\RuntimeServices.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="videoio\decklink\DeckLinkSession.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="videoio\decklink\DeckLinkVideoIOFormat.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="runtime\RuntimeClock.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="videoio\VideoIOFormat.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="videoio\VideoIOTypes.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="videoio\VideoPlayoutScheduler.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>

View File

@@ -1,635 +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
{
constexpr DWORD kStateBroadcastIntervalMs = 250;
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()
{
DWORD lastStateBroadcastMs = GetTickCount();
while (mRunning)
{
TryAcceptClient();
const DWORD nowMs = GetTickCount();
if (nowMs - lastStateBroadcastMs >= kStateBroadcastIntervalMs)
{
BroadcastState();
lastStateBroadcastMs = nowMs;
}
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);
}
},
{ "/api/screenshot", [this](const JsonValue&, std::string& error)
{
return mCallbacks.requestScreenshot && mCallbacks.requestScreenshot(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()
{
if (mClients.empty())
return;
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,105 +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;
std::function<bool(std::string&)> requestScreenshot;
};
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,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;
};

View File

@@ -1,51 +0,0 @@
#include "RuntimeControlBridge.h"
#include "ControlServer.h"
#include "OpenGLComposite.h"
#include "OscServer.h"
#include "RuntimeHost.h"
bool StartRuntimeControlServices(
OpenGLComposite& composite,
RuntimeHost& runtimeHost,
ControlServer& controlServer,
OscServer& oscServer,
std::string& error)
{
ControlServer::Callbacks callbacks;
callbacks.getStateJson = [&composite]() { return composite.GetRuntimeStateJson(); };
callbacks.addLayer = [&composite](const std::string& shaderId, std::string& actionError) { return composite.AddLayer(shaderId, actionError); };
callbacks.removeLayer = [&composite](const std::string& layerId, std::string& actionError) { return composite.RemoveLayer(layerId, actionError); };
callbacks.moveLayer = [&composite](const std::string& layerId, int direction, std::string& actionError) { return composite.MoveLayer(layerId, direction, actionError); };
callbacks.moveLayerToIndex = [&composite](const std::string& layerId, std::size_t targetIndex, std::string& actionError) { return composite.MoveLayerToIndex(layerId, targetIndex, actionError); };
callbacks.setLayerBypass = [&composite](const std::string& layerId, bool bypassed, std::string& actionError) { return composite.SetLayerBypass(layerId, bypassed, actionError); };
callbacks.setLayerShader = [&composite](const std::string& layerId, const std::string& shaderId, std::string& actionError) { return composite.SetLayerShader(layerId, shaderId, actionError); };
callbacks.updateLayerParameter = [&composite](const std::string& layerId, const std::string& parameterId, const std::string& valueJson, std::string& actionError) {
return composite.UpdateLayerParameterJson(layerId, parameterId, valueJson, actionError);
};
callbacks.resetLayerParameters = [&composite](const std::string& layerId, std::string& actionError) { return composite.ResetLayerParameters(layerId, actionError); };
callbacks.saveStackPreset = [&composite](const std::string& presetName, std::string& actionError) { return composite.SaveStackPreset(presetName, actionError); };
callbacks.loadStackPreset = [&composite](const std::string& presetName, std::string& actionError) { return composite.LoadStackPreset(presetName, actionError); };
callbacks.requestScreenshot = [&composite](std::string& actionError) { return composite.RequestScreenshot(actionError); };
callbacks.reloadShader = [&composite](std::string& actionError) {
if (!composite.ReloadShader())
{
actionError = "Shader reload failed. See native app status for details.";
return false;
}
return true;
};
if (!controlServer.Start(runtimeHost.GetUiRoot(), runtimeHost.GetDocsRoot(), runtimeHost.GetServerPort(), callbacks, error))
return false;
runtimeHost.SetServerPort(controlServer.GetPort());
OscServer::Callbacks oscCallbacks;
oscCallbacks.updateParameter = [&composite](const std::string& layerKey, const std::string& parameterKey, const std::string& valueJson, std::string& actionError) {
return composite.UpdateLayerParameterByControlKeyJson(layerKey, parameterKey, valueJson, actionError);
};
if (runtimeHost.GetOscPort() > 0 && !oscServer.Start(runtimeHost.GetOscPort(), oscCallbacks, error))
return false;
return true;
}

View File

@@ -1,15 +0,0 @@
#pragma once
#include <string>
class ControlServer;
class OpenGLComposite;
class OscServer;
class RuntimeHost;
bool StartRuntimeControlServices(
OpenGLComposite& composite,
RuntimeHost& runtimeHost,
ControlServer& controlServer,
OscServer& oscServer,
std::string& error);

View File

@@ -1,119 +0,0 @@
#include "RuntimeServices.h"
#include "ControlServer.h"
#include "OscServer.h"
#include "RuntimeControlBridge.h"
#include "RuntimeHost.h"
#include <windows.h>
RuntimeServices::RuntimeServices() :
mControlServer(std::make_unique<ControlServer>()),
mOscServer(std::make_unique<OscServer>()),
mPollRunning(false),
mRegistryChanged(false),
mReloadRequested(false),
mPollFailed(false)
{
}
RuntimeServices::~RuntimeServices()
{
Stop();
}
bool RuntimeServices::Start(OpenGLComposite& composite, RuntimeHost& runtimeHost, std::string& error)
{
Stop();
if (!StartRuntimeControlServices(composite, runtimeHost, *mControlServer, *mOscServer, error))
{
Stop();
return false;
}
return true;
}
void RuntimeServices::BeginPolling(RuntimeHost& runtimeHost)
{
StartPolling(runtimeHost);
}
void RuntimeServices::Stop()
{
StopPolling();
if (mOscServer)
mOscServer->Stop();
if (mControlServer)
mControlServer->Stop();
}
void RuntimeServices::BroadcastState()
{
if (mControlServer)
mControlServer->BroadcastState();
}
RuntimePollEvents RuntimeServices::ConsumePollEvents()
{
RuntimePollEvents events;
events.registryChanged = mRegistryChanged.exchange(false);
events.reloadRequested = mReloadRequested.exchange(false);
events.failed = mPollFailed.exchange(false);
if (events.failed)
{
std::lock_guard<std::mutex> lock(mPollErrorMutex);
events.error = mPollError;
}
return events;
}
void RuntimeServices::StartPolling(RuntimeHost& runtimeHost)
{
if (mPollRunning.exchange(true))
return;
mPollThread = std::thread([this, &runtimeHost]() { PollLoop(runtimeHost); });
}
void RuntimeServices::StopPolling()
{
if (!mPollRunning.exchange(false))
return;
if (mPollThread.joinable())
mPollThread.join();
}
void RuntimeServices::PollLoop(RuntimeHost& runtimeHost)
{
while (mPollRunning)
{
bool registryChanged = false;
bool reloadRequested = false;
std::string runtimeError;
if (!runtimeHost.PollFileChanges(registryChanged, reloadRequested, runtimeError))
{
{
std::lock_guard<std::mutex> lock(mPollErrorMutex);
mPollError = runtimeError;
}
mPollFailed = true;
}
else
{
if (registryChanged)
mRegistryChanged = true;
if (reloadRequested)
mReloadRequested = true;
}
for (int i = 0; i < 25 && mPollRunning; ++i)
Sleep(10);
}
}

View File

@@ -1,48 +0,0 @@
#pragma once
#include <atomic>
#include <memory>
#include <mutex>
#include <string>
#include <thread>
class ControlServer;
class OpenGLComposite;
class OscServer;
class RuntimeHost;
struct RuntimePollEvents
{
bool registryChanged = false;
bool reloadRequested = false;
bool failed = false;
std::string error;
};
class RuntimeServices
{
public:
RuntimeServices();
~RuntimeServices();
bool Start(OpenGLComposite& composite, RuntimeHost& runtimeHost, std::string& error);
void BeginPolling(RuntimeHost& runtimeHost);
void Stop();
void BroadcastState();
RuntimePollEvents ConsumePollEvents();
private:
void StartPolling(RuntimeHost& runtimeHost);
void StopPolling();
void PollLoop(RuntimeHost& runtimeHost);
std::unique_ptr<ControlServer> mControlServer;
std::unique_ptr<OscServer> mOscServer;
std::thread mPollThread;
std::atomic<bool> mPollRunning;
std::atomic<bool> mRegistryChanged;
std::atomic<bool> mReloadRequested;
std::atomic<bool> mPollFailed;
std::mutex mPollErrorMutex;
std::string mPollError;
};

View File

@@ -1,519 +0,0 @@
#include "DeckLinkDisplayMode.h"
#include "DeckLinkSession.h"
#include "OpenGLComposite.h"
#include "GLExtensions.h"
#include "GlRenderConstants.h"
#include "OpenGLRenderPass.h"
#include "OpenGLRenderPipeline.h"
#include "OpenGLShaderPrograms.h"
#include "OpenGLVideoIOBridge.h"
#include "PngScreenshotWriter.h"
#include "RuntimeServices.h"
#include "ShaderBuildQueue.h"
#include <algorithm>
#include <chrono>
#include <ctime>
#include <filesystem>
#include <iomanip>
#include <memory>
#include <sstream>
#include <string>
#include <vector>
OpenGLComposite::OpenGLComposite(HWND hWnd, HDC hDC, HGLRC hRC) :
hGLWnd(hWnd), hGLDC(hDC), hGLRC(hRC),
mVideoIO(std::make_unique<DeckLinkSession>()),
mRenderer(std::make_unique<OpenGLRenderer>()),
mUseCommittedLayerStates(false),
mScreenshotRequested(false)
{
InitializeCriticalSection(&pMutex);
mRuntimeHost = std::make_unique<RuntimeHost>();
mRenderPipeline = std::make_unique<OpenGLRenderPipeline>(
*mRenderer,
*mRuntimeHost,
[this]() { renderEffect(); },
[this]() { ProcessScreenshotRequest(); },
[this]() { paintGL(false); });
mVideoIOBridge = std::make_unique<OpenGLVideoIOBridge>(
*mVideoIO,
*mRenderer,
*mRenderPipeline,
*mRuntimeHost,
pMutex,
hGLDC,
hGLRC);
mRenderPass = std::make_unique<OpenGLRenderPass>(*mRenderer);
mShaderPrograms = std::make_unique<OpenGLShaderPrograms>(*mRenderer, *mRuntimeHost);
mShaderBuildQueue = std::make_unique<ShaderBuildQueue>(*mRuntimeHost);
mRuntimeServices = std::make_unique<RuntimeServices>();
}
OpenGLComposite::~OpenGLComposite()
{
if (mRuntimeServices)
mRuntimeServices->Stop();
if (mShaderBuildQueue)
mShaderBuildQueue->Stop();
mVideoIO->ReleaseResources();
mRenderer->DestroyResources();
DeleteCriticalSection(&pMutex);
}
bool OpenGLComposite::InitDeckLink()
{
return InitVideoIO();
}
bool OpenGLComposite::InitVideoIO()
{
VideoFormatSelection videoModes;
std::string initFailureReason;
if (mRuntimeHost && mRuntimeHost->GetRepoRoot().empty())
{
std::string runtimeError;
if (!mRuntimeHost->Initialize(runtimeError))
{
MessageBoxA(NULL, runtimeError.c_str(), "Runtime host failed to initialize", MB_OK);
return false;
}
}
if (mRuntimeHost)
{
if (!ResolveConfiguredVideoFormats(
mRuntimeHost->GetInputVideoFormat(),
mRuntimeHost->GetInputFrameRate(),
mRuntimeHost->GetOutputVideoFormat(),
mRuntimeHost->GetOutputFrameRate(),
videoModes,
initFailureReason))
{
MessageBoxA(NULL, initFailureReason.c_str(), "DeckLink mode configuration error", MB_OK);
return false;
}
}
if (!mVideoIO->DiscoverDevicesAndModes(videoModes, initFailureReason))
{
const char* title = initFailureReason == "Please install the Blackmagic DeckLink drivers to use the features of this application."
? "This application requires the DeckLink drivers installed."
: "DeckLink initialization failed";
MessageBoxA(NULL, initFailureReason.c_str(), title, MB_OK | MB_ICONERROR);
return false;
}
const bool outputAlphaRequired = mRuntimeHost && mRuntimeHost->ExternalKeyingEnabled();
if (!mVideoIO->SelectPreferredFormats(videoModes, outputAlphaRequired, initFailureReason))
goto error;
if (! CheckOpenGLExtensions())
{
initFailureReason = "OpenGL extension checks failed.";
goto error;
}
if (! InitOpenGLState())
{
initFailureReason = "OpenGL state initialization failed.";
goto error;
}
PublishVideoIOStatus(mVideoIO->OutputModelName().empty()
? "DeckLink output device selected."
: ("Selected output device: " + mVideoIO->OutputModelName()));
// Resize window to match output video frame, but scale large formats down by half for viewing.
if (mVideoIO->OutputFrameWidth() < 1920)
resizeWindow(mVideoIO->OutputFrameWidth(), mVideoIO->OutputFrameHeight());
else
resizeWindow(mVideoIO->OutputFrameWidth() / 2, mVideoIO->OutputFrameHeight() / 2);
if (!mVideoIO->ConfigureInput([this](const VideoIOFrame& frame) { mVideoIOBridge->VideoFrameArrived(frame); }, videoModes.input, initFailureReason))
{
goto error;
}
if (!mVideoIO->HasInputDevice() && mRuntimeHost)
{
mRuntimeHost->SetSignalStatus(false, mVideoIO->InputFrameWidth(), mVideoIO->InputFrameHeight(), mVideoIO->InputDisplayModeName());
}
if (!mVideoIO->ConfigureOutput([this](const VideoIOCompletion& completion) { mVideoIOBridge->PlayoutFrameCompleted(completion); }, videoModes.output, mRuntimeHost && mRuntimeHost->ExternalKeyingEnabled(), initFailureReason))
{
goto error;
}
PublishVideoIOStatus(mVideoIO->StatusMessage());
return true;
error:
if (!initFailureReason.empty())
MessageBoxA(NULL, initFailureReason.c_str(), "DeckLink initialization failed", MB_OK | MB_ICONERROR);
mVideoIO->ReleaseResources();
return false;
}
void OpenGLComposite::paintGL(bool force)
{
if (!force)
{
if (IsIconic(hGLWnd))
return;
const unsigned previewFps = mRuntimeHost ? mRuntimeHost->GetPreviewFps() : 30u;
if (previewFps == 0)
return;
const auto now = std::chrono::steady_clock::now();
const auto minimumInterval = std::chrono::microseconds(1000000 / (previewFps == 0 ? 1u : previewFps));
if (mLastPreviewPresentTime != std::chrono::steady_clock::time_point() &&
now - mLastPreviewPresentTime < minimumInterval)
{
return;
}
}
if (!TryEnterCriticalSection(&pMutex))
{
ValidateRect(hGLWnd, NULL);
return;
}
mRenderer->PresentToWindow(hGLDC, mVideoIO->OutputFrameWidth(), mVideoIO->OutputFrameHeight());
mLastPreviewPresentTime = std::chrono::steady_clock::now();
ValidateRect(hGLWnd, NULL);
LeaveCriticalSection(&pMutex);
}
void OpenGLComposite::resizeGL(WORD width, WORD height)
{
// We don't set the project or model matrices here since the window data is copied directly from
// an off-screen FBO in paintGL(). Just save the width and height for use in paintGL().
mRenderer->ResizeView(width, height);
}
void OpenGLComposite::resizeWindow(int width, int height)
{
RECT r;
if (GetWindowRect(hGLWnd, &r))
{
SetWindowPos(hGLWnd, HWND_TOP, r.left, r.top, r.left + width, r.top + height, 0);
}
}
void OpenGLComposite::PublishVideoIOStatus(const std::string& statusMessage)
{
if (!mRuntimeHost)
return;
if (!statusMessage.empty())
mVideoIO->SetStatusMessage(statusMessage);
mRuntimeHost->SetVideoIOStatus(
"decklink",
mVideoIO->OutputModelName(),
mVideoIO->SupportsInternalKeying(),
mVideoIO->SupportsExternalKeying(),
mVideoIO->KeyerInterfaceAvailable(),
mRuntimeHost->ExternalKeyingEnabled(),
mVideoIO->ExternalKeyingActive(),
mVideoIO->StatusMessage());
}
bool OpenGLComposite::InitOpenGLState()
{
if (! ResolveGLExtensions())
return false;
std::string runtimeError;
if (mRuntimeHost->GetRepoRoot().empty() && !mRuntimeHost->Initialize(runtimeError))
{
MessageBoxA(NULL, runtimeError.c_str(), "Runtime host failed to initialize", MB_OK);
return false;
}
if (!mRuntimeServices->Start(*this, *mRuntimeHost, runtimeError))
{
MessageBoxA(NULL, runtimeError.c_str(), "Runtime control services failed to start", MB_OK);
return false;
}
// Prepare the runtime shader program generated from the active shader package.
char compilerErrorMessage[1024];
if (!mShaderPrograms->CompileDecodeShader(sizeof(compilerErrorMessage), compilerErrorMessage))
{
MessageBoxA(NULL, compilerErrorMessage, "OpenGL decode shader failed to load or compile", MB_OK);
return false;
}
if (!mShaderPrograms->CompileOutputPackShader(sizeof(compilerErrorMessage), compilerErrorMessage))
{
MessageBoxA(NULL, compilerErrorMessage, "OpenGL output pack shader failed to load or compile", MB_OK);
return false;
}
if (!mShaderPrograms->CompileLayerPrograms(mVideoIO->InputFrameWidth(), mVideoIO->InputFrameHeight(), sizeof(compilerErrorMessage), compilerErrorMessage))
{
MessageBoxA(NULL, compilerErrorMessage, "OpenGL shader failed to load or compile", MB_OK);
return false;
}
mCachedLayerRenderStates = mShaderPrograms->CommittedLayerStates();
mUseCommittedLayerStates = false;
mShaderPrograms->ResetTemporalHistoryState();
std::string rendererError;
if (!mRenderer->InitializeResources(
mVideoIO->InputFrameWidth(),
mVideoIO->InputFrameHeight(),
mVideoIO->CaptureTextureWidth(),
mVideoIO->OutputFrameWidth(),
mVideoIO->OutputFrameHeight(),
mVideoIO->OutputPackTextureWidth(),
rendererError))
{
MessageBoxA(NULL, rendererError.c_str(), "OpenGL initialization error.", MB_OK);
return false;
}
broadcastRuntimeState();
mRuntimeServices->BeginPolling(*mRuntimeHost);
return true;
}
bool OpenGLComposite::Start()
{
return mVideoIO->Start();
}
bool OpenGLComposite::Stop()
{
if (mRuntimeServices)
mRuntimeServices->Stop();
const bool wasExternalKeyingActive = mVideoIO->ExternalKeyingActive();
mVideoIO->Stop();
if (wasExternalKeyingActive)
PublishVideoIOStatus("External keying has been disabled.");
return true;
}
bool OpenGLComposite::ReloadShader()
{
if (mRuntimeHost)
{
mRuntimeHost->SetCompileStatus(true, "Shader rebuild queued.");
mRuntimeHost->ClearReloadRequest();
}
RequestShaderBuild();
broadcastRuntimeState();
return true;
}
bool OpenGLComposite::RequestScreenshot(std::string& error)
{
(void)error;
mScreenshotRequested.store(true);
return true;
}
void OpenGLComposite::renderEffect()
{
ProcessRuntimePollResults();
const bool hasInputSource = mVideoIO->HasInputSource();
std::vector<RuntimeRenderState> layerStates;
if (mUseCommittedLayerStates)
{
layerStates = mShaderPrograms->CommittedLayerStates();
if (mRuntimeHost)
mRuntimeHost->RefreshDynamicRenderStateFields(layerStates);
}
else if (mRuntimeHost)
{
const unsigned renderWidth = mVideoIO->InputFrameWidth();
const unsigned renderHeight = mVideoIO->InputFrameHeight();
const uint64_t renderStateVersion = mRuntimeHost->GetRenderStateVersion();
const bool renderStateCacheValid =
!mCachedLayerRenderStates.empty() &&
mCachedRenderStateVersion == renderStateVersion &&
mCachedRenderStateWidth == renderWidth &&
mCachedRenderStateHeight == renderHeight;
if (renderStateCacheValid)
{
layerStates = mCachedLayerRenderStates;
mRuntimeHost->RefreshDynamicRenderStateFields(layerStates);
}
else
{
if (mRuntimeHost->TryGetLayerRenderStates(renderWidth, renderHeight, layerStates))
{
mCachedLayerRenderStates = layerStates;
mCachedRenderStateVersion = renderStateVersion;
mCachedRenderStateWidth = renderWidth;
mCachedRenderStateHeight = renderHeight;
}
else
{
layerStates = mCachedLayerRenderStates;
mRuntimeHost->RefreshDynamicRenderStateFields(layerStates);
}
}
}
const unsigned historyCap = mRuntimeHost ? mRuntimeHost->GetMaxTemporalHistoryFrames() : 0;
mRenderPass->Render(
hasInputSource,
layerStates,
mVideoIO->InputFrameWidth(),
mVideoIO->InputFrameHeight(),
mVideoIO->CaptureTextureWidth(),
mVideoIO->InputPixelFormat(),
historyCap,
[this](const RuntimeRenderState& state, LayerProgram::TextBinding& textBinding, std::string& error) {
return mShaderPrograms->UpdateTextBindingTexture(state, textBinding, error);
},
[this](const RuntimeRenderState& state, unsigned availableSourceHistoryLength, unsigned availableTemporalHistoryLength) {
return mShaderPrograms->UpdateGlobalParamsBuffer(state, availableSourceHistoryLength, availableTemporalHistoryLength);
});
}
void OpenGLComposite::ProcessScreenshotRequest()
{
if (!mScreenshotRequested.exchange(false))
return;
const unsigned width = mVideoIO ? mVideoIO->OutputFrameWidth() : 0;
const unsigned height = mVideoIO ? mVideoIO->OutputFrameHeight() : 0;
if (width == 0 || height == 0)
return;
std::vector<unsigned char> bottomUpPixels(static_cast<std::size_t>(width) * height * 4);
std::vector<unsigned char> topDownPixels(bottomUpPixels.size());
glBindFramebuffer(GL_READ_FRAMEBUFFER, mRenderer->OutputFramebuffer());
glReadBuffer(GL_COLOR_ATTACHMENT0);
glPixelStorei(GL_PACK_ALIGNMENT, 1);
glPixelStorei(GL_PACK_ROW_LENGTH, 0);
glReadPixels(0, 0, width, height, GL_RGBA, GL_UNSIGNED_BYTE, bottomUpPixels.data());
glPixelStorei(GL_PACK_ALIGNMENT, 4);
const std::size_t rowBytes = static_cast<std::size_t>(width) * 4;
for (unsigned y = 0; y < height; ++y)
{
const unsigned sourceY = height - 1 - y;
std::copy(
bottomUpPixels.begin() + static_cast<std::ptrdiff_t>(sourceY * rowBytes),
bottomUpPixels.begin() + static_cast<std::ptrdiff_t>((sourceY + 1) * rowBytes),
topDownPixels.begin() + static_cast<std::ptrdiff_t>(y * rowBytes));
}
try
{
const std::filesystem::path outputPath = BuildScreenshotPath();
std::filesystem::create_directories(outputPath.parent_path());
WritePngFileAsync(outputPath, width, height, std::move(topDownPixels));
}
catch (const std::exception& exception)
{
OutputDebugStringA((std::string("Screenshot request failed: ") + exception.what() + "\n").c_str());
}
}
std::filesystem::path OpenGLComposite::BuildScreenshotPath() const
{
const std::filesystem::path root = mRuntimeHost && !mRuntimeHost->GetRuntimeRoot().empty()
? mRuntimeHost->GetRuntimeRoot()
: std::filesystem::current_path();
const auto now = std::chrono::system_clock::now();
const auto milliseconds = std::chrono::duration_cast<std::chrono::milliseconds>(now.time_since_epoch()) % 1000;
const std::time_t nowTime = std::chrono::system_clock::to_time_t(now);
std::tm localTime = {};
localtime_s(&localTime, &nowTime);
std::ostringstream filename;
filename << "video-shader-toys-"
<< std::put_time(&localTime, "%Y%m%d-%H%M%S")
<< "-" << std::setw(3) << std::setfill('0') << milliseconds.count()
<< ".png";
return root / "screenshots" / filename.str();
}
bool OpenGLComposite::ProcessRuntimePollResults()
{
if (!mRuntimeHost || !mRuntimeServices)
return true;
const RuntimePollEvents events = mRuntimeServices->ConsumePollEvents();
if (events.failed)
{
mRuntimeHost->SetCompileStatus(false, events.error);
broadcastRuntimeState();
return false;
}
if (events.registryChanged)
broadcastRuntimeState();
if (!events.reloadRequested)
{
PreparedShaderBuild readyBuild;
if (!mShaderBuildQueue || !mShaderBuildQueue->TryConsumeReadyBuild(readyBuild))
return true;
char compilerErrorMessage[1024] = {};
if (!mShaderPrograms->CommitPreparedLayerPrograms(readyBuild, mVideoIO->InputFrameWidth(), mVideoIO->InputFrameHeight(), sizeof(compilerErrorMessage), compilerErrorMessage))
{
mRuntimeHost->SetCompileStatus(false, compilerErrorMessage);
mUseCommittedLayerStates = true;
broadcastRuntimeState();
return false;
}
mUseCommittedLayerStates = false;
mCachedLayerRenderStates = mShaderPrograms->CommittedLayerStates();
mShaderPrograms->ResetTemporalHistoryState();
broadcastRuntimeState();
return true;
}
mRuntimeHost->SetCompileStatus(true, "Shader rebuild queued.");
RequestShaderBuild();
broadcastRuntimeState();
return true;
}
void OpenGLComposite::RequestShaderBuild()
{
if (!mShaderBuildQueue || !mVideoIO)
return;
mUseCommittedLayerStates = true;
if (mRuntimeHost)
mRuntimeHost->ClearReloadRequest();
mShaderBuildQueue->RequestBuild(mVideoIO->InputFrameWidth(), mVideoIO->InputFrameHeight());
}
void OpenGLComposite::broadcastRuntimeState()
{
if (mRuntimeServices)
mRuntimeServices->BroadcastState();
}
void OpenGLComposite::resetTemporalHistoryState()
{
mShaderPrograms->ResetTemporalHistoryState();
}
bool OpenGLComposite::CheckOpenGLExtensions()
{
return true;
}
////////////////////////////////////////////

View File

@@ -1,108 +0,0 @@
#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 "GLExtensions.h"
#include "OpenGLRenderer.h"
#include "RuntimeHost.h"
#include <functional>
#include <atomic>
#include <filesystem>
#include <map>
#include <memory>
#include <string>
#include <vector>
#include <deque>
#include <chrono>
class VideoIODevice;
class OpenGLVideoIOBridge;
class OpenGLRenderPass;
class OpenGLRenderPipeline;
class OpenGLShaderPrograms;
class RuntimeServices;
class ShaderBuildQueue;
class OpenGLComposite
{
public:
OpenGLComposite(HWND hWnd, HDC hDC, HGLRC hRC);
~OpenGLComposite();
bool InitDeckLink();
bool InitVideoIO();
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);
bool RequestScreenshot(std::string& error);
unsigned short GetControlServerPort() const;
unsigned short GetOscPort() const;
std::string GetControlUrl() const;
std::string GetDocsUrl() const;
std::string GetOscAddress() const;
void resizeGL(WORD width, WORD height);
void paintGL(bool force = false);
private:
void resizeWindow(int width, int height);
bool CheckOpenGLExtensions();
void PublishVideoIOStatus(const std::string& statusMessage);
using LayerProgram = OpenGLRenderer::LayerProgram;
HWND hGLWnd;
HDC hGLDC;
HGLRC hGLRC;
CRITICAL_SECTION pMutex;
std::unique_ptr<VideoIODevice> mVideoIO;
std::unique_ptr<OpenGLRenderer> mRenderer;
std::unique_ptr<RuntimeHost> mRuntimeHost;
std::unique_ptr<OpenGLVideoIOBridge> mVideoIOBridge;
std::unique_ptr<OpenGLRenderPass> mRenderPass;
std::unique_ptr<OpenGLRenderPipeline> mRenderPipeline;
std::unique_ptr<OpenGLShaderPrograms> mShaderPrograms;
std::unique_ptr<ShaderBuildQueue> mShaderBuildQueue;
std::unique_ptr<RuntimeServices> mRuntimeServices;
std::vector<RuntimeRenderState> mCachedLayerRenderStates;
uint64_t mCachedRenderStateVersion = 0;
unsigned mCachedRenderStateWidth = 0;
unsigned mCachedRenderStateHeight = 0;
std::atomic<bool> mUseCommittedLayerStates;
std::atomic<bool> mScreenshotRequested;
std::chrono::steady_clock::time_point mLastPreviewPresentTime;
bool InitOpenGLState();
void renderEffect();
bool ProcessRuntimePollResults();
void RequestShaderBuild();
void ProcessScreenshotRequest();
std::filesystem::path BuildScreenshotPath() const;
void broadcastRuntimeState();
void resetTemporalHistoryState();
};
#endif // __OPENGL_COMPOSITE_H__

View File

@@ -1,145 +0,0 @@
#include "OpenGLComposite.h"
std::string OpenGLComposite::GetRuntimeStateJson() const
{
return mRuntimeHost ? mRuntimeHost->BuildStateJson() : "{}";
}
unsigned short OpenGLComposite::GetControlServerPort() const
{
return mRuntimeHost ? mRuntimeHost->GetServerPort() : 0;
}
unsigned short OpenGLComposite::GetOscPort() const
{
return mRuntimeHost ? mRuntimeHost->GetOscPort() : 0;
}
std::string OpenGLComposite::GetControlUrl() const
{
return "http://127.0.0.1:" + std::to_string(GetControlServerPort()) + "/";
}
std::string OpenGLComposite::GetDocsUrl() const
{
return "http://127.0.0.1:" + std::to_string(GetControlServerPort()) + "/docs";
}
std::string OpenGLComposite::GetOscAddress() const
{
return "udp://127.0.0.1:" + std::to_string(GetOscPort()) + " /VideoShaderToys/{Layer}/{Parameter}";
}
bool OpenGLComposite::AddLayer(const std::string& shaderId, std::string& error)
{
if (!mRuntimeHost->AddLayer(shaderId, error))
return false;
ReloadShader();
broadcastRuntimeState();
return true;
}
bool OpenGLComposite::RemoveLayer(const std::string& layerId, std::string& error)
{
if (!mRuntimeHost->RemoveLayer(layerId, error))
return false;
ReloadShader();
broadcastRuntimeState();
return true;
}
bool OpenGLComposite::MoveLayer(const std::string& layerId, int direction, std::string& error)
{
if (!mRuntimeHost->MoveLayer(layerId, direction, error))
return false;
ReloadShader();
broadcastRuntimeState();
return true;
}
bool OpenGLComposite::MoveLayerToIndex(const std::string& layerId, std::size_t targetIndex, std::string& error)
{
if (!mRuntimeHost->MoveLayerToIndex(layerId, targetIndex, error))
return false;
ReloadShader();
broadcastRuntimeState();
return true;
}
bool OpenGLComposite::SetLayerBypass(const std::string& layerId, bool bypassed, std::string& error)
{
if (!mRuntimeHost->SetLayerBypass(layerId, bypassed, error))
return false;
ReloadShader();
broadcastRuntimeState();
return true;
}
bool OpenGLComposite::SetLayerShader(const std::string& layerId, const std::string& shaderId, std::string& error)
{
if (!mRuntimeHost->SetLayerShader(layerId, shaderId, error))
return false;
ReloadShader();
broadcastRuntimeState();
return true;
}
bool OpenGLComposite::UpdateLayerParameterJson(const std::string& layerId, const std::string& parameterId, const std::string& valueJson, std::string& error)
{
JsonValue parsedValue;
if (!ParseJson(valueJson, parsedValue, error))
return false;
if (!mRuntimeHost->UpdateLayerParameter(layerId, parameterId, parsedValue, error))
return false;
broadcastRuntimeState();
return true;
}
bool OpenGLComposite::UpdateLayerParameterByControlKeyJson(const std::string& layerKey, const std::string& parameterKey, const std::string& valueJson, std::string& error)
{
JsonValue parsedValue;
if (!ParseJson(valueJson, parsedValue, error))
return false;
if (!mRuntimeHost->UpdateLayerParameterByControlKey(layerKey, parameterKey, parsedValue, error))
return false;
broadcastRuntimeState();
return true;
}
bool OpenGLComposite::ResetLayerParameters(const std::string& layerId, std::string& error)
{
if (!mRuntimeHost->ResetLayerParameters(layerId, error))
return false;
broadcastRuntimeState();
return true;
}
bool OpenGLComposite::SaveStackPreset(const std::string& presetName, std::string& error)
{
if (!mRuntimeHost->SaveStackPreset(presetName, error))
return false;
broadcastRuntimeState();
return true;
}
bool OpenGLComposite::LoadStackPreset(const std::string& presetName, std::string& error)
{
if (!mRuntimeHost->LoadStackPreset(presetName, error))
return false;
ReloadShader();
broadcastRuntimeState();
return true;
}

View File

@@ -1,276 +0,0 @@
#include "OpenGLRenderPass.h"
#include "GlRenderConstants.h"
#include <map>
OpenGLRenderPass::OpenGLRenderPass(OpenGLRenderer& renderer) :
mRenderer(renderer)
{
}
void OpenGLRenderPass::Render(
bool hasInputSource,
const std::vector<RuntimeRenderState>& layerStates,
unsigned inputFrameWidth,
unsigned inputFrameHeight,
unsigned captureTextureWidth,
VideoIOPixelFormat inputPixelFormat,
unsigned historyCap,
const TextBindingUpdater& updateTextBinding,
const GlobalParamsUpdater& updateGlobalParams)
{
glDisable(GL_SCISSOR_TEST);
glDisable(GL_BLEND);
glDisable(GL_DEPTH_TEST);
if (hasInputSource)
{
RenderDecodePass(inputFrameWidth, inputFrameHeight, captureTextureWidth, inputPixelFormat);
}
else
{
glBindFramebuffer(GL_FRAMEBUFFER, mRenderer.DecodeFramebuffer());
glViewport(0, 0, inputFrameWidth, inputFrameHeight);
glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT);
}
std::vector<LayerProgram>& layerPrograms = mRenderer.LayerPrograms();
if (layerStates.empty() || layerPrograms.empty())
{
glBindFramebuffer(GL_READ_FRAMEBUFFER, mRenderer.DecodeFramebuffer());
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, mRenderer.CompositeFramebuffer());
glBlitFramebuffer(0, 0, inputFrameWidth, inputFrameHeight, 0, 0, inputFrameWidth, inputFrameHeight, GL_COLOR_BUFFER_BIT, GL_LINEAR);
glBindFramebuffer(GL_FRAMEBUFFER, mRenderer.CompositeFramebuffer());
}
else
{
const std::vector<RenderPassDescriptor>& passes = BuildLayerPassDescriptors(layerStates, layerPrograms);
for (const RenderPassDescriptor& pass : passes)
{
RenderLayerPass(
pass,
inputFrameWidth,
inputFrameHeight,
historyCap,
updateTextBinding,
updateGlobalParams);
}
}
mRenderer.TemporalHistory().PushSourceFramebuffer(mRenderer.DecodeFramebuffer(), inputFrameWidth, inputFrameHeight);
}
void OpenGLRenderPass::RenderDecodePass(unsigned inputFrameWidth, unsigned inputFrameHeight, unsigned captureTextureWidth, VideoIOPixelFormat inputPixelFormat)
{
glBindFramebuffer(GL_FRAMEBUFFER, mRenderer.DecodeFramebuffer());
glViewport(0, 0, inputFrameWidth, inputFrameHeight);
glClear(GL_COLOR_BUFFER_BIT);
glActiveTexture(GL_TEXTURE0 + kPackedVideoTextureUnit);
glBindTexture(GL_TEXTURE_2D, mRenderer.CaptureTexture());
glBindVertexArray(mRenderer.FullscreenVertexArray());
glUseProgram(mRenderer.DecodeProgram());
const GLint packedResolutionLocation = mRenderer.DecodePackedResolutionLocation();
const GLint decodedResolutionLocation = mRenderer.DecodeDecodedResolutionLocation();
const GLint inputPixelFormatLocation = mRenderer.DecodeInputPixelFormatLocation();
if (packedResolutionLocation >= 0)
glUniform2f(packedResolutionLocation, static_cast<float>(captureTextureWidth), static_cast<float>(inputFrameHeight));
if (decodedResolutionLocation >= 0)
glUniform2f(decodedResolutionLocation, static_cast<float>(inputFrameWidth), static_cast<float>(inputFrameHeight));
if (inputPixelFormatLocation >= 0)
glUniform1i(inputPixelFormatLocation, inputPixelFormat == VideoIOPixelFormat::V210 ? 1 : 0);
glDrawArrays(GL_TRIANGLES, 0, 3);
glUseProgram(0);
glBindVertexArray(0);
glBindTexture(GL_TEXTURE_2D, 0);
glActiveTexture(GL_TEXTURE0);
}
std::vector<RenderPassDescriptor> OpenGLRenderPass::BuildLayerPassDescriptors(
const std::vector<RuntimeRenderState>& layerStates,
std::vector<LayerProgram>& layerPrograms) const
{
// Flatten the layer stack into concrete GL passes. A layer may now contain
// several shader passes, but the outer stack still sees one visible output
// per layer.
std::vector<RenderPassDescriptor>& passes = mPassScratch;
passes.clear();
const std::size_t passCount = layerStates.size() < layerPrograms.size() ? layerStates.size() : layerPrograms.size();
std::size_t descriptorCount = 0;
for (std::size_t index = 0; index < passCount; ++index)
descriptorCount += layerPrograms[index].passes.size();
passes.reserve(descriptorCount);
GLuint sourceTexture = mRenderer.DecodedTexture();
GLuint sourceFramebuffer = mRenderer.DecodeFramebuffer();
for (std::size_t index = 0; index < passCount; ++index)
{
const RuntimeRenderState& state = layerStates[index];
LayerProgram& layerProgram = layerPrograms[index];
if (layerProgram.passes.empty())
continue;
// Preserve the original two-target layer ping-pong. Intermediate passes
// inside this layer are routed through pooled temporary targets instead.
const std::size_t remaining = layerStates.size() - index;
const bool writeToMain = (remaining % 2) == 1;
const GLuint layerOutputTexture = writeToMain ? mRenderer.CompositeTexture() : mRenderer.LayerTempTexture();
const GLuint layerOutputFramebuffer = writeToMain ? mRenderer.CompositeFramebuffer() : mRenderer.LayerTempFramebuffer();
const RenderPassOutputTarget layerOutputTarget = writeToMain ? RenderPassOutputTarget::Composite : RenderPassOutputTarget::LayerTemp;
const GLuint layerInputTexture = sourceTexture;
const GLuint layerInputFramebuffer = sourceFramebuffer;
GLuint previousPassTexture = layerInputTexture;
GLuint previousPassFramebuffer = layerInputFramebuffer;
std::map<std::string, std::pair<GLuint, GLuint>> namedOutputs;
std::size_t temporaryTargetIndex = 0;
for (std::size_t passIndex = 0; passIndex < layerProgram.passes.size(); ++passIndex)
{
PassProgram& passProgram = layerProgram.passes[passIndex];
const bool lastPassForLayer = passIndex + 1 == layerProgram.passes.size();
const std::string outputName = passProgram.outputName.empty() ? passProgram.passId : passProgram.outputName;
const bool writesLayerOutput = outputName == "layerOutput" || lastPassForLayer;
GLuint passSourceTexture = previousPassTexture;
GLuint passSourceFramebuffer = previousPassFramebuffer;
if (!passProgram.inputNames.empty())
{
// v1 multipass uses the first declared input as gVideoInput.
// Later inputs are parsed for forward compatibility.
const std::string& inputName = passProgram.inputNames.front();
if (inputName == "layerInput")
{
passSourceTexture = layerInputTexture;
passSourceFramebuffer = layerInputFramebuffer;
}
else if (inputName == "previousPass")
{
passSourceTexture = previousPassTexture;
passSourceFramebuffer = previousPassFramebuffer;
}
else
{
auto namedOutputIt = namedOutputs.find(inputName);
if (namedOutputIt != namedOutputs.end())
{
passSourceTexture = namedOutputIt->second.first;
passSourceFramebuffer = namedOutputIt->second.second;
}
}
}
GLuint passDestinationTexture = layerOutputTexture;
GLuint passDestinationFramebuffer = layerOutputFramebuffer;
RenderPassOutputTarget outputTarget = layerOutputTarget;
if (!writesLayerOutput)
{
// Temporary targets are reserved when the shader stack is
// committed, avoiding texture allocation during playback.
if (temporaryTargetIndex < mRenderer.TemporaryRenderTargetCount())
{
const RenderTarget& temporaryTarget = mRenderer.TemporaryRenderTarget(temporaryTargetIndex);
++temporaryTargetIndex;
passDestinationTexture = temporaryTarget.texture;
passDestinationFramebuffer = temporaryTarget.framebuffer;
outputTarget = RenderPassOutputTarget::Temporary;
}
}
RenderPassDescriptor pass;
pass.kind = RenderPassKind::LayerEffect;
pass.outputTarget = outputTarget;
pass.passIndex = passes.size();
pass.passId = passProgram.passId;
pass.layerId = state.layerId;
pass.shaderId = state.shaderId;
pass.sourceTexture = passSourceTexture;
pass.sourceFramebuffer = passIndex == 0 ? layerInputFramebuffer : passSourceFramebuffer;
pass.destinationTexture = passDestinationTexture;
pass.destinationFramebuffer = passDestinationFramebuffer;
pass.layerProgram = &layerProgram;
pass.passProgram = &passProgram;
pass.layerState = &state;
pass.capturePreLayerHistory = passIndex == 0 && state.temporalHistorySource == TemporalHistorySource::PreLayerInput;
passes.push_back(pass);
// A later pass can reference either the explicit output name or the
// pass id, which keeps small manifests pleasant to write.
namedOutputs[outputName] = std::make_pair(passDestinationTexture, passDestinationFramebuffer);
namedOutputs[passProgram.passId] = std::make_pair(passDestinationTexture, passDestinationFramebuffer);
previousPassTexture = passDestinationTexture;
previousPassFramebuffer = passDestinationFramebuffer;
}
sourceTexture = layerOutputTexture;
sourceFramebuffer = layerOutputFramebuffer;
}
return passes;
}
void OpenGLRenderPass::RenderLayerPass(
const RenderPassDescriptor& pass,
unsigned inputFrameWidth,
unsigned inputFrameHeight,
unsigned historyCap,
const TextBindingUpdater& updateTextBinding,
const GlobalParamsUpdater& updateGlobalParams)
{
if (pass.passProgram == nullptr || pass.layerState == nullptr)
return;
RenderShaderProgram(
pass.sourceTexture,
pass.destinationFramebuffer,
*pass.passProgram,
*pass.layerState,
inputFrameWidth,
inputFrameHeight,
historyCap,
updateTextBinding,
updateGlobalParams);
if (pass.capturePreLayerHistory)
mRenderer.TemporalHistory().PushPreLayerFramebuffer(pass.layerId, pass.sourceFramebuffer, inputFrameWidth, inputFrameHeight);
}
void OpenGLRenderPass::RenderShaderProgram(
GLuint sourceTexture,
GLuint destinationFrameBuffer,
PassProgram& passProgram,
const RuntimeRenderState& state,
unsigned inputFrameWidth,
unsigned inputFrameHeight,
unsigned historyCap,
const TextBindingUpdater& updateTextBinding,
const GlobalParamsUpdater& updateGlobalParams)
{
for (LayerProgram::TextBinding& textBinding : passProgram.textBindings)
{
std::string textError;
if (!updateTextBinding(state, textBinding, textError))
OutputDebugStringA((textError + "\n").c_str());
}
glBindFramebuffer(GL_FRAMEBUFFER, destinationFrameBuffer);
glViewport(0, 0, inputFrameWidth, inputFrameHeight);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
const std::vector<GLuint> sourceHistoryTextures = mRenderer.TemporalHistory().ResolveSourceHistoryTextures(sourceTexture, state.isTemporal ? historyCap : 0);
const std::vector<GLuint> temporalHistoryTextures = mRenderer.TemporalHistory().ResolveTemporalHistoryTextures(state, sourceTexture, state.isTemporal ? historyCap : 0);
const ShaderTextureBindings::RuntimeTextureBindingPlan texturePlan =
mTextureBindings.BuildLayerRuntimeBindingPlan(passProgram, sourceTexture, sourceHistoryTextures, temporalHistoryTextures);
mTextureBindings.BindRuntimeTexturePlan(texturePlan);
glBindVertexArray(mRenderer.FullscreenVertexArray());
glUseProgram(passProgram.program);
// The UBO is shared by every pass in a layer; texture routing is what
// changes from pass to pass.
updateGlobalParams(state, mRenderer.TemporalHistory().SourceAvailableCount(), mRenderer.TemporalHistory().AvailableCountForLayer(state.layerId));
glDrawArrays(GL_TRIANGLES, 0, 3);
glUseProgram(0);
glBindVertexArray(0);
mTextureBindings.UnbindRuntimeTexturePlan(texturePlan);
}

View File

@@ -1,60 +0,0 @@
#pragma once
#include "OpenGLRenderer.h"
#include "RenderPassDescriptor.h"
#include "ShaderTextureBindings.h"
#include "ShaderTypes.h"
#include "VideoIOFormat.h"
#include <functional>
#include <string>
#include <vector>
class OpenGLRenderPass
{
public:
using LayerProgram = OpenGLRenderer::LayerProgram;
using PassProgram = OpenGLRenderer::LayerProgram::PassProgram;
using TextBindingUpdater = std::function<bool(const RuntimeRenderState&, LayerProgram::TextBinding&, std::string&)>;
using GlobalParamsUpdater = std::function<bool(const RuntimeRenderState&, unsigned, unsigned)>;
explicit OpenGLRenderPass(OpenGLRenderer& renderer);
void Render(
bool hasInputSource,
const std::vector<RuntimeRenderState>& layerStates,
unsigned inputFrameWidth,
unsigned inputFrameHeight,
unsigned captureTextureWidth,
VideoIOPixelFormat inputPixelFormat,
unsigned historyCap,
const TextBindingUpdater& updateTextBinding,
const GlobalParamsUpdater& updateGlobalParams);
private:
void RenderDecodePass(unsigned inputFrameWidth, unsigned inputFrameHeight, unsigned captureTextureWidth, VideoIOPixelFormat inputPixelFormat);
std::vector<RenderPassDescriptor> BuildLayerPassDescriptors(
const std::vector<RuntimeRenderState>& layerStates,
std::vector<LayerProgram>& layerPrograms) const;
void RenderLayerPass(
const RenderPassDescriptor& pass,
unsigned inputFrameWidth,
unsigned inputFrameHeight,
unsigned historyCap,
const TextBindingUpdater& updateTextBinding,
const GlobalParamsUpdater& updateGlobalParams);
void RenderShaderProgram(
GLuint sourceTexture,
GLuint destinationFrameBuffer,
PassProgram& passProgram,
const RuntimeRenderState& state,
unsigned inputFrameWidth,
unsigned inputFrameHeight,
unsigned historyCap,
const TextBindingUpdater& updateTextBinding,
const GlobalParamsUpdater& updateGlobalParams);
OpenGLRenderer& mRenderer;
ShaderTextureBindings mTextureBindings;
mutable std::vector<RenderPassDescriptor> mPassScratch;
};

View File

@@ -1,279 +0,0 @@
#include "OpenGLRenderPipeline.h"
#include "OpenGLRenderer.h"
#include "RuntimeHost.h"
#include "VideoIOFormat.h"
#include <cstring>
#include <chrono>
#include <gl/gl.h>
OpenGLRenderPipeline::OpenGLRenderPipeline(
OpenGLRenderer& renderer,
RuntimeHost& runtimeHost,
RenderEffectCallback renderEffect,
OutputReadyCallback outputReady,
PaintCallback paint) :
mRenderer(renderer),
mRuntimeHost(runtimeHost),
mRenderEffect(renderEffect),
mOutputReady(outputReady),
mPaint(paint)
{
}
OpenGLRenderPipeline::~OpenGLRenderPipeline()
{
ResetAsyncReadbackState();
}
bool OpenGLRenderPipeline::RenderFrame(const RenderPipelineFrameContext& context, VideoIOOutputFrame& outputFrame)
{
const VideoIOState& state = context.videoState;
const auto renderStartTime = std::chrono::steady_clock::now();
glBindFramebuffer(GL_FRAMEBUFFER, mRenderer.CompositeFramebuffer());
mRenderEffect();
glBindFramebuffer(GL_READ_FRAMEBUFFER, mRenderer.CompositeFramebuffer());
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, mRenderer.OutputFramebuffer());
glBlitFramebuffer(0, 0, state.inputFrameSize.width, state.inputFrameSize.height, 0, 0, state.outputFrameSize.width, state.outputFrameSize.height, GL_COLOR_BUFFER_BIT, GL_LINEAR);
glBindFramebuffer(GL_FRAMEBUFFER, mRenderer.OutputFramebuffer());
if (mOutputReady)
mOutputReady();
if (state.outputPixelFormat == VideoIOPixelFormat::V210 || state.outputPixelFormat == VideoIOPixelFormat::Yuva10)
PackOutputFor10Bit(state);
glFlush();
const auto renderEndTime = std::chrono::steady_clock::now();
const double renderMilliseconds = std::chrono::duration_cast<std::chrono::duration<double, std::milli>>(renderEndTime - renderStartTime).count();
mRuntimeHost.TrySetPerformanceStats(state.frameBudgetMilliseconds, renderMilliseconds);
mRuntimeHost.TryAdvanceFrame();
ReadOutputFrame(state, outputFrame);
if (mPaint)
mPaint();
return true;
}
void OpenGLRenderPipeline::PackOutputFor10Bit(const VideoIOState& state)
{
glBindFramebuffer(GL_FRAMEBUFFER, mRenderer.OutputPackFramebuffer());
glViewport(0, 0, state.outputPackTextureWidth, state.outputFrameSize.height);
glDisable(GL_SCISSOR_TEST);
glDisable(GL_BLEND);
glDisable(GL_DEPTH_TEST);
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, mRenderer.OutputTexture());
glBindVertexArray(mRenderer.FullscreenVertexArray());
glUseProgram(mRenderer.OutputPackProgram());
const GLint outputResolutionLocation = mRenderer.OutputPackResolutionLocation();
const GLint activeWordsLocation = mRenderer.OutputPackActiveWordsLocation();
const GLint packFormatLocation = mRenderer.OutputPackFormatLocation();
if (outputResolutionLocation >= 0)
glUniform2f(outputResolutionLocation, static_cast<float>(state.outputFrameSize.width), static_cast<float>(state.outputFrameSize.height));
if (activeWordsLocation >= 0)
glUniform1f(activeWordsLocation, static_cast<float>(ActiveV210WordsForWidth(state.outputFrameSize.width)));
if (packFormatLocation >= 0)
glUniform1i(packFormatLocation, state.outputPixelFormat == VideoIOPixelFormat::Yuva10 ? 2 : 1);
glDrawArrays(GL_TRIANGLES, 0, 3);
glUseProgram(0);
glBindVertexArray(0);
glBindTexture(GL_TEXTURE_2D, 0);
}
bool OpenGLRenderPipeline::EnsureAsyncReadbackBuffers(std::size_t requiredBytes)
{
if (requiredBytes == 0)
return false;
if (mAsyncReadbackBytes == requiredBytes && mAsyncReadbackSlots[0].pixelPackBuffer != 0)
return true;
ResetAsyncReadbackState();
mAsyncReadbackBytes = requiredBytes;
for (AsyncReadbackSlot& slot : mAsyncReadbackSlots)
{
glGenBuffers(1, &slot.pixelPackBuffer);
glBindBuffer(GL_PIXEL_PACK_BUFFER, slot.pixelPackBuffer);
glBufferData(GL_PIXEL_PACK_BUFFER, static_cast<GLsizeiptr>(requiredBytes), nullptr, GL_STREAM_READ);
slot.sizeBytes = requiredBytes;
slot.inFlight = false;
}
glBindBuffer(GL_PIXEL_PACK_BUFFER, 0);
mAsyncReadbackWriteIndex = 0;
mAsyncReadbackReadIndex = 0;
return true;
}
void OpenGLRenderPipeline::ResetAsyncReadbackState()
{
FlushAsyncReadbackPipeline();
for (AsyncReadbackSlot& slot : mAsyncReadbackSlots)
slot.sizeBytes = 0;
if (mAsyncReadbackSlots[0].pixelPackBuffer != 0)
{
for (AsyncReadbackSlot& slot : mAsyncReadbackSlots)
{
if (slot.pixelPackBuffer != 0)
{
glDeleteBuffers(1, &slot.pixelPackBuffer);
slot.pixelPackBuffer = 0;
}
}
}
mAsyncReadbackWriteIndex = 0;
mAsyncReadbackReadIndex = 0;
mAsyncReadbackBytes = 0;
}
void OpenGLRenderPipeline::FlushAsyncReadbackPipeline()
{
for (AsyncReadbackSlot& slot : mAsyncReadbackSlots)
{
if (slot.fence != nullptr)
{
glDeleteSync(slot.fence);
slot.fence = nullptr;
}
slot.inFlight = false;
}
mAsyncReadbackWriteIndex = 0;
mAsyncReadbackReadIndex = 0;
}
void OpenGLRenderPipeline::QueueAsyncReadback(const VideoIOState& state)
{
const bool usePackedOutput = state.outputPixelFormat == VideoIOPixelFormat::V210 || state.outputPixelFormat == VideoIOPixelFormat::Yuva10;
const std::size_t requiredBytes = static_cast<std::size_t>(state.outputFrameRowBytes) * state.outputFrameSize.height;
const GLenum format = usePackedOutput ? GL_RGBA : GL_BGRA;
const GLenum type = usePackedOutput ? GL_UNSIGNED_BYTE : GL_UNSIGNED_INT_8_8_8_8_REV;
const GLuint framebuffer = usePackedOutput ? mRenderer.OutputPackFramebuffer() : mRenderer.OutputFramebuffer();
const GLsizei readWidth = static_cast<GLsizei>(usePackedOutput ? state.outputPackTextureWidth : state.outputFrameSize.width);
const GLsizei readHeight = static_cast<GLsizei>(state.outputFrameSize.height);
if (requiredBytes == 0)
return;
if (mAsyncReadbackBytes != requiredBytes
|| mAsyncReadbackFormat != format
|| mAsyncReadbackType != type
|| mAsyncReadbackFramebuffer != framebuffer)
{
mAsyncReadbackFormat = format;
mAsyncReadbackType = type;
mAsyncReadbackFramebuffer = framebuffer;
if (!EnsureAsyncReadbackBuffers(requiredBytes))
return;
}
AsyncReadbackSlot& slot = mAsyncReadbackSlots[mAsyncReadbackWriteIndex];
if (slot.fence != nullptr)
{
glDeleteSync(slot.fence);
slot.fence = nullptr;
}
glPixelStorei(GL_PACK_ALIGNMENT, 4);
glPixelStorei(GL_PACK_ROW_LENGTH, 0);
glBindFramebuffer(GL_READ_FRAMEBUFFER, framebuffer);
glBindBuffer(GL_PIXEL_PACK_BUFFER, slot.pixelPackBuffer);
glBufferData(GL_PIXEL_PACK_BUFFER, static_cast<GLsizeiptr>(requiredBytes), nullptr, GL_STREAM_READ);
glReadPixels(0, 0, readWidth, readHeight, format, type, nullptr);
slot.fence = glFenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE, 0);
slot.inFlight = slot.fence != nullptr;
glBindBuffer(GL_PIXEL_PACK_BUFFER, 0);
mAsyncReadbackWriteIndex = (mAsyncReadbackWriteIndex + 1) % mAsyncReadbackSlots.size();
}
bool OpenGLRenderPipeline::TryConsumeAsyncReadback(VideoIOOutputFrame& outputFrame, GLuint64 timeoutNanoseconds)
{
if (mAsyncReadbackBytes == 0 || outputFrame.bytes == nullptr)
return false;
AsyncReadbackSlot& slot = mAsyncReadbackSlots[mAsyncReadbackReadIndex];
if (!slot.inFlight || slot.fence == nullptr || slot.pixelPackBuffer == 0)
return false;
const GLenum waitFlags = timeoutNanoseconds > 0 ? GL_SYNC_FLUSH_COMMANDS_BIT : 0;
const GLenum waitResult = glClientWaitSync(slot.fence, waitFlags, timeoutNanoseconds);
if (waitResult != GL_ALREADY_SIGNALED && waitResult != GL_CONDITION_SATISFIED)
return false;
glDeleteSync(slot.fence);
slot.fence = nullptr;
glBindBuffer(GL_PIXEL_PACK_BUFFER, slot.pixelPackBuffer);
void* mappedBytes = glMapBuffer(GL_PIXEL_PACK_BUFFER, GL_READ_ONLY);
if (mappedBytes == nullptr)
{
glBindBuffer(GL_PIXEL_PACK_BUFFER, 0);
slot.inFlight = false;
mAsyncReadbackReadIndex = (mAsyncReadbackReadIndex + 1) % mAsyncReadbackSlots.size();
return false;
}
std::memcpy(outputFrame.bytes, mappedBytes, slot.sizeBytes);
glUnmapBuffer(GL_PIXEL_PACK_BUFFER);
glBindBuffer(GL_PIXEL_PACK_BUFFER, 0);
slot.inFlight = false;
mAsyncReadbackReadIndex = (mAsyncReadbackReadIndex + 1) % mAsyncReadbackSlots.size();
CacheOutputFrame(outputFrame);
return true;
}
void OpenGLRenderPipeline::CacheOutputFrame(const VideoIOOutputFrame& outputFrame)
{
if (outputFrame.bytes == nullptr || outputFrame.height == 0 || outputFrame.rowBytes <= 0)
return;
const std::size_t byteCount = static_cast<std::size_t>(outputFrame.rowBytes) * outputFrame.height;
mCachedOutputFrame.resize(byteCount);
std::memcpy(mCachedOutputFrame.data(), outputFrame.bytes, byteCount);
}
void OpenGLRenderPipeline::ReadOutputFrameSynchronously(const VideoIOState& state, void* destinationBytes)
{
const bool usePackedOutput = state.outputPixelFormat == VideoIOPixelFormat::V210 || state.outputPixelFormat == VideoIOPixelFormat::Yuva10;
glPixelStorei(GL_PACK_ALIGNMENT, 4);
glPixelStorei(GL_PACK_ROW_LENGTH, 0);
if (usePackedOutput)
{
glBindFramebuffer(GL_READ_FRAMEBUFFER, mRenderer.OutputPackFramebuffer());
glReadPixels(0, 0, state.outputPackTextureWidth, state.outputFrameSize.height, GL_RGBA, GL_UNSIGNED_BYTE, destinationBytes);
}
else
{
glBindFramebuffer(GL_READ_FRAMEBUFFER, mRenderer.OutputFramebuffer());
glReadPixels(0, 0, state.outputFrameSize.width, state.outputFrameSize.height, GL_BGRA, GL_UNSIGNED_INT_8_8_8_8_REV, destinationBytes);
}
}
void OpenGLRenderPipeline::ReadOutputFrame(const VideoIOState& state, VideoIOOutputFrame& outputFrame)
{
if (TryConsumeAsyncReadback(outputFrame, 500000))
{
QueueAsyncReadback(state);
return;
}
// If async readback misses the playout deadline, prefer a fresh synchronous
// frame over reusing stale cached output, then restart the async pipeline.
if (outputFrame.bytes != nullptr)
{
ReadOutputFrameSynchronously(state, outputFrame.bytes);
CacheOutputFrame(outputFrame);
}
FlushAsyncReadbackPipeline();
QueueAsyncReadback(state);
}

View File

@@ -1,68 +0,0 @@
#pragma once
#include "GLExtensions.h"
#include "VideoIOTypes.h"
#include <array>
#include <functional>
#include <vector>
class OpenGLRenderer;
class RuntimeHost;
struct RenderPipelineFrameContext
{
VideoIOState videoState;
VideoIOCompletion completion;
};
class OpenGLRenderPipeline
{
public:
using RenderEffectCallback = std::function<void()>;
using OutputReadyCallback = std::function<void()>;
using PaintCallback = std::function<void()>;
OpenGLRenderPipeline(
OpenGLRenderer& renderer,
RuntimeHost& runtimeHost,
RenderEffectCallback renderEffect,
OutputReadyCallback outputReady,
PaintCallback paint);
~OpenGLRenderPipeline();
bool RenderFrame(const RenderPipelineFrameContext& context, VideoIOOutputFrame& outputFrame);
private:
struct AsyncReadbackSlot
{
GLuint pixelPackBuffer = 0;
GLsync fence = nullptr;
std::size_t sizeBytes = 0;
bool inFlight = false;
};
bool EnsureAsyncReadbackBuffers(std::size_t requiredBytes);
void ResetAsyncReadbackState();
void FlushAsyncReadbackPipeline();
void QueueAsyncReadback(const VideoIOState& state);
bool TryConsumeAsyncReadback(VideoIOOutputFrame& outputFrame, GLuint64 timeoutNanoseconds);
void CacheOutputFrame(const VideoIOOutputFrame& outputFrame);
void ReadOutputFrameSynchronously(const VideoIOState& state, void* destinationBytes);
void PackOutputFor10Bit(const VideoIOState& state);
void ReadOutputFrame(const VideoIOState& state, VideoIOOutputFrame& outputFrame);
OpenGLRenderer& mRenderer;
RuntimeHost& mRuntimeHost;
RenderEffectCallback mRenderEffect;
OutputReadyCallback mOutputReady;
PaintCallback mPaint;
std::array<AsyncReadbackSlot, 3> mAsyncReadbackSlots;
std::size_t mAsyncReadbackWriteIndex = 0;
std::size_t mAsyncReadbackReadIndex = 0;
std::size_t mAsyncReadbackBytes = 0;
GLenum mAsyncReadbackFormat = GL_BGRA;
GLenum mAsyncReadbackType = GL_UNSIGNED_INT_8_8_8_8_REV;
GLuint mAsyncReadbackFramebuffer = 0;
std::vector<unsigned char> mCachedOutputFrame;
};

View File

@@ -1,124 +0,0 @@
#include "OpenGLVideoIOBridge.h"
#include "OpenGLRenderer.h"
#include "RuntimeHost.h"
#include <chrono>
#include <gl/gl.h>
OpenGLVideoIOBridge::OpenGLVideoIOBridge(
VideoIODevice& videoIO,
OpenGLRenderer& renderer,
OpenGLRenderPipeline& renderPipeline,
RuntimeHost& runtimeHost,
CRITICAL_SECTION& mutex,
HDC hdc,
HGLRC hglrc) :
mVideoIO(videoIO),
mRenderer(renderer),
mRenderPipeline(renderPipeline),
mRuntimeHost(runtimeHost),
mMutex(mutex),
mHdc(hdc),
mHglrc(hglrc)
{
}
void OpenGLVideoIOBridge::RecordFramePacing(VideoIOCompletionResult completionResult)
{
const auto now = std::chrono::steady_clock::now();
if (mLastPlayoutCompletionTime != std::chrono::steady_clock::time_point())
{
mCompletionIntervalMilliseconds = std::chrono::duration_cast<std::chrono::duration<double, std::milli>>(now - mLastPlayoutCompletionTime).count();
if (mSmoothedCompletionIntervalMilliseconds <= 0.0)
mSmoothedCompletionIntervalMilliseconds = mCompletionIntervalMilliseconds;
else
mSmoothedCompletionIntervalMilliseconds = mSmoothedCompletionIntervalMilliseconds * 0.9 + mCompletionIntervalMilliseconds * 0.1;
if (mCompletionIntervalMilliseconds > mMaxCompletionIntervalMilliseconds)
mMaxCompletionIntervalMilliseconds = mCompletionIntervalMilliseconds;
}
mLastPlayoutCompletionTime = now;
if (completionResult == VideoIOCompletionResult::DisplayedLate)
++mLateFrameCount;
else if (completionResult == VideoIOCompletionResult::Dropped)
++mDroppedFrameCount;
else if (completionResult == VideoIOCompletionResult::Flushed)
++mFlushedFrameCount;
mRuntimeHost.TrySetFramePacingStats(
mCompletionIntervalMilliseconds,
mSmoothedCompletionIntervalMilliseconds,
mMaxCompletionIntervalMilliseconds,
mLateFrameCount,
mDroppedFrameCount,
mFlushedFrameCount);
}
void OpenGLVideoIOBridge::VideoFrameArrived(const VideoIOFrame& inputFrame)
{
const VideoIOState& state = mVideoIO.State();
mRuntimeHost.TrySetSignalStatus(!inputFrame.hasNoInputSource, state.inputFrameSize.width, state.inputFrameSize.height, state.inputDisplayModeName);
if (inputFrame.hasNoInputSource || inputFrame.bytes == nullptr)
return; // don't transfer texture when there's no input
const long textureSize = inputFrame.rowBytes * static_cast<long>(inputFrame.height);
EnterCriticalSection(&mMutex);
wglMakeCurrent(mHdc, mHglrc); // make OpenGL context current in this thread
glPixelStorei(GL_UNPACK_ALIGNMENT, 4);
glPixelStorei(GL_UNPACK_ROW_LENGTH, 0);
glBindBuffer(GL_PIXEL_UNPACK_BUFFER, mRenderer.TextureUploadBuffer());
glBufferData(GL_PIXEL_UNPACK_BUFFER, textureSize, inputFrame.bytes, GL_DYNAMIC_DRAW);
glBindTexture(GL_TEXTURE_2D, mRenderer.CaptureTexture());
// NULL for last arg indicates use current GL_PIXEL_UNPACK_BUFFER target as texture data.
if (inputFrame.pixelFormat == VideoIOPixelFormat::V210)
glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, state.captureTextureWidth, state.inputFrameSize.height, GL_RGBA, GL_UNSIGNED_BYTE, NULL);
else
glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, state.captureTextureWidth, state.inputFrameSize.height, GL_BGRA, GL_UNSIGNED_INT_8_8_8_8_REV, NULL);
glBindTexture(GL_TEXTURE_2D, 0);
glBindBuffer(GL_PIXEL_UNPACK_BUFFER, 0);
wglMakeCurrent(NULL, NULL);
LeaveCriticalSection(&mMutex);
}
void OpenGLVideoIOBridge::PlayoutFrameCompleted(const VideoIOCompletion& completion)
{
RecordFramePacing(completion.result);
EnterCriticalSection(&mMutex);
VideoIOOutputFrame outputFrame;
if (!mVideoIO.BeginOutputFrame(outputFrame))
{
LeaveCriticalSection(&mMutex);
return;
}
const VideoIOState& state = mVideoIO.State();
RenderPipelineFrameContext frameContext;
frameContext.videoState = state;
frameContext.completion = completion;
// make GL context current in this thread
wglMakeCurrent(mHdc, mHglrc);
mRenderPipeline.RenderFrame(frameContext, outputFrame);
mVideoIO.EndOutputFrame(outputFrame);
mVideoIO.AccountForCompletionResult(completion.result);
// Schedule the next frame for playout
mVideoIO.ScheduleOutputFrame(outputFrame);
wglMakeCurrent(NULL, NULL);
LeaveCriticalSection(&mMutex);
}

View File

@@ -1,44 +0,0 @@
#pragma once
#include "OpenGLRenderPipeline.h"
#include <windows.h>
#include <chrono>
#include <cstdint>
class RuntimeHost;
class OpenGLVideoIOBridge
{
public:
OpenGLVideoIOBridge(
VideoIODevice& videoIO,
OpenGLRenderer& renderer,
OpenGLRenderPipeline& renderPipeline,
RuntimeHost& runtimeHost,
CRITICAL_SECTION& mutex,
HDC hdc,
HGLRC hglrc);
void VideoFrameArrived(const VideoIOFrame& inputFrame);
void PlayoutFrameCompleted(const VideoIOCompletion& completion);
private:
void RecordFramePacing(VideoIOCompletionResult completionResult);
VideoIODevice& mVideoIO;
OpenGLRenderer& mRenderer;
OpenGLRenderPipeline& mRenderPipeline;
RuntimeHost& mRuntimeHost;
CRITICAL_SECTION& mMutex;
HDC mHdc;
HGLRC mHglrc;
std::chrono::steady_clock::time_point mLastPlayoutCompletionTime;
double mCompletionIntervalMilliseconds = 0.0;
double mSmoothedCompletionIntervalMilliseconds = 0.0;
double mMaxCompletionIntervalMilliseconds = 0.0;
uint64_t mLateFrameCount = 0;
uint64_t mDroppedFrameCount = 0;
uint64_t mFlushedFrameCount = 0;
};

View File

@@ -1,137 +0,0 @@
#include "PngScreenshotWriter.h"
#include <windows.h>
#include <wincodec.h>
#include <atlbase.h>
#include <sstream>
#include <thread>
namespace
{
std::string HResultToString(HRESULT hr)
{
std::ostringstream stream;
stream << "HRESULT 0x" << std::hex << static_cast<unsigned long>(hr);
return stream.str();
}
bool WritePngFile(
const std::filesystem::path& outputPath,
unsigned width,
unsigned height,
const std::vector<unsigned char>& bgraPixels,
std::string& error)
{
if (width == 0 || height == 0 || bgraPixels.size() < static_cast<std::size_t>(width) * height * 4)
{
error = "Invalid screenshot dimensions or pixel buffer.";
return false;
}
HRESULT initializeResult = CoInitializeEx(nullptr, COINIT_MULTITHREADED);
const bool shouldUninitialize = SUCCEEDED(initializeResult);
if (FAILED(initializeResult) && initializeResult != RPC_E_CHANGED_MODE)
{
error = "CoInitializeEx failed: " + HResultToString(initializeResult);
return false;
}
CComPtr<IWICImagingFactory> factory;
HRESULT result = CoCreateInstance(
CLSID_WICImagingFactory,
nullptr,
CLSCTX_INPROC_SERVER,
IID_PPV_ARGS(&factory));
if (FAILED(result))
{
error = "Could not create WIC imaging factory: " + HResultToString(result);
if (shouldUninitialize)
CoUninitialize();
return false;
}
CComPtr<IWICStream> stream;
result = factory->CreateStream(&stream);
if (SUCCEEDED(result))
result = stream->InitializeFromFilename(outputPath.wstring().c_str(), GENERIC_WRITE);
if (FAILED(result))
{
error = "Could not open screenshot output file: " + HResultToString(result);
if (shouldUninitialize)
CoUninitialize();
return false;
}
CComPtr<IWICBitmapEncoder> encoder;
result = factory->CreateEncoder(GUID_ContainerFormatPng, nullptr, &encoder);
if (SUCCEEDED(result))
result = encoder->Initialize(stream, WICBitmapEncoderNoCache);
if (FAILED(result))
{
error = "Could not initialize PNG encoder: " + HResultToString(result);
if (shouldUninitialize)
CoUninitialize();
return false;
}
CComPtr<IWICBitmapFrameEncode> frame;
CComPtr<IPropertyBag2> propertyBag;
result = encoder->CreateNewFrame(&frame, &propertyBag);
if (SUCCEEDED(result))
result = frame->Initialize(propertyBag);
if (SUCCEEDED(result))
result = frame->SetSize(width, height);
WICPixelFormatGUID pixelFormat = GUID_WICPixelFormat32bppBGRA;
if (SUCCEEDED(result))
result = frame->SetPixelFormat(&pixelFormat);
if (SUCCEEDED(result) && pixelFormat != GUID_WICPixelFormat32bppBGRA)
{
error = "PNG encoder did not accept BGRA pixel format.";
result = E_FAIL;
}
const UINT stride = width * 4;
const UINT imageSize = stride * height;
if (SUCCEEDED(result))
result = frame->WritePixels(height, stride, imageSize, const_cast<BYTE*>(bgraPixels.data()));
if (SUCCEEDED(result))
result = frame->Commit();
if (SUCCEEDED(result))
result = encoder->Commit();
if (shouldUninitialize)
CoUninitialize();
if (FAILED(result))
{
error = "Could not write screenshot PNG: " + HResultToString(result);
std::error_code ignored;
std::filesystem::remove(outputPath, ignored);
return false;
}
return true;
}
}
void WritePngFileAsync(
const std::filesystem::path& outputPath,
unsigned width,
unsigned height,
std::vector<unsigned char> rgbaPixels)
{
std::thread(
[outputPath, width, height, pixels = std::move(rgbaPixels)]() mutable
{
for (std::size_t index = 0; index + 3 < pixels.size(); index += 4)
std::swap(pixels[index], pixels[index + 2]);
std::string error;
if (!WritePngFile(outputPath, width, height, pixels, error))
OutputDebugStringA(("Screenshot write failed: " + error + "\n").c_str());
else
OutputDebugStringA(("Screenshot written: " + outputPath.string() + "\n").c_str());
}).detach();
}

View File

@@ -1,12 +0,0 @@
#pragma once
#include <filesystem>
#include <string>
#include <vector>
void WritePngFileAsync(
const std::filesystem::path& outputPath,
unsigned width,
unsigned height,
std::vector<unsigned char> rgbaPixels);

View File

@@ -1,39 +0,0 @@
#pragma once
#include "OpenGLRenderer.h"
#include "ShaderTypes.h"
#include <gl/gl.h>
#include <cstddef>
#include <string>
enum class RenderPassKind
{
LayerEffect
};
enum class RenderPassOutputTarget
{
Temporary,
LayerTemp,
Composite
};
struct RenderPassDescriptor
{
RenderPassKind kind = RenderPassKind::LayerEffect;
RenderPassOutputTarget outputTarget = RenderPassOutputTarget::Composite;
std::size_t passIndex = 0;
std::string passId;
std::string layerId;
std::string shaderId;
GLuint sourceTexture = 0;
GLuint sourceFramebuffer = 0;
GLuint destinationTexture = 0;
GLuint destinationFramebuffer = 0;
OpenGLRenderer::LayerProgram* layerProgram = nullptr;
OpenGLRenderer::LayerProgram::PassProgram* passProgram = nullptr;
const RuntimeRenderState* layerState = nullptr;
bool capturePreLayerHistory = false;
};

View File

@@ -1,260 +0,0 @@
#include "TemporalHistoryBuffers.h"
#include "GlRenderConstants.h"
#include "ShaderTypes.h"
#include <algorithm>
#include <sstream>
#include <set>
bool TemporalHistoryBuffers::ValidateTextureUnitBudget(const std::vector<RuntimeRenderState>& layerStates, unsigned historyCap, std::string& error) const
{
unsigned requiredUnits = kSourceHistoryTextureUnitBase;
for (const RuntimeRenderState& state : layerStates)
{
unsigned textTextureCount = 0;
for (const ShaderParameterDefinition& definition : state.parameterDefinitions)
{
if (definition.type == ShaderParameterType::Text)
++textTextureCount;
}
const unsigned totalShaderTextures = static_cast<unsigned>(state.textureAssets.size()) + textTextureCount;
const unsigned layerRequiredUnits = kSourceHistoryTextureUnitBase + (state.isTemporal ? historyCap + historyCap : 0u) + totalShaderTextures;
if (layerRequiredUnits > requiredUnits)
requiredUnits = layerRequiredUnits;
}
GLint maxTextureUnits = 0;
glGetIntegerv(GL_MAX_TEXTURE_IMAGE_UNITS, &maxTextureUnits);
const unsigned availableUnits = maxTextureUnits > 0 ? static_cast<unsigned>(maxTextureUnits) : 0u;
if (requiredUnits > availableUnits)
{
std::ostringstream message;
message << "The current history and shader texture asset configuration requires " << requiredUnits
<< " fragment texture units, but only " << maxTextureUnits << " are available.";
error = message.str();
return false;
}
return true;
}
bool TemporalHistoryBuffers::EnsureResources(const std::vector<RuntimeRenderState>& layerStates, unsigned historyCap, unsigned frameWidth, unsigned frameHeight, std::string& error)
{
const bool sourceHistoryNeeded = std::any_of(layerStates.begin(), layerStates.end(),
[](const RuntimeRenderState& state) { return state.isTemporal && state.effectiveTemporalHistoryLength > 0; });
const unsigned sourceHistoryLength = sourceHistoryNeeded ? historyCap : 0;
if (sourceHistoryRing.effectiveLength != sourceHistoryLength)
{
if (!CreateRing(sourceHistoryRing, sourceHistoryLength, TemporalHistorySource::Source, frameWidth, frameHeight, error))
return false;
mNeedsReset = true;
}
std::set<std::string> requiredPreLayerIds;
for (const RuntimeRenderState& state : layerStates)
{
if (!state.isTemporal || state.temporalHistorySource != TemporalHistorySource::PreLayerInput)
continue;
requiredPreLayerIds.insert(state.layerId);
auto historyIt = preLayerHistoryByLayerId.find(state.layerId);
if (historyIt == preLayerHistoryByLayerId.end() || historyIt->second.effectiveLength != state.effectiveTemporalHistoryLength)
{
Ring replacement;
if (!CreateRing(replacement, state.effectiveTemporalHistoryLength, TemporalHistorySource::PreLayerInput, frameWidth, frameHeight, error))
return false;
preLayerHistoryByLayerId[state.layerId] = std::move(replacement);
mNeedsReset = true;
}
}
for (auto it = preLayerHistoryByLayerId.begin(); it != preLayerHistoryByLayerId.end();)
{
if (requiredPreLayerIds.find(it->first) == requiredPreLayerIds.end())
{
DestroyRing(it->second);
it = preLayerHistoryByLayerId.erase(it);
mNeedsReset = true;
}
else
{
++it;
}
}
if (mNeedsReset)
ResetState();
return true;
}
bool TemporalHistoryBuffers::CreateRing(Ring& ring, unsigned effectiveLength, TemporalHistorySource historySource, unsigned frameWidth, unsigned frameHeight, std::string& error)
{
DestroyRing(ring);
ring.effectiveLength = effectiveLength;
ring.historySource = historySource;
if (effectiveLength == 0)
return true;
ring.slots.resize(effectiveLength);
for (Slot& slot : ring.slots)
{
glGenTextures(1, &slot.texture);
glBindTexture(GL_TEXTURE_2D, slot.texture);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA16F, frameWidth, frameHeight, 0, GL_RGBA, GL_FLOAT, NULL);
glGenFramebuffers(1, &slot.framebuffer);
glBindFramebuffer(GL_FRAMEBUFFER, slot.framebuffer);
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, slot.texture, 0);
const GLenum framebufferStatus = glCheckFramebufferStatus(GL_FRAMEBUFFER);
if (framebufferStatus != GL_FRAMEBUFFER_COMPLETE)
{
error = "Failed to initialize a temporal history framebuffer.";
glBindFramebuffer(GL_FRAMEBUFFER, 0);
glBindTexture(GL_TEXTURE_2D, 0);
DestroyRing(ring);
return false;
}
}
glBindFramebuffer(GL_FRAMEBUFFER, 0);
glBindTexture(GL_TEXTURE_2D, 0);
return true;
}
void TemporalHistoryBuffers::DestroyRing(Ring& ring)
{
for (Slot& slot : ring.slots)
{
if (slot.framebuffer != 0)
glDeleteFramebuffers(1, &slot.framebuffer);
if (slot.texture != 0)
glDeleteTextures(1, &slot.texture);
slot.framebuffer = 0;
slot.texture = 0;
}
ring.slots.clear();
ring.nextWriteIndex = 0;
ring.filledCount = 0;
ring.effectiveLength = 0;
ring.historySource = TemporalHistorySource::None;
}
void TemporalHistoryBuffers::DestroyResources()
{
DestroyRing(sourceHistoryRing);
for (auto& historyEntry : preLayerHistoryByLayerId)
DestroyRing(historyEntry.second);
preLayerHistoryByLayerId.clear();
mNeedsReset = true;
}
void TemporalHistoryBuffers::ResetState()
{
sourceHistoryRing.nextWriteIndex = 0;
sourceHistoryRing.filledCount = 0;
for (auto& historyEntry : preLayerHistoryByLayerId)
{
historyEntry.second.nextWriteIndex = 0;
historyEntry.second.filledCount = 0;
}
mNeedsReset = false;
}
void TemporalHistoryBuffers::PushFramebuffer(GLuint sourceFramebuffer, Ring& ring, unsigned frameWidth, unsigned frameHeight)
{
if (ring.effectiveLength == 0 || ring.slots.empty())
return;
Slot& targetSlot = ring.slots[ring.nextWriteIndex];
glBindFramebuffer(GL_READ_FRAMEBUFFER, sourceFramebuffer);
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, targetSlot.framebuffer);
glBlitFramebuffer(0, 0, frameWidth, frameHeight, 0, 0, frameWidth, frameHeight, GL_COLOR_BUFFER_BIT, GL_LINEAR);
ring.nextWriteIndex = (ring.nextWriteIndex + 1) % ring.slots.size();
ring.filledCount = std::min<std::size_t>(ring.filledCount + 1, ring.slots.size());
}
void TemporalHistoryBuffers::PushSourceFramebuffer(GLuint sourceFramebuffer, unsigned frameWidth, unsigned frameHeight)
{
PushFramebuffer(sourceFramebuffer, sourceHistoryRing, frameWidth, frameHeight);
}
void TemporalHistoryBuffers::PushPreLayerFramebuffer(const std::string& layerId, GLuint sourceFramebuffer, unsigned frameWidth, unsigned frameHeight)
{
auto historyIt = preLayerHistoryByLayerId.find(layerId);
if (historyIt != preLayerHistoryByLayerId.end())
PushFramebuffer(sourceFramebuffer, historyIt->second, frameWidth, frameHeight);
}
void TemporalHistoryBuffers::BindSamplers(const RuntimeRenderState& state, GLuint currentSourceTexture, unsigned historyCap)
{
for (unsigned index = 0; index < historyCap; ++index)
{
glActiveTexture(GL_TEXTURE0 + kSourceHistoryTextureUnitBase + index);
glBindTexture(GL_TEXTURE_2D, ResolveTexture(sourceHistoryRing, currentSourceTexture, index));
}
const GLuint temporalBase = kSourceHistoryTextureUnitBase + historyCap;
const Ring* temporalRing = nullptr;
auto it = preLayerHistoryByLayerId.find(state.layerId);
if (it != preLayerHistoryByLayerId.end())
temporalRing = &it->second;
for (unsigned index = 0; index < historyCap; ++index)
{
glActiveTexture(GL_TEXTURE0 + temporalBase + index);
glBindTexture(GL_TEXTURE_2D, temporalRing ? ResolveTexture(*temporalRing, currentSourceTexture, index) : currentSourceTexture);
}
glActiveTexture(GL_TEXTURE0);
}
std::vector<GLuint> TemporalHistoryBuffers::ResolveSourceHistoryTextures(GLuint fallbackTexture, unsigned historyCap) const
{
std::vector<GLuint> textures;
textures.reserve(historyCap);
for (unsigned index = 0; index < historyCap; ++index)
textures.push_back(ResolveTexture(sourceHistoryRing, fallbackTexture, index));
return textures;
}
std::vector<GLuint> TemporalHistoryBuffers::ResolveTemporalHistoryTextures(const RuntimeRenderState& state, GLuint fallbackTexture, unsigned historyCap) const
{
const Ring* temporalRing = nullptr;
auto it = preLayerHistoryByLayerId.find(state.layerId);
if (it != preLayerHistoryByLayerId.end())
temporalRing = &it->second;
std::vector<GLuint> textures;
textures.reserve(historyCap);
for (unsigned index = 0; index < historyCap; ++index)
textures.push_back(temporalRing ? ResolveTexture(*temporalRing, fallbackTexture, index) : fallbackTexture);
return textures;
}
GLuint TemporalHistoryBuffers::ResolveTexture(const Ring& ring, GLuint fallbackTexture, std::size_t framesAgo) const
{
if (ring.filledCount == 0 || ring.slots.empty())
return fallbackTexture;
const std::size_t clampedOffset = std::min<std::size_t>(framesAgo, ring.filledCount - 1);
const std::size_t newestIndex = (ring.nextWriteIndex + ring.slots.size() - 1) % ring.slots.size();
const std::size_t slotIndex = (newestIndex + ring.slots.size() - clampedOffset) % ring.slots.size();
return ring.slots[slotIndex].texture != 0 ? ring.slots[slotIndex].texture : fallbackTexture;
}
unsigned TemporalHistoryBuffers::SourceAvailableCount() const
{
return static_cast<unsigned>(sourceHistoryRing.filledCount);
}
unsigned TemporalHistoryBuffers::AvailableCountForLayer(const std::string& layerId) const
{
auto it = preLayerHistoryByLayerId.find(layerId);
if (it == preLayerHistoryByLayerId.end())
return 0;
return static_cast<unsigned>(it->second.filledCount);
}

View File

@@ -1,53 +0,0 @@
#pragma once
#include "GLExtensions.h"
#include "ShaderTypes.h"
#include <windows.h>
#include <gl/gl.h>
#include <map>
#include <string>
#include <vector>
struct RuntimeRenderState;
class TemporalHistoryBuffers
{
public:
struct Slot
{
GLuint texture = 0;
GLuint framebuffer = 0;
};
struct Ring
{
std::vector<Slot> slots;
std::size_t nextWriteIndex = 0;
std::size_t filledCount = 0;
unsigned effectiveLength = 0;
TemporalHistorySource historySource = TemporalHistorySource::None;
};
bool ValidateTextureUnitBudget(const std::vector<RuntimeRenderState>& layerStates, unsigned historyCap, std::string& error) const;
bool EnsureResources(const std::vector<RuntimeRenderState>& layerStates, unsigned historyCap, unsigned frameWidth, unsigned frameHeight, std::string& error);
bool CreateRing(Ring& ring, unsigned effectiveLength, TemporalHistorySource historySource, unsigned frameWidth, unsigned frameHeight, std::string& error);
void DestroyRing(Ring& ring);
void DestroyResources();
void ResetState();
void PushFramebuffer(GLuint sourceFramebuffer, Ring& ring, unsigned frameWidth, unsigned frameHeight);
void PushSourceFramebuffer(GLuint sourceFramebuffer, unsigned frameWidth, unsigned frameHeight);
void PushPreLayerFramebuffer(const std::string& layerId, GLuint sourceFramebuffer, unsigned frameWidth, unsigned frameHeight);
void BindSamplers(const RuntimeRenderState& state, GLuint currentSourceTexture, unsigned historyCap);
std::vector<GLuint> ResolveSourceHistoryTextures(GLuint fallbackTexture, unsigned historyCap) const;
std::vector<GLuint> ResolveTemporalHistoryTextures(const RuntimeRenderState& state, GLuint fallbackTexture, unsigned historyCap) const;
GLuint ResolveTexture(const Ring& ring, GLuint fallbackTexture, std::size_t framesAgo) const;
unsigned SourceAvailableCount() const;
unsigned AvailableCountForLayer(const std::string& layerId) const;
private:
Ring sourceHistoryRing;
std::map<std::string, Ring> preLayerHistoryByLayerId;
bool mNeedsReset = true;
};

View File

@@ -1,9 +0,0 @@
#pragma once
#include <gl/gl.h>
constexpr GLuint kDecodedVideoTextureUnit = 1;
constexpr GLuint kSourceHistoryTextureUnitBase = 2;
constexpr GLuint kPackedVideoTextureUnit = 2;
constexpr GLuint kGlobalParamsBindingPoint = 0;
constexpr unsigned kPrerollFrameCount = 8;

View File

@@ -1,57 +0,0 @@
#pragma once
#include <gl/gl.h>
class ScopedGlShader
{
public:
explicit ScopedGlShader(GLuint shader = 0) : mShader(shader) {}
~ScopedGlShader() { reset(); }
ScopedGlShader(const ScopedGlShader&) = delete;
ScopedGlShader& operator=(const ScopedGlShader&) = delete;
GLuint get() const { return mShader; }
GLuint release()
{
GLuint shader = mShader;
mShader = 0;
return shader;
}
void reset(GLuint shader = 0)
{
if (mShader != 0)
glDeleteShader(mShader);
mShader = shader;
}
private:
GLuint mShader;
};
class ScopedGlProgram
{
public:
explicit ScopedGlProgram(GLuint program = 0) : mProgram(program) {}
~ScopedGlProgram() { reset(); }
ScopedGlProgram(const ScopedGlProgram&) = delete;
ScopedGlProgram& operator=(const ScopedGlProgram&) = delete;
GLuint get() const { return mProgram; }
GLuint release()
{
GLuint program = mProgram;
mProgram = 0;
return program;
}
void reset(GLuint program = 0)
{
if (mProgram != 0)
glDeleteProgram(mProgram);
mProgram = program;
}
private:
GLuint mProgram;
};

View File

@@ -1,265 +0,0 @@
#include "OpenGLRenderer.h"
#include "GlRenderConstants.h"
namespace
{
void ConfigureByteFrameTexture(unsigned width, unsigned height)
{
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, NULL);
}
}
bool OpenGLRenderer::InitializeResources(unsigned inputFrameWidth, unsigned inputFrameHeight, unsigned captureTextureWidth, unsigned outputFrameWidth, unsigned outputFrameHeight, unsigned outputPackTextureWidth, std::string& error)
{
glClearColor(0.0f, 0.0f, 0.0f, 0.5f);
glDisable(GL_DEPTH_TEST);
glGenBuffers(1, &mTextureUploadBuffer);
glGenTextures(1, &mCaptureTexture);
glBindTexture(GL_TEXTURE_2D, mCaptureTexture);
ConfigureByteFrameTexture(captureTextureWidth, inputFrameHeight);
glBindTexture(GL_TEXTURE_2D, 0);
glGenRenderbuffers(1, &mIdColorBuf);
glGenRenderbuffers(1, &mIdDepthBuf);
glGenVertexArrays(1, &mFullscreenVAO);
glGenBuffers(1, &mGlobalParamsUBO);
if (!mRenderTargets.Create(RenderTargetId::Decoded, inputFrameWidth, inputFrameHeight, GL_RGBA16F, GL_RGBA, GL_FLOAT, "decode", error))
return false;
if (!mRenderTargets.Create(RenderTargetId::LayerTemp, inputFrameWidth, inputFrameHeight, GL_RGBA16F, GL_RGBA, GL_FLOAT, "layer", error))
return false;
if (!mRenderTargets.Create(RenderTargetId::Composite, inputFrameWidth, inputFrameHeight, GL_RGBA16F, GL_RGBA, GL_FLOAT, "composite", error))
return false;
glBindFramebuffer(GL_FRAMEBUFFER, CompositeFramebuffer());
glBindRenderbuffer(GL_RENDERBUFFER, mIdDepthBuf);
glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH_COMPONENT24, inputFrameWidth, inputFrameHeight);
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT_EXT, GL_RENDERBUFFER, mIdDepthBuf);
if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE)
{
error = "Cannot initialize framebuffer.";
return false;
}
if (!mRenderTargets.Create(RenderTargetId::Output, outputFrameWidth, outputFrameHeight, GL_RGBA16F, GL_RGBA, GL_FLOAT, "output", error))
return false;
if (!mRenderTargets.Create(RenderTargetId::OutputPack, outputPackTextureWidth, outputFrameHeight, GL_RGBA8, GL_RGBA, GL_UNSIGNED_BYTE, "output pack", error))
return false;
glBindTexture(GL_TEXTURE_2D, 0);
glBindRenderbuffer(GL_RENDERBUFFER, 0);
glBindFramebuffer(GL_FRAMEBUFFER, 0);
glBindVertexArray(mFullscreenVAO);
glBindVertexArray(0);
glBindBuffer(GL_UNIFORM_BUFFER, mGlobalParamsUBO);
glBufferData(GL_UNIFORM_BUFFER, 1024, NULL, GL_DYNAMIC_DRAW);
glBindBufferBase(GL_UNIFORM_BUFFER, kGlobalParamsBindingPoint, mGlobalParamsUBO);
glBindBuffer(GL_UNIFORM_BUFFER, 0);
return true;
}
void OpenGLRenderer::SetDecodeShaderProgram(GLuint program, GLuint vertexShader, GLuint fragmentShader)
{
mDecodeProgram = program;
mDecodeVertexShader = vertexShader;
mDecodeFragmentShader = fragmentShader;
mDecodePackedResolutionLocation = program != 0 ? glGetUniformLocation(program, "uPackedVideoResolution") : -1;
mDecodeDecodedResolutionLocation = program != 0 ? glGetUniformLocation(program, "uDecodedVideoResolution") : -1;
mDecodeInputPixelFormatLocation = program != 0 ? glGetUniformLocation(program, "uInputPixelFormat") : -1;
}
void OpenGLRenderer::SetOutputPackShaderProgram(GLuint program, GLuint vertexShader, GLuint fragmentShader)
{
mOutputPackProgram = program;
mOutputPackVertexShader = vertexShader;
mOutputPackFragmentShader = fragmentShader;
mOutputPackResolutionLocation = program != 0 ? glGetUniformLocation(program, "uOutputVideoResolution") : -1;
mOutputPackActiveWordsLocation = program != 0 ? glGetUniformLocation(program, "uActiveV210Words") : -1;
mOutputPackFormatLocation = program != 0 ? glGetUniformLocation(program, "uOutputPackFormat") : -1;
}
bool OpenGLRenderer::ReserveTemporaryRenderTargets(std::size_t count, unsigned width, unsigned height, std::string& error)
{
return mRenderTargets.ReserveTemporaryTargets(count, width, height, GL_RGBA16F, GL_RGBA, GL_FLOAT, error);
}
void OpenGLRenderer::ResizeView(int width, int height)
{
mViewWidth = width;
mViewHeight = height;
}
void OpenGLRenderer::PresentToWindow(HDC hdc, unsigned outputFrameWidth, unsigned outputFrameHeight)
{
int destWidth = mViewWidth;
int destHeight = mViewHeight;
int destX = 0;
int destY = 0;
if (outputFrameWidth > 0 && outputFrameHeight > 0 && mViewWidth > 0 && mViewHeight > 0)
{
const double frameAspect = static_cast<double>(outputFrameWidth) / static_cast<double>(outputFrameHeight);
const double viewAspect = static_cast<double>(mViewWidth) / static_cast<double>(mViewHeight);
if (viewAspect > frameAspect)
{
destHeight = mViewHeight;
destWidth = static_cast<int>(destHeight * frameAspect + 0.5);
destX = (mViewWidth - destWidth) / 2;
}
else
{
destWidth = mViewWidth;
destHeight = static_cast<int>(destWidth / frameAspect + 0.5);
destY = (mViewHeight - destHeight) / 2;
}
}
glBindFramebuffer(GL_READ_FRAMEBUFFER, OutputFramebuffer());
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0);
glDisable(GL_SCISSOR_TEST);
glViewport(0, 0, mViewWidth, mViewHeight);
glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT);
glBlitFramebuffer(0, 0, outputFrameWidth, outputFrameHeight, destX, destY, destX + destWidth, destY + destHeight, GL_COLOR_BUFFER_BIT, GL_LINEAR);
SwapBuffers(hdc);
}
void OpenGLRenderer::DestroyResources()
{
if (mFullscreenVAO != 0)
glDeleteVertexArrays(1, &mFullscreenVAO);
if (mGlobalParamsUBO != 0)
glDeleteBuffers(1, &mGlobalParamsUBO);
if (mIdColorBuf != 0)
glDeleteRenderbuffers(1, &mIdColorBuf);
if (mIdDepthBuf != 0)
glDeleteRenderbuffers(1, &mIdDepthBuf);
if (mCaptureTexture != 0)
glDeleteTextures(1, &mCaptureTexture);
if (mTextureUploadBuffer != 0)
glDeleteBuffers(1, &mTextureUploadBuffer);
mRenderTargets.Destroy();
mFullscreenVAO = 0;
mGlobalParamsUBO = 0;
mIdColorBuf = 0;
mIdDepthBuf = 0;
mCaptureTexture = 0;
mTextureUploadBuffer = 0;
mGlobalParamsUBOSize = 0;
mTemporalHistory.DestroyResources();
DestroyLayerPrograms();
DestroyDecodeShaderProgram();
DestroyOutputPackShaderProgram();
}
void OpenGLRenderer::DestroySingleLayerProgram(LayerProgram& layerProgram)
{
for (LayerProgram::PassProgram& passProgram : layerProgram.passes)
{
for (LayerProgram::TextureBinding& binding : passProgram.textureBindings)
{
if (binding.texture != 0)
{
glDeleteTextures(1, &binding.texture);
binding.texture = 0;
}
}
passProgram.textureBindings.clear();
for (LayerProgram::TextBinding& binding : passProgram.textBindings)
{
if (binding.texture != 0)
{
glDeleteTextures(1, &binding.texture);
binding.texture = 0;
}
}
passProgram.textBindings.clear();
if (passProgram.program != 0)
{
glDeleteProgram(passProgram.program);
passProgram.program = 0;
}
if (passProgram.fragmentShader != 0)
{
glDeleteShader(passProgram.fragmentShader);
passProgram.fragmentShader = 0;
}
if (passProgram.vertexShader != 0)
{
glDeleteShader(passProgram.vertexShader);
passProgram.vertexShader = 0;
}
}
layerProgram.passes.clear();
}
void OpenGLRenderer::DestroyLayerPrograms()
{
for (LayerProgram& layerProgram : mLayerPrograms)
DestroySingleLayerProgram(layerProgram);
mLayerPrograms.clear();
}
void OpenGLRenderer::DestroyDecodeShaderProgram()
{
if (mDecodeProgram != 0)
{
glDeleteProgram(mDecodeProgram);
mDecodeProgram = 0;
}
mDecodePackedResolutionLocation = -1;
mDecodeDecodedResolutionLocation = -1;
mDecodeInputPixelFormatLocation = -1;
if (mDecodeFragmentShader != 0)
{
glDeleteShader(mDecodeFragmentShader);
mDecodeFragmentShader = 0;
}
if (mDecodeVertexShader != 0)
{
glDeleteShader(mDecodeVertexShader);
mDecodeVertexShader = 0;
}
}
void OpenGLRenderer::DestroyOutputPackShaderProgram()
{
if (mOutputPackProgram != 0)
{
glDeleteProgram(mOutputPackProgram);
mOutputPackProgram = 0;
}
mOutputPackResolutionLocation = -1;
mOutputPackActiveWordsLocation = -1;
mOutputPackFormatLocation = -1;
if (mOutputPackFragmentShader != 0)
{
glDeleteShader(mOutputPackFragmentShader);
mOutputPackFragmentShader = 0;
}
if (mOutputPackVertexShader != 0)
{
glDeleteShader(mOutputPackVertexShader);
mOutputPackVertexShader = 0;
}
}

View File

@@ -1,125 +0,0 @@
#pragma once
#include "GLExtensions.h"
#include "RenderTargetPool.h"
#include "ShaderTypes.h"
#include "TemporalHistoryBuffers.h"
#include <windows.h>
#include <filesystem>
#include <gl/gl.h>
#include <string>
#include <vector>
class OpenGLRenderer
{
public:
struct LayerProgram
{
struct TextureBinding
{
std::string samplerName;
std::filesystem::path sourcePath;
GLuint texture = 0;
};
struct TextBinding
{
std::string parameterId;
std::string samplerName;
std::string fontId;
GLuint texture = 0;
std::string renderedText;
unsigned renderedWidth = 0;
unsigned renderedHeight = 0;
};
std::string layerId;
std::string shaderId;
struct PassProgram
{
std::string passId;
std::vector<std::string> inputNames;
std::string outputName;
GLuint shaderTextureBase = 0;
GLuint program = 0;
GLuint vertexShader = 0;
GLuint fragmentShader = 0;
std::vector<TextureBinding> textureBindings;
std::vector<TextBinding> textBindings;
};
std::vector<PassProgram> passes;
};
GLuint CaptureTexture() const { return mCaptureTexture; }
GLuint DecodedTexture() const { return mRenderTargets.Texture(RenderTargetId::Decoded); }
GLuint LayerTempTexture() const { return mRenderTargets.Texture(RenderTargetId::LayerTemp); }
GLuint CompositeTexture() const { return mRenderTargets.Texture(RenderTargetId::Composite); }
GLuint OutputTexture() const { return mRenderTargets.Texture(RenderTargetId::Output); }
GLuint OutputPackTexture() const { return mRenderTargets.Texture(RenderTargetId::OutputPack); }
GLuint TextureUploadBuffer() const { return mTextureUploadBuffer; }
GLuint DecodeFramebuffer() const { return mRenderTargets.Framebuffer(RenderTargetId::Decoded); }
GLuint LayerTempFramebuffer() const { return mRenderTargets.Framebuffer(RenderTargetId::LayerTemp); }
GLuint CompositeFramebuffer() const { return mRenderTargets.Framebuffer(RenderTargetId::Composite); }
GLuint OutputFramebuffer() const { return mRenderTargets.Framebuffer(RenderTargetId::Output); }
GLuint OutputPackFramebuffer() const { return mRenderTargets.Framebuffer(RenderTargetId::OutputPack); }
GLuint FullscreenVertexArray() const { return mFullscreenVAO; }
GLuint GlobalParamsUBO() const { return mGlobalParamsUBO; }
GLuint DecodeProgram() const { return mDecodeProgram; }
GLuint OutputPackProgram() const { return mOutputPackProgram; }
GLint DecodePackedResolutionLocation() const { return mDecodePackedResolutionLocation; }
GLint DecodeDecodedResolutionLocation() const { return mDecodeDecodedResolutionLocation; }
GLint DecodeInputPixelFormatLocation() const { return mDecodeInputPixelFormatLocation; }
GLint OutputPackResolutionLocation() const { return mOutputPackResolutionLocation; }
GLint OutputPackActiveWordsLocation() const { return mOutputPackActiveWordsLocation; }
GLint OutputPackFormatLocation() const { return mOutputPackFormatLocation; }
GLsizeiptr GlobalParamsUBOSize() const { return mGlobalParamsUBOSize; }
void SetGlobalParamsUBOSize(GLsizeiptr size) { mGlobalParamsUBOSize = size; }
void ReplaceLayerPrograms(std::vector<LayerProgram>& newPrograms) { mLayerPrograms.swap(newPrograms); }
std::vector<LayerProgram>& LayerPrograms() { return mLayerPrograms; }
const std::vector<LayerProgram>& LayerPrograms() const { return mLayerPrograms; }
bool ReserveTemporaryRenderTargets(std::size_t count, unsigned width, unsigned height, std::string& error);
const RenderTarget& TemporaryRenderTarget(std::size_t index) const { return mRenderTargets.TemporaryTarget(index); }
std::size_t TemporaryRenderTargetCount() const { return mRenderTargets.TemporaryTargetCount(); }
TemporalHistoryBuffers& TemporalHistory() { return mTemporalHistory; }
const TemporalHistoryBuffers& TemporalHistory() const { return mTemporalHistory; }
void SetDecodeShaderProgram(GLuint program, GLuint vertexShader, GLuint fragmentShader);
void SetOutputPackShaderProgram(GLuint program, GLuint vertexShader, GLuint fragmentShader);
bool InitializeResources(unsigned inputFrameWidth, unsigned inputFrameHeight, unsigned captureTextureWidth, unsigned outputFrameWidth, unsigned outputFrameHeight, unsigned outputPackTextureWidth, std::string& error);
void ResizeView(int width, int height);
void PresentToWindow(HDC hdc, unsigned outputFrameWidth, unsigned outputFrameHeight);
void DestroyResources();
void DestroySingleLayerProgram(LayerProgram& layerProgram);
void DestroyLayerPrograms();
void DestroyDecodeShaderProgram();
void DestroyOutputPackShaderProgram();
private:
GLuint mCaptureTexture = 0;
GLuint mTextureUploadBuffer = 0;
GLuint mIdColorBuf = 0;
GLuint mIdDepthBuf = 0;
GLuint mFullscreenVAO = 0;
GLuint mGlobalParamsUBO = 0;
GLuint mDecodeProgram = 0;
GLuint mDecodeVertexShader = 0;
GLuint mDecodeFragmentShader = 0;
GLint mDecodePackedResolutionLocation = -1;
GLint mDecodeDecodedResolutionLocation = -1;
GLint mDecodeInputPixelFormatLocation = -1;
GLuint mOutputPackProgram = 0;
GLuint mOutputPackVertexShader = 0;
GLuint mOutputPackFragmentShader = 0;
GLint mOutputPackResolutionLocation = -1;
GLint mOutputPackActiveWordsLocation = -1;
GLint mOutputPackFormatLocation = -1;
GLsizeiptr mGlobalParamsUBOSize = 0;
int mViewWidth = 0;
int mViewHeight = 0;
std::vector<LayerProgram> mLayerPrograms;
RenderTargetPool mRenderTargets;
TemporalHistoryBuffers mTemporalHistory;
};

View File

@@ -1,136 +0,0 @@
#include "RenderTargetPool.h"
#include <cstddef>
namespace
{
void ConfigureRenderTargetTexture(
unsigned width,
unsigned height,
GLenum internalFormat,
GLenum pixelFormat,
GLenum pixelType)
{
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
glTexImage2D(GL_TEXTURE_2D, 0, internalFormat, width, height, 0, pixelFormat, pixelType, NULL);
}
}
bool RenderTargetPool::Create(
RenderTargetId id,
unsigned width,
unsigned height,
GLenum internalFormat,
GLenum pixelFormat,
GLenum pixelType,
const char* errorPrefix,
std::string& error)
{
RenderTarget& target = mTargets[TargetIndex(id)];
if (target.texture != 0 || target.framebuffer != 0)
{
error = std::string(errorPrefix) + " render target was already initialized.";
return false;
}
glGenTextures(1, &target.texture);
glBindTexture(GL_TEXTURE_2D, target.texture);
ConfigureRenderTargetTexture(width, height, internalFormat, pixelFormat, pixelType);
glBindTexture(GL_TEXTURE_2D, 0);
glGenFramebuffers(1, &target.framebuffer);
glBindFramebuffer(GL_FRAMEBUFFER, target.framebuffer);
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, target.texture, 0);
if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE)
{
error = std::string("Cannot initialize ") + errorPrefix + " framebuffer.";
glBindFramebuffer(GL_FRAMEBUFFER, 0);
return false;
}
glBindFramebuffer(GL_FRAMEBUFFER, 0);
target.width = width;
target.height = height;
target.internalFormat = internalFormat;
target.pixelFormat = pixelFormat;
target.pixelType = pixelType;
return true;
}
bool RenderTargetPool::ReserveTemporaryTargets(
std::size_t count,
unsigned width,
unsigned height,
GLenum internalFormat,
GLenum pixelFormat,
GLenum pixelType,
std::string& error)
{
if (mTemporaryTargets.size() == count)
return true;
DestroyTemporaryTargets();
mTemporaryTargets.resize(count);
for (std::size_t index = 0; index < mTemporaryTargets.size(); ++index)
{
RenderTarget& target = mTemporaryTargets[index];
glGenTextures(1, &target.texture);
glBindTexture(GL_TEXTURE_2D, target.texture);
ConfigureRenderTargetTexture(width, height, internalFormat, pixelFormat, pixelType);
glBindTexture(GL_TEXTURE_2D, 0);
glGenFramebuffers(1, &target.framebuffer);
glBindFramebuffer(GL_FRAMEBUFFER, target.framebuffer);
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, target.texture, 0);
if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE)
{
error = "Cannot initialize temporary render target.";
glBindFramebuffer(GL_FRAMEBUFFER, 0);
return false;
}
target.width = width;
target.height = height;
target.internalFormat = internalFormat;
target.pixelFormat = pixelFormat;
target.pixelType = pixelType;
}
glBindFramebuffer(GL_FRAMEBUFFER, 0);
return true;
}
void RenderTargetPool::DestroyTemporaryTargets()
{
for (RenderTarget& target : mTemporaryTargets)
{
if (target.framebuffer != 0)
glDeleteFramebuffers(1, &target.framebuffer);
if (target.texture != 0)
glDeleteTextures(1, &target.texture);
}
mTemporaryTargets.clear();
}
void RenderTargetPool::Destroy()
{
for (RenderTarget& target : mTargets)
{
if (target.framebuffer != 0)
glDeleteFramebuffers(1, &target.framebuffer);
if (target.texture != 0)
glDeleteTextures(1, &target.texture);
target = RenderTarget();
}
DestroyTemporaryTargets();
}
const RenderTarget& RenderTargetPool::Target(RenderTargetId id) const
{
return mTargets[TargetIndex(id)];
}

View File

@@ -1,64 +0,0 @@
#pragma once
#include "GLExtensions.h"
#include <array>
#include <string>
#include <vector>
enum class RenderTargetId
{
Decoded,
LayerTemp,
Composite,
Output,
OutputPack,
Count
};
struct RenderTarget
{
GLuint texture = 0;
GLuint framebuffer = 0;
unsigned width = 0;
unsigned height = 0;
GLenum internalFormat = GL_RGBA8;
GLenum pixelFormat = GL_RGBA;
GLenum pixelType = GL_UNSIGNED_BYTE;
};
class RenderTargetPool
{
public:
bool Create(
RenderTargetId id,
unsigned width,
unsigned height,
GLenum internalFormat,
GLenum pixelFormat,
GLenum pixelType,
const char* errorPrefix,
std::string& error);
bool ReserveTemporaryTargets(
std::size_t count,
unsigned width,
unsigned height,
GLenum internalFormat,
GLenum pixelFormat,
GLenum pixelType,
std::string& error);
void DestroyTemporaryTargets();
void Destroy();
GLuint Texture(RenderTargetId id) const { return Target(id).texture; }
GLuint Framebuffer(RenderTargetId id) const { return Target(id).framebuffer; }
const RenderTarget& Target(RenderTargetId id) const;
const RenderTarget& TemporaryTarget(std::size_t index) const { return mTemporaryTargets[index]; }
std::size_t TemporaryTargetCount() const { return mTemporaryTargets.size(); }
private:
static std::size_t TargetIndex(RenderTargetId id) { return static_cast<std::size_t>(id); }
std::array<RenderTarget, static_cast<std::size_t>(RenderTargetId::Count)> mTargets;
std::vector<RenderTarget> mTemporaryTargets;
};

View File

@@ -1,172 +0,0 @@
#include "GlShaderSources.h"
const char* kFullscreenTriangleVertexShaderSource =
"#version 430 core\n"
"out vec2 vTexCoord;\n"
"void main()\n"
"{\n"
" vec2 positions[3] = vec2[3](vec2(-1.0, -1.0), vec2(3.0, -1.0), vec2(-1.0, 3.0));\n"
" vec2 texCoords[3] = vec2[3](vec2(0.0, 0.0), vec2(2.0, 0.0), vec2(0.0, 2.0));\n"
" gl_Position = vec4(positions[gl_VertexID], 0.0, 1.0);\n"
" vTexCoord = texCoords[gl_VertexID];\n"
"}\n";
const char* kDecodeFragmentShaderSource =
"#version 430 core\n"
"layout(binding = 2) uniform sampler2D uPackedVideoInput;\n"
"uniform vec2 uPackedVideoResolution;\n"
"uniform vec2 uDecodedVideoResolution;\n"
"uniform int uInputPixelFormat;\n"
"in vec2 vTexCoord;\n"
"layout(location = 0) out vec4 fragColor;\n"
"vec4 rec709YCbCr2rgba(float Y, float Cb, float Cr, float a)\n"
"{\n"
" Y = (Y * 256.0 - 16.0) / 219.0;\n"
" Cb = (Cb * 256.0 - 16.0) / 224.0 - 0.5;\n"
" Cr = (Cr * 256.0 - 16.0) / 224.0 - 0.5;\n"
" return vec4(Y + 1.5748 * Cr, Y - 0.1873 * Cb - 0.4681 * Cr, Y + 1.8556 * Cb, a);\n"
"}\n"
"vec4 rec709YCbCr10_2rgba(float Y, float Cb, float Cr, float a)\n"
"{\n"
" Y = (Y - 64.0) / 876.0;\n"
" Cb = (Cb - 64.0) / 896.0 - 0.5;\n"
" Cr = (Cr - 64.0) / 896.0 - 0.5;\n"
" return vec4(Y + 1.5748 * Cr, Y - 0.1873 * Cb - 0.4681 * Cr, Y + 1.8556 * Cb, a);\n"
"}\n"
"uint loadV210Word(ivec2 coord)\n"
"{\n"
" vec4 b = round(texelFetch(uPackedVideoInput, coord, 0) * 255.0);\n"
" return uint(b.r) | (uint(b.g) << 8) | (uint(b.b) << 16) | (uint(b.a) << 24);\n"
"}\n"
"float v210Component(uint word, int index)\n"
"{\n"
" return float((word >> uint(index * 10)) & 1023u);\n"
"}\n"
"vec4 decodeUyvy8(ivec2 outputCoord, ivec2 packedSize)\n"
"{\n"
" ivec2 packedCoord = ivec2(clamp(outputCoord.x / 2, 0, packedSize.x - 1), clamp(outputCoord.y, 0, packedSize.y - 1));\n"
" vec4 macroPixel = texelFetch(uPackedVideoInput, packedCoord, 0);\n"
" float ySample = (outputCoord.x & 1) != 0 ? macroPixel.a : macroPixel.g;\n"
" return rec709YCbCr2rgba(ySample, macroPixel.b, macroPixel.r, 1.0);\n"
"}\n"
"vec4 decodeV210(ivec2 outputCoord, ivec2 packedSize)\n"
"{\n"
" int group = outputCoord.x / 6;\n"
" int pixel = outputCoord.x - group * 6;\n"
" int wordBase = group * 4;\n"
" ivec2 rowBase = ivec2(wordBase, clamp(outputCoord.y, 0, packedSize.y - 1));\n"
" uint w0 = loadV210Word(ivec2(min(rowBase.x + 0, packedSize.x - 1), rowBase.y));\n"
" uint w1 = loadV210Word(ivec2(min(rowBase.x + 1, packedSize.x - 1), rowBase.y));\n"
" uint w2 = loadV210Word(ivec2(min(rowBase.x + 2, packedSize.x - 1), rowBase.y));\n"
" uint w3 = loadV210Word(ivec2(min(rowBase.x + 3, packedSize.x - 1), rowBase.y));\n"
" float y0 = v210Component(w0, 1);\n"
" float y1 = v210Component(w1, 0);\n"
" float y2 = v210Component(w1, 2);\n"
" float y3 = v210Component(w2, 1);\n"
" float y4 = v210Component(w3, 0);\n"
" float y5 = v210Component(w3, 2);\n"
" float cb0 = v210Component(w0, 0);\n"
" float cr0 = v210Component(w0, 2);\n"
" float cb2 = v210Component(w1, 1);\n"
" float cr2 = v210Component(w2, 0);\n"
" float cb4 = v210Component(w2, 2);\n"
" float cr4 = v210Component(w3, 1);\n"
" float ySample = pixel == 0 ? y0 : pixel == 1 ? y1 : pixel == 2 ? y2 : pixel == 3 ? y3 : pixel == 4 ? y4 : y5;\n"
" float cbSample = pixel < 2 ? cb0 : pixel < 4 ? cb2 : cb4;\n"
" float crSample = pixel < 2 ? cr0 : pixel < 4 ? cr2 : cr4;\n"
" return rec709YCbCr10_2rgba(ySample, cbSample, crSample, 1.0);\n"
"}\n"
"void main()\n"
"{\n"
" vec2 correctedUv = vec2(vTexCoord.x, 1.0 - vTexCoord.y);\n"
" ivec2 decodedSize = ivec2(max(uDecodedVideoResolution, vec2(1.0, 1.0)));\n"
" ivec2 outputCoord = clamp(ivec2(correctedUv * vec2(decodedSize)), ivec2(0, 0), decodedSize - ivec2(1, 1));\n"
" ivec2 packedSize = ivec2(max(uPackedVideoResolution, vec2(1.0, 1.0)));\n"
" fragColor = uInputPixelFormat == 1 ? decodeV210(outputCoord, packedSize) : decodeUyvy8(outputCoord, packedSize);\n"
"}\n";
const char* kOutputPackFragmentShaderSource =
"#version 430 core\n"
"layout(binding = 0) uniform sampler2D uOutputRgb;\n"
"uniform vec2 uOutputVideoResolution;\n"
"uniform float uActiveV210Words;\n"
"uniform int uOutputPackFormat;\n"
"in vec2 vTexCoord;\n"
"layout(location = 0) out vec4 fragColor;\n"
"vec4 rgbaAt(int x, int y)\n"
"{\n"
" ivec2 size = ivec2(max(uOutputVideoResolution, vec2(1.0, 1.0)));\n"
" return clamp(texelFetch(uOutputRgb, ivec2(clamp(x, 0, size.x - 1), clamp(y, 0, size.y - 1)), 0), vec4(0.0), vec4(1.0));\n"
"}\n"
"vec3 rgbAt(int x, int y)\n"
"{\n"
" return rgbaAt(x, y).rgb;\n"
"}\n"
"vec3 rgbToLegalYcbcr10(vec3 rgb)\n"
"{\n"
" float y = dot(rgb, vec3(0.2126, 0.7152, 0.0722));\n"
" float cb = (rgb.b - y) / 1.8556 + 0.5;\n"
" float cr = (rgb.r - y) / 1.5748 + 0.5;\n"
" return vec3(clamp(round(64.0 + y * 876.0), 64.0, 940.0), clamp(round(64.0 + cb * 896.0), 64.0, 960.0), clamp(round(64.0 + cr * 896.0), 64.0, 960.0));\n"
"}\n"
"uint makeWord(float a, float b, float c)\n"
"{\n"
" return (uint(a) & 1023u) | ((uint(b) & 1023u) << 10) | ((uint(c) & 1023u) << 20);\n"
"}\n"
"vec4 wordToBytes(uint word)\n"
"{\n"
" return vec4(float(word & 255u), float((word >> 8) & 255u), float((word >> 16) & 255u), float((word >> 24) & 255u)) / 255.0;\n"
"}\n"
"vec4 bigEndianWordToBytes(uint word)\n"
"{\n"
" return vec4(float((word >> 24) & 255u), float((word >> 16) & 255u), float((word >> 8) & 255u), float(word & 255u)) / 255.0;\n"
"}\n"
"vec4 packAy10Word(ivec2 outCoord)\n"
"{\n"
" ivec2 size = ivec2(max(uOutputVideoResolution, vec2(1.0, 1.0)));\n"
" if (outCoord.x >= size.x)\n"
" return vec4(0.0);\n"
" int pixelBase = (outCoord.x / 2) * 2;\n"
" int y = outCoord.y;\n"
" vec4 rgba0 = rgbaAt(pixelBase + 0, y);\n"
" vec4 rgba1 = rgbaAt(pixelBase + 1, y);\n"
" vec3 c0 = rgbToLegalYcbcr10(rgba0.rgb);\n"
" vec3 c1 = rgbToLegalYcbcr10(rgba1.rgb);\n"
" float chroma = (outCoord.x & 1) == 0 ? round((c0.y + c1.y) * 0.5) : round((c0.z + c1.z) * 0.5);\n"
" float alpha = round(clamp(((outCoord.x & 1) == 0 ? rgba0.a : rgba1.a), 0.0, 1.0) * 1023.0);\n"
" float luma = (outCoord.x & 1) == 0 ? c0.x : c1.x;\n"
" uint word = ((uint(luma) & 1023u) << 22) | ((uint(chroma) & 1023u) << 12) | ((uint(alpha) & 1023u) << 2);\n"
" return bigEndianWordToBytes(word);\n"
"}\n"
"void main()\n"
"{\n"
" ivec2 outCoord = ivec2(gl_FragCoord.xy);\n"
" if (uOutputPackFormat == 2)\n"
" {\n"
" fragColor = packAy10Word(outCoord);\n"
" return;\n"
" }\n"
" if (float(outCoord.x) >= uActiveV210Words)\n"
" {\n"
" fragColor = vec4(0.0);\n"
" return;\n"
" }\n"
" int group = outCoord.x / 4;\n"
" int wordIndex = outCoord.x - group * 4;\n"
" int pixelBase = group * 6;\n"
" int y = outCoord.y;\n"
" vec3 c0 = rgbToLegalYcbcr10(rgbAt(pixelBase + 0, y));\n"
" vec3 c1 = rgbToLegalYcbcr10(rgbAt(pixelBase + 1, y));\n"
" vec3 c2 = rgbToLegalYcbcr10(rgbAt(pixelBase + 2, y));\n"
" vec3 c3 = rgbToLegalYcbcr10(rgbAt(pixelBase + 3, y));\n"
" vec3 c4 = rgbToLegalYcbcr10(rgbAt(pixelBase + 4, y));\n"
" vec3 c5 = rgbToLegalYcbcr10(rgbAt(pixelBase + 5, y));\n"
" float cb0 = round((c0.y + c1.y) * 0.5);\n"
" float cr0 = round((c0.z + c1.z) * 0.5);\n"
" float cb2 = round((c2.y + c3.y) * 0.5);\n"
" float cr2 = round((c2.z + c3.z) * 0.5);\n"
" float cb4 = round((c4.y + c5.y) * 0.5);\n"
" float cr4 = round((c4.z + c5.z) * 0.5);\n"
" uint word = wordIndex == 0 ? makeWord(cb0, c0.x, cr0) : wordIndex == 1 ? makeWord(c1.x, cb2, c2.x) : wordIndex == 2 ? makeWord(cr2, c3.x, cb4) : makeWord(c4.x, cr4, c5.x);\n"
" fragColor = wordToBytes(word);\n"
"}\n";

View File

@@ -1,5 +0,0 @@
#pragma once
extern const char* kFullscreenTriangleVertexShaderSource;
extern const char* kDecodeFragmentShaderSource;
extern const char* kOutputPackFragmentShaderSource;

View File

@@ -1,103 +0,0 @@
#include "GlobalParamsBuffer.h"
#include "GlRenderConstants.h"
#include "Std140Buffer.h"
#include <vector>
GlobalParamsBuffer::GlobalParamsBuffer(OpenGLRenderer& renderer) :
mRenderer(renderer)
{
}
bool GlobalParamsBuffer::Update(const RuntimeRenderState& state, unsigned availableSourceHistoryLength, unsigned availableTemporalHistoryLength)
{
std::vector<unsigned char>& buffer = mScratchBuffer;
buffer.clear();
buffer.reserve(512);
AppendStd140Float(buffer, static_cast<float>(state.timeSeconds));
AppendStd140Vec2(buffer, static_cast<float>(state.inputWidth), static_cast<float>(state.inputHeight));
AppendStd140Vec2(buffer, static_cast<float>(state.outputWidth), static_cast<float>(state.outputHeight));
AppendStd140Float(buffer, static_cast<float>(state.utcTimeSeconds));
AppendStd140Float(buffer, static_cast<float>(state.utcOffsetSeconds));
AppendStd140Float(buffer, static_cast<float>(state.startupRandom));
AppendStd140Float(buffer, static_cast<float>(state.frameCount));
AppendStd140Float(buffer, static_cast<float>(state.mixAmount));
AppendStd140Float(buffer, static_cast<float>(state.bypass));
const unsigned effectiveSourceHistoryLength = availableSourceHistoryLength < state.effectiveTemporalHistoryLength
? availableSourceHistoryLength
: state.effectiveTemporalHistoryLength;
const unsigned effectiveTemporalHistoryLength = (state.temporalHistorySource == TemporalHistorySource::PreLayerInput)
? (availableTemporalHistoryLength < state.effectiveTemporalHistoryLength ? availableTemporalHistoryLength : state.effectiveTemporalHistoryLength)
: 0u;
AppendStd140Int(buffer, static_cast<int>(effectiveSourceHistoryLength));
AppendStd140Int(buffer, static_cast<int>(effectiveTemporalHistoryLength));
for (const ShaderParameterDefinition& definition : state.parameterDefinitions)
{
auto valueIt = state.parameterValues.find(definition.id);
const ShaderParameterValue value = valueIt != state.parameterValues.end()
? valueIt->second
: ShaderParameterValue();
switch (definition.type)
{
case ShaderParameterType::Float:
AppendStd140Float(buffer, value.numberValues.empty() ? 0.0f : static_cast<float>(value.numberValues[0]));
break;
case ShaderParameterType::Vec2:
AppendStd140Vec2(buffer,
value.numberValues.size() > 0 ? static_cast<float>(value.numberValues[0]) : 0.0f,
value.numberValues.size() > 1 ? static_cast<float>(value.numberValues[1]) : 0.0f);
break;
case ShaderParameterType::Color:
AppendStd140Vec4(buffer,
value.numberValues.size() > 0 ? static_cast<float>(value.numberValues[0]) : 1.0f,
value.numberValues.size() > 1 ? static_cast<float>(value.numberValues[1]) : 1.0f,
value.numberValues.size() > 2 ? static_cast<float>(value.numberValues[2]) : 1.0f,
value.numberValues.size() > 3 ? static_cast<float>(value.numberValues[3]) : 1.0f);
break;
case ShaderParameterType::Boolean:
AppendStd140Int(buffer, value.booleanValue ? 1 : 0);
break;
case ShaderParameterType::Enum:
{
int selectedIndex = 0;
for (std::size_t optionIndex = 0; optionIndex < definition.enumOptions.size(); ++optionIndex)
{
if (definition.enumOptions[optionIndex].value == value.enumValue)
{
selectedIndex = static_cast<int>(optionIndex);
break;
}
}
AppendStd140Int(buffer, selectedIndex);
break;
}
case ShaderParameterType::Text:
break;
case ShaderParameterType::Trigger:
AppendStd140Int(buffer, value.numberValues.empty() ? 0 : static_cast<int>(value.numberValues[0]));
AppendStd140Float(buffer, value.numberValues.size() > 1 ? static_cast<float>(value.numberValues[1]) : -1000000.0f);
break;
}
}
buffer.resize(AlignStd140(buffer.size(), 16), 0);
glBindBuffer(GL_UNIFORM_BUFFER, mRenderer.GlobalParamsUBO());
if (mRenderer.GlobalParamsUBOSize() != static_cast<GLsizeiptr>(buffer.size()))
{
glBufferData(GL_UNIFORM_BUFFER, static_cast<GLsizeiptr>(buffer.size()), buffer.data(), GL_DYNAMIC_DRAW);
mRenderer.SetGlobalParamsUBOSize(static_cast<GLsizeiptr>(buffer.size()));
}
else
{
glBufferSubData(GL_UNIFORM_BUFFER, 0, static_cast<GLsizeiptr>(buffer.size()), buffer.data());
}
glBindBufferBase(GL_UNIFORM_BUFFER, kGlobalParamsBindingPoint, mRenderer.GlobalParamsUBO());
glBindBuffer(GL_UNIFORM_BUFFER, 0);
return true;
}

View File

@@ -1,18 +0,0 @@
#pragma once
#include "OpenGLRenderer.h"
#include "ShaderTypes.h"
#include <vector>
class GlobalParamsBuffer
{
public:
explicit GlobalParamsBuffer(OpenGLRenderer& renderer);
bool Update(const RuntimeRenderState& state, unsigned availableSourceHistoryLength, unsigned availableTemporalHistoryLength);
private:
OpenGLRenderer& mRenderer;
std::vector<unsigned char> mScratchBuffer;
};

View File

@@ -1,187 +0,0 @@
#include "OpenGLShaderPrograms.h"
#include <cstring>
#include <string>
#include <vector>
namespace
{
void CopyErrorMessage(const std::string& message, int errorMessageSize, char* errorMessage)
{
if (!errorMessage || errorMessageSize <= 0)
return;
strncpy_s(errorMessage, errorMessageSize, message.c_str(), _TRUNCATE);
}
std::size_t RequiredTemporaryRenderTargets(const std::vector<OpenGLRenderer::LayerProgram>& layerPrograms)
{
// Only one layer renders at a time, so the pool needs to cover the widest
// layer, not the sum of every intermediate pass in the stack.
std::size_t requiredTargets = 0;
for (const OpenGLRenderer::LayerProgram& layerProgram : layerPrograms)
{
const std::size_t internalPasses = layerProgram.passes.size() > 0 ? layerProgram.passes.size() - 1 : 0;
if (internalPasses > requiredTargets)
requiredTargets = internalPasses;
}
return requiredTargets;
}
}
OpenGLShaderPrograms::OpenGLShaderPrograms(OpenGLRenderer& renderer, RuntimeHost& runtimeHost) :
mRenderer(renderer),
mRuntimeHost(runtimeHost),
mGlobalParamsBuffer(renderer),
mCompiler(renderer, runtimeHost, mTextureBindings)
{
}
bool OpenGLShaderPrograms::CompileLayerPrograms(unsigned inputFrameWidth, unsigned inputFrameHeight, int errorMessageSize, char* errorMessage)
{
const std::vector<RuntimeRenderState> layerStates = mRuntimeHost.GetLayerRenderStates(inputFrameWidth, inputFrameHeight);
std::string temporalError;
const unsigned historyCap = mRuntimeHost.GetMaxTemporalHistoryFrames();
if (!mRenderer.TemporalHistory().ValidateTextureUnitBudget(layerStates, historyCap, temporalError))
{
CopyErrorMessage(temporalError, errorMessageSize, errorMessage);
return false;
}
if (!mRenderer.TemporalHistory().EnsureResources(layerStates, historyCap, inputFrameWidth, inputFrameHeight, temporalError))
{
CopyErrorMessage(temporalError, errorMessageSize, errorMessage);
return false;
}
// Initial startup still compiles synchronously; auto-reload uses the build
// queue so Slang/file work stays off the playback path.
std::vector<LayerProgram> newPrograms;
newPrograms.reserve(layerStates.size());
for (const RuntimeRenderState& state : layerStates)
{
LayerProgram layerProgram;
if (!mCompiler.CompileLayerProgram(state, layerProgram, errorMessageSize, errorMessage))
{
for (LayerProgram& program : newPrograms)
DestroySingleLayerProgram(program);
return false;
}
newPrograms.push_back(layerProgram);
}
std::string targetError;
if (!mRenderer.ReserveTemporaryRenderTargets(RequiredTemporaryRenderTargets(newPrograms), inputFrameWidth, inputFrameHeight, targetError))
{
for (LayerProgram& program : newPrograms)
DestroySingleLayerProgram(program);
CopyErrorMessage(targetError, errorMessageSize, errorMessage);
return false;
}
DestroyLayerPrograms();
mRenderer.ReplaceLayerPrograms(newPrograms);
mCommittedLayerStates = layerStates;
mRuntimeHost.SetCompileStatus(true, "Shader layers compiled successfully.");
mRuntimeHost.ClearReloadRequest();
return true;
}
bool OpenGLShaderPrograms::CommitPreparedLayerPrograms(const PreparedShaderBuild& preparedBuild, unsigned inputFrameWidth, unsigned inputFrameHeight, int errorMessageSize, char* errorMessage)
{
if (!preparedBuild.succeeded)
{
CopyErrorMessage(preparedBuild.message, errorMessageSize, errorMessage);
return false;
}
std::string temporalError;
const unsigned historyCap = mRuntimeHost.GetMaxTemporalHistoryFrames();
if (!mRenderer.TemporalHistory().ValidateTextureUnitBudget(preparedBuild.layerStates, historyCap, temporalError))
{
CopyErrorMessage(temporalError, errorMessageSize, errorMessage);
return false;
}
if (!mRenderer.TemporalHistory().EnsureResources(preparedBuild.layerStates, historyCap, inputFrameWidth, inputFrameHeight, temporalError))
{
CopyErrorMessage(temporalError, errorMessageSize, errorMessage);
return false;
}
// The prepared build already contains GLSL text for each pass. This commit
// step performs the short GL work on the render thread.
std::vector<LayerProgram> newPrograms;
newPrograms.reserve(preparedBuild.layers.size());
for (const PreparedLayerShader& preparedLayer : preparedBuild.layers)
{
LayerProgram layerProgram;
if (!mCompiler.CompilePreparedLayerProgram(preparedLayer.state, preparedLayer.passes, layerProgram, errorMessageSize, errorMessage))
{
for (LayerProgram& program : newPrograms)
DestroySingleLayerProgram(program);
return false;
}
newPrograms.push_back(layerProgram);
}
std::string targetError;
if (!mRenderer.ReserveTemporaryRenderTargets(RequiredTemporaryRenderTargets(newPrograms), inputFrameWidth, inputFrameHeight, targetError))
{
for (LayerProgram& program : newPrograms)
DestroySingleLayerProgram(program);
CopyErrorMessage(targetError, errorMessageSize, errorMessage);
return false;
}
DestroyLayerPrograms();
mRenderer.ReplaceLayerPrograms(newPrograms);
mCommittedLayerStates = preparedBuild.layerStates;
mRuntimeHost.SetCompileStatus(true, "Shader layers compiled successfully.");
mRuntimeHost.ClearReloadRequest();
return true;
}
bool OpenGLShaderPrograms::CompileDecodeShader(int errorMessageSize, char* errorMessage)
{
return mCompiler.CompileDecodeShader(errorMessageSize, errorMessage);
}
bool OpenGLShaderPrograms::CompileOutputPackShader(int errorMessageSize, char* errorMessage)
{
return mCompiler.CompileOutputPackShader(errorMessageSize, errorMessage);
}
void OpenGLShaderPrograms::DestroySingleLayerProgram(LayerProgram& layerProgram)
{
mRenderer.DestroySingleLayerProgram(layerProgram);
}
void OpenGLShaderPrograms::DestroyLayerPrograms()
{
mRenderer.DestroyLayerPrograms();
}
void OpenGLShaderPrograms::DestroyDecodeShaderProgram()
{
mRenderer.DestroyDecodeShaderProgram();
}
void OpenGLShaderPrograms::ResetTemporalHistoryState()
{
mRenderer.TemporalHistory().ResetState();
}
bool OpenGLShaderPrograms::UpdateTextBindingTexture(const RuntimeRenderState& state, LayerProgram::TextBinding& textBinding, std::string& error)
{
return mTextureBindings.UpdateTextBindingTexture(state, textBinding, error);
}
bool OpenGLShaderPrograms::UpdateGlobalParamsBuffer(const RuntimeRenderState& state, unsigned availableSourceHistoryLength, unsigned availableTemporalHistoryLength)
{
return mGlobalParamsBuffer.Update(state, availableSourceHistoryLength, availableTemporalHistoryLength);
}

View File

@@ -1,39 +0,0 @@
#pragma once
#include "GlobalParamsBuffer.h"
#include "OpenGLRenderer.h"
#include "RuntimeHost.h"
#include "ShaderBuildQueue.h"
#include "ShaderTypes.h"
#include "ShaderProgramCompiler.h"
#include "ShaderTextureBindings.h"
#include <string>
class OpenGLShaderPrograms
{
public:
using LayerProgram = OpenGLRenderer::LayerProgram;
OpenGLShaderPrograms(OpenGLRenderer& renderer, RuntimeHost& runtimeHost);
bool CompileLayerPrograms(unsigned inputFrameWidth, unsigned inputFrameHeight, int errorMessageSize, char* errorMessage);
bool CommitPreparedLayerPrograms(const PreparedShaderBuild& preparedBuild, unsigned inputFrameWidth, unsigned inputFrameHeight, int errorMessageSize, char* errorMessage);
bool CompileDecodeShader(int errorMessageSize, char* errorMessage);
bool CompileOutputPackShader(int errorMessageSize, char* errorMessage);
void DestroyLayerPrograms();
void DestroySingleLayerProgram(LayerProgram& layerProgram);
void DestroyDecodeShaderProgram();
void ResetTemporalHistoryState();
const std::vector<RuntimeRenderState>& CommittedLayerStates() const { return mCommittedLayerStates; }
bool UpdateTextBindingTexture(const RuntimeRenderState& state, LayerProgram::TextBinding& textBinding, std::string& error);
bool UpdateGlobalParamsBuffer(const RuntimeRenderState& state, unsigned availableSourceHistoryLength, unsigned availableTemporalHistoryLength);
private:
OpenGLRenderer& mRenderer;
RuntimeHost& mRuntimeHost;
ShaderTextureBindings mTextureBindings;
GlobalParamsBuffer mGlobalParamsBuffer;
ShaderProgramCompiler mCompiler;
std::vector<RuntimeRenderState> mCommittedLayerStates;
};

View File

@@ -1,134 +0,0 @@
#include "ShaderBuildQueue.h"
#include "RuntimeHost.h"
#include <chrono>
#include <utility>
namespace
{
constexpr auto kShaderBuildDebounce = std::chrono::milliseconds(400);
}
ShaderBuildQueue::ShaderBuildQueue(RuntimeHost& runtimeHost) :
mRuntimeHost(runtimeHost),
mWorkerThread([this]() { WorkerLoop(); })
{
}
ShaderBuildQueue::~ShaderBuildQueue()
{
Stop();
}
void ShaderBuildQueue::RequestBuild(unsigned outputWidth, unsigned outputHeight)
{
{
std::lock_guard<std::mutex> lock(mMutex);
mHasRequest = true;
++mRequestedGeneration;
mRequestedOutputWidth = outputWidth;
mRequestedOutputHeight = outputHeight;
mHasReadyBuild = false;
}
mCondition.notify_one();
}
bool ShaderBuildQueue::TryConsumeReadyBuild(PreparedShaderBuild& build)
{
std::lock_guard<std::mutex> lock(mMutex);
if (!mHasReadyBuild)
return false;
build = std::move(mReadyBuild);
mReadyBuild = PreparedShaderBuild();
mHasReadyBuild = false;
return true;
}
void ShaderBuildQueue::Stop()
{
{
std::lock_guard<std::mutex> lock(mMutex);
if (mStopping)
return;
mStopping = true;
}
mCondition.notify_one();
if (mWorkerThread.joinable())
mWorkerThread.join();
}
void ShaderBuildQueue::WorkerLoop()
{
for (;;)
{
uint64_t generation = 0;
unsigned outputWidth = 0;
unsigned outputHeight = 0;
{
std::unique_lock<std::mutex> lock(mMutex);
mCondition.wait(lock, [this]() { return mStopping || mHasRequest; });
if (mStopping)
return;
generation = mRequestedGeneration;
outputWidth = mRequestedOutputWidth;
outputHeight = mRequestedOutputHeight;
mHasRequest = false;
}
for (;;)
{
std::unique_lock<std::mutex> lock(mMutex);
if (mCondition.wait_for(lock, kShaderBuildDebounce, [this, generation]() {
return mStopping || (mHasRequest && mRequestedGeneration != generation);
}))
{
if (mStopping)
return;
generation = mRequestedGeneration;
outputWidth = mRequestedOutputWidth;
outputHeight = mRequestedOutputHeight;
mHasRequest = false;
continue;
}
break;
}
PreparedShaderBuild build = Build(generation, outputWidth, outputHeight);
std::lock_guard<std::mutex> lock(mMutex);
if (mStopping)
return;
if (generation != mRequestedGeneration)
continue;
mReadyBuild = std::move(build);
mHasReadyBuild = true;
}
}
PreparedShaderBuild ShaderBuildQueue::Build(uint64_t generation, unsigned outputWidth, unsigned outputHeight)
{
PreparedShaderBuild build;
build.generation = generation;
build.layerStates = mRuntimeHost.GetLayerRenderStates(outputWidth, outputHeight);
build.layers.reserve(build.layerStates.size());
for (const RuntimeRenderState& state : build.layerStates)
{
PreparedLayerShader layer;
layer.state = state;
if (!mRuntimeHost.BuildLayerPassFragmentShaderSources(state.layerId, layer.passes, build.message))
{
build.succeeded = false;
return build;
}
build.layers.push_back(std::move(layer));
}
build.succeeded = true;
build.message = "Shader layers prepared successfully.";
return build;
}

View File

@@ -1,57 +0,0 @@
#pragma once
#include "ShaderTypes.h"
#include <condition_variable>
#include <cstdint>
#include <mutex>
#include <string>
#include <thread>
#include <vector>
class RuntimeHost;
struct PreparedLayerShader
{
RuntimeRenderState state;
std::vector<ShaderPassBuildSource> passes;
};
struct PreparedShaderBuild
{
uint64_t generation = 0;
bool succeeded = false;
std::string message;
std::vector<RuntimeRenderState> layerStates;
std::vector<PreparedLayerShader> layers;
};
class ShaderBuildQueue
{
public:
explicit ShaderBuildQueue(RuntimeHost& runtimeHost);
~ShaderBuildQueue();
ShaderBuildQueue(const ShaderBuildQueue&) = delete;
ShaderBuildQueue& operator=(const ShaderBuildQueue&) = delete;
void RequestBuild(unsigned outputWidth, unsigned outputHeight);
bool TryConsumeReadyBuild(PreparedShaderBuild& build);
void Stop();
private:
void WorkerLoop();
PreparedShaderBuild Build(uint64_t generation, unsigned outputWidth, unsigned outputHeight);
RuntimeHost& mRuntimeHost;
std::thread mWorkerThread;
std::mutex mMutex;
std::condition_variable mCondition;
bool mStopping = false;
bool mHasRequest = false;
uint64_t mRequestedGeneration = 0;
unsigned mRequestedOutputWidth = 0;
unsigned mRequestedOutputHeight = 0;
bool mHasReadyBuild = false;
PreparedShaderBuild mReadyBuild;
};

View File

@@ -1,233 +0,0 @@
#include "ShaderProgramCompiler.h"
#include "GlRenderConstants.h"
#include "GlScopedObjects.h"
#include "GlShaderSources.h"
#include <cstring>
#include <utility>
#include <vector>
namespace
{
void CopyErrorMessage(const std::string& message, int errorMessageSize, char* errorMessage)
{
if (!errorMessage || errorMessageSize <= 0)
return;
strncpy_s(errorMessage, errorMessageSize, message.c_str(), _TRUNCATE);
}
}
ShaderProgramCompiler::ShaderProgramCompiler(OpenGLRenderer& renderer, RuntimeHost& runtimeHost, ShaderTextureBindings& textureBindings) :
mRenderer(renderer),
mRuntimeHost(runtimeHost),
mTextureBindings(textureBindings)
{
}
bool ShaderProgramCompiler::CompileLayerProgram(const RuntimeRenderState& state, LayerProgram& layerProgram, int errorMessageSize, char* errorMessage)
{
std::vector<ShaderPassBuildSource> passSources;
std::string loadError;
if (!mRuntimeHost.BuildLayerPassFragmentShaderSources(state.layerId, passSources, loadError))
{
CopyErrorMessage(loadError, errorMessageSize, errorMessage);
return false;
}
return CompilePreparedLayerProgram(state, passSources, layerProgram, errorMessageSize, errorMessage);
}
bool ShaderProgramCompiler::CompilePreparedLayerProgram(const RuntimeRenderState& state, const std::vector<ShaderPassBuildSource>& passSources, LayerProgram& layerProgram, int errorMessageSize, char* errorMessage)
{
GLsizei errorBufferSize = 0;
std::string loadError;
const char* vertexSource = kFullscreenTriangleVertexShaderSource;
layerProgram.layerId = state.layerId;
layerProgram.shaderId = state.shaderId;
layerProgram.passes.clear();
for (const auto& passSource : passSources)
{
GLint compileResult = GL_FALSE;
GLint linkResult = GL_FALSE;
const char* fragmentSource = passSource.fragmentShaderSource.c_str();
ScopedGlShader newVertexShader(glCreateShader(GL_VERTEX_SHADER));
glShaderSource(newVertexShader.get(), 1, (const GLchar**)&vertexSource, NULL);
glCompileShader(newVertexShader.get());
glGetShaderiv(newVertexShader.get(), GL_COMPILE_STATUS, &compileResult);
if (compileResult == GL_FALSE)
{
glGetShaderInfoLog(newVertexShader.get(), errorMessageSize, &errorBufferSize, errorMessage);
mRenderer.DestroySingleLayerProgram(layerProgram);
return false;
}
ScopedGlShader newFragmentShader(glCreateShader(GL_FRAGMENT_SHADER));
glShaderSource(newFragmentShader.get(), 1, (const GLchar**)&fragmentSource, NULL);
glCompileShader(newFragmentShader.get());
glGetShaderiv(newFragmentShader.get(), GL_COMPILE_STATUS, &compileResult);
if (compileResult == GL_FALSE)
{
glGetShaderInfoLog(newFragmentShader.get(), errorMessageSize, &errorBufferSize, errorMessage);
mRenderer.DestroySingleLayerProgram(layerProgram);
return false;
}
ScopedGlProgram newProgram(glCreateProgram());
glAttachShader(newProgram.get(), newVertexShader.get());
glAttachShader(newProgram.get(), newFragmentShader.get());
glLinkProgram(newProgram.get());
glGetProgramiv(newProgram.get(), GL_LINK_STATUS, &linkResult);
if (linkResult == GL_FALSE)
{
glGetProgramInfoLog(newProgram.get(), errorMessageSize, &errorBufferSize, errorMessage);
mRenderer.DestroySingleLayerProgram(layerProgram);
return false;
}
std::vector<LayerProgram::TextureBinding> textureBindings;
for (const ShaderTextureAsset& textureAsset : state.textureAssets)
{
LayerProgram::TextureBinding textureBinding;
textureBinding.samplerName = textureAsset.id;
textureBinding.sourcePath = textureAsset.path;
if (!mTextureBindings.LoadTextureAsset(textureAsset, textureBinding.texture, loadError))
{
for (LayerProgram::TextureBinding& loadedTexture : textureBindings)
{
if (loadedTexture.texture != 0)
glDeleteTextures(1, &loadedTexture.texture);
}
CopyErrorMessage(loadError, errorMessageSize, errorMessage);
mRenderer.DestroySingleLayerProgram(layerProgram);
return false;
}
textureBindings.push_back(textureBinding);
}
std::vector<LayerProgram::TextBinding> textBindings;
mTextureBindings.CreateTextBindings(state, textBindings);
PassProgram passProgram;
passProgram.passId = passSource.passId;
passProgram.inputNames = passSource.inputNames;
passProgram.outputName = passSource.outputName;
passProgram.shaderTextureBase = mTextureBindings.ResolveShaderTextureBase(state, mRuntimeHost.GetMaxTemporalHistoryFrames());
passProgram.textureBindings.swap(textureBindings);
passProgram.textBindings.swap(textBindings);
const GLuint globalParamsIndex = glGetUniformBlockIndex(newProgram.get(), "GlobalParams");
if (globalParamsIndex != GL_INVALID_INDEX)
glUniformBlockBinding(newProgram.get(), globalParamsIndex, kGlobalParamsBindingPoint);
const unsigned historyCap = mRuntimeHost.GetMaxTemporalHistoryFrames();
glUseProgram(newProgram.get());
mTextureBindings.AssignLayerSamplerUniforms(newProgram.get(), state, passProgram, historyCap);
glUseProgram(0);
passProgram.program = newProgram.release();
passProgram.vertexShader = newVertexShader.release();
passProgram.fragmentShader = newFragmentShader.release();
layerProgram.passes.push_back(std::move(passProgram));
}
return true;
}
bool ShaderProgramCompiler::CompileDecodeShader(int errorMessageSize, char* errorMessage)
{
GLsizei errorBufferSize = 0;
GLint compileResult = GL_FALSE;
GLint linkResult = GL_FALSE;
const char* vertexSource = kFullscreenTriangleVertexShaderSource;
const char* fragmentSource = kDecodeFragmentShaderSource;
ScopedGlShader newVertexShader(glCreateShader(GL_VERTEX_SHADER));
glShaderSource(newVertexShader.get(), 1, (const GLchar**)&vertexSource, NULL);
glCompileShader(newVertexShader.get());
glGetShaderiv(newVertexShader.get(), GL_COMPILE_STATUS, &compileResult);
if (compileResult == GL_FALSE)
{
glGetShaderInfoLog(newVertexShader.get(), errorMessageSize, &errorBufferSize, errorMessage);
return false;
}
ScopedGlShader newFragmentShader(glCreateShader(GL_FRAGMENT_SHADER));
glShaderSource(newFragmentShader.get(), 1, (const GLchar**)&fragmentSource, NULL);
glCompileShader(newFragmentShader.get());
glGetShaderiv(newFragmentShader.get(), GL_COMPILE_STATUS, &compileResult);
if (compileResult == GL_FALSE)
{
glGetShaderInfoLog(newFragmentShader.get(), errorMessageSize, &errorBufferSize, errorMessage);
return false;
}
ScopedGlProgram newProgram(glCreateProgram());
glAttachShader(newProgram.get(), newVertexShader.get());
glAttachShader(newProgram.get(), newFragmentShader.get());
glLinkProgram(newProgram.get());
glGetProgramiv(newProgram.get(), GL_LINK_STATUS, &linkResult);
if (linkResult == GL_FALSE)
{
glGetProgramInfoLog(newProgram.get(), errorMessageSize, &errorBufferSize, errorMessage);
return false;
}
mRenderer.DestroyDecodeShaderProgram();
mRenderer.SetDecodeShaderProgram(newProgram.release(), newVertexShader.release(), newFragmentShader.release());
return true;
}
bool ShaderProgramCompiler::CompileOutputPackShader(int errorMessageSize, char* errorMessage)
{
GLsizei errorBufferSize = 0;
GLint compileResult = GL_FALSE;
GLint linkResult = GL_FALSE;
const char* vertexSource = kFullscreenTriangleVertexShaderSource;
const char* fragmentSource = kOutputPackFragmentShaderSource;
ScopedGlShader newVertexShader(glCreateShader(GL_VERTEX_SHADER));
glShaderSource(newVertexShader.get(), 1, (const GLchar**)&vertexSource, NULL);
glCompileShader(newVertexShader.get());
glGetShaderiv(newVertexShader.get(), GL_COMPILE_STATUS, &compileResult);
if (compileResult == GL_FALSE)
{
glGetShaderInfoLog(newVertexShader.get(), errorMessageSize, &errorBufferSize, errorMessage);
return false;
}
ScopedGlShader newFragmentShader(glCreateShader(GL_FRAGMENT_SHADER));
glShaderSource(newFragmentShader.get(), 1, (const GLchar**)&fragmentSource, NULL);
glCompileShader(newFragmentShader.get());
glGetShaderiv(newFragmentShader.get(), GL_COMPILE_STATUS, &compileResult);
if (compileResult == GL_FALSE)
{
glGetShaderInfoLog(newFragmentShader.get(), errorMessageSize, &errorBufferSize, errorMessage);
return false;
}
ScopedGlProgram newProgram(glCreateProgram());
glAttachShader(newProgram.get(), newVertexShader.get());
glAttachShader(newProgram.get(), newFragmentShader.get());
glLinkProgram(newProgram.get());
glGetProgramiv(newProgram.get(), GL_LINK_STATUS, &linkResult);
if (linkResult == GL_FALSE)
{
glGetProgramInfoLog(newProgram.get(), errorMessageSize, &errorBufferSize, errorMessage);
return false;
}
glUseProgram(newProgram.get());
const GLint outputSamplerLocation = glGetUniformLocation(newProgram.get(), "uOutputRgb");
if (outputSamplerLocation >= 0)
glUniform1i(outputSamplerLocation, 0);
glUseProgram(0);
mRenderer.DestroyOutputPackShaderProgram();
mRenderer.SetOutputPackShaderProgram(newProgram.release(), newVertexShader.release(), newFragmentShader.release());
return true;
}

View File

@@ -1,27 +0,0 @@
#pragma once
#include "OpenGLRenderer.h"
#include "RuntimeHost.h"
#include "ShaderTextureBindings.h"
#include <string>
#include <vector>
class ShaderProgramCompiler
{
public:
using LayerProgram = OpenGLRenderer::LayerProgram;
using PassProgram = OpenGLRenderer::LayerProgram::PassProgram;
ShaderProgramCompiler(OpenGLRenderer& renderer, RuntimeHost& runtimeHost, ShaderTextureBindings& textureBindings);
bool CompileLayerProgram(const RuntimeRenderState& state, LayerProgram& layerProgram, int errorMessageSize, char* errorMessage);
bool CompilePreparedLayerProgram(const RuntimeRenderState& state, const std::vector<ShaderPassBuildSource>& passSources, LayerProgram& layerProgram, int errorMessageSize, char* errorMessage);
bool CompileDecodeShader(int errorMessageSize, char* errorMessage);
bool CompileOutputPackShader(int errorMessageSize, char* errorMessage);
private:
OpenGLRenderer& mRenderer;
RuntimeHost& mRuntimeHost;
ShaderTextureBindings& mTextureBindings;
};

View File

@@ -1,223 +0,0 @@
#include "ShaderTextureBindings.h"
#include "GlRenderConstants.h"
#include "TextRasterizer.h"
#include "TextureAssetLoader.h"
#include <algorithm>
#include <filesystem>
namespace
{
std::string TextValueForBinding(const RuntimeRenderState& state, const std::string& parameterId)
{
auto valueIt = state.parameterValues.find(parameterId);
return valueIt == state.parameterValues.end() ? std::string() : valueIt->second.textValue;
}
const ShaderFontAsset* FindFontAssetForParameter(const RuntimeRenderState& state, const ShaderParameterDefinition& definition)
{
if (!definition.fontId.empty())
{
for (const ShaderFontAsset& fontAsset : state.fontAssets)
{
if (fontAsset.id == definition.fontId)
return &fontAsset;
}
}
return state.fontAssets.empty() ? nullptr : &state.fontAssets.front();
}
}
bool ShaderTextureBindings::LoadTextureAsset(const ShaderTextureAsset& textureAsset, GLuint& textureId, std::string& error)
{
return ::LoadTextureAsset(textureAsset, textureId, error);
}
void ShaderTextureBindings::CreateTextBindings(const RuntimeRenderState& state, std::vector<LayerProgram::TextBinding>& textBindings)
{
for (const ShaderParameterDefinition& definition : state.parameterDefinitions)
{
if (definition.type != ShaderParameterType::Text)
continue;
LayerProgram::TextBinding textBinding;
textBinding.parameterId = definition.id;
textBinding.samplerName = definition.id + "Texture";
textBinding.fontId = definition.fontId;
glGenTextures(1, &textBinding.texture);
glBindTexture(GL_TEXTURE_2D, textBinding.texture);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
std::vector<unsigned char> empty(static_cast<std::size_t>(kTextTextureWidth) * kTextTextureHeight * 4, 0);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, kTextTextureWidth, kTextTextureHeight, 0, GL_BGRA, GL_UNSIGNED_INT_8_8_8_8_REV, empty.data());
glBindTexture(GL_TEXTURE_2D, 0);
textBindings.push_back(textBinding);
}
}
bool ShaderTextureBindings::UpdateTextBindingTexture(const RuntimeRenderState& state, LayerProgram::TextBinding& textBinding, std::string& error)
{
const std::string text = TextValueForBinding(state, textBinding.parameterId);
if (text == textBinding.renderedText && textBinding.renderedWidth == kTextTextureWidth && textBinding.renderedHeight == kTextTextureHeight)
return true;
auto definitionIt = std::find_if(state.parameterDefinitions.begin(), state.parameterDefinitions.end(),
[&textBinding](const ShaderParameterDefinition& definition) { return definition.id == textBinding.parameterId; });
if (definitionIt == state.parameterDefinitions.end())
return true;
const ShaderFontAsset* fontAsset = FindFontAssetForParameter(state, *definitionIt);
std::filesystem::path fontPath;
if (fontAsset)
fontPath = fontAsset->path;
std::vector<unsigned char> sdf;
if (!RasterizeTextSdf(text, fontPath, sdf, error))
return false;
GLint previousActiveTexture = 0;
GLint previousUnpackBuffer = 0;
glGetIntegerv(GL_ACTIVE_TEXTURE, &previousActiveTexture);
glGetIntegerv(GL_PIXEL_UNPACK_BUFFER_BINDING, &previousUnpackBuffer);
glActiveTexture(GL_TEXTURE0);
glBindBuffer(GL_PIXEL_UNPACK_BUFFER, 0);
glBindTexture(GL_TEXTURE_2D, textBinding.texture);
glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, kTextTextureWidth, kTextTextureHeight, GL_BGRA, GL_UNSIGNED_INT_8_8_8_8_REV, sdf.data());
glBindTexture(GL_TEXTURE_2D, 0);
glBindBuffer(GL_PIXEL_UNPACK_BUFFER, static_cast<GLuint>(previousUnpackBuffer));
glActiveTexture(static_cast<GLenum>(previousActiveTexture));
textBinding.renderedText = text;
textBinding.renderedWidth = kTextTextureWidth;
textBinding.renderedHeight = kTextTextureHeight;
return true;
}
GLint ShaderTextureBindings::FindSamplerUniformLocation(GLuint program, const std::string& samplerName) const
{
GLint location = glGetUniformLocation(program, samplerName.c_str());
if (location >= 0)
return location;
return glGetUniformLocation(program, (samplerName + "_0").c_str());
}
GLuint ShaderTextureBindings::ResolveShaderTextureBase(const RuntimeRenderState& state, unsigned historyCap) const
{
return state.isTemporal ? kSourceHistoryTextureUnitBase + historyCap + historyCap : kSourceHistoryTextureUnitBase;
}
void ShaderTextureBindings::AssignLayerSamplerUniforms(GLuint program, const RuntimeRenderState& state, const PassProgram& passProgram, unsigned historyCap) const
{
const GLuint shaderTextureBase = ResolveShaderTextureBase(state, historyCap);
const GLint videoInputLocation = glGetUniformLocation(program, "gVideoInput");
if (videoInputLocation >= 0)
glUniform1i(videoInputLocation, static_cast<GLint>(kDecodedVideoTextureUnit));
for (unsigned index = 0; index < historyCap; ++index)
{
const std::string sourceSamplerName = "gSourceHistory" + std::to_string(index);
const GLint sourceSamplerLocation = glGetUniformLocation(program, sourceSamplerName.c_str());
if (sourceSamplerLocation >= 0)
glUniform1i(sourceSamplerLocation, static_cast<GLint>(kSourceHistoryTextureUnitBase + index));
const std::string temporalSamplerName = "gTemporalHistory" + std::to_string(index);
const GLint temporalSamplerLocation = glGetUniformLocation(program, temporalSamplerName.c_str());
if (temporalSamplerLocation >= 0)
glUniform1i(temporalSamplerLocation, static_cast<GLint>(kSourceHistoryTextureUnitBase + historyCap + index));
}
for (std::size_t index = 0; index < passProgram.textureBindings.size(); ++index)
{
const GLint textureSamplerLocation = FindSamplerUniformLocation(program, passProgram.textureBindings[index].samplerName);
if (textureSamplerLocation >= 0)
glUniform1i(textureSamplerLocation, static_cast<GLint>(shaderTextureBase + static_cast<GLuint>(index)));
}
const GLuint textTextureBase = shaderTextureBase + static_cast<GLuint>(passProgram.textureBindings.size());
for (std::size_t index = 0; index < passProgram.textBindings.size(); ++index)
{
const GLint textSamplerLocation = FindSamplerUniformLocation(program, passProgram.textBindings[index].samplerName);
if (textSamplerLocation >= 0)
glUniform1i(textSamplerLocation, static_cast<GLint>(textTextureBase + static_cast<GLuint>(index)));
}
}
ShaderTextureBindings::RuntimeTextureBindingPlan ShaderTextureBindings::BuildLayerRuntimeBindingPlan(
const PassProgram& passProgram,
GLuint layerInputTexture,
const std::vector<GLuint>& sourceHistoryTextures,
const std::vector<GLuint>& temporalHistoryTextures) const
{
RuntimeTextureBindingPlan plan;
plan.bindings.push_back({ "layerInput", "gVideoInput", layerInputTexture, kDecodedVideoTextureUnit });
for (std::size_t index = 0; index < sourceHistoryTextures.size(); ++index)
{
plan.bindings.push_back({
"sourceHistory",
"gSourceHistory" + std::to_string(index),
sourceHistoryTextures[index],
kSourceHistoryTextureUnitBase + static_cast<GLuint>(index)
});
}
const GLuint temporalBase = kSourceHistoryTextureUnitBase + static_cast<GLuint>(sourceHistoryTextures.size());
for (std::size_t index = 0; index < temporalHistoryTextures.size(); ++index)
{
plan.bindings.push_back({
"temporalHistory",
"gTemporalHistory" + std::to_string(index),
temporalHistoryTextures[index],
temporalBase + static_cast<GLuint>(index)
});
}
const GLuint shaderTextureBase = passProgram.shaderTextureBase != 0 ? passProgram.shaderTextureBase : kSourceHistoryTextureUnitBase;
for (std::size_t index = 0; index < passProgram.textureBindings.size(); ++index)
{
const LayerProgram::TextureBinding& textureBinding = passProgram.textureBindings[index];
plan.bindings.push_back({
"shaderTexture",
textureBinding.samplerName,
textureBinding.texture,
shaderTextureBase + static_cast<GLuint>(index)
});
}
const GLuint textTextureBase = shaderTextureBase + static_cast<GLuint>(passProgram.textureBindings.size());
for (std::size_t index = 0; index < passProgram.textBindings.size(); ++index)
{
const LayerProgram::TextBinding& textBinding = passProgram.textBindings[index];
plan.bindings.push_back({
"textTexture",
textBinding.samplerName,
textBinding.texture,
textTextureBase + static_cast<GLuint>(index)
});
}
return plan;
}
void ShaderTextureBindings::BindRuntimeTexturePlan(const RuntimeTextureBindingPlan& plan) const
{
for (const RuntimeTextureBinding& binding : plan.bindings)
{
glActiveTexture(GL_TEXTURE0 + binding.textureUnit);
glBindTexture(GL_TEXTURE_2D, binding.texture);
}
glActiveTexture(GL_TEXTURE0);
}
void ShaderTextureBindings::UnbindRuntimeTexturePlan(const RuntimeTextureBindingPlan& plan) const
{
for (const RuntimeTextureBinding& binding : plan.bindings)
{
glActiveTexture(GL_TEXTURE0 + binding.textureUnit);
glBindTexture(GL_TEXTURE_2D, 0);
}
glActiveTexture(GL_TEXTURE0);
}

View File

@@ -1,41 +0,0 @@
#pragma once
#include "OpenGLRenderer.h"
#include "ShaderTypes.h"
#include <string>
#include <vector>
class ShaderTextureBindings
{
public:
using LayerProgram = OpenGLRenderer::LayerProgram;
using PassProgram = OpenGLRenderer::LayerProgram::PassProgram;
struct RuntimeTextureBinding
{
std::string semanticName;
std::string samplerName;
GLuint texture = 0;
GLuint textureUnit = 0;
};
struct RuntimeTextureBindingPlan
{
std::vector<RuntimeTextureBinding> bindings;
};
bool LoadTextureAsset(const ShaderTextureAsset& textureAsset, GLuint& textureId, std::string& error);
void CreateTextBindings(const RuntimeRenderState& state, std::vector<LayerProgram::TextBinding>& textBindings);
bool UpdateTextBindingTexture(const RuntimeRenderState& state, LayerProgram::TextBinding& textBinding, std::string& error);
GLint FindSamplerUniformLocation(GLuint program, const std::string& samplerName) const;
GLuint ResolveShaderTextureBase(const RuntimeRenderState& state, unsigned historyCap) const;
void AssignLayerSamplerUniforms(GLuint program, const RuntimeRenderState& state, const PassProgram& passProgram, unsigned historyCap) const;
RuntimeTextureBindingPlan BuildLayerRuntimeBindingPlan(
const PassProgram& passProgram,
GLuint layerInputTexture,
const std::vector<GLuint>& sourceHistoryTextures,
const std::vector<GLuint>& temporalHistoryTextures) const;
void BindRuntimeTexturePlan(const RuntimeTextureBindingPlan& plan) const;
void UnbindRuntimeTexturePlan(const RuntimeTextureBindingPlan& plan) const;
};

View File

@@ -1,243 +0,0 @@
#include "TextRasterizer.h"
#include <windows.h>
#include <algorithm>
#include <cmath>
#include <cstring>
#include <gdiplus.h>
#include <memory>
namespace
{
constexpr int kTextSdfSpread = 20;
constexpr float kTextFontPixelSize = 144.0f;
constexpr float kTextLayoutPadding = 48.0f;
constexpr float kSdfInfinity = 1.0e20f;
class GdiplusSession
{
public:
GdiplusSession()
{
Gdiplus::GdiplusStartupInput startupInput;
mStarted = Gdiplus::GdiplusStartup(&mToken, &startupInput, NULL) == Gdiplus::Ok;
}
~GdiplusSession()
{
if (mStarted)
Gdiplus::GdiplusShutdown(mToken);
}
GdiplusSession(const GdiplusSession&) = delete;
GdiplusSession& operator=(const GdiplusSession&) = delete;
bool started() const { return mStarted; }
private:
ULONG_PTR mToken = 0;
bool mStarted = false;
};
std::wstring Utf8ToWide(const std::string& text)
{
if (text.empty())
return std::wstring();
const int required = MultiByteToWideChar(CP_UTF8, 0, text.c_str(), -1, NULL, 0);
if (required <= 1)
return std::wstring();
std::wstring wide(static_cast<std::size_t>(required - 1), L'\0');
MultiByteToWideChar(CP_UTF8, 0, text.c_str(), -1, wide.data(), required);
return wide;
}
void DistanceTransform1D(const std::vector<float>& input, std::vector<float>& output, unsigned count)
{
std::vector<unsigned> locations(count, 0);
std::vector<float> boundaries(static_cast<std::size_t>(count) + 1, 0.0f);
unsigned segment = 0;
locations[0] = 0;
boundaries[0] = -kSdfInfinity;
boundaries[1] = kSdfInfinity;
for (unsigned q = 1; q < count; ++q)
{
float intersection = 0.0f;
for (;;)
{
const unsigned location = locations[segment];
intersection =
((input[q] + static_cast<float>(q * q)) - (input[location] + static_cast<float>(location * location))) /
(2.0f * static_cast<float>(q) - 2.0f * static_cast<float>(location));
if (intersection > boundaries[segment] || segment == 0)
break;
--segment;
}
++segment;
locations[segment] = q;
boundaries[segment] = intersection;
boundaries[segment + 1] = kSdfInfinity;
}
segment = 0;
for (unsigned q = 0; q < count; ++q)
{
while (boundaries[segment + 1] < static_cast<float>(q))
++segment;
const unsigned location = locations[segment];
const float delta = static_cast<float>(q) - static_cast<float>(location);
output[q] = delta * delta + input[location];
}
}
std::vector<float> DistanceTransform2D(const std::vector<unsigned char>& targetMask, unsigned width, unsigned height)
{
std::vector<float> rowInput(width, 0.0f);
std::vector<float> rowOutput(width, 0.0f);
std::vector<float> columnInput(height, 0.0f);
std::vector<float> columnOutput(height, 0.0f);
std::vector<float> rowDistance(static_cast<std::size_t>(width) * height, 0.0f);
std::vector<float> distance(static_cast<std::size_t>(width) * height, 0.0f);
for (unsigned y = 0; y < height; ++y)
{
for (unsigned x = 0; x < width; ++x)
rowInput[x] = targetMask[static_cast<std::size_t>(y) * width + x] ? 0.0f : kSdfInfinity;
DistanceTransform1D(rowInput, rowOutput, width);
for (unsigned x = 0; x < width; ++x)
rowDistance[static_cast<std::size_t>(y) * width + x] = rowOutput[x];
}
for (unsigned x = 0; x < width; ++x)
{
for (unsigned y = 0; y < height; ++y)
columnInput[y] = rowDistance[static_cast<std::size_t>(y) * width + x];
DistanceTransform1D(columnInput, columnOutput, height);
for (unsigned y = 0; y < height; ++y)
distance[static_cast<std::size_t>(y) * width + x] = columnOutput[y];
}
return distance;
}
std::vector<unsigned char> BuildTextSdfTexture(const std::vector<unsigned char>& alpha, unsigned width, unsigned height)
{
std::vector<unsigned char> insideMask(static_cast<std::size_t>(width) * height, 0);
std::vector<unsigned char> outsideMask(static_cast<std::size_t>(width) * height, 0);
for (std::size_t index = 0; index < alpha.size(); ++index)
{
const bool inside = alpha[index] > 127;
insideMask[index] = inside ? 1 : 0;
outsideMask[index] = inside ? 0 : 1;
}
const std::vector<float> distanceToInside = DistanceTransform2D(insideMask, width, height);
const std::vector<float> distanceToOutside = DistanceTransform2D(outsideMask, width, height);
std::vector<unsigned char> sdf(static_cast<std::size_t>(width) * height * 4, 0);
for (unsigned y = 0; y < height; ++y)
{
const unsigned flippedY = height - 1 - y;
for (unsigned x = 0; x < width; ++x)
{
const std::size_t source = static_cast<std::size_t>(y) * width + x;
const float signedDistance = std::sqrt(distanceToOutside[source]) - std::sqrt(distanceToInside[source]);
const float normalized = std::clamp(
0.5f + signedDistance / static_cast<float>(kTextSdfSpread * 2),
0.0f,
1.0f);
const unsigned char value = static_cast<unsigned char>(normalized * 255.0f + 0.5f);
const std::size_t out = (static_cast<std::size_t>(flippedY) * width + x) * 4;
sdf[out + 0] = value;
sdf[out + 1] = value;
sdf[out + 2] = value;
sdf[out + 3] = value;
}
}
return sdf;
}
}
bool RasterizeTextSdf(const std::string& text, const std::filesystem::path& fontPath, std::vector<unsigned char>& sdf, std::string& error)
{
GdiplusSession gdiplus;
if (!gdiplus.started())
{
error = "Could not start GDI+ for text rendering.";
return false;
}
Gdiplus::PrivateFontCollection fontCollection;
Gdiplus::FontFamily fallbackFamily(L"Arial");
Gdiplus::FontFamily* fontFamily = &fallbackFamily;
std::unique_ptr<Gdiplus::FontFamily[]> families;
const std::wstring wideFontPath = fontPath.empty() ? std::wstring() : fontPath.wstring();
if (!wideFontPath.empty())
{
if (fontCollection.AddFontFile(wideFontPath.c_str()) != Gdiplus::Ok)
{
error = "Could not load packaged font file for text rendering: " + fontPath.string();
return false;
}
const INT familyCount = fontCollection.GetFamilyCount();
if (familyCount <= 0)
{
error = "Packaged font did not contain a usable font family: " + fontPath.string();
return false;
}
families.reset(new Gdiplus::FontFamily[familyCount]);
INT found = 0;
if (fontCollection.GetFamilies(familyCount, families.get(), &found) != Gdiplus::Ok || found <= 0)
{
error = "Could not read the packaged font family: " + fontPath.string();
return false;
}
fontFamily = &families[0];
}
Gdiplus::Bitmap bitmap(kTextTextureWidth, kTextTextureHeight, PixelFormat32bppARGB);
Gdiplus::Graphics graphics(&bitmap);
graphics.SetCompositingMode(Gdiplus::CompositingModeSourceCopy);
graphics.Clear(Gdiplus::Color(255, 0, 0, 0));
graphics.SetCompositingMode(Gdiplus::CompositingModeSourceOver);
graphics.SetTextRenderingHint(Gdiplus::TextRenderingHintAntiAlias);
graphics.SetSmoothingMode(Gdiplus::SmoothingModeHighQuality);
Gdiplus::Font font(fontFamily, kTextFontPixelSize, Gdiplus::FontStyleRegular, Gdiplus::UnitPixel);
Gdiplus::SolidBrush brush(Gdiplus::Color(255, 255, 255, 255));
Gdiplus::StringFormat format;
format.SetAlignment(Gdiplus::StringAlignmentNear);
format.SetLineAlignment(Gdiplus::StringAlignmentCenter);
format.SetFormatFlags(Gdiplus::StringFormatFlagsNoWrap | Gdiplus::StringFormatFlagsMeasureTrailingSpaces);
const Gdiplus::RectF layout(
kTextLayoutPadding,
0.0f,
static_cast<Gdiplus::REAL>(kTextTextureWidth) - (kTextLayoutPadding * 2.0f),
static_cast<Gdiplus::REAL>(kTextTextureHeight));
const std::wstring wideText = Utf8ToWide(text);
graphics.DrawString(wideText.c_str(), -1, &font, layout, &format, &brush);
std::vector<unsigned char> alpha(static_cast<std::size_t>(kTextTextureWidth) * kTextTextureHeight, 0);
for (unsigned y = 0; y < kTextTextureHeight; ++y)
{
for (unsigned x = 0; x < kTextTextureWidth; ++x)
{
Gdiplus::Color pixel;
bitmap.GetPixel(x, y, &pixel);
BYTE luminance = pixel.GetRed();
if (pixel.GetGreen() > luminance)
luminance = pixel.GetGreen();
if (pixel.GetBlue() > luminance)
luminance = pixel.GetBlue();
alpha[static_cast<std::size_t>(y) * kTextTextureWidth + x] = static_cast<unsigned char>(luminance);
}
}
sdf = BuildTextSdfTexture(alpha, kTextTextureWidth, kTextTextureHeight);
return true;
}

View File

@@ -1,10 +0,0 @@
#pragma once
#include <filesystem>
#include <string>
#include <vector>
constexpr unsigned kTextTextureWidth = 4096;
constexpr unsigned kTextTextureHeight = 512;
bool RasterizeTextSdf(const std::string& text, const std::filesystem::path& fontPath, std::vector<unsigned char>& sdf, std::string& error);

View File

@@ -1,222 +0,0 @@
#include "TextureAssetLoader.h"
#include <windows.h>
#include <wincodec.h>
#include <atlbase.h>
#include <algorithm>
#include <cctype>
#include <cstring>
#include <fstream>
#include <sstream>
#include <string>
#include <vector>
#ifndef GL_RGBA32F
#define GL_RGBA32F 0x8814
#endif
namespace
{
std::string LowercaseExtension(const std::filesystem::path& path)
{
std::string extension = path.extension().string();
std::transform(extension.begin(), extension.end(), extension.begin(),
[](unsigned char value) { return static_cast<char>(std::tolower(value)); });
return extension;
}
bool LoadCubeTextureAsset(const ShaderTextureAsset& textureAsset, GLuint& textureId, std::string& error)
{
std::ifstream file(textureAsset.path);
if (!file)
{
error = "Could not open shader LUT asset: " + textureAsset.path.string();
return false;
}
unsigned lutSize = 0;
std::vector<float> values;
std::string line;
while (std::getline(file, line))
{
const std::size_t commentStart = line.find('#');
if (commentStart != std::string::npos)
line.resize(commentStart);
std::istringstream stream(line);
std::string firstToken;
if (!(stream >> firstToken))
continue;
if (firstToken == "TITLE" || firstToken == "DOMAIN_MIN" || firstToken == "DOMAIN_MAX")
continue;
if (firstToken == "LUT_3D_SIZE")
{
stream >> lutSize;
continue;
}
if (firstToken == "LUT_1D_SIZE")
{
error = "Only 3D .cube LUT assets are supported: " + textureAsset.path.string();
return false;
}
float red = 0.0f;
float green = 0.0f;
float blue = 0.0f;
try
{
red = std::stof(firstToken);
}
catch (...)
{
error = "Unsupported .cube directive in shader LUT asset: " + firstToken;
return false;
}
if (!(stream >> green >> blue))
{
error = "Malformed RGB entry in shader LUT asset: " + textureAsset.path.string();
return false;
}
values.push_back(red);
values.push_back(green);
values.push_back(blue);
values.push_back(1.0f);
}
if (lutSize == 0)
{
error = "Shader LUT asset is missing LUT_3D_SIZE: " + textureAsset.path.string();
return false;
}
const std::size_t expectedFloats = static_cast<std::size_t>(lutSize) * lutSize * lutSize * 4;
if (values.size() != expectedFloats)
{
error = "Shader LUT asset entry count does not match LUT_3D_SIZE: " + textureAsset.path.string();
return false;
}
const GLsizei atlasWidth = static_cast<GLsizei>(lutSize * lutSize);
const GLsizei atlasHeight = static_cast<GLsizei>(lutSize);
glGenTextures(1, &textureId);
glBindTexture(GL_TEXTURE_2D, textureId);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
glPixelStorei(GL_UNPACK_ALIGNMENT, 4);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA32F, atlasWidth, atlasHeight, 0, GL_RGBA, GL_FLOAT, values.data());
glBindTexture(GL_TEXTURE_2D, 0);
return true;
}
}
bool LoadTextureAsset(const ShaderTextureAsset& textureAsset, GLuint& textureId, std::string& error)
{
textureId = 0;
if (LowercaseExtension(textureAsset.path) == ".cube")
return LoadCubeTextureAsset(textureAsset, textureId, error);
HRESULT comInitResult = CoInitializeEx(NULL, COINIT_MULTITHREADED);
const bool shouldUninitializeCom = (comInitResult == S_OK || comInitResult == S_FALSE);
if (FAILED(comInitResult) && comInitResult != RPC_E_CHANGED_MODE)
{
error = "Could not initialize COM to load shader texture assets.";
return false;
}
CComPtr<IWICImagingFactory> imagingFactory;
HRESULT result = CoCreateInstance(CLSID_WICImagingFactory, NULL, CLSCTX_INPROC_SERVER, IID_PPV_ARGS(&imagingFactory));
if (FAILED(result) || !imagingFactory)
{
if (shouldUninitializeCom)
CoUninitialize();
error = "Could not create a WIC imaging factory to load shader texture assets.";
return false;
}
CComPtr<IWICBitmapDecoder> bitmapDecoder;
result = imagingFactory->CreateDecoderFromFilename(textureAsset.path.wstring().c_str(), NULL, GENERIC_READ, WICDecodeMetadataCacheOnLoad, &bitmapDecoder);
if (FAILED(result) || !bitmapDecoder)
{
if (shouldUninitializeCom)
CoUninitialize();
error = "Could not open shader texture asset: " + textureAsset.path.string();
return false;
}
CComPtr<IWICBitmapFrameDecode> bitmapFrame;
result = bitmapDecoder->GetFrame(0, &bitmapFrame);
if (FAILED(result) || !bitmapFrame)
{
if (shouldUninitializeCom)
CoUninitialize();
error = "Could not decode the first frame of shader texture asset: " + textureAsset.path.string();
return false;
}
CComPtr<IWICFormatConverter> formatConverter;
result = imagingFactory->CreateFormatConverter(&formatConverter);
if (FAILED(result) || !formatConverter)
{
if (shouldUninitializeCom)
CoUninitialize();
error = "Could not create a WIC format converter for shader texture asset: " + textureAsset.path.string();
return false;
}
result = formatConverter->Initialize(bitmapFrame, GUID_WICPixelFormat32bppBGRA, WICBitmapDitherTypeNone, NULL, 0.0, WICBitmapPaletteTypeCustom);
if (FAILED(result))
{
if (shouldUninitializeCom)
CoUninitialize();
error = "Could not convert shader texture asset to BGRA: " + textureAsset.path.string();
return false;
}
UINT width = 0;
UINT height = 0;
result = formatConverter->GetSize(&width, &height);
if (FAILED(result) || width == 0 || height == 0)
{
if (shouldUninitializeCom)
CoUninitialize();
error = "Shader texture asset has an invalid size: " + textureAsset.path.string();
return false;
}
const UINT stride = width * 4;
std::vector<unsigned char> pixels(static_cast<std::size_t>(stride) * static_cast<std::size_t>(height));
result = formatConverter->CopyPixels(NULL, stride, static_cast<UINT>(pixels.size()), pixels.data());
if (FAILED(result))
{
if (shouldUninitializeCom)
CoUninitialize();
error = "Could not read shader texture pixels: " + textureAsset.path.string();
return false;
}
std::vector<unsigned char> flippedPixels(pixels.size());
for (UINT row = 0; row < height; ++row)
{
const std::size_t srcOffset = static_cast<std::size_t>(row) * stride;
const std::size_t dstOffset = static_cast<std::size_t>(height - 1 - row) * stride;
std::memcpy(flippedPixels.data() + dstOffset, pixels.data() + srcOffset, stride);
}
glGenTextures(1, &textureId);
glBindTexture(GL_TEXTURE_2D, textureId);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, static_cast<GLsizei>(width), static_cast<GLsizei>(height), 0, GL_BGRA, GL_UNSIGNED_BYTE, flippedPixels.data());
glBindTexture(GL_TEXTURE_2D, 0);
if (shouldUninitializeCom)
CoUninitialize();
return true;
}

View File

@@ -1,11 +0,0 @@
#pragma once
#include "GLExtensions.h"
#include "ShaderTypes.h"
#include <windows.h>
#include <gl/gl.h>
#include <string>
bool LoadTextureAsset(const ShaderTextureAsset& textureAsset, GLuint& textureId, std::string& error);

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

View File

@@ -1,45 +0,0 @@
#include "RuntimeClock.h"
#include <chrono>
namespace
{
bool ToUtcTime(std::time_t time, std::tm& utcTime)
{
return gmtime_s(&utcTime, &time) == 0;
}
bool ToLocalTime(std::time_t time, std::tm& localTime)
{
return localtime_s(&localTime, &time) == 0;
}
}
RuntimeClockSnapshot GetRuntimeClockSnapshot()
{
return MakeRuntimeClockSnapshot(std::chrono::system_clock::to_time_t(std::chrono::system_clock::now()));
}
RuntimeClockSnapshot MakeRuntimeClockSnapshot(std::time_t now)
{
RuntimeClockSnapshot snapshot;
std::tm utcTime = {};
if (!ToUtcTime(now, utcTime))
return snapshot;
snapshot.utcTimeSeconds =
static_cast<double>(utcTime.tm_hour * 3600 + utcTime.tm_min * 60 + utcTime.tm_sec);
std::tm localTime = {};
if (!ToLocalTime(now, localTime))
return snapshot;
utcTime.tm_isdst = localTime.tm_isdst;
const std::time_t localAsTime = std::mktime(&localTime);
const std::time_t utcAsLocalTime = std::mktime(&utcTime);
if (localAsTime != static_cast<std::time_t>(-1) && utcAsLocalTime != static_cast<std::time_t>(-1))
snapshot.utcOffsetSeconds = std::difftime(localAsTime, utcAsLocalTime);
return snapshot;
}

View File

@@ -1,12 +0,0 @@
#pragma once
#include <ctime>
struct RuntimeClockSnapshot
{
double utcTimeSeconds = 0.0;
double utcOffsetSeconds = 0.0;
};
RuntimeClockSnapshot GetRuntimeClockSnapshot();
RuntimeClockSnapshot MakeRuntimeClockSnapshot(std::time_t now);

File diff suppressed because it is too large Load Diff

View File

@@ -1,189 +0,0 @@
#pragma once
#include "RuntimeJson.h"
#include "ShaderTypes.h"
#include <atomic>
#include <chrono>
#include <filesystem>
#include <map>
#include <mutex>
#include <string>
#include <utility>
#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);
bool TrySetSignalStatus(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 SetVideoIOStatus(const std::string& backendName, const std::string& modelName, bool supportsInternalKeying, bool supportsExternalKeying,
bool keyerInterfaceAvailable, bool externalKeyingRequested, bool externalKeyingActive, const std::string& statusMessage);
void SetPerformanceStats(double frameBudgetMilliseconds, double renderMilliseconds);
bool TrySetPerformanceStats(double frameBudgetMilliseconds, double renderMilliseconds);
void SetFramePacingStats(double completionIntervalMilliseconds, double smoothedCompletionIntervalMilliseconds,
double maxCompletionIntervalMilliseconds, uint64_t lateFrameCount, uint64_t droppedFrameCount, uint64_t flushedFrameCount);
bool TrySetFramePacingStats(double completionIntervalMilliseconds, double smoothedCompletionIntervalMilliseconds,
double maxCompletionIntervalMilliseconds, uint64_t lateFrameCount, uint64_t droppedFrameCount, uint64_t flushedFrameCount);
void AdvanceFrame();
bool TryAdvanceFrame();
bool BuildLayerPassFragmentShaderSources(const std::string& layerId, std::vector<ShaderPassBuildSource>& passSources, std::string& error);
std::vector<RuntimeRenderState> GetLayerRenderStates(unsigned outputWidth, unsigned outputHeight) const;
bool TryGetLayerRenderStates(unsigned outputWidth, unsigned outputHeight, std::vector<RuntimeRenderState>& states) const;
void RefreshDynamicRenderStateFields(std::vector<RuntimeRenderState>& states) const;
std::string BuildStateJson() const;
uint64_t GetRenderStateVersion() const { return mRenderStateVersion.load(std::memory_order_relaxed); }
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; }
unsigned GetPreviewFps() const { return mConfig.previewFps; }
bool ExternalKeyingEnabled() const { return mConfig.enableExternalKeying; }
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;
unsigned previewFps = 30;
bool enableExternalKeying = false;
std::string inputVideoFormat = "1080p";
std::string inputFrameRate = "59.94";
std::string outputVideoFormat = "1080p";
std::string outputFrameRate = "59.94";
};
struct DeckLinkOutputStatus
{
std::string backendName = "decklink";
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 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);
void BuildLayerRenderStatesLocked(unsigned outputWidth, unsigned outputHeight, std::vector<RuntimeRenderState>& states) const;
JsonValue BuildStateValue() const;
JsonValue SerializeLayerStackLocked() const;
bool DeserializeLayerStackLocked(const JsonValue& layersValue, std::vector<LayerPersistentState>& layers, std::string& error);
void NormalizePersistentLayerIdsLocked();
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();
void SetSignalStatusLocked(bool hasSignal, unsigned width, unsigned height, const std::string& modeName);
void MarkRenderStateDirtyLocked();
void SetPerformanceStatsLocked(double frameBudgetMilliseconds, double renderMilliseconds);
void SetFramePacingStatsLocked(double completionIntervalMilliseconds, double smoothedCompletionIntervalMilliseconds,
double maxCompletionIntervalMilliseconds, uint64_t lateFrameCount, uint64_t droppedFrameCount, uint64_t flushedFrameCount);
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;
std::vector<ShaderPackageStatus> mPackageStatuses;
bool mReloadRequested;
bool mCompileSucceeded;
std::string mCompileMessage;
bool mHasSignal;
unsigned mSignalWidth;
unsigned mSignalHeight;
std::string mSignalModeName;
double mFrameBudgetMilliseconds;
double mRenderMilliseconds;
double mSmoothedRenderMilliseconds;
double mCompletionIntervalMilliseconds;
double mSmoothedCompletionIntervalMilliseconds;
double mMaxCompletionIntervalMilliseconds;
double mStartupRandom;
uint64_t mLateFrameCount;
uint64_t mDroppedFrameCount;
uint64_t mFlushedFrameCount;
DeckLinkOutputStatus mDeckLinkOutputStatus;
unsigned short mServerPort;
bool mAutoReloadEnabled;
std::chrono::steady_clock::time_point mStartTime;
std::chrono::steady_clock::time_point mLastScanTime;
std::atomic<uint64_t> mFrameCounter{ 0 };
std::atomic<uint64_t> mRenderStateVersion{ 0 };
uint64_t mNextLayerId;
};

View File

@@ -1,777 +0,0 @@
#include "stdafx.h"
#include "ShaderPackageRegistry.h"
#include "RuntimeJson.h"
#include <algorithm>
#include <cmath>
#include <cctype>
#include <fstream>
#include <sstream>
namespace
{
std::string Trim(const std::string& text)
{
std::size_t start = 0;
while (start < text.size() && std::isspace(static_cast<unsigned char>(text[start])))
++start;
std::size_t end = text.size();
while (end > start && std::isspace(static_cast<unsigned char>(text[end - 1])))
--end;
return text.substr(start, end - start);
}
bool IsFiniteNumber(double value)
{
return std::isfinite(value) != 0;
}
bool ParseShaderParameterType(const std::string& typeName, ShaderParameterType& type)
{
if (typeName == "float")
{
type = ShaderParameterType::Float;
return true;
}
if (typeName == "vec2")
{
type = ShaderParameterType::Vec2;
return true;
}
if (typeName == "color")
{
type = ShaderParameterType::Color;
return true;
}
if (typeName == "bool")
{
type = ShaderParameterType::Boolean;
return true;
}
if (typeName == "enum")
{
type = ShaderParameterType::Enum;
return true;
}
if (typeName == "text")
{
type = ShaderParameterType::Text;
return true;
}
if (typeName == "trigger")
{
type = ShaderParameterType::Trigger;
return true;
}
return false;
}
bool ParseTemporalHistorySource(const std::string& sourceName, TemporalHistorySource& source)
{
if (sourceName == "source")
{
source = TemporalHistorySource::Source;
return true;
}
if (sourceName == "preLayerInput")
{
source = TemporalHistorySource::PreLayerInput;
return true;
}
if (sourceName == "none")
{
source = TemporalHistorySource::None;
return true;
}
return false;
}
std::string ReadTextFile(const std::filesystem::path& path, std::string& error)
{
std::ifstream input(path, std::ios::binary);
if (!input)
{
error = "Could not open file: " + path.string();
return std::string();
}
std::ostringstream buffer;
buffer << input.rdbuf();
return buffer.str();
}
std::string ManifestPathMessage(const std::filesystem::path& manifestPath)
{
return manifestPath.string();
}
bool RequireStringField(const JsonValue& object, const char* fieldName, std::string& value, const std::filesystem::path& manifestPath, std::string& error)
{
const JsonValue* fieldValue = object.find(fieldName);
if (!fieldValue || !fieldValue->isString())
{
error = "Shader manifest is missing required string '" + std::string(fieldName) + "' field: " + ManifestPathMessage(manifestPath);
return false;
}
value = fieldValue->asString();
return true;
}
bool RequireNonEmptyStringField(const JsonValue& object, const char* fieldName, std::string& value, const std::filesystem::path& manifestPath, std::string& error)
{
if (!RequireStringField(object, fieldName, value, manifestPath, error))
return false;
if (Trim(value).empty())
{
error = "Shader manifest string '" + std::string(fieldName) + "' must not be empty: " + ManifestPathMessage(manifestPath);
return false;
}
return true;
}
bool OptionalStringField(const JsonValue& object, const char* fieldName, std::string& value, const std::string& fallback, const std::filesystem::path& manifestPath, std::string& error)
{
const JsonValue* fieldValue = object.find(fieldName);
if (!fieldValue)
{
value = fallback;
return true;
}
if (!fieldValue->isString())
{
error = "Shader manifest field '" + std::string(fieldName) + "' must be a string in: " + ManifestPathMessage(manifestPath);
return false;
}
value = fieldValue->asString();
return true;
}
bool OptionalArrayField(const JsonValue& object, const char* fieldName, const JsonValue*& value, const std::filesystem::path& manifestPath, std::string& error)
{
value = object.find(fieldName);
if (!value)
return true;
if (!value->isArray())
{
error = "Shader manifest '" + std::string(fieldName) + "' field must be an array in: " + ManifestPathMessage(manifestPath);
return false;
}
return true;
}
bool OptionalObjectField(const JsonValue& object, const char* fieldName, const JsonValue*& value, const std::filesystem::path& manifestPath, std::string& error)
{
value = object.find(fieldName);
if (!value)
return true;
if (!value->isObject())
{
error = "Shader manifest '" + std::string(fieldName) + "' field must be an object in: " + ManifestPathMessage(manifestPath);
return false;
}
return true;
}
bool NumberListFromJsonValue(const JsonValue& value, std::vector<double>& numbers, const std::string& fieldName, const std::filesystem::path& manifestPath, std::string& error)
{
if (value.isNumber())
{
numbers.push_back(value.asNumber());
return true;
}
if (value.isArray())
{
numbers = JsonArrayToNumbers(value);
if (numbers.size() != value.asArray().size())
{
error = "Shader parameter field '" + fieldName + "' must contain only numbers in: " + ManifestPathMessage(manifestPath);
return false;
}
return true;
}
error = "Shader parameter field '" + fieldName + "' must be a number or array of numbers in: " + ManifestPathMessage(manifestPath);
return false;
}
bool ValidateShaderIdentifier(const std::string& identifier, const std::string& fieldName, const std::filesystem::path& manifestPath, std::string& error)
{
if (identifier.empty() || !(std::isalpha(static_cast<unsigned char>(identifier.front())) || identifier.front() == '_'))
{
error = "Shader manifest field '" + fieldName + "' must be a valid shader identifier in: " + ManifestPathMessage(manifestPath);
return false;
}
for (char ch : identifier)
{
const unsigned char unsignedCh = static_cast<unsigned char>(ch);
if (!(std::isalnum(unsignedCh) || ch == '_'))
{
error = "Shader manifest field '" + fieldName + "' must be a valid shader identifier in: " + ManifestPathMessage(manifestPath);
return false;
}
}
return true;
}
bool ParseShaderMetadata(const JsonValue& manifestJson, ShaderPackage& shaderPackage, const std::filesystem::path& manifestPath, std::string& error)
{
if (!RequireStringField(manifestJson, "id", shaderPackage.id, manifestPath, error) ||
!RequireStringField(manifestJson, "name", shaderPackage.displayName, manifestPath, error) ||
!OptionalStringField(manifestJson, "description", shaderPackage.description, "", manifestPath, error) ||
!OptionalStringField(manifestJson, "category", shaderPackage.category, "", manifestPath, error) ||
!OptionalStringField(manifestJson, "entryPoint", shaderPackage.entryPoint, "shadeVideo", manifestPath, error))
{
return false;
}
if (!ValidateShaderIdentifier(shaderPackage.entryPoint, "entryPoint", manifestPath, error))
return false;
shaderPackage.directoryPath = manifestPath.parent_path();
shaderPackage.shaderPath = shaderPackage.directoryPath / "shader.slang";
shaderPackage.manifestPath = manifestPath;
return true;
}
bool ParsePassDefinitions(const JsonValue& manifestJson, ShaderPackage& shaderPackage, const std::filesystem::path& manifestPath, std::string& error)
{
const JsonValue* passesValue = nullptr;
if (!OptionalArrayField(manifestJson, "passes", passesValue, manifestPath, error))
return false;
if (!passesValue)
{
// Existing shader packages are treated as a single implicit pass, so
// multipass support does not require manifest churn.
ShaderPassDefinition pass;
pass.id = "main";
pass.entryPoint = shaderPackage.entryPoint;
pass.sourcePath = shaderPackage.shaderPath;
pass.outputName = "layerOutput";
if (!std::filesystem::exists(pass.sourcePath))
{
error = "Shader source not found for package " + shaderPackage.id + ": " + pass.sourcePath.string();
return false;
}
pass.sourceWriteTime = std::filesystem::last_write_time(pass.sourcePath);
shaderPackage.passes.push_back(pass);
return true;
}
if (passesValue->asArray().empty())
{
error = "Shader manifest 'passes' field must not be empty in: " + ManifestPathMessage(manifestPath);
return false;
}
for (const JsonValue& passJson : passesValue->asArray())
{
if (!passJson.isObject())
{
error = "Shader pass entry must be an object in: " + ManifestPathMessage(manifestPath);
return false;
}
std::string passId;
std::string sourcePath;
if (!RequireNonEmptyStringField(passJson, "id", passId, manifestPath, error) ||
!RequireNonEmptyStringField(passJson, "source", sourcePath, manifestPath, error))
{
error = "Shader pass is missing required 'id' or 'source' in: " + ManifestPathMessage(manifestPath);
return false;
}
if (!ValidateShaderIdentifier(passId, "passes[].id", manifestPath, error))
return false;
for (const ShaderPassDefinition& existingPass : shaderPackage.passes)
{
if (existingPass.id == passId)
{
error = "Duplicate shader pass id '" + passId + "' in: " + ManifestPathMessage(manifestPath);
return false;
}
}
ShaderPassDefinition pass;
pass.id = passId;
pass.sourcePath = shaderPackage.directoryPath / sourcePath;
if (!OptionalStringField(passJson, "entryPoint", pass.entryPoint, shaderPackage.entryPoint, manifestPath, error) ||
!OptionalStringField(passJson, "output", pass.outputName, passId, manifestPath, error))
{
return false;
}
if (!ValidateShaderIdentifier(pass.entryPoint, "passes[].entryPoint", manifestPath, error))
return false;
const JsonValue* inputsValue = nullptr;
if (!OptionalArrayField(passJson, "inputs", inputsValue, manifestPath, error))
return false;
if (inputsValue)
{
for (const JsonValue& inputValue : inputsValue->asArray())
{
if (!inputValue.isString())
{
error = "Shader pass inputs must be strings in: " + ManifestPathMessage(manifestPath);
return false;
}
pass.inputNames.push_back(inputValue.asString());
}
}
// Keep source validation in the registry. Bad pass declarations then
// appear as unavailable shaders instead of failing at render time.
if (!std::filesystem::exists(pass.sourcePath))
{
error = "Shader pass source not found for package " + shaderPackage.id + ": " + pass.sourcePath.string();
return false;
}
pass.sourceWriteTime = std::filesystem::last_write_time(pass.sourcePath);
shaderPackage.passes.push_back(pass);
}
shaderPackage.shaderPath = shaderPackage.passes.front().sourcePath;
return true;
}
bool ParseTextureAssets(const JsonValue& manifestJson, ShaderPackage& shaderPackage, const std::filesystem::path& manifestPath, std::string& error)
{
const JsonValue* texturesValue = nullptr;
if (!OptionalArrayField(manifestJson, "textures", texturesValue, manifestPath, error))
return false;
if (!texturesValue)
return true;
for (const JsonValue& textureJson : texturesValue->asArray())
{
if (!textureJson.isObject())
{
error = "Shader texture entry must be an object in: " + ManifestPathMessage(manifestPath);
return false;
}
std::string textureId;
std::string texturePath;
if (!RequireNonEmptyStringField(textureJson, "id", textureId, manifestPath, error) ||
!RequireNonEmptyStringField(textureJson, "path", texturePath, manifestPath, error))
{
error = "Shader texture is missing required 'id' or 'path' in: " + ManifestPathMessage(manifestPath);
return false;
}
if (!ValidateShaderIdentifier(textureId, "textures[].id", manifestPath, error))
return false;
ShaderTextureAsset textureAsset;
textureAsset.id = textureId;
textureAsset.path = shaderPackage.directoryPath / texturePath;
if (!std::filesystem::exists(textureAsset.path))
{
error = "Shader texture asset not found for package " + shaderPackage.id + ": " + textureAsset.path.string();
return false;
}
textureAsset.writeTime = std::filesystem::last_write_time(textureAsset.path);
shaderPackage.textureAssets.push_back(textureAsset);
}
return true;
}
bool ParseFontAssets(const JsonValue& manifestJson, ShaderPackage& shaderPackage, const std::filesystem::path& manifestPath, std::string& error)
{
const JsonValue* fontsValue = nullptr;
if (!OptionalArrayField(manifestJson, "fonts", fontsValue, manifestPath, error))
return false;
if (!fontsValue)
return true;
for (const JsonValue& fontJson : fontsValue->asArray())
{
if (!fontJson.isObject())
{
error = "Shader font entry must be an object in: " + ManifestPathMessage(manifestPath);
return false;
}
std::string fontId;
std::string fontPath;
if (!RequireNonEmptyStringField(fontJson, "id", fontId, manifestPath, error) ||
!RequireNonEmptyStringField(fontJson, "path", fontPath, manifestPath, error))
{
error = "Shader font is missing required 'id' or 'path' in: " + ManifestPathMessage(manifestPath);
return false;
}
if (!ValidateShaderIdentifier(fontId, "fonts[].id", manifestPath, error))
return false;
ShaderFontAsset fontAsset;
fontAsset.id = fontId;
fontAsset.path = shaderPackage.directoryPath / fontPath;
if (!std::filesystem::exists(fontAsset.path))
{
error = "Shader font asset not found for package " + shaderPackage.id + ": " + fontAsset.path.string();
return false;
}
fontAsset.writeTime = std::filesystem::last_write_time(fontAsset.path);
shaderPackage.fontAssets.push_back(fontAsset);
}
return true;
}
bool ParseTemporalSettings(const JsonValue& manifestJson, ShaderPackage& shaderPackage, unsigned maxTemporalHistoryFrames, const std::filesystem::path& manifestPath, std::string& error)
{
const JsonValue* temporalValue = nullptr;
if (!OptionalObjectField(manifestJson, "temporal", temporalValue, manifestPath, error))
return false;
if (!temporalValue)
return true;
const JsonValue* enabledValue = temporalValue->find("enabled");
if (!enabledValue || !enabledValue->asBoolean(false))
return true;
std::string historySourceName;
if (!RequireNonEmptyStringField(*temporalValue, "historySource", historySourceName, manifestPath, error))
{
error = "Temporal shader is missing required 'historySource' in: " + ManifestPathMessage(manifestPath);
return false;
}
const JsonValue* historyLengthValue = temporalValue->find("historyLength");
if (!historyLengthValue || !historyLengthValue->isNumber())
{
error = "Temporal shader is missing required numeric 'historyLength' in: " + ManifestPathMessage(manifestPath);
return false;
}
TemporalHistorySource historySource = TemporalHistorySource::None;
if (!ParseTemporalHistorySource(historySourceName, historySource))
{
error = "Unsupported temporal historySource '" + historySourceName + "' in: " + ManifestPathMessage(manifestPath);
return false;
}
const double requestedHistoryLength = historyLengthValue->asNumber();
if (!IsFiniteNumber(requestedHistoryLength) || requestedHistoryLength <= 0.0 || std::floor(requestedHistoryLength) != requestedHistoryLength)
{
error = "Temporal shader 'historyLength' must be a positive integer in: " + ManifestPathMessage(manifestPath);
return false;
}
shaderPackage.temporal.enabled = true;
shaderPackage.temporal.historySource = historySource;
shaderPackage.temporal.requestedHistoryLength = static_cast<unsigned>(requestedHistoryLength);
shaderPackage.temporal.effectiveHistoryLength = std::min(shaderPackage.temporal.requestedHistoryLength, maxTemporalHistoryFrames);
return true;
}
bool ParseParameterNumberField(const JsonValue& parameterJson, const char* fieldName, std::vector<double>& values, const std::filesystem::path& manifestPath, std::string& error)
{
if (const JsonValue* fieldValue = parameterJson.find(fieldName))
return NumberListFromJsonValue(*fieldValue, values, fieldName, manifestPath, error);
return true;
}
bool ParseParameterDefault(const JsonValue& parameterJson, ShaderParameterDefinition& definition, const std::filesystem::path& manifestPath, std::string& error)
{
const JsonValue* defaultValue = parameterJson.find("default");
if (!defaultValue)
return true;
if (definition.type == ShaderParameterType::Boolean)
{
if (!defaultValue->isBoolean())
{
error = "Boolean parameter default must be a boolean for: " + definition.id;
return false;
}
definition.defaultBoolean = defaultValue->asBoolean(false);
return true;
}
if (definition.type == ShaderParameterType::Enum)
{
if (!defaultValue->isString())
{
error = "Enum parameter default must be a string for: " + definition.id;
return false;
}
definition.defaultEnumValue = defaultValue->asString();
return true;
}
if (definition.type == ShaderParameterType::Text)
{
if (!defaultValue->isString())
{
error = "Text parameter default must be a string for: " + definition.id;
return false;
}
definition.defaultTextValue = defaultValue->asString();
return true;
}
return NumberListFromJsonValue(*defaultValue, definition.defaultNumbers, "default", manifestPath, error);
}
bool ParseParameterOptions(const JsonValue& parameterJson, ShaderParameterDefinition& definition, const std::filesystem::path& manifestPath, std::string& error)
{
const JsonValue* optionsValue = nullptr;
if (!OptionalArrayField(parameterJson, "options", optionsValue, manifestPath, error) || !optionsValue)
{
error = "Enum parameter is missing 'options' in: " + ManifestPathMessage(manifestPath);
return false;
}
for (const JsonValue& optionJson : optionsValue->asArray())
{
if (!optionJson.isObject())
{
error = "Enum parameter option must be an object in: " + ManifestPathMessage(manifestPath);
return false;
}
ShaderParameterOption option;
if (!RequireStringField(optionJson, "value", option.value, manifestPath, error) ||
!RequireStringField(optionJson, "label", option.label, manifestPath, error))
{
error = "Enum parameter option is missing 'value' or 'label' in: " + ManifestPathMessage(manifestPath);
return false;
}
definition.enumOptions.push_back(option);
}
bool defaultFound = definition.defaultEnumValue.empty();
for (const ShaderParameterOption& option : definition.enumOptions)
{
if (option.value == definition.defaultEnumValue)
{
defaultFound = true;
break;
}
}
if (!defaultFound)
{
error = "Enum parameter default is not present in its option list for: " + definition.id;
return false;
}
return true;
}
bool ParseParameterDefinition(const JsonValue& parameterJson, ShaderParameterDefinition& definition, const std::filesystem::path& manifestPath, std::string& error)
{
if (!parameterJson.isObject())
{
error = "Shader parameter entry must be an object in: " + ManifestPathMessage(manifestPath);
return false;
}
std::string typeName;
if (!RequireStringField(parameterJson, "id", definition.id, manifestPath, error) ||
!RequireStringField(parameterJson, "label", definition.label, manifestPath, error) ||
!RequireStringField(parameterJson, "type", typeName, manifestPath, error))
{
error = "Shader parameter is missing required fields in: " + ManifestPathMessage(manifestPath);
return false;
}
if (!ParseShaderParameterType(typeName, definition.type))
{
error = "Unsupported parameter type '" + typeName + "' in: " + ManifestPathMessage(manifestPath);
return false;
}
if (!ValidateShaderIdentifier(definition.id, "parameters[].id", manifestPath, error))
return false;
if (!OptionalStringField(parameterJson, "description", definition.description, "", manifestPath, error))
return false;
if (!ParseParameterDefault(parameterJson, definition, manifestPath, error) ||
!ParseParameterNumberField(parameterJson, "min", definition.minNumbers, manifestPath, error) ||
!ParseParameterNumberField(parameterJson, "max", definition.maxNumbers, manifestPath, error) ||
!ParseParameterNumberField(parameterJson, "step", definition.stepNumbers, manifestPath, error))
{
return false;
}
if (definition.type == ShaderParameterType::Text)
{
if (const JsonValue* fontValue = parameterJson.find("font"))
{
if (!fontValue->isString())
{
error = "Text parameter 'font' must be a string for: " + definition.id;
return false;
}
definition.fontId = fontValue->asString();
if (!definition.fontId.empty() && !ValidateShaderIdentifier(definition.fontId, "parameters[].font", manifestPath, error))
return false;
}
if (const JsonValue* maxLengthValue = parameterJson.find("maxLength"))
{
if (!maxLengthValue->isNumber() || maxLengthValue->asNumber() < 1.0 || maxLengthValue->asNumber() > 256.0)
{
error = "Text parameter 'maxLength' must be a number from 1 to 256 for: " + definition.id;
return false;
}
definition.maxLength = static_cast<unsigned>(maxLengthValue->asNumber());
}
}
if (definition.type == ShaderParameterType::Enum)
return ParseParameterOptions(parameterJson, definition, manifestPath, error);
return true;
}
bool ParseParameterDefinitions(const JsonValue& manifestJson, ShaderPackage& shaderPackage, const std::filesystem::path& manifestPath, std::string& error)
{
const JsonValue* parametersValue = nullptr;
if (!OptionalArrayField(manifestJson, "parameters", parametersValue, manifestPath, error))
return false;
if (!parametersValue)
return true;
for (const JsonValue& parameterJson : parametersValue->asArray())
{
ShaderParameterDefinition definition;
if (!ParseParameterDefinition(parameterJson, definition, manifestPath, error))
return false;
shaderPackage.parameters.push_back(definition);
}
return true;
}
std::string UniqueUnavailableShaderId(const std::filesystem::path& manifestPath, const std::string& parsedId)
{
const std::string fallbackId = manifestPath.parent_path().filename().string();
const std::string baseId = parsedId.empty() ? fallbackId : parsedId;
return baseId + "@invalid:" + fallbackId;
}
ShaderPackageStatus BuildUnavailableStatus(const std::filesystem::path& manifestPath, const ShaderPackage& partialPackage, const std::string& packageError)
{
ShaderPackageStatus status;
status.id = UniqueUnavailableShaderId(manifestPath, partialPackage.id);
status.displayName = !partialPackage.displayName.empty() ? partialPackage.displayName : manifestPath.parent_path().filename().string();
status.description = partialPackage.description;
status.category = !partialPackage.category.empty() ? partialPackage.category : "Unavailable";
status.available = false;
status.error = packageError;
return status;
}
ShaderPackageStatus BuildAvailableStatus(const ShaderPackage& shaderPackage)
{
ShaderPackageStatus status;
status.id = shaderPackage.id;
status.displayName = shaderPackage.displayName;
status.description = shaderPackage.description;
status.category = shaderPackage.category;
status.available = true;
return status;
}
}
ShaderPackageRegistry::ShaderPackageRegistry(unsigned maxTemporalHistoryFrames)
: mMaxTemporalHistoryFrames(maxTemporalHistoryFrames)
{
}
bool ShaderPackageRegistry::Scan(
const std::filesystem::path& shaderRoot,
std::map<std::string, ShaderPackage>& packagesById,
std::vector<std::string>& packageOrder,
std::vector<ShaderPackageStatus>& packageStatuses,
std::string& error) const
{
packagesById.clear();
packageOrder.clear();
packageStatuses.clear();
if (!std::filesystem::exists(shaderRoot))
{
error = "Shader library directory does not exist: " + shaderRoot.string();
return false;
}
for (const auto& entry : std::filesystem::directory_iterator(shaderRoot))
{
if (!entry.is_directory())
continue;
std::filesystem::path manifestPath = entry.path() / "shader.json";
if (!std::filesystem::exists(manifestPath))
continue;
ShaderPackage shaderPackage;
if (!ParseManifest(manifestPath, shaderPackage, error))
{
packageStatuses.push_back(BuildUnavailableStatus(manifestPath, shaderPackage, error));
error.clear();
continue;
}
if (packagesById.find(shaderPackage.id) != packagesById.end())
{
packageStatuses.push_back(BuildUnavailableStatus(manifestPath, shaderPackage, "Duplicate shader id found: " + shaderPackage.id));
continue;
}
packageOrder.push_back(shaderPackage.id);
packageStatuses.push_back(BuildAvailableStatus(shaderPackage));
packagesById[shaderPackage.id] = shaderPackage;
}
std::sort(packageOrder.begin(), packageOrder.end());
std::sort(packageStatuses.begin(), packageStatuses.end(), [](const ShaderPackageStatus& left, const ShaderPackageStatus& right) {
return left.displayName < right.displayName;
});
return true;
}
bool ShaderPackageRegistry::ParseManifest(const std::filesystem::path& manifestPath, ShaderPackage& shaderPackage, std::string& error) const
{
const std::string manifestText = ReadTextFile(manifestPath, error);
if (manifestText.empty())
return false;
JsonValue manifestJson;
if (!ParseJson(manifestText, manifestJson, error))
return false;
if (!manifestJson.isObject())
{
error = "Shader manifest root must be an object: " + manifestPath.string();
return false;
}
if (!ParseShaderMetadata(manifestJson, shaderPackage, manifestPath, error))
return false;
if (!ParsePassDefinitions(manifestJson, shaderPackage, manifestPath, error))
return false;
shaderPackage.shaderWriteTime = shaderPackage.passes.front().sourceWriteTime;
for (const ShaderPassDefinition& pass : shaderPackage.passes)
{
if (pass.sourceWriteTime > shaderPackage.shaderWriteTime)
shaderPackage.shaderWriteTime = pass.sourceWriteTime;
}
shaderPackage.manifestWriteTime = std::filesystem::last_write_time(shaderPackage.manifestPath);
return ParseTextureAssets(manifestJson, shaderPackage, manifestPath, error) &&
ParseFontAssets(manifestJson, shaderPackage, manifestPath, error) &&
ParseTemporalSettings(manifestJson, shaderPackage, mMaxTemporalHistoryFrames, manifestPath, error) &&
ParseParameterDefinitions(manifestJson, shaderPackage, manifestPath, error);
}

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,137 +0,0 @@
#pragma once
#include "DeckLinkDisplayMode.h"
#include "VideoIOFormat.h"
#include <cstdint>
#include <functional>
#include <string>
enum class VideoIOBackend
{
DeckLink
};
enum class VideoIOCompletionResult
{
Completed,
DisplayedLate,
Dropped,
Flushed,
Unknown
};
struct VideoIOConfig
{
VideoFormatSelection videoModes;
bool externalKeyingEnabled = false;
bool preferTenBit = true;
};
struct VideoIOState
{
FrameSize inputFrameSize;
FrameSize outputFrameSize;
VideoIOPixelFormat inputPixelFormat = VideoIOPixelFormat::Uyvy8;
VideoIOPixelFormat outputPixelFormat = VideoIOPixelFormat::Bgra8;
unsigned inputFrameRowBytes = 0;
unsigned outputFrameRowBytes = 0;
unsigned captureTextureWidth = 0;
unsigned outputPackTextureWidth = 0;
std::string inputDisplayModeName = "1080p59.94";
std::string outputDisplayModeName = "1080p59.94";
std::string outputModelName;
std::string statusMessage;
std::string formatStatusMessage;
bool hasInputDevice = false;
bool hasInputSource = false;
bool supportsInternalKeying = false;
bool supportsExternalKeying = false;
bool keyerInterfaceAvailable = false;
bool externalKeyingActive = false;
double frameBudgetMilliseconds = 0.0;
};
struct VideoIOFrame
{
void* bytes = nullptr;
long rowBytes = 0;
unsigned width = 0;
unsigned height = 0;
VideoIOPixelFormat pixelFormat = VideoIOPixelFormat::Uyvy8;
bool hasNoInputSource = false;
};
struct VideoIOOutputFrame
{
void* bytes = nullptr;
long rowBytes = 0;
unsigned width = 0;
unsigned height = 0;
VideoIOPixelFormat pixelFormat = VideoIOPixelFormat::Bgra8;
void* nativeFrame = nullptr;
void* nativeBuffer = nullptr;
};
struct VideoIOCompletion
{
VideoIOCompletionResult result = VideoIOCompletionResult::Completed;
};
struct VideoIOScheduleTime
{
int64_t streamTime = 0;
int64_t duration = 0;
int64_t timeScale = 0;
uint64_t frameIndex = 0;
};
class VideoIODevice
{
public:
using InputFrameCallback = std::function<void(const VideoIOFrame&)>;
using OutputFrameCallback = std::function<void(const VideoIOCompletion&)>;
virtual ~VideoIODevice() = default;
virtual void ReleaseResources() = 0;
virtual bool DiscoverDevicesAndModes(const VideoFormatSelection& videoModes, std::string& error) = 0;
virtual bool SelectPreferredFormats(const VideoFormatSelection& videoModes, bool outputAlphaRequired, std::string& error) = 0;
virtual bool ConfigureInput(InputFrameCallback callback, const VideoFormat& inputVideoMode, std::string& error) = 0;
virtual bool ConfigureOutput(OutputFrameCallback callback, const VideoFormat& outputVideoMode, bool externalKeyingEnabled, std::string& error) = 0;
virtual bool Start() = 0;
virtual bool Stop() = 0;
virtual const VideoIOState& State() const = 0;
virtual VideoIOState& MutableState() = 0;
virtual bool BeginOutputFrame(VideoIOOutputFrame& frame) = 0;
virtual void EndOutputFrame(VideoIOOutputFrame& frame) = 0;
virtual bool ScheduleOutputFrame(const VideoIOOutputFrame& frame) = 0;
virtual void AccountForCompletionResult(VideoIOCompletionResult result) = 0;
bool HasInputDevice() const { return State().hasInputDevice; }
bool HasInputSource() const { return State().hasInputSource; }
bool InputOutputDimensionsDiffer() const { return State().inputFrameSize != State().outputFrameSize; }
const FrameSize& InputFrameSize() const { return State().inputFrameSize; }
const FrameSize& OutputFrameSize() const { return State().outputFrameSize; }
unsigned InputFrameWidth() const { return State().inputFrameSize.width; }
unsigned InputFrameHeight() const { return State().inputFrameSize.height; }
unsigned OutputFrameWidth() const { return State().outputFrameSize.width; }
unsigned OutputFrameHeight() const { return State().outputFrameSize.height; }
VideoIOPixelFormat InputPixelFormat() const { return State().inputPixelFormat; }
VideoIOPixelFormat OutputPixelFormat() const { return State().outputPixelFormat; }
bool InputIsTenBit() const { return VideoIOPixelFormatIsTenBit(State().inputPixelFormat); }
bool OutputIsTenBit() const { return VideoIOPixelFormatIsTenBit(State().outputPixelFormat); }
unsigned InputFrameRowBytes() const { return State().inputFrameRowBytes; }
unsigned OutputFrameRowBytes() const { return State().outputFrameRowBytes; }
unsigned CaptureTextureWidth() const { return State().captureTextureWidth; }
unsigned OutputPackTextureWidth() const { return State().outputPackTextureWidth; }
const std::string& FormatStatusMessage() const { return State().formatStatusMessage; }
const std::string& InputDisplayModeName() const { return State().inputDisplayModeName; }
const std::string& OutputModelName() const { return State().outputModelName; }
bool SupportsInternalKeying() const { return State().supportsInternalKeying; }
bool SupportsExternalKeying() const { return State().supportsExternalKeying; }
bool KeyerInterfaceAvailable() const { return State().keyerInterfaceAvailable; }
bool ExternalKeyingActive() const { return State().externalKeyingActive; }
const std::string& StatusMessage() const { return State().statusMessage; }
double FrameBudgetMilliseconds() const { return State().frameBudgetMilliseconds; }
void SetStatusMessage(const std::string& message) { MutableState().statusMessage = message; }
};

View File

@@ -1,37 +0,0 @@
#include "VideoPlayoutScheduler.h"
void VideoPlayoutScheduler::Configure(int64_t frameDuration, int64_t timeScale)
{
mFrameDuration = frameDuration;
mTimeScale = timeScale;
Reset();
}
void VideoPlayoutScheduler::Reset()
{
mScheduledFrameIndex = 0;
}
VideoIOScheduleTime VideoPlayoutScheduler::NextScheduleTime()
{
VideoIOScheduleTime time;
time.streamTime = static_cast<int64_t>(mScheduledFrameIndex) * mFrameDuration;
time.duration = mFrameDuration;
time.timeScale = mTimeScale;
time.frameIndex = mScheduledFrameIndex;
++mScheduledFrameIndex;
return time;
}
void VideoPlayoutScheduler::AccountForCompletionResult(VideoIOCompletionResult result)
{
if (result == VideoIOCompletionResult::DisplayedLate || result == VideoIOCompletionResult::Dropped)
mScheduledFrameIndex += 2;
}
double VideoPlayoutScheduler::FrameBudgetMilliseconds() const
{
return mTimeScale != 0
? (static_cast<double>(mFrameDuration) * 1000.0) / static_cast<double>(mTimeScale)
: 0.0;
}

View File

@@ -1,22 +0,0 @@
#pragma once
#include "VideoIOTypes.h"
#include <cstdint>
class VideoPlayoutScheduler
{
public:
void Configure(int64_t frameDuration, int64_t timeScale);
void Reset();
VideoIOScheduleTime NextScheduleTime();
void AccountForCompletionResult(VideoIOCompletionResult result);
double FrameBudgetMilliseconds() const;
uint64_t ScheduledFrameIndex() const { return mScheduledFrameIndex; }
int64_t TimeScale() const { return mTimeScale; }
private:
int64_t mFrameDuration = 0;
int64_t mTimeScale = 0;
uint64_t mScheduledFrameIndex = 0;
};

View File

@@ -1,146 +0,0 @@
#include "DeckLinkDisplayMode.h"
#include <cctype>
std::string NormalizeModeToken(const std::string& value)
{
std::string normalized;
for (unsigned char ch : value)
{
if (std::isalnum(ch))
normalized.push_back(static_cast<char>(std::tolower(ch)));
}
return normalized;
}
bool ResolveConfiguredDisplayMode(const std::string& videoFormat, const std::string& frameRate, BMDDisplayMode& displayMode, std::string& displayModeName)
{
VideoFormat videoMode;
if (!ResolveConfiguredVideoFormat(videoFormat, frameRate, videoMode))
return false;
displayMode = videoMode.displayMode;
displayModeName = videoMode.displayName;
return true;
}
bool ResolveConfiguredVideoFormat(const std::string& videoFormat, const std::string& frameRate, VideoFormat& videoMode)
{
const std::string formatToken = NormalizeModeToken(videoFormat);
const std::string frameToken = NormalizeModeToken(frameRate);
const std::string combinedToken = formatToken + frameToken;
struct ModeOption
{
const char* token;
BMDDisplayMode mode;
const char* displayName;
};
static const ModeOption options[] =
{
{ "720p50", bmdModeHD720p50, "720p50" },
{ "hd720p50", bmdModeHD720p50, "720p50" },
{ "720p5994", bmdModeHD720p5994, "720p59.94" },
{ "hd720p5994", bmdModeHD720p5994, "720p59.94" },
{ "720p60", bmdModeHD720p60, "720p60" },
{ "hd720p60", bmdModeHD720p60, "720p60" },
{ "1080i50", bmdModeHD1080i50, "1080i50" },
{ "hd1080i50", bmdModeHD1080i50, "1080i50" },
{ "1080i5994", bmdModeHD1080i5994, "1080i59.94" },
{ "hd1080i5994", bmdModeHD1080i5994, "1080i59.94" },
{ "1080i60", bmdModeHD1080i6000, "1080i60" },
{ "hd1080i60", bmdModeHD1080i6000, "1080i60" },
{ "1080p2398", bmdModeHD1080p2398, "1080p23.98" },
{ "hd1080p2398", bmdModeHD1080p2398, "1080p23.98" },
{ "1080p24", bmdModeHD1080p24, "1080p24" },
{ "hd1080p24", bmdModeHD1080p24, "1080p24" },
{ "1080p25", bmdModeHD1080p25, "1080p25" },
{ "hd1080p25", bmdModeHD1080p25, "1080p25" },
{ "1080p2997", bmdModeHD1080p2997, "1080p29.97" },
{ "hd1080p2997", bmdModeHD1080p2997, "1080p29.97" },
{ "1080p30", bmdModeHD1080p30, "1080p30" },
{ "hd1080p30", bmdModeHD1080p30, "1080p30" },
{ "1080p50", bmdModeHD1080p50, "1080p50" },
{ "hd1080p50", bmdModeHD1080p50, "1080p50" },
{ "1080p5994", bmdModeHD1080p5994, "1080p59.94" },
{ "hd1080p5994", bmdModeHD1080p5994, "1080p59.94" },
{ "1080p60", bmdModeHD1080p6000, "1080p60" },
{ "hd1080p60", bmdModeHD1080p6000, "1080p60" },
{ "2160p2398", bmdMode4K2160p2398, "2160p23.98" },
{ "4k2160p2398", bmdMode4K2160p2398, "2160p23.98" },
{ "2160p24", bmdMode4K2160p24, "2160p24" },
{ "4k2160p24", bmdMode4K2160p24, "2160p24" },
{ "2160p25", bmdMode4K2160p25, "2160p25" },
{ "4k2160p25", bmdMode4K2160p25, "2160p25" },
{ "2160p2997", bmdMode4K2160p2997, "2160p29.97" },
{ "4k2160p2997", bmdMode4K2160p2997, "2160p29.97" },
{ "2160p30", bmdMode4K2160p30, "2160p30" },
{ "4k2160p30", bmdMode4K2160p30, "2160p30" },
{ "2160p50", bmdMode4K2160p50, "2160p50" },
{ "4k2160p50", bmdMode4K2160p50, "2160p50" },
{ "2160p5994", bmdMode4K2160p5994, "2160p59.94" },
{ "4k2160p5994", bmdMode4K2160p5994, "2160p59.94" },
{ "2160p60", bmdMode4K2160p60, "2160p60" },
{ "4k2160p60", bmdMode4K2160p60, "2160p60" }
};
for (const ModeOption& option : options)
{
if (combinedToken == option.token || (frameToken.empty() && formatToken == option.token))
{
videoMode.displayMode = option.mode;
videoMode.displayName = option.displayName;
return true;
}
}
return false;
}
bool ResolveConfiguredVideoFormats(
const std::string& inputVideoFormat,
const std::string& inputFrameRate,
const std::string& outputVideoFormat,
const std::string& outputFrameRate,
VideoFormatSelection& videoModes,
std::string& error)
{
if (!ResolveConfiguredVideoFormat(inputVideoFormat, inputFrameRate, videoModes.input))
{
error = "Unsupported DeckLink inputVideoFormat/inputFrameRate in config/runtime-host.json: " +
inputVideoFormat + " / " + inputFrameRate;
return false;
}
if (!ResolveConfiguredVideoFormat(outputVideoFormat, outputFrameRate, videoModes.output))
{
error = "Unsupported DeckLink outputVideoFormat/outputFrameRate in config/runtime-host.json: " +
outputVideoFormat + " / " + outputFrameRate;
return false;
}
return true;
}
bool FindDeckLinkDisplayMode(IDeckLinkDisplayModeIterator* iterator, BMDDisplayMode targetMode, IDeckLinkDisplayMode** foundMode)
{
if (!iterator || !foundMode)
return false;
*foundMode = NULL;
IDeckLinkDisplayMode* candidate = NULL;
while (iterator->Next(&candidate) == S_OK)
{
if (candidate->GetDisplayMode() == targetMode)
{
*foundMode = candidate;
return true;
}
candidate->Release();
candidate = NULL;
}
return false;
}

View File

@@ -1,104 +0,0 @@
#include "DeckLinkFrameTransfer.h"
#include "DeckLinkSession.h"
////////////////////////////////////////////
// DeckLink Capture Delegate Class
////////////////////////////////////////////
CaptureDelegate::CaptureDelegate(DeckLinkSession* pOwner) :
m_pOwner(pOwner),
mRefCount(1)
{
}
HRESULT CaptureDelegate::QueryInterface(REFIID, LPVOID* ppv)
{
*ppv = NULL;
return E_NOINTERFACE;
}
ULONG CaptureDelegate::AddRef()
{
return InterlockedIncrement(&mRefCount);
}
ULONG CaptureDelegate::Release()
{
int newCount = InterlockedDecrement(&mRefCount);
if (newCount == 0)
delete this;
return newCount;
}
HRESULT CaptureDelegate::VideoInputFrameArrived(IDeckLinkVideoInputFrame* inputFrame, IDeckLinkAudioInputPacket*)
{
if (!inputFrame)
{
// It's possible to receive a NULL inputFrame, but a valid audioPacket. Ignore audio-only frame.
return S_OK;
}
bool hasNoInputSource = (inputFrame->GetFlags() & bmdFrameHasNoInputSource) == bmdFrameHasNoInputSource;
m_pOwner->HandleVideoInputFrame(inputFrame, hasNoInputSource);
return S_OK;
}
HRESULT CaptureDelegate::VideoInputFormatChanged(BMDVideoInputFormatChangedEvents, IDeckLinkDisplayMode*, BMDDetectedVideoInputFormatFlags)
{
return S_OK;
}
////////////////////////////////////////////
// DeckLink Playout Delegate Class
////////////////////////////////////////////
PlayoutDelegate::PlayoutDelegate(DeckLinkSession* pOwner) :
m_pOwner(pOwner),
mRefCount(1)
{
}
HRESULT PlayoutDelegate::QueryInterface(REFIID, LPVOID* ppv)
{
*ppv = NULL;
return E_NOINTERFACE;
}
ULONG PlayoutDelegate::AddRef()
{
return InterlockedIncrement(&mRefCount);
}
ULONG PlayoutDelegate::Release()
{
int newCount = InterlockedDecrement(&mRefCount);
if (newCount == 0)
delete this;
return newCount;
}
HRESULT PlayoutDelegate::ScheduledFrameCompleted(IDeckLinkVideoFrame* completedFrame, BMDOutputFrameCompletionResult result)
{
switch (result)
{
case bmdOutputFrameDisplayedLate:
OutputDebugStringA("ScheduledFrameCompleted() frame did not complete: Frame Displayed Late\n");
break;
case bmdOutputFrameDropped:
OutputDebugStringA("ScheduledFrameCompleted() frame did not complete: Frame Dropped\n");
break;
case bmdOutputFrameCompleted:
case bmdOutputFrameFlushed:
// Don't log bmdOutputFrameFlushed result since it is expected when Stop() is called
break;
default:
OutputDebugStringA("ScheduledFrameCompleted() frame did not complete: Unknown error\n");
}
m_pOwner->HandlePlayoutFrameCompleted(completedFrame, result);
return S_OK;
}
HRESULT PlayoutDelegate::ScheduledPlaybackHasStopped()
{
return S_OK;
}

View File

@@ -1,49 +0,0 @@
#pragma once
#include <windows.h>
#include <atomic>
#include "DeckLinkAPI_h.h"
class DeckLinkSession;
////////////////////////////////////////////
// Capture Delegate Class
////////////////////////////////////////////
class CaptureDelegate : public IDeckLinkInputCallback
{
DeckLinkSession* m_pOwner;
LONG mRefCount;
public:
CaptureDelegate(DeckLinkSession* 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
{
DeckLinkSession* m_pOwner;
LONG mRefCount;
public:
PlayoutDelegate(DeckLinkSession* 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();
};

View File

@@ -1,621 +0,0 @@
#include "DeckLinkSession.h"
#include "GlRenderConstants.h"
#include <atlbase.h>
#include <cstdio>
#include <cstring>
#include <new>
#include <sstream>
#include <utility>
#include <vector>
namespace
{
std::string BstrToUtf8(BSTR value)
{
if (value == nullptr)
return std::string();
const int requiredBytes = WideCharToMultiByte(CP_UTF8, 0, value, -1, NULL, 0, NULL, NULL);
if (requiredBytes <= 1)
return std::string();
std::vector<char> utf8Name(static_cast<std::size_t>(requiredBytes), '\0');
if (WideCharToMultiByte(CP_UTF8, 0, value, -1, utf8Name.data(), requiredBytes, NULL, NULL) <= 0)
return std::string();
return std::string(utf8Name.data());
}
bool InputSupportsFormat(IDeckLinkInput* input, BMDDisplayMode displayMode, BMDPixelFormat pixelFormat)
{
if (input == nullptr)
return false;
BOOL supported = FALSE;
BMDDisplayMode actualMode = bmdModeUnknown;
const HRESULT result = input->DoesSupportVideoMode(
bmdVideoConnectionUnspecified,
displayMode,
pixelFormat,
bmdNoVideoInputConversion,
bmdSupportedVideoModeDefault,
&actualMode,
&supported);
return result == S_OK && supported != FALSE;
}
bool OutputSupportsFormat(IDeckLinkOutput* output, BMDDisplayMode displayMode, BMDPixelFormat pixelFormat)
{
if (output == nullptr)
return false;
BOOL supported = FALSE;
BMDDisplayMode actualMode = bmdModeUnknown;
const HRESULT result = output->DoesSupportVideoMode(
bmdVideoConnectionUnspecified,
displayMode,
pixelFormat,
bmdNoVideoOutputConversion,
bmdSupportedVideoModeDefault,
&actualMode,
&supported);
return result == S_OK && supported != FALSE;
}
}
DeckLinkSession::~DeckLinkSession()
{
ReleaseResources();
}
void DeckLinkSession::ReleaseResources()
{
if (input != nullptr)
input->SetCallback(nullptr);
captureDelegate.Release();
input.Release();
if (output != nullptr)
output->SetScheduledFrameCompletionCallback(nullptr);
if (keyer != nullptr)
{
keyer->Disable();
mState.externalKeyingActive = false;
}
keyer.Release();
playoutDelegate.Release();
outputVideoFrameQueue.clear();
output.Release();
}
bool DeckLinkSession::DiscoverDevicesAndModes(const VideoFormatSelection& videoModes, std::string& error)
{
CComPtr<IDeckLinkIterator> deckLinkIterator;
CComPtr<IDeckLinkDisplayMode> inputMode;
CComPtr<IDeckLinkDisplayMode> outputMode;
mState.inputDisplayModeName = videoModes.input.displayName;
mState.outputDisplayModeName = videoModes.output.displayName;
HRESULT result = CoCreateInstance(CLSID_CDeckLinkIterator, nullptr, CLSCTX_ALL, IID_IDeckLinkIterator, reinterpret_cast<void**>(&deckLinkIterator));
if (FAILED(result))
{
error = "Please install the Blackmagic DeckLink drivers to use the features of this application.";
return false;
}
CComPtr<IDeckLink> deckLink;
while (deckLinkIterator->Next(&deckLink) == S_OK)
{
int64_t duplexMode;
bool deviceSupportsInternalKeying = false;
bool deviceSupportsExternalKeying = false;
std::string modelName;
CComPtr<IDeckLinkProfileAttributes> deckLinkAttributes;
if (deckLink->QueryInterface(IID_IDeckLinkProfileAttributes, (void**)&deckLinkAttributes) != S_OK)
{
printf("Could not obtain the IDeckLinkProfileAttributes interface\n");
deckLink.Release();
continue;
}
result = deckLinkAttributes->GetInt(BMDDeckLinkDuplex, &duplexMode);
BOOL attributeFlag = FALSE;
if (deckLinkAttributes->GetFlag(BMDDeckLinkSupportsInternalKeying, &attributeFlag) == S_OK)
deviceSupportsInternalKeying = (attributeFlag != FALSE);
attributeFlag = FALSE;
if (deckLinkAttributes->GetFlag(BMDDeckLinkSupportsExternalKeying, &attributeFlag) == S_OK)
deviceSupportsExternalKeying = (attributeFlag != FALSE);
CComBSTR modelNameBstr;
if (deckLinkAttributes->GetString(BMDDeckLinkModelName, &modelNameBstr) == S_OK)
modelName = BstrToUtf8(modelNameBstr);
if (result != S_OK || duplexMode == bmdDuplexInactive)
{
deckLink.Release();
continue;
}
bool inputUsed = false;
if (!input && deckLink->QueryInterface(IID_IDeckLinkInput, (void**)&input) == S_OK)
inputUsed = true;
if (!output && (!inputUsed || (duplexMode == bmdDuplexFull)))
{
if (deckLink->QueryInterface(IID_IDeckLinkOutput, (void**)&output) != S_OK)
output.Release();
else
{
mState.outputModelName = modelName;
mState.supportsInternalKeying = deviceSupportsInternalKeying;
mState.supportsExternalKeying = deviceSupportsExternalKeying;
}
}
deckLink.Release();
if (output && input)
break;
}
if (!output)
{
error = "Expected an Output DeckLink device";
ReleaseResources();
return false;
}
CComPtr<IDeckLinkDisplayModeIterator> inputDisplayModeIterator;
if (input && input->GetDisplayModeIterator(&inputDisplayModeIterator) != S_OK)
{
error = "Cannot get input Display Mode Iterator.";
ReleaseResources();
return false;
}
if (input && !FindDeckLinkDisplayMode(inputDisplayModeIterator, videoModes.input.displayMode, &inputMode))
{
error = "Cannot get specified input BMDDisplayMode for configured mode: " + videoModes.input.displayName;
ReleaseResources();
return false;
}
inputDisplayModeIterator.Release();
CComPtr<IDeckLinkDisplayModeIterator> outputDisplayModeIterator;
if (output->GetDisplayModeIterator(&outputDisplayModeIterator) != S_OK)
{
error = "Cannot get output Display Mode Iterator.";
ReleaseResources();
return false;
}
if (!FindDeckLinkDisplayMode(outputDisplayModeIterator, videoModes.output.displayMode, &outputMode))
{
error = "Cannot get specified output BMDDisplayMode for configured mode: " + videoModes.output.displayName;
ReleaseResources();
return false;
}
mState.outputFrameSize = { static_cast<unsigned>(outputMode->GetWidth()), static_cast<unsigned>(outputMode->GetHeight()) };
mState.inputFrameSize = inputMode
? FrameSize{ static_cast<unsigned>(inputMode->GetWidth()), static_cast<unsigned>(inputMode->GetHeight()) }
: mState.outputFrameSize;
if (!input)
mState.inputDisplayModeName = "No input - black frame";
BMDTimeValue frameDuration = 0;
BMDTimeScale frameTimescale = 0;
outputMode->GetFrameRate(&frameDuration, &frameTimescale);
mScheduler.Configure(frameDuration, frameTimescale);
mState.frameBudgetMilliseconds = mScheduler.FrameBudgetMilliseconds();
mState.inputFrameRowBytes = mState.inputFrameSize.width * 2u;
mState.outputFrameRowBytes = mState.outputFrameSize.width * 4u;
mState.captureTextureWidth = mState.inputFrameSize.width / 2u;
mState.outputPackTextureWidth = mState.outputFrameSize.width;
mState.hasInputDevice = input != nullptr;
mState.hasInputSource = false;
return true;
}
bool DeckLinkSession::SelectPreferredFormats(const VideoFormatSelection& videoModes, bool outputAlphaRequired, std::string& error)
{
if (!output)
{
error = "Expected an Output DeckLink device";
return false;
}
mState.formatStatusMessage.clear();
const bool inputTenBitSupported = input != nullptr && InputSupportsFormat(input, videoModes.input.displayMode, bmdFormat10BitYUV);
mState.inputPixelFormat = input != nullptr ? ChoosePreferredVideoIOFormat(inputTenBitSupported) : VideoIOPixelFormat::Uyvy8;
if (input != nullptr && !inputTenBitSupported)
mState.formatStatusMessage += "DeckLink input does not report 10-bit YUV support for the configured mode; using 8-bit capture. ";
const bool outputTenBitSupported = OutputSupportsFormat(output, videoModes.output.displayMode, bmdFormat10BitYUV);
const bool outputTenBitYuvaSupported = OutputSupportsFormat(output, videoModes.output.displayMode, bmdFormat10BitYUVA);
mState.outputPixelFormat = outputAlphaRequired
? (outputTenBitYuvaSupported ? VideoIOPixelFormat::Yuva10 : VideoIOPixelFormat::Bgra8)
: (outputTenBitSupported ? VideoIOPixelFormat::V210 : VideoIOPixelFormat::Bgra8);
if (outputAlphaRequired && outputTenBitYuvaSupported)
mState.formatStatusMessage += "External keying requires alpha; using 10-bit YUVA output. ";
else if (outputAlphaRequired)
mState.formatStatusMessage += "External keying requires alpha, but DeckLink output does not report 10-bit YUVA support for the configured mode; using 8-bit BGRA output. ";
else if (!outputTenBitSupported)
mState.formatStatusMessage += "DeckLink output does not report 10-bit YUV support for the configured mode; using 8-bit BGRA output. ";
int deckLinkOutputRowBytes = 0;
if (output->RowBytesForPixelFormat(DeckLinkPixelFormatForVideoIO(mState.outputPixelFormat), mState.outputFrameSize.width, &deckLinkOutputRowBytes) != S_OK)
{
error = "DeckLink output setup failed while calculating output row bytes.";
return false;
}
mState.outputFrameRowBytes = static_cast<unsigned>(deckLinkOutputRowBytes);
mState.outputPackTextureWidth = OutputIsTenBit()
? PackedTextureWidthFromRowBytes(mState.outputFrameRowBytes)
: mState.outputFrameSize.width;
if (InputIsTenBit())
{
int deckLinkInputRowBytes = 0;
if (output->RowBytesForPixelFormat(bmdFormat10BitYUV, mState.inputFrameSize.width, &deckLinkInputRowBytes) == S_OK)
mState.inputFrameRowBytes = static_cast<unsigned>(deckLinkInputRowBytes);
else
mState.inputFrameRowBytes = MinimumV210RowBytes(mState.inputFrameSize.width);
}
else
{
mState.inputFrameRowBytes = mState.inputFrameSize.width * 2u;
}
mState.captureTextureWidth = InputIsTenBit()
? PackedTextureWidthFromRowBytes(mState.inputFrameRowBytes)
: mState.inputFrameSize.width / 2u;
std::ostringstream status;
status << "DeckLink formats: capture " << (input ? VideoIOPixelFormatName(mState.inputPixelFormat) : "none")
<< ", output " << VideoIOPixelFormatName(mState.outputPixelFormat) << ".";
if (!mState.formatStatusMessage.empty())
status << " " << mState.formatStatusMessage;
mState.formatStatusMessage = status.str();
return true;
}
bool DeckLinkSession::ConfigureInput(InputFrameCallback callback, const VideoFormat& inputVideoMode, std::string& error)
{
mInputFrameCallback = std::move(callback);
if (!input)
{
mState.hasInputSource = false;
mState.inputDisplayModeName = "No input - black frame";
return true;
}
const BMDPixelFormat deckLinkInputPixelFormat = DeckLinkPixelFormatForVideoIO(mState.inputPixelFormat);
if (input->EnableVideoInput(inputVideoMode.displayMode, deckLinkInputPixelFormat, bmdVideoInputFlagDefault) != S_OK)
{
if (mState.inputPixelFormat == VideoIOPixelFormat::V210)
{
OutputDebugStringA("DeckLink 10-bit input could not be enabled; falling back to 8-bit capture.\n");
mState.inputPixelFormat = VideoIOPixelFormat::Uyvy8;
mState.inputFrameRowBytes = mState.inputFrameSize.width * 2u;
mState.captureTextureWidth = mState.inputFrameSize.width / 2u;
if (input->EnableVideoInput(inputVideoMode.displayMode, bmdFormat8BitYUV, bmdVideoInputFlagDefault) == S_OK)
{
std::ostringstream status;
status << "DeckLink formats: capture " << VideoIOPixelFormatName(mState.inputPixelFormat)
<< ", output " << VideoIOPixelFormatName(mState.outputPixelFormat)
<< ". DeckLink 10-bit input enable failed; using 8-bit capture.";
mState.formatStatusMessage = status.str();
goto input_enabled;
}
}
OutputDebugStringA("DeckLink input could not be enabled; continuing in output-only black-frame mode.\n");
input.Release();
mState.hasInputDevice = false;
mState.hasInputSource = false;
mState.inputDisplayModeName = "No input - black frame";
return true;
}
input_enabled:
captureDelegate.Attach(new (std::nothrow) CaptureDelegate(this));
if (captureDelegate == nullptr)
{
error = "DeckLink input setup failed while creating the capture callback.";
return false;
}
if (input->SetCallback(captureDelegate) != S_OK)
{
error = "DeckLink input setup failed while installing the capture callback.";
return false;
}
return true;
}
bool DeckLinkSession::ConfigureOutput(OutputFrameCallback callback, const VideoFormat& outputVideoMode, bool externalKeyingEnabled, std::string& error)
{
mOutputFrameCallback = std::move(callback);
if (output->EnableVideoOutput(outputVideoMode.displayMode, bmdVideoOutputFlagDefault) != S_OK)
{
error = "DeckLink output setup failed while enabling video output.";
return false;
}
if (output->QueryInterface(IID_IDeckLinkKeyer, (void**)&keyer) == S_OK && keyer != NULL)
mState.keyerInterfaceAvailable = true;
if (externalKeyingEnabled)
{
if (!mState.supportsExternalKeying)
{
mState.statusMessage = "External keying was requested, but the selected DeckLink output does not report external keying support.";
}
else if (!mState.keyerInterfaceAvailable)
{
mState.statusMessage = "External keying was requested, but the selected DeckLink output does not expose the IDeckLinkKeyer interface.";
}
else if (keyer->Enable(TRUE) != S_OK || keyer->SetLevel(255) != S_OK)
{
mState.statusMessage = "External keying was requested, but enabling the DeckLink keyer failed.";
}
else
{
mState.externalKeyingActive = true;
mState.statusMessage = "External keying is active on the selected DeckLink output.";
}
}
else if (mState.supportsExternalKeying)
{
mState.statusMessage = "Selected DeckLink output supports external keying. Set enableExternalKeying to true in runtime-host.json to request it.";
}
for (int i = 0; i < 10; i++)
{
CComPtr<IDeckLinkMutableVideoFrame> outputFrame;
const BMDPixelFormat deckLinkOutputPixelFormat = DeckLinkPixelFormatForVideoIO(mState.outputPixelFormat);
if (output->CreateVideoFrame(mState.outputFrameSize.width, mState.outputFrameSize.height, mState.outputFrameRowBytes, deckLinkOutputPixelFormat, bmdFrameFlagFlipVertical, &outputFrame) != S_OK)
{
error = "DeckLink output setup failed while creating an output video frame.";
return false;
}
outputVideoFrameQueue.push_back(outputFrame);
}
playoutDelegate.Attach(new (std::nothrow) PlayoutDelegate(this));
if (playoutDelegate == nullptr)
{
error = "DeckLink output setup failed while creating the playout callback.";
return false;
}
if (output->SetScheduledFrameCompletionCallback(playoutDelegate) != S_OK)
{
error = "DeckLink output setup failed while installing the scheduled-frame callback.";
return false;
}
if (!mState.formatStatusMessage.empty())
mState.statusMessage = mState.statusMessage.empty() ? mState.formatStatusMessage : mState.formatStatusMessage + " " + mState.statusMessage;
return true;
}
double DeckLinkSession::FrameBudgetMilliseconds() const
{
return mScheduler.FrameBudgetMilliseconds();
}
bool DeckLinkSession::BeginOutputFrame(VideoIOOutputFrame& frame)
{
CComPtr<IDeckLinkMutableVideoFrame> outputVideoFrame = outputVideoFrameQueue.front();
outputVideoFrameQueue.push_back(outputVideoFrame);
outputVideoFrameQueue.pop_front();
CComPtr<IDeckLinkVideoBuffer> outputVideoFrameBuffer;
if (outputVideoFrame->QueryInterface(IID_IDeckLinkVideoBuffer, (void**)&outputVideoFrameBuffer) != S_OK)
return false;
if (outputVideoFrameBuffer->StartAccess(bmdBufferAccessWrite) != S_OK)
return false;
void* pFrame = nullptr;
outputVideoFrameBuffer->GetBytes(&pFrame);
frame.bytes = pFrame;
frame.rowBytes = outputVideoFrame->GetRowBytes();
frame.width = mState.outputFrameSize.width;
frame.height = mState.outputFrameSize.height;
frame.pixelFormat = mState.outputPixelFormat;
frame.nativeFrame = outputVideoFrame.p;
frame.nativeBuffer = outputVideoFrameBuffer.Detach();
return true;
}
void DeckLinkSession::EndOutputFrame(VideoIOOutputFrame& frame)
{
IDeckLinkVideoBuffer* outputVideoFrameBuffer = static_cast<IDeckLinkVideoBuffer*>(frame.nativeBuffer);
if (outputVideoFrameBuffer != nullptr)
{
outputVideoFrameBuffer->EndAccess(bmdBufferAccessWrite);
outputVideoFrameBuffer->Release();
}
frame.nativeBuffer = nullptr;
frame.bytes = nullptr;
}
void DeckLinkSession::AccountForCompletionResult(VideoIOCompletionResult completionResult)
{
mScheduler.AccountForCompletionResult(completionResult);
}
bool DeckLinkSession::ScheduleOutputFrame(const VideoIOOutputFrame& frame)
{
IDeckLinkMutableVideoFrame* outputVideoFrame = static_cast<IDeckLinkMutableVideoFrame*>(frame.nativeFrame);
const VideoIOScheduleTime scheduleTime = mScheduler.NextScheduleTime();
if (outputVideoFrame == nullptr || output->ScheduleVideoFrame(outputVideoFrame, scheduleTime.streamTime, scheduleTime.duration, scheduleTime.timeScale) != S_OK)
return false;
return true;
}
bool DeckLinkSession::Start()
{
mScheduler.Reset();
if (!output)
{
MessageBoxA(NULL, "Cannot start playout because no DeckLink output device is available.", "DeckLink start failed", MB_OK | MB_ICONERROR);
return false;
}
if (outputVideoFrameQueue.empty())
{
MessageBoxA(NULL, "Cannot start playout because the output frame queue is empty.", "DeckLink start failed", MB_OK | MB_ICONERROR);
return false;
}
for (unsigned i = 0; i < kPrerollFrameCount; i++)
{
CComPtr<IDeckLinkMutableVideoFrame> outputVideoFrame = outputVideoFrameQueue.front();
outputVideoFrameQueue.push_back(outputVideoFrame);
outputVideoFrameQueue.pop_front();
CComPtr<IDeckLinkVideoBuffer> outputVideoFrameBuffer;
if (outputVideoFrame->QueryInterface(IID_IDeckLinkVideoBuffer, (void**)&outputVideoFrameBuffer) != S_OK)
{
MessageBoxA(NULL, "Could not query the preroll output frame buffer.", "DeckLink start failed", MB_OK | MB_ICONERROR);
return false;
}
if (outputVideoFrameBuffer->StartAccess(bmdBufferAccessWrite) != S_OK)
{
MessageBoxA(NULL, "Could not write to the preroll output frame buffer.", "DeckLink start failed", MB_OK | MB_ICONERROR);
return false;
}
void* pFrame = nullptr;
outputVideoFrameBuffer->GetBytes((void**)&pFrame);
memset(pFrame, 0, outputVideoFrame->GetRowBytes() * mState.outputFrameSize.height);
outputVideoFrameBuffer->EndAccess(bmdBufferAccessWrite);
const VideoIOScheduleTime scheduleTime = mScheduler.NextScheduleTime();
if (output->ScheduleVideoFrame(outputVideoFrame, scheduleTime.streamTime, scheduleTime.duration, scheduleTime.timeScale) != S_OK)
{
MessageBoxA(NULL, "Could not schedule a preroll output frame.", "DeckLink start failed", MB_OK | MB_ICONERROR);
return false;
}
}
if (input)
{
if (input->StartStreams() != S_OK)
{
MessageBoxA(NULL, "Could not start the DeckLink input stream.", "DeckLink start failed", MB_OK | MB_ICONERROR);
return false;
}
}
if (output->StartScheduledPlayback(0, mScheduler.TimeScale(), 1.0) != S_OK)
{
MessageBoxA(NULL, "Could not start DeckLink scheduled playback.", "DeckLink start failed", MB_OK | MB_ICONERROR);
return false;
}
return true;
}
bool DeckLinkSession::Stop()
{
if (keyer != nullptr)
{
keyer->Disable();
mState.externalKeyingActive = false;
}
if (input)
{
input->StopStreams();
input->DisableVideoInput();
}
if (output)
{
output->StopScheduledPlayback(0, NULL, 0);
output->DisableVideoOutput();
}
return true;
}
void DeckLinkSession::HandleVideoInputFrame(IDeckLinkVideoInputFrame* inputFrame, bool hasNoInputSource)
{
mState.hasInputSource = !hasNoInputSource;
if (hasNoInputSource || mInputFrameCallback == nullptr)
{
VideoIOFrame frame;
frame.width = mState.inputFrameSize.width;
frame.height = mState.inputFrameSize.height;
frame.pixelFormat = mState.inputPixelFormat;
frame.hasNoInputSource = hasNoInputSource;
if (mInputFrameCallback)
mInputFrameCallback(frame);
return;
}
CComPtr<IDeckLinkVideoBuffer> inputFrameBuffer;
void* videoPixels = nullptr;
if (inputFrame->QueryInterface(IID_IDeckLinkVideoBuffer, (void**)&inputFrameBuffer) != S_OK)
return;
if (inputFrameBuffer->StartAccess(bmdBufferAccessRead) != S_OK)
return;
inputFrameBuffer->GetBytes(&videoPixels);
VideoIOFrame frame;
frame.bytes = videoPixels;
frame.rowBytes = inputFrame->GetRowBytes();
frame.width = static_cast<unsigned>(inputFrame->GetWidth());
frame.height = static_cast<unsigned>(inputFrame->GetHeight());
frame.pixelFormat = mState.inputPixelFormat;
frame.hasNoInputSource = hasNoInputSource;
mInputFrameCallback(frame);
inputFrameBuffer->EndAccess(bmdBufferAccessRead);
}
void DeckLinkSession::HandlePlayoutFrameCompleted(IDeckLinkVideoFrame*, BMDOutputFrameCompletionResult completionResult)
{
if (!mOutputFrameCallback)
return;
VideoIOCompletion completion;
switch (completionResult)
{
case bmdOutputFrameDisplayedLate:
completion.result = VideoIOCompletionResult::DisplayedLate;
break;
case bmdOutputFrameDropped:
completion.result = VideoIOCompletionResult::Dropped;
break;
case bmdOutputFrameFlushed:
completion.result = VideoIOCompletionResult::Flushed;
break;
case bmdOutputFrameCompleted:
completion.result = VideoIOCompletionResult::Completed;
break;
default:
completion.result = VideoIOCompletionResult::Unknown;
break;
}
mOutputFrameCallback(completion);
}

View File

@@ -1,13 +1,55 @@
{
"shaderLibrary": "shaders",
"serverPort": 8080,
"oscPort": 9000,
"inputVideoFormat": "1080p",
"inputFrameRate": "59.94",
"outputVideoFormat": "1080p",
"outputFrameRate": "59.94",
"$schema": "./runtime-host.schema.json",
"autoReload": true,
"input": {
"backend": "ndi",
"device": "AIDENLAPTOP (Test Pattern)",
"frameRate": "59.94",
"resolution": "1080p"
},
"maxTemporalHistoryFrames": 12,
"previewFps": 30,
"enableExternalKeying": true
"oscBindAddress": "0.0.0.0",
"oscPort": 9000,
"oscSmoothing": 0.18,
"output": {
"backend": "ndi",
"device": "Shader",
"frameRate": "59.94",
"keying": {
"alphaRequired": false,
"external": false
},
"pixelFormat": "auto",
"resolution": "1080p"
},
"windowOutput": {
"fullscreen": true,
"borderless": true,
"display": "default",
"x": 0,
"y": 0,
"width": 0,
"height": 0,
"vsync": true,
"allowTearing": false
},
"colorPipeline": {
"ocioEnabled": false,
"ocioConfig": "",
"inputColorSpace": "scene_linear",
"workingColorSpace": "scene_linear",
"outputColorSpace": "rec709",
"display": "sRGB",
"view": "Rec.709",
"look": "",
"exposure": 0,
"gamma": 1,
"workingFormat": "rgba16f",
"linearWorkingSpace": true
},
"previewEnabled": true,
"previewFps": 59.94,
"runtimeShaderId": "",
"serverPort": 8080,
"shaderLibrary": "shaders"
}

View File

@@ -0,0 +1,361 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://render-cadence.local/schemas/runtime-host.schema.json",
"title": "Render Cadence Runtime Host Configuration",
"description": "Startup configuration for the Render Cadence native host. This schema documents the settings currently read by AppConfigProvider.",
"type": "object",
"additionalProperties": false,
"properties": {
"$schema": {
"type": "string",
"description": "Editor-only schema reference. The native host ignores this field."
},
"shaderLibrary": {
"type": "string",
"default": "shaders",
"description": "Path to the shader package library directory. Relative paths are resolved from the process/repo working location."
},
"serverPort": {
"type": "integer",
"minimum": 1,
"maximum": 65535,
"default": 8080,
"description": "Preferred HTTP control server port."
},
"oscBindAddress": {
"type": "string",
"default": "0.0.0.0",
"description": "OSC bind address reserved for the control surface. The current native host reports this through an OSC status stub but does not start the UDP listener yet."
},
"oscPort": {
"type": "integer",
"minimum": 0,
"maximum": 65535,
"default": 9000,
"description": "OSC UDP port reserved for the control surface. Use 0 to mark the OSC stub disabled."
},
"oscSmoothing": {
"type": "number",
"minimum": 0,
"default": 0.18,
"description": "Reserved OSC smoothing amount reported through the OSC status stub."
},
"input": {
"$ref": "#/$defs/input"
},
"output": {
"$ref": "#/$defs/output"
},
"windowOutput": {
"$ref": "#/$defs/windowOutput"
},
"colorPipeline": {
"$ref": "#/$defs/colorPipeline"
},
"autoReload": {
"type": "boolean",
"default": true,
"description": "When true, the runtime control layer may automatically rescan/reload shader packages when requested by the app flow."
},
"maxTemporalHistoryFrames": {
"type": "integer",
"minimum": 0,
"default": 12,
"description": "Maximum temporal history frames exposed to shaders that request history."
},
"previewEnabled": {
"type": "boolean",
"default": false,
"description": "Starts the optional preview window on its own thread."
},
"previewFps": {
"type": "number",
"exclusiveMinimum": 0,
"default": 59.94,
"description": "Target repaint rate for the optional preview window. It does not change render/output cadence."
},
"runtimeShaderId": {
"type": "string",
"default": "",
"description": "Optional startup shader package id used only when no saved runtime layer stack is restored. Leave empty to keep the simple fallback renderer until layers are added or restored."
}
},
"required": [
"shaderLibrary",
"serverPort",
"input",
"output"
],
"$defs": {
"inputBackend": {
"type": "string",
"enum": [
"decklink",
"ndi",
"window",
"fullscreen",
"borderless",
"none",
"disabled",
"off"
]
},
"outputBackend": {
"type": "string",
"enum": [
"decklink",
"ndi",
"none",
"disabled",
"off"
]
},
"resolution": {
"type": "string",
"enum": [
"720p",
"1080i",
"1080p",
"2160p",
"4k",
"uhd"
]
},
"frameRate": {
"type": "string",
"enum": [
"23.98",
"24",
"25",
"29.97",
"30",
"50",
"59.94",
"60"
]
},
"input": {
"type": "object",
"additionalProperties": false,
"description": "Video input backend configuration. DeckLink uses the configured resolution/frameRate as a capture mode. NDI adapts to the received source shape and logs if it differs from these expected values.",
"properties": {
"backend": {
"$ref": "#/$defs/inputBackend",
"default": "decklink",
"description": "Input backend. Use 'decklink' for Blackmagic capture, 'ndi' for NDI receive, or 'none'/'disabled'/'off' for black fallback input."
},
"device": {
"type": "string",
"default": "default",
"description": "Input device/source selector. DeckLink currently uses 'default'. NDI accepts 'default'/'auto' for the first discovered source or an exact NDI source name."
},
"resolution": {
"$ref": "#/$defs/resolution",
"default": "1080p",
"description": "Expected input resolution/mode. For NDI this is advisory only; received source dimensions are used."
},
"frameRate": {
"$ref": "#/$defs/frameRate",
"default": "59.94",
"description": "Expected input frame rate. DeckLink uses this to resolve the hardware mode; NDI currently treats it as an expectation for logging/state."
}
},
"required": [
"backend",
"device",
"resolution",
"frameRate"
]
},
"output": {
"type": "object",
"additionalProperties": false,
"description": "Video output backend configuration.",
"properties": {
"backend": {
"$ref": "#/$defs/outputBackend",
"default": "decklink",
"description": "Output backend. Use 'decklink' for Blackmagic playout, 'ndi' for NDI send, or 'none'/'disabled'/'off' to disable scheduled output."
},
"device": {
"type": "string",
"default": "default",
"description": "Output device selector. DeckLink currently uses the first compatible/default output device. NDI uses this as the advertised sender name; 'default' becomes 'Render Cadence'."
},
"resolution": {
"$ref": "#/$defs/resolution",
"default": "1080p",
"description": "Output render and video mode resolution."
},
"frameRate": {
"$ref": "#/$defs/frameRate",
"default": "59.94",
"description": "Output render cadence and video mode frame rate."
},
"pixelFormat": {
"type": "string",
"enum": [
"auto",
"bgra8",
"uyvy8"
],
"default": "auto",
"description": "Requested system-memory output pixel format. Auto uses UYVY8 for DeckLink/NDI unless Output alpha is enabled, then BGRA8."
},
"keying": {
"type": "object",
"additionalProperties": false,
"description": "Output alpha options. The control UI exposes these as a single Output alpha control.",
"properties": {
"external": {
"type": "boolean",
"default": false,
"description": "DeckLink external-keyer backing field. The control UI only writes this true for DeckLink output."
},
"alphaRequired": {
"type": "boolean",
"default": false,
"description": "General output alpha request. When true, automatic output pixel-format selection uses an alpha-carrying system-frame format."
}
},
"required": [
"external",
"alphaRequired"
]
}
},
"required": [
"backend",
"device",
"resolution",
"frameRate"
]
},
"windowOutput": {
"type": "object",
"additionalProperties": false,
"description": "Reserved settings for a future full-resolution/full-frame-rate borderless or fullscreen video output backend. These are not used by the preview window.",
"properties": {
"fullscreen": {
"type": "boolean",
"default": true,
"description": "When the future window backend is implemented, request exclusive/fullscreen-style presentation."
},
"borderless": {
"type": "boolean",
"default": true,
"description": "When true, the future window backend should create a borderless output window."
},
"display": {
"type": "string",
"default": "default",
"description": "Display/monitor selector for the future window backend."
},
"x": {
"type": "integer",
"default": 0,
"description": "Window X position used when fullscreen is false or when a backend needs explicit placement."
},
"y": {
"type": "integer",
"default": 0,
"description": "Window Y position used when fullscreen is false or when a backend needs explicit placement."
},
"width": {
"type": "integer",
"minimum": 0,
"default": 0,
"description": "Optional window width override. Zero means use output resolution."
},
"height": {
"type": "integer",
"minimum": 0,
"default": 0,
"description": "Optional window height override. Zero means use output resolution."
},
"vsync": {
"type": "boolean",
"default": true,
"description": "Presentation sync request for the future window backend."
},
"allowTearing": {
"type": "boolean",
"default": false,
"description": "Allow tearing for the future window backend when the graphics API and OS support it."
}
}
},
"colorPipeline": {
"type": "object",
"additionalProperties": false,
"description": "Reserved color-management settings for a future OCIO-backed linear RGBA16F render pipeline.",
"properties": {
"ocioEnabled": {
"type": "boolean",
"default": false,
"description": "Enable future OpenColorIO transforms. Currently parsed and reported only."
},
"ocioConfig": {
"type": "string",
"default": "",
"description": "Path to an OCIO config file or package-relative config identifier."
},
"inputColorSpace": {
"type": "string",
"default": "scene_linear",
"description": "Input/video color space to convert from at the input edge."
},
"workingColorSpace": {
"type": "string",
"default": "scene_linear",
"description": "Linear working space shader packages should be able to assume."
},
"outputColorSpace": {
"type": "string",
"default": "rec709",
"description": "Output transform target color space for scheduled video output."
},
"display": {
"type": "string",
"default": "sRGB",
"description": "OCIO display name for preview/window/display transforms."
},
"view": {
"type": "string",
"default": "Rec.709",
"description": "OCIO view name for preview/window/display transforms."
},
"look": {
"type": "string",
"default": "",
"description": "Optional OCIO look name."
},
"exposure": {
"type": "number",
"default": 0,
"description": "Reserved display transform exposure adjustment."
},
"gamma": {
"type": "number",
"exclusiveMinimum": 0,
"default": 1,
"description": "Reserved display transform gamma adjustment."
},
"workingFormat": {
"type": "string",
"enum": [
"rgba16f",
"rgba32f"
],
"default": "rgba16f",
"description": "Future render target/intermediate working format."
},
"linearWorkingSpace": {
"type": "boolean",
"default": true,
"description": "Documents the intended linear-light working pipeline."
}
}
}
}
}

View File

@@ -0,0 +1,198 @@
# 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
- a pluggable app-side runtime-content controller
- the default runtime Slang shader package stack from `shaders/`
- 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, video backend factory, runtime-content controller boundary, and the default shader runtime-content controller
- `src/render`: cadence clock, input texture upload, render-content boundary, readback, and runtime GL support
- `src/render/thread`: render thread lifecycle, cadence loop, metrics, and runtime shader commit mailbox
- `src/render/runtime`: render-thread-owned runtime shader scene, renderer, text texture upload cache, and shared-context shader prepare worker
- `src/frames`: system-memory frame exchange
- `src/video/core`: generic video IO edge contracts, mode descriptions, formats, and output scheduling thread
- `src/video/decklink`: current DeckLink input/output backend
- `src/video/playout`: backend-adjacent playout policy, queues, frame pools, and scheduling helpers
- `src/video/legacy`: older backend pipeline pieces kept separate from the current app path
- `src/runtime/catalog`: supported shader catalog and package filtering
- `src/runtime/layers`: app-side runtime layer model, restore, reload, and render snapshot construction
- `src/runtime/shader`: background Slang build bridge and prepared shader artifact types
- `src/runtime/state`: runtime JSON helpers, parameter normalization, and debounced runtime-state persistence
- `src/runtime/text`: MSDF/MTSDF font atlas build and CPU-side prepared text texture composition
- `src/control`: command parsing, HTTP/WebSocket transport helpers, OSC status stub, OpenAPI state JSON
- `src/app/RenderCadenceHttpRoutes.*`: this app's current HTTP endpoint map
- `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. Initialize the active `IRuntimeContentController`. The checked-in app uses `ShaderRuntimeContentController`, which loads the supported shader catalog, starts runtime-state persistence, and tries to restore `runtime/runtime_state.json`.
3. If restore fails or no usable state exists, the shader controller falls back to the optional configured startup shader. The checked-in config leaves `runtimeShaderId` empty, so a fresh host keeps the simple fallback renderer.
4. Start the render thread.
5. Start the active runtime-content controller. The shader controller queues background Slang builds for every pending active layer.
6. Build a small completed-frame reserve.
7. Start optional preview, optional video 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 Content And Shader Layer State
`RenderCadenceApp` owns startup, video output, preview, telemetry, OSC status, and HTTP server lifetime. It does not own the Slang shader stack directly. Runtime content plugs in through `IRuntimeContentController`.
The checked-in implementation is `ShaderRuntimeContentController`. It wraps `RuntimeLayerController`, exposes shader catalog/layer JSON for `/api/state`, handles shader layer POST commands, and publishes render-ready shader layer snapshots to the render thread.
`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
- optional shader-declared custom UI metadata
- 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 optional configured startup 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.
The host configuration editor is separate from runtime layer persistence. The UI reads active and saved startup config through `/api/config`, saves `config/runtime-host.json` through `/api/config/save`, and requests a native host restart through `/api/app/restart`. Render cadence, video input/output selection, resolution, frame rate, output pixel format, HTTP port, and preview settings are still startup-owned; they are not hot-swapped inside the cadence path.
## Shader Reload
`POST /api/reload` and the control UI reload button:
- rescan `shaders/`
- re-read manifests
- rebuild the supported shader catalog
- refresh optional shader custom UI metadata
- 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 default shader runtime-content controller publishes a render-layer artifact. The render thread forwards pending layer snapshots to the active render-content adapter. The default `RuntimeShaderRenderContent` owns the 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 through that adapter.
## Video And Preview
Video input and output are optional edges. `input.backend` and `output.backend` select the concrete backend through the app-side backend factory. DeckLink and NDI are the current concrete backends, and `none` disables an edge. `input` and `output` also carry the device selector plus resolution/frame-rate settings. Configured video modes are represented in `src/video/core` and translated to backend-specific modes only inside the concrete edge.
The input edge writes CPU frames into `InputFrameMailbox`. The current DeckLink backend captures BGRA8 directly where possible, or raw UYVY8 for render-thread GPU decode. The input edge does not call GL, render, preview, screenshot, shader, or output scheduling code.
The output edge consumes completed system-memory frames from `SystemFrameExchange`. The render thread owns output pixel packing before readback: BGRA8 is read directly, and UYVY8 is packed on the GPU into a half-width RGBA8 target before async PBO readback. DeckLink and NDI output then schedule/send those completed CPU frames without invoking GL or converting pixels. If video output is unavailable, the app continues running render cadence, control, preview, telemetry, and logging.
Runtime state exposes backend-neutral output telemetry through `videoOutput`. Portable fields such as `enabled`, `backend`, and `scheduleFailures` stay at that level; backend-specific counters live under `videoOutput.backendMetrics`.
`PreviewWindowThread` is optional and uses a non-consuming system-memory tap. It paints BGRA8 directly, decodes UYVY8 only for preview presentation, 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. `HttpControlServer` owns socket lifetime, HTTP parsing, static asset helpers, OpenAPI/Swagger helper serving, and WebSocket state transport. `RenderCadenceHttpRoutes` owns this app's current endpoint map:
- UI assets
- shader package custom UI assets under `/shader-assets/{shaderId}/...`
- OpenAPI/Swagger docs
- `GET /api/state`
- `/ws` state updates
- layer mutation POST routes, dispatched to the active runtime-content controller
- `/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`.
Forks can reuse the HTTP/WebSocket shell without keeping these endpoints by installing a different route callback.
`OscControlServer` is currently a lifecycle/status stub. It consumes startup OSC config, exposes configured/disabled/not-listening state through `/api/state`, and leaves UDP socket receive, OSC decode, and runtime dispatch unimplemented until that ingress boundary is built deliberately.
## 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 transport and app-route dispatch
- frame exchange and input mailbox behavior
- video format and scheduling helpers
Run:
```powershell
cmake --build --preset build-debug --target RUN_TESTS --parallel
```

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 native host has an OSC service stub that consumes config and reports status in `/api/state`; it does not open a UDP listener or dispatch messages yet. 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.
Today, `oscPort: 0` marks the OSC stub disabled and any nonzero port marks it configured but not listening. When OSC ingress is implemented, `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
@@ -61,6 +63,8 @@ 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
@@ -72,6 +76,8 @@ Examples:
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
@@ -114,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,164 @@
# 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 Render Context
Only the render thread may bind and use its primary OpenGL context.
For a fork that replaces the renderer with D3D or another graphics API, read this rule as the same ownership contract: the cadence/render thread owns the primary render device/context path, and non-render services exchange prepared data with it rather than calling rendering APIs themselves.
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.
Output pixel-format packing that requires GL belongs in the render-owned readback path before frame publication. Video I/O edges may select supported system-memory formats, but they must not invoke GL or become hidden pixel-conversion renderers.
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

@@ -6,24 +6,214 @@ 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: Config
description: Startup host configuration and restart control.
- 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
/shader-assets/{shaderId}/{assetPath}:
get:
tags: [Static]
summary: Serve a shader package UI asset
description: Serves custom shader UI files declared by `shader.json`. Assets are resolved through the active runtime-content controller and must stay inside that shader package's `ui/` directory.
operationId: getShaderPackageAsset
parameters:
- name: shaderId
in: path
required: true
description: Shader package id.
schema:
type: string
- name: assetPath
in: path
required: true
description: Relative asset path below the shader package, usually `ui/controls.js`.
schema:
type: string
responses:
"200":
description: Shader package UI 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, the shader did not declare custom UI, 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 +226,83 @@ paths:
application/json:
schema:
$ref: "#/components/schemas/RuntimeState"
/api/config:
get:
tags: [Config]
summary: Get active and saved host config
operationId: getHostConfig
responses:
"200":
description: Active startup config and the config currently saved on disk.
content:
application/json:
schema:
$ref: "#/components/schemas/HostConfigResponse"
/api/ndi/sources:
get:
tags: [Config]
summary: Discover NDI sources
description: Returns currently discoverable NDI sources for the host-config input device picker. Manual source names remain valid even when discovery returns no matches.
operationId: getNdiSources
responses:
"200":
description: Current NDI source discovery result.
content:
application/json:
schema:
$ref: "#/components/schemas/NdiSourcesResponse"
/api/config/save:
post:
tags: [Config]
summary: Save host config
description: Saves `runtime-host.json`. Startup-owned services use the new values after app restart.
operationId: saveHostConfig
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/HostConfig"
responses:
"200":
$ref: "#/components/responses/ActionOk"
"400":
$ref: "#/components/responses/ActionError"
/api/app/restart:
post:
tags: [Config]
summary: Restart the native host
operationId: restartHost
requestBody:
required: false
content:
application/json:
schema:
type: object
additionalProperties: false
responses:
"200":
$ref: "#/components/responses/ActionOk"
"400":
$ref: "#/components/responses/ActionError"
/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 +339,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 +435,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 +452,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,7 +470,7 @@ paths:
post:
tags: [Runtime]
summary: Reload shaders
description: Rescans the shader library, re-reads manifests, queues shader compilation, and refreshes shader availability/errors. If a changed shader fails, the previous working stack remains active where possible.
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
@@ -218,8 +487,8 @@ paths:
/api/screenshot:
post:
tags: [Runtime]
summary: Queue a PNG screenshot of the final output render target
description: Captures the next completed output render target and writes it under `runtime/screenshots/`.
summary: Planned screenshot route
description: Planned screenshot route. The current native command path currently returns an unimplemented action result.
operationId: queueScreenshot
requestBody:
required: false
@@ -348,6 +617,116 @@ components:
type: string
example: live-show-look
additionalProperties: false
HostConfigResponse:
type: object
properties:
ok:
type: boolean
path:
type: string
active:
$ref: "#/components/schemas/HostConfig"
disk:
$ref: "#/components/schemas/HostConfig"
diskLoaded:
type: boolean
restartRequired:
type: boolean
error:
type: string
NdiSourcesResponse:
type: object
properties:
ok:
type: boolean
error:
type: string
sources:
type: array
items:
$ref: "#/components/schemas/NdiSource"
additionalProperties: false
NdiSource:
type: object
properties:
name:
type: string
description: User-visible NDI source name, usually `MACHINE (SOURCE)`.
urlAddress:
type: string
description: SDK-provided URL/address for diagnostics. Saved host config still uses the source name.
additionalProperties: false
HostConfig:
type: object
properties:
$schema:
type: string
shaderLibrary:
type: string
serverPort:
type: number
oscBindAddress:
type: string
oscPort:
type: number
oscSmoothing:
type: number
input:
$ref: "#/components/schemas/HostVideoInputConfig"
output:
$ref: "#/components/schemas/HostVideoOutputConfig"
autoReload:
type: boolean
maxTemporalHistoryFrames:
type: number
previewEnabled:
type: boolean
previewFps:
type: number
runtimeShaderId:
type: string
description: Optional startup shader id used only when no saved runtime layer stack is restored.
default: ""
additionalProperties: false
HostVideoInputConfig:
type: object
properties:
backend:
type: string
enum: [decklink, ndi, none]
device:
type: string
resolution:
type: string
frameRate:
type: string
additionalProperties: false
HostVideoOutputConfig:
type: object
properties:
backend:
type: string
enum: [decklink, ndi, none]
device:
type: string
resolution:
type: string
frameRate:
type: string
pixelFormat:
type: string
enum: [auto, bgra8, uyvy8]
keying:
type: object
properties:
external:
type: boolean
description: DeckLink external-keyer backing field. The bundled UI exposes this together with alphaRequired as Output alpha and only writes it true for DeckLink output.
alphaRequired:
type: boolean
description: General output alpha request. When true, automatic output pixel-format selection uses an alpha-carrying system-frame format.
additionalProperties: false
additionalProperties: false
RuntimeState:
type: object
properties:
@@ -357,12 +736,18 @@ components:
$ref: "#/components/schemas/RuntimeStatus"
video:
$ref: "#/components/schemas/VideoStatus"
videoOutput:
$ref: "#/components/schemas/VideoOutputStatus"
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:
@@ -382,20 +767,77 @@ components:
type: number
oscPort:
type: number
oscBindAddress:
type: string
oscSmoothing:
type: number
autoReload:
type: boolean
maxTemporalHistoryFrames:
type: number
enableExternalKeying:
previewFps:
type: number
osc:
$ref: "#/components/schemas/AppOscStatus"
input:
type: object
properties:
backend:
type: string
enum: [decklink, ndi, none]
device:
type: string
resolution:
type: string
frameRate:
type: string
output:
type: object
properties:
backend:
type: string
enum: [decklink, ndi, none]
device:
type: string
resolution:
type: string
frameRate:
type: string
pixelFormat:
type: string
enum: [auto, bgra8, uyvy8]
systemFramePixelFormat:
type: string
keying:
type: object
properties:
outputAlpha:
type: boolean
inputVideoFormat:
description: Operator-facing derived alpha state. True when alphaRequired or the DeckLink external-keyer backing field is true.
external:
type: boolean
description: DeckLink external-keyer backing field. The bundled UI exposes this together with alphaRequired as Output alpha and only writes it true for DeckLink output.
alphaRequired:
type: boolean
description: General output alpha request. When true, automatic output pixel-format selection uses an alpha-carrying system-frame format.
AppOscStatus:
type: object
properties:
configured:
type: boolean
description: True when OSC has a nonzero configured port.
listening:
type: boolean
description: False in the current native host because UDP OSC ingress is only stubbed.
bindAddress:
type: string
inputFrameRate:
type: string
outputVideoFormat:
type: string
outputFrameRate:
port:
type: number
smoothing:
type: number
statusMessage:
type: string
additionalProperties: false
RuntimeStatus:
type: object
properties:
@@ -416,6 +858,52 @@ components:
type: number
modeName:
type: string
VideoOutputStatus:
type: object
description: Backend-neutral output telemetry. Backend-specific counters live under `backendMetrics`.
properties:
enabled:
type: boolean
backend:
type: string
example: decklink
statusMessage:
type: string
scheduleFailures:
type: number
completions:
type: number
late:
type: number
dropped:
type: number
backendMetrics:
$ref: "#/components/schemas/VideoOutputBackendMetrics"
VideoOutputBackendMetrics:
type: object
description: Backend-specific output metrics. For `decklink`, this contains DeckLink schedule and buffer telemetry.
additionalProperties: true
properties:
bufferedAvailable:
type: boolean
buffered:
type: number
nullable: true
scheduleCallMs:
type: number
scheduleLeadAvailable:
type: boolean
scheduleLeadFrames:
type: number
nullable: true
playbackFrameIndex:
type: number
nextScheduleFrameIndex:
type: number
playbackStreamTime:
type: number
scheduleRealignments:
type: number
DeckLinkStatus:
type: object
deprecated: true
@@ -462,10 +950,12 @@ 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:
@@ -478,6 +968,262 @@ components:
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 async PBO readback for the selected system-frame pixel format after rendering.
completedReadbackCopyMs:
type: number
description: Most recent duration spent mapping and copying a completed 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:
@@ -497,6 +1243,31 @@ components:
description: Error text for unavailable shader packages.
temporal:
$ref: "#/components/schemas/TemporalState"
feedback:
$ref: "#/components/schemas/FeedbackState"
ui:
$ref: "#/components/schemas/ShaderUiDefinition"
nullable: true
ShaderUiDefinition:
type: object
nullable: true
properties:
type:
type: string
enum: [webComponent]
entry:
type: string
description: Package-relative JavaScript module path from the shader manifest.
example: ui/controls.js
tag:
type: string
description: Custom element tag registered by the module.
example: my-shader-controls
assetUrl:
type: string
description: HTTP URL for loading the JavaScript module from the local control server.
example: /shader-assets/my-shader/ui/controls.js
additionalProperties: false
TemporalState:
type: object
properties:
@@ -509,6 +1280,13 @@ components:
type: number
effectiveHistoryLength:
type: number
FeedbackState:
type: object
properties:
enabled:
type: boolean
writePass:
type: string
LayerState:
type: object
properties:
@@ -522,6 +1300,9 @@ components:
type: boolean
temporal:
$ref: "#/components/schemas/TemporalState"
ui:
$ref: "#/components/schemas/ShaderUiDefinition"
nullable: true
parameters:
type: array
items:
@@ -570,6 +1351,9 @@ components:
font:
type: string
description: Font asset id used by text parameters, when declared.
fontParameter:
type: string
description: Enum parameter id used to select a text parameter font at runtime.
value:
description: Current parameter value.
oneOf:

View File

@@ -17,9 +17,9 @@ Generated files:
- `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`: autosaved latest layer stack, layer order, bypass state, shader assignments, and parameter values. The host reloads this file on startup.
- `stack_presets/*.json`: user-saved layer stack presets.
- `screenshots/*.png`: screenshots captured from the final output render target through the control UI/API.
- `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

@@ -19,6 +19,7 @@ struct ShaderContext
float bypass;
int sourceHistoryLength;
int temporalHistoryLength;
int feedbackAvailable;
};
cbuffer GlobalParams
@@ -34,16 +35,23 @@ cbuffer GlobalParams
float gBypass;
int gSourceHistoryLength;
int gTemporalHistoryLength;
int gFeedbackAvailable;
{{PARAMETER_UNIFORMS}}};
Sampler2D<float4> gVideoInput;
{{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 sampleLayerInput(float2 tc)
{
return gLayerInput.Sample(tc);
}
float4 sampleSourceHistory(int framesAgo, float2 tc)
{
if (gSourceHistoryLength <= 0)
@@ -74,6 +82,8 @@ float4 sampleTemporalHistory(int framesAgo, float2 tc)
}
}
{{FEEDBACK_HELPER}}
{{TEXT_HELPERS}}
#include "{{USER_SHADER_INCLUDE}}"
@@ -94,6 +104,7 @@ float4 fragmentMain(FragmentInput input) : SV_Target
context.bypass = gBypass;
context.sourceHistoryLength = gSourceHistoryLength;
context.temporalHistoryLength = gTemporalHistoryLength;
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);

View File

@@ -101,9 +101,18 @@ Optional fields:
- `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.
- `ui`: optional custom control UI module for this shader.
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`
@@ -113,6 +122,78 @@ Shader-visible identifiers must be valid Slang-style identifiers:
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.
## Custom Control UI
Shaders may optionally declare a custom browser control panel implemented as a standard Web Component. This only changes the bundled React control surface; it does not change shader validation, parameter storage, render cadence, or video I/O behavior.
Manifest example:
```json
{
"ui": {
"type": "webComponent",
"entry": "ui/controls.js",
"tag": "my-shader-controls"
}
}
```
The `entry` file is loaded as a JavaScript module from:
```text
/shader-assets/{shaderId}/ui/controls.js
```
Rules:
- `type` must be `webComponent`.
- `entry` must be a safe relative `.js` or `.mjs` path inside the shader package. Use the package `ui/` directory for UI assets.
- `tag` must be a valid custom element name with a hyphen, for example `my-shader-controls`.
- The `entry` file must exist when the manifest is loaded.
- Custom UI controls must update declared manifest parameters. They cannot create hidden runtime parameters or bypass host validation.
- The React UI still provides a **Default controls** fallback for each layer.
Custom element API:
```js
class MyShaderControls extends HTMLElement {
set layer(value) {}
set parameters(value) {}
set values(value) {}
connectedCallback() {}
}
customElements.define("my-shader-controls", MyShaderControls);
```
The host sets these properties whenever layer state changes:
- `layer`: full layer object from `/api/state`.
- `parameters`: array of manifest parameter definitions with current `value`.
- `values`: object keyed by parameter id.
- `setParameter(id, value)`: updates a declared layer parameter through the same route used by the default controls.
- `requestReset()`: resets the layer parameters through the normal reset route.
A custom element may either call the functions directly:
```js
this.setParameter("strength", 0.75);
this.requestReset();
```
or dispatch events:
```js
this.dispatchEvent(new CustomEvent("shader-parameter-change", {
detail: { parameterId: "strength", value: 0.75 }
}));
this.dispatchEvent(new CustomEvent("shader-reset-parameters"));
```
When the host updates the element's properties, it also dispatches `shader-layer-update` with `{ layer, parameters, values }` in `event.detail`.
## Render Passes
Most shaders should omit `passes`. The runtime then creates one implicit pass:
@@ -194,6 +275,98 @@ Pass output names:
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`.
@@ -239,6 +412,7 @@ struct ShaderContext
float bypass;
int sourceHistoryLength;
int temporalHistoryLength;
int feedbackAvailable;
};
```
@@ -257,6 +431,7 @@ Fields:
- `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:
@@ -270,17 +445,23 @@ Color/precision notes:
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);
```
`sampleVideo` samples the live decoded source video.
`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
@@ -291,6 +472,57 @@ float4 shadeVideo(ShaderContext context)
}
```
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`.
@@ -433,6 +665,40 @@ 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.
Text parameters can also choose their font from an enum parameter by setting `fontParameter` to that enum parameter's `id`:
```json
{
"fonts": [
{ "id": "inter", "path": "fonts/Inter-Regular.ttf" },
{ "id": "mono", "path": "fonts/Mono-Regular.ttf" }
],
"parameters": [
{
"id": "font",
"label": "Font",
"type": "enum",
"default": "inter",
"options": [
{ "value": "inter", "label": "Inter" },
{ "value": "mono", "label": "Mono" }
]
},
{
"id": "titleText",
"label": "Title",
"type": "text",
"default": "LIVE",
"font": "inter",
"fontParameter": "font",
"maxLength": 64
}
]
}
```
Every option `value` in the font selector enum must match a declared font asset `id`. The `font` field remains useful as the default/fallback font for the text parameter, while `fontParameter` lets operators switch atlases at runtime without adding shader-specific code.
Trigger example:
```json
@@ -460,6 +726,7 @@ Parameter validation:
- `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`.
- Text `fontParameter` values must reference an enum parameter whose option values are declared font asset IDs.
- 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.
@@ -641,6 +908,7 @@ For multipass shaders, these files reflect the most recently compiled pass. If a
- 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.
- For selectable fonts, use a text parameter `fontParameter` that points at an enum whose option values are font IDs.
- 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.
@@ -656,5 +924,7 @@ Before committing a new shader package:
- Texture files referenced by `textures` exist.
- Font files referenced by `fonts` exist.
- Enum defaults are present in their `options`.
- Text `fontParameter` selectors reference valid font assets through their enum options.
- Custom UI entries, when present, load from a safe package-relative JavaScript module and register the declared web component tag.
- Temporal shaders handle short or empty history gracefully.
- The app can reload and compile the shader without errors.

View File

@@ -40,12 +40,12 @@ 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 sampleVideo(clamp(uv, 0.0, 1.0));
return sampleLayerInput(clamp(uv, 0.0, 1.0));
if (edgeMode == 2)
return sampleVideo(float2(mirroredCoordinate(uv.x), mirroredCoordinate(uv.y)));
return sampleLayerInput(float2(mirroredCoordinate(uv.x), mirroredCoordinate(uv.y)));
float edgeMask = sourceBoundsMask(uv, resolution);
float4 color = sampleVideo(clamp(uv, 0.0, 1.0));
float4 color = sampleLayerInput(clamp(uv, 0.0, 1.0));
return lerp(outsideColor, color, edgeMask);
}

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

@@ -59,6 +59,26 @@
],
"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",

View File

@@ -61,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);
@@ -99,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));
}

Binary file not shown.

View File

@@ -0,0 +1,60 @@
Analog Mono Plus Pixel Font - License Agreement
Copyright © Andrew Gleeson, 2026
heygleeson@gmail.com
1. GRANT OF LICENSE
This Agreement is a license, not an agreement of sale. Licensee shall
not acquire any copyright ownership or equivalent rights to any of the
Licensed Content. Seller and the Licensed Content sources retain all
right, title and interest in and to all of the copyrights, trademarks,
and all other proprietary rights in the Licensed Content. All rights
in and to Licensed Content not expressly granted in this agreement are
retained by Seller or its suppliers.
Licensee is permitted to use the Licensed Content in unlimited
commercial projects. A commercial project is one defined as a Work for
Distribution launched with the capability to generate revenue, or
intention to generate revenue through the sale of, licensing of, or
otherwise intend to generate revenue directly from the Work for
Distribution.
2. RESTRICTION ON USE
Licensed Content may not be used contrary to any restriction on use
indicated herein.
Licensed Content may not be resold, sublicensed, assigned, transferred
or otherwise made available to third parties except as incorporated
into Works for Distribution.
Licensed Content may not be distributed to third parties as standalone
files or in a way that unreasonably permits the recipient to extract
the Licensed Content for use separately and apart from the Work for
Distribution.
Licensee may not distribute the Licensed Content in any library or
reusable template, including but not limited to game templates, website
templates intended to allow reproduction by third parties on electronic
or printed products.
Licensee may not distribute Licensed Content in a manner meant to enable
third parties to create derivative works incorporating Licensed Content.
Licensee may not superficially modify the Licensed Content and sell it
to others for consumption, reproduction or re-sale.
Licensee shall not use the Licensed Content in a manner that violates
the law of any applicable jurisdiction.
Licensee shall not claim copyright or attribution of Licensed Content.
3. TERM AND TERMINATION
The license contained in this Agreement terminates automatically without
notice from Seller if Licensee fails to comply with any provision of
this Agreement. Upon termination, Licensee must with immediate effect
stop using the Licensed Content, destroy, delete and remove the Licensed
Content from Licensees premises, computer systems and storage. Licensee
must also make all reasonable efforts to ensure that copies of the
licensed content are removed from any locations it has been distributed to.

View File

@@ -8,15 +8,37 @@
{
"id": "roboto",
"path": "fonts/Roboto-Regular.ttf"
},
{
"id": "analogMono",
"path": "fonts/AnalogMono.ttf"
}
],
"parameters": [
{
"id": "font",
"label": "Font",
"type": "enum",
"default": "roboto",
"options": [
{
"value": "roboto",
"label": "Roboto"
},
{
"value": "analogMono",
"label": "Analog Mono"
}
],
"description": "Font atlas used by the text overlay."
},
{
"id": "titleText",
"label": "Text",
"type": "text",
"default": "VIDEO SHADER",
"font": "roboto",
"fontParameter": "font",
"maxLength": 64,
"description": "Text string rendered into the SDF text texture."
},
@@ -46,7 +68,7 @@
"id": "scale",
"label": "Scale",
"type": "float",
"default": 0.42,
"default": 1,
"min": 0.1,
"max": 3,
"step": 0.01,
@@ -72,7 +94,7 @@
0,
0,
0,
0.8
1
],
"description": "Text outline color and alpha."
},
@@ -80,10 +102,10 @@
"id": "outlineWidth",
"label": "Outline Width",
"type": "float",
"default": 0.12,
"default": 0.22,
"min": 0,
"max": 0.5,
"step": 0.01,
"max": 1,
"step": 0.02,
"description": "Width of the SDF outline around the text."
},
{

View File

@@ -16,11 +16,18 @@ float sdfCoverage(float2 uv, float edge, float aa)
return smoothstep(edge - aa, edge + aa, distance);
}
float coverage(float distance, float edge, float aa)
{
return smoothstep(edge - aa, edge + aa, distance);
}
float4 shadeVideo(ShaderContext context)
{
float2 resolution = max(context.outputResolution, float2(1.0, 1.0));
float aspect = resolution.x / resolution.y;
float2 textSize = float2(0.72 * scale, 0.09 * scale * aspect);
float textHeight = 0.09 * scale * aspect;
float textWidth = textHeight * max(titleTextTextureAspect, 0.01) / aspect;
float2 textSize = float2(textWidth, textHeight);
float2 safeTextSize = max(textSize, float2(0.0001, 0.0001));
float2 textUv = (context.uv - position) / safeTextSize;
bool insideTextRect = textUv.x >= 0.0 && textUv.x <= 1.0 && textUv.y >= 0.0 && textUv.y <= 1.0;
@@ -30,20 +37,17 @@ float4 shadeVideo(ShaderContext context)
float aa = max(fwidth(distance) * (1.75 + softness * 5.0), 0.0025);
float2 pixelTextUv = (1.0 / resolution) / safeTextSize;
float2 sampleOffset = pixelTextUv * 0.38;
float fill = (
sdfCoverage(textUv, edge, aa) * 2.0 +
sdfCoverage(textUv + float2(sampleOffset.x, sampleOffset.y), edge, aa) +
sdfCoverage(textUv + float2(-sampleOffset.x, sampleOffset.y), edge, aa) +
sdfCoverage(textUv + float2(sampleOffset.x, -sampleOffset.y), edge, aa) +
sdfCoverage(textUv + float2(-sampleOffset.x, -sampleOffset.y), edge, aa)) / 6.0;
float outlineDistance = outlineWidth * 0.16;
float outlineEdge = edge - outlineDistance;
float outline = (
sdfCoverage(textUv, outlineEdge, aa) * 2.0 +
sdfCoverage(textUv + float2(sampleOffset.x, sampleOffset.y), outlineEdge, aa) +
sdfCoverage(textUv + float2(-sampleOffset.x, sampleOffset.y), outlineEdge, aa) +
sdfCoverage(textUv + float2(sampleOffset.x, -sampleOffset.y), outlineEdge, aa) +
sdfCoverage(textUv + float2(-sampleOffset.x, -sampleOffset.y), outlineEdge, aa)) / 6.0;
float msdfDistance = sampleTitleTextMsdf(textUv);
float msdfFill = (
coverage(msdfDistance, edge, aa) * 2.0 +
coverage(sampleTitleTextMsdf(textUv + float2(sampleOffset.x, sampleOffset.y)), edge, aa) +
coverage(sampleTitleTextMsdf(textUv + float2(-sampleOffset.x, sampleOffset.y)), edge, aa) +
coverage(sampleTitleTextMsdf(textUv + float2(sampleOffset.x, -sampleOffset.y)), edge, aa) +
coverage(sampleTitleTextMsdf(textUv + float2(-sampleOffset.x, -sampleOffset.y)), edge, aa)) / 6.0;
float sdfFill = coverage(distance, edge, aa);
float fill = min(msdfFill, sdfFill);
float outlineEdge = edge - min(outlineWidth * 0.7, 0.48);
float outline = coverage(distance, outlineEdge, aa);
float outlineAlpha = saturate(outline - fill) * outlineColor.a;
float fillAlpha = fill * fillColor.a;
float textAlpha = max(fillAlpha, outlineAlpha);

View File

@@ -24,6 +24,11 @@
"output": "layerOutput"
}
],
"ui": {
"type": "webComponent",
"entry": "ui/controls.js",
"tag": "vhs-controls"
},
"parameters": [
{
"id": "wiggle",

View File

@@ -63,6 +63,11 @@ float grainScalar(float2 uv)
return frac(sin(dot(uv, float2(12.9898, 78.233))) * 43758.5453);
}
float4 sampleVhsFrame(float2 uv)
{
return sampleVideo(saturate(uv));
}
float3 animatedChromaGrain(float2 uv, float time, float2 outputResolution, float grainSize)
{
float safeGrainSize = max(grainSize, 0.001);
@@ -142,15 +147,15 @@ float3 softBloom(float2 uv, float2 outputResolution, float radius)
float2 dx = float2(pixel.x * radius, 0.0);
float2 dy = float2(0.0, pixel.y * radius);
float3 sum = sampleVideo(frac(uv)).rgb * 0.28;
sum += sampleVideo(frac(uv + dx)).rgb * 0.14;
sum += sampleVideo(frac(uv - dx)).rgb * 0.14;
sum += sampleVideo(frac(uv + dy)).rgb * 0.14;
sum += sampleVideo(frac(uv - dy)).rgb * 0.14;
sum += sampleVideo(frac(uv + dx + dy)).rgb * 0.075;
sum += sampleVideo(frac(uv + dx - dy)).rgb * 0.075;
sum += sampleVideo(frac(uv - dx + dy)).rgb * 0.075;
sum += sampleVideo(frac(uv - dx - dy)).rgb * 0.075;
float3 sum = sampleVhsFrame(uv).rgb * 0.28;
sum += sampleVhsFrame(uv + dx).rgb * 0.14;
sum += sampleVhsFrame(uv - dx).rgb * 0.14;
sum += sampleVhsFrame(uv + dy).rgb * 0.14;
sum += sampleVhsFrame(uv - dy).rgb * 0.14;
sum += sampleVhsFrame(uv + dx + dy).rgb * 0.075;
sum += sampleVhsFrame(uv + dx - dy).rgb * 0.075;
sum += sampleVhsFrame(uv - dx + dy).rgb * 0.075;
sum += sampleVhsFrame(uv - dx - dy).rgb * 0.075;
return sum;
}
@@ -158,11 +163,11 @@ float3 softCrossBlur(float2 uv, float2 outputResolution, float radius)
{
float2 pixel = 1.0 / max(outputResolution, float2(1.0, 1.0));
float2 offset = pixel * radius;
float3 sum = sampleVideo(frac(uv)).rgb * 0.40;
sum += sampleVideo(frac(uv + float2(offset.x, 0.0))).rgb * 0.15;
sum += sampleVideo(frac(uv - float2(offset.x, 0.0))).rgb * 0.15;
sum += sampleVideo(frac(uv + float2(0.0, offset.y))).rgb * 0.15;
sum += sampleVideo(frac(uv - float2(0.0, offset.y))).rgb * 0.15;
float3 sum = sampleVhsFrame(uv).rgb * 0.40;
sum += sampleVhsFrame(uv + float2(offset.x, 0.0)).rgb * 0.15;
sum += sampleVhsFrame(uv - float2(offset.x, 0.0)).rgb * 0.15;
sum += sampleVhsFrame(uv + float2(0.0, offset.y)).rgb * 0.15;
sum += sampleVhsFrame(uv - float2(0.0, offset.y)).rgb * 0.15;
return sum;
}
@@ -174,14 +179,14 @@ float3 applyChromaCrawl(float3 color, float2 uv, float time, float2 outputResolu
float2 pixel = 1.0 / max(outputResolution, float2(1.0, 1.0));
float lumaCenter = dot(color, float3(0.299, 0.587, 0.114));
float lumaX = dot(sampleVideo(frac(uv + float2(pixel.x, 0.0))).rgb, float3(0.299, 0.587, 0.114));
float lumaY = dot(sampleVideo(frac(uv + float2(0.0, pixel.y))).rgb, float3(0.299, 0.587, 0.114));
float lumaX = dot(sampleVhsFrame(uv + float2(pixel.x, 0.0)).rgb, float3(0.299, 0.587, 0.114));
float lumaY = dot(sampleVhsFrame(uv + float2(0.0, pixel.y)).rgb, float3(0.299, 0.587, 0.114));
float edge = saturate((abs(lumaX - lumaCenter) + abs(lumaY - lumaCenter)) * 6.0);
float phase = sin(uv.y * outputResolution.y * 1.35 + time * 36.0) * cos(uv.x * outputResolution.x * 0.55 - time * 21.0);
float2 crawlOffset = float2(phase, -phase * 0.35) * pixel * (1.0 + amount * 8.0);
float3 shiftedA = sampleVideo(frac(uv + crawlOffset)).rgb;
float3 shiftedB = sampleVideo(frac(uv - crawlOffset * 0.75)).rgb;
float3 shiftedA = sampleVhsFrame(uv + crawlOffset).rgb;
float3 shiftedB = sampleVhsFrame(uv - crawlOffset * 0.75).rgb;
float3 crawled = color;
crawled.r = lerp(color.r, shiftedA.r, edge * amount);
crawled.b = lerp(color.b, shiftedB.b, edge * amount);
@@ -249,7 +254,7 @@ float3 blurVhs(float2 uv, float d, int sampleCount)
break;
float2 offset = circle(start, float(sampleCount), float(i)) * scale;
sum += sampleVideo(frac(uv + offset)).rgb * weight;
sum += sampleVhsFrame(uv + offset).rgb * weight;
}
return sum;
@@ -299,23 +304,23 @@ float4 buildTapeSmear(ShaderContext context)
float4 finishVhs(ShaderContext context)
{
float time = distortedTapeTime(context);
float3 color = sampleVideo(context.uv).rgb;
float3 color = sampleVhsFrame(context.uv).rgb;
// Radial red/blue offsets create lens and deck misregistration before the
// wider tape effects are layered in.
float2 centered = context.uv * 2.0 - 1.0;
centered.x *= context.outputResolution.x / max(context.outputResolution.y, 1.0);
float2 aberrationOffset = centered * (aberrationAmount * 0.0015);
float redAberration = sampleVideo(frac(context.uv + aberrationOffset)).r;
float blueAberration = sampleVideo(frac(context.uv - aberrationOffset)).b;
float redAberration = sampleVhsFrame(context.uv + aberrationOffset).r;
float blueAberration = sampleVhsFrame(context.uv - aberrationOffset).b;
color.r = lerp(color.r, redAberration, 0.35);
color.b = lerp(color.b, blueAberration, 0.35);
float2 halationOffset = float2(0.0015, 0.0) * (1.0 + smear * 0.35);
float3 halationSource =
sampleVideo(frac(context.uv + halationOffset)).rgb * 0.4 +
sampleVideo(frac(context.uv - halationOffset)).rgb * 0.4 +
sampleVideo(frac(context.uv + halationOffset * 2.0)).rgb * 0.2;
sampleVhsFrame(context.uv + halationOffset).rgb * 0.4 +
sampleVhsFrame(context.uv - halationOffset).rgb * 0.4 +
sampleVhsFrame(context.uv + halationOffset * 2.0).rgb * 0.2;
float halationLuma = dot(halationSource, float3(0.299, 0.587, 0.114));
float halationMask = smoothstep(0.45, 1.0, halationLuma) * halationAmount;
color += halationSource * float3(1.0, 0.38, 0.24) * halationMask * 0.35;

BIN
shaders/vhs/ui/7segment.ttf Normal file

Binary file not shown.

698
shaders/vhs/ui/controls.js vendored Normal file
View File

@@ -0,0 +1,698 @@
const PARAMETER_GROUPS = [
{
title: "Tape Transport",
note: "Deck instability and horizontal smear.",
parameters: ["wiggle", "wiggleSpeed", "smear", "blurSamples", "sharpnessDrift"],
},
{
title: "Picture Wear",
note: "Copied-tape softness, glow, fade, and edge falloff.",
parameters: ["generationLoss", "fadeAmount", "vignetteAmount", "halationAmount", "bloomAmount", "aberrationAmount"],
},
{
title: "Signal Noise",
note: "Static, grain, scanlines, and chroma shimmer.",
parameters: ["noiseAmount", "noiseSize", "staticAmount", "staticLines", "scanlineAmount", "chromaCrawlAmount"],
},
];
const PRESETS = [
{
label: "Clean SP",
values: {
wiggle: 0.02,
wiggleSpeed: 18,
smear: 0.45,
blurSamples: 9,
generationLoss: 0.06,
fadeAmount: 0.08,
noiseAmount: 0.025,
staticAmount: 0.01,
staticLines: 0.18,
scanlineAmount: 0.04,
chromaCrawlAmount: 0.015,
sharpnessDrift: 0.04,
},
},
{
label: "Rental Copy",
values: {
wiggle: 0.18,
wiggleSpeed: 31,
smear: 1.2,
blurSamples: 15,
generationLoss: 0.36,
fadeAmount: 0.33,
noiseAmount: 0.075,
staticAmount: 0.06,
staticLines: 0.85,
scanlineAmount: 0.12,
chromaCrawlAmount: 0.065,
sharpnessDrift: 0.22,
},
},
{
label: "Tracking Lost",
values: {
wiggle: 0.72,
wiggleSpeed: 64,
smear: 1.75,
blurSamples: 15,
generationLoss: 0.58,
fadeAmount: 0.48,
noiseAmount: 0.12,
staticAmount: 0.16,
staticLines: 1.2,
scanlineAmount: 0.2,
chromaCrawlAmount: 0.14,
sharpnessDrift: 0.45,
},
},
];
const SEGMENT_FONT_FAMILY = "VhsSevenSegment";
const SEGMENT_FONT_URL = new URL("./7segment.ttf", import.meta.url).href;
let segmentFontLoadPromise = null;
function ensureSegmentFontLoaded() {
if (segmentFontLoadPromise || !("FontFace" in window) || !document.fonts) {
return segmentFontLoadPromise;
}
const font = new FontFace(SEGMENT_FONT_FAMILY, `url("${SEGMENT_FONT_URL}")`);
segmentFontLoadPromise = font.load()
.then((loadedFont) => {
document.fonts.add(loadedFont);
return loadedFont;
})
.catch(() => null);
return segmentFontLoadPromise;
}
function numberValue(parameter) {
const value = Number(parameter?.value ?? parameter?.defaultValue ?? 0);
return Number.isFinite(value) ? value : 0;
}
function formatValue(value, step) {
if (step >= 1) {
return String(Math.round(value));
}
if (step >= 0.05) {
return value.toFixed(2);
}
return value.toFixed(3);
}
class VhsControls extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: "open" });
this._parameters = [];
this._parameterMap = new Map();
this._values = {};
this._setParameter = null;
}
set layer(value) {
this._layer = value;
}
set parameters(value) {
this._parameters = Array.isArray(value) ? value : [];
this._parameterMap = new Map(this._parameters.map((parameter) => [parameter.id, parameter]));
this.render();
}
set values(value) {
this._values = value && typeof value === "object" ? value : {};
this.render();
}
set setParameter(callback) {
this._setParameter = typeof callback === "function" ? callback : null;
}
set requestReset(callback) {
this._requestReset = typeof callback === "function" ? callback : null;
}
connectedCallback() {
ensureSegmentFontLoaded();
this.render();
}
updateParameter(parameterId, value) {
if (this._setParameter) {
this._setParameter(parameterId, value);
return;
}
this.dispatchEvent(new CustomEvent("shader-parameter-change", {
detail: { parameterId, value },
bubbles: true,
composed: true,
}));
}
applyPreset(preset) {
for (const [parameterId, value] of Object.entries(preset.values)) {
if (this._parameterMap.has(parameterId)) {
this.updateParameter(parameterId, value);
}
}
}
renderSlider(parameter) {
const value = numberValue(parameter);
const min = Number(parameter.min?.[0] ?? 0);
const max = Number(parameter.max?.[0] ?? 1);
const step = Number(parameter.step?.[0] ?? 0.01);
const safeMin = Number.isFinite(min) ? min : 0;
const safeMax = Number.isFinite(max) && max > safeMin ? max : safeMin + 1;
const safeStep = Number.isFinite(step) && step > 0 ? step : 0.01;
const percent = Math.max(0, Math.min(100, ((value - safeMin) / (safeMax - safeMin)) * 100));
const angle = -135 + percent * 2.7;
return `
<label class="control" title="${parameter.description || ""}" style="--angle: ${angle}deg; --arc: ${percent * 2.7}deg">
<span class="control__head">
<span>${parameter.label || parameter.id}</span>
<output>${formatValue(value, safeStep)}</output>
</span>
<span class="knob-stage">
<span class="knob" aria-hidden="true"><span></span></span>
<input
type="range"
data-parameter="${parameter.id}"
min="${safeMin}"
max="${safeMax}"
step="${safeStep}"
value="${value}"
>
</span>
</label>
`;
}
render() {
if (!this.shadowRoot) {
return;
}
const groups = PARAMETER_GROUPS.map((group) => {
const controls = group.parameters
.map((parameterId) => this._parameterMap.get(parameterId))
.filter(Boolean)
.map((parameter) => this.renderSlider(parameter))
.join("");
if (!controls) {
return "";
}
return `
<section class="group">
<div class="group__header">
<h4>${group.title}</h4>
<p>${group.note}</p>
</div>
<div class="controls">${controls}</div>
</section>
`;
}).join("");
this.shadowRoot.innerHTML = `
<style>
@font-face {
font-family: "${SEGMENT_FONT_FAMILY}";
src: url("${SEGMENT_FONT_URL}") format("truetype");
font-display: swap;
}
:host {
display: block;
color: #151a1b;
font-family: "Arial Narrow", "Roboto Condensed", system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
}
* {
box-sizing: border-box;
}
.vhs-panel {
display: grid;
gap: 8px;
overflow: hidden;
padding: 13px 13px 20px;
border: 1px solid #252a2a;
border-radius: 3px;
background:
repeating-linear-gradient(90deg, rgba(255, 255, 255, 0.08) 0 1px, transparent 1px 5px),
repeating-linear-gradient(0deg, rgba(0, 0, 0, 0.035) 0 1px, transparent 1px 6px),
linear-gradient(#cfd2c2, #aeb3a4 54%, #8d9387);
box-shadow:
inset 0 1px 0 rgba(255, 255, 255, 0.62),
inset 0 -2px 5px rgba(0, 0, 0, 0.3),
0 10px 24px rgba(0, 0, 0, 0.24);
}
.tab {
display: grid;
place-items: center;
min-height: 28px;
padding: 0 11px;
border: 1px solid #282c29;
border-radius: 1px;
background: linear-gradient(#eceee5, #b9beb1);
color: #2d2f2b;
font-size: 11px;
font-weight: 800;
letter-spacing: 0.04em;
text-transform: uppercase;
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.62), 0 1px 2px rgba(0, 0, 0, 0.2);
}
.faceplate {
display: grid;
grid-template-columns: minmax(0, 1fr) auto;
gap: 12px;
min-height: 94px;
padding: 0;
border-bottom: 0;
background:
linear-gradient(90deg, rgba(255, 255, 255, 0.16), transparent),
transparent;
}
.brand {
position: relative;
min-width: 0;
padding: 18px 18px 14px 76px;
border: 1px solid rgba(0, 0, 0, 0.24);
border-radius: 2px;
background:
radial-gradient(circle at 96% 82%, rgba(255, 255, 255, 0.42) 0 10px, transparent 11px),
linear-gradient(90deg, rgba(255, 255, 255, 0.32), transparent 46%),
linear-gradient(#dbddd2, #c5c8b9);
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.6);
}
.brand::before,
.brand::after {
content: "";
position: absolute;
width: 18px;
height: 18px;
border-radius: 50%;
border: 1px solid rgba(0, 0, 0, 0.08);
background: rgba(255, 255, 255, 0.24);
}
.brand::before {
top: 18px;
left: 32px;
}
.brand::after {
right: 14px;
bottom: 12px;
}
.brand h3 {
margin: 0;
color: #272727;
font-size: clamp(30px, 6vw, 44px);
font-weight: 260;
line-height: 0.9;
letter-spacing: 0;
}
.brand strong {
display: block;
margin-top: 5px;
color: #ff3f18;
font-size: 15px;
font-weight: 680;
letter-spacing: 0.08em;
text-transform: uppercase;
}
.brand p {
margin: 10px 0 0;
color: #3c3b38;
font-size: 13px;
font-weight: 700;
letter-spacing: 0.05em;
text-transform: uppercase;
}
.display {
display: grid;
grid-template-columns: minmax(0, 1fr) auto;
gap: 14px;
align-items: center;
padding: 10px 12px;
background:
linear-gradient(125deg, rgba(255, 255, 255, 0.06), transparent 34%),
linear-gradient(#566052, #232925);
border: 1px solid #222;
border-radius: 2px;
box-shadow:
inset 0 18px 28px rgba(255, 255, 255, 0.03),
inset 0 -12px 24px rgba(0, 0, 0, 0.62);
}
.lcd {
min-width: 128px;
justify-self: stretch;
min-height: 62px;
padding: 9px 14px;
border: 2px solid #55604f;
border-radius: 2px;
background:
repeating-linear-gradient(0deg, rgba(32, 48, 32, 0.12) 0 1px, transparent 1px 3px),
linear-gradient(rgba(255, 255, 255, 0.18), transparent 48%),
linear-gradient(#a5af8e, #778269);
color: #111717;
font-family: "${SEGMENT_FONT_FAMILY}", ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
font-size: clamp(26px, 5vw, 42px);
font-weight: 400;
letter-spacing: 0;
line-height: 1;
text-shadow: 0 1px 0 rgba(255, 255, 255, 0.18);
}
.lcd small {
display: block;
margin-bottom: 3px;
color: #1f2b22;
font-family: "Arial Narrow", "Roboto Condensed", system-ui, sans-serif;
font-size: 10px;
font-weight: 800;
letter-spacing: 0.06em;
vertical-align: top;
text-shadow: none;
}
.master-knob {
position: relative;
width: 64px;
height: 64px;
border-radius: 50%;
background: radial-gradient(circle at 35% 28%, #4a4a4a, #050505 68%);
box-shadow:
inset 0 0 0 5px #191919,
inset 0 0 0 7px #333,
0 2px 4px rgba(0, 0, 0, 0.38);
}
.presets {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 7px;
padding: 0;
}
button {
min-height: 34px;
border: 1px solid #0a0a0a;
border-radius: 2px;
background: linear-gradient(#22262a, #08090a);
color: #f5f1eb;
font: inherit;
font-size: 11px;
font-weight: 700;
letter-spacing: 0.04em;
text-transform: uppercase;
cursor: pointer;
box-shadow:
inset 0 1px 0 rgba(255, 255, 255, 0.18),
inset 0 -3px 0 rgba(0, 0, 0, 0.42),
0 2px 3px rgba(0, 0, 0, 0.3);
}
button:hover {
background: linear-gradient(#ff6b2f, #df2d0a);
color: #fff;
box-shadow:
inset 0 1px 0 rgba(255, 255, 255, 0.3),
inset 0 -3px 0 rgba(80, 12, 0, 0.36),
0 0 14px rgba(255, 73, 25, 0.36);
}
.controls-shell {
position: relative;
padding: 0;
background:
linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.18), transparent),
transparent;
}
.controls-shell::after {
content: "REMOTE CONTROL PANEL";
display: block;
margin-top: 9px;
color: #474a43;
font-size: 10px;
font-weight: 700;
letter-spacing: 0.07em;
text-align: center;
text-transform: uppercase;
}
.group {
display: grid;
gap: 9px;
padding: 12px 13px 14px;
margin-top: 10px;
border: 1px solid rgba(0, 0, 0, 0.36);
border-radius: 2px;
background:
linear-gradient(90deg, rgba(255, 255, 255, 0.12), transparent 42%),
linear-gradient(#cdd0c1, #adb2a3);
box-shadow:
inset 0 1px 0 rgba(255, 255, 255, 0.55),
inset 0 -1px 0 rgba(0, 0, 0, 0.17);
}
.group__header {
display: flex;
align-items: baseline;
justify-content: space-between;
gap: 12px;
}
h4 {
margin: 0;
color: #242321;
font-size: 15px;
letter-spacing: 0.04em;
text-transform: uppercase;
}
.group__header p {
margin: 0;
color: #55504a;
font-size: 11px;
text-align: right;
}
.controls {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(128px, 1fr));
gap: 10px;
}
.control {
position: relative;
display: grid;
gap: 6px;
min-width: 0;
padding: 10px 10px 12px;
border: 1px solid rgba(30, 34, 30, 0.36);
border-radius: 2px;
background:
linear-gradient(90deg, rgba(255, 255, 255, 0.22), transparent 58%),
#c8ccbd;
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.45), inset 0 -1px 0 rgba(0, 0, 0, 0.16);
}
.control::before {
content: "";
position: absolute;
left: 8px;
bottom: 8px;
width: 6px;
height: 6px;
border-radius: 50%;
background: #1d201d;
box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.22);
}
.control:nth-child(3n + 1)::before {
background: #e13b1a;
box-shadow: 0 0 6px rgba(255, 65, 30, 0.62);
}
.control:nth-child(3n + 2)::before {
background: #1788d3;
box-shadow: 0 0 6px rgba(50, 160, 255, 0.5);
}
.control__head {
display: flex;
align-items: baseline;
justify-content: space-between;
gap: 8px;
color: #252525;
font-size: 10px;
font-weight: 740;
letter-spacing: 0.03em;
text-transform: uppercase;
}
output {
min-width: 48px;
padding: 3px 6px;
border: 1px solid rgba(0, 0, 0, 0.4);
border-radius: 2px;
background: #1d2720;
color: #e5ffd0;
font-family: "${SEGMENT_FONT_FAMILY}", ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
font-size: 13px;
font-weight: 400;
text-align: right;
text-shadow: 0 0 5px rgba(190, 255, 150, 0.26);
}
.knob-stage {
position: relative;
display: grid;
place-items: center;
min-height: 74px;
}
.knob {
position: relative;
width: 58px;
height: 58px;
border-radius: 50%;
background:
radial-gradient(circle at 35% 30%, #555, #1d1d1d 56%, #030303 100%);
box-shadow:
0 0 0 7px #777d72,
0 0 0 7px rgba(20, 22, 20, 0.62),
inset 0 2px 4px rgba(255, 255, 255, 0.18),
inset 0 -6px 10px rgba(0, 0, 0, 0.64),
0 4px 7px rgba(0, 0, 0, 0.45);
}
.knob::before {
content: "";
position: absolute;
inset: -12px;
border-radius: 50%;
background:
conic-gradient(from -135deg, #f4511e 0 var(--arc), #2e342f var(--arc) 270deg, transparent 270deg 360deg);
-webkit-mask: radial-gradient(circle, transparent 0 62%, #000 63% 70%, transparent 71%);
mask: radial-gradient(circle, transparent 0 62%, #000 63% 70%, transparent 71%);
opacity: 0.92;
z-index: -1;
}
.knob span {
position: absolute;
left: 50%;
top: 8px;
width: 3px;
height: 19px;
border-radius: 999px;
background: #f1f1ed;
transform: translateX(-50%) rotate(var(--angle));
transform-origin: 50% 21px;
box-shadow: 0 0 4px rgba(255, 255, 255, 0.45);
}
input[type="range"] {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
margin: 0;
opacity: 0;
cursor: pointer;
background: transparent;
}
@media (max-width: 560px) {
.faceplate,
.display,
.group__header,
.presets {
grid-template-columns: 1fr;
}
.display {
padding: 8px;
}
.group__header {
display: grid;
}
.group__header p {
text-align: left;
}
}
</style>
<div class="vhs-panel">
<header class="faceplate">
<div class="brand">
<h3>TX-VHS</h3>
<strong>Video Tape Remote</strong>
<p>Remote Control Panel</p>
</div>
<span class="tab" aria-hidden="true">Remote Control Panel</span>
</header>
<section class="display">
<div class="lcd"><small>TRACK</small> 03.7</div>
<div class="master-knob" aria-hidden="true"></div>
</section>
<div class="presets">
${PRESETS.map((preset, index) => `<button type="button" data-preset="${index}">${preset.label}</button>`).join("")}
</div>
<div class="controls-shell">
${groups}
</div>
</div>
`;
this.shadowRoot.querySelectorAll("input[data-parameter]").forEach((input) => {
input.addEventListener("input", (event) => {
const target = event.currentTarget;
const parameterId = target.dataset.parameter;
const value = Number(target.value);
const percent = Math.max(0, Math.min(100, ((value - Number(target.min)) / (Number(target.max) - Number(target.min))) * 100));
const control = target.closest(".control");
if (control) {
control.style.setProperty("--angle", `${-135 + percent * 2.7}deg`);
control.style.setProperty("--arc", `${percent * 2.7}deg`);
const output = control.querySelector("output");
if (output) {
output.textContent = formatValue(value, Number(target.step));
}
}
this.updateParameter(parameterId, value);
});
});
this.shadowRoot.querySelectorAll("button[data-preset]").forEach((button) => {
button.addEventListener("click", () => {
const preset = PRESETS[Number(button.dataset.preset)];
if (preset) {
this.applyPreset(preset);
}
});
});
}
}
customElements.define("vhs-controls", VhsControls);

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