Compare commits
11 Commits
0c16665610
...
v0.0.5
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c38c22834d | ||
|
|
c8a4bd4c7b | ||
|
|
46129a6044 | ||
|
|
8fcb51d140 | ||
|
|
944773c248 | ||
|
|
7777cfc194 | ||
|
|
198639ae3f | ||
|
|
d7ca42b51b | ||
|
|
f11d531e0c | ||
|
|
a3635b5d31 | ||
|
|
bc9aa6fbad |
@@ -49,10 +49,6 @@ set(APP_SOURCES
|
|||||||
"${APP_DIR}/videoio/decklink/DeckLinkSession.h"
|
"${APP_DIR}/videoio/decklink/DeckLinkSession.h"
|
||||||
"${APP_DIR}/videoio/decklink/DeckLinkVideoIOFormat.cpp"
|
"${APP_DIR}/videoio/decklink/DeckLinkVideoIOFormat.cpp"
|
||||||
"${APP_DIR}/videoio/decklink/DeckLinkVideoIOFormat.h"
|
"${APP_DIR}/videoio/decklink/DeckLinkVideoIOFormat.h"
|
||||||
"${APP_DIR}/videoio/VideoIOBackendFactory.cpp"
|
|
||||||
"${APP_DIR}/videoio/VideoIOBackendFactory.h"
|
|
||||||
"${APP_DIR}/videoio/VideoIOConfig.cpp"
|
|
||||||
"${APP_DIR}/videoio/VideoIOConfig.h"
|
|
||||||
"${APP_DIR}/gl/renderer/GLExtensions.cpp"
|
"${APP_DIR}/gl/renderer/GLExtensions.cpp"
|
||||||
"${APP_DIR}/gl/renderer/GLExtensions.h"
|
"${APP_DIR}/gl/renderer/GLExtensions.h"
|
||||||
"${APP_DIR}/gl/shader/GlobalParamsBuffer.cpp"
|
"${APP_DIR}/gl/shader/GlobalParamsBuffer.cpp"
|
||||||
@@ -69,6 +65,8 @@ set(APP_SOURCES
|
|||||||
"${APP_DIR}/gl/pipeline/OpenGLRenderPipeline.cpp"
|
"${APP_DIR}/gl/pipeline/OpenGLRenderPipeline.cpp"
|
||||||
"${APP_DIR}/gl/pipeline/OpenGLRenderPipeline.h"
|
"${APP_DIR}/gl/pipeline/OpenGLRenderPipeline.h"
|
||||||
"${APP_DIR}/gl/pipeline/RenderPassDescriptor.h"
|
"${APP_DIR}/gl/pipeline/RenderPassDescriptor.h"
|
||||||
|
"${APP_DIR}/gl/pipeline/ShaderFeedbackBuffers.cpp"
|
||||||
|
"${APP_DIR}/gl/pipeline/ShaderFeedbackBuffers.h"
|
||||||
"${APP_DIR}/gl/renderer/OpenGLRenderer.cpp"
|
"${APP_DIR}/gl/renderer/OpenGLRenderer.cpp"
|
||||||
"${APP_DIR}/gl/renderer/OpenGLRenderer.h"
|
"${APP_DIR}/gl/renderer/OpenGLRenderer.h"
|
||||||
"${APP_DIR}/gl/renderer/RenderTargetPool.cpp"
|
"${APP_DIR}/gl/renderer/RenderTargetPool.cpp"
|
||||||
@@ -208,35 +206,6 @@ endif()
|
|||||||
|
|
||||||
add_test(NAME RuntimeParameterUtilsTests COMMAND RuntimeParameterUtilsTests)
|
add_test(NAME RuntimeParameterUtilsTests COMMAND RuntimeParameterUtilsTests)
|
||||||
|
|
||||||
add_executable(RuntimeHostVideoIOStateTests
|
|
||||||
"${APP_DIR}/runtime/RuntimeHost.cpp"
|
|
||||||
"${APP_DIR}/runtime/RuntimeClock.cpp"
|
|
||||||
"${APP_DIR}/runtime/RuntimeJson.cpp"
|
|
||||||
"${APP_DIR}/runtime/RuntimeParameterUtils.cpp"
|
|
||||||
"${APP_DIR}/shader/ShaderCompiler.cpp"
|
|
||||||
"${APP_DIR}/shader/ShaderPackageRegistry.cpp"
|
|
||||||
"${APP_DIR}/videoio/VideoIOConfig.cpp"
|
|
||||||
"${CMAKE_CURRENT_SOURCE_DIR}/tests/RuntimeHostVideoIOStateTests.cpp"
|
|
||||||
)
|
|
||||||
|
|
||||||
target_include_directories(RuntimeHostVideoIOStateTests PRIVATE
|
|
||||||
"${APP_DIR}"
|
|
||||||
"${APP_DIR}/platform"
|
|
||||||
"${APP_DIR}/runtime"
|
|
||||||
"${APP_DIR}/shader"
|
|
||||||
"${APP_DIR}/videoio"
|
|
||||||
)
|
|
||||||
|
|
||||||
target_link_libraries(RuntimeHostVideoIOStateTests PRIVATE
|
|
||||||
Advapi32
|
|
||||||
)
|
|
||||||
|
|
||||||
if(MSVC)
|
|
||||||
target_compile_options(RuntimeHostVideoIOStateTests PRIVATE /W3)
|
|
||||||
endif()
|
|
||||||
|
|
||||||
add_test(NAME RuntimeHostVideoIOStateTests COMMAND RuntimeHostVideoIOStateTests)
|
|
||||||
|
|
||||||
add_executable(Std140BufferTests
|
add_executable(Std140BufferTests
|
||||||
"${CMAKE_CURRENT_SOURCE_DIR}/tests/Std140BufferTests.cpp"
|
"${CMAKE_CURRENT_SOURCE_DIR}/tests/Std140BufferTests.cpp"
|
||||||
)
|
)
|
||||||
@@ -351,7 +320,6 @@ endif()
|
|||||||
add_test(NAME VideoPlayoutSchedulerTests COMMAND VideoPlayoutSchedulerTests)
|
add_test(NAME VideoPlayoutSchedulerTests COMMAND VideoPlayoutSchedulerTests)
|
||||||
|
|
||||||
add_executable(VideoIODeviceFakeTests
|
add_executable(VideoIODeviceFakeTests
|
||||||
"${APP_DIR}/videoio/VideoIOConfig.cpp"
|
|
||||||
"${APP_DIR}/videoio/VideoIOFormat.cpp"
|
"${APP_DIR}/videoio/VideoIOFormat.cpp"
|
||||||
"${CMAKE_CURRENT_SOURCE_DIR}/tests/VideoIODeviceFakeTests.cpp"
|
"${CMAKE_CURRENT_SOURCE_DIR}/tests/VideoIODeviceFakeTests.cpp"
|
||||||
)
|
)
|
||||||
@@ -368,43 +336,6 @@ endif()
|
|||||||
|
|
||||||
add_test(NAME VideoIODeviceFakeTests COMMAND VideoIODeviceFakeTests)
|
add_test(NAME VideoIODeviceFakeTests COMMAND VideoIODeviceFakeTests)
|
||||||
|
|
||||||
add_executable(VideoIOBackendFactoryTests
|
|
||||||
"${APP_DIR}/videoio/decklink/DeckLinkAPI_i.c"
|
|
||||||
"${APP_DIR}/videoio/decklink/DeckLinkSession.cpp"
|
|
||||||
"${APP_DIR}/videoio/decklink/DeckLinkSession.h"
|
|
||||||
"${APP_DIR}/videoio/decklink/DeckLinkDisplayMode.cpp"
|
|
||||||
"${APP_DIR}/videoio/decklink/DeckLinkDisplayMode.h"
|
|
||||||
"${APP_DIR}/videoio/decklink/DeckLinkVideoIOFormat.cpp"
|
|
||||||
"${APP_DIR}/videoio/decklink/DeckLinkVideoIOFormat.h"
|
|
||||||
"${APP_DIR}/videoio/decklink/DeckLinkFrameTransfer.cpp"
|
|
||||||
"${APP_DIR}/videoio/decklink/DeckLinkFrameTransfer.h"
|
|
||||||
"${APP_DIR}/videoio/VideoIOBackendFactory.cpp"
|
|
||||||
"${APP_DIR}/videoio/VideoIOBackendFactory.h"
|
|
||||||
"${APP_DIR}/videoio/VideoIOConfig.cpp"
|
|
||||||
"${APP_DIR}/videoio/VideoIOConfig.h"
|
|
||||||
"${APP_DIR}/videoio/VideoIOFormat.cpp"
|
|
||||||
"${APP_DIR}/videoio/VideoPlayoutScheduler.cpp"
|
|
||||||
"${APP_DIR}/videoio/VideoPlayoutScheduler.h"
|
|
||||||
"${CMAKE_CURRENT_SOURCE_DIR}/tests/VideoIOBackendFactoryTests.cpp"
|
|
||||||
)
|
|
||||||
|
|
||||||
target_include_directories(VideoIOBackendFactoryTests PRIVATE
|
|
||||||
"${APP_DIR}"
|
|
||||||
"${APP_DIR}/gl/renderer"
|
|
||||||
"${APP_DIR}/videoio"
|
|
||||||
"${APP_DIR}/videoio/decklink"
|
|
||||||
)
|
|
||||||
|
|
||||||
target_link_libraries(VideoIOBackendFactoryTests PRIVATE
|
|
||||||
Ole32
|
|
||||||
)
|
|
||||||
|
|
||||||
if(MSVC)
|
|
||||||
target_compile_options(VideoIOBackendFactoryTests PRIVATE /W3)
|
|
||||||
endif()
|
|
||||||
|
|
||||||
add_test(NAME VideoIOBackendFactoryTests COMMAND VideoIOBackendFactoryTests)
|
|
||||||
|
|
||||||
install(TARGETS LoopThroughWithOpenGLCompositing
|
install(TARGETS LoopThroughWithOpenGLCompositing
|
||||||
RUNTIME DESTINATION "."
|
RUNTIME DESTINATION "."
|
||||||
)
|
)
|
||||||
@@ -418,7 +349,7 @@ install(FILES "${SLANG_LICENSE_FILE}"
|
|||||||
RENAME "SLANG_LICENSE.txt"
|
RENAME "SLANG_LICENSE.txt"
|
||||||
)
|
)
|
||||||
|
|
||||||
install(FILES "${CMAKE_CURRENT_SOURCE_DIR}/SHADER_CONTRACT.md"
|
install(FILES "${CMAKE_CURRENT_SOURCE_DIR}/shaders/SHADER_CONTRACT.md"
|
||||||
DESTINATION "."
|
DESTINATION "."
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
109
OSC/Test.json
109
OSC/Test.json
@@ -36,7 +36,7 @@
|
|||||||
"preArgs": "",
|
"preArgs": "",
|
||||||
"typeTags": "",
|
"typeTags": "",
|
||||||
"decimals": 2,
|
"decimals": 2,
|
||||||
"target": "127.0.0.1:9000",
|
"target": "192.168.1.46:9000",
|
||||||
"ignoreDefaults": false,
|
"ignoreDefaults": false,
|
||||||
"bypass": false,
|
"bypass": false,
|
||||||
"onCreate": "",
|
"onCreate": "",
|
||||||
@@ -53,8 +53,8 @@
|
|||||||
"visible": true,
|
"visible": true,
|
||||||
"interaction": true,
|
"interaction": true,
|
||||||
"comments": "XY control for Fisheye Reproject pan and tilt.",
|
"comments": "XY control for Fisheye Reproject pan and tilt.",
|
||||||
"width": 420,
|
"width": 460,
|
||||||
"height": 420,
|
"height": 250,
|
||||||
"expand": false,
|
"expand": false,
|
||||||
"colorText": "auto",
|
"colorText": "auto",
|
||||||
"colorWidget": "auto",
|
"colorWidget": "auto",
|
||||||
@@ -70,14 +70,14 @@
|
|||||||
"css": "",
|
"css": "",
|
||||||
"pips": true,
|
"pips": true,
|
||||||
"snap": false,
|
"snap": false,
|
||||||
"spring": false,
|
"spring": true,
|
||||||
"rangeX": {
|
"rangeX": {
|
||||||
"min": -60,
|
"min": -1,
|
||||||
"max": 60
|
"max": 1
|
||||||
},
|
},
|
||||||
"rangeY": {
|
"rangeY": {
|
||||||
"min": 45,
|
"min": 1,
|
||||||
"max": -45
|
"max": -1
|
||||||
},
|
},
|
||||||
"logScaleX": false,
|
"logScaleX": false,
|
||||||
"logScaleY": false,
|
"logScaleY": false,
|
||||||
@@ -94,13 +94,13 @@
|
|||||||
"address": "/VideoShaderToys/fisheye-reproject/xy",
|
"address": "/VideoShaderToys/fisheye-reproject/xy",
|
||||||
"preArgs": "",
|
"preArgs": "",
|
||||||
"typeTags": "",
|
"typeTags": "",
|
||||||
"decimals": "2f",
|
"decimals": "3f",
|
||||||
"target": "127.0.0.1:9000",
|
"target": "192.168.1.46:9000",
|
||||||
"ignoreDefaults": false,
|
"ignoreDefaults": false,
|
||||||
"bypass": true,
|
"bypass": true,
|
||||||
"onCreate": "",
|
"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 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});",
|
"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": "",
|
"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,
|
"pointSize": 20,
|
||||||
"ephemeral": false,
|
"ephemeral": false,
|
||||||
"label": "",
|
"label": "",
|
||||||
@@ -121,7 +121,7 @@
|
|||||||
"interaction": true,
|
"interaction": true,
|
||||||
"comments": "",
|
"comments": "",
|
||||||
"width": 90,
|
"width": 90,
|
||||||
"height": 420,
|
"height": 250,
|
||||||
"expand": false,
|
"expand": false,
|
||||||
"colorText": "auto",
|
"colorText": "auto",
|
||||||
"colorWidget": "auto",
|
"colorWidget": "auto",
|
||||||
@@ -144,90 +144,29 @@
|
|||||||
"gradient": [],
|
"gradient": [],
|
||||||
"snap": false,
|
"snap": false,
|
||||||
"touchZone": "all",
|
"touchZone": "all",
|
||||||
"spring": false,
|
"spring": true,
|
||||||
"doubleTap": false,
|
"doubleTap": false,
|
||||||
"range": {
|
"range": {
|
||||||
"min": 100,
|
"min": -1,
|
||||||
"max": 10
|
"max": 1
|
||||||
},
|
},
|
||||||
"logScale": false,
|
"logScale": false,
|
||||||
"sensitivity": 1,
|
"sensitivity": 1,
|
||||||
"steps": "",
|
"steps": "",
|
||||||
"origin": "auto",
|
"origin": "auto",
|
||||||
"value": "",
|
"value": 0,
|
||||||
"default": 90,
|
"default": 0,
|
||||||
"linkId": "",
|
"linkId": "",
|
||||||
"address": "/VideoShaderToys/fisheye-reproject/virtualFovDegrees",
|
"address": "/VideoShaderToys/fisheye-reproject/virtualFovDegrees",
|
||||||
"preArgs": "",
|
"preArgs": "",
|
||||||
"typeTags": "",
|
"typeTags": "",
|
||||||
"decimals": 2,
|
"decimals": "3f",
|
||||||
"target": "127.0.0.1:9000",
|
"target": "192.168.1.46: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": "",
|
|
||||||
"ignoreDefaults": false,
|
"ignoreDefaults": false,
|
||||||
"bypass": true,
|
"bypass": true,
|
||||||
"onCreate": "",
|
"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 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});",
|
"onValue": "var state = globalThis.__fisheyeFovStick = globalThis.__fisheyeFovStick || {};\nvar stick = Number(value);\nstate.stick = isFinite(stick) ? state.applyCurve(stick) : 0;",
|
||||||
"onTouch": ""
|
"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": []
|
"tabs": []
|
||||||
|
|||||||
@@ -141,7 +141,9 @@ Current native test coverage includes:
|
|||||||
{
|
{
|
||||||
"shaderLibrary": "shaders",
|
"shaderLibrary": "shaders",
|
||||||
"serverPort": 8080,
|
"serverPort": 8080,
|
||||||
|
"oscBindAddress": "127.0.0.1",
|
||||||
"oscPort": 9000,
|
"oscPort": 9000,
|
||||||
|
"oscSmoothing": 0.18,
|
||||||
"inputVideoFormat": "1080p",
|
"inputVideoFormat": "1080p",
|
||||||
"inputFrameRate": "59.94",
|
"inputFrameRate": "59.94",
|
||||||
"outputVideoFormat": "1080p",
|
"outputVideoFormat": "1080p",
|
||||||
@@ -203,13 +205,13 @@ runtime/screenshots/
|
|||||||
|
|
||||||
## OSC Control
|
## OSC Control
|
||||||
|
|
||||||
The native host also listens for local OSC parameter control on the configured `oscPort`:
|
The native host also listens for OSC parameter control on the configured `oscBindAddress` and `oscPort`:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
/VideoShaderToys/{LayerNameOrID}/{ParameterNameOrID}
|
/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 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. OSC updates are coalesced and applied once per render tick, UI state broadcasts are throttled, and OSC-driven parameter changes are not autosaved to `runtime/runtime_state.json`. `oscSmoothing` adds a small per-frame easing amount for numeric OSC controls such as floats, `vec2`, and `color`, while booleans, enums, text, and triggers stay immediate. The default bind address is `127.0.0.1`; set `oscBindAddress` to `0.0.0.0` to accept OSC on all IPv4 interfaces. See `docs/OSC_CONTROL.md` for details.
|
||||||
|
|
||||||
## Shader Packages
|
## Shader Packages
|
||||||
|
|
||||||
@@ -275,3 +277,4 @@ If `SLANG_ROOT` is not set, the workflow falls back to the repo-local default un
|
|||||||
- Mipmapping for shader-declared textures
|
- Mipmapping for shader-declared textures
|
||||||
- Anotate included shaders
|
- Anotate included shaders
|
||||||
- allow 3 vector exposed controls
|
- allow 3 vector exposed controls
|
||||||
|
- add nearest sampling to the extra shader pass
|
||||||
@@ -412,10 +412,10 @@ LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Setup OpenGL and video I/O capture/playout object
|
// Setup OpenGL and DeckLink capture and playout object
|
||||||
pOpenGLComposite = new OpenGLComposite(hWnd, hDC, hRC);
|
pOpenGLComposite = new OpenGLComposite(hWnd, hDC, hRC);
|
||||||
|
|
||||||
if (pOpenGLComposite->InitializeVideoIO())
|
if (pOpenGLComposite->InitDeckLink())
|
||||||
{
|
{
|
||||||
wglMakeCurrent( NULL, NULL );
|
wglMakeCurrent( NULL, NULL );
|
||||||
if (pOpenGLComposite->Start())
|
if (pOpenGLComposite->Start())
|
||||||
@@ -423,11 +423,11 @@ LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
|
|||||||
PostMessage(hWnd, kCreateStatusStripMessage, 0, 0);
|
PostMessage(hWnd, kCreateStatusStripMessage, 0, 0);
|
||||||
break; // success
|
break; // success
|
||||||
}
|
}
|
||||||
MessageBoxA(NULL, "The OpenGL/video I/O runtime initialized, but playout failed to start. See the previous start message for the failing call.", "Startup failed", MB_OK | MB_ICONERROR);
|
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
|
else
|
||||||
{
|
{
|
||||||
MessageBoxA(NULL, "The OpenGL/video I/O runtime failed to initialize. See the previous initialization message for the failing call.", "Startup failed", MB_OK | MB_ICONERROR);
|
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
|
// Failed to initialize - cleanup
|
||||||
@@ -438,7 +438,7 @@ LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
|
|||||||
}
|
}
|
||||||
catch (...)
|
catch (...)
|
||||||
{
|
{
|
||||||
ShowUnhandledExceptionMessage("Startup failed while creating the OpenGL/video I/O runtime.");
|
ShowUnhandledExceptionMessage("Startup failed while creating the OpenGL/DeckLink runtime.");
|
||||||
PostMessage(hWnd, WM_CLOSE, 0, 0);
|
PostMessage(hWnd, WM_CLOSE, 0, 0);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -474,7 +474,7 @@ LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
|
|||||||
}
|
}
|
||||||
catch (...)
|
catch (...)
|
||||||
{
|
{
|
||||||
ShowUnhandledExceptionMessage("Shutdown failed while tearing down the OpenGL/video I/O runtime.");
|
ShowUnhandledExceptionMessage("Shutdown failed while tearing down the OpenGL/DeckLink runtime.");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Deselect the current rendering context and delete it
|
// Deselect the current rendering context and delete it
|
||||||
@@ -531,7 +531,7 @@ LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
|
|||||||
if (!sInteractiveResize && pOpenGLComposite)
|
if (!sInteractiveResize && pOpenGLComposite)
|
||||||
{
|
{
|
||||||
wglMakeCurrent(hDC, hRC);
|
wglMakeCurrent(hDC, hRC);
|
||||||
pOpenGLComposite->paintGL();
|
pOpenGLComposite->paintGL(true);
|
||||||
wglMakeCurrent( NULL, NULL );
|
wglMakeCurrent( NULL, NULL );
|
||||||
RaiseStatusControls(sStatusStrip);
|
RaiseStatusControls(sStatusStrip);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,6 +17,7 @@
|
|||||||
namespace
|
namespace
|
||||||
{
|
{
|
||||||
constexpr DWORD kStateBroadcastIntervalMs = 250;
|
constexpr DWORD kStateBroadcastIntervalMs = 250;
|
||||||
|
constexpr DWORD kStateBroadcastThrottleMs = 50;
|
||||||
|
|
||||||
bool InitializeWinsock(std::string& error)
|
bool InitializeWinsock(std::string& error)
|
||||||
{
|
{
|
||||||
@@ -75,7 +76,7 @@ std::string GuessContentType(const std::filesystem::path& assetPath)
|
|||||||
}
|
}
|
||||||
|
|
||||||
ControlServer::ControlServer()
|
ControlServer::ControlServer()
|
||||||
: mPort(0), mRunning(false)
|
: mPort(0), mRunning(false), mBroadcastPending(false)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -161,10 +162,16 @@ void ControlServer::Stop()
|
|||||||
|
|
||||||
void ControlServer::BroadcastState()
|
void ControlServer::BroadcastState()
|
||||||
{
|
{
|
||||||
|
mBroadcastPending = false;
|
||||||
std::lock_guard<std::mutex> lock(mMutex);
|
std::lock_guard<std::mutex> lock(mMutex);
|
||||||
BroadcastStateLocked();
|
BroadcastStateLocked();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void ControlServer::RequestBroadcastState()
|
||||||
|
{
|
||||||
|
mBroadcastPending = true;
|
||||||
|
}
|
||||||
|
|
||||||
void ControlServer::ServerLoop()
|
void ControlServer::ServerLoop()
|
||||||
{
|
{
|
||||||
DWORD lastStateBroadcastMs = GetTickCount();
|
DWORD lastStateBroadcastMs = GetTickCount();
|
||||||
@@ -173,7 +180,12 @@ void ControlServer::ServerLoop()
|
|||||||
TryAcceptClient();
|
TryAcceptClient();
|
||||||
|
|
||||||
const DWORD nowMs = GetTickCount();
|
const DWORD nowMs = GetTickCount();
|
||||||
if (nowMs - lastStateBroadcastMs >= kStateBroadcastIntervalMs)
|
if (mBroadcastPending && nowMs - lastStateBroadcastMs >= kStateBroadcastThrottleMs)
|
||||||
|
{
|
||||||
|
BroadcastState();
|
||||||
|
lastStateBroadcastMs = nowMs;
|
||||||
|
}
|
||||||
|
else if (nowMs - lastStateBroadcastMs >= kStateBroadcastIntervalMs)
|
||||||
{
|
{
|
||||||
BroadcastState();
|
BroadcastState();
|
||||||
lastStateBroadcastMs = nowMs;
|
lastStateBroadcastMs = nowMs;
|
||||||
@@ -469,6 +481,7 @@ bool ControlServer::HandleWebSocketUpgrade(UniqueSocket clientSocket, const Http
|
|||||||
client.socket.reset(clientSocket.release());
|
client.socket.reset(clientSocket.release());
|
||||||
client.websocket = true;
|
client.websocket = true;
|
||||||
mClients.push_back(std::move(client));
|
mClients.push_back(std::move(client));
|
||||||
|
mBroadcastPending = false;
|
||||||
BroadcastStateLocked();
|
BroadcastStateLocked();
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ public:
|
|||||||
bool Start(const std::filesystem::path& uiRoot, const std::filesystem::path& docsRoot, unsigned short preferredPort, const Callbacks& callbacks, std::string& error);
|
bool Start(const std::filesystem::path& uiRoot, const std::filesystem::path& docsRoot, unsigned short preferredPort, const Callbacks& callbacks, std::string& error);
|
||||||
void Stop();
|
void Stop();
|
||||||
void BroadcastState();
|
void BroadcastState();
|
||||||
|
void RequestBroadcastState();
|
||||||
|
|
||||||
unsigned short GetPort() const { return mPort; }
|
unsigned short GetPort() const { return mPort; }
|
||||||
|
|
||||||
@@ -100,6 +101,7 @@ private:
|
|||||||
unsigned short mPort;
|
unsigned short mPort;
|
||||||
std::thread mThread;
|
std::thread mThread;
|
||||||
std::atomic<bool> mRunning;
|
std::atomic<bool> mRunning;
|
||||||
|
std::atomic<bool> mBroadcastPending;
|
||||||
mutable std::mutex mMutex;
|
mutable std::mutex mMutex;
|
||||||
std::vector<ClientConnection> mClients;
|
std::vector<ClientConnection> mClients;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ OscServer::~OscServer()
|
|||||||
Stop();
|
Stop();
|
||||||
}
|
}
|
||||||
|
|
||||||
bool OscServer::Start(unsigned short port, const Callbacks& callbacks, std::string& error)
|
bool OscServer::Start(const std::string& bindAddress, unsigned short port, const Callbacks& callbacks, std::string& error)
|
||||||
{
|
{
|
||||||
if (port == 0)
|
if (port == 0)
|
||||||
return true;
|
return true;
|
||||||
@@ -78,11 +78,15 @@ bool OscServer::Start(unsigned short port, const Callbacks& callbacks, std::stri
|
|||||||
|
|
||||||
sockaddr_in address = {};
|
sockaddr_in address = {};
|
||||||
address.sin_family = AF_INET;
|
address.sin_family = AF_INET;
|
||||||
address.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
|
if (!TryParseBindAddress(bindAddress, address.sin_addr, error))
|
||||||
|
{
|
||||||
|
mSocket.reset();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
address.sin_port = htons(static_cast<u_short>(port));
|
address.sin_port = htons(static_cast<u_short>(port));
|
||||||
if (bind(mSocket.get(), reinterpret_cast<sockaddr*>(&address), sizeof(address)) != 0)
|
if (bind(mSocket.get(), reinterpret_cast<sockaddr*>(&address), sizeof(address)) != 0)
|
||||||
{
|
{
|
||||||
error = "Could not bind OSC listener to UDP port " + std::to_string(port) + ".";
|
error = "Could not bind OSC listener to " + bindAddress + ":" + std::to_string(port) + ".";
|
||||||
mSocket.reset();
|
mSocket.reset();
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -92,6 +96,24 @@ bool OscServer::Start(unsigned short port, const Callbacks& callbacks, std::stri
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool OscServer::TryParseBindAddress(const std::string& bindAddress, in_addr& address, std::string& error)
|
||||||
|
{
|
||||||
|
if (bindAddress.empty())
|
||||||
|
{
|
||||||
|
error = "OSC bind address must not be empty.";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
address = {};
|
||||||
|
if (InetPtonA(AF_INET, bindAddress.c_str(), &address) != 1)
|
||||||
|
{
|
||||||
|
error = "Invalid OSC bind address '" + bindAddress + "'. Use an IPv4 address such as 127.0.0.1 or 0.0.0.0.";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
void OscServer::Stop()
|
void OscServer::Stop()
|
||||||
{
|
{
|
||||||
mRunning = false;
|
mRunning = false;
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ public:
|
|||||||
OscServer();
|
OscServer();
|
||||||
~OscServer();
|
~OscServer();
|
||||||
|
|
||||||
bool Start(unsigned short port, const Callbacks& callbacks, std::string& error);
|
bool Start(const std::string& bindAddress, unsigned short port, const Callbacks& callbacks, std::string& error);
|
||||||
void Stop();
|
void Stop();
|
||||||
|
|
||||||
unsigned short GetPort() const { return mPort; }
|
unsigned short GetPort() const { return mPort; }
|
||||||
@@ -37,6 +37,7 @@ private:
|
|||||||
void ServerLoop();
|
void ServerLoop();
|
||||||
bool DecodeMessage(const char* data, int byteCount, OscMessage& message, std::string& error) const;
|
bool DecodeMessage(const char* data, int byteCount, OscMessage& message, std::string& error) const;
|
||||||
bool DispatchMessage(const OscMessage& message, std::string& error) const;
|
bool DispatchMessage(const OscMessage& message, std::string& error) const;
|
||||||
|
static bool TryParseBindAddress(const std::string& bindAddress, in_addr& address, std::string& error);
|
||||||
static bool DecodeArgument(const char* data, int byteCount, int& offset, char valueType, std::string& valueJson);
|
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 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 ReadInt32(const char* data, int byteCount, int& offset, int& value);
|
||||||
|
|||||||
@@ -4,10 +4,12 @@
|
|||||||
#include "OpenGLComposite.h"
|
#include "OpenGLComposite.h"
|
||||||
#include "OscServer.h"
|
#include "OscServer.h"
|
||||||
#include "RuntimeHost.h"
|
#include "RuntimeHost.h"
|
||||||
|
#include "RuntimeServices.h"
|
||||||
|
|
||||||
bool StartRuntimeControlServices(
|
bool StartRuntimeControlServices(
|
||||||
OpenGLComposite& composite,
|
OpenGLComposite& composite,
|
||||||
RuntimeHost& runtimeHost,
|
RuntimeHost& runtimeHost,
|
||||||
|
RuntimeServices& runtimeServices,
|
||||||
ControlServer& controlServer,
|
ControlServer& controlServer,
|
||||||
OscServer& oscServer,
|
OscServer& oscServer,
|
||||||
std::string& error)
|
std::string& error)
|
||||||
@@ -41,10 +43,10 @@ bool StartRuntimeControlServices(
|
|||||||
runtimeHost.SetServerPort(controlServer.GetPort());
|
runtimeHost.SetServerPort(controlServer.GetPort());
|
||||||
|
|
||||||
OscServer::Callbacks oscCallbacks;
|
OscServer::Callbacks oscCallbacks;
|
||||||
oscCallbacks.updateParameter = [&composite](const std::string& layerKey, const std::string& parameterKey, const std::string& valueJson, std::string& actionError) {
|
oscCallbacks.updateParameter = [&runtimeServices](const std::string& layerKey, const std::string& parameterKey, const std::string& valueJson, std::string& actionError) {
|
||||||
return composite.UpdateLayerParameterByControlKeyJson(layerKey, parameterKey, valueJson, actionError);
|
return runtimeServices.QueueOscUpdate(layerKey, parameterKey, valueJson, actionError);
|
||||||
};
|
};
|
||||||
if (runtimeHost.GetOscPort() > 0 && !oscServer.Start(runtimeHost.GetOscPort(), oscCallbacks, error))
|
if (runtimeHost.GetOscPort() > 0 && !oscServer.Start(runtimeHost.GetOscBindAddress(), runtimeHost.GetOscPort(), oscCallbacks, error))
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
|
|||||||
@@ -6,10 +6,12 @@ class ControlServer;
|
|||||||
class OpenGLComposite;
|
class OpenGLComposite;
|
||||||
class OscServer;
|
class OscServer;
|
||||||
class RuntimeHost;
|
class RuntimeHost;
|
||||||
|
class RuntimeServices;
|
||||||
|
|
||||||
bool StartRuntimeControlServices(
|
bool StartRuntimeControlServices(
|
||||||
OpenGLComposite& composite,
|
OpenGLComposite& composite,
|
||||||
RuntimeHost& runtimeHost,
|
RuntimeHost& runtimeHost,
|
||||||
|
RuntimeServices& runtimeServices,
|
||||||
ControlServer& controlServer,
|
ControlServer& controlServer,
|
||||||
OscServer& oscServer,
|
OscServer& oscServer,
|
||||||
std::string& error);
|
std::string& error);
|
||||||
|
|||||||
@@ -4,7 +4,6 @@
|
|||||||
#include "OscServer.h"
|
#include "OscServer.h"
|
||||||
#include "RuntimeControlBridge.h"
|
#include "RuntimeControlBridge.h"
|
||||||
#include "RuntimeHost.h"
|
#include "RuntimeHost.h"
|
||||||
|
|
||||||
#include <windows.h>
|
#include <windows.h>
|
||||||
|
|
||||||
RuntimeServices::RuntimeServices() :
|
RuntimeServices::RuntimeServices() :
|
||||||
@@ -26,7 +25,7 @@ bool RuntimeServices::Start(OpenGLComposite& composite, RuntimeHost& runtimeHost
|
|||||||
{
|
{
|
||||||
Stop();
|
Stop();
|
||||||
|
|
||||||
if (!StartRuntimeControlServices(composite, runtimeHost, *mControlServer, *mOscServer, error))
|
if (!StartRuntimeControlServices(composite, runtimeHost, *this, *mControlServer, *mOscServer, error))
|
||||||
{
|
{
|
||||||
Stop();
|
Stop();
|
||||||
return false;
|
return false;
|
||||||
@@ -57,6 +56,108 @@ void RuntimeServices::BroadcastState()
|
|||||||
mControlServer->BroadcastState();
|
mControlServer->BroadcastState();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void RuntimeServices::RequestBroadcastState()
|
||||||
|
{
|
||||||
|
if (mControlServer)
|
||||||
|
mControlServer->RequestBroadcastState();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool RuntimeServices::QueueOscUpdate(const std::string& layerKey, const std::string& parameterKey, const std::string& valueJson, std::string& error)
|
||||||
|
{
|
||||||
|
(void)error;
|
||||||
|
|
||||||
|
PendingOscUpdate update;
|
||||||
|
update.layerKey = layerKey;
|
||||||
|
update.parameterKey = parameterKey;
|
||||||
|
update.valueJson = valueJson;
|
||||||
|
|
||||||
|
const std::string routeKey = layerKey + "\n" + parameterKey;
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(mPendingOscMutex);
|
||||||
|
mPendingOscUpdates[routeKey] = std::move(update);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool RuntimeServices::ApplyPendingOscUpdates(std::vector<AppliedOscUpdate>& appliedUpdates, std::string& error)
|
||||||
|
{
|
||||||
|
appliedUpdates.clear();
|
||||||
|
|
||||||
|
std::map<std::string, PendingOscUpdate> pending;
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(mPendingOscMutex);
|
||||||
|
if (mPendingOscUpdates.empty())
|
||||||
|
return true;
|
||||||
|
pending.swap(mPendingOscUpdates);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const auto& entry : pending)
|
||||||
|
{
|
||||||
|
JsonValue targetValue;
|
||||||
|
std::string parseError;
|
||||||
|
if (!ParseJson(entry.second.valueJson, targetValue, parseError))
|
||||||
|
{
|
||||||
|
OutputDebugStringA(("OSC queued value parse failed: " + parseError + "\n").c_str());
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
AppliedOscUpdate appliedUpdate;
|
||||||
|
appliedUpdate.routeKey = entry.first;
|
||||||
|
appliedUpdate.layerKey = entry.second.layerKey;
|
||||||
|
appliedUpdate.parameterKey = entry.second.parameterKey;
|
||||||
|
appliedUpdate.targetValue = targetValue;
|
||||||
|
appliedUpdates.push_back(std::move(appliedUpdate));
|
||||||
|
}
|
||||||
|
|
||||||
|
(void)error;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool RuntimeServices::QueueOscCommit(const std::string& routeKey, const std::string& layerKey, const std::string& parameterKey, const JsonValue& value, uint64_t generation, std::string& error)
|
||||||
|
{
|
||||||
|
(void)error;
|
||||||
|
|
||||||
|
PendingOscCommit commit;
|
||||||
|
commit.routeKey = routeKey;
|
||||||
|
commit.layerKey = layerKey;
|
||||||
|
commit.parameterKey = parameterKey;
|
||||||
|
commit.value = value;
|
||||||
|
commit.generation = generation;
|
||||||
|
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(mPendingOscCommitMutex);
|
||||||
|
mPendingOscCommits[routeKey] = std::move(commit);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void RuntimeServices::ClearOscState()
|
||||||
|
{
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(mPendingOscMutex);
|
||||||
|
mPendingOscUpdates.clear();
|
||||||
|
}
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(mPendingOscCommitMutex);
|
||||||
|
mPendingOscCommits.clear();
|
||||||
|
}
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(mCompletedOscCommitMutex);
|
||||||
|
mCompletedOscCommits.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void RuntimeServices::ConsumeCompletedOscCommits(std::vector<CompletedOscCommit>& completedCommits)
|
||||||
|
{
|
||||||
|
completedCommits.clear();
|
||||||
|
|
||||||
|
std::lock_guard<std::mutex> lock(mCompletedOscCommitMutex);
|
||||||
|
if (mCompletedOscCommits.empty())
|
||||||
|
return;
|
||||||
|
|
||||||
|
completedCommits.swap(mCompletedOscCommits);
|
||||||
|
}
|
||||||
|
|
||||||
RuntimePollEvents RuntimeServices::ConsumePollEvents()
|
RuntimePollEvents RuntimeServices::ConsumePollEvents()
|
||||||
{
|
{
|
||||||
RuntimePollEvents events;
|
RuntimePollEvents events;
|
||||||
@@ -94,6 +195,33 @@ void RuntimeServices::PollLoop(RuntimeHost& runtimeHost)
|
|||||||
{
|
{
|
||||||
while (mPollRunning)
|
while (mPollRunning)
|
||||||
{
|
{
|
||||||
|
std::map<std::string, PendingOscCommit> pendingCommits;
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(mPendingOscCommitMutex);
|
||||||
|
pendingCommits.swap(mPendingOscCommits);
|
||||||
|
}
|
||||||
|
for (const auto& entry : pendingCommits)
|
||||||
|
{
|
||||||
|
std::string commitError;
|
||||||
|
if (runtimeHost.UpdateLayerParameterByControlKey(
|
||||||
|
entry.second.layerKey,
|
||||||
|
entry.second.parameterKey,
|
||||||
|
entry.second.value,
|
||||||
|
false,
|
||||||
|
commitError))
|
||||||
|
{
|
||||||
|
CompletedOscCommit completedCommit;
|
||||||
|
completedCommit.routeKey = entry.second.routeKey;
|
||||||
|
completedCommit.generation = entry.second.generation;
|
||||||
|
std::lock_guard<std::mutex> lock(mCompletedOscCommitMutex);
|
||||||
|
mCompletedOscCommits.push_back(std::move(completedCommit));
|
||||||
|
}
|
||||||
|
else if (!commitError.empty())
|
||||||
|
{
|
||||||
|
OutputDebugStringA(("OSC commit failed: " + commitError + "\n").c_str());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
bool registryChanged = false;
|
bool registryChanged = false;
|
||||||
bool reloadRequested = false;
|
bool reloadRequested = false;
|
||||||
std::string runtimeError;
|
std::string runtimeError;
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
#include "RuntimeJson.h"
|
||||||
|
#include "ShaderTypes.h"
|
||||||
|
|
||||||
#include <atomic>
|
#include <atomic>
|
||||||
|
#include <map>
|
||||||
#include <memory>
|
#include <memory>
|
||||||
#include <mutex>
|
#include <mutex>
|
||||||
#include <string>
|
#include <string>
|
||||||
@@ -22,6 +26,20 @@ struct RuntimePollEvents
|
|||||||
class RuntimeServices
|
class RuntimeServices
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
|
struct AppliedOscUpdate
|
||||||
|
{
|
||||||
|
std::string routeKey;
|
||||||
|
std::string layerKey;
|
||||||
|
std::string parameterKey;
|
||||||
|
JsonValue targetValue;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct CompletedOscCommit
|
||||||
|
{
|
||||||
|
std::string routeKey;
|
||||||
|
uint64_t generation = 0;
|
||||||
|
};
|
||||||
|
|
||||||
RuntimeServices();
|
RuntimeServices();
|
||||||
~RuntimeServices();
|
~RuntimeServices();
|
||||||
|
|
||||||
@@ -29,9 +47,31 @@ public:
|
|||||||
void BeginPolling(RuntimeHost& runtimeHost);
|
void BeginPolling(RuntimeHost& runtimeHost);
|
||||||
void Stop();
|
void Stop();
|
||||||
void BroadcastState();
|
void BroadcastState();
|
||||||
|
void RequestBroadcastState();
|
||||||
|
bool QueueOscUpdate(const std::string& layerKey, const std::string& parameterKey, const std::string& valueJson, std::string& error);
|
||||||
|
bool ApplyPendingOscUpdates(std::vector<AppliedOscUpdate>& appliedUpdates, std::string& error);
|
||||||
|
bool QueueOscCommit(const std::string& routeKey, const std::string& layerKey, const std::string& parameterKey, const JsonValue& value, uint64_t generation, std::string& error);
|
||||||
|
void ClearOscState();
|
||||||
|
void ConsumeCompletedOscCommits(std::vector<CompletedOscCommit>& completedCommits);
|
||||||
RuntimePollEvents ConsumePollEvents();
|
RuntimePollEvents ConsumePollEvents();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
struct PendingOscUpdate
|
||||||
|
{
|
||||||
|
std::string layerKey;
|
||||||
|
std::string parameterKey;
|
||||||
|
std::string valueJson;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct PendingOscCommit
|
||||||
|
{
|
||||||
|
std::string routeKey;
|
||||||
|
std::string layerKey;
|
||||||
|
std::string parameterKey;
|
||||||
|
JsonValue value;
|
||||||
|
uint64_t generation = 0;
|
||||||
|
};
|
||||||
|
|
||||||
void StartPolling(RuntimeHost& runtimeHost);
|
void StartPolling(RuntimeHost& runtimeHost);
|
||||||
void StopPolling();
|
void StopPolling();
|
||||||
void PollLoop(RuntimeHost& runtimeHost);
|
void PollLoop(RuntimeHost& runtimeHost);
|
||||||
@@ -45,4 +85,10 @@ private:
|
|||||||
std::atomic<bool> mPollFailed;
|
std::atomic<bool> mPollFailed;
|
||||||
std::mutex mPollErrorMutex;
|
std::mutex mPollErrorMutex;
|
||||||
std::string mPollError;
|
std::string mPollError;
|
||||||
|
std::mutex mPendingOscMutex;
|
||||||
|
std::map<std::string, PendingOscUpdate> mPendingOscUpdates;
|
||||||
|
std::mutex mPendingOscCommitMutex;
|
||||||
|
std::map<std::string, PendingOscCommit> mPendingOscCommits;
|
||||||
|
std::mutex mCompletedOscCommitMutex;
|
||||||
|
std::vector<CompletedOscCommit> mCompletedOscCommits;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
#include "DeckLinkDisplayMode.h"
|
||||||
|
#include "DeckLinkSession.h"
|
||||||
#include "OpenGLComposite.h"
|
#include "OpenGLComposite.h"
|
||||||
#include "GLExtensions.h"
|
#include "GLExtensions.h"
|
||||||
#include "GlRenderConstants.h"
|
#include "GlRenderConstants.h"
|
||||||
@@ -6,22 +8,96 @@
|
|||||||
#include "OpenGLShaderPrograms.h"
|
#include "OpenGLShaderPrograms.h"
|
||||||
#include "OpenGLVideoIOBridge.h"
|
#include "OpenGLVideoIOBridge.h"
|
||||||
#include "PngScreenshotWriter.h"
|
#include "PngScreenshotWriter.h"
|
||||||
|
#include "RuntimeParameterUtils.h"
|
||||||
#include "RuntimeServices.h"
|
#include "RuntimeServices.h"
|
||||||
#include "ShaderBuildQueue.h"
|
#include "ShaderBuildQueue.h"
|
||||||
#include "VideoIOBackendFactory.h"
|
|
||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
|
#include <cctype>
|
||||||
#include <chrono>
|
#include <chrono>
|
||||||
|
#include <cmath>
|
||||||
#include <ctime>
|
#include <ctime>
|
||||||
#include <filesystem>
|
#include <filesystem>
|
||||||
#include <iomanip>
|
#include <iomanip>
|
||||||
#include <memory>
|
#include <memory>
|
||||||
|
#include <set>
|
||||||
#include <sstream>
|
#include <sstream>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
constexpr auto kOscOverlayCommitDelay = std::chrono::milliseconds(150);
|
||||||
|
constexpr double kOscSmoothingReferenceFps = 60.0;
|
||||||
|
constexpr double kOscSmoothingMaxStepSeconds = 0.25;
|
||||||
|
|
||||||
|
std::string SimplifyOscControlKey(const std::string& text)
|
||||||
|
{
|
||||||
|
std::string simplified;
|
||||||
|
for (unsigned char ch : text)
|
||||||
|
{
|
||||||
|
if (std::isalnum(ch))
|
||||||
|
simplified.push_back(static_cast<char>(std::tolower(ch)));
|
||||||
|
}
|
||||||
|
return simplified;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool MatchesOscControlKey(const std::string& candidate, const std::string& key)
|
||||||
|
{
|
||||||
|
return candidate == key || SimplifyOscControlKey(candidate) == SimplifyOscControlKey(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
double ClampOscAlpha(double value)
|
||||||
|
{
|
||||||
|
return (std::max)(0.0, (std::min)(1.0, value));
|
||||||
|
}
|
||||||
|
|
||||||
|
double ComputeTimeBasedOscAlpha(double smoothing, double deltaSeconds)
|
||||||
|
{
|
||||||
|
const double clampedSmoothing = ClampOscAlpha(smoothing);
|
||||||
|
if (clampedSmoothing <= 0.0)
|
||||||
|
return 0.0;
|
||||||
|
if (clampedSmoothing >= 1.0)
|
||||||
|
return 1.0;
|
||||||
|
|
||||||
|
const double clampedDeltaSeconds = (std::max)(0.0, (std::min)(kOscSmoothingMaxStepSeconds, deltaSeconds));
|
||||||
|
if (clampedDeltaSeconds <= 0.0)
|
||||||
|
return 0.0;
|
||||||
|
|
||||||
|
const double frameScale = clampedDeltaSeconds * kOscSmoothingReferenceFps;
|
||||||
|
return ClampOscAlpha(1.0 - std::pow(1.0 - clampedSmoothing, frameScale));
|
||||||
|
}
|
||||||
|
|
||||||
|
JsonValue BuildOscCommitValue(const ShaderParameterDefinition& definition, const ShaderParameterValue& value)
|
||||||
|
{
|
||||||
|
switch (definition.type)
|
||||||
|
{
|
||||||
|
case ShaderParameterType::Boolean:
|
||||||
|
return JsonValue(value.booleanValue);
|
||||||
|
case ShaderParameterType::Enum:
|
||||||
|
return JsonValue(value.enumValue);
|
||||||
|
case ShaderParameterType::Text:
|
||||||
|
return JsonValue(value.textValue);
|
||||||
|
case ShaderParameterType::Trigger:
|
||||||
|
case ShaderParameterType::Float:
|
||||||
|
return JsonValue(value.numberValues.empty() ? 0.0 : value.numberValues.front());
|
||||||
|
case ShaderParameterType::Vec2:
|
||||||
|
case ShaderParameterType::Color:
|
||||||
|
{
|
||||||
|
JsonValue array = JsonValue::MakeArray();
|
||||||
|
for (double number : value.numberValues)
|
||||||
|
array.pushBack(JsonValue(number));
|
||||||
|
return array;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return JsonValue();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
OpenGLComposite::OpenGLComposite(HWND hWnd, HDC hDC, HGLRC hRC) :
|
OpenGLComposite::OpenGLComposite(HWND hWnd, HDC hDC, HGLRC hRC) :
|
||||||
hGLWnd(hWnd), hGLDC(hDC), hGLRC(hRC),
|
hGLWnd(hWnd), hGLDC(hDC), hGLRC(hRC),
|
||||||
|
mVideoIO(std::make_unique<DeckLinkSession>()),
|
||||||
mRenderer(std::make_unique<OpenGLRenderer>()),
|
mRenderer(std::make_unique<OpenGLRenderer>()),
|
||||||
mUseCommittedLayerStates(false),
|
mUseCommittedLayerStates(false),
|
||||||
mScreenshotRequested(false)
|
mScreenshotRequested(false)
|
||||||
@@ -33,9 +109,9 @@ OpenGLComposite::OpenGLComposite(HWND hWnd, HDC hDC, HGLRC hRC) :
|
|||||||
*mRuntimeHost,
|
*mRuntimeHost,
|
||||||
[this]() { renderEffect(); },
|
[this]() { renderEffect(); },
|
||||||
[this]() { ProcessScreenshotRequest(); },
|
[this]() { ProcessScreenshotRequest(); },
|
||||||
[this]() { paintGL(); });
|
[this]() { paintGL(false); });
|
||||||
mVideoIOBridge = std::make_unique<OpenGLVideoIOBridge>(
|
mVideoIOBridge = std::make_unique<OpenGLVideoIOBridge>(
|
||||||
nullptr,
|
*mVideoIO,
|
||||||
*mRenderer,
|
*mRenderer,
|
||||||
*mRenderPipeline,
|
*mRenderPipeline,
|
||||||
*mRuntimeHost,
|
*mRuntimeHost,
|
||||||
@@ -54,15 +130,20 @@ OpenGLComposite::~OpenGLComposite()
|
|||||||
mRuntimeServices->Stop();
|
mRuntimeServices->Stop();
|
||||||
if (mShaderBuildQueue)
|
if (mShaderBuildQueue)
|
||||||
mShaderBuildQueue->Stop();
|
mShaderBuildQueue->Stop();
|
||||||
if (mVideoIO)
|
|
||||||
mVideoIO->ReleaseResources();
|
mVideoIO->ReleaseResources();
|
||||||
mRenderer->DestroyResources();
|
mRenderer->DestroyResources();
|
||||||
|
|
||||||
DeleteCriticalSection(&pMutex);
|
DeleteCriticalSection(&pMutex);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool OpenGLComposite::InitializeVideoIO()
|
bool OpenGLComposite::InitDeckLink()
|
||||||
{
|
{
|
||||||
|
return InitVideoIO();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool OpenGLComposite::InitVideoIO()
|
||||||
|
{
|
||||||
|
VideoFormatSelection videoModes;
|
||||||
std::string initFailureReason;
|
std::string initFailureReason;
|
||||||
|
|
||||||
if (mRuntimeHost && mRuntimeHost->GetRepoRoot().empty())
|
if (mRuntimeHost && mRuntimeHost->GetRepoRoot().empty())
|
||||||
@@ -75,31 +156,31 @@ bool OpenGLComposite::InitializeVideoIO()
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!mRuntimeHost)
|
if (mRuntimeHost)
|
||||||
{
|
{
|
||||||
initFailureReason = "Runtime host is not available.";
|
if (!ResolveConfiguredVideoFormats(
|
||||||
MessageBoxA(NULL, initFailureReason.c_str(), "Video I/O initialization failed", MB_OK | MB_ICONERROR);
|
mRuntimeHost->GetInputVideoFormat(),
|
||||||
|
mRuntimeHost->GetInputFrameRate(),
|
||||||
|
mRuntimeHost->GetOutputVideoFormat(),
|
||||||
|
mRuntimeHost->GetOutputFrameRate(),
|
||||||
|
videoModes,
|
||||||
|
initFailureReason))
|
||||||
|
{
|
||||||
|
MessageBoxA(NULL, initFailureReason.c_str(), "DeckLink mode configuration error", MB_OK);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const VideoIOConfiguration videoIOConfig = mRuntimeHost->GetVideoIOConfiguration();
|
|
||||||
mVideoIO = CreateVideoIODevice(videoIOConfig.backendId, initFailureReason);
|
|
||||||
if (!mVideoIO)
|
|
||||||
{
|
|
||||||
MessageBoxA(NULL, initFailureReason.c_str(), "Video I/O initialization failed", MB_OK | MB_ICONERROR);
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
mVideoIOBridge->SetVideoIODevice(mVideoIO.get());
|
|
||||||
|
|
||||||
if (!mVideoIO->DiscoverDevicesAndModes(videoIOConfig, initFailureReason))
|
if (!mVideoIO->DiscoverDevicesAndModes(videoModes, initFailureReason))
|
||||||
{
|
{
|
||||||
const char* title = initFailureReason == "Please install the Blackmagic DeckLink drivers to use the features of this application."
|
const char* title = initFailureReason == "Please install the Blackmagic DeckLink drivers to use the features of this application."
|
||||||
? "This application requires the selected video I/O drivers installed."
|
? "This application requires the DeckLink drivers installed."
|
||||||
: "Video I/O initialization failed";
|
: "DeckLink initialization failed";
|
||||||
MessageBoxA(NULL, initFailureReason.c_str(), title, MB_OK | MB_ICONERROR);
|
MessageBoxA(NULL, initFailureReason.c_str(), title, MB_OK | MB_ICONERROR);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (!mVideoIO->SelectPreferredFormats(videoIOConfig, initFailureReason))
|
const bool outputAlphaRequired = mRuntimeHost && mRuntimeHost->ExternalKeyingEnabled();
|
||||||
|
if (!mVideoIO->SelectPreferredFormats(videoModes, outputAlphaRequired, initFailureReason))
|
||||||
goto error;
|
goto error;
|
||||||
|
|
||||||
if (! CheckOpenGLExtensions())
|
if (! CheckOpenGLExtensions())
|
||||||
@@ -114,9 +195,9 @@ bool OpenGLComposite::InitializeVideoIO()
|
|||||||
goto error;
|
goto error;
|
||||||
}
|
}
|
||||||
|
|
||||||
PublishVideoIOStatus(mVideoIO->DeviceName().empty()
|
PublishVideoIOStatus(mVideoIO->OutputModelName().empty()
|
||||||
? "Video I/O output device selected."
|
? "DeckLink output device selected."
|
||||||
: ("Selected output device: " + mVideoIO->DeviceName()));
|
: ("Selected output device: " + mVideoIO->OutputModelName()));
|
||||||
|
|
||||||
// Resize window to match output video frame, but scale large formats down by half for viewing.
|
// Resize window to match output video frame, but scale large formats down by half for viewing.
|
||||||
if (mVideoIO->OutputFrameWidth() < 1920)
|
if (mVideoIO->OutputFrameWidth() < 1920)
|
||||||
@@ -124,7 +205,7 @@ bool OpenGLComposite::InitializeVideoIO()
|
|||||||
else
|
else
|
||||||
resizeWindow(mVideoIO->OutputFrameWidth() / 2, mVideoIO->OutputFrameHeight() / 2);
|
resizeWindow(mVideoIO->OutputFrameWidth() / 2, mVideoIO->OutputFrameHeight() / 2);
|
||||||
|
|
||||||
if (!mVideoIO->ConfigureInput([this](const VideoIOFrame& frame) { mVideoIOBridge->VideoFrameArrived(frame); }, initFailureReason))
|
if (!mVideoIO->ConfigureInput([this](const VideoIOFrame& frame) { mVideoIOBridge->VideoFrameArrived(frame); }, videoModes.input, initFailureReason))
|
||||||
{
|
{
|
||||||
goto error;
|
goto error;
|
||||||
}
|
}
|
||||||
@@ -133,7 +214,7 @@ bool OpenGLComposite::InitializeVideoIO()
|
|||||||
mRuntimeHost->SetSignalStatus(false, mVideoIO->InputFrameWidth(), mVideoIO->InputFrameHeight(), mVideoIO->InputDisplayModeName());
|
mRuntimeHost->SetSignalStatus(false, mVideoIO->InputFrameWidth(), mVideoIO->InputFrameHeight(), mVideoIO->InputDisplayModeName());
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!mVideoIO->ConfigureOutput([this](const VideoIOCompletion& completion) { mVideoIOBridge->PlayoutFrameCompleted(completion); }, initFailureReason))
|
if (!mVideoIO->ConfigureOutput([this](const VideoIOCompletion& completion) { mVideoIOBridge->PlayoutFrameCompleted(completion); }, videoModes.output, mRuntimeHost && mRuntimeHost->ExternalKeyingEnabled(), initFailureReason))
|
||||||
{
|
{
|
||||||
goto error;
|
goto error;
|
||||||
}
|
}
|
||||||
@@ -144,16 +225,31 @@ bool OpenGLComposite::InitializeVideoIO()
|
|||||||
|
|
||||||
error:
|
error:
|
||||||
if (!initFailureReason.empty())
|
if (!initFailureReason.empty())
|
||||||
MessageBoxA(NULL, initFailureReason.c_str(), "Video I/O initialization failed", MB_OK | MB_ICONERROR);
|
MessageBoxA(NULL, initFailureReason.c_str(), "DeckLink initialization failed", MB_OK | MB_ICONERROR);
|
||||||
mVideoIO->ReleaseResources();
|
mVideoIO->ReleaseResources();
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
void OpenGLComposite::paintGL()
|
void OpenGLComposite::paintGL(bool force)
|
||||||
{
|
{
|
||||||
if (!mVideoIO)
|
if (!force)
|
||||||
|
{
|
||||||
|
if (IsIconic(hGLWnd))
|
||||||
return;
|
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))
|
if (!TryEnterCriticalSection(&pMutex))
|
||||||
{
|
{
|
||||||
ValidateRect(hGLWnd, NULL);
|
ValidateRect(hGLWnd, NULL);
|
||||||
@@ -161,6 +257,7 @@ void OpenGLComposite::paintGL()
|
|||||||
}
|
}
|
||||||
|
|
||||||
mRenderer->PresentToWindow(hGLDC, mVideoIO->OutputFrameWidth(), mVideoIO->OutputFrameHeight());
|
mRenderer->PresentToWindow(hGLDC, mVideoIO->OutputFrameWidth(), mVideoIO->OutputFrameHeight());
|
||||||
|
mLastPreviewPresentTime = std::chrono::steady_clock::now();
|
||||||
ValidateRect(hGLWnd, NULL);
|
ValidateRect(hGLWnd, NULL);
|
||||||
LeaveCriticalSection(&pMutex);
|
LeaveCriticalSection(&pMutex);
|
||||||
}
|
}
|
||||||
@@ -183,13 +280,21 @@ void OpenGLComposite::resizeWindow(int width, int height)
|
|||||||
|
|
||||||
void OpenGLComposite::PublishVideoIOStatus(const std::string& statusMessage)
|
void OpenGLComposite::PublishVideoIOStatus(const std::string& statusMessage)
|
||||||
{
|
{
|
||||||
if (!mRuntimeHost || !mVideoIO)
|
if (!mRuntimeHost)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
if (!statusMessage.empty())
|
if (!statusMessage.empty())
|
||||||
mVideoIO->SetStatusMessage(statusMessage);
|
mVideoIO->SetStatusMessage(statusMessage);
|
||||||
|
|
||||||
mRuntimeHost->SetVideoIOStatus(mVideoIO->State());
|
mRuntimeHost->SetVideoIOStatus(
|
||||||
|
"decklink",
|
||||||
|
mVideoIO->OutputModelName(),
|
||||||
|
mVideoIO->SupportsInternalKeying(),
|
||||||
|
mVideoIO->SupportsExternalKeying(),
|
||||||
|
mVideoIO->KeyerInterfaceAvailable(),
|
||||||
|
mRuntimeHost->ExternalKeyingEnabled(),
|
||||||
|
mVideoIO->ExternalKeyingActive(),
|
||||||
|
mVideoIO->StatusMessage());
|
||||||
}
|
}
|
||||||
|
|
||||||
bool OpenGLComposite::InitOpenGLState()
|
bool OpenGLComposite::InitOpenGLState()
|
||||||
@@ -223,15 +328,6 @@ bool OpenGLComposite::InitOpenGLState()
|
|||||||
return false;
|
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;
|
std::string rendererError;
|
||||||
if (!mRenderer->InitializeResources(
|
if (!mRenderer->InitializeResources(
|
||||||
mVideoIO->InputFrameWidth(),
|
mVideoIO->InputFrameWidth(),
|
||||||
@@ -246,6 +342,17 @@ bool OpenGLComposite::InitOpenGLState()
|
|||||||
return false;
|
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();
|
||||||
|
mShaderPrograms->ResetShaderFeedbackState();
|
||||||
|
|
||||||
broadcastRuntimeState();
|
broadcastRuntimeState();
|
||||||
mRuntimeServices->BeginPolling(*mRuntimeHost);
|
mRuntimeServices->BeginPolling(*mRuntimeHost);
|
||||||
return true;
|
return true;
|
||||||
@@ -269,8 +376,9 @@ bool OpenGLComposite::Stop()
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool OpenGLComposite::ReloadShader()
|
bool OpenGLComposite::ReloadShader(bool preserveFeedbackState)
|
||||||
{
|
{
|
||||||
|
mPreserveFeedbackOnNextShaderBuild = preserveFeedbackState;
|
||||||
if (mRuntimeHost)
|
if (mRuntimeHost)
|
||||||
{
|
{
|
||||||
mRuntimeHost->SetCompileStatus(true, "Shader rebuild queued.");
|
mRuntimeHost->SetCompileStatus(true, "Shader rebuild queued.");
|
||||||
@@ -291,12 +399,198 @@ bool OpenGLComposite::RequestScreenshot(std::string& error)
|
|||||||
void OpenGLComposite::renderEffect()
|
void OpenGLComposite::renderEffect()
|
||||||
{
|
{
|
||||||
ProcessRuntimePollResults();
|
ProcessRuntimePollResults();
|
||||||
|
std::vector<RuntimeServices::AppliedOscUpdate> appliedOscUpdates;
|
||||||
|
std::vector<RuntimeServices::CompletedOscCommit> completedOscCommits;
|
||||||
|
if (mRuntimeHost && mRuntimeServices)
|
||||||
|
{
|
||||||
|
std::string oscError;
|
||||||
|
if (!mRuntimeServices->ApplyPendingOscUpdates(appliedOscUpdates, oscError) && !oscError.empty())
|
||||||
|
OutputDebugStringA(("OSC apply failed: " + oscError + "\n").c_str());
|
||||||
|
mRuntimeServices->ConsumeCompletedOscCommits(completedOscCommits);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const RuntimeServices::CompletedOscCommit& completedCommit : completedOscCommits)
|
||||||
|
{
|
||||||
|
auto overlayIt = mOscOverlayStates.find(completedCommit.routeKey);
|
||||||
|
if (overlayIt == mOscOverlayStates.end())
|
||||||
|
continue;
|
||||||
|
|
||||||
|
OscOverlayState& overlay = overlayIt->second;
|
||||||
|
if (overlay.commitQueued &&
|
||||||
|
overlay.pendingCommitGeneration == completedCommit.generation &&
|
||||||
|
overlay.generation == completedCommit.generation)
|
||||||
|
{
|
||||||
|
mOscOverlayStates.erase(overlayIt);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
std::set<std::string> pendingOscRouteKeys;
|
||||||
|
const auto oscNow = std::chrono::steady_clock::now();
|
||||||
|
for (const RuntimeServices::AppliedOscUpdate& update : appliedOscUpdates)
|
||||||
|
{
|
||||||
|
const std::string routeKey = update.routeKey;
|
||||||
|
auto overlayIt = mOscOverlayStates.find(routeKey);
|
||||||
|
if (overlayIt == mOscOverlayStates.end())
|
||||||
|
{
|
||||||
|
OscOverlayState overlay;
|
||||||
|
overlay.layerKey = update.layerKey;
|
||||||
|
overlay.parameterKey = update.parameterKey;
|
||||||
|
overlay.targetValue = update.targetValue;
|
||||||
|
overlay.lastUpdatedTime = oscNow;
|
||||||
|
overlay.lastAppliedTime = oscNow;
|
||||||
|
overlay.generation = 1;
|
||||||
|
mOscOverlayStates[routeKey] = std::move(overlay);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
overlayIt->second.targetValue = update.targetValue;
|
||||||
|
overlayIt->second.lastUpdatedTime = oscNow;
|
||||||
|
overlayIt->second.generation += 1;
|
||||||
|
overlayIt->second.commitQueued = false;
|
||||||
|
}
|
||||||
|
pendingOscRouteKeys.insert(routeKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto applyOscOverlays = [&](std::vector<RuntimeRenderState>& states, bool allowCommit)
|
||||||
|
{
|
||||||
|
if (states.empty() || mOscOverlayStates.empty() || !mRuntimeHost)
|
||||||
|
return;
|
||||||
|
|
||||||
|
const double smoothing = ClampOscAlpha(mRuntimeHost->GetOscSmoothing());
|
||||||
|
std::vector<std::string> overlayKeysToRemove;
|
||||||
|
for (auto& item : mOscOverlayStates)
|
||||||
|
{
|
||||||
|
OscOverlayState& overlay = item.second;
|
||||||
|
auto stateIt = std::find_if(states.begin(), states.end(),
|
||||||
|
[&overlay](const RuntimeRenderState& state)
|
||||||
|
{
|
||||||
|
return MatchesOscControlKey(state.layerId, overlay.layerKey) ||
|
||||||
|
MatchesOscControlKey(state.shaderId, overlay.layerKey) ||
|
||||||
|
MatchesOscControlKey(state.shaderName, overlay.layerKey);
|
||||||
|
});
|
||||||
|
if (stateIt == states.end())
|
||||||
|
continue;
|
||||||
|
|
||||||
|
auto definitionIt = std::find_if(stateIt->parameterDefinitions.begin(), stateIt->parameterDefinitions.end(),
|
||||||
|
[&overlay](const ShaderParameterDefinition& definition)
|
||||||
|
{
|
||||||
|
return MatchesOscControlKey(definition.id, overlay.parameterKey) ||
|
||||||
|
MatchesOscControlKey(definition.label, overlay.parameterKey);
|
||||||
|
});
|
||||||
|
if (definitionIt == stateIt->parameterDefinitions.end())
|
||||||
|
continue;
|
||||||
|
|
||||||
|
if (definitionIt->type == ShaderParameterType::Trigger)
|
||||||
|
{
|
||||||
|
if (pendingOscRouteKeys.find(item.first) == pendingOscRouteKeys.end())
|
||||||
|
continue;
|
||||||
|
|
||||||
|
ShaderParameterValue& value = stateIt->parameterValues[definitionIt->id];
|
||||||
|
const double previousCount = value.numberValues.empty() ? 0.0 : value.numberValues[0];
|
||||||
|
const double triggerTime = stateIt->timeSeconds;
|
||||||
|
value.numberValues = { previousCount + 1.0, triggerTime };
|
||||||
|
overlayKeysToRemove.push_back(item.first);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
ShaderParameterValue targetValue;
|
||||||
|
std::string normalizeError;
|
||||||
|
if (!NormalizeAndValidateParameterValue(*definitionIt, overlay.targetValue, targetValue, normalizeError))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
const bool smoothable =
|
||||||
|
smoothing > 0.0 &&
|
||||||
|
(definitionIt->type == ShaderParameterType::Float ||
|
||||||
|
definitionIt->type == ShaderParameterType::Vec2 ||
|
||||||
|
definitionIt->type == ShaderParameterType::Color);
|
||||||
|
if (!smoothable)
|
||||||
|
{
|
||||||
|
overlay.currentValue = targetValue;
|
||||||
|
overlay.hasCurrentValue = true;
|
||||||
|
stateIt->parameterValues[definitionIt->id] = overlay.currentValue;
|
||||||
|
if (allowCommit &&
|
||||||
|
!overlay.commitQueued &&
|
||||||
|
oscNow - overlay.lastUpdatedTime >= kOscOverlayCommitDelay &&
|
||||||
|
mRuntimeServices)
|
||||||
|
{
|
||||||
|
std::string commitError;
|
||||||
|
if (mRuntimeServices->QueueOscCommit(item.first, overlay.layerKey, overlay.parameterKey, overlay.targetValue, overlay.generation, commitError))
|
||||||
|
{
|
||||||
|
overlay.pendingCommitGeneration = overlay.generation;
|
||||||
|
overlay.commitQueued = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!overlay.hasCurrentValue)
|
||||||
|
{
|
||||||
|
overlay.currentValue = DefaultValueForDefinition(*definitionIt);
|
||||||
|
auto currentIt = stateIt->parameterValues.find(definitionIt->id);
|
||||||
|
if (currentIt != stateIt->parameterValues.end())
|
||||||
|
overlay.currentValue = currentIt->second;
|
||||||
|
overlay.hasCurrentValue = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (overlay.currentValue.numberValues.size() != targetValue.numberValues.size())
|
||||||
|
overlay.currentValue.numberValues = targetValue.numberValues;
|
||||||
|
|
||||||
|
double smoothingAlpha = smoothing;
|
||||||
|
if (overlay.lastAppliedTime != std::chrono::steady_clock::time_point())
|
||||||
|
{
|
||||||
|
const double deltaSeconds =
|
||||||
|
std::chrono::duration_cast<std::chrono::duration<double>>(oscNow - overlay.lastAppliedTime).count();
|
||||||
|
smoothingAlpha = ComputeTimeBasedOscAlpha(smoothing, deltaSeconds);
|
||||||
|
}
|
||||||
|
overlay.lastAppliedTime = oscNow;
|
||||||
|
|
||||||
|
ShaderParameterValue nextValue = targetValue;
|
||||||
|
bool converged = true;
|
||||||
|
for (std::size_t index = 0; index < targetValue.numberValues.size(); ++index)
|
||||||
|
{
|
||||||
|
const double currentNumber = overlay.currentValue.numberValues[index];
|
||||||
|
const double targetNumber = targetValue.numberValues[index];
|
||||||
|
const double delta = targetNumber - currentNumber;
|
||||||
|
double nextNumber = currentNumber + delta * smoothingAlpha;
|
||||||
|
if (std::fabs(delta) <= 0.0005)
|
||||||
|
nextNumber = targetNumber;
|
||||||
|
else
|
||||||
|
converged = false;
|
||||||
|
nextValue.numberValues[index] = nextNumber;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (converged)
|
||||||
|
nextValue.numberValues = targetValue.numberValues;
|
||||||
|
|
||||||
|
overlay.currentValue = nextValue;
|
||||||
|
overlay.hasCurrentValue = true;
|
||||||
|
stateIt->parameterValues[definitionIt->id] = overlay.currentValue;
|
||||||
|
if (allowCommit &&
|
||||||
|
converged &&
|
||||||
|
!overlay.commitQueued &&
|
||||||
|
oscNow - overlay.lastUpdatedTime >= kOscOverlayCommitDelay &&
|
||||||
|
mRuntimeServices)
|
||||||
|
{
|
||||||
|
std::string commitError;
|
||||||
|
JsonValue committedValue = BuildOscCommitValue(*definitionIt, overlay.currentValue);
|
||||||
|
if (mRuntimeServices->QueueOscCommit(item.first, overlay.layerKey, overlay.parameterKey, committedValue, overlay.generation, commitError))
|
||||||
|
{
|
||||||
|
overlay.pendingCommitGeneration = overlay.generation;
|
||||||
|
overlay.commitQueued = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const std::string& overlayKey : overlayKeysToRemove)
|
||||||
|
mOscOverlayStates.erase(overlayKey);
|
||||||
|
};
|
||||||
|
|
||||||
const bool hasInputSource = mVideoIO->HasInputSource();
|
const bool hasInputSource = mVideoIO->HasInputSource();
|
||||||
std::vector<RuntimeRenderState> layerStates;
|
std::vector<RuntimeRenderState> layerStates;
|
||||||
if (mUseCommittedLayerStates)
|
if (mUseCommittedLayerStates)
|
||||||
{
|
{
|
||||||
layerStates = mShaderPrograms->CommittedLayerStates();
|
layerStates = mShaderPrograms->CommittedLayerStates();
|
||||||
|
applyOscOverlays(layerStates, false);
|
||||||
if (mRuntimeHost)
|
if (mRuntimeHost)
|
||||||
mRuntimeHost->RefreshDynamicRenderStateFields(layerStates);
|
mRuntimeHost->RefreshDynamicRenderStateFields(layerStates);
|
||||||
}
|
}
|
||||||
@@ -305,6 +599,7 @@ void OpenGLComposite::renderEffect()
|
|||||||
const unsigned renderWidth = mVideoIO->InputFrameWidth();
|
const unsigned renderWidth = mVideoIO->InputFrameWidth();
|
||||||
const unsigned renderHeight = mVideoIO->InputFrameHeight();
|
const unsigned renderHeight = mVideoIO->InputFrameHeight();
|
||||||
const uint64_t renderStateVersion = mRuntimeHost->GetRenderStateVersion();
|
const uint64_t renderStateVersion = mRuntimeHost->GetRenderStateVersion();
|
||||||
|
const uint64_t parameterStateVersion = mRuntimeHost->GetParameterStateVersion();
|
||||||
const bool renderStateCacheValid =
|
const bool renderStateCacheValid =
|
||||||
!mCachedLayerRenderStates.empty() &&
|
!mCachedLayerRenderStates.empty() &&
|
||||||
mCachedRenderStateVersion == renderStateVersion &&
|
mCachedRenderStateVersion == renderStateVersion &&
|
||||||
@@ -313,6 +608,13 @@ void OpenGLComposite::renderEffect()
|
|||||||
|
|
||||||
if (renderStateCacheValid)
|
if (renderStateCacheValid)
|
||||||
{
|
{
|
||||||
|
applyOscOverlays(mCachedLayerRenderStates, true);
|
||||||
|
if (mCachedParameterStateVersion != parameterStateVersion &&
|
||||||
|
mRuntimeHost->TryRefreshCachedLayerStates(mCachedLayerRenderStates))
|
||||||
|
{
|
||||||
|
mCachedParameterStateVersion = parameterStateVersion;
|
||||||
|
applyOscOverlays(mCachedLayerRenderStates, true);
|
||||||
|
}
|
||||||
layerStates = mCachedLayerRenderStates;
|
layerStates = mCachedLayerRenderStates;
|
||||||
mRuntimeHost->RefreshDynamicRenderStateFields(layerStates);
|
mRuntimeHost->RefreshDynamicRenderStateFields(layerStates);
|
||||||
}
|
}
|
||||||
@@ -322,11 +624,15 @@ void OpenGLComposite::renderEffect()
|
|||||||
{
|
{
|
||||||
mCachedLayerRenderStates = layerStates;
|
mCachedLayerRenderStates = layerStates;
|
||||||
mCachedRenderStateVersion = renderStateVersion;
|
mCachedRenderStateVersion = renderStateVersion;
|
||||||
|
mCachedParameterStateVersion = parameterStateVersion;
|
||||||
mCachedRenderStateWidth = renderWidth;
|
mCachedRenderStateWidth = renderWidth;
|
||||||
mCachedRenderStateHeight = renderHeight;
|
mCachedRenderStateHeight = renderHeight;
|
||||||
|
applyOscOverlays(mCachedLayerRenderStates, true);
|
||||||
|
layerStates = mCachedLayerRenderStates;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
applyOscOverlays(mCachedLayerRenderStates, true);
|
||||||
layerStates = mCachedLayerRenderStates;
|
layerStates = mCachedLayerRenderStates;
|
||||||
mRuntimeHost->RefreshDynamicRenderStateFields(layerStates);
|
mRuntimeHost->RefreshDynamicRenderStateFields(layerStates);
|
||||||
}
|
}
|
||||||
@@ -344,8 +650,8 @@ void OpenGLComposite::renderEffect()
|
|||||||
[this](const RuntimeRenderState& state, LayerProgram::TextBinding& textBinding, std::string& error) {
|
[this](const RuntimeRenderState& state, LayerProgram::TextBinding& textBinding, std::string& error) {
|
||||||
return mShaderPrograms->UpdateTextBindingTexture(state, textBinding, error);
|
return mShaderPrograms->UpdateTextBindingTexture(state, textBinding, error);
|
||||||
},
|
},
|
||||||
[this](const RuntimeRenderState& state, unsigned availableSourceHistoryLength, unsigned availableTemporalHistoryLength) {
|
[this](const RuntimeRenderState& state, unsigned availableSourceHistoryLength, unsigned availableTemporalHistoryLength, bool feedbackAvailable) {
|
||||||
return mShaderPrograms->UpdateGlobalParamsBuffer(state, availableSourceHistoryLength, availableTemporalHistoryLength);
|
return mShaderPrograms->UpdateGlobalParamsBuffer(state, availableSourceHistoryLength, availableTemporalHistoryLength, feedbackAvailable);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -439,6 +745,7 @@ bool OpenGLComposite::ProcessRuntimePollResults()
|
|||||||
{
|
{
|
||||||
mRuntimeHost->SetCompileStatus(false, compilerErrorMessage);
|
mRuntimeHost->SetCompileStatus(false, compilerErrorMessage);
|
||||||
mUseCommittedLayerStates = true;
|
mUseCommittedLayerStates = true;
|
||||||
|
mPreserveFeedbackOnNextShaderBuild = false;
|
||||||
broadcastRuntimeState();
|
broadcastRuntimeState();
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -446,11 +753,15 @@ bool OpenGLComposite::ProcessRuntimePollResults()
|
|||||||
mUseCommittedLayerStates = false;
|
mUseCommittedLayerStates = false;
|
||||||
mCachedLayerRenderStates = mShaderPrograms->CommittedLayerStates();
|
mCachedLayerRenderStates = mShaderPrograms->CommittedLayerStates();
|
||||||
mShaderPrograms->ResetTemporalHistoryState();
|
mShaderPrograms->ResetTemporalHistoryState();
|
||||||
|
if (!mPreserveFeedbackOnNextShaderBuild)
|
||||||
|
mShaderPrograms->ResetShaderFeedbackState();
|
||||||
|
mPreserveFeedbackOnNextShaderBuild = false;
|
||||||
broadcastRuntimeState();
|
broadcastRuntimeState();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
mRuntimeHost->SetCompileStatus(true, "Shader rebuild queued.");
|
mRuntimeHost->SetCompileStatus(true, "Shader rebuild queued.");
|
||||||
|
mPreserveFeedbackOnNextShaderBuild = false;
|
||||||
RequestShaderBuild();
|
RequestShaderBuild();
|
||||||
broadcastRuntimeState();
|
broadcastRuntimeState();
|
||||||
return true;
|
return true;
|
||||||
@@ -476,6 +787,7 @@ void OpenGLComposite::broadcastRuntimeState()
|
|||||||
void OpenGLComposite::resetTemporalHistoryState()
|
void OpenGLComposite::resetTemporalHistoryState()
|
||||||
{
|
{
|
||||||
mShaderPrograms->ResetTemporalHistoryState();
|
mShaderPrograms->ResetTemporalHistoryState();
|
||||||
|
mShaderPrograms->ResetShaderFeedbackState();
|
||||||
}
|
}
|
||||||
|
|
||||||
bool OpenGLComposite::CheckOpenGLExtensions()
|
bool OpenGLComposite::CheckOpenGLExtensions()
|
||||||
|
|||||||
@@ -23,6 +23,7 @@
|
|||||||
#include <string>
|
#include <string>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
#include <deque>
|
#include <deque>
|
||||||
|
#include <chrono>
|
||||||
|
|
||||||
class VideoIODevice;
|
class VideoIODevice;
|
||||||
class OpenGLVideoIOBridge;
|
class OpenGLVideoIOBridge;
|
||||||
@@ -39,10 +40,11 @@ public:
|
|||||||
OpenGLComposite(HWND hWnd, HDC hDC, HGLRC hRC);
|
OpenGLComposite(HWND hWnd, HDC hDC, HGLRC hRC);
|
||||||
~OpenGLComposite();
|
~OpenGLComposite();
|
||||||
|
|
||||||
bool InitializeVideoIO();
|
bool InitDeckLink();
|
||||||
|
bool InitVideoIO();
|
||||||
bool Start();
|
bool Start();
|
||||||
bool Stop();
|
bool Stop();
|
||||||
bool ReloadShader();
|
bool ReloadShader(bool preserveFeedbackState = false);
|
||||||
std::string GetRuntimeStateJson() const;
|
std::string GetRuntimeStateJson() const;
|
||||||
bool AddLayer(const std::string& shaderId, std::string& error);
|
bool AddLayer(const std::string& shaderId, std::string& error);
|
||||||
bool RemoveLayer(const std::string& layerId, std::string& error);
|
bool RemoveLayer(const std::string& layerId, std::string& error);
|
||||||
@@ -58,18 +60,32 @@ public:
|
|||||||
bool RequestScreenshot(std::string& error);
|
bool RequestScreenshot(std::string& error);
|
||||||
unsigned short GetControlServerPort() const;
|
unsigned short GetControlServerPort() const;
|
||||||
unsigned short GetOscPort() const;
|
unsigned short GetOscPort() const;
|
||||||
|
std::string GetOscBindAddress() const;
|
||||||
std::string GetControlUrl() const;
|
std::string GetControlUrl() const;
|
||||||
std::string GetDocsUrl() const;
|
std::string GetDocsUrl() const;
|
||||||
std::string GetOscAddress() const;
|
std::string GetOscAddress() const;
|
||||||
|
|
||||||
void resizeGL(WORD width, WORD height);
|
void resizeGL(WORD width, WORD height);
|
||||||
void paintGL();
|
void paintGL(bool force = false);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void resizeWindow(int width, int height);
|
void resizeWindow(int width, int height);
|
||||||
bool CheckOpenGLExtensions();
|
bool CheckOpenGLExtensions();
|
||||||
void PublishVideoIOStatus(const std::string& statusMessage);
|
void PublishVideoIOStatus(const std::string& statusMessage);
|
||||||
using LayerProgram = OpenGLRenderer::LayerProgram;
|
using LayerProgram = OpenGLRenderer::LayerProgram;
|
||||||
|
struct OscOverlayState
|
||||||
|
{
|
||||||
|
std::string layerKey;
|
||||||
|
std::string parameterKey;
|
||||||
|
JsonValue targetValue;
|
||||||
|
ShaderParameterValue currentValue;
|
||||||
|
bool hasCurrentValue = false;
|
||||||
|
std::chrono::steady_clock::time_point lastUpdatedTime;
|
||||||
|
std::chrono::steady_clock::time_point lastAppliedTime;
|
||||||
|
uint64_t generation = 0;
|
||||||
|
uint64_t pendingCommitGeneration = 0;
|
||||||
|
bool commitQueued = false;
|
||||||
|
};
|
||||||
|
|
||||||
HWND hGLWnd;
|
HWND hGLWnd;
|
||||||
HDC hGLDC;
|
HDC hGLDC;
|
||||||
@@ -87,10 +103,14 @@ private:
|
|||||||
std::unique_ptr<RuntimeServices> mRuntimeServices;
|
std::unique_ptr<RuntimeServices> mRuntimeServices;
|
||||||
std::vector<RuntimeRenderState> mCachedLayerRenderStates;
|
std::vector<RuntimeRenderState> mCachedLayerRenderStates;
|
||||||
uint64_t mCachedRenderStateVersion = 0;
|
uint64_t mCachedRenderStateVersion = 0;
|
||||||
|
uint64_t mCachedParameterStateVersion = 0;
|
||||||
unsigned mCachedRenderStateWidth = 0;
|
unsigned mCachedRenderStateWidth = 0;
|
||||||
unsigned mCachedRenderStateHeight = 0;
|
unsigned mCachedRenderStateHeight = 0;
|
||||||
|
std::map<std::string, OscOverlayState> mOscOverlayStates;
|
||||||
std::atomic<bool> mUseCommittedLayerStates;
|
std::atomic<bool> mUseCommittedLayerStates;
|
||||||
std::atomic<bool> mScreenshotRequested;
|
std::atomic<bool> mScreenshotRequested;
|
||||||
|
std::chrono::steady_clock::time_point mLastPreviewPresentTime;
|
||||||
|
bool mPreserveFeedbackOnNextShaderBuild = false;
|
||||||
|
|
||||||
bool InitOpenGLState();
|
bool InitOpenGLState();
|
||||||
void renderEffect();
|
void renderEffect();
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
#include "OpenGLComposite.h"
|
#include "OpenGLComposite.h"
|
||||||
|
#include "RuntimeServices.h"
|
||||||
|
|
||||||
std::string OpenGLComposite::GetRuntimeStateJson() const
|
std::string OpenGLComposite::GetRuntimeStateJson() const
|
||||||
{
|
{
|
||||||
@@ -15,6 +16,11 @@ unsigned short OpenGLComposite::GetOscPort() const
|
|||||||
return mRuntimeHost ? mRuntimeHost->GetOscPort() : 0;
|
return mRuntimeHost ? mRuntimeHost->GetOscPort() : 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
std::string OpenGLComposite::GetOscBindAddress() const
|
||||||
|
{
|
||||||
|
return mRuntimeHost ? mRuntimeHost->GetOscBindAddress() : "127.0.0.1";
|
||||||
|
}
|
||||||
|
|
||||||
std::string OpenGLComposite::GetControlUrl() const
|
std::string OpenGLComposite::GetControlUrl() const
|
||||||
{
|
{
|
||||||
return "http://127.0.0.1:" + std::to_string(GetControlServerPort()) + "/";
|
return "http://127.0.0.1:" + std::to_string(GetControlServerPort()) + "/";
|
||||||
@@ -27,7 +33,7 @@ std::string OpenGLComposite::GetDocsUrl() const
|
|||||||
|
|
||||||
std::string OpenGLComposite::GetOscAddress() const
|
std::string OpenGLComposite::GetOscAddress() const
|
||||||
{
|
{
|
||||||
return "udp://127.0.0.1:" + std::to_string(GetOscPort()) + " /VideoShaderToys/{Layer}/{Parameter}";
|
return "udp://" + GetOscBindAddress() + ":" + std::to_string(GetOscPort()) + " /VideoShaderToys/{Layer}/{Parameter}";
|
||||||
}
|
}
|
||||||
|
|
||||||
bool OpenGLComposite::AddLayer(const std::string& shaderId, std::string& error)
|
bool OpenGLComposite::AddLayer(const std::string& shaderId, std::string& error)
|
||||||
@@ -35,7 +41,7 @@ bool OpenGLComposite::AddLayer(const std::string& shaderId, std::string& error)
|
|||||||
if (!mRuntimeHost->AddLayer(shaderId, error))
|
if (!mRuntimeHost->AddLayer(shaderId, error))
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
ReloadShader();
|
ReloadShader(true);
|
||||||
broadcastRuntimeState();
|
broadcastRuntimeState();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -45,7 +51,7 @@ bool OpenGLComposite::RemoveLayer(const std::string& layerId, std::string& error
|
|||||||
if (!mRuntimeHost->RemoveLayer(layerId, error))
|
if (!mRuntimeHost->RemoveLayer(layerId, error))
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
ReloadShader();
|
ReloadShader(true);
|
||||||
broadcastRuntimeState();
|
broadcastRuntimeState();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -55,7 +61,7 @@ bool OpenGLComposite::MoveLayer(const std::string& layerId, int direction, std::
|
|||||||
if (!mRuntimeHost->MoveLayer(layerId, direction, error))
|
if (!mRuntimeHost->MoveLayer(layerId, direction, error))
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
ReloadShader();
|
ReloadShader(true);
|
||||||
broadcastRuntimeState();
|
broadcastRuntimeState();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -65,7 +71,7 @@ bool OpenGLComposite::MoveLayerToIndex(const std::string& layerId, std::size_t t
|
|||||||
if (!mRuntimeHost->MoveLayerToIndex(layerId, targetIndex, error))
|
if (!mRuntimeHost->MoveLayerToIndex(layerId, targetIndex, error))
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
ReloadShader();
|
ReloadShader(true);
|
||||||
broadcastRuntimeState();
|
broadcastRuntimeState();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -121,6 +127,11 @@ bool OpenGLComposite::ResetLayerParameters(const std::string& layerId, std::stri
|
|||||||
if (!mRuntimeHost->ResetLayerParameters(layerId, error))
|
if (!mRuntimeHost->ResetLayerParameters(layerId, error))
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
|
mOscOverlayStates.clear();
|
||||||
|
if (mRuntimeServices)
|
||||||
|
mRuntimeServices->ClearOscState();
|
||||||
|
resetTemporalHistoryState();
|
||||||
|
|
||||||
broadcastRuntimeState();
|
broadcastRuntimeState();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -59,6 +59,7 @@ void OpenGLRenderPass::Render(
|
|||||||
}
|
}
|
||||||
|
|
||||||
mRenderer.TemporalHistory().PushSourceFramebuffer(mRenderer.DecodeFramebuffer(), inputFrameWidth, inputFrameHeight);
|
mRenderer.TemporalHistory().PushSourceFramebuffer(mRenderer.DecodeFramebuffer(), inputFrameWidth, inputFrameHeight);
|
||||||
|
mRenderer.FeedbackBuffers().FinalizeFrame();
|
||||||
}
|
}
|
||||||
|
|
||||||
void OpenGLRenderPass::RenderDecodePass(unsigned inputFrameWidth, unsigned inputFrameHeight, unsigned captureTextureWidth, VideoIOPixelFormat inputPixelFormat)
|
void OpenGLRenderPass::RenderDecodePass(unsigned inputFrameWidth, unsigned inputFrameHeight, unsigned captureTextureWidth, VideoIOPixelFormat inputPixelFormat)
|
||||||
@@ -187,6 +188,7 @@ std::vector<RenderPassDescriptor> OpenGLRenderPass::BuildLayerPassDescriptors(
|
|||||||
pass.passId = passProgram.passId;
|
pass.passId = passProgram.passId;
|
||||||
pass.layerId = state.layerId;
|
pass.layerId = state.layerId;
|
||||||
pass.shaderId = state.shaderId;
|
pass.shaderId = state.shaderId;
|
||||||
|
pass.layerInputTexture = layerInputTexture;
|
||||||
pass.sourceTexture = passSourceTexture;
|
pass.sourceTexture = passSourceTexture;
|
||||||
pass.sourceFramebuffer = passIndex == 0 ? layerInputFramebuffer : passSourceFramebuffer;
|
pass.sourceFramebuffer = passIndex == 0 ? layerInputFramebuffer : passSourceFramebuffer;
|
||||||
pass.destinationTexture = passDestinationTexture;
|
pass.destinationTexture = passDestinationTexture;
|
||||||
@@ -195,6 +197,7 @@ std::vector<RenderPassDescriptor> OpenGLRenderPass::BuildLayerPassDescriptors(
|
|||||||
pass.passProgram = &passProgram;
|
pass.passProgram = &passProgram;
|
||||||
pass.layerState = &state;
|
pass.layerState = &state;
|
||||||
pass.capturePreLayerHistory = passIndex == 0 && state.temporalHistorySource == TemporalHistorySource::PreLayerInput;
|
pass.capturePreLayerHistory = passIndex == 0 && state.temporalHistorySource == TemporalHistorySource::PreLayerInput;
|
||||||
|
pass.captureFeedbackWrite = state.feedback.enabled && passProgram.passId == state.feedback.writePassId;
|
||||||
passes.push_back(pass);
|
passes.push_back(pass);
|
||||||
|
|
||||||
// A later pass can reference either the explicit output name or the
|
// A later pass can reference either the explicit output name or the
|
||||||
@@ -224,6 +227,7 @@ void OpenGLRenderPass::RenderLayerPass(
|
|||||||
return;
|
return;
|
||||||
|
|
||||||
RenderShaderProgram(
|
RenderShaderProgram(
|
||||||
|
pass.layerInputTexture,
|
||||||
pass.sourceTexture,
|
pass.sourceTexture,
|
||||||
pass.destinationFramebuffer,
|
pass.destinationFramebuffer,
|
||||||
*pass.passProgram,
|
*pass.passProgram,
|
||||||
@@ -236,9 +240,12 @@ void OpenGLRenderPass::RenderLayerPass(
|
|||||||
|
|
||||||
if (pass.capturePreLayerHistory)
|
if (pass.capturePreLayerHistory)
|
||||||
mRenderer.TemporalHistory().PushPreLayerFramebuffer(pass.layerId, pass.sourceFramebuffer, inputFrameWidth, inputFrameHeight);
|
mRenderer.TemporalHistory().PushPreLayerFramebuffer(pass.layerId, pass.sourceFramebuffer, inputFrameWidth, inputFrameHeight);
|
||||||
|
if (pass.captureFeedbackWrite)
|
||||||
|
mRenderer.FeedbackBuffers().CaptureFeedbackFramebuffer(pass.layerId, pass.destinationFramebuffer, inputFrameWidth, inputFrameHeight);
|
||||||
}
|
}
|
||||||
|
|
||||||
void OpenGLRenderPass::RenderShaderProgram(
|
void OpenGLRenderPass::RenderShaderProgram(
|
||||||
|
GLuint layerInputTexture,
|
||||||
GLuint sourceTexture,
|
GLuint sourceTexture,
|
||||||
GLuint destinationFrameBuffer,
|
GLuint destinationFrameBuffer,
|
||||||
PassProgram& passProgram,
|
PassProgram& passProgram,
|
||||||
@@ -261,14 +268,19 @@ void OpenGLRenderPass::RenderShaderProgram(
|
|||||||
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
|
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> sourceHistoryTextures = mRenderer.TemporalHistory().ResolveSourceHistoryTextures(sourceTexture, state.isTemporal ? historyCap : 0);
|
||||||
const std::vector<GLuint> temporalHistoryTextures = mRenderer.TemporalHistory().ResolveTemporalHistoryTextures(state, sourceTexture, state.isTemporal ? historyCap : 0);
|
const std::vector<GLuint> temporalHistoryTextures = mRenderer.TemporalHistory().ResolveTemporalHistoryTextures(state, sourceTexture, state.isTemporal ? historyCap : 0);
|
||||||
|
const GLuint feedbackTexture = mRenderer.FeedbackBuffers().ResolveReadTexture(state);
|
||||||
const ShaderTextureBindings::RuntimeTextureBindingPlan texturePlan =
|
const ShaderTextureBindings::RuntimeTextureBindingPlan texturePlan =
|
||||||
mTextureBindings.BuildLayerRuntimeBindingPlan(passProgram, sourceTexture, sourceHistoryTextures, temporalHistoryTextures);
|
mTextureBindings.BuildLayerRuntimeBindingPlan(passProgram, sourceTexture, layerInputTexture, state, feedbackTexture, sourceHistoryTextures, temporalHistoryTextures);
|
||||||
mTextureBindings.BindRuntimeTexturePlan(texturePlan);
|
mTextureBindings.BindRuntimeTexturePlan(texturePlan);
|
||||||
glBindVertexArray(mRenderer.FullscreenVertexArray());
|
glBindVertexArray(mRenderer.FullscreenVertexArray());
|
||||||
glUseProgram(passProgram.program);
|
glUseProgram(passProgram.program);
|
||||||
// The UBO is shared by every pass in a layer; texture routing is what
|
// The UBO is shared by every pass in a layer; texture routing is what
|
||||||
// changes from pass to pass.
|
// changes from pass to pass.
|
||||||
updateGlobalParams(state, mRenderer.TemporalHistory().SourceAvailableCount(), mRenderer.TemporalHistory().AvailableCountForLayer(state.layerId));
|
updateGlobalParams(
|
||||||
|
state,
|
||||||
|
mRenderer.TemporalHistory().SourceAvailableCount(),
|
||||||
|
mRenderer.TemporalHistory().AvailableCountForLayer(state.layerId),
|
||||||
|
mRenderer.FeedbackBuffers().FeedbackAvailable(state));
|
||||||
glDrawArrays(GL_TRIANGLES, 0, 3);
|
glDrawArrays(GL_TRIANGLES, 0, 3);
|
||||||
glUseProgram(0);
|
glUseProgram(0);
|
||||||
glBindVertexArray(0);
|
glBindVertexArray(0);
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ public:
|
|||||||
using LayerProgram = OpenGLRenderer::LayerProgram;
|
using LayerProgram = OpenGLRenderer::LayerProgram;
|
||||||
using PassProgram = OpenGLRenderer::LayerProgram::PassProgram;
|
using PassProgram = OpenGLRenderer::LayerProgram::PassProgram;
|
||||||
using TextBindingUpdater = std::function<bool(const RuntimeRenderState&, LayerProgram::TextBinding&, std::string&)>;
|
using TextBindingUpdater = std::function<bool(const RuntimeRenderState&, LayerProgram::TextBinding&, std::string&)>;
|
||||||
using GlobalParamsUpdater = std::function<bool(const RuntimeRenderState&, unsigned, unsigned)>;
|
using GlobalParamsUpdater = std::function<bool(const RuntimeRenderState&, unsigned, unsigned, bool)>;
|
||||||
|
|
||||||
explicit OpenGLRenderPass(OpenGLRenderer& renderer);
|
explicit OpenGLRenderPass(OpenGLRenderer& renderer);
|
||||||
|
|
||||||
@@ -44,6 +44,7 @@ private:
|
|||||||
const TextBindingUpdater& updateTextBinding,
|
const TextBindingUpdater& updateTextBinding,
|
||||||
const GlobalParamsUpdater& updateGlobalParams);
|
const GlobalParamsUpdater& updateGlobalParams);
|
||||||
void RenderShaderProgram(
|
void RenderShaderProgram(
|
||||||
|
GLuint layerInputTexture,
|
||||||
GLuint sourceTexture,
|
GLuint sourceTexture,
|
||||||
GLuint destinationFrameBuffer,
|
GLuint destinationFrameBuffer,
|
||||||
PassProgram& passProgram,
|
PassProgram& passProgram,
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
#include <gl/gl.h>
|
#include <gl/gl.h>
|
||||||
|
|
||||||
OpenGLVideoIOBridge::OpenGLVideoIOBridge(
|
OpenGLVideoIOBridge::OpenGLVideoIOBridge(
|
||||||
VideoIODevice* videoIO,
|
VideoIODevice& videoIO,
|
||||||
OpenGLRenderer& renderer,
|
OpenGLRenderer& renderer,
|
||||||
OpenGLRenderPipeline& renderPipeline,
|
OpenGLRenderPipeline& renderPipeline,
|
||||||
RuntimeHost& runtimeHost,
|
RuntimeHost& runtimeHost,
|
||||||
@@ -24,11 +24,6 @@ OpenGLVideoIOBridge::OpenGLVideoIOBridge(
|
|||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
void OpenGLVideoIOBridge::SetVideoIODevice(VideoIODevice* videoIO)
|
|
||||||
{
|
|
||||||
mVideoIO = videoIO;
|
|
||||||
}
|
|
||||||
|
|
||||||
void OpenGLVideoIOBridge::RecordFramePacing(VideoIOCompletionResult completionResult)
|
void OpenGLVideoIOBridge::RecordFramePacing(VideoIOCompletionResult completionResult)
|
||||||
{
|
{
|
||||||
const auto now = std::chrono::steady_clock::now();
|
const auto now = std::chrono::steady_clock::now();
|
||||||
@@ -62,10 +57,7 @@ void OpenGLVideoIOBridge::RecordFramePacing(VideoIOCompletionResult completionRe
|
|||||||
|
|
||||||
void OpenGLVideoIOBridge::VideoFrameArrived(const VideoIOFrame& inputFrame)
|
void OpenGLVideoIOBridge::VideoFrameArrived(const VideoIOFrame& inputFrame)
|
||||||
{
|
{
|
||||||
if (mVideoIO == nullptr)
|
const VideoIOState& state = mVideoIO.State();
|
||||||
return;
|
|
||||||
|
|
||||||
const VideoIOState& state = mVideoIO->State();
|
|
||||||
mRuntimeHost.TrySetSignalStatus(!inputFrame.hasNoInputSource, state.inputFrameSize.width, state.inputFrameSize.height, state.inputDisplayModeName);
|
mRuntimeHost.TrySetSignalStatus(!inputFrame.hasNoInputSource, state.inputFrameSize.width, state.inputFrameSize.height, state.inputDisplayModeName);
|
||||||
|
|
||||||
if (inputFrame.hasNoInputSource || inputFrame.bytes == nullptr)
|
if (inputFrame.hasNoInputSource || inputFrame.bytes == nullptr)
|
||||||
@@ -73,7 +65,10 @@ void OpenGLVideoIOBridge::VideoFrameArrived(const VideoIOFrame& inputFrame)
|
|||||||
|
|
||||||
const long textureSize = inputFrame.rowBytes * static_cast<long>(inputFrame.height);
|
const long textureSize = inputFrame.rowBytes * static_cast<long>(inputFrame.height);
|
||||||
|
|
||||||
EnterCriticalSection(&mMutex);
|
// Never let input upload stall the playout/render callback. If the GL bridge
|
||||||
|
// is busy producing an output frame, skip this upload and use the next input.
|
||||||
|
if (!TryEnterCriticalSection(&mMutex))
|
||||||
|
return;
|
||||||
|
|
||||||
wglMakeCurrent(mHdc, mHglrc); // make OpenGL context current in this thread
|
wglMakeCurrent(mHdc, mHglrc); // make OpenGL context current in this thread
|
||||||
|
|
||||||
@@ -99,37 +94,31 @@ void OpenGLVideoIOBridge::VideoFrameArrived(const VideoIOFrame& inputFrame)
|
|||||||
|
|
||||||
void OpenGLVideoIOBridge::PlayoutFrameCompleted(const VideoIOCompletion& completion)
|
void OpenGLVideoIOBridge::PlayoutFrameCompleted(const VideoIOCompletion& completion)
|
||||||
{
|
{
|
||||||
if (mVideoIO == nullptr)
|
|
||||||
return;
|
|
||||||
|
|
||||||
RecordFramePacing(completion.result);
|
RecordFramePacing(completion.result);
|
||||||
|
|
||||||
EnterCriticalSection(&mMutex);
|
|
||||||
|
|
||||||
VideoIOOutputFrame outputFrame;
|
VideoIOOutputFrame outputFrame;
|
||||||
if (!mVideoIO->BeginOutputFrame(outputFrame))
|
if (!mVideoIO.BeginOutputFrame(outputFrame))
|
||||||
{
|
|
||||||
LeaveCriticalSection(&mMutex);
|
|
||||||
return;
|
return;
|
||||||
}
|
const VideoIOState& state = mVideoIO.State();
|
||||||
const VideoIOState& state = mVideoIO->State();
|
|
||||||
RenderPipelineFrameContext frameContext;
|
RenderPipelineFrameContext frameContext;
|
||||||
frameContext.videoState = state;
|
frameContext.videoState = state;
|
||||||
frameContext.completion = completion;
|
frameContext.completion = completion;
|
||||||
|
|
||||||
|
EnterCriticalSection(&mMutex);
|
||||||
|
|
||||||
// make GL context current in this thread
|
// make GL context current in this thread
|
||||||
wglMakeCurrent(mHdc, mHglrc);
|
wglMakeCurrent(mHdc, mHglrc);
|
||||||
|
|
||||||
mRenderPipeline.RenderFrame(frameContext, outputFrame);
|
mRenderPipeline.RenderFrame(frameContext, outputFrame);
|
||||||
|
|
||||||
mVideoIO->EndOutputFrame(outputFrame);
|
|
||||||
|
|
||||||
mVideoIO->AccountForCompletionResult(completion.result);
|
|
||||||
|
|
||||||
// Schedule the next frame for playout
|
|
||||||
mVideoIO->ScheduleOutputFrame(outputFrame);
|
|
||||||
|
|
||||||
wglMakeCurrent(NULL, NULL);
|
wglMakeCurrent(NULL, NULL);
|
||||||
|
|
||||||
LeaveCriticalSection(&mMutex);
|
LeaveCriticalSection(&mMutex);
|
||||||
|
|
||||||
|
mVideoIO.EndOutputFrame(outputFrame);
|
||||||
|
|
||||||
|
mVideoIO.AccountForCompletionResult(completion.result);
|
||||||
|
|
||||||
|
// Schedule the next frame for playout after the GL bridge is released so
|
||||||
|
// input uploads are not blocked by non-GL output bookkeeping.
|
||||||
|
mVideoIO.ScheduleOutputFrame(outputFrame);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ class OpenGLVideoIOBridge
|
|||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
OpenGLVideoIOBridge(
|
OpenGLVideoIOBridge(
|
||||||
VideoIODevice* videoIO,
|
VideoIODevice& videoIO,
|
||||||
OpenGLRenderer& renderer,
|
OpenGLRenderer& renderer,
|
||||||
OpenGLRenderPipeline& renderPipeline,
|
OpenGLRenderPipeline& renderPipeline,
|
||||||
RuntimeHost& runtimeHost,
|
RuntimeHost& runtimeHost,
|
||||||
@@ -21,15 +21,13 @@ public:
|
|||||||
HDC hdc,
|
HDC hdc,
|
||||||
HGLRC hglrc);
|
HGLRC hglrc);
|
||||||
|
|
||||||
void SetVideoIODevice(VideoIODevice* videoIO);
|
|
||||||
|
|
||||||
void VideoFrameArrived(const VideoIOFrame& inputFrame);
|
void VideoFrameArrived(const VideoIOFrame& inputFrame);
|
||||||
void PlayoutFrameCompleted(const VideoIOCompletion& completion);
|
void PlayoutFrameCompleted(const VideoIOCompletion& completion);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void RecordFramePacing(VideoIOCompletionResult completionResult);
|
void RecordFramePacing(VideoIOCompletionResult completionResult);
|
||||||
|
|
||||||
VideoIODevice* mVideoIO;
|
VideoIODevice& mVideoIO;
|
||||||
OpenGLRenderer& mRenderer;
|
OpenGLRenderer& mRenderer;
|
||||||
OpenGLRenderPipeline& mRenderPipeline;
|
OpenGLRenderPipeline& mRenderPipeline;
|
||||||
RuntimeHost& mRuntimeHost;
|
RuntimeHost& mRuntimeHost;
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ struct RenderPassDescriptor
|
|||||||
std::string passId;
|
std::string passId;
|
||||||
std::string layerId;
|
std::string layerId;
|
||||||
std::string shaderId;
|
std::string shaderId;
|
||||||
|
GLuint layerInputTexture = 0;
|
||||||
GLuint sourceTexture = 0;
|
GLuint sourceTexture = 0;
|
||||||
GLuint sourceFramebuffer = 0;
|
GLuint sourceFramebuffer = 0;
|
||||||
GLuint destinationTexture = 0;
|
GLuint destinationTexture = 0;
|
||||||
@@ -36,4 +37,5 @@ struct RenderPassDescriptor
|
|||||||
OpenGLRenderer::LayerProgram::PassProgram* passProgram = nullptr;
|
OpenGLRenderer::LayerProgram::PassProgram* passProgram = nullptr;
|
||||||
const RuntimeRenderState* layerState = nullptr;
|
const RuntimeRenderState* layerState = nullptr;
|
||||||
bool capturePreLayerHistory = false;
|
bool capturePreLayerHistory = false;
|
||||||
|
bool captureFeedbackWrite = false;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,202 @@
|
|||||||
|
#include "ShaderFeedbackBuffers.h"
|
||||||
|
|
||||||
|
#include <set>
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
void ConfigureFeedbackTexture(unsigned frameWidth, unsigned frameHeight)
|
||||||
|
{
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ShaderFeedbackBuffers::EnsureResources(const std::vector<RuntimeRenderState>& layerStates, unsigned frameWidth, unsigned frameHeight, std::string& error)
|
||||||
|
{
|
||||||
|
if (!EnsureZeroTexture())
|
||||||
|
{
|
||||||
|
error = "Failed to initialize shader feedback fallback texture.";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::set<std::string> requiredLayerIds;
|
||||||
|
for (const RuntimeRenderState& state : layerStates)
|
||||||
|
{
|
||||||
|
if (!state.feedback.enabled)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
requiredLayerIds.insert(state.layerId);
|
||||||
|
auto surfaceIt = mSurfacesByLayerId.find(state.layerId);
|
||||||
|
if (surfaceIt == mSurfacesByLayerId.end() ||
|
||||||
|
surfaceIt->second.width != frameWidth ||
|
||||||
|
surfaceIt->second.height != frameHeight)
|
||||||
|
{
|
||||||
|
Surface replacement;
|
||||||
|
if (!CreateSurface(replacement, frameWidth, frameHeight, error))
|
||||||
|
return false;
|
||||||
|
mSurfacesByLayerId[state.layerId] = std::move(replacement);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (auto it = mSurfacesByLayerId.begin(); it != mSurfacesByLayerId.end();)
|
||||||
|
{
|
||||||
|
if (requiredLayerIds.find(it->first) == requiredLayerIds.end())
|
||||||
|
{
|
||||||
|
DestroySurface(it->second);
|
||||||
|
it = mSurfacesByLayerId.erase(it);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
++it;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ShaderFeedbackBuffers::DestroyResources()
|
||||||
|
{
|
||||||
|
for (auto& entry : mSurfacesByLayerId)
|
||||||
|
DestroySurface(entry.second);
|
||||||
|
mSurfacesByLayerId.clear();
|
||||||
|
|
||||||
|
if (mZeroTexture != 0)
|
||||||
|
{
|
||||||
|
glDeleteTextures(1, &mZeroTexture);
|
||||||
|
mZeroTexture = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void ShaderFeedbackBuffers::ResetState()
|
||||||
|
{
|
||||||
|
for (auto& entry : mSurfacesByLayerId)
|
||||||
|
ClearSurfaceState(entry.second);
|
||||||
|
}
|
||||||
|
|
||||||
|
GLuint ShaderFeedbackBuffers::ResolveReadTexture(const RuntimeRenderState& state) const
|
||||||
|
{
|
||||||
|
if (!state.feedback.enabled)
|
||||||
|
return mZeroTexture;
|
||||||
|
|
||||||
|
auto surfaceIt = mSurfacesByLayerId.find(state.layerId);
|
||||||
|
if (surfaceIt == mSurfacesByLayerId.end() || !surfaceIt->second.hasData)
|
||||||
|
return mZeroTexture;
|
||||||
|
|
||||||
|
return surfaceIt->second.slots[surfaceIt->second.readIndex].texture != 0
|
||||||
|
? surfaceIt->second.slots[surfaceIt->second.readIndex].texture
|
||||||
|
: mZeroTexture;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ShaderFeedbackBuffers::FeedbackAvailable(const RuntimeRenderState& state) const
|
||||||
|
{
|
||||||
|
if (!state.feedback.enabled)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
auto surfaceIt = mSurfacesByLayerId.find(state.layerId);
|
||||||
|
return surfaceIt != mSurfacesByLayerId.end() && surfaceIt->second.hasData;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ShaderFeedbackBuffers::CaptureFeedbackFramebuffer(const std::string& layerId, GLuint sourceFramebuffer, unsigned frameWidth, unsigned frameHeight)
|
||||||
|
{
|
||||||
|
auto surfaceIt = mSurfacesByLayerId.find(layerId);
|
||||||
|
if (surfaceIt == mSurfacesByLayerId.end())
|
||||||
|
return;
|
||||||
|
|
||||||
|
Surface& surface = surfaceIt->second;
|
||||||
|
const unsigned writeIndex = 1u - surface.readIndex;
|
||||||
|
glBindFramebuffer(GL_READ_FRAMEBUFFER, sourceFramebuffer);
|
||||||
|
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, surface.slots[writeIndex].framebuffer);
|
||||||
|
glBlitFramebuffer(0, 0, frameWidth, frameHeight, 0, 0, frameWidth, frameHeight, GL_COLOR_BUFFER_BIT, GL_LINEAR);
|
||||||
|
surface.pendingWrite = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ShaderFeedbackBuffers::FinalizeFrame()
|
||||||
|
{
|
||||||
|
for (auto& entry : mSurfacesByLayerId)
|
||||||
|
{
|
||||||
|
Surface& surface = entry.second;
|
||||||
|
if (!surface.pendingWrite)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
surface.readIndex = 1u - surface.readIndex;
|
||||||
|
surface.hasData = true;
|
||||||
|
surface.pendingWrite = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ShaderFeedbackBuffers::EnsureZeroTexture()
|
||||||
|
{
|
||||||
|
if (mZeroTexture != 0)
|
||||||
|
return true;
|
||||||
|
|
||||||
|
glGenTextures(1, &mZeroTexture);
|
||||||
|
glBindTexture(GL_TEXTURE_2D, mZeroTexture);
|
||||||
|
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);
|
||||||
|
const float zeroPixel[4] = { 0.0f, 0.0f, 0.0f, 0.0f };
|
||||||
|
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA16F, 1, 1, 0, GL_RGBA, GL_FLOAT, zeroPixel);
|
||||||
|
glBindTexture(GL_TEXTURE_2D, 0);
|
||||||
|
return mZeroTexture != 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ShaderFeedbackBuffers::CreateSurface(Surface& surface, unsigned frameWidth, unsigned frameHeight, std::string& error)
|
||||||
|
{
|
||||||
|
DestroySurface(surface);
|
||||||
|
|
||||||
|
surface.width = frameWidth;
|
||||||
|
surface.height = frameHeight;
|
||||||
|
for (Slot& slot : surface.slots)
|
||||||
|
{
|
||||||
|
glGenTextures(1, &slot.texture);
|
||||||
|
glBindTexture(GL_TEXTURE_2D, slot.texture);
|
||||||
|
ConfigureFeedbackTexture(frameWidth, frameHeight);
|
||||||
|
|
||||||
|
glGenFramebuffers(1, &slot.framebuffer);
|
||||||
|
glBindFramebuffer(GL_FRAMEBUFFER, slot.framebuffer);
|
||||||
|
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, slot.texture, 0);
|
||||||
|
if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE)
|
||||||
|
{
|
||||||
|
error = "Failed to initialize a shader feedback framebuffer.";
|
||||||
|
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||||
|
glBindTexture(GL_TEXTURE_2D, 0);
|
||||||
|
DestroySurface(surface);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||||
|
glBindTexture(GL_TEXTURE_2D, 0);
|
||||||
|
ClearSurfaceState(surface);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ShaderFeedbackBuffers::DestroySurface(Surface& surface)
|
||||||
|
{
|
||||||
|
for (Slot& slot : surface.slots)
|
||||||
|
{
|
||||||
|
if (slot.framebuffer != 0)
|
||||||
|
glDeleteFramebuffers(1, &slot.framebuffer);
|
||||||
|
if (slot.texture != 0)
|
||||||
|
glDeleteTextures(1, &slot.texture);
|
||||||
|
slot.framebuffer = 0;
|
||||||
|
slot.texture = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
surface.width = 0;
|
||||||
|
surface.height = 0;
|
||||||
|
surface.readIndex = 0;
|
||||||
|
surface.hasData = false;
|
||||||
|
surface.pendingWrite = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ShaderFeedbackBuffers::ClearSurfaceState(Surface& surface)
|
||||||
|
{
|
||||||
|
surface.readIndex = 0;
|
||||||
|
surface.hasData = false;
|
||||||
|
surface.pendingWrite = false;
|
||||||
|
}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "GLExtensions.h"
|
||||||
|
#include "ShaderTypes.h"
|
||||||
|
|
||||||
|
#include <map>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
class ShaderFeedbackBuffers
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
struct Slot
|
||||||
|
{
|
||||||
|
GLuint texture = 0;
|
||||||
|
GLuint framebuffer = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct Surface
|
||||||
|
{
|
||||||
|
Slot slots[2];
|
||||||
|
unsigned width = 0;
|
||||||
|
unsigned height = 0;
|
||||||
|
unsigned readIndex = 0;
|
||||||
|
bool hasData = false;
|
||||||
|
bool pendingWrite = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
bool EnsureResources(const std::vector<RuntimeRenderState>& layerStates, unsigned frameWidth, unsigned frameHeight, std::string& error);
|
||||||
|
void DestroyResources();
|
||||||
|
void ResetState();
|
||||||
|
GLuint ResolveReadTexture(const RuntimeRenderState& state) const;
|
||||||
|
bool FeedbackAvailable(const RuntimeRenderState& state) const;
|
||||||
|
void CaptureFeedbackFramebuffer(const std::string& layerId, GLuint sourceFramebuffer, unsigned frameWidth, unsigned frameHeight);
|
||||||
|
void FinalizeFrame();
|
||||||
|
|
||||||
|
private:
|
||||||
|
bool EnsureZeroTexture();
|
||||||
|
bool CreateSurface(Surface& surface, unsigned frameWidth, unsigned frameHeight, std::string& error);
|
||||||
|
void DestroySurface(Surface& surface);
|
||||||
|
void ClearSurfaceState(Surface& surface);
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::map<std::string, Surface> mSurfacesByLayerId;
|
||||||
|
GLuint mZeroTexture = 0;
|
||||||
|
};
|
||||||
@@ -19,7 +19,8 @@ bool TemporalHistoryBuffers::ValidateTextureUnitBudget(const std::vector<Runtime
|
|||||||
++textTextureCount;
|
++textTextureCount;
|
||||||
}
|
}
|
||||||
const unsigned totalShaderTextures = static_cast<unsigned>(state.textureAssets.size()) + textTextureCount;
|
const unsigned totalShaderTextures = static_cast<unsigned>(state.textureAssets.size()) + textTextureCount;
|
||||||
const unsigned layerRequiredUnits = kSourceHistoryTextureUnitBase + (state.isTemporal ? historyCap + historyCap : 0u) + totalShaderTextures;
|
const unsigned feedbackTextureCount = state.feedback.enabled ? 1u : 0u;
|
||||||
|
const unsigned layerRequiredUnits = kSourceHistoryTextureUnitBase + (state.isTemporal ? historyCap + historyCap : 0u) + feedbackTextureCount + totalShaderTextures;
|
||||||
if (layerRequiredUnits > requiredUnits)
|
if (layerRequiredUnits > requiredUnits)
|
||||||
requiredUnits = layerRequiredUnits;
|
requiredUnits = layerRequiredUnits;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,8 +2,9 @@
|
|||||||
|
|
||||||
#include <gl/gl.h>
|
#include <gl/gl.h>
|
||||||
|
|
||||||
|
constexpr GLuint kLayerInputTextureUnit = 0;
|
||||||
constexpr GLuint kDecodedVideoTextureUnit = 1;
|
constexpr GLuint kDecodedVideoTextureUnit = 1;
|
||||||
constexpr GLuint kSourceHistoryTextureUnitBase = 2;
|
constexpr GLuint kSourceHistoryTextureUnitBase = 2;
|
||||||
constexpr GLuint kPackedVideoTextureUnit = 2;
|
constexpr GLuint kPackedVideoTextureUnit = 2;
|
||||||
constexpr GLuint kGlobalParamsBindingPoint = 0;
|
constexpr GLuint kGlobalParamsBindingPoint = 0;
|
||||||
constexpr unsigned kPrerollFrameCount = 8;
|
constexpr unsigned kPrerollFrameCount = 12;
|
||||||
|
|||||||
@@ -63,6 +63,7 @@ bool OpenGLRenderer::InitializeResources(unsigned inputFrameWidth, unsigned inpu
|
|||||||
glBindBufferBase(GL_UNIFORM_BUFFER, kGlobalParamsBindingPoint, mGlobalParamsUBO);
|
glBindBufferBase(GL_UNIFORM_BUFFER, kGlobalParamsBindingPoint, mGlobalParamsUBO);
|
||||||
glBindBuffer(GL_UNIFORM_BUFFER, 0);
|
glBindBuffer(GL_UNIFORM_BUFFER, 0);
|
||||||
|
|
||||||
|
mResourcesInitialized = true;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -157,8 +158,10 @@ void OpenGLRenderer::DestroyResources()
|
|||||||
mCaptureTexture = 0;
|
mCaptureTexture = 0;
|
||||||
mTextureUploadBuffer = 0;
|
mTextureUploadBuffer = 0;
|
||||||
mGlobalParamsUBOSize = 0;
|
mGlobalParamsUBOSize = 0;
|
||||||
|
mResourcesInitialized = false;
|
||||||
|
|
||||||
mTemporalHistory.DestroyResources();
|
mTemporalHistory.DestroyResources();
|
||||||
|
mFeedbackBuffers.DestroyResources();
|
||||||
DestroyLayerPrograms();
|
DestroyLayerPrograms();
|
||||||
DestroyDecodeShaderProgram();
|
DestroyDecodeShaderProgram();
|
||||||
DestroyOutputPackShaderProgram();
|
DestroyOutputPackShaderProgram();
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
#include "GLExtensions.h"
|
#include "GLExtensions.h"
|
||||||
#include "RenderTargetPool.h"
|
#include "RenderTargetPool.h"
|
||||||
|
#include "ShaderFeedbackBuffers.h"
|
||||||
#include "ShaderTypes.h"
|
#include "ShaderTypes.h"
|
||||||
#include "TemporalHistoryBuffers.h"
|
#include "TemporalHistoryBuffers.h"
|
||||||
|
|
||||||
@@ -78,6 +79,7 @@ public:
|
|||||||
GLint OutputPackFormatLocation() const { return mOutputPackFormatLocation; }
|
GLint OutputPackFormatLocation() const { return mOutputPackFormatLocation; }
|
||||||
GLsizeiptr GlobalParamsUBOSize() const { return mGlobalParamsUBOSize; }
|
GLsizeiptr GlobalParamsUBOSize() const { return mGlobalParamsUBOSize; }
|
||||||
void SetGlobalParamsUBOSize(GLsizeiptr size) { mGlobalParamsUBOSize = size; }
|
void SetGlobalParamsUBOSize(GLsizeiptr size) { mGlobalParamsUBOSize = size; }
|
||||||
|
bool ResourcesInitialized() const { return mResourcesInitialized; }
|
||||||
void ReplaceLayerPrograms(std::vector<LayerProgram>& newPrograms) { mLayerPrograms.swap(newPrograms); }
|
void ReplaceLayerPrograms(std::vector<LayerProgram>& newPrograms) { mLayerPrograms.swap(newPrograms); }
|
||||||
std::vector<LayerProgram>& LayerPrograms() { return mLayerPrograms; }
|
std::vector<LayerProgram>& LayerPrograms() { return mLayerPrograms; }
|
||||||
const std::vector<LayerProgram>& LayerPrograms() const { return mLayerPrograms; }
|
const std::vector<LayerProgram>& LayerPrograms() const { return mLayerPrograms; }
|
||||||
@@ -86,6 +88,8 @@ public:
|
|||||||
std::size_t TemporaryRenderTargetCount() const { return mRenderTargets.TemporaryTargetCount(); }
|
std::size_t TemporaryRenderTargetCount() const { return mRenderTargets.TemporaryTargetCount(); }
|
||||||
TemporalHistoryBuffers& TemporalHistory() { return mTemporalHistory; }
|
TemporalHistoryBuffers& TemporalHistory() { return mTemporalHistory; }
|
||||||
const TemporalHistoryBuffers& TemporalHistory() const { return mTemporalHistory; }
|
const TemporalHistoryBuffers& TemporalHistory() const { return mTemporalHistory; }
|
||||||
|
ShaderFeedbackBuffers& FeedbackBuffers() { return mFeedbackBuffers; }
|
||||||
|
const ShaderFeedbackBuffers& FeedbackBuffers() const { return mFeedbackBuffers; }
|
||||||
void SetDecodeShaderProgram(GLuint program, GLuint vertexShader, GLuint fragmentShader);
|
void SetDecodeShaderProgram(GLuint program, GLuint vertexShader, GLuint fragmentShader);
|
||||||
void SetOutputPackShaderProgram(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);
|
bool InitializeResources(unsigned inputFrameWidth, unsigned inputFrameHeight, unsigned captureTextureWidth, unsigned outputFrameWidth, unsigned outputFrameHeight, unsigned outputPackTextureWidth, std::string& error);
|
||||||
@@ -117,9 +121,11 @@ private:
|
|||||||
GLint mOutputPackActiveWordsLocation = -1;
|
GLint mOutputPackActiveWordsLocation = -1;
|
||||||
GLint mOutputPackFormatLocation = -1;
|
GLint mOutputPackFormatLocation = -1;
|
||||||
GLsizeiptr mGlobalParamsUBOSize = 0;
|
GLsizeiptr mGlobalParamsUBOSize = 0;
|
||||||
|
bool mResourcesInitialized = false;
|
||||||
int mViewWidth = 0;
|
int mViewWidth = 0;
|
||||||
int mViewHeight = 0;
|
int mViewHeight = 0;
|
||||||
std::vector<LayerProgram> mLayerPrograms;
|
std::vector<LayerProgram> mLayerPrograms;
|
||||||
RenderTargetPool mRenderTargets;
|
RenderTargetPool mRenderTargets;
|
||||||
TemporalHistoryBuffers mTemporalHistory;
|
TemporalHistoryBuffers mTemporalHistory;
|
||||||
|
ShaderFeedbackBuffers mFeedbackBuffers;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ GlobalParamsBuffer::GlobalParamsBuffer(OpenGLRenderer& renderer) :
|
|||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
bool GlobalParamsBuffer::Update(const RuntimeRenderState& state, unsigned availableSourceHistoryLength, unsigned availableTemporalHistoryLength)
|
bool GlobalParamsBuffer::Update(const RuntimeRenderState& state, unsigned availableSourceHistoryLength, unsigned availableTemporalHistoryLength, bool feedbackAvailable)
|
||||||
{
|
{
|
||||||
std::vector<unsigned char>& buffer = mScratchBuffer;
|
std::vector<unsigned char>& buffer = mScratchBuffer;
|
||||||
buffer.clear();
|
buffer.clear();
|
||||||
@@ -33,6 +33,7 @@ bool GlobalParamsBuffer::Update(const RuntimeRenderState& state, unsigned availa
|
|||||||
: 0u;
|
: 0u;
|
||||||
AppendStd140Int(buffer, static_cast<int>(effectiveSourceHistoryLength));
|
AppendStd140Int(buffer, static_cast<int>(effectiveSourceHistoryLength));
|
||||||
AppendStd140Int(buffer, static_cast<int>(effectiveTemporalHistoryLength));
|
AppendStd140Int(buffer, static_cast<int>(effectiveTemporalHistoryLength));
|
||||||
|
AppendStd140Int(buffer, feedbackAvailable ? 1 : 0);
|
||||||
|
|
||||||
for (const ShaderParameterDefinition& definition : state.parameterDefinitions)
|
for (const ShaderParameterDefinition& definition : state.parameterDefinitions)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ class GlobalParamsBuffer
|
|||||||
public:
|
public:
|
||||||
explicit GlobalParamsBuffer(OpenGLRenderer& renderer);
|
explicit GlobalParamsBuffer(OpenGLRenderer& renderer);
|
||||||
|
|
||||||
bool Update(const RuntimeRenderState& state, unsigned availableSourceHistoryLength, unsigned availableTemporalHistoryLength);
|
bool Update(const RuntimeRenderState& state, unsigned availableSourceHistoryLength, unsigned availableTemporalHistoryLength, bool feedbackAvailable);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
OpenGLRenderer& mRenderer;
|
OpenGLRenderer& mRenderer;
|
||||||
|
|||||||
@@ -52,6 +52,12 @@ bool OpenGLShaderPrograms::CompileLayerPrograms(unsigned inputFrameWidth, unsign
|
|||||||
CopyErrorMessage(temporalError, errorMessageSize, errorMessage);
|
CopyErrorMessage(temporalError, errorMessageSize, errorMessage);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
if (mRenderer.ResourcesInitialized() &&
|
||||||
|
!mRenderer.FeedbackBuffers().EnsureResources(layerStates, inputFrameWidth, inputFrameHeight, temporalError))
|
||||||
|
{
|
||||||
|
CopyErrorMessage(temporalError, errorMessageSize, errorMessage);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
// Initial startup still compiles synchronously; auto-reload uses the build
|
// Initial startup still compiles synchronously; auto-reload uses the build
|
||||||
// queue so Slang/file work stays off the playback path.
|
// queue so Slang/file work stays off the playback path.
|
||||||
@@ -109,6 +115,12 @@ bool OpenGLShaderPrograms::CommitPreparedLayerPrograms(const PreparedShaderBuild
|
|||||||
CopyErrorMessage(temporalError, errorMessageSize, errorMessage);
|
CopyErrorMessage(temporalError, errorMessageSize, errorMessage);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
if (mRenderer.ResourcesInitialized() &&
|
||||||
|
!mRenderer.FeedbackBuffers().EnsureResources(preparedBuild.layerStates, inputFrameWidth, inputFrameHeight, temporalError))
|
||||||
|
{
|
||||||
|
CopyErrorMessage(temporalError, errorMessageSize, errorMessage);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
// The prepared build already contains GLSL text for each pass. This commit
|
// The prepared build already contains GLSL text for each pass. This commit
|
||||||
// step performs the short GL work on the render thread.
|
// step performs the short GL work on the render thread.
|
||||||
@@ -176,12 +188,17 @@ void OpenGLShaderPrograms::ResetTemporalHistoryState()
|
|||||||
mRenderer.TemporalHistory().ResetState();
|
mRenderer.TemporalHistory().ResetState();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void OpenGLShaderPrograms::ResetShaderFeedbackState()
|
||||||
|
{
|
||||||
|
mRenderer.FeedbackBuffers().ResetState();
|
||||||
|
}
|
||||||
|
|
||||||
bool OpenGLShaderPrograms::UpdateTextBindingTexture(const RuntimeRenderState& state, LayerProgram::TextBinding& textBinding, std::string& error)
|
bool OpenGLShaderPrograms::UpdateTextBindingTexture(const RuntimeRenderState& state, LayerProgram::TextBinding& textBinding, std::string& error)
|
||||||
{
|
{
|
||||||
return mTextureBindings.UpdateTextBindingTexture(state, textBinding, error);
|
return mTextureBindings.UpdateTextBindingTexture(state, textBinding, error);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool OpenGLShaderPrograms::UpdateGlobalParamsBuffer(const RuntimeRenderState& state, unsigned availableSourceHistoryLength, unsigned availableTemporalHistoryLength)
|
bool OpenGLShaderPrograms::UpdateGlobalParamsBuffer(const RuntimeRenderState& state, unsigned availableSourceHistoryLength, unsigned availableTemporalHistoryLength, bool feedbackAvailable)
|
||||||
{
|
{
|
||||||
return mGlobalParamsBuffer.Update(state, availableSourceHistoryLength, availableTemporalHistoryLength);
|
return mGlobalParamsBuffer.Update(state, availableSourceHistoryLength, availableTemporalHistoryLength, feedbackAvailable);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,9 +25,10 @@ public:
|
|||||||
void DestroySingleLayerProgram(LayerProgram& layerProgram);
|
void DestroySingleLayerProgram(LayerProgram& layerProgram);
|
||||||
void DestroyDecodeShaderProgram();
|
void DestroyDecodeShaderProgram();
|
||||||
void ResetTemporalHistoryState();
|
void ResetTemporalHistoryState();
|
||||||
|
void ResetShaderFeedbackState();
|
||||||
const std::vector<RuntimeRenderState>& CommittedLayerStates() const { return mCommittedLayerStates; }
|
const std::vector<RuntimeRenderState>& CommittedLayerStates() const { return mCommittedLayerStates; }
|
||||||
bool UpdateTextBindingTexture(const RuntimeRenderState& state, LayerProgram::TextBinding& textBinding, std::string& error);
|
bool UpdateTextBindingTexture(const RuntimeRenderState& state, LayerProgram::TextBinding& textBinding, std::string& error);
|
||||||
bool UpdateGlobalParamsBuffer(const RuntimeRenderState& state, unsigned availableSourceHistoryLength, unsigned availableTemporalHistoryLength);
|
bool UpdateGlobalParamsBuffer(const RuntimeRenderState& state, unsigned availableSourceHistoryLength, unsigned availableTemporalHistoryLength, bool feedbackAvailable);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
OpenGLRenderer& mRenderer;
|
OpenGLRenderer& mRenderer;
|
||||||
|
|||||||
@@ -103,16 +103,25 @@ GLint ShaderTextureBindings::FindSamplerUniformLocation(GLuint program, const st
|
|||||||
return glGetUniformLocation(program, (samplerName + "_0").c_str());
|
return glGetUniformLocation(program, (samplerName + "_0").c_str());
|
||||||
}
|
}
|
||||||
|
|
||||||
GLuint ShaderTextureBindings::ResolveShaderTextureBase(const RuntimeRenderState& state, unsigned historyCap) const
|
GLuint ShaderTextureBindings::ResolveFeedbackTextureUnit(const RuntimeRenderState& state, unsigned historyCap) const
|
||||||
{
|
{
|
||||||
return state.isTemporal ? kSourceHistoryTextureUnitBase + historyCap + historyCap : kSourceHistoryTextureUnitBase;
|
return state.isTemporal ? kSourceHistoryTextureUnitBase + historyCap + historyCap : kSourceHistoryTextureUnitBase;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
GLuint ShaderTextureBindings::ResolveShaderTextureBase(const RuntimeRenderState& state, unsigned historyCap) const
|
||||||
|
{
|
||||||
|
return ResolveFeedbackTextureUnit(state, historyCap) + (state.feedback.enabled ? 1u : 0u);
|
||||||
|
}
|
||||||
|
|
||||||
void ShaderTextureBindings::AssignLayerSamplerUniforms(GLuint program, const RuntimeRenderState& state, const PassProgram& passProgram, unsigned historyCap) const
|
void ShaderTextureBindings::AssignLayerSamplerUniforms(GLuint program, const RuntimeRenderState& state, const PassProgram& passProgram, unsigned historyCap) const
|
||||||
{
|
{
|
||||||
const GLuint shaderTextureBase = ResolveShaderTextureBase(state, historyCap);
|
const GLuint shaderTextureBase = ResolveShaderTextureBase(state, historyCap);
|
||||||
|
|
||||||
const GLint videoInputLocation = glGetUniformLocation(program, "gVideoInput");
|
const GLint layerInputLocation = FindSamplerUniformLocation(program, "gLayerInput");
|
||||||
|
if (layerInputLocation >= 0)
|
||||||
|
glUniform1i(layerInputLocation, static_cast<GLint>(kLayerInputTextureUnit));
|
||||||
|
|
||||||
|
const GLint videoInputLocation = FindSamplerUniformLocation(program, "gVideoInput");
|
||||||
if (videoInputLocation >= 0)
|
if (videoInputLocation >= 0)
|
||||||
glUniform1i(videoInputLocation, static_cast<GLint>(kDecodedVideoTextureUnit));
|
glUniform1i(videoInputLocation, static_cast<GLint>(kDecodedVideoTextureUnit));
|
||||||
|
|
||||||
@@ -129,6 +138,13 @@ void ShaderTextureBindings::AssignLayerSamplerUniforms(GLuint program, const Run
|
|||||||
glUniform1i(temporalSamplerLocation, static_cast<GLint>(kSourceHistoryTextureUnitBase + historyCap + index));
|
glUniform1i(temporalSamplerLocation, static_cast<GLint>(kSourceHistoryTextureUnitBase + historyCap + index));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (state.feedback.enabled)
|
||||||
|
{
|
||||||
|
const GLint feedbackSamplerLocation = FindSamplerUniformLocation(program, "gFeedbackState");
|
||||||
|
if (feedbackSamplerLocation >= 0)
|
||||||
|
glUniform1i(feedbackSamplerLocation, static_cast<GLint>(ResolveFeedbackTextureUnit(state, historyCap)));
|
||||||
|
}
|
||||||
|
|
||||||
for (std::size_t index = 0; index < passProgram.textureBindings.size(); ++index)
|
for (std::size_t index = 0; index < passProgram.textureBindings.size(); ++index)
|
||||||
{
|
{
|
||||||
const GLint textureSamplerLocation = FindSamplerUniformLocation(program, passProgram.textureBindings[index].samplerName);
|
const GLint textureSamplerLocation = FindSamplerUniformLocation(program, passProgram.textureBindings[index].samplerName);
|
||||||
@@ -148,10 +164,14 @@ void ShaderTextureBindings::AssignLayerSamplerUniforms(GLuint program, const Run
|
|||||||
ShaderTextureBindings::RuntimeTextureBindingPlan ShaderTextureBindings::BuildLayerRuntimeBindingPlan(
|
ShaderTextureBindings::RuntimeTextureBindingPlan ShaderTextureBindings::BuildLayerRuntimeBindingPlan(
|
||||||
const PassProgram& passProgram,
|
const PassProgram& passProgram,
|
||||||
GLuint layerInputTexture,
|
GLuint layerInputTexture,
|
||||||
|
GLuint originalLayerInputTexture,
|
||||||
|
const RuntimeRenderState& state,
|
||||||
|
GLuint feedbackTexture,
|
||||||
const std::vector<GLuint>& sourceHistoryTextures,
|
const std::vector<GLuint>& sourceHistoryTextures,
|
||||||
const std::vector<GLuint>& temporalHistoryTextures) const
|
const std::vector<GLuint>& temporalHistoryTextures) const
|
||||||
{
|
{
|
||||||
RuntimeTextureBindingPlan plan;
|
RuntimeTextureBindingPlan plan;
|
||||||
|
plan.bindings.push_back({ "originalLayerInput", "gLayerInput", originalLayerInputTexture, kLayerInputTextureUnit });
|
||||||
plan.bindings.push_back({ "layerInput", "gVideoInput", layerInputTexture, kDecodedVideoTextureUnit });
|
plan.bindings.push_back({ "layerInput", "gVideoInput", layerInputTexture, kDecodedVideoTextureUnit });
|
||||||
|
|
||||||
for (std::size_t index = 0; index < sourceHistoryTextures.size(); ++index)
|
for (std::size_t index = 0; index < sourceHistoryTextures.size(); ++index)
|
||||||
@@ -175,7 +195,20 @@ ShaderTextureBindings::RuntimeTextureBindingPlan ShaderTextureBindings::BuildLay
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const GLuint shaderTextureBase = passProgram.shaderTextureBase != 0 ? passProgram.shaderTextureBase : kSourceHistoryTextureUnitBase;
|
const GLuint feedbackTextureUnit = ResolveFeedbackTextureUnit(state, static_cast<unsigned>(sourceHistoryTextures.size()));
|
||||||
|
if (state.feedback.enabled)
|
||||||
|
{
|
||||||
|
plan.bindings.push_back({
|
||||||
|
"feedbackState",
|
||||||
|
"gFeedbackState",
|
||||||
|
feedbackTexture,
|
||||||
|
feedbackTextureUnit
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const GLuint shaderTextureBase = passProgram.shaderTextureBase != 0
|
||||||
|
? passProgram.shaderTextureBase
|
||||||
|
: feedbackTextureUnit + (state.feedback.enabled ? 1u : 0u);
|
||||||
for (std::size_t index = 0; index < passProgram.textureBindings.size(); ++index)
|
for (std::size_t index = 0; index < passProgram.textureBindings.size(); ++index)
|
||||||
{
|
{
|
||||||
const LayerProgram::TextureBinding& textureBinding = passProgram.textureBindings[index];
|
const LayerProgram::TextureBinding& textureBinding = passProgram.textureBindings[index];
|
||||||
|
|||||||
@@ -29,11 +29,15 @@ public:
|
|||||||
void CreateTextBindings(const RuntimeRenderState& state, std::vector<LayerProgram::TextBinding>& textBindings);
|
void CreateTextBindings(const RuntimeRenderState& state, std::vector<LayerProgram::TextBinding>& textBindings);
|
||||||
bool UpdateTextBindingTexture(const RuntimeRenderState& state, LayerProgram::TextBinding& textBinding, std::string& error);
|
bool UpdateTextBindingTexture(const RuntimeRenderState& state, LayerProgram::TextBinding& textBinding, std::string& error);
|
||||||
GLint FindSamplerUniformLocation(GLuint program, const std::string& samplerName) const;
|
GLint FindSamplerUniformLocation(GLuint program, const std::string& samplerName) const;
|
||||||
|
GLuint ResolveFeedbackTextureUnit(const RuntimeRenderState& state, unsigned historyCap) const;
|
||||||
GLuint ResolveShaderTextureBase(const RuntimeRenderState& state, unsigned historyCap) const;
|
GLuint ResolveShaderTextureBase(const RuntimeRenderState& state, unsigned historyCap) const;
|
||||||
void AssignLayerSamplerUniforms(GLuint program, const RuntimeRenderState& state, const PassProgram& passProgram, unsigned historyCap) const;
|
void AssignLayerSamplerUniforms(GLuint program, const RuntimeRenderState& state, const PassProgram& passProgram, unsigned historyCap) const;
|
||||||
RuntimeTextureBindingPlan BuildLayerRuntimeBindingPlan(
|
RuntimeTextureBindingPlan BuildLayerRuntimeBindingPlan(
|
||||||
const PassProgram& passProgram,
|
const PassProgram& passProgram,
|
||||||
GLuint layerInputTexture,
|
GLuint layerInputTexture,
|
||||||
|
GLuint originalLayerInputTexture,
|
||||||
|
const RuntimeRenderState& state,
|
||||||
|
GLuint feedbackTexture,
|
||||||
const std::vector<GLuint>& sourceHistoryTextures,
|
const std::vector<GLuint>& sourceHistoryTextures,
|
||||||
const std::vector<GLuint>& temporalHistoryTextures) const;
|
const std::vector<GLuint>& temporalHistoryTextures) const;
|
||||||
void BindRuntimeTexturePlan(const RuntimeTextureBindingPlan& plan) const;
|
void BindRuntimeTexturePlan(const RuntimeTextureBindingPlan& plan) const;
|
||||||
|
|||||||
@@ -33,6 +33,11 @@ bool IsFiniteNumber(double value)
|
|||||||
return std::isfinite(value) != 0;
|
return std::isfinite(value) != 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
double Clamp01(double value)
|
||||||
|
{
|
||||||
|
return std::max(0.0, std::min(1.0, value));
|
||||||
|
}
|
||||||
|
|
||||||
std::string ToLowerCopy(std::string text)
|
std::string ToLowerCopy(std::string text)
|
||||||
{
|
{
|
||||||
std::transform(text.begin(), text.end(), text.begin(),
|
std::transform(text.begin(), text.end(), text.begin(),
|
||||||
@@ -56,6 +61,20 @@ bool MatchesControlKey(const std::string& candidate, const std::string& key)
|
|||||||
return candidate == key || SimplifyControlKey(candidate) == SimplifyControlKey(key);
|
return candidate == key || SimplifyControlKey(candidate) == SimplifyControlKey(key);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool JsonValueContainsOnlyNumbers(const JsonValue& value)
|
||||||
|
{
|
||||||
|
if (!value.isArray())
|
||||||
|
return false;
|
||||||
|
|
||||||
|
for (const JsonValue& item : value.asArray())
|
||||||
|
{
|
||||||
|
if (!item.isNumber() || !IsFiniteNumber(item.asNumber()))
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
double GenerateStartupRandom()
|
double GenerateStartupRandom()
|
||||||
{
|
{
|
||||||
std::random_device randomDevice;
|
std::random_device randomDevice;
|
||||||
@@ -970,7 +989,7 @@ bool RuntimeHost::SetLayerBypass(const std::string& layerId, bool bypassed, std:
|
|||||||
|
|
||||||
layer->bypass = bypassed;
|
layer->bypass = bypassed;
|
||||||
mReloadRequested = true;
|
mReloadRequested = true;
|
||||||
MarkRenderStateDirtyLocked();
|
MarkParameterStateDirtyLocked();
|
||||||
return SavePersistentState(error);
|
return SavePersistentState(error);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1032,7 +1051,7 @@ bool RuntimeHost::UpdateLayerParameter(const std::string& layerId, const std::st
|
|||||||
const double previousCount = value.numberValues.empty() ? 0.0 : value.numberValues[0];
|
const double previousCount = value.numberValues.empty() ? 0.0 : value.numberValues[0];
|
||||||
const double triggerTime = std::chrono::duration_cast<std::chrono::duration<double>>(std::chrono::steady_clock::now() - mStartTime).count();
|
const double triggerTime = std::chrono::duration_cast<std::chrono::duration<double>>(std::chrono::steady_clock::now() - mStartTime).count();
|
||||||
value.numberValues = { previousCount + 1.0, triggerTime };
|
value.numberValues = { previousCount + 1.0, triggerTime };
|
||||||
MarkRenderStateDirtyLocked();
|
MarkParameterStateDirtyLocked();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1041,11 +1060,16 @@ bool RuntimeHost::UpdateLayerParameter(const std::string& layerId, const std::st
|
|||||||
return false;
|
return false;
|
||||||
|
|
||||||
layer->parameterValues[parameterId] = normalized;
|
layer->parameterValues[parameterId] = normalized;
|
||||||
MarkRenderStateDirtyLocked();
|
MarkParameterStateDirtyLocked();
|
||||||
return SavePersistentState(error);
|
return SavePersistentState(error);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool RuntimeHost::UpdateLayerParameterByControlKey(const std::string& layerKey, const std::string& parameterKey, const JsonValue& newValue, std::string& error)
|
bool RuntimeHost::UpdateLayerParameterByControlKey(const std::string& layerKey, const std::string& parameterKey, const JsonValue& newValue, std::string& error)
|
||||||
|
{
|
||||||
|
return UpdateLayerParameterByControlKey(layerKey, parameterKey, newValue, true, error);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool RuntimeHost::UpdateLayerParameterByControlKey(const std::string& layerKey, const std::string& parameterKey, const JsonValue& newValue, bool persistState, std::string& error)
|
||||||
{
|
{
|
||||||
std::lock_guard<std::mutex> lock(mMutex);
|
std::lock_guard<std::mutex> lock(mMutex);
|
||||||
|
|
||||||
@@ -1089,7 +1113,7 @@ bool RuntimeHost::UpdateLayerParameterByControlKey(const std::string& layerKey,
|
|||||||
const double previousCount = value.numberValues.empty() ? 0.0 : value.numberValues[0];
|
const double previousCount = value.numberValues.empty() ? 0.0 : value.numberValues[0];
|
||||||
const double triggerTime = std::chrono::duration_cast<std::chrono::duration<double>>(std::chrono::steady_clock::now() - mStartTime).count();
|
const double triggerTime = std::chrono::duration_cast<std::chrono::duration<double>>(std::chrono::steady_clock::now() - mStartTime).count();
|
||||||
value.numberValues = { previousCount + 1.0, triggerTime };
|
value.numberValues = { previousCount + 1.0, triggerTime };
|
||||||
MarkRenderStateDirtyLocked();
|
MarkParameterStateDirtyLocked();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1098,8 +1122,141 @@ bool RuntimeHost::UpdateLayerParameterByControlKey(const std::string& layerKey,
|
|||||||
return false;
|
return false;
|
||||||
|
|
||||||
matchedLayer->parameterValues[parameterIt->id] = normalized;
|
matchedLayer->parameterValues[parameterIt->id] = normalized;
|
||||||
MarkRenderStateDirtyLocked();
|
MarkParameterStateDirtyLocked();
|
||||||
return SavePersistentState(error);
|
return !persistState || SavePersistentState(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool RuntimeHost::ApplyOscTargetByControlKey(const std::string& layerKey, const std::string& parameterKey, const JsonValue& targetValue, double smoothingAmount, bool& keepApplying, std::string& resolvedLayerId, std::string& resolvedParameterId, ShaderParameterValue& appliedValue, std::string& error)
|
||||||
|
{
|
||||||
|
keepApplying = false;
|
||||||
|
resolvedLayerId.clear();
|
||||||
|
resolvedParameterId.clear();
|
||||||
|
appliedValue = ShaderParameterValue();
|
||||||
|
|
||||||
|
std::lock_guard<std::mutex> lock(mMutex);
|
||||||
|
|
||||||
|
LayerPersistentState* matchedLayer = nullptr;
|
||||||
|
const ShaderPackage* matchedPackage = nullptr;
|
||||||
|
for (LayerPersistentState& layer : mPersistentState.layers)
|
||||||
|
{
|
||||||
|
auto shaderIt = mPackagesById.find(layer.shaderId);
|
||||||
|
if (shaderIt == mPackagesById.end())
|
||||||
|
continue;
|
||||||
|
|
||||||
|
if (MatchesControlKey(layer.id, layerKey) || MatchesControlKey(shaderIt->second.id, layerKey) ||
|
||||||
|
MatchesControlKey(shaderIt->second.displayName, layerKey))
|
||||||
|
{
|
||||||
|
matchedLayer = &layer;
|
||||||
|
matchedPackage = &shaderIt->second;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!matchedLayer || !matchedPackage)
|
||||||
|
{
|
||||||
|
error = "Unknown OSC layer key: " + layerKey;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
resolvedLayerId = matchedLayer->id;
|
||||||
|
|
||||||
|
const auto parameterIt = std::find_if(matchedPackage->parameters.begin(), matchedPackage->parameters.end(),
|
||||||
|
[¶meterKey](const ShaderParameterDefinition& definition)
|
||||||
|
{
|
||||||
|
return MatchesControlKey(definition.id, parameterKey) || MatchesControlKey(definition.label, parameterKey);
|
||||||
|
});
|
||||||
|
if (parameterIt == matchedPackage->parameters.end())
|
||||||
|
{
|
||||||
|
error = "Unknown OSC parameter key: " + parameterKey;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
resolvedParameterId = parameterIt->id;
|
||||||
|
|
||||||
|
if (parameterIt->type == ShaderParameterType::Trigger)
|
||||||
|
{
|
||||||
|
ShaderParameterValue& value = matchedLayer->parameterValues[parameterIt->id];
|
||||||
|
const double previousCount = value.numberValues.empty() ? 0.0 : value.numberValues[0];
|
||||||
|
const double triggerTime = std::chrono::duration_cast<std::chrono::duration<double>>(std::chrono::steady_clock::now() - mStartTime).count();
|
||||||
|
value.numberValues = { previousCount + 1.0, triggerTime };
|
||||||
|
MarkParameterStateDirtyLocked();
|
||||||
|
appliedValue = value;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
ShaderParameterValue normalizedTarget;
|
||||||
|
if (!NormalizeAndValidateValue(*parameterIt, targetValue, normalizedTarget, error))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
const bool smoothableType =
|
||||||
|
parameterIt->type == ShaderParameterType::Float ||
|
||||||
|
parameterIt->type == ShaderParameterType::Vec2 ||
|
||||||
|
parameterIt->type == ShaderParameterType::Color;
|
||||||
|
const bool smoothableInput = targetValue.isNumber() || JsonValueContainsOnlyNumbers(targetValue);
|
||||||
|
const double alpha = Clamp01(smoothingAmount);
|
||||||
|
|
||||||
|
if (!smoothableType || !smoothableInput || alpha <= 0.0)
|
||||||
|
{
|
||||||
|
matchedLayer->parameterValues[parameterIt->id] = normalizedTarget;
|
||||||
|
MarkParameterStateDirtyLocked();
|
||||||
|
appliedValue = normalizedTarget;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
ShaderParameterValue currentValue = DefaultValueForDefinition(*parameterIt);
|
||||||
|
auto currentIt = matchedLayer->parameterValues.find(parameterIt->id);
|
||||||
|
if (currentIt != matchedLayer->parameterValues.end())
|
||||||
|
currentValue = currentIt->second;
|
||||||
|
|
||||||
|
ShaderParameterValue nextValue = normalizedTarget;
|
||||||
|
nextValue.numberValues = normalizedTarget.numberValues;
|
||||||
|
|
||||||
|
if (currentValue.numberValues.size() != normalizedTarget.numberValues.size())
|
||||||
|
currentValue.numberValues = normalizedTarget.numberValues;
|
||||||
|
|
||||||
|
bool changed = false;
|
||||||
|
bool converged = true;
|
||||||
|
for (std::size_t index = 0; index < normalizedTarget.numberValues.size(); ++index)
|
||||||
|
{
|
||||||
|
const double currentNumber = currentValue.numberValues[index];
|
||||||
|
const double targetNumber = normalizedTarget.numberValues[index];
|
||||||
|
const double delta = targetNumber - currentNumber;
|
||||||
|
double nextNumber = currentNumber + delta * alpha;
|
||||||
|
|
||||||
|
if (std::fabs(delta) <= 0.0005)
|
||||||
|
{
|
||||||
|
nextNumber = targetNumber;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
converged = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (std::fabs(nextNumber - currentNumber) > 0.0000001)
|
||||||
|
changed = true;
|
||||||
|
|
||||||
|
nextValue.numberValues[index] = nextNumber;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!converged)
|
||||||
|
{
|
||||||
|
keepApplying = true;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
nextValue.numberValues = normalizedTarget.numberValues;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!changed && !keepApplying)
|
||||||
|
{
|
||||||
|
appliedValue = matchedLayer->parameterValues[parameterIt->id];
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
matchedLayer->parameterValues[parameterIt->id] = nextValue;
|
||||||
|
MarkParameterStateDirtyLocked();
|
||||||
|
appliedValue = nextValue;
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool RuntimeHost::ResetLayerParameters(const std::string& layerId, std::string& error)
|
bool RuntimeHost::ResetLayerParameters(const std::string& layerId, std::string& error)
|
||||||
@@ -1122,7 +1279,7 @@ bool RuntimeHost::ResetLayerParameters(const std::string& layerId, std::string&
|
|||||||
|
|
||||||
layer->parameterValues.clear();
|
layer->parameterValues.clear();
|
||||||
EnsureLayerDefaultsLocked(*layer, shaderIt->second);
|
EnsureLayerDefaultsLocked(*layer, shaderIt->second);
|
||||||
MarkRenderStateDirtyLocked();
|
MarkParameterStateDirtyLocked();
|
||||||
return SavePersistentState(error);
|
return SavePersistentState(error);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1226,22 +1383,33 @@ void RuntimeHost::SetSignalStatusLocked(bool hasSignal, unsigned width, unsigned
|
|||||||
void RuntimeHost::MarkRenderStateDirtyLocked()
|
void RuntimeHost::MarkRenderStateDirtyLocked()
|
||||||
{
|
{
|
||||||
mRenderStateVersion.fetch_add(1, std::memory_order_relaxed);
|
mRenderStateVersion.fetch_add(1, std::memory_order_relaxed);
|
||||||
|
mParameterStateVersion.fetch_add(1, std::memory_order_relaxed);
|
||||||
}
|
}
|
||||||
|
|
||||||
void RuntimeHost::SetVideoIOStatus(const VideoIOState& state)
|
void RuntimeHost::MarkParameterStateDirtyLocked()
|
||||||
|
{
|
||||||
|
mParameterStateVersion.fetch_add(1, std::memory_order_relaxed);
|
||||||
|
}
|
||||||
|
|
||||||
|
void RuntimeHost::SetDeckLinkOutputStatus(const std::string& modelName, bool supportsInternalKeying, bool supportsExternalKeying,
|
||||||
|
bool keyerInterfaceAvailable, bool externalKeyingRequested, bool externalKeyingActive, const std::string& statusMessage)
|
||||||
|
{
|
||||||
|
SetVideoIOStatus("decklink", modelName, supportsInternalKeying, supportsExternalKeying, keyerInterfaceAvailable,
|
||||||
|
externalKeyingRequested, externalKeyingActive, statusMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
void RuntimeHost::SetVideoIOStatus(const std::string& backendName, const std::string& modelName, bool supportsInternalKeying, bool supportsExternalKeying,
|
||||||
|
bool keyerInterfaceAvailable, bool externalKeyingRequested, bool externalKeyingActive, const std::string& statusMessage)
|
||||||
{
|
{
|
||||||
std::lock_guard<std::mutex> lock(mMutex);
|
std::lock_guard<std::mutex> lock(mMutex);
|
||||||
mVideoIOStatus.backendId = state.backendId;
|
mDeckLinkOutputStatus.backendName = backendName;
|
||||||
mVideoIOStatus.deviceName = state.deviceName;
|
mDeckLinkOutputStatus.modelName = modelName;
|
||||||
mVideoIOStatus.capabilities = state.capabilities;
|
mDeckLinkOutputStatus.supportsInternalKeying = supportsInternalKeying;
|
||||||
mVideoIOStatus.hasInputDevice = state.hasInputDevice;
|
mDeckLinkOutputStatus.supportsExternalKeying = supportsExternalKeying;
|
||||||
mVideoIOStatus.hasInputSource = state.hasInputSource;
|
mDeckLinkOutputStatus.keyerInterfaceAvailable = keyerInterfaceAvailable;
|
||||||
mVideoIOStatus.inputDisplayModeName = state.inputDisplayModeName;
|
mDeckLinkOutputStatus.externalKeyingRequested = externalKeyingRequested;
|
||||||
mVideoIOStatus.outputDisplayModeName = state.outputDisplayModeName;
|
mDeckLinkOutputStatus.externalKeyingActive = externalKeyingActive;
|
||||||
mVideoIOStatus.externalKeyingRequested = state.externalKeyingRequested;
|
mDeckLinkOutputStatus.statusMessage = statusMessage;
|
||||||
mVideoIOStatus.externalKeyingActive = state.externalKeyingActive;
|
|
||||||
mVideoIOStatus.statusMessage = state.statusMessage;
|
|
||||||
mVideoIOStatus.formatStatusMessage = state.formatStatusMessage;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void RuntimeHost::SetPerformanceStats(double frameBudgetMilliseconds, double renderMilliseconds)
|
void RuntimeHost::SetPerformanceStats(double frameBudgetMilliseconds, double renderMilliseconds)
|
||||||
@@ -1383,6 +1551,34 @@ bool RuntimeHost::TryGetLayerRenderStates(unsigned outputWidth, unsigned outputH
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool RuntimeHost::TryRefreshCachedLayerStates(std::vector<RuntimeRenderState>& states) const
|
||||||
|
{
|
||||||
|
std::unique_lock<std::mutex> lock(mMutex, std::try_to_lock);
|
||||||
|
if (!lock.owns_lock())
|
||||||
|
return false;
|
||||||
|
|
||||||
|
for (RuntimeRenderState& state : states)
|
||||||
|
{
|
||||||
|
const auto layerIt = std::find_if(mPersistentState.layers.begin(), mPersistentState.layers.end(),
|
||||||
|
[&state](const LayerPersistentState& layer) { return layer.id == state.layerId; });
|
||||||
|
if (layerIt == mPersistentState.layers.end())
|
||||||
|
continue;
|
||||||
|
|
||||||
|
state.bypass = layerIt->bypass ? 1.0 : 0.0;
|
||||||
|
state.parameterValues.clear();
|
||||||
|
for (const ShaderParameterDefinition& definition : state.parameterDefinitions)
|
||||||
|
{
|
||||||
|
ShaderParameterValue value = DefaultValueForDefinition(definition);
|
||||||
|
auto valueIt = layerIt->parameterValues.find(definition.id);
|
||||||
|
if (valueIt != layerIt->parameterValues.end())
|
||||||
|
value = valueIt->second;
|
||||||
|
state.parameterValues[definition.id] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
void RuntimeHost::RefreshDynamicRenderStateFields(std::vector<RuntimeRenderState>& states) const
|
void RuntimeHost::RefreshDynamicRenderStateFields(std::vector<RuntimeRenderState>& states) const
|
||||||
{
|
{
|
||||||
const RuntimeClockSnapshot clock = GetRuntimeClockSnapshot();
|
const RuntimeClockSnapshot clock = GetRuntimeClockSnapshot();
|
||||||
@@ -1410,6 +1606,7 @@ void RuntimeHost::BuildLayerRenderStatesLocked(unsigned outputWidth, unsigned ou
|
|||||||
RuntimeRenderState state;
|
RuntimeRenderState state;
|
||||||
state.layerId = layer.id;
|
state.layerId = layer.id;
|
||||||
state.shaderId = layer.shaderId;
|
state.shaderId = layer.shaderId;
|
||||||
|
state.shaderName = shaderIt->second.displayName;
|
||||||
state.mixAmount = 1.0;
|
state.mixAmount = 1.0;
|
||||||
state.bypass = layer.bypass ? 1.0 : 0.0;
|
state.bypass = layer.bypass ? 1.0 : 0.0;
|
||||||
state.inputWidth = mSignalWidth;
|
state.inputWidth = mSignalWidth;
|
||||||
@@ -1423,6 +1620,7 @@ void RuntimeHost::BuildLayerRenderStatesLocked(unsigned outputWidth, unsigned ou
|
|||||||
state.temporalHistorySource = shaderIt->second.temporal.historySource;
|
state.temporalHistorySource = shaderIt->second.temporal.historySource;
|
||||||
state.requestedTemporalHistoryLength = shaderIt->second.temporal.requestedHistoryLength;
|
state.requestedTemporalHistoryLength = shaderIt->second.temporal.requestedHistoryLength;
|
||||||
state.effectiveTemporalHistoryLength = shaderIt->second.temporal.effectiveHistoryLength;
|
state.effectiveTemporalHistoryLength = shaderIt->second.temporal.effectiveHistoryLength;
|
||||||
|
state.feedback = shaderIt->second.feedback;
|
||||||
|
|
||||||
for (const ShaderParameterDefinition& definition : shaderIt->second.parameters)
|
for (const ShaderParameterDefinition& definition : shaderIt->second.parameters)
|
||||||
{
|
{
|
||||||
@@ -1469,6 +1667,10 @@ bool RuntimeHost::LoadConfig(std::string& error)
|
|||||||
mConfig.serverPort = static_cast<unsigned short>(serverPortValue->asNumber(mConfig.serverPort));
|
mConfig.serverPort = static_cast<unsigned short>(serverPortValue->asNumber(mConfig.serverPort));
|
||||||
if (const JsonValue* oscPortValue = configJson.find("oscPort"))
|
if (const JsonValue* oscPortValue = configJson.find("oscPort"))
|
||||||
mConfig.oscPort = static_cast<unsigned short>(oscPortValue->asNumber(mConfig.oscPort));
|
mConfig.oscPort = static_cast<unsigned short>(oscPortValue->asNumber(mConfig.oscPort));
|
||||||
|
if (const JsonValue* oscBindAddressValue = configJson.find("oscBindAddress"))
|
||||||
|
mConfig.oscBindAddress = oscBindAddressValue->asString();
|
||||||
|
if (const JsonValue* oscSmoothingValue = configJson.find("oscSmoothing"))
|
||||||
|
mConfig.oscSmoothing = Clamp01(oscSmoothingValue->asNumber(mConfig.oscSmoothing));
|
||||||
if (const JsonValue* autoReloadValue = configJson.find("autoReload"))
|
if (const JsonValue* autoReloadValue = configJson.find("autoReload"))
|
||||||
mConfig.autoReload = autoReloadValue->asBoolean(mConfig.autoReload);
|
mConfig.autoReload = autoReloadValue->asBoolean(mConfig.autoReload);
|
||||||
if (const JsonValue* maxTemporalHistoryFramesValue = configJson.find("maxTemporalHistoryFrames"))
|
if (const JsonValue* maxTemporalHistoryFramesValue = configJson.find("maxTemporalHistoryFrames"))
|
||||||
@@ -1476,67 +1678,66 @@ bool RuntimeHost::LoadConfig(std::string& error)
|
|||||||
const double configuredValue = maxTemporalHistoryFramesValue->asNumber(static_cast<double>(mConfig.maxTemporalHistoryFrames));
|
const double configuredValue = maxTemporalHistoryFramesValue->asNumber(static_cast<double>(mConfig.maxTemporalHistoryFrames));
|
||||||
mConfig.maxTemporalHistoryFrames = configuredValue <= 0.0 ? 0u : static_cast<unsigned>(configuredValue);
|
mConfig.maxTemporalHistoryFrames = configuredValue <= 0.0 ? 0u : static_cast<unsigned>(configuredValue);
|
||||||
}
|
}
|
||||||
if (const JsonValue* videoBackendValue = configJson.find("videoBackend"))
|
if (const JsonValue* previewFpsValue = configJson.find("previewFps"))
|
||||||
{
|
{
|
||||||
VideoIOBackendId backendId = mConfig.videoIO.backendId;
|
const double configuredValue = previewFpsValue->asNumber(static_cast<double>(mConfig.previewFps));
|
||||||
if (videoBackendValue->isString() && ParseVideoIOBackendId(videoBackendValue->asString(), backendId))
|
mConfig.previewFps = configuredValue <= 0.0 ? 0u : static_cast<unsigned>(configuredValue);
|
||||||
mConfig.videoIO.backendId = backendId;
|
|
||||||
}
|
}
|
||||||
if (const JsonValue* enableExternalKeyingValue = configJson.find("enableExternalKeying"))
|
if (const JsonValue* enableExternalKeyingValue = configJson.find("enableExternalKeying"))
|
||||||
mConfig.videoIO.externalKeyingEnabled = enableExternalKeyingValue->asBoolean(mConfig.videoIO.externalKeyingEnabled);
|
mConfig.enableExternalKeying = enableExternalKeyingValue->asBoolean(mConfig.enableExternalKeying);
|
||||||
if (const JsonValue* videoFormatValue = configJson.find("videoFormat"))
|
if (const JsonValue* videoFormatValue = configJson.find("videoFormat"))
|
||||||
{
|
{
|
||||||
if (videoFormatValue->isString() && !videoFormatValue->asString().empty())
|
if (videoFormatValue->isString() && !videoFormatValue->asString().empty())
|
||||||
{
|
{
|
||||||
mConfig.videoIO.inputMode.videoFormat = videoFormatValue->asString();
|
mConfig.inputVideoFormat = videoFormatValue->asString();
|
||||||
mConfig.videoIO.outputMode.videoFormat = videoFormatValue->asString();
|
mConfig.outputVideoFormat = videoFormatValue->asString();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (const JsonValue* frameRateValue = configJson.find("frameRate"))
|
if (const JsonValue* frameRateValue = configJson.find("frameRate"))
|
||||||
{
|
{
|
||||||
if (frameRateValue->isString() && !frameRateValue->asString().empty())
|
if (frameRateValue->isString() && !frameRateValue->asString().empty())
|
||||||
{
|
{
|
||||||
mConfig.videoIO.inputMode.frameRate = frameRateValue->asString();
|
mConfig.inputFrameRate = frameRateValue->asString();
|
||||||
mConfig.videoIO.outputMode.frameRate = frameRateValue->asString();
|
mConfig.outputFrameRate = frameRateValue->asString();
|
||||||
}
|
}
|
||||||
else if (frameRateValue->isNumber())
|
else if (frameRateValue->isNumber())
|
||||||
{
|
{
|
||||||
std::ostringstream stream;
|
std::ostringstream stream;
|
||||||
stream << frameRateValue->asNumber();
|
stream << frameRateValue->asNumber();
|
||||||
mConfig.videoIO.inputMode.frameRate = stream.str();
|
mConfig.inputFrameRate = stream.str();
|
||||||
mConfig.videoIO.outputMode.frameRate = stream.str();
|
mConfig.outputFrameRate = stream.str();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (const JsonValue* inputVideoFormatValue = configJson.find("inputVideoFormat"))
|
if (const JsonValue* inputVideoFormatValue = configJson.find("inputVideoFormat"))
|
||||||
{
|
{
|
||||||
if (inputVideoFormatValue->isString() && !inputVideoFormatValue->asString().empty())
|
if (inputVideoFormatValue->isString() && !inputVideoFormatValue->asString().empty())
|
||||||
mConfig.videoIO.inputMode.videoFormat = inputVideoFormatValue->asString();
|
mConfig.inputVideoFormat = inputVideoFormatValue->asString();
|
||||||
}
|
}
|
||||||
if (const JsonValue* inputFrameRateValue = configJson.find("inputFrameRate"))
|
if (const JsonValue* inputFrameRateValue = configJson.find("inputFrameRate"))
|
||||||
{
|
{
|
||||||
if (inputFrameRateValue->isString() && !inputFrameRateValue->asString().empty())
|
if (inputFrameRateValue->isString() && !inputFrameRateValue->asString().empty())
|
||||||
mConfig.videoIO.inputMode.frameRate = inputFrameRateValue->asString();
|
mConfig.inputFrameRate = inputFrameRateValue->asString();
|
||||||
else if (inputFrameRateValue->isNumber())
|
else if (inputFrameRateValue->isNumber())
|
||||||
{
|
{
|
||||||
std::ostringstream stream;
|
std::ostringstream stream;
|
||||||
stream << inputFrameRateValue->asNumber();
|
stream << inputFrameRateValue->asNumber();
|
||||||
mConfig.videoIO.inputMode.frameRate = stream.str();
|
mConfig.inputFrameRate = stream.str();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (const JsonValue* outputVideoFormatValue = configJson.find("outputVideoFormat"))
|
if (const JsonValue* outputVideoFormatValue = configJson.find("outputVideoFormat"))
|
||||||
{
|
{
|
||||||
if (outputVideoFormatValue->isString() && !outputVideoFormatValue->asString().empty())
|
if (outputVideoFormatValue->isString() && !outputVideoFormatValue->asString().empty())
|
||||||
mConfig.videoIO.outputMode.videoFormat = outputVideoFormatValue->asString();
|
mConfig.outputVideoFormat = outputVideoFormatValue->asString();
|
||||||
}
|
}
|
||||||
if (const JsonValue* outputFrameRateValue = configJson.find("outputFrameRate"))
|
if (const JsonValue* outputFrameRateValue = configJson.find("outputFrameRate"))
|
||||||
{
|
{
|
||||||
if (outputFrameRateValue->isString() && !outputFrameRateValue->asString().empty())
|
if (outputFrameRateValue->isString() && !outputFrameRateValue->asString().empty())
|
||||||
mConfig.videoIO.outputMode.frameRate = outputFrameRateValue->asString();
|
mConfig.outputFrameRate = outputFrameRateValue->asString();
|
||||||
else if (outputFrameRateValue->isNumber())
|
else if (outputFrameRateValue->isNumber())
|
||||||
{
|
{
|
||||||
std::ostringstream stream;
|
std::ostringstream stream;
|
||||||
stream << outputFrameRateValue->asNumber();
|
stream << outputFrameRateValue->asNumber();
|
||||||
mConfig.videoIO.outputMode.frameRate = stream.str();
|
mConfig.outputFrameRate = stream.str();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1866,14 +2067,16 @@ JsonValue RuntimeHost::BuildStateValue() const
|
|||||||
JsonValue app = JsonValue::MakeObject();
|
JsonValue app = JsonValue::MakeObject();
|
||||||
app.set("serverPort", JsonValue(static_cast<double>(mServerPort)));
|
app.set("serverPort", JsonValue(static_cast<double>(mServerPort)));
|
||||||
app.set("oscPort", JsonValue(static_cast<double>(mConfig.oscPort)));
|
app.set("oscPort", JsonValue(static_cast<double>(mConfig.oscPort)));
|
||||||
|
app.set("oscBindAddress", JsonValue(mConfig.oscBindAddress));
|
||||||
|
app.set("oscSmoothing", JsonValue(mConfig.oscSmoothing));
|
||||||
app.set("autoReload", JsonValue(mAutoReloadEnabled));
|
app.set("autoReload", JsonValue(mAutoReloadEnabled));
|
||||||
app.set("maxTemporalHistoryFrames", JsonValue(static_cast<double>(mConfig.maxTemporalHistoryFrames)));
|
app.set("maxTemporalHistoryFrames", JsonValue(static_cast<double>(mConfig.maxTemporalHistoryFrames)));
|
||||||
app.set("videoBackend", JsonValue(VideoIOBackendName(mConfig.videoIO.backendId)));
|
app.set("previewFps", JsonValue(static_cast<double>(mConfig.previewFps)));
|
||||||
app.set("enableExternalKeying", JsonValue(mConfig.videoIO.externalKeyingEnabled));
|
app.set("enableExternalKeying", JsonValue(mConfig.enableExternalKeying));
|
||||||
app.set("inputVideoFormat", JsonValue(mConfig.videoIO.inputMode.videoFormat));
|
app.set("inputVideoFormat", JsonValue(mConfig.inputVideoFormat));
|
||||||
app.set("inputFrameRate", JsonValue(mConfig.videoIO.inputMode.frameRate));
|
app.set("inputFrameRate", JsonValue(mConfig.inputFrameRate));
|
||||||
app.set("outputVideoFormat", JsonValue(mConfig.videoIO.outputMode.videoFormat));
|
app.set("outputVideoFormat", JsonValue(mConfig.outputVideoFormat));
|
||||||
app.set("outputFrameRate", JsonValue(mConfig.videoIO.outputMode.frameRate));
|
app.set("outputFrameRate", JsonValue(mConfig.outputFrameRate));
|
||||||
root.set("app", app);
|
root.set("app", app);
|
||||||
|
|
||||||
JsonValue runtime = JsonValue::MakeObject();
|
JsonValue runtime = JsonValue::MakeObject();
|
||||||
@@ -1889,22 +2092,25 @@ JsonValue RuntimeHost::BuildStateValue() const
|
|||||||
video.set("modeName", JsonValue(mSignalModeName));
|
video.set("modeName", JsonValue(mSignalModeName));
|
||||||
root.set("video", video);
|
root.set("video", video);
|
||||||
|
|
||||||
|
JsonValue deckLink = JsonValue::MakeObject();
|
||||||
|
deckLink.set("modelName", JsonValue(mDeckLinkOutputStatus.modelName));
|
||||||
|
deckLink.set("supportsInternalKeying", JsonValue(mDeckLinkOutputStatus.supportsInternalKeying));
|
||||||
|
deckLink.set("supportsExternalKeying", JsonValue(mDeckLinkOutputStatus.supportsExternalKeying));
|
||||||
|
deckLink.set("keyerInterfaceAvailable", JsonValue(mDeckLinkOutputStatus.keyerInterfaceAvailable));
|
||||||
|
deckLink.set("externalKeyingRequested", JsonValue(mDeckLinkOutputStatus.externalKeyingRequested));
|
||||||
|
deckLink.set("externalKeyingActive", JsonValue(mDeckLinkOutputStatus.externalKeyingActive));
|
||||||
|
deckLink.set("statusMessage", JsonValue(mDeckLinkOutputStatus.statusMessage));
|
||||||
|
root.set("decklink", deckLink);
|
||||||
|
|
||||||
JsonValue videoIO = JsonValue::MakeObject();
|
JsonValue videoIO = JsonValue::MakeObject();
|
||||||
videoIO.set("backend", JsonValue(VideoIOBackendName(mVideoIOStatus.backendId)));
|
videoIO.set("backend", JsonValue(mDeckLinkOutputStatus.backendName));
|
||||||
videoIO.set("deviceName", JsonValue(mVideoIOStatus.deviceName));
|
videoIO.set("modelName", JsonValue(mDeckLinkOutputStatus.modelName));
|
||||||
videoIO.set("hasInputDevice", JsonValue(mVideoIOStatus.hasInputDevice));
|
videoIO.set("supportsInternalKeying", JsonValue(mDeckLinkOutputStatus.supportsInternalKeying));
|
||||||
videoIO.set("hasInputSource", JsonValue(mVideoIOStatus.hasInputSource));
|
videoIO.set("supportsExternalKeying", JsonValue(mDeckLinkOutputStatus.supportsExternalKeying));
|
||||||
videoIO.set("inputModeName", JsonValue(mVideoIOStatus.inputDisplayModeName));
|
videoIO.set("keyerInterfaceAvailable", JsonValue(mDeckLinkOutputStatus.keyerInterfaceAvailable));
|
||||||
videoIO.set("outputModeName", JsonValue(mVideoIOStatus.outputDisplayModeName));
|
videoIO.set("externalKeyingRequested", JsonValue(mDeckLinkOutputStatus.externalKeyingRequested));
|
||||||
JsonValue capabilities = JsonValue::MakeObject();
|
videoIO.set("externalKeyingActive", JsonValue(mDeckLinkOutputStatus.externalKeyingActive));
|
||||||
capabilities.set("supportsInternalKeying", JsonValue(mVideoIOStatus.capabilities.supportsInternalKeying));
|
videoIO.set("statusMessage", JsonValue(mDeckLinkOutputStatus.statusMessage));
|
||||||
capabilities.set("supportsExternalKeying", JsonValue(mVideoIOStatus.capabilities.supportsExternalKeying));
|
|
||||||
capabilities.set("keyerInterfaceAvailable", JsonValue(mVideoIOStatus.capabilities.keyerInterfaceAvailable));
|
|
||||||
videoIO.set("capabilities", capabilities);
|
|
||||||
videoIO.set("externalKeyingRequested", JsonValue(mVideoIOStatus.externalKeyingRequested));
|
|
||||||
videoIO.set("externalKeyingActive", JsonValue(mVideoIOStatus.externalKeyingActive));
|
|
||||||
videoIO.set("statusMessage", JsonValue(mVideoIOStatus.statusMessage));
|
|
||||||
videoIO.set("formatStatusMessage", JsonValue(mVideoIOStatus.formatStatusMessage));
|
|
||||||
root.set("videoIO", videoIO);
|
root.set("videoIO", videoIO);
|
||||||
|
|
||||||
JsonValue performance = JsonValue::MakeObject();
|
JsonValue performance = JsonValue::MakeObject();
|
||||||
@@ -1942,6 +2148,13 @@ JsonValue RuntimeHost::BuildStateValue() const
|
|||||||
temporal.set("effectiveHistoryLength", JsonValue(static_cast<double>(shaderIt->second.temporal.effectiveHistoryLength)));
|
temporal.set("effectiveHistoryLength", JsonValue(static_cast<double>(shaderIt->second.temporal.effectiveHistoryLength)));
|
||||||
shader.set("temporal", temporal);
|
shader.set("temporal", temporal);
|
||||||
}
|
}
|
||||||
|
if (status.available && shaderIt != mPackagesById.end() && shaderIt->second.feedback.enabled)
|
||||||
|
{
|
||||||
|
JsonValue feedback = JsonValue::MakeObject();
|
||||||
|
feedback.set("enabled", JsonValue(true));
|
||||||
|
feedback.set("writePass", JsonValue(shaderIt->second.feedback.writePassId));
|
||||||
|
shader.set("feedback", feedback);
|
||||||
|
}
|
||||||
shaderLibrary.pushBack(shader);
|
shaderLibrary.pushBack(shader);
|
||||||
}
|
}
|
||||||
root.set("shaders", shaderLibrary);
|
root.set("shaders", shaderLibrary);
|
||||||
@@ -1979,6 +2192,13 @@ JsonValue RuntimeHost::SerializeLayerStackLocked() const
|
|||||||
temporal.set("effectiveHistoryLength", JsonValue(static_cast<double>(shaderIt->second.temporal.effectiveHistoryLength)));
|
temporal.set("effectiveHistoryLength", JsonValue(static_cast<double>(shaderIt->second.temporal.effectiveHistoryLength)));
|
||||||
layerValue.set("temporal", temporal);
|
layerValue.set("temporal", temporal);
|
||||||
}
|
}
|
||||||
|
if (shaderIt->second.feedback.enabled)
|
||||||
|
{
|
||||||
|
JsonValue feedback = JsonValue::MakeObject();
|
||||||
|
feedback.set("enabled", JsonValue(true));
|
||||||
|
feedback.set("writePass", JsonValue(shaderIt->second.feedback.writePassId));
|
||||||
|
layerValue.set("feedback", feedback);
|
||||||
|
}
|
||||||
|
|
||||||
JsonValue parameters = JsonValue::MakeArray();
|
JsonValue parameters = JsonValue::MakeArray();
|
||||||
for (const ShaderParameterDefinition& definition : shaderIt->second.parameters)
|
for (const ShaderParameterDefinition& definition : shaderIt->second.parameters)
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
|
|
||||||
#include "RuntimeJson.h"
|
#include "RuntimeJson.h"
|
||||||
#include "ShaderTypes.h"
|
#include "ShaderTypes.h"
|
||||||
#include "VideoIOTypes.h"
|
|
||||||
|
|
||||||
#include <atomic>
|
#include <atomic>
|
||||||
#include <chrono>
|
#include <chrono>
|
||||||
@@ -32,6 +31,8 @@ public:
|
|||||||
bool SetLayerShader(const std::string& layerId, const std::string& shaderId, 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 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 UpdateLayerParameterByControlKey(const std::string& layerKey, const std::string& parameterKey, const JsonValue& newValue, std::string& error);
|
||||||
|
bool UpdateLayerParameterByControlKey(const std::string& layerKey, const std::string& parameterKey, const JsonValue& newValue, bool persistState, std::string& error);
|
||||||
|
bool ApplyOscTargetByControlKey(const std::string& layerKey, const std::string& parameterKey, const JsonValue& targetValue, double smoothingAmount, bool& keepApplying, std::string& resolvedLayerId, std::string& resolvedParameterId, ShaderParameterValue& appliedValue, std::string& error);
|
||||||
bool ResetLayerParameters(const std::string& layerId, std::string& error);
|
bool ResetLayerParameters(const std::string& layerId, std::string& error);
|
||||||
bool SaveStackPreset(const std::string& presetName, std::string& error) const;
|
bool SaveStackPreset(const std::string& presetName, std::string& error) const;
|
||||||
bool LoadStackPreset(const std::string& presetName, std::string& error);
|
bool LoadStackPreset(const std::string& presetName, std::string& error);
|
||||||
@@ -39,7 +40,10 @@ public:
|
|||||||
void SetCompileStatus(bool succeeded, const std::string& message);
|
void SetCompileStatus(bool succeeded, const std::string& message);
|
||||||
void SetSignalStatus(bool hasSignal, unsigned width, unsigned height, const std::string& modeName);
|
void SetSignalStatus(bool hasSignal, unsigned width, unsigned height, const std::string& modeName);
|
||||||
bool TrySetSignalStatus(bool hasSignal, unsigned width, unsigned height, const std::string& modeName);
|
bool TrySetSignalStatus(bool hasSignal, unsigned width, unsigned height, const std::string& modeName);
|
||||||
void SetVideoIOStatus(const VideoIOState& state);
|
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);
|
void SetPerformanceStats(double frameBudgetMilliseconds, double renderMilliseconds);
|
||||||
bool TrySetPerformanceStats(double frameBudgetMilliseconds, double renderMilliseconds);
|
bool TrySetPerformanceStats(double frameBudgetMilliseconds, double renderMilliseconds);
|
||||||
void SetFramePacingStats(double completionIntervalMilliseconds, double smoothedCompletionIntervalMilliseconds,
|
void SetFramePacingStats(double completionIntervalMilliseconds, double smoothedCompletionIntervalMilliseconds,
|
||||||
@@ -52,9 +56,11 @@ public:
|
|||||||
bool BuildLayerPassFragmentShaderSources(const std::string& layerId, std::vector<ShaderPassBuildSource>& passSources, std::string& error);
|
bool BuildLayerPassFragmentShaderSources(const std::string& layerId, std::vector<ShaderPassBuildSource>& passSources, std::string& error);
|
||||||
std::vector<RuntimeRenderState> GetLayerRenderStates(unsigned outputWidth, unsigned outputHeight) const;
|
std::vector<RuntimeRenderState> GetLayerRenderStates(unsigned outputWidth, unsigned outputHeight) const;
|
||||||
bool TryGetLayerRenderStates(unsigned outputWidth, unsigned outputHeight, std::vector<RuntimeRenderState>& states) const;
|
bool TryGetLayerRenderStates(unsigned outputWidth, unsigned outputHeight, std::vector<RuntimeRenderState>& states) const;
|
||||||
|
bool TryRefreshCachedLayerStates(std::vector<RuntimeRenderState>& states) const;
|
||||||
void RefreshDynamicRenderStateFields(std::vector<RuntimeRenderState>& states) const;
|
void RefreshDynamicRenderStateFields(std::vector<RuntimeRenderState>& states) const;
|
||||||
std::string BuildStateJson() const;
|
std::string BuildStateJson() const;
|
||||||
uint64_t GetRenderStateVersion() const { return mRenderStateVersion.load(std::memory_order_relaxed); }
|
uint64_t GetRenderStateVersion() const { return mRenderStateVersion.load(std::memory_order_relaxed); }
|
||||||
|
uint64_t GetParameterStateVersion() const { return mParameterStateVersion.load(std::memory_order_relaxed); }
|
||||||
|
|
||||||
const std::filesystem::path& GetRepoRoot() const { return mRepoRoot; }
|
const std::filesystem::path& GetRepoRoot() const { return mRepoRoot; }
|
||||||
const std::filesystem::path& GetUiRoot() const { return mUiRoot; }
|
const std::filesystem::path& GetUiRoot() const { return mUiRoot; }
|
||||||
@@ -62,9 +68,15 @@ public:
|
|||||||
const std::filesystem::path& GetRuntimeRoot() const { return mRuntimeRoot; }
|
const std::filesystem::path& GetRuntimeRoot() const { return mRuntimeRoot; }
|
||||||
unsigned short GetServerPort() const { return mServerPort; }
|
unsigned short GetServerPort() const { return mServerPort; }
|
||||||
unsigned short GetOscPort() const { return mConfig.oscPort; }
|
unsigned short GetOscPort() const { return mConfig.oscPort; }
|
||||||
|
const std::string& GetOscBindAddress() const { return mConfig.oscBindAddress; }
|
||||||
|
double GetOscSmoothing() const { return mConfig.oscSmoothing; }
|
||||||
unsigned GetMaxTemporalHistoryFrames() const { return mConfig.maxTemporalHistoryFrames; }
|
unsigned GetMaxTemporalHistoryFrames() const { return mConfig.maxTemporalHistoryFrames; }
|
||||||
bool ExternalKeyingEnabled() const { return mConfig.videoIO.externalKeyingEnabled; }
|
unsigned GetPreviewFps() const { return mConfig.previewFps; }
|
||||||
VideoIOConfiguration GetVideoIOConfiguration() const { return mConfig.videoIO; }
|
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);
|
void SetServerPort(unsigned short port);
|
||||||
bool AutoReloadEnabled() const { return mAutoReloadEnabled; }
|
bool AutoReloadEnabled() const { return mAutoReloadEnabled; }
|
||||||
|
|
||||||
@@ -74,24 +86,28 @@ private:
|
|||||||
std::string shaderLibrary = "shaders";
|
std::string shaderLibrary = "shaders";
|
||||||
unsigned short serverPort = 8080;
|
unsigned short serverPort = 8080;
|
||||||
unsigned short oscPort = 9000;
|
unsigned short oscPort = 9000;
|
||||||
|
std::string oscBindAddress = "127.0.0.1";
|
||||||
|
double oscSmoothing = 0.18;
|
||||||
bool autoReload = true;
|
bool autoReload = true;
|
||||||
unsigned maxTemporalHistoryFrames = 4;
|
unsigned maxTemporalHistoryFrames = 4;
|
||||||
VideoIOConfiguration videoIO;
|
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 VideoIOStatusSnapshot
|
struct DeckLinkOutputStatus
|
||||||
{
|
{
|
||||||
VideoIOBackendId backendId = VideoIOBackendId::DeckLink;
|
std::string backendName = "decklink";
|
||||||
std::string deviceName;
|
std::string modelName;
|
||||||
VideoIOCapabilities capabilities;
|
bool supportsInternalKeying = false;
|
||||||
bool hasInputDevice = false;
|
bool supportsExternalKeying = false;
|
||||||
bool hasInputSource = false;
|
bool keyerInterfaceAvailable = false;
|
||||||
std::string inputDisplayModeName = "1080p59.94";
|
|
||||||
std::string outputDisplayModeName = "1080p59.94";
|
|
||||||
bool externalKeyingRequested = false;
|
bool externalKeyingRequested = false;
|
||||||
bool externalKeyingActive = false;
|
bool externalKeyingActive = false;
|
||||||
std::string statusMessage;
|
std::string statusMessage;
|
||||||
std::string formatStatusMessage;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
struct LayerPersistentState
|
struct LayerPersistentState
|
||||||
@@ -131,6 +147,7 @@ private:
|
|||||||
std::string GenerateLayerId();
|
std::string GenerateLayerId();
|
||||||
void SetSignalStatusLocked(bool hasSignal, unsigned width, unsigned height, const std::string& modeName);
|
void SetSignalStatusLocked(bool hasSignal, unsigned width, unsigned height, const std::string& modeName);
|
||||||
void MarkRenderStateDirtyLocked();
|
void MarkRenderStateDirtyLocked();
|
||||||
|
void MarkParameterStateDirtyLocked();
|
||||||
void SetPerformanceStatsLocked(double frameBudgetMilliseconds, double renderMilliseconds);
|
void SetPerformanceStatsLocked(double frameBudgetMilliseconds, double renderMilliseconds);
|
||||||
void SetFramePacingStatsLocked(double completionIntervalMilliseconds, double smoothedCompletionIntervalMilliseconds,
|
void SetFramePacingStatsLocked(double completionIntervalMilliseconds, double smoothedCompletionIntervalMilliseconds,
|
||||||
double maxCompletionIntervalMilliseconds, uint64_t lateFrameCount, uint64_t droppedFrameCount, uint64_t flushedFrameCount);
|
double maxCompletionIntervalMilliseconds, uint64_t lateFrameCount, uint64_t droppedFrameCount, uint64_t flushedFrameCount);
|
||||||
@@ -170,12 +187,13 @@ private:
|
|||||||
uint64_t mLateFrameCount;
|
uint64_t mLateFrameCount;
|
||||||
uint64_t mDroppedFrameCount;
|
uint64_t mDroppedFrameCount;
|
||||||
uint64_t mFlushedFrameCount;
|
uint64_t mFlushedFrameCount;
|
||||||
VideoIOStatusSnapshot mVideoIOStatus;
|
DeckLinkOutputStatus mDeckLinkOutputStatus;
|
||||||
unsigned short mServerPort;
|
unsigned short mServerPort;
|
||||||
bool mAutoReloadEnabled;
|
bool mAutoReloadEnabled;
|
||||||
std::chrono::steady_clock::time_point mStartTime;
|
std::chrono::steady_clock::time_point mStartTime;
|
||||||
std::chrono::steady_clock::time_point mLastScanTime;
|
std::chrono::steady_clock::time_point mLastScanTime;
|
||||||
std::atomic<uint64_t> mFrameCounter{ 0 };
|
std::atomic<uint64_t> mFrameCounter{ 0 };
|
||||||
std::atomic<uint64_t> mRenderStateVersion{ 0 };
|
std::atomic<uint64_t> mRenderStateVersion{ 0 };
|
||||||
|
std::atomic<uint64_t> mParameterStateVersion{ 0 };
|
||||||
uint64_t mNextLayerId;
|
uint64_t mNextLayerId;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -178,6 +178,11 @@ bool ShaderCompiler::BuildWrapperSlangSource(const ShaderPackage& shaderPackage,
|
|||||||
const unsigned historySamplerCount = shaderPackage.temporal.enabled ? mMaxTemporalHistoryFrames : 0;
|
const unsigned historySamplerCount = shaderPackage.temporal.enabled ? mMaxTemporalHistoryFrames : 0;
|
||||||
wrapperSource = ReplaceAll(wrapperSource, "{{SOURCE_HISTORY_SAMPLERS}}", BuildHistorySamplerDeclarations("gSourceHistory", historySamplerCount));
|
wrapperSource = ReplaceAll(wrapperSource, "{{SOURCE_HISTORY_SAMPLERS}}", BuildHistorySamplerDeclarations("gSourceHistory", historySamplerCount));
|
||||||
wrapperSource = ReplaceAll(wrapperSource, "{{TEMPORAL_HISTORY_SAMPLERS}}", BuildHistorySamplerDeclarations("gTemporalHistory", historySamplerCount));
|
wrapperSource = ReplaceAll(wrapperSource, "{{TEMPORAL_HISTORY_SAMPLERS}}", BuildHistorySamplerDeclarations("gTemporalHistory", historySamplerCount));
|
||||||
|
wrapperSource = ReplaceAll(wrapperSource, "{{FEEDBACK_SAMPLER}}", shaderPackage.feedback.enabled ? "Sampler2D<float4> gFeedbackState;\n" : "");
|
||||||
|
wrapperSource = ReplaceAll(wrapperSource, "{{FEEDBACK_HELPER}}",
|
||||||
|
shaderPackage.feedback.enabled
|
||||||
|
? "float4 sampleFeedback(float2 tc)\n{\n\tif (gFeedbackAvailable <= 0)\n\t\treturn float4(0.0, 0.0, 0.0, 0.0);\n\treturn gFeedbackState.Sample(tc);\n}\n"
|
||||||
|
: "float4 sampleFeedback(float2 tc)\n{\n\treturn float4(0.0, 0.0, 0.0, 0.0);\n}\n");
|
||||||
wrapperSource = ReplaceAll(wrapperSource, "{{TEXTURE_SAMPLERS}}", BuildTextureSamplerDeclarations(shaderPackage.textureAssets));
|
wrapperSource = ReplaceAll(wrapperSource, "{{TEXTURE_SAMPLERS}}", BuildTextureSamplerDeclarations(shaderPackage.textureAssets));
|
||||||
wrapperSource = ReplaceAll(wrapperSource, "{{TEXT_SAMPLERS}}", BuildTextSamplerDeclarations(shaderPackage.parameters));
|
wrapperSource = ReplaceAll(wrapperSource, "{{TEXT_SAMPLERS}}", BuildTextSamplerDeclarations(shaderPackage.parameters));
|
||||||
wrapperSource = ReplaceAll(wrapperSource, "{{TEXT_HELPERS}}", BuildTextHelpers(shaderPackage.parameters));
|
wrapperSource = ReplaceAll(wrapperSource, "{{TEXT_HELPERS}}", BuildTextHelpers(shaderPackage.parameters));
|
||||||
|
|||||||
@@ -473,6 +473,46 @@ bool ParseTemporalSettings(const JsonValue& manifestJson, ShaderPackage& shaderP
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool ParseFeedbackSettings(const JsonValue& manifestJson, ShaderPackage& shaderPackage, const std::filesystem::path& manifestPath, std::string& error)
|
||||||
|
{
|
||||||
|
const JsonValue* feedbackValue = nullptr;
|
||||||
|
if (!OptionalObjectField(manifestJson, "feedback", feedbackValue, manifestPath, error))
|
||||||
|
return false;
|
||||||
|
if (!feedbackValue)
|
||||||
|
return true;
|
||||||
|
|
||||||
|
const JsonValue* enabledValue = feedbackValue->find("enabled");
|
||||||
|
if (!enabledValue || !enabledValue->asBoolean(false))
|
||||||
|
return true;
|
||||||
|
|
||||||
|
shaderPackage.feedback.enabled = true;
|
||||||
|
if (!OptionalStringField(*feedbackValue, "writePass", shaderPackage.feedback.writePassId, "", manifestPath, error))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if (shaderPackage.feedback.writePassId.empty())
|
||||||
|
{
|
||||||
|
if (shaderPackage.passes.empty())
|
||||||
|
{
|
||||||
|
error = "Feedback-enabled shader has no passes to target in: " + ManifestPathMessage(manifestPath);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
shaderPackage.feedback.writePassId = shaderPackage.passes.back().id;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!ValidateShaderIdentifier(shaderPackage.feedback.writePassId, "feedback.writePass", manifestPath, error))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
const auto passIt = std::find_if(shaderPackage.passes.begin(), shaderPackage.passes.end(),
|
||||||
|
[&shaderPackage](const ShaderPassDefinition& pass) { return pass.id == shaderPackage.feedback.writePassId; });
|
||||||
|
if (passIt == shaderPackage.passes.end())
|
||||||
|
{
|
||||||
|
error = "Feedback writePass '" + shaderPackage.feedback.writePassId + "' does not match any declared pass in: " + ManifestPathMessage(manifestPath);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
bool ParseParameterNumberField(const JsonValue& parameterJson, const char* fieldName, std::vector<double>& values, const std::filesystem::path& manifestPath, std::string& error)
|
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))
|
if (const JsonValue* fieldValue = parameterJson.find(fieldName))
|
||||||
@@ -773,5 +813,6 @@ bool ShaderPackageRegistry::ParseManifest(const std::filesystem::path& manifestP
|
|||||||
return ParseTextureAssets(manifestJson, shaderPackage, manifestPath, error) &&
|
return ParseTextureAssets(manifestJson, shaderPackage, manifestPath, error) &&
|
||||||
ParseFontAssets(manifestJson, shaderPackage, manifestPath, error) &&
|
ParseFontAssets(manifestJson, shaderPackage, manifestPath, error) &&
|
||||||
ParseTemporalSettings(manifestJson, shaderPackage, mMaxTemporalHistoryFrames, manifestPath, error) &&
|
ParseTemporalSettings(manifestJson, shaderPackage, mMaxTemporalHistoryFrames, manifestPath, error) &&
|
||||||
|
ParseFeedbackSettings(manifestJson, shaderPackage, manifestPath, error) &&
|
||||||
ParseParameterDefinitions(manifestJson, shaderPackage, manifestPath, error);
|
ParseParameterDefinitions(manifestJson, shaderPackage, manifestPath, error);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -63,6 +63,12 @@ struct TemporalSettings
|
|||||||
unsigned effectiveHistoryLength = 0;
|
unsigned effectiveHistoryLength = 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
struct FeedbackSettings
|
||||||
|
{
|
||||||
|
bool enabled = false;
|
||||||
|
std::string writePassId;
|
||||||
|
};
|
||||||
|
|
||||||
struct ShaderTextureAsset
|
struct ShaderTextureAsset
|
||||||
{
|
{
|
||||||
std::string id;
|
std::string id;
|
||||||
@@ -110,6 +116,7 @@ struct ShaderPackage
|
|||||||
std::vector<ShaderTextureAsset> textureAssets;
|
std::vector<ShaderTextureAsset> textureAssets;
|
||||||
std::vector<ShaderFontAsset> fontAssets;
|
std::vector<ShaderFontAsset> fontAssets;
|
||||||
TemporalSettings temporal;
|
TemporalSettings temporal;
|
||||||
|
FeedbackSettings feedback;
|
||||||
std::filesystem::file_time_type shaderWriteTime;
|
std::filesystem::file_time_type shaderWriteTime;
|
||||||
std::filesystem::file_time_type manifestWriteTime;
|
std::filesystem::file_time_type manifestWriteTime;
|
||||||
};
|
};
|
||||||
@@ -128,6 +135,7 @@ struct RuntimeRenderState
|
|||||||
{
|
{
|
||||||
std::string layerId;
|
std::string layerId;
|
||||||
std::string shaderId;
|
std::string shaderId;
|
||||||
|
std::string shaderName;
|
||||||
std::vector<ShaderParameterDefinition> parameterDefinitions;
|
std::vector<ShaderParameterDefinition> parameterDefinitions;
|
||||||
std::map<std::string, ShaderParameterValue> parameterValues;
|
std::map<std::string, ShaderParameterValue> parameterValues;
|
||||||
std::vector<ShaderTextureAsset> textureAssets;
|
std::vector<ShaderTextureAsset> textureAssets;
|
||||||
@@ -147,4 +155,5 @@ struct RuntimeRenderState
|
|||||||
TemporalHistorySource temporalHistorySource = TemporalHistorySource::None;
|
TemporalHistorySource temporalHistorySource = TemporalHistorySource::None;
|
||||||
unsigned requestedTemporalHistoryLength = 0;
|
unsigned requestedTemporalHistoryLength = 0;
|
||||||
unsigned effectiveTemporalHistoryLength = 0;
|
unsigned effectiveTemporalHistoryLength = 0;
|
||||||
|
FeedbackSettings feedback;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,16 +0,0 @@
|
|||||||
#include "VideoIOBackendFactory.h"
|
|
||||||
|
|
||||||
#include "DeckLinkSession.h"
|
|
||||||
#include "VideoIOTypes.h"
|
|
||||||
|
|
||||||
std::unique_ptr<VideoIODevice> CreateVideoIODevice(VideoIOBackendId backendId, std::string& error)
|
|
||||||
{
|
|
||||||
switch (backendId)
|
|
||||||
{
|
|
||||||
case VideoIOBackendId::DeckLink:
|
|
||||||
return std::make_unique<DeckLinkSession>();
|
|
||||||
}
|
|
||||||
|
|
||||||
error = "Unsupported video I/O backend.";
|
|
||||||
return nullptr;
|
|
||||||
}
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
|
|
||||||
#include "VideoIOConfig.h"
|
|
||||||
|
|
||||||
#include <memory>
|
|
||||||
#include <string>
|
|
||||||
|
|
||||||
class VideoIODevice;
|
|
||||||
|
|
||||||
std::unique_ptr<VideoIODevice> CreateVideoIODevice(VideoIOBackendId backendId, std::string& error);
|
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
#include "VideoIOConfig.h"
|
|
||||||
|
|
||||||
#include <algorithm>
|
|
||||||
#include <cctype>
|
|
||||||
|
|
||||||
namespace
|
|
||||||
{
|
|
||||||
std::string NormalizeToken(std::string value)
|
|
||||||
{
|
|
||||||
std::transform(value.begin(), value.end(), value.begin(),
|
|
||||||
[](unsigned char ch) { return static_cast<char>(std::tolower(ch)); });
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const char* VideoIOBackendName(VideoIOBackendId backendId)
|
|
||||||
{
|
|
||||||
switch (backendId)
|
|
||||||
{
|
|
||||||
case VideoIOBackendId::DeckLink:
|
|
||||||
return "decklink";
|
|
||||||
}
|
|
||||||
return "unknown";
|
|
||||||
}
|
|
||||||
|
|
||||||
bool ParseVideoIOBackendId(const std::string& value, VideoIOBackendId& backendId)
|
|
||||||
{
|
|
||||||
const std::string normalized = NormalizeToken(value);
|
|
||||||
if (normalized.empty() || normalized == "decklink")
|
|
||||||
{
|
|
||||||
backendId = VideoIOBackendId::DeckLink;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
@@ -1,44 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
|
|
||||||
#include <string>
|
|
||||||
|
|
||||||
enum class VideoIOBackendId
|
|
||||||
{
|
|
||||||
DeckLink
|
|
||||||
};
|
|
||||||
|
|
||||||
const char* VideoIOBackendName(VideoIOBackendId backendId);
|
|
||||||
bool ParseVideoIOBackendId(const std::string& value, VideoIOBackendId& backendId);
|
|
||||||
|
|
||||||
struct FrameSize
|
|
||||||
{
|
|
||||||
unsigned width = 0;
|
|
||||||
unsigned height = 0;
|
|
||||||
|
|
||||||
bool IsEmpty() const { return width == 0 || height == 0; }
|
|
||||||
};
|
|
||||||
|
|
||||||
inline bool operator==(const FrameSize& left, const FrameSize& right)
|
|
||||||
{
|
|
||||||
return left.width == right.width && left.height == right.height;
|
|
||||||
}
|
|
||||||
|
|
||||||
inline bool operator!=(const FrameSize& left, const FrameSize& right)
|
|
||||||
{
|
|
||||||
return !(left == right);
|
|
||||||
}
|
|
||||||
|
|
||||||
struct VideoIOModeConfiguration
|
|
||||||
{
|
|
||||||
std::string videoFormat = "1080p";
|
|
||||||
std::string frameRate = "59.94";
|
|
||||||
};
|
|
||||||
|
|
||||||
struct VideoIOConfiguration
|
|
||||||
{
|
|
||||||
VideoIOBackendId backendId = VideoIOBackendId::DeckLink;
|
|
||||||
VideoIOModeConfiguration inputMode;
|
|
||||||
VideoIOModeConfiguration outputMode;
|
|
||||||
bool externalKeyingEnabled = false;
|
|
||||||
bool preferTenBit = true;
|
|
||||||
};
|
|
||||||
@@ -1,17 +1,15 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include "VideoIOConfig.h"
|
#include "DeckLinkDisplayMode.h"
|
||||||
#include "VideoIOFormat.h"
|
#include "VideoIOFormat.h"
|
||||||
|
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
#include <functional>
|
#include <functional>
|
||||||
#include <string>
|
#include <string>
|
||||||
|
|
||||||
struct VideoIOCapabilities
|
enum class VideoIOBackend
|
||||||
{
|
{
|
||||||
bool supportsInternalKeying = false;
|
DeckLink
|
||||||
bool supportsExternalKeying = false;
|
|
||||||
bool keyerInterfaceAvailable = false;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
enum class VideoIOCompletionResult
|
enum class VideoIOCompletionResult
|
||||||
@@ -23,9 +21,15 @@ enum class VideoIOCompletionResult
|
|||||||
Unknown
|
Unknown
|
||||||
};
|
};
|
||||||
|
|
||||||
|
struct VideoIOConfig
|
||||||
|
{
|
||||||
|
VideoFormatSelection videoModes;
|
||||||
|
bool externalKeyingEnabled = false;
|
||||||
|
bool preferTenBit = true;
|
||||||
|
};
|
||||||
|
|
||||||
struct VideoIOState
|
struct VideoIOState
|
||||||
{
|
{
|
||||||
VideoIOBackendId backendId = VideoIOBackendId::DeckLink;
|
|
||||||
FrameSize inputFrameSize;
|
FrameSize inputFrameSize;
|
||||||
FrameSize outputFrameSize;
|
FrameSize outputFrameSize;
|
||||||
VideoIOPixelFormat inputPixelFormat = VideoIOPixelFormat::Uyvy8;
|
VideoIOPixelFormat inputPixelFormat = VideoIOPixelFormat::Uyvy8;
|
||||||
@@ -36,13 +40,14 @@ struct VideoIOState
|
|||||||
unsigned outputPackTextureWidth = 0;
|
unsigned outputPackTextureWidth = 0;
|
||||||
std::string inputDisplayModeName = "1080p59.94";
|
std::string inputDisplayModeName = "1080p59.94";
|
||||||
std::string outputDisplayModeName = "1080p59.94";
|
std::string outputDisplayModeName = "1080p59.94";
|
||||||
std::string deviceName;
|
std::string outputModelName;
|
||||||
std::string statusMessage;
|
std::string statusMessage;
|
||||||
std::string formatStatusMessage;
|
std::string formatStatusMessage;
|
||||||
bool hasInputDevice = false;
|
bool hasInputDevice = false;
|
||||||
bool hasInputSource = false;
|
bool hasInputSource = false;
|
||||||
VideoIOCapabilities capabilities;
|
bool supportsInternalKeying = false;
|
||||||
bool externalKeyingRequested = false;
|
bool supportsExternalKeying = false;
|
||||||
|
bool keyerInterfaceAvailable = false;
|
||||||
bool externalKeyingActive = false;
|
bool externalKeyingActive = false;
|
||||||
double frameBudgetMilliseconds = 0.0;
|
double frameBudgetMilliseconds = 0.0;
|
||||||
};
|
};
|
||||||
@@ -88,12 +93,11 @@ public:
|
|||||||
using OutputFrameCallback = std::function<void(const VideoIOCompletion&)>;
|
using OutputFrameCallback = std::function<void(const VideoIOCompletion&)>;
|
||||||
|
|
||||||
virtual ~VideoIODevice() = default;
|
virtual ~VideoIODevice() = default;
|
||||||
virtual VideoIOBackendId BackendId() const = 0;
|
|
||||||
virtual void ReleaseResources() = 0;
|
virtual void ReleaseResources() = 0;
|
||||||
virtual bool DiscoverDevicesAndModes(const VideoIOConfiguration& config, std::string& error) = 0;
|
virtual bool DiscoverDevicesAndModes(const VideoFormatSelection& videoModes, std::string& error) = 0;
|
||||||
virtual bool SelectPreferredFormats(const VideoIOConfiguration& config, std::string& error) = 0;
|
virtual bool SelectPreferredFormats(const VideoFormatSelection& videoModes, bool outputAlphaRequired, std::string& error) = 0;
|
||||||
virtual bool ConfigureInput(InputFrameCallback callback, std::string& error) = 0;
|
virtual bool ConfigureInput(InputFrameCallback callback, const VideoFormat& inputVideoMode, std::string& error) = 0;
|
||||||
virtual bool ConfigureOutput(OutputFrameCallback callback, std::string& error) = 0;
|
virtual bool ConfigureOutput(OutputFrameCallback callback, const VideoFormat& outputVideoMode, bool externalKeyingEnabled, std::string& error) = 0;
|
||||||
virtual bool Start() = 0;
|
virtual bool Start() = 0;
|
||||||
virtual bool Stop() = 0;
|
virtual bool Stop() = 0;
|
||||||
virtual const VideoIOState& State() const = 0;
|
virtual const VideoIOState& State() const = 0;
|
||||||
@@ -122,11 +126,10 @@ public:
|
|||||||
unsigned OutputPackTextureWidth() const { return State().outputPackTextureWidth; }
|
unsigned OutputPackTextureWidth() const { return State().outputPackTextureWidth; }
|
||||||
const std::string& FormatStatusMessage() const { return State().formatStatusMessage; }
|
const std::string& FormatStatusMessage() const { return State().formatStatusMessage; }
|
||||||
const std::string& InputDisplayModeName() const { return State().inputDisplayModeName; }
|
const std::string& InputDisplayModeName() const { return State().inputDisplayModeName; }
|
||||||
const std::string& DeviceName() const { return State().deviceName; }
|
const std::string& OutputModelName() const { return State().outputModelName; }
|
||||||
bool SupportsInternalKeying() const { return State().capabilities.supportsInternalKeying; }
|
bool SupportsInternalKeying() const { return State().supportsInternalKeying; }
|
||||||
bool SupportsExternalKeying() const { return State().capabilities.supportsExternalKeying; }
|
bool SupportsExternalKeying() const { return State().supportsExternalKeying; }
|
||||||
bool KeyerInterfaceAvailable() const { return State().capabilities.keyerInterfaceAvailable; }
|
bool KeyerInterfaceAvailable() const { return State().keyerInterfaceAvailable; }
|
||||||
bool ExternalKeyingRequested() const { return State().externalKeyingRequested; }
|
|
||||||
bool ExternalKeyingActive() const { return State().externalKeyingActive; }
|
bool ExternalKeyingActive() const { return State().externalKeyingActive; }
|
||||||
const std::string& StatusMessage() const { return State().statusMessage; }
|
const std::string& StatusMessage() const { return State().statusMessage; }
|
||||||
double FrameBudgetMilliseconds() const { return State().frameBudgetMilliseconds; }
|
double FrameBudgetMilliseconds() const { return State().frameBudgetMilliseconds; }
|
||||||
|
|||||||
@@ -13,10 +13,10 @@ std::string NormalizeModeToken(const std::string& value)
|
|||||||
return normalized;
|
return normalized;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool ResolveConfiguredDeckLinkDisplayMode(const VideoIOModeConfiguration& mode, BMDDisplayMode& displayMode, std::string& displayModeName)
|
bool ResolveConfiguredDisplayMode(const std::string& videoFormat, const std::string& frameRate, BMDDisplayMode& displayMode, std::string& displayModeName)
|
||||||
{
|
{
|
||||||
DeckLinkVideoMode videoMode;
|
VideoFormat videoMode;
|
||||||
if (!ResolveConfiguredDeckLinkVideoMode(mode, videoMode))
|
if (!ResolveConfiguredVideoFormat(videoFormat, frameRate, videoMode))
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
displayMode = videoMode.displayMode;
|
displayMode = videoMode.displayMode;
|
||||||
@@ -24,10 +24,10 @@ bool ResolveConfiguredDeckLinkDisplayMode(const VideoIOModeConfiguration& mode,
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool ResolveConfiguredDeckLinkVideoMode(const VideoIOModeConfiguration& mode, DeckLinkVideoMode& videoMode)
|
bool ResolveConfiguredVideoFormat(const std::string& videoFormat, const std::string& frameRate, VideoFormat& videoMode)
|
||||||
{
|
{
|
||||||
const std::string formatToken = NormalizeModeToken(mode.videoFormat);
|
const std::string formatToken = NormalizeModeToken(videoFormat);
|
||||||
const std::string frameToken = NormalizeModeToken(mode.frameRate);
|
const std::string frameToken = NormalizeModeToken(frameRate);
|
||||||
const std::string combinedToken = formatToken + frameToken;
|
const std::string combinedToken = formatToken + frameToken;
|
||||||
|
|
||||||
struct ModeOption
|
struct ModeOption
|
||||||
@@ -98,22 +98,25 @@ bool ResolveConfiguredDeckLinkVideoMode(const VideoIOModeConfiguration& mode, De
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool ResolveConfiguredDeckLinkVideoModes(
|
bool ResolveConfiguredVideoFormats(
|
||||||
const VideoIOConfiguration& config,
|
const std::string& inputVideoFormat,
|
||||||
DeckLinkVideoModeSelection& videoModes,
|
const std::string& inputFrameRate,
|
||||||
|
const std::string& outputVideoFormat,
|
||||||
|
const std::string& outputFrameRate,
|
||||||
|
VideoFormatSelection& videoModes,
|
||||||
std::string& error)
|
std::string& error)
|
||||||
{
|
{
|
||||||
if (!ResolveConfiguredDeckLinkVideoMode(config.inputMode, videoModes.input))
|
if (!ResolveConfiguredVideoFormat(inputVideoFormat, inputFrameRate, videoModes.input))
|
||||||
{
|
{
|
||||||
error = "Unsupported DeckLink input mode in config/runtime-host.json: " +
|
error = "Unsupported DeckLink inputVideoFormat/inputFrameRate in config/runtime-host.json: " +
|
||||||
config.inputMode.videoFormat + " / " + config.inputMode.frameRate;
|
inputVideoFormat + " / " + inputFrameRate;
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!ResolveConfiguredDeckLinkVideoMode(config.outputMode, videoModes.output))
|
if (!ResolveConfiguredVideoFormat(outputVideoFormat, outputFrameRate, videoModes.output))
|
||||||
{
|
{
|
||||||
error = "Unsupported DeckLink output mode in config/runtime-host.json: " +
|
error = "Unsupported DeckLink outputVideoFormat/outputFrameRate in config/runtime-host.json: " +
|
||||||
config.outputMode.videoFormat + " / " + config.outputMode.frameRate;
|
outputVideoFormat + " / " + outputFrameRate;
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,27 +1,47 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include "DeckLinkAPI_h.h"
|
#include "DeckLinkAPI_h.h"
|
||||||
#include "VideoIOConfig.h"
|
|
||||||
|
|
||||||
#include <string>
|
#include <string>
|
||||||
|
|
||||||
struct DeckLinkVideoMode
|
struct FrameSize
|
||||||
|
{
|
||||||
|
unsigned width = 0;
|
||||||
|
unsigned height = 0;
|
||||||
|
|
||||||
|
bool IsEmpty() const { return width == 0 || height == 0; }
|
||||||
|
};
|
||||||
|
|
||||||
|
inline bool operator==(const FrameSize& left, const FrameSize& right)
|
||||||
|
{
|
||||||
|
return left.width == right.width && left.height == right.height;
|
||||||
|
}
|
||||||
|
|
||||||
|
inline bool operator!=(const FrameSize& left, const FrameSize& right)
|
||||||
|
{
|
||||||
|
return !(left == right);
|
||||||
|
}
|
||||||
|
|
||||||
|
struct VideoFormat
|
||||||
{
|
{
|
||||||
BMDDisplayMode displayMode = bmdModeHD1080p5994;
|
BMDDisplayMode displayMode = bmdModeHD1080p5994;
|
||||||
std::string displayName = "1080p59.94";
|
std::string displayName = "1080p59.94";
|
||||||
};
|
};
|
||||||
|
|
||||||
struct DeckLinkVideoModeSelection
|
struct VideoFormatSelection
|
||||||
{
|
{
|
||||||
DeckLinkVideoMode input;
|
VideoFormat input;
|
||||||
DeckLinkVideoMode output;
|
VideoFormat output;
|
||||||
};
|
};
|
||||||
|
|
||||||
std::string NormalizeModeToken(const std::string& value);
|
std::string NormalizeModeToken(const std::string& value);
|
||||||
bool ResolveConfiguredDeckLinkDisplayMode(const VideoIOModeConfiguration& mode, BMDDisplayMode& displayMode, std::string& displayModeName);
|
bool ResolveConfiguredDisplayMode(const std::string& videoFormat, const std::string& frameRate, BMDDisplayMode& displayMode, std::string& displayModeName);
|
||||||
bool ResolveConfiguredDeckLinkVideoMode(const VideoIOModeConfiguration& mode, DeckLinkVideoMode& videoMode);
|
bool ResolveConfiguredVideoFormat(const std::string& videoFormat, const std::string& frameRate, VideoFormat& videoMode);
|
||||||
bool ResolveConfiguredDeckLinkVideoModes(
|
bool ResolveConfiguredVideoFormats(
|
||||||
const VideoIOConfiguration& config,
|
const std::string& inputVideoFormat,
|
||||||
DeckLinkVideoModeSelection& videoModes,
|
const std::string& inputFrameRate,
|
||||||
|
const std::string& outputVideoFormat,
|
||||||
|
const std::string& outputFrameRate,
|
||||||
|
VideoFormatSelection& videoModes,
|
||||||
std::string& error);
|
std::string& error);
|
||||||
bool FindDeckLinkDisplayMode(IDeckLinkDisplayModeIterator* iterator, BMDDisplayMode targetMode, IDeckLinkDisplayMode** foundMode);
|
bool FindDeckLinkDisplayMode(IDeckLinkDisplayModeIterator* iterator, BMDDisplayMode targetMode, IDeckLinkDisplayMode** foundMode);
|
||||||
|
|||||||
@@ -92,19 +92,14 @@ void DeckLinkSession::ReleaseResources()
|
|||||||
output.Release();
|
output.Release();
|
||||||
}
|
}
|
||||||
|
|
||||||
bool DeckLinkSession::DiscoverDevicesAndModes(const VideoIOConfiguration& config, std::string& error)
|
bool DeckLinkSession::DiscoverDevicesAndModes(const VideoFormatSelection& videoModes, std::string& error)
|
||||||
{
|
{
|
||||||
CComPtr<IDeckLinkIterator> deckLinkIterator;
|
CComPtr<IDeckLinkIterator> deckLinkIterator;
|
||||||
CComPtr<IDeckLinkDisplayMode> inputMode;
|
CComPtr<IDeckLinkDisplayMode> inputMode;
|
||||||
CComPtr<IDeckLinkDisplayMode> outputMode;
|
CComPtr<IDeckLinkDisplayMode> outputMode;
|
||||||
|
|
||||||
mState.backendId = BackendId();
|
mState.inputDisplayModeName = videoModes.input.displayName;
|
||||||
mState.externalKeyingRequested = config.externalKeyingEnabled;
|
mState.outputDisplayModeName = videoModes.output.displayName;
|
||||||
if (!ResolveConfiguredDeckLinkVideoModes(config, mConfiguredModes, error))
|
|
||||||
return false;
|
|
||||||
|
|
||||||
mState.inputDisplayModeName = mConfiguredModes.input.displayName;
|
|
||||||
mState.outputDisplayModeName = mConfiguredModes.output.displayName;
|
|
||||||
|
|
||||||
HRESULT result = CoCreateInstance(CLSID_CDeckLinkIterator, nullptr, CLSCTX_ALL, IID_IDeckLinkIterator, reinterpret_cast<void**>(&deckLinkIterator));
|
HRESULT result = CoCreateInstance(CLSID_CDeckLinkIterator, nullptr, CLSCTX_ALL, IID_IDeckLinkIterator, reinterpret_cast<void**>(&deckLinkIterator));
|
||||||
if (FAILED(result))
|
if (FAILED(result))
|
||||||
@@ -156,9 +151,9 @@ bool DeckLinkSession::DiscoverDevicesAndModes(const VideoIOConfiguration& config
|
|||||||
output.Release();
|
output.Release();
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
mState.deviceName = modelName;
|
mState.outputModelName = modelName;
|
||||||
mState.capabilities.supportsInternalKeying = deviceSupportsInternalKeying;
|
mState.supportsInternalKeying = deviceSupportsInternalKeying;
|
||||||
mState.capabilities.supportsExternalKeying = deviceSupportsExternalKeying;
|
mState.supportsExternalKeying = deviceSupportsExternalKeying;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -183,9 +178,9 @@ bool DeckLinkSession::DiscoverDevicesAndModes(const VideoIOConfiguration& config
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (input && !FindDeckLinkDisplayMode(inputDisplayModeIterator, mConfiguredModes.input.displayMode, &inputMode))
|
if (input && !FindDeckLinkDisplayMode(inputDisplayModeIterator, videoModes.input.displayMode, &inputMode))
|
||||||
{
|
{
|
||||||
error = "Cannot get specified input BMDDisplayMode for configured mode: " + mConfiguredModes.input.displayName;
|
error = "Cannot get specified input BMDDisplayMode for configured mode: " + videoModes.input.displayName;
|
||||||
ReleaseResources();
|
ReleaseResources();
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -199,9 +194,9 @@ bool DeckLinkSession::DiscoverDevicesAndModes(const VideoIOConfiguration& config
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!FindDeckLinkDisplayMode(outputDisplayModeIterator, mConfiguredModes.output.displayMode, &outputMode))
|
if (!FindDeckLinkDisplayMode(outputDisplayModeIterator, videoModes.output.displayMode, &outputMode))
|
||||||
{
|
{
|
||||||
error = "Cannot get specified output BMDDisplayMode for configured mode: " + mConfiguredModes.output.displayName;
|
error = "Cannot get specified output BMDDisplayMode for configured mode: " + videoModes.output.displayName;
|
||||||
ReleaseResources();
|
ReleaseResources();
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -228,7 +223,7 @@ bool DeckLinkSession::DiscoverDevicesAndModes(const VideoIOConfiguration& config
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool DeckLinkSession::SelectPreferredFormats(const VideoIOConfiguration& config, std::string& error)
|
bool DeckLinkSession::SelectPreferredFormats(const VideoFormatSelection& videoModes, bool outputAlphaRequired, std::string& error)
|
||||||
{
|
{
|
||||||
if (!output)
|
if (!output)
|
||||||
{
|
{
|
||||||
@@ -238,19 +233,19 @@ bool DeckLinkSession::SelectPreferredFormats(const VideoIOConfiguration& config,
|
|||||||
|
|
||||||
mState.formatStatusMessage.clear();
|
mState.formatStatusMessage.clear();
|
||||||
|
|
||||||
const bool inputTenBitSupported = input != nullptr && InputSupportsFormat(input, mConfiguredModes.input.displayMode, bmdFormat10BitYUV);
|
const bool inputTenBitSupported = input != nullptr && InputSupportsFormat(input, videoModes.input.displayMode, bmdFormat10BitYUV);
|
||||||
mState.inputPixelFormat = input != nullptr ? ChoosePreferredVideoIOFormat(inputTenBitSupported) : VideoIOPixelFormat::Uyvy8;
|
mState.inputPixelFormat = input != nullptr ? ChoosePreferredVideoIOFormat(inputTenBitSupported) : VideoIOPixelFormat::Uyvy8;
|
||||||
if (input != nullptr && !inputTenBitSupported)
|
if (input != nullptr && !inputTenBitSupported)
|
||||||
mState.formatStatusMessage += "DeckLink input does not report 10-bit YUV support for the configured mode; using 8-bit capture. ";
|
mState.formatStatusMessage += "DeckLink input does not report 10-bit YUV support for the configured mode; using 8-bit capture. ";
|
||||||
|
|
||||||
const bool outputTenBitSupported = OutputSupportsFormat(output, mConfiguredModes.output.displayMode, bmdFormat10BitYUV);
|
const bool outputTenBitSupported = OutputSupportsFormat(output, videoModes.output.displayMode, bmdFormat10BitYUV);
|
||||||
const bool outputTenBitYuvaSupported = OutputSupportsFormat(output, mConfiguredModes.output.displayMode, bmdFormat10BitYUVA);
|
const bool outputTenBitYuvaSupported = OutputSupportsFormat(output, videoModes.output.displayMode, bmdFormat10BitYUVA);
|
||||||
mState.outputPixelFormat = config.externalKeyingEnabled
|
mState.outputPixelFormat = outputAlphaRequired
|
||||||
? (outputTenBitYuvaSupported ? VideoIOPixelFormat::Yuva10 : VideoIOPixelFormat::Bgra8)
|
? (outputTenBitYuvaSupported ? VideoIOPixelFormat::Yuva10 : VideoIOPixelFormat::Bgra8)
|
||||||
: (outputTenBitSupported ? VideoIOPixelFormat::V210 : VideoIOPixelFormat::Bgra8);
|
: (outputTenBitSupported ? VideoIOPixelFormat::V210 : VideoIOPixelFormat::Bgra8);
|
||||||
if (config.externalKeyingEnabled && outputTenBitYuvaSupported)
|
if (outputAlphaRequired && outputTenBitYuvaSupported)
|
||||||
mState.formatStatusMessage += "External keying requires alpha; using 10-bit YUVA output. ";
|
mState.formatStatusMessage += "External keying requires alpha; using 10-bit YUVA output. ";
|
||||||
else if (config.externalKeyingEnabled)
|
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. ";
|
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)
|
else if (!outputTenBitSupported)
|
||||||
mState.formatStatusMessage += "DeckLink output does not report 10-bit YUV support for the configured mode; using 8-bit BGRA output. ";
|
mState.formatStatusMessage += "DeckLink output does not report 10-bit YUV support for the configured mode; using 8-bit BGRA output. ";
|
||||||
@@ -291,7 +286,7 @@ bool DeckLinkSession::SelectPreferredFormats(const VideoIOConfiguration& config,
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool DeckLinkSession::ConfigureInput(InputFrameCallback callback, std::string& error)
|
bool DeckLinkSession::ConfigureInput(InputFrameCallback callback, const VideoFormat& inputVideoMode, std::string& error)
|
||||||
{
|
{
|
||||||
mInputFrameCallback = std::move(callback);
|
mInputFrameCallback = std::move(callback);
|
||||||
|
|
||||||
@@ -303,7 +298,7 @@ bool DeckLinkSession::ConfigureInput(InputFrameCallback callback, std::string& e
|
|||||||
}
|
}
|
||||||
|
|
||||||
const BMDPixelFormat deckLinkInputPixelFormat = DeckLinkPixelFormatForVideoIO(mState.inputPixelFormat);
|
const BMDPixelFormat deckLinkInputPixelFormat = DeckLinkPixelFormatForVideoIO(mState.inputPixelFormat);
|
||||||
if (input->EnableVideoInput(mConfiguredModes.input.displayMode, deckLinkInputPixelFormat, bmdVideoInputFlagDefault) != S_OK)
|
if (input->EnableVideoInput(inputVideoMode.displayMode, deckLinkInputPixelFormat, bmdVideoInputFlagDefault) != S_OK)
|
||||||
{
|
{
|
||||||
if (mState.inputPixelFormat == VideoIOPixelFormat::V210)
|
if (mState.inputPixelFormat == VideoIOPixelFormat::V210)
|
||||||
{
|
{
|
||||||
@@ -311,7 +306,7 @@ bool DeckLinkSession::ConfigureInput(InputFrameCallback callback, std::string& e
|
|||||||
mState.inputPixelFormat = VideoIOPixelFormat::Uyvy8;
|
mState.inputPixelFormat = VideoIOPixelFormat::Uyvy8;
|
||||||
mState.inputFrameRowBytes = mState.inputFrameSize.width * 2u;
|
mState.inputFrameRowBytes = mState.inputFrameSize.width * 2u;
|
||||||
mState.captureTextureWidth = mState.inputFrameSize.width / 2u;
|
mState.captureTextureWidth = mState.inputFrameSize.width / 2u;
|
||||||
if (input->EnableVideoInput(mConfiguredModes.input.displayMode, bmdFormat8BitYUV, bmdVideoInputFlagDefault) == S_OK)
|
if (input->EnableVideoInput(inputVideoMode.displayMode, bmdFormat8BitYUV, bmdVideoInputFlagDefault) == S_OK)
|
||||||
{
|
{
|
||||||
std::ostringstream status;
|
std::ostringstream status;
|
||||||
status << "DeckLink formats: capture " << VideoIOPixelFormatName(mState.inputPixelFormat)
|
status << "DeckLink formats: capture " << VideoIOPixelFormatName(mState.inputPixelFormat)
|
||||||
@@ -346,26 +341,26 @@ input_enabled:
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool DeckLinkSession::ConfigureOutput(OutputFrameCallback callback, std::string& error)
|
bool DeckLinkSession::ConfigureOutput(OutputFrameCallback callback, const VideoFormat& outputVideoMode, bool externalKeyingEnabled, std::string& error)
|
||||||
{
|
{
|
||||||
mOutputFrameCallback = std::move(callback);
|
mOutputFrameCallback = std::move(callback);
|
||||||
|
|
||||||
if (output->EnableVideoOutput(mConfiguredModes.output.displayMode, bmdVideoOutputFlagDefault) != S_OK)
|
if (output->EnableVideoOutput(outputVideoMode.displayMode, bmdVideoOutputFlagDefault) != S_OK)
|
||||||
{
|
{
|
||||||
error = "DeckLink output setup failed while enabling video output.";
|
error = "DeckLink output setup failed while enabling video output.";
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (output->QueryInterface(IID_IDeckLinkKeyer, (void**)&keyer) == S_OK && keyer != NULL)
|
if (output->QueryInterface(IID_IDeckLinkKeyer, (void**)&keyer) == S_OK && keyer != NULL)
|
||||||
mState.capabilities.keyerInterfaceAvailable = true;
|
mState.keyerInterfaceAvailable = true;
|
||||||
|
|
||||||
if (mState.externalKeyingRequested)
|
if (externalKeyingEnabled)
|
||||||
{
|
{
|
||||||
if (!mState.capabilities.supportsExternalKeying)
|
if (!mState.supportsExternalKeying)
|
||||||
{
|
{
|
||||||
mState.statusMessage = "External keying was requested, but the selected DeckLink output does not report external keying support.";
|
mState.statusMessage = "External keying was requested, but the selected DeckLink output does not report external keying support.";
|
||||||
}
|
}
|
||||||
else if (!mState.capabilities.keyerInterfaceAvailable)
|
else if (!mState.keyerInterfaceAvailable)
|
||||||
{
|
{
|
||||||
mState.statusMessage = "External keying was requested, but the selected DeckLink output does not expose the IDeckLinkKeyer interface.";
|
mState.statusMessage = "External keying was requested, but the selected DeckLink output does not expose the IDeckLinkKeyer interface.";
|
||||||
}
|
}
|
||||||
@@ -379,7 +374,7 @@ bool DeckLinkSession::ConfigureOutput(OutputFrameCallback callback, std::string&
|
|||||||
mState.statusMessage = "External keying is active on the selected DeckLink output.";
|
mState.statusMessage = "External keying is active on the selected DeckLink output.";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if (mState.capabilities.supportsExternalKeying)
|
else if (mState.supportsExternalKeying)
|
||||||
{
|
{
|
||||||
mState.statusMessage = "Selected DeckLink output supports external keying. Set enableExternalKeying to true in runtime-host.json to request it.";
|
mState.statusMessage = "Selected DeckLink output supports external keying. Set enableExternalKeying to true in runtime-host.json to request it.";
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,14 +20,41 @@ public:
|
|||||||
DeckLinkSession() = default;
|
DeckLinkSession() = default;
|
||||||
~DeckLinkSession();
|
~DeckLinkSession();
|
||||||
|
|
||||||
VideoIOBackendId BackendId() const override { return VideoIOBackendId::DeckLink; }
|
|
||||||
void ReleaseResources() override;
|
void ReleaseResources() override;
|
||||||
bool DiscoverDevicesAndModes(const VideoIOConfiguration& config, std::string& error) override;
|
bool DiscoverDevicesAndModes(const VideoFormatSelection& videoModes, std::string& error) override;
|
||||||
bool SelectPreferredFormats(const VideoIOConfiguration& config, std::string& error) override;
|
bool SelectPreferredFormats(const VideoFormatSelection& videoModes, bool outputAlphaRequired, std::string& error) override;
|
||||||
bool ConfigureInput(InputFrameCallback callback, std::string& error) override;
|
bool ConfigureInput(InputFrameCallback callback, const VideoFormat& inputVideoMode, std::string& error) override;
|
||||||
bool ConfigureOutput(OutputFrameCallback callback, std::string& error) override;
|
bool ConfigureOutput(OutputFrameCallback callback, const VideoFormat& outputVideoMode, bool externalKeyingEnabled, std::string& error) override;
|
||||||
bool Start() override;
|
bool Start() override;
|
||||||
bool Stop() override;
|
bool Stop() override;
|
||||||
|
|
||||||
|
bool HasInputDevice() const { return mState.hasInputDevice; }
|
||||||
|
bool HasInputSource() const { return mState.hasInputSource; }
|
||||||
|
void SetInputSourceMissing(bool missing) { mState.hasInputSource = !missing; }
|
||||||
|
bool InputOutputDimensionsDiffer() const { return mState.inputFrameSize != mState.outputFrameSize; }
|
||||||
|
const FrameSize& InputFrameSize() const { return mState.inputFrameSize; }
|
||||||
|
const FrameSize& OutputFrameSize() const { return mState.outputFrameSize; }
|
||||||
|
unsigned InputFrameWidth() const { return mState.inputFrameSize.width; }
|
||||||
|
unsigned InputFrameHeight() const { return mState.inputFrameSize.height; }
|
||||||
|
unsigned OutputFrameWidth() const { return mState.outputFrameSize.width; }
|
||||||
|
unsigned OutputFrameHeight() const { return mState.outputFrameSize.height; }
|
||||||
|
VideoIOPixelFormat InputPixelFormat() const { return mState.inputPixelFormat; }
|
||||||
|
VideoIOPixelFormat OutputPixelFormat() const { return mState.outputPixelFormat; }
|
||||||
|
bool InputIsTenBit() const { return VideoIOPixelFormatIsTenBit(mState.inputPixelFormat); }
|
||||||
|
bool OutputIsTenBit() const { return VideoIOPixelFormatIsTenBit(mState.outputPixelFormat); }
|
||||||
|
unsigned InputFrameRowBytes() const { return mState.inputFrameRowBytes; }
|
||||||
|
unsigned OutputFrameRowBytes() const { return mState.outputFrameRowBytes; }
|
||||||
|
unsigned CaptureTextureWidth() const { return mState.captureTextureWidth; }
|
||||||
|
unsigned OutputPackTextureWidth() const { return mState.outputPackTextureWidth; }
|
||||||
|
const std::string& FormatStatusMessage() const { return mState.formatStatusMessage; }
|
||||||
|
const std::string& InputDisplayModeName() const { return mState.inputDisplayModeName; }
|
||||||
|
const std::string& OutputModelName() const { return mState.outputModelName; }
|
||||||
|
bool SupportsInternalKeying() const { return mState.supportsInternalKeying; }
|
||||||
|
bool SupportsExternalKeying() const { return mState.supportsExternalKeying; }
|
||||||
|
bool KeyerInterfaceAvailable() const { return mState.keyerInterfaceAvailable; }
|
||||||
|
bool ExternalKeyingActive() const { return mState.externalKeyingActive; }
|
||||||
|
const std::string& StatusMessage() const { return mState.statusMessage; }
|
||||||
|
void SetStatusMessage(const std::string& message) { mState.statusMessage = message; }
|
||||||
const VideoIOState& State() const override { return mState; }
|
const VideoIOState& State() const override { return mState; }
|
||||||
VideoIOState& MutableState() override { return mState; }
|
VideoIOState& MutableState() override { return mState; }
|
||||||
double FrameBudgetMilliseconds() const;
|
double FrameBudgetMilliseconds() const;
|
||||||
@@ -49,5 +76,4 @@ private:
|
|||||||
VideoPlayoutScheduler mScheduler;
|
VideoPlayoutScheduler mScheduler;
|
||||||
InputFrameCallback mInputFrameCallback;
|
InputFrameCallback mInputFrameCallback;
|
||||||
OutputFrameCallback mOutputFrameCallback;
|
OutputFrameCallback mOutputFrameCallback;
|
||||||
DeckLinkVideoModeSelection mConfiguredModes;
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,13 +1,15 @@
|
|||||||
{
|
{
|
||||||
"shaderLibrary": "shaders",
|
"shaderLibrary": "shaders",
|
||||||
"serverPort": 8080,
|
"serverPort": 8080,
|
||||||
|
"oscBindAddress": "0.0.0.0",
|
||||||
"oscPort": 9000,
|
"oscPort": 9000,
|
||||||
"videoBackend": "decklink",
|
"oscSmoothing": 0.18,
|
||||||
"inputVideoFormat": "1080p",
|
"inputVideoFormat": "1080p",
|
||||||
"inputFrameRate": "59.94",
|
"inputFrameRate": "59.94",
|
||||||
"outputVideoFormat": "1080p",
|
"outputVideoFormat": "1080p",
|
||||||
"outputFrameRate": "59.94",
|
"outputFrameRate": "59.94",
|
||||||
"autoReload": true,
|
"autoReload": true,
|
||||||
"maxTemporalHistoryFrames": 12,
|
"maxTemporalHistoryFrames": 12,
|
||||||
|
"previewFps": 30,
|
||||||
"enableExternalKeying": true
|
"enableExternalKeying": true
|
||||||
}
|
}
|
||||||
|
|||||||
638
docs/ARCHITECTURE_RESILIENCE_REVIEW.md
Normal file
638
docs/ARCHITECTURE_RESILIENCE_REVIEW.md
Normal file
@@ -0,0 +1,638 @@
|
|||||||
|
# Architecture Resilience Review
|
||||||
|
|
||||||
|
This note summarizes the main architectural improvements that would make the app more resilient during live use, especially around timing isolation, failure isolation, and recoverability.
|
||||||
|
|
||||||
|
Phase checklist:
|
||||||
|
|
||||||
|
- [ ] Define subsystem boundaries and target architecture
|
||||||
|
- [ ] Introduce an internal event model
|
||||||
|
- [ ] Split `RuntimeHost`
|
||||||
|
- [ ] Make the render thread the sole GL owner
|
||||||
|
- [ ] Refactor live state layering into an explicit composition model
|
||||||
|
- [ ] Move persistence onto a background snapshot writer
|
||||||
|
- [ ] Make DeckLink/backend lifecycle explicit with a state machine
|
||||||
|
- [ ] Add structured health, telemetry, and operational reporting
|
||||||
|
|
||||||
|
## Timing Review
|
||||||
|
|
||||||
|
The recent OSC work removed several control-path stalls, but the app still has a few deeper timing characteristics that matter for live resilience:
|
||||||
|
|
||||||
|
- output playout is still effectively render-on-demand from the DeckLink completion callback
|
||||||
|
- output buffering and preroll are now larger, but the buffering model is still static and only loosely related to actual render cost
|
||||||
|
- GPU readback is partly asynchronous, but the fallback path still returns to synchronous readback on any miss
|
||||||
|
- preview presentation is still tied to the playout render path
|
||||||
|
- background service timing still relies on coarse polling sleeps
|
||||||
|
|
||||||
|
Those points are important because they affect not just average performance, but how the app behaves under brief spikes, device jitter, or load bursts.
|
||||||
|
|
||||||
|
## Key Findings
|
||||||
|
|
||||||
|
### 1. `RuntimeHost` is carrying too many responsibilities
|
||||||
|
|
||||||
|
`RuntimeHost` currently acts as:
|
||||||
|
|
||||||
|
- config store
|
||||||
|
- persistent state store
|
||||||
|
- live parameter/state authority
|
||||||
|
- shader package registry owner
|
||||||
|
- status/telemetry sink
|
||||||
|
- control mutation entrypoint
|
||||||
|
|
||||||
|
That makes it a single contention and failure domain. It is also why OSC and render timing issues repeatedly surfaced around shared state access.
|
||||||
|
|
||||||
|
Relevant code:
|
||||||
|
|
||||||
|
- [RuntimeHost.h](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/runtime/RuntimeHost.h:15)
|
||||||
|
|
||||||
|
Recommended direction:
|
||||||
|
|
||||||
|
- split persisted config/state from live render-facing state
|
||||||
|
- separate status/telemetry updates from control mutation paths
|
||||||
|
- make render consume snapshots rather than sharing a large mutable authority object
|
||||||
|
|
||||||
|
### 2. OpenGL ownership is still centralized behind one shared lock
|
||||||
|
|
||||||
|
Even after recent timing improvements, preview, input upload, and playout rendering still rely on one shared GL context protected by one `CRITICAL_SECTION`.
|
||||||
|
|
||||||
|
Relevant code:
|
||||||
|
|
||||||
|
- [OpenGLComposite.h](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/gl/OpenGLComposite.h:93)
|
||||||
|
- [OpenGLComposite.cpp](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/gl/OpenGLComposite.cpp:253)
|
||||||
|
- [OpenGLVideoIOBridge.cpp](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/gl/pipeline/OpenGLVideoIOBridge.cpp:70)
|
||||||
|
|
||||||
|
This is still a central choke point and limits timing isolation.
|
||||||
|
|
||||||
|
Recommended direction:
|
||||||
|
|
||||||
|
- use one dedicated render thread as the sole GL owner
|
||||||
|
- have input/output/control threads queue work instead of performing GL work directly
|
||||||
|
- remove ad hoc GL use from callback threads
|
||||||
|
|
||||||
|
### 3. Control flow is spread across polling and shared-memory patterns
|
||||||
|
|
||||||
|
`RuntimeServices` currently mixes:
|
||||||
|
|
||||||
|
- file polling
|
||||||
|
- deferred OSC commit handling
|
||||||
|
- control service orchestration
|
||||||
|
|
||||||
|
OSC ingest, overlay application, and host sync are distributed across several components.
|
||||||
|
|
||||||
|
Relevant code:
|
||||||
|
|
||||||
|
- [RuntimeServices.h](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/control/RuntimeServices.h:26)
|
||||||
|
- [RuntimeServices.cpp](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/control/RuntimeServices.cpp:178)
|
||||||
|
|
||||||
|
Recommended direction:
|
||||||
|
|
||||||
|
- introduce a small internal event pipeline or message bus
|
||||||
|
- use typed events for OSC, reloads, persistence requests, and status changes
|
||||||
|
- make timing ownership explicit per subsystem
|
||||||
|
|
||||||
|
Example event types:
|
||||||
|
|
||||||
|
- `OscParameterTargeted`
|
||||||
|
- `RenderOverlaySettled`
|
||||||
|
- `PersistStateRequested`
|
||||||
|
- `ShaderReloadRequested`
|
||||||
|
- `DeckLinkStatusChanged`
|
||||||
|
|
||||||
|
### 4. Error handling is still heavily UI-coupled
|
||||||
|
|
||||||
|
Failures are often surfaced via `MessageBoxA`, while background services mainly log with `OutputDebugStringA`.
|
||||||
|
|
||||||
|
Relevant code:
|
||||||
|
|
||||||
|
- [OpenGLComposite.cpp](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/gl/OpenGLComposite.cpp:314)
|
||||||
|
- [DeckLinkSession.cpp](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/videoio/decklink/DeckLinkSession.cpp:478)
|
||||||
|
- [RuntimeServices.cpp](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/control/RuntimeServices.cpp:205)
|
||||||
|
|
||||||
|
This is not ideal for a live system where modal dialogs and silent debug logging are both poor operational behavior.
|
||||||
|
|
||||||
|
Recommended direction:
|
||||||
|
|
||||||
|
- introduce structured in-app error reporting
|
||||||
|
- define severity levels and counters
|
||||||
|
- prefer degraded runtime states over modal failure handling where possible
|
||||||
|
- add a rolling log file for operational troubleshooting
|
||||||
|
|
||||||
|
### 5. Live OSC overlay and persisted state are still separate concepts without a formal model
|
||||||
|
|
||||||
|
The current design works better now, but it still relies on hand-managed reconciliation between:
|
||||||
|
|
||||||
|
- persisted parameter state in `RuntimeHost`
|
||||||
|
- transient OSC overlay state in `OpenGLComposite`
|
||||||
|
|
||||||
|
Relevant code:
|
||||||
|
|
||||||
|
- [OpenGLComposite.h](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/gl/OpenGLComposite.h:66)
|
||||||
|
|
||||||
|
Recommended direction:
|
||||||
|
|
||||||
|
Formalize three layers of state:
|
||||||
|
|
||||||
|
- base persisted state
|
||||||
|
- operator/UI committed state
|
||||||
|
- transient live automation overlay
|
||||||
|
|
||||||
|
Then render can always resolve:
|
||||||
|
|
||||||
|
- `final = base + committed + transient`
|
||||||
|
|
||||||
|
That avoids special-case sync behavior becoming scattered across the code.
|
||||||
|
|
||||||
|
### 6. DeckLink lifecycle could be modeled more explicitly
|
||||||
|
|
||||||
|
`DeckLinkSession` has a number of imperative calls, but startup, preroll, running, degraded, and stopped are not represented as an explicit state machine.
|
||||||
|
|
||||||
|
Relevant code:
|
||||||
|
|
||||||
|
- [DeckLinkSession.h](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/videoio/decklink/DeckLinkSession.h:17)
|
||||||
|
|
||||||
|
Recommended direction:
|
||||||
|
|
||||||
|
- introduce explicit session states
|
||||||
|
- define allowed transitions
|
||||||
|
- centralize recovery behavior
|
||||||
|
- make shutdown ordering and degraded-mode behavior more predictable
|
||||||
|
|
||||||
|
Timing-specific additions:
|
||||||
|
|
||||||
|
- separate "device callback received" from "render the next output frame" so output cadence is not driven directly by the completion callback thread
|
||||||
|
- make playout headroom configurable and adaptive instead of using a fixed compile-time preroll count
|
||||||
|
- track an explicit backend health state such as `running-steady`, `catching-up`, `late`, and `dropping`
|
||||||
|
|
||||||
|
Relevant timing code:
|
||||||
|
|
||||||
|
- [OpenGLVideoIOBridge.cpp](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/gl/pipeline/OpenGLVideoIOBridge.cpp:86)
|
||||||
|
- [DeckLinkSession.cpp](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/videoio/decklink/DeckLinkSession.cpp:420)
|
||||||
|
- [DeckLinkSession.cpp](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/videoio/decklink/DeckLinkSession.cpp:487)
|
||||||
|
- [VideoPlayoutScheduler.cpp](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/videoio/VideoPlayoutScheduler.cpp:26)
|
||||||
|
|
||||||
|
Why this matters:
|
||||||
|
|
||||||
|
- `PlayoutFrameCompleted()` currently begins an output frame, takes the shared GL path, renders, reads back, and schedules the next frame in one callback-driven flow.
|
||||||
|
- `VideoPlayoutScheduler::AccountForCompletionResult()` currently reacts to both late and dropped frames by blindly advancing the schedule index by `2`, which is simple but not especially robust.
|
||||||
|
- `kPrerollFrameCount` is now `12`, but `DeckLinkSession::ConfigureOutput()` still creates a fixed pool of `10` mutable output frames. That mismatch suggests the buffering model is not being sized from one coherent source of truth.
|
||||||
|
|
||||||
|
Recommended direction:
|
||||||
|
|
||||||
|
- move playout to a producer/consumer model where a render worker fills output buffers ahead of the DeckLink callback
|
||||||
|
- define buffer-pool sizing from one policy object, for example: preroll depth, minimum spare buffers, and allowed catch-up depth
|
||||||
|
- replace fixed "skip two frames" recovery with measured lag accounting based on actual scheduled-versus-completed position
|
||||||
|
- expose playout latency as a runtime setting or policy, rather than burying it in a constant
|
||||||
|
|
||||||
|
### 6a. The current playout timing model is still callback-coupled
|
||||||
|
|
||||||
|
The app now has more headroom, but the next output frame is still produced directly in the scheduled-frame completion callback path.
|
||||||
|
|
||||||
|
Relevant code:
|
||||||
|
|
||||||
|
- [OpenGLVideoIOBridge.cpp](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/gl/pipeline/OpenGLVideoIOBridge.cpp:86)
|
||||||
|
- [DeckLinkFrameTransfer.cpp](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/videoio/decklink/DeckLinkFrameTransfer.cpp:53)
|
||||||
|
|
||||||
|
That means the completion callback is currently responsible for:
|
||||||
|
|
||||||
|
- frame pacing accounting
|
||||||
|
- acquiring the next output buffer
|
||||||
|
- taking the GL critical section
|
||||||
|
- rendering the composite
|
||||||
|
- performing output readback
|
||||||
|
- scheduling the next frame
|
||||||
|
|
||||||
|
This works when the app is comfortably within budget, but it makes deadline misses much harder to absorb gracefully.
|
||||||
|
|
||||||
|
Recommended direction:
|
||||||
|
|
||||||
|
- make the DeckLink callback a lightweight notifier
|
||||||
|
- have a dedicated playout worker or render worker keep an ahead-of-time queue of ready output frames
|
||||||
|
- treat callback time as control-plane time, not render time
|
||||||
|
|
||||||
|
### 6b. A producer/consumer playout model would be a better long-term fit
|
||||||
|
|
||||||
|
The stronger architecture for this app is:
|
||||||
|
|
||||||
|
- a render scheduler or dedicated render thread runs at the configured video cadence
|
||||||
|
- rendering produces completed output frames ahead of need
|
||||||
|
- those frames are placed into a bounded queue or ring buffer
|
||||||
|
- the DeckLink side consumes already-prepared frames when callbacks indicate they are needed
|
||||||
|
|
||||||
|
That is a better fit than callback-driven rendering because it separates:
|
||||||
|
|
||||||
|
- render timing
|
||||||
|
- GL ownership
|
||||||
|
- output-device timing
|
||||||
|
- latency policy
|
||||||
|
|
||||||
|
In that model:
|
||||||
|
|
||||||
|
- render is the producer
|
||||||
|
- DeckLink is the timing consumer
|
||||||
|
- the queue between them becomes the main place to manage latency versus resilience
|
||||||
|
|
||||||
|
Why this is preferable:
|
||||||
|
|
||||||
|
- brief callback jitter is less likely to become a visible dropped frame
|
||||||
|
- render spikes can be absorbed by queue headroom instead of immediately missing output deadlines
|
||||||
|
- latency becomes an explicit policy choice rather than an incidental side effect of callback timing
|
||||||
|
- queue depth, underruns, stale-frame reuse, and catch-up behavior become measurable and tunable
|
||||||
|
|
||||||
|
Recommended direction:
|
||||||
|
|
||||||
|
- move toward a bounded producer/consumer playout queue
|
||||||
|
- make queue depth and target headroom runtime policy, not compile-time constants
|
||||||
|
- define explicit underrun behavior, for example:
|
||||||
|
- reuse newest completed frame
|
||||||
|
- reuse last scheduled frame
|
||||||
|
- output black or degraded frame
|
||||||
|
- keep DeckLink callbacks limited to dequeue/schedule/accounting work wherever possible
|
||||||
|
|
||||||
|
### 7. Persistence should be more asynchronous and debounced
|
||||||
|
|
||||||
|
`SavePersistentState()` is still called directly from many update paths.
|
||||||
|
|
||||||
|
Relevant code:
|
||||||
|
|
||||||
|
- [RuntimeHost.cpp](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/runtime/RuntimeHost.cpp:1841)
|
||||||
|
|
||||||
|
Recent OSC work already reduced this problem for live automation, but the broader architecture would still benefit from:
|
||||||
|
|
||||||
|
- a debounced persistence queue
|
||||||
|
- atomic write-behind snapshots
|
||||||
|
- clear separation between state mutation and disk flush
|
||||||
|
|
||||||
|
This improves both resilience and timing safety.
|
||||||
|
|
||||||
|
### 8. Telemetry is useful, but still too coarse
|
||||||
|
|
||||||
|
The app already records render timing and playout pacing, which is a good foundation.
|
||||||
|
|
||||||
|
Relevant code:
|
||||||
|
|
||||||
|
- [OpenGLRenderPipeline.cpp](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/gl/pipeline/OpenGLRenderPipeline.cpp:24)
|
||||||
|
- [OpenGLVideoIOBridge.cpp](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/gl/pipeline/OpenGLVideoIOBridge.cpp:24)
|
||||||
|
|
||||||
|
Recommended direction:
|
||||||
|
|
||||||
|
Add lightweight tracing for:
|
||||||
|
|
||||||
|
- input callback latency
|
||||||
|
- input upload skip count
|
||||||
|
- GL lock wait time
|
||||||
|
- render queue depth
|
||||||
|
- render time
|
||||||
|
- pass build/compile latency
|
||||||
|
- readback time
|
||||||
|
- output scheduling lag
|
||||||
|
- output queue depth
|
||||||
|
- preroll depth versus spare-buffer depth
|
||||||
|
- preview present cost and skipped-preview count
|
||||||
|
- control queue depth
|
||||||
|
- `RuntimeHost` lock contention
|
||||||
|
|
||||||
|
That would make future tuning and failure diagnosis much easier.
|
||||||
|
|
||||||
|
Timing-specific observations from the current code:
|
||||||
|
|
||||||
|
- render time is captured as one total number in [OpenGLRenderPipeline.cpp](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/gl/pipeline/OpenGLRenderPipeline.cpp:24), but not split into draw, pack, readback wait, readback copy, or preview present
|
||||||
|
- frame pacing stats are recorded in [OpenGLVideoIOBridge.cpp](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/gl/pipeline/OpenGLVideoIOBridge.cpp:17), but there is no explicit visibility into how much queued playout headroom remains
|
||||||
|
- input uploads are intentionally skipped when the GL bridge is busy in [OpenGLVideoIOBridge.cpp](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/gl/pipeline/OpenGLVideoIOBridge.cpp:60), but the app does not currently surface how often that is happening
|
||||||
|
|
||||||
|
### 8a. Preview and playout are still too close together
|
||||||
|
|
||||||
|
The desktop preview is rate-limited, but still presented from inside the render pipeline path.
|
||||||
|
|
||||||
|
Relevant code:
|
||||||
|
|
||||||
|
- [OpenGLRenderPipeline.cpp](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/gl/pipeline/OpenGLRenderPipeline.cpp:54)
|
||||||
|
- [OpenGLComposite.cpp](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/gl/OpenGLComposite.cpp:235)
|
||||||
|
|
||||||
|
This means preview presentation can still consume time on the same path that is trying to meet output deadlines.
|
||||||
|
|
||||||
|
Recommended direction:
|
||||||
|
|
||||||
|
- treat preview as best-effort and entirely subordinate to playout
|
||||||
|
- move preview present to a separate presentation schedule fed from the latest completed render
|
||||||
|
- record preview skips and preview present cost independently from playout timing
|
||||||
|
|
||||||
|
### 8b. Readback is improved, but still not fully deadline-safe
|
||||||
|
|
||||||
|
The async readback path is a good step, but the miss path still falls back to synchronous `glReadPixels()` and then flushes the async pipeline.
|
||||||
|
|
||||||
|
Relevant code:
|
||||||
|
|
||||||
|
- [OpenGLRenderPipeline.cpp](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/gl/pipeline/OpenGLRenderPipeline.cpp:150)
|
||||||
|
- [OpenGLRenderPipeline.cpp](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/gl/pipeline/OpenGLRenderPipeline.cpp:228)
|
||||||
|
|
||||||
|
That means a single late GPU fence can push the app back onto the most timing-sensitive path exactly when it is already under pressure.
|
||||||
|
|
||||||
|
Recommended direction:
|
||||||
|
|
||||||
|
- increase readback instrumentation before changing policy again
|
||||||
|
- consider deeper readback buffering or a true stale-frame reuse policy instead of immediate synchronous fallback
|
||||||
|
- separate "freshest possible frame" policy from "never miss output deadline" policy and make that tradeoff explicit
|
||||||
|
|
||||||
|
### 8c. Background control and file-watch timing are still coarse
|
||||||
|
|
||||||
|
`RuntimeServices::PollLoop()` currently uses a `25 x Sleep(10)` loop, which gives it a coarse `~250 ms` cadence for file-watch polling and deferred OSC commit work.
|
||||||
|
|
||||||
|
Relevant code:
|
||||||
|
|
||||||
|
- [RuntimeServices.cpp](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/control/RuntimeServices.cpp:245)
|
||||||
|
|
||||||
|
That is acceptable for non-critical background work, but it is still too blunt to be the long-term timing model for coordination-heavy runtime services.
|
||||||
|
|
||||||
|
Recommended direction:
|
||||||
|
|
||||||
|
- replace coarse sleep polling with waitable events or condition-variable driven wakeups where practical
|
||||||
|
- isolate truly background work from latency-sensitive control reconciliation
|
||||||
|
- add separate metrics for queue age, not just queue depth
|
||||||
|
|
||||||
|
## Phased Roadmap
|
||||||
|
|
||||||
|
This roadmap is ordered by architectural dependency rather than by “quick wins.” The goal is to move the app toward clearer ownership boundaries and safer live behavior without doing later work on top of foundations that are likely to change again.
|
||||||
|
|
||||||
|
### Phase 1. Define subsystem boundaries and target architecture
|
||||||
|
|
||||||
|
Before changing major internals, formalize the target responsibilities for each major part of the app.
|
||||||
|
|
||||||
|
Target split:
|
||||||
|
|
||||||
|
- `RuntimeStore`
|
||||||
|
- persisted config
|
||||||
|
- persisted layer stack
|
||||||
|
- preset persistence
|
||||||
|
- `RuntimeSnapshot`
|
||||||
|
- render-facing immutable or near-immutable snapshots
|
||||||
|
- parameter values prepared for the render path
|
||||||
|
- `ControlServices`
|
||||||
|
- OSC ingress
|
||||||
|
- web control ingress
|
||||||
|
- reload/file-watch requests
|
||||||
|
- commit/persist requests
|
||||||
|
- `RenderEngine`
|
||||||
|
- sole owner of live GL rendering
|
||||||
|
- sole consumer of render snapshots plus transient overlays
|
||||||
|
- `VideoBackend`
|
||||||
|
- DeckLink input/output lifecycle
|
||||||
|
- pacing and scheduling
|
||||||
|
- `Health/Telemetry`
|
||||||
|
- logging
|
||||||
|
- counters
|
||||||
|
- timing traces
|
||||||
|
- degraded-state reporting
|
||||||
|
|
||||||
|
Why this phase comes first:
|
||||||
|
|
||||||
|
- it prevents later refactors from reintroducing responsibility overlap
|
||||||
|
- it gives names to the seams the later phases will build around
|
||||||
|
- it reduces the risk of replacing one monolith with several poorly-defined ones
|
||||||
|
|
||||||
|
Suggested deliverables:
|
||||||
|
|
||||||
|
- a short architecture diagram
|
||||||
|
- a responsibility table for each subsystem
|
||||||
|
- a list of allowed dependencies between subsystems
|
||||||
|
|
||||||
|
### Phase 2. Introduce an internal event model
|
||||||
|
|
||||||
|
Once subsystem boundaries are defined, introduce a typed event pipeline between them. This should happen before large state splits so the app has a stable coordination model.
|
||||||
|
|
||||||
|
Example event families:
|
||||||
|
|
||||||
|
- control events
|
||||||
|
- `OscParameterTargeted`
|
||||||
|
- `UiParameterCommitted`
|
||||||
|
- `TriggerFired`
|
||||||
|
- runtime events
|
||||||
|
- `ShaderReloadRequested`
|
||||||
|
- `PackagesRescanned`
|
||||||
|
- `PersistStateRequested`
|
||||||
|
- render events
|
||||||
|
- `OverlayApplied`
|
||||||
|
- `OverlaySettled`
|
||||||
|
- `SnapshotPublished`
|
||||||
|
- backend events
|
||||||
|
- `InputSignalChanged`
|
||||||
|
- `OutputLateFrameDetected`
|
||||||
|
- `OutputDroppedFrameDetected`
|
||||||
|
- health events
|
||||||
|
- `SubsystemWarningRaised`
|
||||||
|
- `SubsystemRecovered`
|
||||||
|
|
||||||
|
Why this phase comes second:
|
||||||
|
|
||||||
|
- it provides a migration path away from direct cross-calls
|
||||||
|
- it makes ownership explicit before data structures are split apart
|
||||||
|
- it lets you move one subsystem at a time without losing coordination
|
||||||
|
|
||||||
|
Suggested outcome:
|
||||||
|
|
||||||
|
- the app stops relying on “shared object plus mutex plus polling” as the default coordination pattern
|
||||||
|
|
||||||
|
### Phase 3. Split `RuntimeHost` into persistent state, render snapshot state, and service-facing coordination
|
||||||
|
|
||||||
|
After the event model exists, break apart `RuntimeHost`.
|
||||||
|
|
||||||
|
Recommended split:
|
||||||
|
|
||||||
|
- `RuntimeStore`
|
||||||
|
- owns config and saved layer data
|
||||||
|
- handles serialization/deserialization
|
||||||
|
- does not sit on the live render path
|
||||||
|
- `RuntimeCoordinator`
|
||||||
|
- resolves control actions
|
||||||
|
- validates mutations
|
||||||
|
- publishes new snapshots
|
||||||
|
- bridges events between services and render
|
||||||
|
- `RuntimeSnapshotProvider`
|
||||||
|
- publishes immutable render snapshots
|
||||||
|
- avoids large shared mutable structures on the render path
|
||||||
|
|
||||||
|
Why this phase comes before render-thread isolation:
|
||||||
|
|
||||||
|
- render isolation is easier when the render thread consumes clean snapshots instead of a large mutable host object
|
||||||
|
- otherwise the GL refactor still drags along too much shared state complexity
|
||||||
|
|
||||||
|
Primary design rule:
|
||||||
|
|
||||||
|
- render should read snapshots
|
||||||
|
- persistence should write stored state
|
||||||
|
- services should request mutations through the coordinator
|
||||||
|
|
||||||
|
### Phase 4. Make the render thread the sole GL owner
|
||||||
|
|
||||||
|
With state and coordination cleaner, move to a dedicated render-thread model.
|
||||||
|
|
||||||
|
Target behavior:
|
||||||
|
|
||||||
|
- one thread owns the GL context
|
||||||
|
- input callbacks never perform GL work directly
|
||||||
|
- output callbacks never perform GL work directly
|
||||||
|
- preview presentation, texture upload, render passes, readback, and output pack work are all issued by the render thread
|
||||||
|
|
||||||
|
Other threads should only:
|
||||||
|
|
||||||
|
- enqueue new video frames
|
||||||
|
- enqueue control updates
|
||||||
|
- enqueue backend events
|
||||||
|
- consume produced output buffers
|
||||||
|
|
||||||
|
Why this phase comes here:
|
||||||
|
|
||||||
|
- it is much safer once state access and control coordination are no longer centered on `RuntimeHost`
|
||||||
|
- it avoids coupling the render-thread refactor to storage and service refactors at the same time
|
||||||
|
|
||||||
|
Expected benefits:
|
||||||
|
|
||||||
|
- less cross-thread GL contention
|
||||||
|
- easier timing reasoning
|
||||||
|
- much lower risk of callback-driven stalls
|
||||||
|
- a clearer foundation for future GPU pipeline work
|
||||||
|
|
||||||
|
### Phase 5. Refactor live state layering into an explicit composition model
|
||||||
|
|
||||||
|
Once rendering and snapshots are isolated, formalize how final parameter values are derived.
|
||||||
|
|
||||||
|
Recommended layers:
|
||||||
|
|
||||||
|
- base persisted state
|
||||||
|
- operator-committed live state
|
||||||
|
- transient automation overlay
|
||||||
|
|
||||||
|
Render should derive final values from a clear composition rule such as:
|
||||||
|
|
||||||
|
- `final = base + committed + transient`
|
||||||
|
|
||||||
|
Why this phase follows render isolation:
|
||||||
|
|
||||||
|
- once render owns snapshot consumption, it becomes much easier to cleanly evaluate layered state without touching persistence or control services
|
||||||
|
- it turns the current OSC overlay behavior into a first-class model instead of an implementation detail
|
||||||
|
|
||||||
|
Expected benefits:
|
||||||
|
|
||||||
|
- fewer one-off sync rules
|
||||||
|
- clearer behavior for OSC, UI changes, and automation
|
||||||
|
- easier future expansion to presets, cues, or timed transitions
|
||||||
|
|
||||||
|
### Phase 6. Move persistence onto a background snapshot writer
|
||||||
|
|
||||||
|
After the state model is explicit, persistence should become a background concern rather than a synchronous side effect of mutations.
|
||||||
|
|
||||||
|
Target behavior:
|
||||||
|
|
||||||
|
- mutations update authoritative in-memory stored state
|
||||||
|
- persistence requests are queued
|
||||||
|
- disk writes are debounced and coalesced
|
||||||
|
- writes are atomic and versioned where practical
|
||||||
|
|
||||||
|
Why this phase comes after state splitting:
|
||||||
|
|
||||||
|
- otherwise persistence logic will need to be rewritten twice
|
||||||
|
- it should operate on the new `RuntimeStore` model, not on the current mixed-responsibility object
|
||||||
|
|
||||||
|
Expected benefits:
|
||||||
|
|
||||||
|
- less timing interference
|
||||||
|
- better corruption resistance
|
||||||
|
- cleaner restart/recovery semantics
|
||||||
|
|
||||||
|
### Phase 7. Make DeckLink/backend lifecycle explicit with a state machine
|
||||||
|
|
||||||
|
Once the render and state layers are cleaner, refactor the video backend into an explicit lifecycle model.
|
||||||
|
|
||||||
|
Suggested states:
|
||||||
|
|
||||||
|
- uninitialized
|
||||||
|
- devices-discovered
|
||||||
|
- configured
|
||||||
|
- prerolling
|
||||||
|
- running
|
||||||
|
- degraded
|
||||||
|
- stopping
|
||||||
|
- stopped
|
||||||
|
- failed
|
||||||
|
|
||||||
|
Why this phase belongs here:
|
||||||
|
|
||||||
|
- the backend should integrate with the new event model
|
||||||
|
- degraded/recovery behavior will be easier once rendering and state coordination are already more deterministic
|
||||||
|
|
||||||
|
Expected benefits:
|
||||||
|
|
||||||
|
- safer startup/shutdown ordering
|
||||||
|
- clearer recovery behavior
|
||||||
|
- easier handling of missing input, dropped frames, or reconfiguration
|
||||||
|
- a clearer place to own playout headroom policy, output queue sizing, and late-frame recovery behavior
|
||||||
|
|
||||||
|
### Phase 8. Add structured health, telemetry, and operational reporting
|
||||||
|
|
||||||
|
This phase should happen after the main ownership changes so the telemetry can reflect the final architecture instead of a transient one.
|
||||||
|
|
||||||
|
Recommended coverage:
|
||||||
|
|
||||||
|
- render queue depth
|
||||||
|
- GL lock wait time, if any shared lock remains
|
||||||
|
- input callback latency
|
||||||
|
- input upload skip count
|
||||||
|
- output scheduling lag
|
||||||
|
- output queue depth and spare-buffer depth
|
||||||
|
- readback timing
|
||||||
|
- readback fence wait timing
|
||||||
|
- synchronous readback fallback count
|
||||||
|
- preview present timing and skipped-preview count
|
||||||
|
- snapshot publish frequency
|
||||||
|
- persistence queue depth
|
||||||
|
- event queue depth
|
||||||
|
- backend state transitions
|
||||||
|
- warning/error counters per subsystem
|
||||||
|
|
||||||
|
Also replace modal-only error handling with:
|
||||||
|
|
||||||
|
- structured in-app health state
|
||||||
|
- severity-based logging
|
||||||
|
- rolling log files
|
||||||
|
- operator-visible degraded-state messages
|
||||||
|
|
||||||
|
Why this phase comes last:
|
||||||
|
|
||||||
|
- it should instrument the architecture you intend to keep
|
||||||
|
- otherwise instrumentation work gets invalidated by the refactor
|
||||||
|
|
||||||
|
## Recommended Execution Order
|
||||||
|
|
||||||
|
If this is approached as a serious architecture program rather than opportunistic cleanup, the recommended order is:
|
||||||
|
|
||||||
|
1. Define subsystem boundaries and target architecture.
|
||||||
|
2. Introduce the internal event model.
|
||||||
|
3. Split `RuntimeHost`.
|
||||||
|
4. Make the render thread the sole GL owner.
|
||||||
|
5. Formalize live state layering and composition.
|
||||||
|
6. Move persistence to a background snapshot writer.
|
||||||
|
7. Refactor DeckLink/backend lifecycle into an explicit state machine.
|
||||||
|
8. Add structured telemetry, health reporting, and operational diagnostics.
|
||||||
|
|
||||||
|
## Why This Order Makes Sense
|
||||||
|
|
||||||
|
This order tries to avoid doing foundational work twice.
|
||||||
|
|
||||||
|
- The event model comes before major subsystem extraction so coordination patterns stabilize early.
|
||||||
|
- `RuntimeHost` is split before render isolation so the render thread does not inherit the current monolithic state model.
|
||||||
|
- Live state layering is formalized only after render ownership is clearer.
|
||||||
|
- Persistence is moved later so it can target the final state model rather than the current one.
|
||||||
|
- Telemetry is intentionally late so it instruments the architecture that survives the refactor.
|
||||||
|
|
||||||
|
## Short Version
|
||||||
|
|
||||||
|
The app is in a much better place than it was before the OSC timing work, but the main remaining architectural risk is still shared ownership. Too many responsibilities converge on `RuntimeHost` and the shared GL path. The most sensible path forward is:
|
||||||
|
|
||||||
|
1. define boundaries
|
||||||
|
2. establish an event model
|
||||||
|
3. split state ownership
|
||||||
|
4. isolate rendering
|
||||||
|
5. formalize layered live state
|
||||||
|
6. background persistence
|
||||||
|
7. explicit backend lifecycle
|
||||||
|
8. health and telemetry
|
||||||
|
|
||||||
|
That sequence gives each later phase a cleaner foundation than the current app has today.
|
||||||
@@ -8,11 +8,15 @@ Set the UDP port in `config/runtime-host.json`:
|
|||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"oscPort": 9000
|
"oscBindAddress": "127.0.0.1",
|
||||||
|
"oscPort": 9000,
|
||||||
|
"oscSmoothing": 0.18
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
Set `oscPort` to `0` to disable the OSC listener.
|
Set `oscPort` to `0` to disable the OSC listener.
|
||||||
|
Set `oscBindAddress` to `127.0.0.1` to keep OSC local to the host, or `0.0.0.0` to listen on all IPv4 interfaces.
|
||||||
|
Set `oscSmoothing` to a value from `0.0` to `1.0` to add a subtle per-frame easing amount for numeric OSC controls. `0.0` disables smoothing, and larger values respond more quickly.
|
||||||
|
|
||||||
## Address Pattern
|
## Address Pattern
|
||||||
|
|
||||||
@@ -61,6 +65,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.
|
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:
|
Examples:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
@@ -72,6 +78,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.
|
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:
|
For `trigger` parameters, the OSC value is treated as a pulse. A simple integer or boolean message is enough:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
@@ -114,10 +122,21 @@ send('127.0.0.1:9000', '/VideoShaderToys/fisheye-reproject/tiltDegrees', {type:
|
|||||||
|
|
||||||
## Network
|
## Network
|
||||||
|
|
||||||
The listener binds to localhost only:
|
By default the listener binds to localhost only:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
127.0.0.1:<oscPort>
|
127.0.0.1:<oscPort>
|
||||||
```
|
```
|
||||||
|
|
||||||
This keeps the control surface local to the machine running Video Shader Toys.
|
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.
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ struct ShaderContext
|
|||||||
float bypass;
|
float bypass;
|
||||||
int sourceHistoryLength;
|
int sourceHistoryLength;
|
||||||
int temporalHistoryLength;
|
int temporalHistoryLength;
|
||||||
|
int feedbackAvailable;
|
||||||
};
|
};
|
||||||
|
|
||||||
cbuffer GlobalParams
|
cbuffer GlobalParams
|
||||||
@@ -34,16 +35,23 @@ cbuffer GlobalParams
|
|||||||
float gBypass;
|
float gBypass;
|
||||||
int gSourceHistoryLength;
|
int gSourceHistoryLength;
|
||||||
int gTemporalHistoryLength;
|
int gTemporalHistoryLength;
|
||||||
|
int gFeedbackAvailable;
|
||||||
{{PARAMETER_UNIFORMS}}};
|
{{PARAMETER_UNIFORMS}}};
|
||||||
|
|
||||||
Sampler2D<float4> gVideoInput;
|
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}}
|
{{TEXT_SAMPLERS}}
|
||||||
float4 sampleVideo(float2 tc)
|
float4 sampleVideo(float2 tc)
|
||||||
{
|
{
|
||||||
return gVideoInput.Sample(tc);
|
return gVideoInput.Sample(tc);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
float4 sampleLayerInput(float2 tc)
|
||||||
|
{
|
||||||
|
return gLayerInput.Sample(tc);
|
||||||
|
}
|
||||||
|
|
||||||
float4 sampleSourceHistory(int framesAgo, float2 tc)
|
float4 sampleSourceHistory(int framesAgo, float2 tc)
|
||||||
{
|
{
|
||||||
if (gSourceHistoryLength <= 0)
|
if (gSourceHistoryLength <= 0)
|
||||||
@@ -74,6 +82,8 @@ float4 sampleTemporalHistory(int framesAgo, float2 tc)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
{{FEEDBACK_HELPER}}
|
||||||
|
|
||||||
{{TEXT_HELPERS}}
|
{{TEXT_HELPERS}}
|
||||||
#include "{{USER_SHADER_INCLUDE}}"
|
#include "{{USER_SHADER_INCLUDE}}"
|
||||||
|
|
||||||
@@ -94,6 +104,7 @@ float4 fragmentMain(FragmentInput input) : SV_Target
|
|||||||
context.bypass = gBypass;
|
context.bypass = gBypass;
|
||||||
context.sourceHistoryLength = gSourceHistoryLength;
|
context.sourceHistoryLength = gSourceHistoryLength;
|
||||||
context.temporalHistoryLength = gTemporalHistoryLength;
|
context.temporalHistoryLength = gTemporalHistoryLength;
|
||||||
|
context.feedbackAvailable = gFeedbackAvailable;
|
||||||
float4 effectedColor = {{ENTRY_POINT_CALL}};
|
float4 effectedColor = {{ENTRY_POINT_CALL}};
|
||||||
float mixValue = clamp(gBypass > 0.5 ? 0.0 : gMixAmount, 0.0, 1.0);
|
float mixValue = clamp(gBypass > 0.5 ? 0.0 : gMixAmount, 0.0, 1.0);
|
||||||
return lerp(context.sourceColor, effectedColor, mixValue);
|
return lerp(context.sourceColor, effectedColor, mixValue);
|
||||||
|
|||||||
@@ -101,9 +101,17 @@ Optional fields:
|
|||||||
- `textures`: texture assets to load and expose as samplers.
|
- `textures`: texture assets to load and expose as samplers.
|
||||||
- `fonts`: packaged font assets for live text parameters.
|
- `fonts`: packaged font assets for live text parameters.
|
||||||
- `temporal`: history-buffer requirements.
|
- `temporal`: history-buffer requirements.
|
||||||
|
- `feedback`: optional previous-frame shader-local feedback surface.
|
||||||
|
|
||||||
Parameter objects may also include an optional `description` string. The control UI displays it as one-line helper text with the full text available on hover, so use it for short operational guidance rather than long documentation.
|
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:
|
Shader-visible identifiers must be valid Slang-style identifiers:
|
||||||
|
|
||||||
- `entryPoint`
|
- `entryPoint`
|
||||||
@@ -194,6 +202,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.
|
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
|
## Slang Entry Point
|
||||||
|
|
||||||
Your shader file must implement the manifest `entryPoint`.
|
Your shader file must implement the manifest `entryPoint`.
|
||||||
@@ -239,6 +339,7 @@ struct ShaderContext
|
|||||||
float bypass;
|
float bypass;
|
||||||
int sourceHistoryLength;
|
int sourceHistoryLength;
|
||||||
int temporalHistoryLength;
|
int temporalHistoryLength;
|
||||||
|
int feedbackAvailable;
|
||||||
};
|
};
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -257,6 +358,7 @@ Fields:
|
|||||||
- `bypass`: `1.0` when the layer is bypassed, otherwise `0.0`.
|
- `bypass`: `1.0` when the layer is bypassed, otherwise `0.0`.
|
||||||
- `sourceHistoryLength`: number of usable source-history frames currently available.
|
- `sourceHistoryLength`: number of usable source-history frames currently available.
|
||||||
- `temporalHistoryLength`: number of usable temporal frames currently available for this layer.
|
- `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:
|
Color/precision notes:
|
||||||
|
|
||||||
@@ -270,17 +372,23 @@ Color/precision notes:
|
|||||||
The wrapper provides:
|
The wrapper provides:
|
||||||
|
|
||||||
```slang
|
```slang
|
||||||
|
float4 sampleLayerInput(float2 uv);
|
||||||
float4 sampleVideo(float2 uv);
|
float4 sampleVideo(float2 uv);
|
||||||
float4 sampleSourceHistory(int framesAgo, float2 uv);
|
float4 sampleSourceHistory(int framesAgo, float2 uv);
|
||||||
float4 sampleTemporalHistory(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`.
|
`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`.
|
`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:
|
Example:
|
||||||
|
|
||||||
```slang
|
```slang
|
||||||
@@ -291,6 +399,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
|
## Parameters
|
||||||
|
|
||||||
Manifest parameters are exposed to Slang as global values with the same `id`.
|
Manifest parameters are exposed to Slang as global values with the same `id`.
|
||||||
66
shaders/feedback-data-blocks/shader.json
Normal file
66
shaders/feedback-data-blocks/shader.json
Normal 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."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
152
shaders/feedback-data-blocks/shader.slang
Normal file
152
shaders/feedback-data-blocks/shader.slang
Normal 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);
|
||||||
|
}
|
||||||
110
shaders/feedback-highlight-accumulator/shader.json
Normal file
110
shaders/feedback-highlight-accumulator/shader.json
Normal 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."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
39
shaders/feedback-highlight-accumulator/shader.slang
Normal file
39
shaders/feedback-highlight-accumulator/shader.slang
Normal 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);
|
||||||
|
}
|
||||||
@@ -59,6 +59,26 @@
|
|||||||
],
|
],
|
||||||
"description": "Normalized fisheye radius; adjust X/Y when the image is cropped or squeezed."
|
"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",
|
"id": "virtualFovDegrees",
|
||||||
"label": "Virtual FOV",
|
"label": "Virtual FOV",
|
||||||
|
|||||||
@@ -61,6 +61,20 @@ float normalizedFisheyeRadius(float theta, float halfFov)
|
|||||||
return theta / safeHalfFov;
|
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)
|
float4 shadeVideo(ShaderContext context)
|
||||||
{
|
{
|
||||||
float2 screen = float2(context.uv.x * 2.0 - 1.0, 1.0 - context.uv.y * 2.0);
|
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)
|
if (sourceUv.x < 0.0 || sourceUv.x > 1.0 || sourceUv.y < 0.0 || sourceUv.y > 1.0)
|
||||||
return outsideColor;
|
return outsideColor;
|
||||||
|
|
||||||
return sampleVideo(sourceUv);
|
float sourceMask = sourceUvRectMask(sourceUv, context.inputResolution);
|
||||||
|
float4 sourceColor = sampleVideo(sourceUv);
|
||||||
|
return saturate(lerp(outsideColor, sourceColor, sourceMask));
|
||||||
}
|
}
|
||||||
|
|||||||
49
shaders/white-balance-correction/shader.json
Normal file
49
shaders/white-balance-correction/shader.json
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
{
|
||||||
|
"id": "white-balance-correction",
|
||||||
|
"name": "White Balance Correction",
|
||||||
|
"description": "Provides operator-friendly warm/cool, green/magenta, and exposure correction intended to pair with the White Match Probe.",
|
||||||
|
"category": "Color",
|
||||||
|
"entryPoint": "shadeVideo",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"id": "warmCool",
|
||||||
|
"label": "Warm / Cool",
|
||||||
|
"type": "float",
|
||||||
|
"default": 0.0,
|
||||||
|
"min": -1.0,
|
||||||
|
"max": 1.0,
|
||||||
|
"step": 0.001,
|
||||||
|
"description": "Moves the image cooler at negative values and warmer at positive values."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "greenMagenta",
|
||||||
|
"label": "Green / Magenta",
|
||||||
|
"type": "float",
|
||||||
|
"default": 0.0,
|
||||||
|
"min": -1.0,
|
||||||
|
"max": 1.0,
|
||||||
|
"step": 0.001,
|
||||||
|
"description": "Moves the image toward magenta at negative values and toward green at positive values."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "exposure",
|
||||||
|
"label": "Exposure",
|
||||||
|
"type": "float",
|
||||||
|
"default": 0.0,
|
||||||
|
"min": -4.0,
|
||||||
|
"max": 4.0,
|
||||||
|
"step": 0.01,
|
||||||
|
"description": "Exposure offset in stop units, using a Blender-style 2^exposure brightness scale."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "strength",
|
||||||
|
"label": "Strength",
|
||||||
|
"type": "float",
|
||||||
|
"default": 1.0,
|
||||||
|
"min": 0.0,
|
||||||
|
"max": 1.0,
|
||||||
|
"step": 0.01,
|
||||||
|
"description": "Blends the correction with the original image."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
35
shaders/white-balance-correction/shader.slang
Normal file
35
shaders/white-balance-correction/shader.slang
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
float3 applyWhiteBalanceOnly(float3 color)
|
||||||
|
{
|
||||||
|
float warmAmount = clamp(warmCool, -1.0, 1.0);
|
||||||
|
float tintAmount = clamp(greenMagenta, -1.0, 1.0);
|
||||||
|
|
||||||
|
// Warm/cool pivots red against blue while keeping green more stable.
|
||||||
|
float3 warmCoolGain = float3(
|
||||||
|
exp2(warmAmount * 0.35),
|
||||||
|
exp2(-abs(warmAmount) * 0.08),
|
||||||
|
exp2(-warmAmount * 0.35));
|
||||||
|
|
||||||
|
// Green/magenta pivots green against the average of red and blue.
|
||||||
|
float3 tintGain = float3(
|
||||||
|
exp2(-tintAmount * 0.22),
|
||||||
|
exp2(tintAmount * 0.35),
|
||||||
|
exp2(-tintAmount * 0.22));
|
||||||
|
|
||||||
|
return color * warmCoolGain * tintGain;
|
||||||
|
}
|
||||||
|
|
||||||
|
float3 applyExposureLikeBlender(float3 color)
|
||||||
|
{
|
||||||
|
// Match the compositor-style exposure model: every +1.0 stop doubles the
|
||||||
|
// image and every -1.0 stop halves it.
|
||||||
|
return color * exp2(exposure);
|
||||||
|
}
|
||||||
|
|
||||||
|
float4 shadeVideo(ShaderContext context)
|
||||||
|
{
|
||||||
|
float4 source = context.sourceColor;
|
||||||
|
float3 balanced = applyWhiteBalanceOnly(source.rgb);
|
||||||
|
float3 corrected = applyExposureLikeBlender(balanced);
|
||||||
|
source.rgb = lerp(source.rgb, corrected, saturate(strength));
|
||||||
|
return source;
|
||||||
|
}
|
||||||
147
shaders/white-match-probe/shader.json
Normal file
147
shaders/white-match-probe/shader.json
Normal file
@@ -0,0 +1,147 @@
|
|||||||
|
{
|
||||||
|
"id": "white-match-probe",
|
||||||
|
"name": "White Match Probe",
|
||||||
|
"description": "Samples a movable box, stores a reference color on trigger using shader-local feedback, and compares the current sample against a captured or manual reference for camera matching.",
|
||||||
|
"category": "Scopes & Guides",
|
||||||
|
"entryPoint": "storeReferenceState",
|
||||||
|
"passes": [
|
||||||
|
{
|
||||||
|
"id": "store",
|
||||||
|
"source": "shader.slang",
|
||||||
|
"entryPoint": "storeReferenceState",
|
||||||
|
"output": "referenceState"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "display",
|
||||||
|
"source": "shader.slang",
|
||||||
|
"entryPoint": "displayReferenceCompare",
|
||||||
|
"inputs": [
|
||||||
|
"referenceState"
|
||||||
|
],
|
||||||
|
"output": "layerOutput"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"feedback": {
|
||||||
|
"enabled": true,
|
||||||
|
"writePass": "store"
|
||||||
|
},
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"id": "referenceSource",
|
||||||
|
"label": "Reference Source",
|
||||||
|
"type": "enum",
|
||||||
|
"default": "captured",
|
||||||
|
"options": [
|
||||||
|
{
|
||||||
|
"value": "captured",
|
||||||
|
"label": "Captured Sample"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value": "manual",
|
||||||
|
"label": "Manual Color"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "Choose whether the probe compares against a captured screen sample or a manually selected reference color."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "captureReference",
|
||||||
|
"label": "Capture Reference",
|
||||||
|
"type": "trigger",
|
||||||
|
"description": "Stores the current sample box average as the held reference."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "sampleCenter",
|
||||||
|
"label": "Sample Center",
|
||||||
|
"type": "vec2",
|
||||||
|
"default": [
|
||||||
|
0.5,
|
||||||
|
0.5
|
||||||
|
],
|
||||||
|
"min": [
|
||||||
|
0.0,
|
||||||
|
0.0
|
||||||
|
],
|
||||||
|
"max": [
|
||||||
|
1.0,
|
||||||
|
1.0
|
||||||
|
],
|
||||||
|
"step": [
|
||||||
|
0.001,
|
||||||
|
0.001
|
||||||
|
],
|
||||||
|
"description": "Center of the sample box in normalized coordinates."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "sampleSize",
|
||||||
|
"label": "Sample Size",
|
||||||
|
"type": "vec2",
|
||||||
|
"default": [
|
||||||
|
0.14,
|
||||||
|
0.14
|
||||||
|
],
|
||||||
|
"min": [
|
||||||
|
0.02,
|
||||||
|
0.02
|
||||||
|
],
|
||||||
|
"max": [
|
||||||
|
0.5,
|
||||||
|
0.5
|
||||||
|
],
|
||||||
|
"step": [
|
||||||
|
0.001,
|
||||||
|
0.001
|
||||||
|
],
|
||||||
|
"description": "Width and height of the sample box."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "manualReference",
|
||||||
|
"label": "Manual Reference",
|
||||||
|
"type": "color",
|
||||||
|
"default": [
|
||||||
|
1.0,
|
||||||
|
1.0,
|
||||||
|
1.0,
|
||||||
|
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": "Manual reference color used when Reference Source is set to Manual Color."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "overlayOpacity",
|
||||||
|
"label": "Overlay Opacity",
|
||||||
|
"type": "float",
|
||||||
|
"default": 0.9,
|
||||||
|
"min": 0.0,
|
||||||
|
"max": 1.0,
|
||||||
|
"step": 0.01,
|
||||||
|
"description": "Strength of the swatch and box overlay."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "differenceGain",
|
||||||
|
"label": "Difference Gain",
|
||||||
|
"type": "float",
|
||||||
|
"default": 2.0,
|
||||||
|
"min": 0.0,
|
||||||
|
"max": 8.0,
|
||||||
|
"step": 0.01,
|
||||||
|
"description": "Scales the displayed reference-vs-current difference."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
235
shaders/white-match-probe/shader.slang
Normal file
235
shaders/white-match-probe/shader.slang
Normal file
@@ -0,0 +1,235 @@
|
|||||||
|
static const int kReferenceCellIndex = 0;
|
||||||
|
static const int kMetadataCellIndex = 1;
|
||||||
|
|
||||||
|
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));
|
||||||
|
}
|
||||||
|
|
||||||
|
float3 sampleProbeAverage(ShaderContext context)
|
||||||
|
{
|
||||||
|
float2 clampedSize = clamp(sampleSize, float2(0.001, 0.001), float2(1.0, 1.0));
|
||||||
|
float2 halfSize = clampedSize * 0.5;
|
||||||
|
float2 minUv = clamp(sampleCenter - halfSize, float2(0.0, 0.0), float2(1.0, 1.0));
|
||||||
|
float2 maxUv = clamp(sampleCenter + halfSize, float2(0.0, 0.0), float2(1.0, 1.0));
|
||||||
|
|
||||||
|
float3 total = float3(0.0, 0.0, 0.0);
|
||||||
|
float weight = 0.0;
|
||||||
|
for (int y = 0; y < 3; ++y)
|
||||||
|
{
|
||||||
|
for (int x = 0; x < 3; ++x)
|
||||||
|
{
|
||||||
|
float2 t = float2((float(x) + 0.5) / 3.0, (float(y) + 0.5) / 3.0);
|
||||||
|
float2 uv = lerp(minUv, maxUv, t);
|
||||||
|
total += sampleLayerInput(uv).rgb;
|
||||||
|
weight += 1.0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return total / max(weight, 0.0001);
|
||||||
|
}
|
||||||
|
|
||||||
|
float4 storeReferenceState(ShaderContext context)
|
||||||
|
{
|
||||||
|
float2 pixelCoord = floor(context.uv * context.outputResolution);
|
||||||
|
float3 currentSample = sampleProbeAverage(context);
|
||||||
|
|
||||||
|
float previousTriggerCount = context.feedbackAvailable > 0
|
||||||
|
? readStoredCell(context, kMetadataCellIndex).r
|
||||||
|
: -1.0;
|
||||||
|
float currentTriggerCount = float(captureReference);
|
||||||
|
bool captureNow = context.feedbackAvailable <= 0 || currentTriggerCount > previousTriggerCount + 0.5;
|
||||||
|
|
||||||
|
float3 storedReference = context.feedbackAvailable > 0
|
||||||
|
? readStoredCell(context, kReferenceCellIndex).rgb
|
||||||
|
: currentSample;
|
||||||
|
if (captureNow)
|
||||||
|
storedReference = currentSample;
|
||||||
|
|
||||||
|
float4 metadata = float4(currentTriggerCount, captureReferenceTime, 0.0, 1.0);
|
||||||
|
if (!captureNow && context.feedbackAvailable > 0)
|
||||||
|
metadata = readStoredCell(context, kMetadataCellIndex);
|
||||||
|
|
||||||
|
if (pixelIsInsideCell(pixelCoord, kReferenceCellIndex))
|
||||||
|
return float4(storedReference, 1.0);
|
||||||
|
if (pixelIsInsideCell(pixelCoord, kMetadataCellIndex))
|
||||||
|
return metadata;
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
float luminance(float3 color)
|
||||||
|
{
|
||||||
|
return dot(color, float3(0.2126, 0.7152, 0.0722));
|
||||||
|
}
|
||||||
|
|
||||||
|
float3 activeReferenceColor(float3 capturedReference)
|
||||||
|
{
|
||||||
|
// Enum parameters are exposed as their zero-based option index.
|
||||||
|
// 0 = captured sample, 1 = manual color.
|
||||||
|
return referenceSource == 1 ? manualReference.rgb : capturedReference;
|
||||||
|
}
|
||||||
|
|
||||||
|
float4 displayReferenceCompare(ShaderContext context)
|
||||||
|
{
|
||||||
|
float3 liveColor = sampleLayerInput(context.uv).rgb;
|
||||||
|
float3 currentSample = sampleProbeAverage(context);
|
||||||
|
float3 capturedReference = sampleVideo(cellCenterUvForIndex(context, kReferenceCellIndex)).rgb;
|
||||||
|
float3 storedReference = activeReferenceColor(capturedReference);
|
||||||
|
float3 delta = currentSample - storedReference;
|
||||||
|
float3 absoluteDelta = abs(delta);
|
||||||
|
float differenceMagnitude = max(absoluteDelta.r, max(absoluteDelta.g, absoluteDelta.b));
|
||||||
|
|
||||||
|
float3 displayColor = liveColor;
|
||||||
|
float opacity = saturate(overlayOpacity);
|
||||||
|
|
||||||
|
float2 halfSize = clamp(sampleSize, float2(0.001, 0.001), float2(1.0, 1.0)) * 0.5;
|
||||||
|
float2 boxMin = clamp(sampleCenter - halfSize, float2(0.0, 0.0), float2(1.0, 1.0));
|
||||||
|
float2 boxMax = clamp(sampleCenter + halfSize, float2(0.0, 0.0), float2(1.0, 1.0));
|
||||||
|
float pixelThickness = 2.0 / max(min(context.outputResolution.x, context.outputResolution.y), 1.0);
|
||||||
|
float outerOutline = borderMask(context.uv, boxMin - pixelThickness, boxMax + pixelThickness, pixelThickness);
|
||||||
|
float innerOutline = borderMask(context.uv, boxMin, boxMax, pixelThickness);
|
||||||
|
if (outerOutline > 0.5)
|
||||||
|
displayColor = float3(0.0, 0.0, 0.0);
|
||||||
|
if (innerOutline > 0.5)
|
||||||
|
displayColor = lerp(displayColor, float3(1.0, 0.0, 0.0), opacity);
|
||||||
|
|
||||||
|
float2 swatchSize = float2(0.06, 0.07);
|
||||||
|
float2 panelOrigin = float2(0.03, 0.04);
|
||||||
|
float2 gap = float2(0.075, 0.0);
|
||||||
|
float2 refMin = panelOrigin;
|
||||||
|
float2 curMin = panelOrigin + gap;
|
||||||
|
float2 diffMin = panelOrigin + gap * 2.0;
|
||||||
|
float2 refMax = refMin + swatchSize;
|
||||||
|
float2 curMax = curMin + swatchSize;
|
||||||
|
float2 diffMax = diffMin + swatchSize;
|
||||||
|
float swatchBorder = min(swatchSize.x, swatchSize.y) * 0.08;
|
||||||
|
|
||||||
|
float refFill = rectMask(context.uv, refMin, refMax);
|
||||||
|
float curFill = rectMask(context.uv, curMin, curMax);
|
||||||
|
float diffFill = rectMask(context.uv, diffMin, diffMax);
|
||||||
|
float refOutline = borderMask(context.uv, refMin, refMax, swatchBorder);
|
||||||
|
float curOutline = borderMask(context.uv, curMin, curMax, swatchBorder);
|
||||||
|
float diffOutline = borderMask(context.uv, diffMin, diffMax, swatchBorder);
|
||||||
|
|
||||||
|
if (refFill > 0.5)
|
||||||
|
displayColor = storedReference;
|
||||||
|
if (curFill > 0.5)
|
||||||
|
displayColor = currentSample;
|
||||||
|
if (diffFill > 0.5)
|
||||||
|
{
|
||||||
|
float3 neutralBase = float3(0.5, 0.5, 0.5);
|
||||||
|
float3 signedDeltaDisplay = saturate(neutralBase + delta * max(differenceGain, 0.0) * 0.5);
|
||||||
|
displayColor = signedDeltaDisplay;
|
||||||
|
}
|
||||||
|
if (refOutline > 0.5 || curOutline > 0.5 || diffOutline > 0.5)
|
||||||
|
displayColor = float3(0.0, 0.0, 0.0);
|
||||||
|
|
||||||
|
// Approximate the difference in two operator-friendly axes:
|
||||||
|
// warm/cool leans red versus blue, and green/magenta leans green versus
|
||||||
|
// the average of red and blue. Centered bars make "match" obvious.
|
||||||
|
float warmCool = clamp((delta.r - delta.b) * max(differenceGain, 0.0), -1.0, 1.0);
|
||||||
|
float greenMagenta = clamp((delta.g - (delta.r + delta.b) * 0.5) * max(differenceGain, 0.0), -1.0, 1.0);
|
||||||
|
float brightnessDelta = clamp((luminance(currentSample) - luminance(storedReference)) * max(differenceGain, 0.0) * 1.5, -1.0, 1.0);
|
||||||
|
|
||||||
|
float barWidth = 0.18;
|
||||||
|
float barHeight = 0.018;
|
||||||
|
float halfBarWidth = barWidth * 0.5;
|
||||||
|
float2 warmCoolMin = float2(0.03, 0.13);
|
||||||
|
float2 warmCoolMax = warmCoolMin + float2(barWidth, barHeight);
|
||||||
|
float2 tintMin = float2(0.03, 0.157);
|
||||||
|
float2 tintMax = tintMin + float2(barWidth, barHeight);
|
||||||
|
float2 brightnessMin = float2(0.03, 0.184);
|
||||||
|
float2 brightnessMax = brightnessMin + float2(barWidth, barHeight);
|
||||||
|
float centerX = warmCoolMin.x + halfBarWidth;
|
||||||
|
|
||||||
|
float warmCoolFill = rectMask(context.uv, warmCoolMin, warmCoolMax);
|
||||||
|
float tintFill = rectMask(context.uv, tintMin, tintMax);
|
||||||
|
float brightnessFill = rectMask(context.uv, brightnessMin, brightnessMax);
|
||||||
|
float warmCoolOutline = borderMask(context.uv, warmCoolMin, warmCoolMax, barHeight * 0.12);
|
||||||
|
float tintOutline = borderMask(context.uv, tintMin, tintMax, barHeight * 0.12);
|
||||||
|
float brightnessOutline = borderMask(context.uv, brightnessMin, brightnessMax, barHeight * 0.12);
|
||||||
|
float targetHalfWidth = 0.0015;
|
||||||
|
float warmCoolCenter = rectMask(context.uv, float2(centerX - targetHalfWidth, warmCoolMin.y), float2(centerX + targetHalfWidth, warmCoolMax.y));
|
||||||
|
float tintCenter = rectMask(context.uv, float2(centerX - targetHalfWidth, tintMin.y), float2(centerX + targetHalfWidth, tintMax.y));
|
||||||
|
float brightnessCenter = rectMask(context.uv, float2(centerX - targetHalfWidth, brightnessMin.y), float2(centerX + targetHalfWidth, brightnessMax.y));
|
||||||
|
|
||||||
|
float warmCoolPosition = centerX + warmCool * halfBarWidth;
|
||||||
|
float tintPosition = centerX + greenMagenta * halfBarWidth;
|
||||||
|
float brightnessPosition = centerX + brightnessDelta * halfBarWidth;
|
||||||
|
float indicatorHalfWidth = 0.0018;
|
||||||
|
float warmCoolIndicator = rectMask(context.uv, float2(warmCoolPosition - indicatorHalfWidth, warmCoolMin.y), float2(warmCoolPosition + indicatorHalfWidth, warmCoolMax.y));
|
||||||
|
float tintIndicator = rectMask(context.uv, float2(tintPosition - indicatorHalfWidth, tintMin.y), float2(tintPosition + indicatorHalfWidth, tintMax.y));
|
||||||
|
float brightnessIndicator = rectMask(context.uv, float2(brightnessPosition - indicatorHalfWidth, brightnessMin.y), float2(brightnessPosition + indicatorHalfWidth, brightnessMax.y));
|
||||||
|
|
||||||
|
if (warmCoolFill > 0.5)
|
||||||
|
{
|
||||||
|
float gradientT = saturate((context.uv.x - warmCoolMin.x) / max(barWidth, 0.0001));
|
||||||
|
float3 coolColor = float3(0.18, 0.52, 1.0);
|
||||||
|
float3 warmColor = float3(1.0, 0.48, 0.10);
|
||||||
|
float3 gradientColor = gradientT <= 0.5
|
||||||
|
? lerp(coolColor, float3(1.0, 1.0, 1.0), gradientT * 2.0)
|
||||||
|
: lerp(float3(1.0, 1.0, 1.0), warmColor, (gradientT - 0.5) * 2.0);
|
||||||
|
displayColor = gradientColor;
|
||||||
|
}
|
||||||
|
if (tintFill > 0.5)
|
||||||
|
{
|
||||||
|
float gradientT = saturate((context.uv.x - tintMin.x) / max(barWidth, 0.0001));
|
||||||
|
float3 magentaColor = float3(1.0, 0.25, 0.75);
|
||||||
|
float3 greenColor = float3(0.18, 0.92, 0.32);
|
||||||
|
float3 gradientColor = gradientT <= 0.5
|
||||||
|
? lerp(magentaColor, float3(1.0, 1.0, 1.0), gradientT * 2.0)
|
||||||
|
: lerp(float3(1.0, 1.0, 1.0), greenColor, (gradientT - 0.5) * 2.0);
|
||||||
|
displayColor = gradientColor;
|
||||||
|
}
|
||||||
|
if (brightnessFill > 0.5)
|
||||||
|
{
|
||||||
|
float gradientT = saturate((context.uv.x - brightnessMin.x) / max(barWidth, 0.0001));
|
||||||
|
float3 darkColor = float3(0.18, 0.18, 0.18);
|
||||||
|
float3 gradientColor = lerp(darkColor, float3(1.0, 1.0, 1.0), gradientT);
|
||||||
|
displayColor = gradientColor;
|
||||||
|
}
|
||||||
|
if (warmCoolCenter > 0.5 || tintCenter > 0.5 || brightnessCenter > 0.5)
|
||||||
|
displayColor = float3(0.12, 0.45, 1.0);
|
||||||
|
if (warmCoolIndicator > 0.5 || tintIndicator > 0.5 || brightnessIndicator > 0.5)
|
||||||
|
displayColor = float3(1.0, 0.0, 0.0);
|
||||||
|
if (warmCoolOutline > 0.5 || tintOutline > 0.5 || brightnessOutline > 0.5)
|
||||||
|
displayColor = float3(0.0, 0.0, 0.0);
|
||||||
|
|
||||||
|
return float4(saturate(displayColor), 1.0);
|
||||||
|
}
|
||||||
@@ -74,6 +74,11 @@ struct OscServerTestAccess
|
|||||||
return server.DispatchMessage(message, error);
|
return server.DispatchMessage(message, error);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static bool TryParseBindAddress(const std::string& bindAddress, in_addr& address, std::string& error)
|
||||||
|
{
|
||||||
|
return OscServer::TryParseBindAddress(bindAddress, address, error);
|
||||||
|
}
|
||||||
|
|
||||||
static void SetUpdateParameterCallback(
|
static void SetUpdateParameterCallback(
|
||||||
OscServer& server,
|
OscServer& server,
|
||||||
const std::function<bool(const std::string&, const std::string&, const std::string&, std::string&)>& callback)
|
const std::function<bool(const std::string&, const std::string&, const std::string&, std::string&)>& callback)
|
||||||
@@ -191,6 +196,23 @@ void TestRejectsUnsupportedAddress()
|
|||||||
Expect(!called, "unsupported OSC namespace does not invoke callback");
|
Expect(!called, "unsupported OSC namespace does not invoke callback");
|
||||||
Expect(!error.empty(), "unsupported OSC address reports an error");
|
Expect(!error.empty(), "unsupported OSC address reports an error");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void TestParsesOscBindAddress()
|
||||||
|
{
|
||||||
|
in_addr loopback = {};
|
||||||
|
std::string error;
|
||||||
|
Expect(OscServerTestAccess::TryParseBindAddress("127.0.0.1", loopback, error), "loopback OSC bind address parses");
|
||||||
|
Expect(loopback.S_un.S_addr != 0, "loopback OSC bind address produces a socket address");
|
||||||
|
|
||||||
|
in_addr wildcard = {};
|
||||||
|
error.clear();
|
||||||
|
Expect(OscServerTestAccess::TryParseBindAddress("0.0.0.0", wildcard, error), "wildcard OSC bind address parses");
|
||||||
|
|
||||||
|
in_addr invalid = {};
|
||||||
|
error.clear();
|
||||||
|
Expect(!OscServerTestAccess::TryParseBindAddress("localhost", invalid, error), "hostname OSC bind address is rejected");
|
||||||
|
Expect(!error.empty(), "invalid OSC bind address reports an error");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
int main()
|
int main()
|
||||||
@@ -201,6 +223,7 @@ int main()
|
|||||||
TestDecodeIntStringAndBoolMessages();
|
TestDecodeIntStringAndBoolMessages();
|
||||||
TestDispatchValidAddress();
|
TestDispatchValidAddress();
|
||||||
TestRejectsUnsupportedAddress();
|
TestRejectsUnsupportedAddress();
|
||||||
|
TestParsesOscBindAddress();
|
||||||
|
|
||||||
if (gFailures != 0)
|
if (gFailures != 0)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,67 +0,0 @@
|
|||||||
#include "RuntimeHost.h"
|
|
||||||
|
|
||||||
#include <iostream>
|
|
||||||
|
|
||||||
namespace
|
|
||||||
{
|
|
||||||
int gFailures = 0;
|
|
||||||
|
|
||||||
void Expect(bool condition, const char* message)
|
|
||||||
{
|
|
||||||
if (condition)
|
|
||||||
return;
|
|
||||||
|
|
||||||
std::cerr << "FAIL: " << message << "\n";
|
|
||||||
++gFailures;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
int main()
|
|
||||||
{
|
|
||||||
RuntimeHost runtimeHost;
|
|
||||||
std::string error;
|
|
||||||
Expect(runtimeHost.Initialize(error), "runtime host initializes");
|
|
||||||
Expect(error.empty(), "runtime host initialization does not report an error");
|
|
||||||
|
|
||||||
VideoIOState state;
|
|
||||||
state.backendId = VideoIOBackendId::DeckLink;
|
|
||||||
state.deviceName = "Test Device";
|
|
||||||
state.hasInputDevice = true;
|
|
||||||
state.hasInputSource = true;
|
|
||||||
state.inputDisplayModeName = "fake input";
|
|
||||||
state.outputDisplayModeName = "fake output";
|
|
||||||
state.capabilities.supportsInternalKeying = true;
|
|
||||||
state.capabilities.supportsExternalKeying = true;
|
|
||||||
state.capabilities.keyerInterfaceAvailable = true;
|
|
||||||
state.externalKeyingRequested = true;
|
|
||||||
state.externalKeyingActive = true;
|
|
||||||
state.statusMessage = "ready";
|
|
||||||
state.formatStatusMessage = "using fake formats";
|
|
||||||
runtimeHost.SetVideoIOStatus(state);
|
|
||||||
|
|
||||||
JsonValue root;
|
|
||||||
Expect(ParseJson(runtimeHost.BuildStateJson(), root, error), "runtime state json parses");
|
|
||||||
Expect(root.find("videoIO") != nullptr, "runtime state exposes videoIO");
|
|
||||||
Expect(root.find("decklink") == nullptr, "runtime state no longer exposes a decklink top-level block");
|
|
||||||
|
|
||||||
const JsonValue* app = root.find("app");
|
|
||||||
Expect(app != nullptr, "runtime state exposes app settings");
|
|
||||||
Expect(app != nullptr && app->find("videoBackend") != nullptr, "app settings expose videoBackend");
|
|
||||||
Expect(app != nullptr && app->find("videoBackend")->asString() == "decklink", "videoBackend serializes as decklink");
|
|
||||||
|
|
||||||
const JsonValue* videoIO = root.find("videoIO");
|
|
||||||
Expect(videoIO != nullptr && videoIO->find("backend") != nullptr, "videoIO exposes backend");
|
|
||||||
Expect(videoIO != nullptr && videoIO->find("backend")->asString() == "decklink", "videoIO backend serializes as decklink");
|
|
||||||
Expect(videoIO != nullptr && videoIO->find("deviceName") != nullptr, "videoIO exposes device name");
|
|
||||||
Expect(videoIO != nullptr && videoIO->find("deviceName")->asString() == "Test Device", "videoIO device name matches");
|
|
||||||
Expect(videoIO != nullptr && videoIO->find("capabilities") != nullptr, "videoIO exposes capabilities");
|
|
||||||
|
|
||||||
if (gFailures != 0)
|
|
||||||
{
|
|
||||||
std::cerr << gFailures << " RuntimeHost video I/O state test failure(s).\n";
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
std::cout << "RuntimeHost video I/O state tests passed.\n";
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
@@ -58,6 +58,7 @@ void TestValidManifest()
|
|||||||
"textures": [{ "id": "maskTex", "path": "mask.png" }],
|
"textures": [{ "id": "maskTex", "path": "mask.png" }],
|
||||||
"fonts": [{ "id": "inter", "path": "Inter.ttf" }],
|
"fonts": [{ "id": "inter", "path": "Inter.ttf" }],
|
||||||
"temporal": { "enabled": true, "historySource": "source", "historyLength": 8 },
|
"temporal": { "enabled": true, "historySource": "source", "historyLength": 8 },
|
||||||
|
"feedback": { "enabled": true },
|
||||||
"parameters": [
|
"parameters": [
|
||||||
{ "id": "gain", "label": "Gain", "description": "Scales the output intensity.", "type": "float", "default": 0.5, "min": 0, "max": 1 },
|
{ "id": "gain", "label": "Gain", "description": "Scales the output intensity.", "type": "float", "default": 0.5, "min": 0, "max": 1 },
|
||||||
{ "id": "titleText", "label": "Title", "type": "text", "default": "LIVE", "font": "inter", "maxLength": 32 },
|
{ "id": "titleText", "label": "Title", "type": "text", "default": "LIVE", "font": "inter", "maxLength": 32 },
|
||||||
@@ -77,6 +78,7 @@ void TestValidManifest()
|
|||||||
Expect(package.textureAssets.size() == 1 && package.textureAssets[0].id == "maskTex", "texture assets parse");
|
Expect(package.textureAssets.size() == 1 && package.textureAssets[0].id == "maskTex", "texture assets parse");
|
||||||
Expect(package.fontAssets.size() == 1 && package.fontAssets[0].id == "inter", "font assets parse");
|
Expect(package.fontAssets.size() == 1 && package.fontAssets[0].id == "inter", "font assets parse");
|
||||||
Expect(package.temporal.enabled && package.temporal.effectiveHistoryLength == 4, "temporal history is capped");
|
Expect(package.temporal.enabled && package.temporal.effectiveHistoryLength == 4, "temporal history is capped");
|
||||||
|
Expect(package.feedback.enabled && package.feedback.writePassId == "main", "feedback defaults to the implicit main pass");
|
||||||
Expect(package.parameters.size() == 4, "parameters parse");
|
Expect(package.parameters.size() == 4, "parameters parse");
|
||||||
Expect(package.parameters[0].description == "Scales the output intensity.", "parameter descriptions parse");
|
Expect(package.parameters[0].description == "Scales the output intensity.", "parameter descriptions parse");
|
||||||
Expect(package.parameters[1].type == ShaderParameterType::Text && package.parameters[1].defaultTextValue == "LIVE", "text parameter parses");
|
Expect(package.parameters[1].type == ShaderParameterType::Text && package.parameters[1].defaultTextValue == "LIVE", "text parameter parses");
|
||||||
@@ -96,6 +98,7 @@ void TestExplicitPassManifest()
|
|||||||
{ "id": "blurX", "source": "blur-x.slang", "entryPoint": "blurHorizontal", "inputs": ["layerInput"], "output": "blurredX" },
|
{ "id": "blurX", "source": "blur-x.slang", "entryPoint": "blurHorizontal", "inputs": ["layerInput"], "output": "blurredX" },
|
||||||
{ "id": "final", "source": "final.slang", "entryPoint": "finish", "inputs": ["blurredX"], "output": "layerOutput" }
|
{ "id": "final", "source": "final.slang", "entryPoint": "finish", "inputs": ["blurredX"], "output": "layerOutput" }
|
||||||
],
|
],
|
||||||
|
"feedback": { "enabled": true, "writePass": "blurX" },
|
||||||
"parameters": []
|
"parameters": []
|
||||||
})");
|
})");
|
||||||
WriteFile(root / "multi" / "blur-x.slang", "float4 blurHorizontal(float2 uv) { return float4(uv, 0.0, 1.0); }\n");
|
WriteFile(root / "multi" / "blur-x.slang", "float4 blurHorizontal(float2 uv) { return float4(uv, 0.0, 1.0); }\n");
|
||||||
@@ -109,6 +112,7 @@ void TestExplicitPassManifest()
|
|||||||
Expect(package.passes[0].id == "blurX" && package.passes[0].entryPoint == "blurHorizontal", "first pass metadata parses");
|
Expect(package.passes[0].id == "blurX" && package.passes[0].entryPoint == "blurHorizontal", "first pass metadata parses");
|
||||||
Expect(package.passes[0].inputNames.size() == 1 && package.passes[0].inputNames[0] == "layerInput", "pass inputs parse");
|
Expect(package.passes[0].inputNames.size() == 1 && package.passes[0].inputNames[0] == "layerInput", "pass inputs parse");
|
||||||
Expect(package.passes[1].outputName == "layerOutput", "pass output parses");
|
Expect(package.passes[1].outputName == "layerOutput", "pass output parses");
|
||||||
|
Expect(package.feedback.enabled && package.feedback.writePassId == "blurX", "explicit feedback write pass parses");
|
||||||
|
|
||||||
std::filesystem::remove_all(root);
|
std::filesystem::remove_all(root);
|
||||||
}
|
}
|
||||||
@@ -190,6 +194,25 @@ void TestInvalidTemporalSettings()
|
|||||||
std::filesystem::remove_all(root);
|
std::filesystem::remove_all(root);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void TestInvalidFeedbackSettings()
|
||||||
|
{
|
||||||
|
const std::filesystem::path root = MakeTestRoot();
|
||||||
|
WriteShaderPackage(root, "bad-feedback", R"({
|
||||||
|
"id": "bad-feedback",
|
||||||
|
"name": "Bad Feedback",
|
||||||
|
"feedback": { "enabled": true, "writePass": "missingPass" },
|
||||||
|
"parameters": []
|
||||||
|
})");
|
||||||
|
|
||||||
|
ShaderPackageRegistry registry(4);
|
||||||
|
ShaderPackage package;
|
||||||
|
std::string error;
|
||||||
|
Expect(!registry.ParseManifest(root / "bad-feedback" / "shader.json", package, error), "invalid feedback manifest is rejected");
|
||||||
|
Expect(error.find("writePass") != std::string::npos, "invalid feedback error names writePass");
|
||||||
|
|
||||||
|
std::filesystem::remove_all(root);
|
||||||
|
}
|
||||||
|
|
||||||
void TestDisabledTemporalSettingsAreIgnored()
|
void TestDisabledTemporalSettingsAreIgnored()
|
||||||
{
|
{
|
||||||
const std::filesystem::path root = MakeTestRoot();
|
const std::filesystem::path root = MakeTestRoot();
|
||||||
@@ -264,6 +287,7 @@ int main()
|
|||||||
TestMissingFontAsset();
|
TestMissingFontAsset();
|
||||||
TestInvalidManifest();
|
TestInvalidManifest();
|
||||||
TestInvalidTemporalSettings();
|
TestInvalidTemporalSettings();
|
||||||
|
TestInvalidFeedbackSettings();
|
||||||
TestDisabledTemporalSettingsAreIgnored();
|
TestDisabledTemporalSettingsAreIgnored();
|
||||||
TestDuplicateScan();
|
TestDuplicateScan();
|
||||||
TestInvalidPackageDoesNotFailScan();
|
TestInvalidPackageDoesNotFailScan();
|
||||||
|
|||||||
@@ -1,41 +0,0 @@
|
|||||||
#include "VideoIOBackendFactory.h"
|
|
||||||
#include "VideoIOTypes.h"
|
|
||||||
|
|
||||||
#include <iostream>
|
|
||||||
|
|
||||||
namespace
|
|
||||||
{
|
|
||||||
int gFailures = 0;
|
|
||||||
|
|
||||||
void Expect(bool condition, const char* message)
|
|
||||||
{
|
|
||||||
if (condition)
|
|
||||||
return;
|
|
||||||
|
|
||||||
std::cerr << "FAIL: " << message << "\n";
|
|
||||||
++gFailures;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
int main()
|
|
||||||
{
|
|
||||||
std::string error;
|
|
||||||
std::unique_ptr<VideoIODevice> device = CreateVideoIODevice(VideoIOBackendId::DeckLink, error);
|
|
||||||
Expect(device != nullptr, "decklink backend factory returns a device");
|
|
||||||
Expect(!device || device->BackendId() == VideoIOBackendId::DeckLink, "decklink backend reports decklink id");
|
|
||||||
Expect(error.empty(), "supported backend does not produce an error");
|
|
||||||
|
|
||||||
error.clear();
|
|
||||||
device = CreateVideoIODevice(static_cast<VideoIOBackendId>(999), error);
|
|
||||||
Expect(device == nullptr, "unknown backend id is rejected");
|
|
||||||
Expect(!error.empty(), "unknown backend reports an error");
|
|
||||||
|
|
||||||
if (gFailures != 0)
|
|
||||||
{
|
|
||||||
std::cerr << gFailures << " VideoIO backend factory test failure(s).\n";
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
std::cout << "VideoIO backend factory tests passed.\n";
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
@@ -19,26 +19,20 @@ void Expect(bool condition, const char* message)
|
|||||||
class FakeVideoIODevice : public VideoIODevice
|
class FakeVideoIODevice : public VideoIODevice
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
VideoIOBackendId BackendId() const override { return VideoIOBackendId::DeckLink; }
|
|
||||||
void ReleaseResources() override {}
|
void ReleaseResources() override {}
|
||||||
|
|
||||||
bool DiscoverDevicesAndModes(const VideoIOConfiguration&, std::string&) override
|
bool DiscoverDevicesAndModes(const VideoFormatSelection&, std::string&) override
|
||||||
{
|
{
|
||||||
mState.backendId = BackendId();
|
|
||||||
mState.inputFrameSize = { 1920, 1080 };
|
mState.inputFrameSize = { 1920, 1080 };
|
||||||
mState.outputFrameSize = { 1920, 1080 };
|
mState.outputFrameSize = { 1920, 1080 };
|
||||||
mState.inputDisplayModeName = "fake 1080p";
|
mState.inputDisplayModeName = "fake 1080p";
|
||||||
mState.outputDisplayModeName = "fake 1080p";
|
mState.outputModelName = "Fake Video IO";
|
||||||
mState.deviceName = "Fake Video IO";
|
|
||||||
mState.capabilities.supportsInternalKeying = true;
|
|
||||||
mState.capabilities.supportsExternalKeying = true;
|
|
||||||
mState.hasInputDevice = true;
|
mState.hasInputDevice = true;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool SelectPreferredFormats(const VideoIOConfiguration& config, std::string&) override
|
bool SelectPreferredFormats(const VideoFormatSelection&, bool, std::string&) override
|
||||||
{
|
{
|
||||||
mState.externalKeyingRequested = config.externalKeyingEnabled;
|
|
||||||
mState.inputPixelFormat = VideoIOPixelFormat::Uyvy8;
|
mState.inputPixelFormat = VideoIOPixelFormat::Uyvy8;
|
||||||
mState.outputPixelFormat = VideoIOPixelFormat::Bgra8;
|
mState.outputPixelFormat = VideoIOPixelFormat::Bgra8;
|
||||||
mState.inputFrameRowBytes = VideoIORowBytes(mState.inputPixelFormat, mState.inputFrameSize.width);
|
mState.inputFrameRowBytes = VideoIORowBytes(mState.inputPixelFormat, mState.inputFrameSize.width);
|
||||||
@@ -48,13 +42,13 @@ public:
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool ConfigureInput(InputFrameCallback callback, std::string&) override
|
bool ConfigureInput(InputFrameCallback callback, const VideoFormat&, std::string&) override
|
||||||
{
|
{
|
||||||
mInputCallback = callback;
|
mInputCallback = callback;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool ConfigureOutput(OutputFrameCallback callback, std::string&) override
|
bool ConfigureOutput(OutputFrameCallback callback, const VideoFormat&, bool, std::string&) override
|
||||||
{
|
{
|
||||||
mOutputCallback = callback;
|
mOutputCallback = callback;
|
||||||
return true;
|
return true;
|
||||||
@@ -120,19 +114,19 @@ private:
|
|||||||
int main()
|
int main()
|
||||||
{
|
{
|
||||||
FakeVideoIODevice device;
|
FakeVideoIODevice device;
|
||||||
VideoIOConfiguration config;
|
VideoFormatSelection selection;
|
||||||
std::string error;
|
std::string error;
|
||||||
bool inputSeen = false;
|
bool inputSeen = false;
|
||||||
bool outputSeen = false;
|
bool outputSeen = false;
|
||||||
|
|
||||||
Expect(device.DiscoverDevicesAndModes(config, error), "fake discovery succeeds");
|
Expect(device.DiscoverDevicesAndModes(selection, error), "fake discovery succeeds");
|
||||||
Expect(device.SelectPreferredFormats(config, error), "fake format selection succeeds");
|
Expect(device.SelectPreferredFormats(selection, false, error), "fake format selection succeeds");
|
||||||
Expect(device.ConfigureInput([&](const VideoIOFrame& frame) {
|
Expect(device.ConfigureInput([&](const VideoIOFrame& frame) {
|
||||||
inputSeen = frame.bytes != nullptr && frame.width == 1920 && frame.pixelFormat == VideoIOPixelFormat::Uyvy8;
|
inputSeen = frame.bytes != nullptr && frame.width == 1920 && frame.pixelFormat == VideoIOPixelFormat::Uyvy8;
|
||||||
}, error), "fake input config succeeds");
|
}, selection.input, error), "fake input config succeeds");
|
||||||
Expect(device.ConfigureOutput([&](const VideoIOCompletion& completion) {
|
Expect(device.ConfigureOutput([&](const VideoIOCompletion& completion) {
|
||||||
outputSeen = completion.result == VideoIOCompletionResult::Completed;
|
outputSeen = completion.result == VideoIOCompletionResult::Completed;
|
||||||
}, error), "fake output config succeeds");
|
}, selection.output, false, error), "fake output config succeeds");
|
||||||
Expect(device.Start(), "fake device starts");
|
Expect(device.Start(), "fake device starts");
|
||||||
|
|
||||||
VideoIOOutputFrame outputFrame;
|
VideoIOOutputFrame outputFrame;
|
||||||
|
|||||||
@@ -11,6 +11,12 @@ export function StackPresetToolbar({
|
|||||||
onSelectedPresetNameChange,
|
onSelectedPresetNameChange,
|
||||||
}) {
|
}) {
|
||||||
const [screenshotQueued, setScreenshotQueued] = useState(false);
|
const [screenshotQueued, setScreenshotQueued] = useState(false);
|
||||||
|
const trimmedPresetName = presetName.trim();
|
||||||
|
const normalizedPresetName = trimmedPresetName
|
||||||
|
.toLowerCase()
|
||||||
|
.replace(/[^a-z0-9]+/g, "-")
|
||||||
|
.replace(/^-+|-+$/g, "");
|
||||||
|
const willOverwrite = normalizedPresetName ? stackPresets.includes(normalizedPresetName) : false;
|
||||||
|
|
||||||
async function requestScreenshot() {
|
async function requestScreenshot() {
|
||||||
setScreenshotQueued(true);
|
setScreenshotQueued(true);
|
||||||
@@ -61,23 +67,34 @@ export function StackPresetToolbar({
|
|||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="button-with-icon"
|
className={`button-with-icon${willOverwrite ? " stack-panel__save--overwrite" : ""}`}
|
||||||
disabled={!presetName.trim()}
|
disabled={!trimmedPresetName}
|
||||||
onClick={() => {
|
onClick={async () => {
|
||||||
const trimmedName = presetName.trim();
|
if (!trimmedPresetName) {
|
||||||
if (!trimmedName) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
postJson("/api/stack-presets/save", { presetName: trimmedName });
|
const response = await postJson("/api/stack-presets/save", { presetName: trimmedPresetName });
|
||||||
onSelectedPresetNameChange(
|
if (response?.ok) {
|
||||||
trimmedName.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, ""),
|
onSelectedPresetNameChange(normalizedPresetName);
|
||||||
);
|
onPresetNameChange("");
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Save size={16} strokeWidth={1.9} aria-hidden="true" />
|
<Save size={16} strokeWidth={1.9} aria-hidden="true" />
|
||||||
<span>Save</span>
|
<span>{willOverwrite ? "Overwrite" : "Save"}</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
{trimmedPresetName ? (
|
||||||
|
<p className="muted toolbar__status" role="status">
|
||||||
|
{willOverwrite
|
||||||
|
? `This will overwrite the existing preset "${normalizedPresetName}".`
|
||||||
|
: `This will save as "${normalizedPresetName}".`}
|
||||||
|
</p>
|
||||||
|
) : (
|
||||||
|
<p className="muted toolbar__status toolbar__status--placeholder" aria-hidden="true">
|
||||||
|
Preset status
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="toolbar__group">
|
<div className="toolbar__group">
|
||||||
@@ -109,6 +126,9 @@ export function StackPresetToolbar({
|
|||||||
<span>Recall</span>
|
<span>Recall</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
<p className="muted toolbar__status toolbar__status--placeholder" aria-hidden="true">
|
||||||
|
Preset status
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -34,6 +34,27 @@ export function useThrottledParameterValue(parameter, onParameterChange) {
|
|||||||
}
|
}
|
||||||
}, [draftValue, currentValue]);
|
}, [draftValue, currentValue]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isInteractingRef.current) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (valuesMatch(currentValue, latestDraftRef.current)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pendingTimeoutRef.current) {
|
||||||
|
clearTimeout(pendingTimeoutRef.current);
|
||||||
|
pendingTimeoutRef.current = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
setDraftValue(currentValue);
|
||||||
|
setAppliedValue(currentValue);
|
||||||
|
latestDraftRef.current = currentValue;
|
||||||
|
isDirtyRef.current = false;
|
||||||
|
lastSentAtRef.current = 0;
|
||||||
|
}, [currentValue]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
return () => {
|
return () => {
|
||||||
if (pendingTimeoutRef.current) {
|
if (pendingTimeoutRef.current) {
|
||||||
|
|||||||
@@ -515,19 +515,46 @@ pre {
|
|||||||
|
|
||||||
.toolbar__group {
|
.toolbar__group {
|
||||||
display: grid;
|
display: grid;
|
||||||
|
grid-template-rows: auto auto auto;
|
||||||
|
align-content: start;
|
||||||
gap: 0.5rem;
|
gap: 0.5rem;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.toolbar__inline {
|
.toolbar__inline {
|
||||||
grid-template-columns: minmax(0, 1fr) auto;
|
grid-template-columns: minmax(0, 1fr) 9.5rem;
|
||||||
align-items: stretch;
|
align-items: stretch;
|
||||||
gap: 0.5rem;
|
gap: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.toolbar__inline input,
|
||||||
|
.toolbar__inline select,
|
||||||
.toolbar__inline button {
|
.toolbar__inline button {
|
||||||
width: auto;
|
min-height: 3rem;
|
||||||
min-width: var(--button-min-width);
|
}
|
||||||
|
|
||||||
|
.toolbar__inline button {
|
||||||
|
width: 100%;
|
||||||
|
min-width: 9.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stack-panel__save--overwrite {
|
||||||
|
background: #b42318;
|
||||||
|
border-color: #8f1d13;
|
||||||
|
color: #fff7f5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stack-panel__save--overwrite:hover:not(:disabled) {
|
||||||
|
background: #912018;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toolbar__status {
|
||||||
|
min-height: 1.6rem;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toolbar__status--placeholder {
|
||||||
|
visibility: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.layer-stack {
|
.layer-stack {
|
||||||
|
|||||||
Reference in New Issue
Block a user