25 Commits

Author SHA1 Message Date
Aiden
120f899b0d docs
Some checks failed
CI / React UI Build (push) Successful in 11s
CI / Windows Release Package (push) Has been cancelled
CI / Native Windows Build And Tests (push) Has been cancelled
2026-05-10 23:57:05 +10:00
Aiden
41075bbc61 more seperation 2026-05-10 23:53:27 +10:00
Aiden
7f0f60c0e3 ore untangling
All checks were successful
CI / React UI Build (push) Successful in 11s
CI / Native Windows Build And Tests (push) Successful in 2m38s
CI / Windows Release Package (push) Successful in 2m36s
2026-05-10 23:31:45 +10:00
Aiden
739231d5a1 Phase 1 clean-up and separation of concerns
All checks were successful
CI / React UI Build (push) Successful in 10s
CI / Native Windows Build And Tests (push) Successful in 2m34s
CI / Windows Release Package (push) Successful in 2m42s
2026-05-10 23:21:13 +10:00
Aiden
3629227aa9 intial phase 1 subsytem split
All checks were successful
CI / React UI Build (push) Successful in 11s
CI / Native Windows Build And Tests (push) Successful in 2m31s
CI / Windows Release Package (push) Successful in 2m32s
2026-05-10 23:03:15 +10:00
Aiden
618831d578 Phase 1 goals
All checks were successful
CI / React UI Build (push) Successful in 11s
CI / Native Windows Build And Tests (push) Successful in 2m26s
CI / Windows Release Package (push) Successful in 2m32s
2026-05-10 22:36:34 +10:00
Aiden
c38c22834d Preroll udpate
All checks were successful
CI / React UI Build (push) Successful in 11s
CI / Native Windows Build And Tests (push) Successful in 2m29s
CI / Windows Release Package (push) Successful in 2m30s
2026-05-10 22:30:47 +10:00
Aiden
c8a4bd4c7b adjustments to control and stack saving
All checks were successful
CI / React UI Build (push) Successful in 11s
CI / Native Windows Build And Tests (push) Successful in 2m28s
CI / Windows Release Package (push) Successful in 2m44s
2026-05-10 22:10:54 +10:00
Aiden
46129a6044 UI fix
All checks were successful
CI / React UI Build (push) Successful in 10s
CI / Native Windows Build And Tests (push) Successful in 2m26s
CI / Windows Release Package (push) Successful in 2m28s
2026-05-10 21:27:13 +10:00
Aiden
8fcb51d140 example data store
All checks were successful
CI / React UI Build (push) Successful in 11s
CI / Native Windows Build And Tests (push) Successful in 2m24s
CI / Windows Release Package (push) Successful in 2m34s
2026-05-10 21:11:17 +10:00
Aiden
944773c248 added new layer input pass
All checks were successful
CI / React UI Build (push) Successful in 11s
CI / Native Windows Build And Tests (push) Successful in 2m25s
CI / Windows Release Package (push) Successful in 2m28s
2026-05-10 21:00:34 +10:00
Aiden
7777cfc194 data storage
All checks were successful
CI / React UI Build (push) Successful in 11s
CI / Native Windows Build And Tests (push) Successful in 2m23s
CI / Windows Release Package (push) Successful in 2m46s
2026-05-10 20:39:28 +10:00
Aiden
198639ae3f OSC sync back
All checks were successful
CI / React UI Build (push) Successful in 11s
CI / Native Windows Build And Tests (push) Successful in 2m22s
CI / Windows Release Package (push) Successful in 2m29s
2026-05-10 18:58:26 +10:00
Aiden
d7ca42b51b OSC fixes
All checks were successful
CI / React UI Build (push) Successful in 11s
CI / Native Windows Build And Tests (push) Successful in 2m22s
CI / Windows Release Package (push) Successful in 2m43s
2026-05-10 18:37:30 +10:00
Aiden
f11d531e0c OSC bind address
All checks were successful
CI / React UI Build (push) Successful in 11s
CI / Native Windows Build And Tests (push) Successful in 2m41s
CI / Windows Release Package (push) Successful in 2m30s
2026-05-10 17:23:28 +10:00
Aiden
a3635b5d31 Revert "preview changes"
All checks were successful
CI / React UI Build (push) Successful in 11s
CI / Native Windows Build And Tests (push) Successful in 2m23s
CI / Windows Release Package (push) Successful in 2m28s
This reverts commit 98f5cbe309.
2026-05-09 16:47:45 +10:00
Aiden
bc9aa6fbad Revert "Video backend"
This reverts commit 4ffbb97abf.
2026-05-09 16:47:43 +10:00
Aiden
0c16665610 Revert "Decklink separation"
Some checks failed
CI / Windows Release Package (push) Has been cancelled
CI / React UI Build (push) Has been cancelled
CI / Native Windows Build And Tests (push) Has been cancelled
This reverts commit 46f2f1ece5.
2026-05-09 16:47:33 +10:00
Aiden
46f2f1ece5 Decklink separation 2026-05-09 14:42:11 +10:00
Aiden
4ffbb97abf Video backend
All checks were successful
CI / React UI Build (push) Successful in 11s
CI / Native Windows Build And Tests (push) Successful in 2m43s
CI / Windows Release Package (push) Successful in 2m54s
2026-05-09 14:15:49 +10:00
Aiden
98f5cbe309 preview changes
All checks were successful
CI / React UI Build (push) Successful in 11s
CI / Native Windows Build And Tests (push) Successful in 2m23s
CI / Windows Release Package (push) Successful in 2m41s
2026-05-09 13:53:00 +10:00
Aiden
93d856b3b6 CPU optimisations
Some checks failed
CI / React UI Build (push) Successful in 37s
CI / Windows Release Package (push) Has been cancelled
CI / Native Windows Build And Tests (push) Has been cancelled
2026-05-09 13:50:27 +10:00
6ea6971dd6 more shaders and updates/changes
All checks were successful
CI / React UI Build (push) Successful in 10s
CI / Native Windows Build And Tests (push) Successful in 2m22s
CI / Windows Release Package (push) Successful in 2m35s
2026-05-08 20:32:19 +10:00
163d70e9bd Annotations
All checks were successful
CI / React UI Build (push) Successful in 11s
CI / Native Windows Build And Tests (push) Successful in 2m22s
CI / Windows Release Package (push) Successful in 2m28s
2026-05-08 20:01:22 +10:00
8afef5065a Update README.md
All checks were successful
CI / React UI Build (push) Successful in 11s
CI / Native Windows Build And Tests (push) Successful in 2m20s
CI / Windows Release Package (push) Successful in 2m34s
2026-05-08 19:14:31 +10:00
110 changed files with 10844 additions and 1200 deletions

View File

@@ -34,6 +34,8 @@ set(APP_SOURCES
"${APP_DIR}/videoio/decklink/DeckLinkAPI_i.c"
"${APP_DIR}/control/ControlServer.cpp"
"${APP_DIR}/control/ControlServer.h"
"${APP_DIR}/control/ControlServices.cpp"
"${APP_DIR}/control/ControlServices.h"
"${APP_DIR}/control/OscServer.cpp"
"${APP_DIR}/control/OscServer.h"
"${APP_DIR}/control/RuntimeControlBridge.cpp"
@@ -60,11 +62,15 @@ set(APP_SOURCES
"${APP_DIR}/gl/OpenGLComposite.cpp"
"${APP_DIR}/gl/OpenGLComposite.h"
"${APP_DIR}/gl/OpenGLCompositeRuntimeControls.cpp"
"${APP_DIR}/gl/RenderEngine.cpp"
"${APP_DIR}/gl/RenderEngine.h"
"${APP_DIR}/gl/pipeline/OpenGLRenderPass.cpp"
"${APP_DIR}/gl/pipeline/OpenGLRenderPass.h"
"${APP_DIR}/gl/pipeline/OpenGLRenderPipeline.cpp"
"${APP_DIR}/gl/pipeline/OpenGLRenderPipeline.h"
"${APP_DIR}/gl/pipeline/RenderPassDescriptor.h"
"${APP_DIR}/gl/pipeline/ShaderFeedbackBuffers.cpp"
"${APP_DIR}/gl/pipeline/ShaderFeedbackBuffers.h"
"${APP_DIR}/gl/renderer/OpenGLRenderer.cpp"
"${APP_DIR}/gl/renderer/OpenGLRenderer.h"
"${APP_DIR}/gl/renderer/RenderTargetPool.cpp"
@@ -96,12 +102,20 @@ set(APP_SOURCES
"${APP_DIR}/resource.h"
"${APP_DIR}/runtime/RuntimeHost.cpp"
"${APP_DIR}/runtime/RuntimeHost.h"
"${APP_DIR}/runtime/HealthTelemetry.cpp"
"${APP_DIR}/runtime/HealthTelemetry.h"
"${APP_DIR}/runtime/RuntimeCoordinator.cpp"
"${APP_DIR}/runtime/RuntimeCoordinator.h"
"${APP_DIR}/runtime/RuntimeSnapshotProvider.cpp"
"${APP_DIR}/runtime/RuntimeSnapshotProvider.h"
"${APP_DIR}/runtime/RuntimeClock.cpp"
"${APP_DIR}/runtime/RuntimeClock.h"
"${APP_DIR}/runtime/RuntimeJson.cpp"
"${APP_DIR}/runtime/RuntimeJson.h"
"${APP_DIR}/runtime/RuntimeParameterUtils.cpp"
"${APP_DIR}/runtime/RuntimeParameterUtils.h"
"${APP_DIR}/runtime/RuntimeStore.cpp"
"${APP_DIR}/runtime/RuntimeStore.h"
"${APP_DIR}/shader/ShaderCompiler.cpp"
"${APP_DIR}/shader/ShaderCompiler.h"
"${APP_DIR}/shader/ShaderPackageRegistry.cpp"
@@ -112,6 +126,8 @@ set(APP_SOURCES
"${APP_DIR}/targetver.h"
"${APP_DIR}/videoio/VideoIOFormat.cpp"
"${APP_DIR}/videoio/VideoIOFormat.h"
"${APP_DIR}/videoio/VideoBackend.cpp"
"${APP_DIR}/videoio/VideoBackend.h"
"${APP_DIR}/videoio/VideoIOTypes.h"
"${APP_DIR}/videoio/VideoPlayoutScheduler.cpp"
"${APP_DIR}/videoio/VideoPlayoutScheduler.h"
@@ -347,7 +363,7 @@ install(FILES "${SLANG_LICENSE_FILE}"
RENAME "SLANG_LICENSE.txt"
)
install(FILES "${CMAKE_CURRENT_SOURCE_DIR}/SHADER_CONTRACT.md"
install(FILES "${CMAKE_CURRENT_SOURCE_DIR}/shaders/SHADER_CONTRACT.md"
DESTINATION "."
)

View File

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

View File

@@ -141,7 +141,9 @@ Current native test coverage includes:
{
"shaderLibrary": "shaders",
"serverPort": 8080,
"oscBindAddress": "127.0.0.1",
"oscPort": 9000,
"oscSmoothing": 0.18,
"inputVideoFormat": "1080p",
"inputFrameRate": "59.94",
"outputVideoFormat": "1080p",
@@ -203,13 +205,13 @@ runtime/screenshots/
## 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
/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
@@ -273,3 +275,6 @@ If `SLANG_ROOT` is not set, the workflow falls back to the repo-local default un
- compute shaders or a small 1x1 or nx1 RGBA16f render target for arbitrary data storage
- allow shaders to read other shaders data store based on name? or output over OSC
- Mipmapping for shader-declared textures
- Anotate included shaders
- allow 3 vector exposed controls
- add nearest sampling to the extra shader pass

View File

@@ -531,7 +531,7 @@ LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
if (!sInteractiveResize && pOpenGLComposite)
{
wglMakeCurrent(hDC, hRC);
pOpenGLComposite->paintGL();
pOpenGLComposite->paintGL(true);
wglMakeCurrent( NULL, NULL );
RaiseStatusControls(sStatusStrip);
}

View File

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

View File

@@ -1,245 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<Project DefaultTargets="Build" ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup Label="ProjectConfigurations">
<ProjectConfiguration Include="Debug|Win32">
<Configuration>Debug</Configuration>
<Platform>Win32</Platform>
</ProjectConfiguration>
<ProjectConfiguration Include="Debug|x64">
<Configuration>Debug</Configuration>
<Platform>x64</Platform>
</ProjectConfiguration>
<ProjectConfiguration Include="Release|Win32">
<Configuration>Release</Configuration>
<Platform>Win32</Platform>
</ProjectConfiguration>
<ProjectConfiguration Include="Release|x64">
<Configuration>Release</Configuration>
<Platform>x64</Platform>
</ProjectConfiguration>
</ItemGroup>
<PropertyGroup Label="Globals">
<ProjectGuid>{92C79085-CA51-4008-95DB-5403D2E19885}</ProjectGuid>
<RootNamespace>LoopThroughWithOpenGLCompositing</RootNamespace>
<Keyword>Win32Proj</Keyword>
<WindowsTargetPlatformVersion>10.0.26100.0</WindowsTargetPlatformVersion>
</PropertyGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|Win32'" Label="Configuration">
<ConfigurationType>Application</ConfigurationType>
<PlatformToolset>v143</PlatformToolset>
<CharacterSet>MultiByte</CharacterSet>
<WholeProgramOptimization>true</WholeProgramOptimization>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'" Label="Configuration">
<ConfigurationType>Application</ConfigurationType>
<PlatformToolset>v143</PlatformToolset>
<CharacterSet>MultiByte</CharacterSet>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'" Label="Configuration">
<ConfigurationType>Application</ConfigurationType>
<PlatformToolset>v143</PlatformToolset>
<CharacterSet>MultiByte</CharacterSet>
<WholeProgramOptimization>true</WholeProgramOptimization>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'" Label="Configuration">
<ConfigurationType>Application</ConfigurationType>
<PlatformToolset>v143</PlatformToolset>
<CharacterSet>MultiByte</CharacterSet>
</PropertyGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.props" />
<ImportGroup Label="ExtensionSettings">
</ImportGroup>
<ImportGroup Condition="'$(Configuration)|$(Platform)'=='Release|Win32'" Label="PropertySheets">
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
</ImportGroup>
<ImportGroup Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'" Label="PropertySheets">
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
</ImportGroup>
<ImportGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'" Label="PropertySheets">
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
</ImportGroup>
<ImportGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'" Label="PropertySheets">
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
</ImportGroup>
<PropertyGroup Label="UserMacros" />
<PropertyGroup>
<_ProjectFileVersion>12.0.21005.1</_ProjectFileVersion>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">
<OutDir>$(SolutionDir)$(Configuration)\</OutDir>
<IntDir>$(Configuration)\</IntDir>
<LinkIncremental>true</LinkIncremental>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
<OutDir>$(SolutionDir)$(Platform)\$(Configuration)\</OutDir>
<IntDir>$(Platform)\$(Configuration)\</IntDir>
<LinkIncremental>true</LinkIncremental>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">
<OutDir>$(SolutionDir)$(Configuration)\</OutDir>
<IntDir>$(Configuration)\</IntDir>
<LinkIncremental>false</LinkIncremental>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
<OutDir>$(SolutionDir)$(Platform)\$(Configuration)\</OutDir>
<IntDir>$(Platform)\$(Configuration)\</IntDir>
<LinkIncremental>false</LinkIncremental>
</PropertyGroup>
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">
<ClCompile>
<Optimization>Disabled</Optimization>
<AdditionalIncludeDirectories>.;control;gl;gl\pipeline;gl\renderer;gl\shader;videoio;videoio\decklink;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
<PreprocessorDefinitions>WIN32;_DEBUG;_WINDOWS;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<BasicRuntimeChecks>EnableFastChecks</BasicRuntimeChecks>
<RuntimeLibrary>MultiThreadedDebugDLL</RuntimeLibrary>
<PrecompiledHeader />
<WarningLevel>Level3</WarningLevel>
<DebugInformationFormat>EditAndContinue</DebugInformationFormat>
<LanguageStandard>stdcpp17</LanguageStandard>
</ClCompile>
<Link>
<AdditionalDependencies>opengl32.lib;Glu32.lib;Windowscodecs.lib;Ole32.lib;%(AdditionalDependencies)</AdditionalDependencies>
<GenerateDebugInformation>true</GenerateDebugInformation>
<SubSystem>Windows</SubSystem>
<TargetMachine>MachineX86</TargetMachine>
</Link>
</ItemDefinitionGroup>
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
<Midl>
<TargetEnvironment>X64</TargetEnvironment>
</Midl>
<ClCompile>
<Optimization>Disabled</Optimization>
<AdditionalIncludeDirectories>.;control;gl;gl\pipeline;gl\renderer;gl\shader;videoio;videoio\decklink;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
<PreprocessorDefinitions>WIN32;_DEBUG;_WINDOWS;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<BasicRuntimeChecks>EnableFastChecks</BasicRuntimeChecks>
<RuntimeLibrary>MultiThreadedDebugDLL</RuntimeLibrary>
<PrecompiledHeader />
<WarningLevel>Level3</WarningLevel>
<DebugInformationFormat>ProgramDatabase</DebugInformationFormat>
<LanguageStandard>stdcpp17</LanguageStandard>
</ClCompile>
<Link>
<AdditionalDependencies>opengl32.lib;Glu32.lib;Windowscodecs.lib;Ole32.lib;%(AdditionalDependencies)</AdditionalDependencies>
<GenerateDebugInformation>true</GenerateDebugInformation>
<SubSystem>Windows</SubSystem>
<TargetMachine>MachineX64</TargetMachine>
</Link>
</ItemDefinitionGroup>
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">
<ClCompile>
<Optimization>MaxSpeed</Optimization>
<IntrinsicFunctions>true</IntrinsicFunctions>
<AdditionalIncludeDirectories>.;control;gl;gl\pipeline;gl\renderer;gl\shader;videoio;videoio\decklink;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
<PreprocessorDefinitions>WIN32;NDEBUG;_WINDOWS;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<RuntimeLibrary>MultiThreadedDLL</RuntimeLibrary>
<FunctionLevelLinking>true</FunctionLevelLinking>
<PrecompiledHeader />
<WarningLevel>Level3</WarningLevel>
<DebugInformationFormat>ProgramDatabase</DebugInformationFormat>
<LanguageStandard>stdcpp17</LanguageStandard>
</ClCompile>
<Link>
<AdditionalDependencies>opengl32.lib;Glu32.lib;Windowscodecs.lib;Ole32.lib;%(AdditionalDependencies)</AdditionalDependencies>
<GenerateDebugInformation>true</GenerateDebugInformation>
<SubSystem>Windows</SubSystem>
<OptimizeReferences>true</OptimizeReferences>
<EnableCOMDATFolding>true</EnableCOMDATFolding>
<TargetMachine>MachineX86</TargetMachine>
</Link>
</ItemDefinitionGroup>
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
<Midl>
<TargetEnvironment>X64</TargetEnvironment>
</Midl>
<ClCompile>
<Optimization>MaxSpeed</Optimization>
<IntrinsicFunctions>true</IntrinsicFunctions>
<AdditionalIncludeDirectories>.;control;gl;gl\pipeline;gl\renderer;gl\shader;videoio;videoio\decklink;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
<PreprocessorDefinitions>WIN32;NDEBUG;_WINDOWS;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<RuntimeLibrary>MultiThreadedDLL</RuntimeLibrary>
<FunctionLevelLinking>true</FunctionLevelLinking>
<PrecompiledHeader />
<WarningLevel>Level3</WarningLevel>
<DebugInformationFormat>ProgramDatabase</DebugInformationFormat>
<LanguageStandard>stdcpp17</LanguageStandard>
</ClCompile>
<Link>
<AdditionalDependencies>opengl32.lib;Glu32.lib;Windowscodecs.lib;Ole32.lib;%(AdditionalDependencies)</AdditionalDependencies>
<GenerateDebugInformation>true</GenerateDebugInformation>
<SubSystem>Windows</SubSystem>
<OptimizeReferences>true</OptimizeReferences>
<EnableCOMDATFolding>true</EnableCOMDATFolding>
<TargetMachine>MachineX64</TargetMachine>
</Link>
</ItemDefinitionGroup>
<ItemGroup>
<ClCompile Include="gl\renderer\GLExtensions.cpp" />
<ClCompile Include="LoopThroughWithOpenGLCompositing.cpp" />
<ClCompile Include="gl\OpenGLComposite.cpp" />
<ClCompile Include="gl\pipeline\OpenGLRenderPass.cpp" />
<ClCompile Include="gl\pipeline\OpenGLRenderPipeline.cpp" />
<ClCompile Include="gl\renderer\OpenGLRenderer.cpp" />
<ClCompile Include="gl\renderer\RenderTargetPool.cpp" />
<ClCompile Include="gl\shader\OpenGLShaderPrograms.cpp" />
<ClCompile Include="gl\pipeline\PngScreenshotWriter.cpp" />
<ClCompile Include="gl\shader\ShaderBuildQueue.cpp" />
<ClCompile Include="gl\pipeline\TemporalHistoryBuffers.cpp" />
<ClCompile Include="gl\pipeline\OpenGLVideoIOBridge.cpp" />
<ClCompile Include="stdafx.cpp">
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">Create</PrecompiledHeader>
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">Create</PrecompiledHeader>
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">Create</PrecompiledHeader>
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|x64'">Create</PrecompiledHeader>
</ClCompile>
<ClCompile Include="videoio\decklink\DeckLinkAPI_i.c" />
<ClCompile Include="control\RuntimeServices.cpp" />
<ClCompile Include="videoio\decklink\DeckLinkSession.cpp" />
<ClCompile Include="videoio\decklink\DeckLinkVideoIOFormat.cpp" />
<ClCompile Include="runtime\RuntimeClock.cpp" />
<ClCompile Include="videoio\VideoIOFormat.cpp" />
<ClCompile Include="videoio\VideoPlayoutScheduler.cpp" />
</ItemGroup>
<ItemGroup>
<ClInclude Include="gl\renderer\GLExtensions.h" />
<ClInclude Include="LoopThroughWithOpenGLCompositing.h" />
<ClInclude Include="gl\OpenGLComposite.h" />
<ClInclude Include="gl\pipeline\OpenGLRenderPass.h" />
<ClInclude Include="gl\pipeline\OpenGLRenderPipeline.h" />
<ClInclude Include="gl\pipeline\RenderPassDescriptor.h" />
<ClInclude Include="gl\renderer\OpenGLRenderer.h" />
<ClInclude Include="gl\renderer\RenderTargetPool.h" />
<ClInclude Include="gl\shader\OpenGLShaderPrograms.h" />
<ClInclude Include="gl\pipeline\PngScreenshotWriter.h" />
<ClInclude Include="gl\shader\ShaderBuildQueue.h" />
<ClInclude Include="gl\pipeline\TemporalHistoryBuffers.h" />
<ClInclude Include="gl\pipeline\OpenGLVideoIOBridge.h" />
<ClInclude Include="resource.h" />
<ClInclude Include="stdafx.h" />
<ClInclude Include="targetver.h" />
<ClInclude Include="control\RuntimeServices.h" />
<ClInclude Include="videoio\decklink\DeckLinkSession.h" />
<ClInclude Include="videoio\decklink\DeckLinkVideoIOFormat.h" />
<ClInclude Include="runtime\RuntimeClock.h" />
<ClInclude Include="videoio\VideoIOFormat.h" />
<ClInclude Include="videoio\VideoIOTypes.h" />
<ClInclude Include="videoio\VideoPlayoutScheduler.h" />
</ItemGroup>
<ItemGroup>
<Image Include="LoopThroughWithOpenGLCompositing.ico" />
<Image Include="small.ico" />
</ItemGroup>
<ItemGroup>
<None Include="video_effect.slang" />
</ItemGroup>
<ItemGroup>
<ResourceCompile Include="LoopThroughWithOpenGLCompositing.rc" />
</ItemGroup>
<ItemGroup>
<Midl Include="..\..\3rdParty\Blackmagic DeckLink SDK 16.0\Win\include\DeckLinkAPI.idl" />
</ItemGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
<ImportGroup Label="ExtensionTargets">
</ImportGroup>
</Project>

View File

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

View File

@@ -17,6 +17,7 @@
namespace
{
constexpr DWORD kStateBroadcastIntervalMs = 250;
constexpr DWORD kStateBroadcastThrottleMs = 50;
bool InitializeWinsock(std::string& error)
{
@@ -75,7 +76,7 @@ std::string GuessContentType(const std::filesystem::path& assetPath)
}
ControlServer::ControlServer()
: mPort(0), mRunning(false)
: mPort(0), mRunning(false), mBroadcastPending(false)
{
}
@@ -161,10 +162,16 @@ void ControlServer::Stop()
void ControlServer::BroadcastState()
{
mBroadcastPending = false;
std::lock_guard<std::mutex> lock(mMutex);
BroadcastStateLocked();
}
void ControlServer::RequestBroadcastState()
{
mBroadcastPending = true;
}
void ControlServer::ServerLoop()
{
DWORD lastStateBroadcastMs = GetTickCount();
@@ -173,7 +180,12 @@ void ControlServer::ServerLoop()
TryAcceptClient();
const DWORD nowMs = GetTickCount();
if (nowMs - lastStateBroadcastMs >= kStateBroadcastIntervalMs)
if (mBroadcastPending && nowMs - lastStateBroadcastMs >= kStateBroadcastThrottleMs)
{
BroadcastState();
lastStateBroadcastMs = nowMs;
}
else if (nowMs - lastStateBroadcastMs >= kStateBroadcastIntervalMs)
{
BroadcastState();
lastStateBroadcastMs = nowMs;
@@ -469,6 +481,7 @@ bool ControlServer::HandleWebSocketUpgrade(UniqueSocket clientSocket, const Http
client.socket.reset(clientSocket.release());
client.websocket = true;
mClients.push_back(std::move(client));
mBroadcastPending = false;
BroadcastStateLocked();
}
return true;
@@ -501,6 +514,9 @@ bool ControlServer::SendWebSocketText(SOCKET clientSocket, const std::string& pa
void ControlServer::BroadcastStateLocked()
{
if (mClients.empty())
return;
const std::string stateMessage = mCallbacks.getStateJson ? mCallbacks.getStateJson() : "{}";
for (auto it = mClients.begin(); it != mClients.end();)
{

View File

@@ -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);
void Stop();
void BroadcastState();
void RequestBroadcastState();
unsigned short GetPort() const { return mPort; }
@@ -100,6 +101,7 @@ private:
unsigned short mPort;
std::thread mThread;
std::atomic<bool> mRunning;
std::atomic<bool> mBroadcastPending;
mutable std::mutex mMutex;
std::vector<ClientConnection> mClients;
};

View File

@@ -0,0 +1,247 @@
#include "ControlServices.h"
#include "ControlServer.h"
#include "OscServer.h"
#include "RuntimeControlBridge.h"
#include "RuntimeHost.h"
#include <windows.h>
ControlServices::ControlServices() :
mControlServer(std::make_unique<ControlServer>()),
mOscServer(std::make_unique<OscServer>()),
mPollRunning(false),
mRegistryChanged(false),
mReloadRequested(false),
mPollFailed(false)
{
}
ControlServices::~ControlServices()
{
Stop();
}
bool ControlServices::Start(OpenGLComposite& composite, RuntimeHost& runtimeHost, std::string& error)
{
Stop();
if (!StartControlServicesBoundary(composite, runtimeHost, *this, *mControlServer, *mOscServer, error))
{
Stop();
return false;
}
return true;
}
void ControlServices::BeginPolling(RuntimeHost& runtimeHost)
{
StartPolling(runtimeHost);
}
void ControlServices::Stop()
{
StopPolling();
if (mOscServer)
mOscServer->Stop();
if (mControlServer)
mControlServer->Stop();
}
void ControlServices::BroadcastState()
{
if (mControlServer)
mControlServer->BroadcastState();
}
void ControlServices::RequestBroadcastState()
{
if (mControlServer)
mControlServer->RequestBroadcastState();
}
bool ControlServices::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 ControlServices::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 ControlServices::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 ControlServices::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 ControlServices::ConsumeCompletedOscCommits(std::vector<CompletedOscCommit>& completedCommits)
{
completedCommits.clear();
std::lock_guard<std::mutex> lock(mCompletedOscCommitMutex);
if (mCompletedOscCommits.empty())
return;
completedCommits.swap(mCompletedOscCommits);
}
RuntimePollEvents ControlServices::ConsumePollEvents()
{
RuntimePollEvents events;
events.registryChanged = mRegistryChanged.exchange(false);
events.reloadRequested = mReloadRequested.exchange(false);
events.failed = mPollFailed.exchange(false);
if (events.failed)
{
std::lock_guard<std::mutex> lock(mPollErrorMutex);
events.error = mPollError;
}
return events;
}
void ControlServices::StartPolling(RuntimeHost& runtimeHost)
{
if (mPollRunning.exchange(true))
return;
mPollThread = std::thread([this, &runtimeHost]() { PollLoop(runtimeHost); });
}
void ControlServices::StopPolling()
{
if (!mPollRunning.exchange(false))
return;
if (mPollThread.joinable())
mPollThread.join();
}
void ControlServices::PollLoop(RuntimeHost& runtimeHost)
{
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 reloadRequested = false;
std::string runtimeError;
if (!runtimeHost.PollFileChanges(registryChanged, reloadRequested, runtimeError))
{
{
std::lock_guard<std::mutex> lock(mPollErrorMutex);
mPollError = runtimeError;
}
mPollFailed = true;
}
else
{
if (registryChanged)
mRegistryChanged = true;
if (reloadRequested)
mReloadRequested = true;
}
for (int i = 0; i < 25 && mPollRunning; ++i)
Sleep(10);
}
}

View File

@@ -0,0 +1,95 @@
#pragma once
#include "RuntimeJson.h"
#include "ShaderTypes.h"
#include <atomic>
#include <map>
#include <memory>
#include <mutex>
#include <string>
#include <thread>
#include <vector>
class ControlServer;
class OpenGLComposite;
class OscServer;
class RuntimeHost;
struct RuntimePollEvents
{
bool registryChanged = false;
bool reloadRequested = false;
bool failed = false;
std::string error;
};
class ControlServices
{
public:
struct AppliedOscUpdate
{
std::string routeKey;
std::string layerKey;
std::string parameterKey;
JsonValue targetValue;
};
struct CompletedOscCommit
{
std::string routeKey;
uint64_t generation = 0;
};
ControlServices();
~ControlServices();
bool Start(OpenGLComposite& composite, RuntimeHost& runtimeHost, std::string& error);
void BeginPolling(RuntimeHost& runtimeHost);
void Stop();
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();
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 StopPolling();
void PollLoop(RuntimeHost& runtimeHost);
std::unique_ptr<ControlServer> mControlServer;
std::unique_ptr<OscServer> mOscServer;
std::thread mPollThread;
std::atomic<bool> mPollRunning;
std::atomic<bool> mRegistryChanged;
std::atomic<bool> mReloadRequested;
std::atomic<bool> mPollFailed;
std::mutex mPollErrorMutex;
std::string mPollError;
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;
};

View File

@@ -55,7 +55,7 @@ OscServer::~OscServer()
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)
return true;
@@ -78,11 +78,15 @@ bool OscServer::Start(unsigned short port, const Callbacks& callbacks, std::stri
sockaddr_in address = {};
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));
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();
return false;
}
@@ -92,6 +96,24 @@ bool OscServer::Start(unsigned short port, const Callbacks& callbacks, std::stri
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()
{
mRunning = false;

View File

@@ -20,7 +20,7 @@ public:
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();
unsigned short GetPort() const { return mPort; }
@@ -37,6 +37,7 @@ private:
void ServerLoop();
bool DecodeMessage(const char* data, int byteCount, OscMessage& message, std::string& error) const;
bool DispatchMessage(const OscMessage& message, std::string& error) const;
static bool 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 ReadPaddedString(const char* data, int byteCount, int& offset, std::string& value);
static bool ReadInt32(const char* data, int byteCount, int& offset, int& value);

View File

@@ -1,13 +1,15 @@
#include "RuntimeControlBridge.h"
#include "ControlServices.h"
#include "ControlServer.h"
#include "OpenGLComposite.h"
#include "OscServer.h"
#include "RuntimeHost.h"
bool StartRuntimeControlServices(
bool StartControlServicesBoundary(
OpenGLComposite& composite,
RuntimeHost& runtimeHost,
ControlServices& controlServices,
ControlServer& controlServer,
OscServer& oscServer,
std::string& error)
@@ -41,10 +43,10 @@ bool StartRuntimeControlServices(
runtimeHost.SetServerPort(controlServer.GetPort());
OscServer::Callbacks oscCallbacks;
oscCallbacks.updateParameter = [&composite](const std::string& layerKey, const std::string& parameterKey, const std::string& valueJson, std::string& actionError) {
return composite.UpdateLayerParameterByControlKeyJson(layerKey, parameterKey, valueJson, actionError);
oscCallbacks.updateParameter = [&controlServices](const std::string& layerKey, const std::string& parameterKey, const std::string& valueJson, std::string& actionError) {
return controlServices.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 true;

View File

@@ -3,13 +3,15 @@
#include <string>
class ControlServer;
class ControlServices;
class OpenGLComposite;
class OscServer;
class RuntimeHost;
bool StartRuntimeControlServices(
bool StartControlServicesBoundary(
OpenGLComposite& composite,
RuntimeHost& runtimeHost,
ControlServices& controlServices,
ControlServer& controlServer,
OscServer& oscServer,
std::string& error);

View File

@@ -1,19 +1,7 @@
#include "RuntimeServices.h"
#include "ControlServer.h"
#include "OscServer.h"
#include "RuntimeControlBridge.h"
#include "RuntimeHost.h"
#include <windows.h>
RuntimeServices::RuntimeServices() :
mControlServer(std::make_unique<ControlServer>()),
mOscServer(std::make_unique<OscServer>()),
mPollRunning(false),
mRegistryChanged(false),
mReloadRequested(false),
mPollFailed(false)
mControlServices(std::make_unique<ControlServices>())
{
}
@@ -24,96 +12,72 @@ RuntimeServices::~RuntimeServices()
bool RuntimeServices::Start(OpenGLComposite& composite, RuntimeHost& runtimeHost, std::string& error)
{
Stop();
if (!StartRuntimeControlServices(composite, runtimeHost, *mControlServer, *mOscServer, error))
{
Stop();
return false;
}
return true;
return mControlServices && mControlServices->Start(composite, runtimeHost, error);
}
void RuntimeServices::BeginPolling(RuntimeHost& runtimeHost)
{
StartPolling(runtimeHost);
if (mControlServices)
mControlServices->BeginPolling(runtimeHost);
}
void RuntimeServices::Stop()
{
StopPolling();
if (mOscServer)
mOscServer->Stop();
if (mControlServer)
mControlServer->Stop();
if (mControlServices)
mControlServices->Stop();
}
void RuntimeServices::BroadcastState()
{
if (mControlServer)
mControlServer->BroadcastState();
if (mControlServices)
mControlServices->BroadcastState();
}
void RuntimeServices::RequestBroadcastState()
{
if (mControlServices)
mControlServices->RequestBroadcastState();
}
bool RuntimeServices::QueueOscUpdate(const std::string& layerKey, const std::string& parameterKey, const std::string& valueJson, std::string& error)
{
return mControlServices && mControlServices->QueueOscUpdate(layerKey, parameterKey, valueJson, error);
}
bool RuntimeServices::ApplyPendingOscUpdates(std::vector<AppliedOscUpdate>& appliedUpdates, std::string& error)
{
if (!mControlServices)
{
appliedUpdates.clear();
return true;
}
return mControlServices->ApplyPendingOscUpdates(appliedUpdates, error);
}
bool RuntimeServices::QueueOscCommit(const std::string& routeKey, const std::string& layerKey, const std::string& parameterKey, const JsonValue& value, uint64_t generation, std::string& error)
{
return mControlServices && mControlServices->QueueOscCommit(routeKey, layerKey, parameterKey, value, generation, error);
}
void RuntimeServices::ClearOscState()
{
if (mControlServices)
mControlServices->ClearOscState();
}
void RuntimeServices::ConsumeCompletedOscCommits(std::vector<CompletedOscCommit>& completedCommits)
{
if (!mControlServices)
{
completedCommits.clear();
return;
}
mControlServices->ConsumeCompletedOscCommits(completedCommits);
}
RuntimePollEvents RuntimeServices::ConsumePollEvents()
{
RuntimePollEvents events;
events.registryChanged = mRegistryChanged.exchange(false);
events.reloadRequested = mReloadRequested.exchange(false);
events.failed = mPollFailed.exchange(false);
if (events.failed)
{
std::lock_guard<std::mutex> lock(mPollErrorMutex);
events.error = mPollError;
}
return events;
}
void RuntimeServices::StartPolling(RuntimeHost& runtimeHost)
{
if (mPollRunning.exchange(true))
return;
mPollThread = std::thread([this, &runtimeHost]() { PollLoop(runtimeHost); });
}
void RuntimeServices::StopPolling()
{
if (!mPollRunning.exchange(false))
return;
if (mPollThread.joinable())
mPollThread.join();
}
void RuntimeServices::PollLoop(RuntimeHost& runtimeHost)
{
while (mPollRunning)
{
bool registryChanged = false;
bool reloadRequested = false;
std::string runtimeError;
if (!runtimeHost.PollFileChanges(registryChanged, reloadRequested, runtimeError))
{
{
std::lock_guard<std::mutex> lock(mPollErrorMutex);
mPollError = runtimeError;
}
mPollFailed = true;
}
else
{
if (registryChanged)
mRegistryChanged = true;
if (reloadRequested)
mReloadRequested = true;
}
for (int i = 0; i < 25 && mPollRunning; ++i)
Sleep(10);
}
return mControlServices ? mControlServices->ConsumePollEvents() : RuntimePollEvents{};
}

View File

@@ -1,27 +1,18 @@
#pragma once
#include <atomic>
#include "ControlServices.h"
#include <memory>
#include <mutex>
#include <string>
#include <thread>
class ControlServer;
class OpenGLComposite;
class OscServer;
class RuntimeHost;
struct RuntimePollEvents
{
bool registryChanged = false;
bool reloadRequested = false;
bool failed = false;
std::string error;
};
class RuntimeServices
{
public:
using AppliedOscUpdate = ControlServices::AppliedOscUpdate;
using CompletedOscCommit = ControlServices::CompletedOscCommit;
RuntimeServices();
~RuntimeServices();
@@ -29,20 +20,14 @@ public:
void BeginPolling(RuntimeHost& runtimeHost);
void Stop();
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();
private:
void StartPolling(RuntimeHost& runtimeHost);
void StopPolling();
void PollLoop(RuntimeHost& runtimeHost);
std::unique_ptr<ControlServer> mControlServer;
std::unique_ptr<OscServer> mOscServer;
std::thread mPollThread;
std::atomic<bool> mPollRunning;
std::atomic<bool> mRegistryChanged;
std::atomic<bool> mReloadRequested;
std::atomic<bool> mPollFailed;
std::mutex mPollErrorMutex;
std::string mPollError;
std::unique_ptr<ControlServices> mControlServices;
};

View File

@@ -1,52 +1,118 @@
#include "DeckLinkDisplayMode.h"
#include "DeckLinkSession.h"
#include "OpenGLComposite.h"
#include "GLExtensions.h"
#include "GlRenderConstants.h"
#include "OpenGLRenderPass.h"
#include "OpenGLRenderPipeline.h"
#include "OpenGLShaderPrograms.h"
#include "OpenGLVideoIOBridge.h"
#include "PngScreenshotWriter.h"
#include "RenderEngine.h"
#include "RuntimeParameterUtils.h"
#include "RuntimeServices.h"
#include "ShaderBuildQueue.h"
#include "VideoBackend.h"
#include <algorithm>
#include <cctype>
#include <chrono>
#include <cmath>
#include <ctime>
#include <filesystem>
#include <iomanip>
#include <memory>
#include <set>
#include <sstream>
#include <string>
#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) :
hGLWnd(hWnd), hGLDC(hDC), hGLRC(hRC),
mVideoIO(std::make_unique<DeckLinkSession>()),
mRenderer(std::make_unique<OpenGLRenderer>()),
mUseCommittedLayerStates(false),
mScreenshotRequested(false)
{
InitializeCriticalSection(&pMutex);
mRuntimeHost = std::make_unique<RuntimeHost>();
mRenderPipeline = std::make_unique<OpenGLRenderPipeline>(
*mRenderer,
*mRuntimeHost,
[this]() { renderEffect(); },
[this]() { ProcessScreenshotRequest(); },
[this]() { paintGL(); });
mVideoIOBridge = std::make_unique<OpenGLVideoIOBridge>(
*mVideoIO,
*mRenderer,
*mRenderPipeline,
*mRuntimeHost,
mRuntimeStore = std::make_unique<RuntimeStore>(*mRuntimeHost);
mRuntimeSnapshotProvider = std::make_unique<RuntimeSnapshotProvider>(*mRuntimeHost);
mRuntimeCoordinator = std::make_unique<RuntimeCoordinator>(*mRuntimeStore);
mRenderEngine = std::make_unique<RenderEngine>(
*mRuntimeSnapshotProvider,
mRuntimeHost->GetHealthTelemetry(),
pMutex,
hGLDC,
hGLRC);
mRenderPass = std::make_unique<OpenGLRenderPass>(*mRenderer);
mShaderPrograms = std::make_unique<OpenGLShaderPrograms>(*mRenderer, *mRuntimeHost);
mShaderBuildQueue = std::make_unique<ShaderBuildQueue>(*mRuntimeHost);
hGLRC,
[this]() { renderEffect(); },
[this]() { ProcessScreenshotRequest(); },
[this]() { paintGL(false); });
mVideoBackend = std::make_unique<VideoBackend>(*mRenderEngine, mRuntimeHost->GetHealthTelemetry());
mShaderBuildQueue = std::make_unique<ShaderBuildQueue>(*mRuntimeSnapshotProvider);
mRuntimeServices = std::make_unique<RuntimeServices>();
}
@@ -56,8 +122,8 @@ OpenGLComposite::~OpenGLComposite()
mRuntimeServices->Stop();
if (mShaderBuildQueue)
mShaderBuildQueue->Stop();
mVideoIO->ReleaseResources();
mRenderer->DestroyResources();
if (mVideoBackend)
mVideoBackend->ReleaseResources();
DeleteCriticalSection(&pMutex);
}
@@ -72,23 +138,23 @@ bool OpenGLComposite::InitVideoIO()
VideoFormatSelection videoModes;
std::string initFailureReason;
if (mRuntimeHost && mRuntimeHost->GetRepoRoot().empty())
if (mRuntimeStore && mRuntimeStore->GetRuntimeRepositoryRoot().empty())
{
std::string runtimeError;
if (!mRuntimeHost->Initialize(runtimeError))
if (!mRuntimeStore->InitializeStore(runtimeError))
{
MessageBoxA(NULL, runtimeError.c_str(), "Runtime host failed to initialize", MB_OK);
return false;
}
}
if (mRuntimeHost)
if (mRuntimeStore)
{
if (!ResolveConfiguredVideoFormats(
mRuntimeHost->GetInputVideoFormat(),
mRuntimeHost->GetInputFrameRate(),
mRuntimeHost->GetOutputVideoFormat(),
mRuntimeHost->GetOutputFrameRate(),
mRuntimeStore->GetConfiguredInputVideoFormat(),
mRuntimeStore->GetConfiguredInputFrameRate(),
mRuntimeStore->GetConfiguredOutputVideoFormat(),
mRuntimeStore->GetConfiguredOutputFrameRate(),
videoModes,
initFailureReason))
{
@@ -97,7 +163,7 @@ bool OpenGLComposite::InitVideoIO()
}
}
if (!mVideoIO->DiscoverDevicesAndModes(videoModes, initFailureReason))
if (!mVideoBackend->DiscoverDevicesAndModes(videoModes, initFailureReason))
{
const char* title = initFailureReason == "Please install the Blackmagic DeckLink drivers to use the features of this application."
? "This application requires the DeckLink drivers installed."
@@ -105,8 +171,8 @@ bool OpenGLComposite::InitVideoIO()
MessageBoxA(NULL, initFailureReason.c_str(), title, MB_OK | MB_ICONERROR);
return false;
}
const bool outputAlphaRequired = mRuntimeHost && mRuntimeHost->ExternalKeyingEnabled();
if (!mVideoIO->SelectPreferredFormats(videoModes, outputAlphaRequired, initFailureReason))
const bool outputAlphaRequired = mRuntimeStore && mRuntimeStore->IsExternalKeyingConfigured();
if (!mVideoBackend->SelectPreferredFormats(videoModes, outputAlphaRequired, initFailureReason))
goto error;
if (! CheckOpenGLExtensions())
@@ -121,59 +187,68 @@ bool OpenGLComposite::InitVideoIO()
goto error;
}
PublishVideoIOStatus(mVideoIO->OutputModelName().empty()
PublishVideoIOStatus(mVideoBackend->OutputModelName().empty()
? "DeckLink output device selected."
: ("Selected output device: " + mVideoIO->OutputModelName()));
: ("Selected output device: " + mVideoBackend->OutputModelName()));
// Resize window to match output video frame, but scale large formats down by half for viewing.
if (mVideoIO->OutputFrameWidth() < 1920)
resizeWindow(mVideoIO->OutputFrameWidth(), mVideoIO->OutputFrameHeight());
if (mVideoBackend->OutputFrameWidth() < 1920)
resizeWindow(mVideoBackend->OutputFrameWidth(), mVideoBackend->OutputFrameHeight());
else
resizeWindow(mVideoIO->OutputFrameWidth() / 2, mVideoIO->OutputFrameHeight() / 2);
resizeWindow(mVideoBackend->OutputFrameWidth() / 2, mVideoBackend->OutputFrameHeight() / 2);
if (!mVideoIO->ConfigureInput([this](const VideoIOFrame& frame) { mVideoIOBridge->VideoFrameArrived(frame); }, videoModes.input, initFailureReason))
if (!mVideoBackend->ConfigureInput(videoModes.input, initFailureReason))
{
goto error;
}
if (!mVideoIO->HasInputDevice() && mRuntimeHost)
if (!mVideoBackend->HasInputDevice() && mRuntimeHost)
{
mRuntimeHost->SetSignalStatus(false, mVideoIO->InputFrameWidth(), mVideoIO->InputFrameHeight(), mVideoIO->InputDisplayModeName());
mRuntimeHost->GetHealthTelemetry().ReportSignalStatus(
false,
mVideoBackend->InputFrameWidth(),
mVideoBackend->InputFrameHeight(),
mVideoBackend->InputDisplayModeName());
}
if (!mVideoIO->ConfigureOutput([this](const VideoIOCompletion& completion) { mVideoIOBridge->PlayoutFrameCompleted(completion); }, videoModes.output, mRuntimeHost && mRuntimeHost->ExternalKeyingEnabled(), initFailureReason))
if (!mVideoBackend->ConfigureOutput(videoModes.output, mRuntimeStore && mRuntimeStore->IsExternalKeyingConfigured(), initFailureReason))
{
goto error;
}
PublishVideoIOStatus(mVideoIO->StatusMessage());
PublishVideoIOStatus(mVideoBackend->StatusMessage());
return true;
error:
if (!initFailureReason.empty())
MessageBoxA(NULL, initFailureReason.c_str(), "DeckLink initialization failed", MB_OK | MB_ICONERROR);
mVideoIO->ReleaseResources();
mVideoBackend->ReleaseResources();
return false;
}
void OpenGLComposite::paintGL()
void OpenGLComposite::paintGL(bool force)
{
if (!TryEnterCriticalSection(&pMutex))
if (!force)
{
if (IsIconic(hGLWnd))
return;
}
const unsigned previewFps = mRuntimeStore ? mRuntimeStore->GetConfiguredPreviewFps() : 30u;
if (!mRenderEngine->TryPresentPreview(force, previewFps, mVideoBackend->OutputFrameWidth(), mVideoBackend->OutputFrameHeight()))
{
ValidateRect(hGLWnd, NULL);
return;
}
mRenderer->PresentToWindow(hGLDC, mVideoIO->OutputFrameWidth(), mVideoIO->OutputFrameHeight());
ValidateRect(hGLWnd, NULL);
LeaveCriticalSection(&pMutex);
}
void OpenGLComposite::resizeGL(WORD width, WORD height)
{
// We don't set the project or model matrices here since the window data is copied directly from
// an off-screen FBO in paintGL(). Just save the width and height for use in paintGL().
mRenderer->ResizeView(width, height);
mRenderEngine->ResizeView(width, height);
}
void OpenGLComposite::resizeWindow(int width, int height)
@@ -191,17 +266,17 @@ void OpenGLComposite::PublishVideoIOStatus(const std::string& statusMessage)
return;
if (!statusMessage.empty())
mVideoIO->SetStatusMessage(statusMessage);
mVideoBackend->SetStatusMessage(statusMessage);
mRuntimeHost->SetVideoIOStatus(
"decklink",
mVideoIO->OutputModelName(),
mVideoIO->SupportsInternalKeying(),
mVideoIO->SupportsExternalKeying(),
mVideoIO->KeyerInterfaceAvailable(),
mRuntimeHost->ExternalKeyingEnabled(),
mVideoIO->ExternalKeyingActive(),
mVideoIO->StatusMessage());
mVideoBackend->OutputModelName(),
mVideoBackend->SupportsInternalKeying(),
mVideoBackend->SupportsExternalKeying(),
mVideoBackend->KeyerInterfaceAvailable(),
mRuntimeStore ? mRuntimeStore->IsExternalKeyingConfigured() : false,
mVideoBackend->ExternalKeyingActive(),
mVideoBackend->StatusMessage());
}
bool OpenGLComposite::InitOpenGLState()
@@ -210,7 +285,7 @@ bool OpenGLComposite::InitOpenGLState()
return false;
std::string runtimeError;
if (mRuntimeHost->GetRepoRoot().empty() && !mRuntimeHost->Initialize(runtimeError))
if (mRuntimeStore->GetRuntimeRepositoryRoot().empty() && !mRuntimeStore->InitializeStore(runtimeError))
{
MessageBoxA(NULL, runtimeError.c_str(), "Runtime host failed to initialize", MB_OK);
return false;
@@ -224,40 +299,42 @@ bool OpenGLComposite::InitOpenGLState()
// Prepare the runtime shader program generated from the active shader package.
char compilerErrorMessage[1024];
if (!mShaderPrograms->CompileDecodeShader(sizeof(compilerErrorMessage), compilerErrorMessage))
if (!mRenderEngine->CompileDecodeShader(sizeof(compilerErrorMessage), compilerErrorMessage))
{
MessageBoxA(NULL, compilerErrorMessage, "OpenGL decode shader failed to load or compile", MB_OK);
return false;
}
if (!mShaderPrograms->CompileOutputPackShader(sizeof(compilerErrorMessage), compilerErrorMessage))
if (!mRenderEngine->CompileOutputPackShader(sizeof(compilerErrorMessage), compilerErrorMessage))
{
MessageBoxA(NULL, compilerErrorMessage, "OpenGL output pack shader failed to load or compile", MB_OK);
return false;
}
if (!mShaderPrograms->CompileLayerPrograms(mVideoIO->InputFrameWidth(), mVideoIO->InputFrameHeight(), sizeof(compilerErrorMessage), compilerErrorMessage))
{
MessageBoxA(NULL, compilerErrorMessage, "OpenGL shader failed to load or compile", MB_OK);
return false;
}
mCachedLayerRenderStates = mShaderPrograms->CommittedLayerStates();
mUseCommittedLayerStates = false;
mShaderPrograms->ResetTemporalHistoryState();
std::string rendererError;
if (!mRenderer->InitializeResources(
mVideoIO->InputFrameWidth(),
mVideoIO->InputFrameHeight(),
mVideoIO->CaptureTextureWidth(),
mVideoIO->OutputFrameWidth(),
mVideoIO->OutputFrameHeight(),
mVideoIO->OutputPackTextureWidth(),
if (!mRenderEngine->InitializeResources(
mVideoBackend->InputFrameWidth(),
mVideoBackend->InputFrameHeight(),
mVideoBackend->CaptureTextureWidth(),
mVideoBackend->OutputFrameWidth(),
mVideoBackend->OutputFrameHeight(),
mVideoBackend->OutputPackTextureWidth(),
rendererError))
{
MessageBoxA(NULL, rendererError.c_str(), "OpenGL initialization error.", MB_OK);
return false;
}
if (!mRenderEngine->CompileLayerPrograms(mVideoBackend->InputFrameWidth(), mVideoBackend->InputFrameHeight(), sizeof(compilerErrorMessage), compilerErrorMessage))
{
MessageBoxA(NULL, compilerErrorMessage, "OpenGL shader failed to load or compile", MB_OK);
return false;
}
mRuntimeStore->SetCompileStatus(true, "Shader layers compiled successfully.");
mUseCommittedLayerStates = false;
mRenderEngine->ResetTemporalHistoryState();
mRenderEngine->ResetShaderFeedbackState();
broadcastRuntimeState();
mRuntimeServices->BeginPolling(*mRuntimeHost);
return true;
@@ -265,7 +342,7 @@ bool OpenGLComposite::InitOpenGLState()
bool OpenGLComposite::Start()
{
return mVideoIO->Start();
return mVideoBackend->Start();
}
bool OpenGLComposite::Stop()
@@ -273,24 +350,18 @@ bool OpenGLComposite::Stop()
if (mRuntimeServices)
mRuntimeServices->Stop();
const bool wasExternalKeyingActive = mVideoIO->ExternalKeyingActive();
mVideoIO->Stop();
const bool wasExternalKeyingActive = mVideoBackend->ExternalKeyingActive();
mVideoBackend->Stop();
if (wasExternalKeyingActive)
PublishVideoIOStatus("External keying has been disabled.");
return true;
}
bool OpenGLComposite::ReloadShader()
bool OpenGLComposite::ReloadShader(bool preserveFeedbackState)
{
if (mRuntimeHost)
{
mRuntimeHost->SetCompileStatus(true, "Shader rebuild queued.");
mRuntimeHost->ClearReloadRequest();
}
RequestShaderBuild();
broadcastRuntimeState();
return true;
return mRuntimeCoordinator &&
ApplyRuntimeCoordinatorResult(mRuntimeCoordinator->RequestShaderReload(preserveFeedbackState));
}
bool OpenGLComposite::RequestScreenshot(std::string& error)
@@ -303,42 +374,209 @@ bool OpenGLComposite::RequestScreenshot(std::string& error)
void OpenGLComposite::renderEffect()
{
ProcessRuntimePollResults();
const bool hasInputSource = mVideoIO->HasInputSource();
std::vector<RuntimeRenderState> layerStates;
if (mUseCommittedLayerStates)
std::vector<RuntimeServices::AppliedOscUpdate> appliedOscUpdates;
std::vector<RuntimeServices::CompletedOscCommit> completedOscCommits;
if (mRuntimeHost && mRuntimeServices)
{
layerStates = mShaderPrograms->CommittedLayerStates();
if (mRuntimeHost)
mRuntimeHost->RefreshDynamicRenderStateFields(layerStates);
std::string oscError;
if (!mRuntimeServices->ApplyPendingOscUpdates(appliedOscUpdates, oscError) && !oscError.empty())
OutputDebugStringA(("OSC apply failed: " + oscError + "\n").c_str());
mRuntimeServices->ConsumeCompletedOscCommits(completedOscCommits);
}
else if (mRuntimeHost)
for (const RuntimeServices::CompletedOscCommit& completedCommit : completedOscCommits)
{
if (mRuntimeHost->TryGetLayerRenderStates(mVideoIO->InputFrameWidth(), mVideoIO->InputFrameHeight(), layerStates))
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)
{
mCachedLayerRenderStates = layerStates;
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
{
layerStates = mCachedLayerRenderStates;
mRuntimeHost->RefreshDynamicRenderStateFields(layerStates);
overlayIt->second.targetValue = update.targetValue;
overlayIt->second.lastUpdatedTime = oscNow;
overlayIt->second.generation += 1;
overlayIt->second.commitQueued = false;
}
pendingOscRouteKeys.insert(routeKey);
}
const unsigned historyCap = mRuntimeHost ? mRuntimeHost->GetMaxTemporalHistoryFrames() : 0;
mRenderPass->Render(
const auto applyOscOverlays = [&](std::vector<RuntimeRenderState>& states, bool allowCommit)
{
if (states.empty() || mOscOverlayStates.empty())
return;
const double smoothing = ClampOscAlpha(mRuntimeStore ? mRuntimeStore->GetConfiguredOscSmoothing() : 0.0);
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 = mVideoBackend->HasInputSource();
std::vector<RuntimeRenderState> layerStates;
mRenderEngine->ResolveRenderLayerStates(
mUseCommittedLayerStates.load(),
mVideoBackend->InputFrameWidth(),
mVideoBackend->InputFrameHeight(),
applyOscOverlays,
layerStates);
const unsigned historyCap = mRuntimeStore ? mRuntimeStore->GetConfiguredMaxTemporalHistoryFrames() : 0;
mRenderEngine->RenderLayerStack(
hasInputSource,
layerStates,
mVideoIO->InputFrameWidth(),
mVideoIO->InputFrameHeight(),
mVideoIO->CaptureTextureWidth(),
mVideoIO->InputPixelFormat(),
historyCap,
[this](const RuntimeRenderState& state, LayerProgram::TextBinding& textBinding, std::string& error) {
return mShaderPrograms->UpdateTextBindingTexture(state, textBinding, error);
},
[this](const RuntimeRenderState& state, unsigned availableSourceHistoryLength, unsigned availableTemporalHistoryLength) {
return mShaderPrograms->UpdateGlobalParamsBuffer(state, availableSourceHistoryLength, availableTemporalHistoryLength);
});
mVideoBackend->InputFrameWidth(),
mVideoBackend->InputFrameHeight(),
mVideoBackend->CaptureTextureWidth(),
mVideoBackend->InputPixelFormat(),
historyCap);
}
void OpenGLComposite::ProcessScreenshotRequest()
@@ -346,30 +584,14 @@ void OpenGLComposite::ProcessScreenshotRequest()
if (!mScreenshotRequested.exchange(false))
return;
const unsigned width = mVideoIO ? mVideoIO->OutputFrameWidth() : 0;
const unsigned height = mVideoIO ? mVideoIO->OutputFrameHeight() : 0;
const unsigned width = mVideoBackend ? mVideoBackend->OutputFrameWidth() : 0;
const unsigned height = mVideoBackend ? mVideoBackend->OutputFrameHeight() : 0;
if (width == 0 || height == 0)
return;
std::vector<unsigned char> bottomUpPixels(static_cast<std::size_t>(width) * height * 4);
std::vector<unsigned char> topDownPixels(bottomUpPixels.size());
glBindFramebuffer(GL_READ_FRAMEBUFFER, mRenderer->OutputFramebuffer());
glReadBuffer(GL_COLOR_ATTACHMENT0);
glPixelStorei(GL_PACK_ALIGNMENT, 1);
glPixelStorei(GL_PACK_ROW_LENGTH, 0);
glReadPixels(0, 0, width, height, GL_RGBA, GL_UNSIGNED_BYTE, bottomUpPixels.data());
glPixelStorei(GL_PACK_ALIGNMENT, 4);
const std::size_t rowBytes = static_cast<std::size_t>(width) * 4;
for (unsigned y = 0; y < height; ++y)
{
const unsigned sourceY = height - 1 - y;
std::copy(
bottomUpPixels.begin() + static_cast<std::ptrdiff_t>(sourceY * rowBytes),
bottomUpPixels.begin() + static_cast<std::ptrdiff_t>((sourceY + 1) * rowBytes),
topDownPixels.begin() + static_cast<std::ptrdiff_t>(y * rowBytes));
}
std::vector<unsigned char> topDownPixels;
if (!mRenderEngine->CaptureOutputFrameRgbaTopDown(width, height, topDownPixels))
return;
try
{
@@ -385,8 +607,8 @@ void OpenGLComposite::ProcessScreenshotRequest()
std::filesystem::path OpenGLComposite::BuildScreenshotPath() const
{
const std::filesystem::path root = mRuntimeHost && !mRuntimeHost->GetRuntimeRoot().empty()
? mRuntimeHost->GetRuntimeRoot()
const std::filesystem::path root = mRuntimeStore && !mRuntimeStore->GetRuntimeDataRoot().empty()
? mRuntimeStore->GetRuntimeDataRoot()
: std::filesystem::current_path();
const auto now = std::chrono::system_clock::now();
@@ -406,14 +628,13 @@ std::filesystem::path OpenGLComposite::BuildScreenshotPath() const
bool OpenGLComposite::ProcessRuntimePollResults()
{
if (!mRuntimeHost || !mRuntimeServices)
if (!mRuntimeServices)
return true;
const RuntimePollEvents events = mRuntimeServices->ConsumePollEvents();
if (events.failed)
{
mRuntimeHost->SetCompileStatus(false, events.error);
broadcastRuntimeState();
ApplyRuntimeCoordinatorResult(mRuntimeCoordinator->HandleRuntimePollFailure(events.error));
return false;
}
@@ -427,36 +648,98 @@ bool OpenGLComposite::ProcessRuntimePollResults()
return true;
char compilerErrorMessage[1024] = {};
if (!mShaderPrograms->CommitPreparedLayerPrograms(readyBuild, mVideoIO->InputFrameWidth(), mVideoIO->InputFrameHeight(), sizeof(compilerErrorMessage), compilerErrorMessage))
if (!mRenderEngine->ApplyPreparedShaderBuild(
readyBuild,
mVideoBackend->InputFrameWidth(),
mVideoBackend->InputFrameHeight(),
mRuntimeCoordinator && mRuntimeCoordinator->PreserveFeedbackOnNextShaderBuild(),
sizeof(compilerErrorMessage),
compilerErrorMessage))
{
mRuntimeHost->SetCompileStatus(false, compilerErrorMessage);
mUseCommittedLayerStates = true;
broadcastRuntimeState();
ApplyRuntimeCoordinatorResult(mRuntimeCoordinator->HandlePreparedShaderBuildFailure(compilerErrorMessage));
return false;
}
mUseCommittedLayerStates = false;
mCachedLayerRenderStates = mShaderPrograms->CommittedLayerStates();
mShaderPrograms->ResetTemporalHistoryState();
broadcastRuntimeState();
ApplyRuntimeCoordinatorResult(mRuntimeCoordinator->HandlePreparedShaderBuildSuccess());
return true;
}
mRuntimeHost->SetCompileStatus(true, "Shader rebuild queued.");
RequestShaderBuild();
broadcastRuntimeState();
ApplyRuntimeCoordinatorResult(mRuntimeCoordinator->HandleRuntimeReloadRequest());
return true;
}
void OpenGLComposite::RequestShaderBuild()
{
if (!mShaderBuildQueue || !mVideoIO)
if (!mShaderBuildQueue || !mVideoBackend)
return;
mUseCommittedLayerStates = true;
if (mRuntimeHost)
mRuntimeHost->ClearReloadRequest();
mShaderBuildQueue->RequestBuild(mVideoIO->InputFrameWidth(), mVideoIO->InputFrameHeight());
mShaderBuildQueue->RequestBuild(mVideoBackend->InputFrameWidth(), mVideoBackend->InputFrameHeight());
}
bool OpenGLComposite::ApplyRuntimeCoordinatorResult(const RuntimeCoordinatorResult& result, std::string* error)
{
if (!result.accepted)
{
if (error)
*error = result.errorMessage;
return false;
}
if (result.compileStatusChanged && mRuntimeStore)
mRuntimeStore->SetCompileStatus(result.compileStatusSucceeded, result.compileStatusMessage);
if (result.clearReloadRequest && mRuntimeStore)
mRuntimeStore->ClearReloadRequest();
switch (result.committedStateMode)
{
case RuntimeCoordinatorCommittedStateMode::UseCommittedStates:
mUseCommittedLayerStates = true;
break;
case RuntimeCoordinatorCommittedStateMode::UseLiveSnapshots:
mUseCommittedLayerStates = false;
break;
case RuntimeCoordinatorCommittedStateMode::Unchanged:
default:
break;
}
if (result.clearTransientOscState)
{
mOscOverlayStates.clear();
if (mRuntimeServices)
mRuntimeServices->ClearOscState();
}
ApplyRuntimeCoordinatorRenderReset(result.renderResetScope);
if (result.shaderBuildRequested)
RequestShaderBuild();
if (result.runtimeStateBroadcastRequired)
broadcastRuntimeState();
return true;
}
void OpenGLComposite::ApplyRuntimeCoordinatorRenderReset(RuntimeCoordinatorRenderResetScope resetScope)
{
if (!mRenderEngine)
return;
switch (resetScope)
{
case RuntimeCoordinatorRenderResetScope::TemporalHistoryOnly:
mRenderEngine->ResetTemporalHistoryState();
break;
case RuntimeCoordinatorRenderResetScope::TemporalHistoryAndFeedback:
mRenderEngine->ResetTemporalHistoryState();
mRenderEngine->ResetShaderFeedbackState();
break;
case RuntimeCoordinatorRenderResetScope::None:
default:
break;
}
}
void OpenGLComposite::broadcastRuntimeState()
@@ -465,11 +748,6 @@ void OpenGLComposite::broadcastRuntimeState()
mRuntimeServices->BroadcastState();
}
void OpenGLComposite::resetTemporalHistoryState()
{
mShaderPrograms->ResetTemporalHistoryState();
}
bool OpenGLComposite::CheckOpenGLExtensions()
{
return true;

View File

@@ -12,8 +12,10 @@
#include <comutil.h>
#include "GLExtensions.h"
#include "OpenGLRenderer.h"
#include "RuntimeCoordinator.h"
#include "RuntimeHost.h"
#include "RuntimeSnapshotProvider.h"
#include "RuntimeStore.h"
#include <functional>
#include <atomic>
@@ -23,14 +25,12 @@
#include <string>
#include <vector>
#include <deque>
#include <chrono>
class VideoIODevice;
class OpenGLVideoIOBridge;
class OpenGLRenderPass;
class OpenGLRenderPipeline;
class OpenGLShaderPrograms;
class RenderEngine;
class RuntimeServices;
class ShaderBuildQueue;
class VideoBackend;
class OpenGLComposite
@@ -43,7 +43,7 @@ public:
bool InitVideoIO();
bool Start();
bool Stop();
bool ReloadShader();
bool ReloadShader(bool preserveFeedbackState = false);
std::string GetRuntimeStateJson() const;
bool AddLayer(const std::string& shaderId, std::string& error);
bool RemoveLayer(const std::string& layerId, std::string& error);
@@ -59,34 +59,46 @@ public:
bool RequestScreenshot(std::string& error);
unsigned short GetControlServerPort() const;
unsigned short GetOscPort() const;
std::string GetOscBindAddress() const;
std::string GetControlUrl() const;
std::string GetDocsUrl() const;
std::string GetOscAddress() const;
void resizeGL(WORD width, WORD height);
void paintGL();
void paintGL(bool force = false);
private:
void resizeWindow(int width, int height);
bool CheckOpenGLExtensions();
void PublishVideoIOStatus(const std::string& statusMessage);
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;
HDC hGLDC;
HGLRC hGLRC;
CRITICAL_SECTION pMutex;
std::unique_ptr<VideoIODevice> mVideoIO;
std::unique_ptr<OpenGLRenderer> mRenderer;
std::unique_ptr<RuntimeHost> mRuntimeHost;
std::unique_ptr<OpenGLVideoIOBridge> mVideoIOBridge;
std::unique_ptr<OpenGLRenderPass> mRenderPass;
std::unique_ptr<OpenGLRenderPipeline> mRenderPipeline;
std::unique_ptr<OpenGLShaderPrograms> mShaderPrograms;
std::unique_ptr<RuntimeStore> mRuntimeStore;
std::unique_ptr<RuntimeCoordinator> mRuntimeCoordinator;
std::unique_ptr<RuntimeSnapshotProvider> mRuntimeSnapshotProvider;
std::unique_ptr<RenderEngine> mRenderEngine;
std::unique_ptr<ShaderBuildQueue> mShaderBuildQueue;
std::unique_ptr<RuntimeServices> mRuntimeServices;
std::vector<RuntimeRenderState> mCachedLayerRenderStates;
std::unique_ptr<VideoBackend> mVideoBackend;
std::map<std::string, OscOverlayState> mOscOverlayStates;
std::atomic<bool> mUseCommittedLayerStates;
std::atomic<bool> mScreenshotRequested;
@@ -94,10 +106,11 @@ private:
void renderEffect();
bool ProcessRuntimePollResults();
void RequestShaderBuild();
bool ApplyRuntimeCoordinatorResult(const RuntimeCoordinatorResult& result, std::string* error = nullptr);
void ApplyRuntimeCoordinatorRenderReset(RuntimeCoordinatorRenderResetScope resetScope);
void ProcessScreenshotRequest();
std::filesystem::path BuildScreenshotPath() const;
void broadcastRuntimeState();
void resetTemporalHistoryState();
};
#endif // __OPENGL_COMPOSITE_H__

View File

@@ -1,18 +1,24 @@
#include "OpenGLComposite.h"
#include "RuntimeServices.h"
std::string OpenGLComposite::GetRuntimeStateJson() const
{
return mRuntimeHost ? mRuntimeHost->BuildStateJson() : "{}";
return mRuntimeStore ? mRuntimeStore->BuildPersistentStateJson() : "{}";
}
unsigned short OpenGLComposite::GetControlServerPort() const
{
return mRuntimeHost ? mRuntimeHost->GetServerPort() : 0;
return mRuntimeStore ? mRuntimeStore->GetConfiguredControlServerPort() : 0;
}
unsigned short OpenGLComposite::GetOscPort() const
{
return mRuntimeHost ? mRuntimeHost->GetOscPort() : 0;
return mRuntimeStore ? mRuntimeStore->GetConfiguredOscPort() : 0;
}
std::string OpenGLComposite::GetOscBindAddress() const
{
return mRuntimeStore ? mRuntimeStore->GetConfiguredOscBindAddress() : "127.0.0.1";
}
std::string OpenGLComposite::GetControlUrl() const
@@ -27,67 +33,43 @@ std::string OpenGLComposite::GetDocsUrl() 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)
{
if (!mRuntimeHost->AddLayer(shaderId, error))
return false;
ReloadShader();
broadcastRuntimeState();
return true;
return mRuntimeCoordinator &&
ApplyRuntimeCoordinatorResult(mRuntimeCoordinator->AddLayer(shaderId), &error);
}
bool OpenGLComposite::RemoveLayer(const std::string& layerId, std::string& error)
{
if (!mRuntimeHost->RemoveLayer(layerId, error))
return false;
ReloadShader();
broadcastRuntimeState();
return true;
return mRuntimeCoordinator &&
ApplyRuntimeCoordinatorResult(mRuntimeCoordinator->RemoveLayer(layerId), &error);
}
bool OpenGLComposite::MoveLayer(const std::string& layerId, int direction, std::string& error)
{
if (!mRuntimeHost->MoveLayer(layerId, direction, error))
return false;
ReloadShader();
broadcastRuntimeState();
return true;
return mRuntimeCoordinator &&
ApplyRuntimeCoordinatorResult(mRuntimeCoordinator->MoveLayer(layerId, direction), &error);
}
bool OpenGLComposite::MoveLayerToIndex(const std::string& layerId, std::size_t targetIndex, std::string& error)
{
if (!mRuntimeHost->MoveLayerToIndex(layerId, targetIndex, error))
return false;
ReloadShader();
broadcastRuntimeState();
return true;
return mRuntimeCoordinator &&
ApplyRuntimeCoordinatorResult(mRuntimeCoordinator->MoveLayerToIndex(layerId, targetIndex), &error);
}
bool OpenGLComposite::SetLayerBypass(const std::string& layerId, bool bypassed, std::string& error)
{
if (!mRuntimeHost->SetLayerBypass(layerId, bypassed, error))
return false;
ReloadShader();
broadcastRuntimeState();
return true;
return mRuntimeCoordinator &&
ApplyRuntimeCoordinatorResult(mRuntimeCoordinator->SetLayerBypass(layerId, bypassed), &error);
}
bool OpenGLComposite::SetLayerShader(const std::string& layerId, const std::string& shaderId, std::string& error)
{
if (!mRuntimeHost->SetLayerShader(layerId, shaderId, error))
return false;
ReloadShader();
broadcastRuntimeState();
return true;
return mRuntimeCoordinator &&
ApplyRuntimeCoordinatorResult(mRuntimeCoordinator->SetLayerShader(layerId, shaderId), &error);
}
bool OpenGLComposite::UpdateLayerParameterJson(const std::string& layerId, const std::string& parameterId, const std::string& valueJson, std::string& error)
@@ -96,11 +78,8 @@ bool OpenGLComposite::UpdateLayerParameterJson(const std::string& layerId, const
if (!ParseJson(valueJson, parsedValue, error))
return false;
if (!mRuntimeHost->UpdateLayerParameter(layerId, parameterId, parsedValue, error))
return false;
broadcastRuntimeState();
return true;
return mRuntimeCoordinator &&
ApplyRuntimeCoordinatorResult(mRuntimeCoordinator->UpdateLayerParameter(layerId, parameterId, parsedValue), &error);
}
bool OpenGLComposite::UpdateLayerParameterByControlKeyJson(const std::string& layerKey, const std::string& parameterKey, const std::string& valueJson, std::string& error)
@@ -109,37 +88,24 @@ bool OpenGLComposite::UpdateLayerParameterByControlKeyJson(const std::string& la
if (!ParseJson(valueJson, parsedValue, error))
return false;
if (!mRuntimeHost->UpdateLayerParameterByControlKey(layerKey, parameterKey, parsedValue, error))
return false;
broadcastRuntimeState();
return true;
return mRuntimeCoordinator &&
ApplyRuntimeCoordinatorResult(mRuntimeCoordinator->UpdateLayerParameterByControlKey(layerKey, parameterKey, parsedValue), &error);
}
bool OpenGLComposite::ResetLayerParameters(const std::string& layerId, std::string& error)
{
if (!mRuntimeHost->ResetLayerParameters(layerId, error))
return false;
broadcastRuntimeState();
return true;
return mRuntimeCoordinator &&
ApplyRuntimeCoordinatorResult(mRuntimeCoordinator->ResetLayerParameters(layerId), &error);
}
bool OpenGLComposite::SaveStackPreset(const std::string& presetName, std::string& error)
{
if (!mRuntimeHost->SaveStackPreset(presetName, error))
return false;
broadcastRuntimeState();
return true;
return mRuntimeCoordinator &&
ApplyRuntimeCoordinatorResult(mRuntimeCoordinator->SaveStackPreset(presetName), &error);
}
bool OpenGLComposite::LoadStackPreset(const std::string& presetName, std::string& error)
{
if (!mRuntimeHost->LoadStackPreset(presetName, error))
return false;
ReloadShader();
broadcastRuntimeState();
return true;
return mRuntimeCoordinator &&
ApplyRuntimeCoordinatorResult(mRuntimeCoordinator->LoadStackPreset(presetName), &error);
}

View File

@@ -0,0 +1,311 @@
#include "RenderEngine.h"
#include <gl/gl.h>
#include <algorithm>
#include <cstddef>
RenderEngine::RenderEngine(
RuntimeSnapshotProvider& runtimeSnapshotProvider,
HealthTelemetry& healthTelemetry,
CRITICAL_SECTION& mutex,
HDC hdc,
HGLRC hglrc,
RenderEffectCallback renderEffect,
ScreenshotCallback screenshotReady,
PreviewPaintCallback previewPaint) :
mRenderer(),
mRenderPass(mRenderer),
mRenderPipeline(mRenderer, runtimeSnapshotProvider, healthTelemetry, std::move(renderEffect), std::move(screenshotReady), std::move(previewPaint)),
mShaderPrograms(mRenderer, runtimeSnapshotProvider),
mRuntimeSnapshotProvider(runtimeSnapshotProvider),
mMutex(mutex),
mHdc(hdc),
mHglrc(hglrc)
{
}
RenderEngine::~RenderEngine()
{
mRenderer.DestroyResources();
}
bool RenderEngine::CompileDecodeShader(int errorMessageSize, char* errorMessage)
{
return mShaderPrograms.CompileDecodeShader(errorMessageSize, errorMessage);
}
bool RenderEngine::CompileOutputPackShader(int errorMessageSize, char* errorMessage)
{
return mShaderPrograms.CompileOutputPackShader(errorMessageSize, errorMessage);
}
bool RenderEngine::InitializeResources(
unsigned inputFrameWidth,
unsigned inputFrameHeight,
unsigned captureTextureWidth,
unsigned outputFrameWidth,
unsigned outputFrameHeight,
unsigned outputPackTextureWidth,
std::string& error)
{
return mRenderer.InitializeResources(
inputFrameWidth,
inputFrameHeight,
captureTextureWidth,
outputFrameWidth,
outputFrameHeight,
outputPackTextureWidth,
error);
}
bool RenderEngine::CompileLayerPrograms(unsigned inputFrameWidth, unsigned inputFrameHeight, int errorMessageSize, char* errorMessage)
{
return mShaderPrograms.CompileLayerPrograms(inputFrameWidth, inputFrameHeight, errorMessageSize, errorMessage);
}
bool RenderEngine::CommitPreparedLayerPrograms(const PreparedShaderBuild& preparedBuild, unsigned inputFrameWidth, unsigned inputFrameHeight, int errorMessageSize, char* errorMessage)
{
return mShaderPrograms.CommitPreparedLayerPrograms(preparedBuild, inputFrameWidth, inputFrameHeight, errorMessageSize, errorMessage);
}
bool RenderEngine::ApplyPreparedShaderBuild(
const PreparedShaderBuild& preparedBuild,
unsigned inputFrameWidth,
unsigned inputFrameHeight,
bool preserveFeedbackState,
int errorMessageSize,
char* errorMessage)
{
if (!CommitPreparedLayerPrograms(preparedBuild, inputFrameWidth, inputFrameHeight, errorMessageSize, errorMessage))
return false;
mCachedLayerRenderStates = mShaderPrograms.CommittedLayerStates();
mCachedRenderStateVersion = preparedBuild.renderSnapshot.versions.renderStateVersion;
mCachedParameterStateVersion = preparedBuild.renderSnapshot.versions.parameterStateVersion;
mCachedRenderStateWidth = preparedBuild.renderSnapshot.outputWidth;
mCachedRenderStateHeight = preparedBuild.renderSnapshot.outputHeight;
ResetTemporalHistoryState();
if (!preserveFeedbackState)
ResetShaderFeedbackState();
return true;
}
const std::vector<RuntimeRenderState>& RenderEngine::CommittedLayerStates() const
{
return mShaderPrograms.CommittedLayerStates();
}
void RenderEngine::ResetTemporalHistoryState()
{
mShaderPrograms.ResetTemporalHistoryState();
}
void RenderEngine::ResetShaderFeedbackState()
{
mShaderPrograms.ResetShaderFeedbackState();
}
void RenderEngine::ResizeView(int width, int height)
{
mRenderer.ResizeView(width, height);
}
bool RenderEngine::TryPresentPreview(bool force, unsigned previewFps, unsigned outputFrameWidth, unsigned outputFrameHeight)
{
if (!force)
{
if (previewFps == 0)
return false;
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 false;
}
}
if (!TryEnterCriticalSection(&mMutex))
return false;
mRenderer.PresentToWindow(mHdc, outputFrameWidth, outputFrameHeight);
mLastPreviewPresentTime = std::chrono::steady_clock::now();
LeaveCriticalSection(&mMutex);
return true;
}
bool RenderEngine::TryUploadInputFrame(const VideoIOFrame& inputFrame, const VideoIOState& videoState)
{
if (inputFrame.hasNoInputSource || inputFrame.bytes == nullptr)
return true;
const long textureSize = inputFrame.rowBytes * static_cast<long>(inputFrame.height);
if (!TryEnterCriticalSection(&mMutex))
return false;
wglMakeCurrent(mHdc, mHglrc);
glPixelStorei(GL_UNPACK_ALIGNMENT, 4);
glPixelStorei(GL_UNPACK_ROW_LENGTH, 0);
glBindBuffer(GL_PIXEL_UNPACK_BUFFER, mRenderer.TextureUploadBuffer());
glBufferData(GL_PIXEL_UNPACK_BUFFER, textureSize, inputFrame.bytes, GL_DYNAMIC_DRAW);
glBindTexture(GL_TEXTURE_2D, mRenderer.CaptureTexture());
if (inputFrame.pixelFormat == VideoIOPixelFormat::V210)
glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, videoState.captureTextureWidth, videoState.inputFrameSize.height, GL_RGBA, GL_UNSIGNED_BYTE, NULL);
else
glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, videoState.captureTextureWidth, videoState.inputFrameSize.height, GL_BGRA, GL_UNSIGNED_INT_8_8_8_8_REV, NULL);
glBindTexture(GL_TEXTURE_2D, 0);
glBindBuffer(GL_PIXEL_UNPACK_BUFFER, 0);
wglMakeCurrent(NULL, NULL);
LeaveCriticalSection(&mMutex);
return true;
}
bool RenderEngine::RenderOutputFrame(const RenderPipelineFrameContext& context, VideoIOOutputFrame& outputFrame)
{
EnterCriticalSection(&mMutex);
wglMakeCurrent(mHdc, mHglrc);
const bool rendered = mRenderPipeline.RenderFrame(context, outputFrame);
wglMakeCurrent(NULL, NULL);
LeaveCriticalSection(&mMutex);
return rendered;
}
bool RenderEngine::ResolveRenderLayerStates(
bool useCommittedLayerStates,
unsigned renderWidth,
unsigned renderHeight,
OverlayApplier overlayApplier,
std::vector<RuntimeRenderState>& layerStates)
{
layerStates.clear();
if (useCommittedLayerStates)
{
layerStates = mShaderPrograms.CommittedLayerStates();
if (overlayApplier)
overlayApplier(layerStates, false);
mRuntimeSnapshotProvider.RefreshDynamicRenderStateFields(layerStates);
return true;
}
const RuntimeSnapshotVersions versions = mRuntimeSnapshotProvider.GetVersions();
const bool renderStateCacheValid =
!mCachedLayerRenderStates.empty() &&
mCachedRenderStateVersion == versions.renderStateVersion &&
mCachedRenderStateWidth == renderWidth &&
mCachedRenderStateHeight == renderHeight;
if (renderStateCacheValid)
{
RuntimeRenderStateSnapshot renderSnapshot;
renderSnapshot.outputWidth = renderWidth;
renderSnapshot.outputHeight = renderHeight;
renderSnapshot.versions.renderStateVersion = mCachedRenderStateVersion;
renderSnapshot.versions.parameterStateVersion = mCachedParameterStateVersion;
renderSnapshot.states = mCachedLayerRenderStates;
if (overlayApplier)
overlayApplier(renderSnapshot.states, true);
if (mCachedParameterStateVersion != versions.parameterStateVersion &&
mRuntimeSnapshotProvider.TryRefreshSnapshotParameters(renderSnapshot))
{
mCachedParameterStateVersion = renderSnapshot.versions.parameterStateVersion;
if (overlayApplier)
overlayApplier(renderSnapshot.states, true);
}
mCachedLayerRenderStates = renderSnapshot.states;
layerStates = renderSnapshot.states;
mRuntimeSnapshotProvider.RefreshDynamicRenderStateFields(layerStates);
return true;
}
RuntimeRenderStateSnapshot renderSnapshot;
if (mRuntimeSnapshotProvider.TryGetRenderStateSnapshot(renderWidth, renderHeight, renderSnapshot))
{
mCachedLayerRenderStates = renderSnapshot.states;
mCachedRenderStateVersion = renderSnapshot.versions.renderStateVersion;
mCachedParameterStateVersion = renderSnapshot.versions.parameterStateVersion;
mCachedRenderStateWidth = renderSnapshot.outputWidth;
mCachedRenderStateHeight = renderSnapshot.outputHeight;
if (overlayApplier)
overlayApplier(mCachedLayerRenderStates, true);
layerStates = mCachedLayerRenderStates;
return true;
}
if (overlayApplier)
overlayApplier(mCachedLayerRenderStates, true);
layerStates = mCachedLayerRenderStates;
mRuntimeSnapshotProvider.RefreshDynamicRenderStateFields(layerStates);
return !layerStates.empty();
}
void RenderEngine::RenderLayerStack(
bool hasInputSource,
const std::vector<RuntimeRenderState>& layerStates,
unsigned inputFrameWidth,
unsigned inputFrameHeight,
unsigned captureTextureWidth,
VideoIOPixelFormat inputPixelFormat,
unsigned historyCap)
{
mRenderPass.Render(
hasInputSource,
layerStates,
inputFrameWidth,
inputFrameHeight,
captureTextureWidth,
inputPixelFormat,
historyCap,
[this](const RuntimeRenderState& state, OpenGLRenderer::LayerProgram::TextBinding& textBinding, std::string& error) {
return mShaderPrograms.UpdateTextBindingTexture(state, textBinding, error);
},
[this](const RuntimeRenderState& state, unsigned availableSourceHistoryLength, unsigned availableTemporalHistoryLength, bool feedbackAvailable) {
return mShaderPrograms.UpdateGlobalParamsBuffer(state, availableSourceHistoryLength, availableTemporalHistoryLength, feedbackAvailable);
});
}
bool RenderEngine::ReadOutputFrameRgba(unsigned width, unsigned height, std::vector<unsigned char>& bottomUpPixels)
{
if (width == 0 || height == 0)
return false;
EnterCriticalSection(&mMutex);
wglMakeCurrent(mHdc, mHglrc);
bottomUpPixels.resize(static_cast<std::size_t>(width) * height * 4);
glBindFramebuffer(GL_READ_FRAMEBUFFER, mRenderer.OutputFramebuffer());
glReadBuffer(GL_COLOR_ATTACHMENT0);
glPixelStorei(GL_PACK_ALIGNMENT, 1);
glPixelStorei(GL_PACK_ROW_LENGTH, 0);
glReadPixels(0, 0, width, height, GL_RGBA, GL_UNSIGNED_BYTE, bottomUpPixels.data());
glPixelStorei(GL_PACK_ALIGNMENT, 4);
wglMakeCurrent(NULL, NULL);
LeaveCriticalSection(&mMutex);
return true;
}
bool RenderEngine::CaptureOutputFrameRgbaTopDown(unsigned width, unsigned height, std::vector<unsigned char>& topDownPixels)
{
std::vector<unsigned char> bottomUpPixels;
if (!ReadOutputFrameRgba(width, height, bottomUpPixels))
return false;
topDownPixels.resize(bottomUpPixels.size());
const std::size_t rowBytes = static_cast<std::size_t>(width) * 4;
for (unsigned y = 0; y < height; ++y)
{
const unsigned sourceY = height - 1 - y;
std::copy(
bottomUpPixels.begin() + static_cast<std::ptrdiff_t>(sourceY * rowBytes),
bottomUpPixels.begin() + static_cast<std::ptrdiff_t>((sourceY + 1) * rowBytes),
topDownPixels.begin() + static_cast<std::ptrdiff_t>(y * rowBytes));
}
return true;
}

View File

@@ -0,0 +1,96 @@
#pragma once
#include "OpenGLRenderPass.h"
#include "OpenGLRenderPipeline.h"
#include "OpenGLRenderer.h"
#include "OpenGLShaderPrograms.h"
#include "HealthTelemetry.h"
#include "RuntimeSnapshotProvider.h"
#include <windows.h>
#include <cstdint>
#include <chrono>
#include <functional>
#include <string>
#include <vector>
class RenderEngine
{
public:
using RenderEffectCallback = std::function<void()>;
using ScreenshotCallback = std::function<void()>;
using PreviewPaintCallback = std::function<void()>;
using OverlayApplier = std::function<void(std::vector<RuntimeRenderState>& states, bool allowCommit)>;
RenderEngine(
RuntimeSnapshotProvider& runtimeSnapshotProvider,
HealthTelemetry& healthTelemetry,
CRITICAL_SECTION& mutex,
HDC hdc,
HGLRC hglrc,
RenderEffectCallback renderEffect,
ScreenshotCallback screenshotReady,
PreviewPaintCallback previewPaint);
~RenderEngine();
bool CompileDecodeShader(int errorMessageSize, char* errorMessage);
bool CompileOutputPackShader(int errorMessageSize, char* errorMessage);
bool InitializeResources(
unsigned inputFrameWidth,
unsigned inputFrameHeight,
unsigned captureTextureWidth,
unsigned outputFrameWidth,
unsigned outputFrameHeight,
unsigned outputPackTextureWidth,
std::string& error);
bool CompileLayerPrograms(unsigned inputFrameWidth, unsigned inputFrameHeight, int errorMessageSize, char* errorMessage);
bool CommitPreparedLayerPrograms(const PreparedShaderBuild& preparedBuild, unsigned inputFrameWidth, unsigned inputFrameHeight, int errorMessageSize, char* errorMessage);
bool ApplyPreparedShaderBuild(
const PreparedShaderBuild& preparedBuild,
unsigned inputFrameWidth,
unsigned inputFrameHeight,
bool preserveFeedbackState,
int errorMessageSize,
char* errorMessage);
const std::vector<RuntimeRenderState>& CommittedLayerStates() const;
void ResetTemporalHistoryState();
void ResetShaderFeedbackState();
void ResizeView(int width, int height);
bool TryPresentPreview(bool force, unsigned previewFps, unsigned outputFrameWidth, unsigned outputFrameHeight);
bool TryUploadInputFrame(const VideoIOFrame& inputFrame, const VideoIOState& videoState);
bool RenderOutputFrame(const RenderPipelineFrameContext& context, VideoIOOutputFrame& outputFrame);
bool ResolveRenderLayerStates(
bool useCommittedLayerStates,
unsigned renderWidth,
unsigned renderHeight,
OverlayApplier overlayApplier,
std::vector<RuntimeRenderState>& layerStates);
void RenderLayerStack(
bool hasInputSource,
const std::vector<RuntimeRenderState>& layerStates,
unsigned inputFrameWidth,
unsigned inputFrameHeight,
unsigned captureTextureWidth,
VideoIOPixelFormat inputPixelFormat,
unsigned historyCap);
bool ReadOutputFrameRgba(unsigned width, unsigned height, std::vector<unsigned char>& bottomUpPixels);
bool CaptureOutputFrameRgbaTopDown(unsigned width, unsigned height, std::vector<unsigned char>& topDownPixels);
private:
OpenGLRenderer mRenderer;
OpenGLRenderPass mRenderPass;
OpenGLRenderPipeline mRenderPipeline;
OpenGLShaderPrograms mShaderPrograms;
RuntimeSnapshotProvider& mRuntimeSnapshotProvider;
CRITICAL_SECTION& mMutex;
HDC mHdc;
HGLRC mHglrc;
std::vector<RuntimeRenderState> mCachedLayerRenderStates;
uint64_t mCachedRenderStateVersion = 0;
uint64_t mCachedParameterStateVersion = 0;
unsigned mCachedRenderStateWidth = 0;
unsigned mCachedRenderStateHeight = 0;
std::chrono::steady_clock::time_point mLastPreviewPresentTime;
};

View File

@@ -45,7 +45,7 @@ void OpenGLRenderPass::Render(
}
else
{
const std::vector<RenderPassDescriptor> passes = BuildLayerPassDescriptors(layerStates, layerPrograms);
const std::vector<RenderPassDescriptor>& passes = BuildLayerPassDescriptors(layerStates, layerPrograms);
for (const RenderPassDescriptor& pass : passes)
{
RenderLayerPass(
@@ -59,6 +59,7 @@ void OpenGLRenderPass::Render(
}
mRenderer.TemporalHistory().PushSourceFramebuffer(mRenderer.DecodeFramebuffer(), inputFrameWidth, inputFrameHeight);
mRenderer.FeedbackBuffers().FinalizeFrame();
}
void OpenGLRenderPass::RenderDecodePass(unsigned inputFrameWidth, unsigned inputFrameHeight, unsigned captureTextureWidth, VideoIOPixelFormat inputPixelFormat)
@@ -71,9 +72,9 @@ void OpenGLRenderPass::RenderDecodePass(unsigned inputFrameWidth, unsigned input
glBindVertexArray(mRenderer.FullscreenVertexArray());
glUseProgram(mRenderer.DecodeProgram());
const GLint packedResolutionLocation = glGetUniformLocation(mRenderer.DecodeProgram(), "uPackedVideoResolution");
const GLint decodedResolutionLocation = glGetUniformLocation(mRenderer.DecodeProgram(), "uDecodedVideoResolution");
const GLint inputPixelFormatLocation = glGetUniformLocation(mRenderer.DecodeProgram(), "uInputPixelFormat");
const GLint packedResolutionLocation = mRenderer.DecodePackedResolutionLocation();
const GLint decodedResolutionLocation = mRenderer.DecodeDecodedResolutionLocation();
const GLint inputPixelFormatLocation = mRenderer.DecodeInputPixelFormatLocation();
if (packedResolutionLocation >= 0)
glUniform2f(packedResolutionLocation, static_cast<float>(captureTextureWidth), static_cast<float>(inputFrameHeight));
if (decodedResolutionLocation >= 0)
@@ -96,7 +97,8 @@ std::vector<RenderPassDescriptor> OpenGLRenderPass::BuildLayerPassDescriptors(
// Flatten the layer stack into concrete GL passes. A layer may now contain
// several shader passes, but the outer stack still sees one visible output
// per layer.
std::vector<RenderPassDescriptor> passes;
std::vector<RenderPassDescriptor>& passes = mPassScratch;
passes.clear();
const std::size_t passCount = layerStates.size() < layerPrograms.size() ? layerStates.size() : layerPrograms.size();
std::size_t descriptorCount = 0;
for (std::size_t index = 0; index < passCount; ++index)
@@ -186,6 +188,7 @@ std::vector<RenderPassDescriptor> OpenGLRenderPass::BuildLayerPassDescriptors(
pass.passId = passProgram.passId;
pass.layerId = state.layerId;
pass.shaderId = state.shaderId;
pass.layerInputTexture = layerInputTexture;
pass.sourceTexture = passSourceTexture;
pass.sourceFramebuffer = passIndex == 0 ? layerInputFramebuffer : passSourceFramebuffer;
pass.destinationTexture = passDestinationTexture;
@@ -194,6 +197,7 @@ std::vector<RenderPassDescriptor> OpenGLRenderPass::BuildLayerPassDescriptors(
pass.passProgram = &passProgram;
pass.layerState = &state;
pass.capturePreLayerHistory = passIndex == 0 && state.temporalHistorySource == TemporalHistorySource::PreLayerInput;
pass.captureFeedbackWrite = state.feedback.enabled && passProgram.passId == state.feedback.writePassId;
passes.push_back(pass);
// A later pass can reference either the explicit output name or the
@@ -223,6 +227,7 @@ void OpenGLRenderPass::RenderLayerPass(
return;
RenderShaderProgram(
pass.layerInputTexture,
pass.sourceTexture,
pass.destinationFramebuffer,
*pass.passProgram,
@@ -235,9 +240,12 @@ void OpenGLRenderPass::RenderLayerPass(
if (pass.capturePreLayerHistory)
mRenderer.TemporalHistory().PushPreLayerFramebuffer(pass.layerId, pass.sourceFramebuffer, inputFrameWidth, inputFrameHeight);
if (pass.captureFeedbackWrite)
mRenderer.FeedbackBuffers().CaptureFeedbackFramebuffer(pass.layerId, pass.destinationFramebuffer, inputFrameWidth, inputFrameHeight);
}
void OpenGLRenderPass::RenderShaderProgram(
GLuint layerInputTexture,
GLuint sourceTexture,
GLuint destinationFrameBuffer,
PassProgram& passProgram,
@@ -260,14 +268,19 @@ void OpenGLRenderPass::RenderShaderProgram(
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
const std::vector<GLuint> sourceHistoryTextures = mRenderer.TemporalHistory().ResolveSourceHistoryTextures(sourceTexture, state.isTemporal ? historyCap : 0);
const std::vector<GLuint> temporalHistoryTextures = mRenderer.TemporalHistory().ResolveTemporalHistoryTextures(state, sourceTexture, state.isTemporal ? historyCap : 0);
const GLuint feedbackTexture = mRenderer.FeedbackBuffers().ResolveReadTexture(state);
const ShaderTextureBindings::RuntimeTextureBindingPlan texturePlan =
mTextureBindings.BuildLayerRuntimeBindingPlan(passProgram, sourceTexture, sourceHistoryTextures, temporalHistoryTextures);
mTextureBindings.BuildLayerRuntimeBindingPlan(passProgram, sourceTexture, layerInputTexture, state, feedbackTexture, sourceHistoryTextures, temporalHistoryTextures);
mTextureBindings.BindRuntimeTexturePlan(texturePlan);
glBindVertexArray(mRenderer.FullscreenVertexArray());
glUseProgram(passProgram.program);
// The UBO is shared by every pass in a layer; texture routing is what
// changes from pass to pass.
updateGlobalParams(state, mRenderer.TemporalHistory().SourceAvailableCount(), mRenderer.TemporalHistory().AvailableCountForLayer(state.layerId));
updateGlobalParams(
state,
mRenderer.TemporalHistory().SourceAvailableCount(),
mRenderer.TemporalHistory().AvailableCountForLayer(state.layerId),
mRenderer.FeedbackBuffers().FeedbackAvailable(state));
glDrawArrays(GL_TRIANGLES, 0, 3);
glUseProgram(0);
glBindVertexArray(0);

View File

@@ -16,7 +16,7 @@ public:
using LayerProgram = OpenGLRenderer::LayerProgram;
using PassProgram = OpenGLRenderer::LayerProgram::PassProgram;
using TextBindingUpdater = std::function<bool(const RuntimeRenderState&, LayerProgram::TextBinding&, std::string&)>;
using GlobalParamsUpdater = std::function<bool(const RuntimeRenderState&, unsigned, unsigned)>;
using GlobalParamsUpdater = std::function<bool(const RuntimeRenderState&, unsigned, unsigned, bool)>;
explicit OpenGLRenderPass(OpenGLRenderer& renderer);
@@ -44,6 +44,7 @@ private:
const TextBindingUpdater& updateTextBinding,
const GlobalParamsUpdater& updateGlobalParams);
void RenderShaderProgram(
GLuint layerInputTexture,
GLuint sourceTexture,
GLuint destinationFrameBuffer,
PassProgram& passProgram,
@@ -56,4 +57,5 @@ private:
OpenGLRenderer& mRenderer;
ShaderTextureBindings mTextureBindings;
mutable std::vector<RenderPassDescriptor> mPassScratch;
};

View File

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

View File

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

View File

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

View File

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

View File

@@ -28,6 +28,7 @@ struct RenderPassDescriptor
std::string passId;
std::string layerId;
std::string shaderId;
GLuint layerInputTexture = 0;
GLuint sourceTexture = 0;
GLuint sourceFramebuffer = 0;
GLuint destinationTexture = 0;
@@ -36,4 +37,5 @@ struct RenderPassDescriptor
OpenGLRenderer::LayerProgram::PassProgram* passProgram = nullptr;
const RuntimeRenderState* layerState = nullptr;
bool capturePreLayerHistory = false;
bool captureFeedbackWrite = false;
};

View File

@@ -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;
}

View File

@@ -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;
};

View File

@@ -19,7 +19,8 @@ bool TemporalHistoryBuffers::ValidateTextureUnitBudget(const std::vector<Runtime
++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)
requiredUnits = layerRequiredUnits;
}

View File

@@ -62,6 +62,8 @@ PFNGLGENBUFFERSPROC glGenBuffers;
PFNGLDELETEBUFFERSPROC glDeleteBuffers;
PFNGLBINDBUFFERPROC glBindBuffer;
PFNGLBUFFERDATAPROC glBufferData;
PFNGLMAPBUFFERPROC glMapBuffer;
PFNGLUNMAPBUFFERPROC glUnmapBuffer;
PFNGLBUFFERSUBDATAPROC glBufferSubData;
PFNGLBINDBUFFERBASEPROC glBindBufferBase;
PFNGLACTIVETEXTUREPROC glActiveTexture;
@@ -131,6 +133,8 @@ bool ResolveGLExtensions()
glDeleteBuffers = (PFNGLDELETEBUFFERSPROC) wglGetProcAddress("glDeleteBuffers");
glBindBuffer = (PFNGLBINDBUFFERPROC) wglGetProcAddress("glBindBuffer");
glBufferData = (PFNGLBUFFERDATAPROC) wglGetProcAddress("glBufferData");
glMapBuffer = (PFNGLMAPBUFFERPROC) wglGetProcAddress("glMapBuffer");
glUnmapBuffer = (PFNGLUNMAPBUFFERPROC) wglGetProcAddress("glUnmapBuffer");
glBufferSubData = (PFNGLBUFFERSUBDATAPROC) wglGetProcAddress("glBufferSubData");
glBindBufferBase = (PFNGLBINDBUFFERBASEPROC) wglGetProcAddress("glBindBufferBase");
glActiveTexture = (PFNGLACTIVETEXTUREPROC) wglGetProcAddress("glActiveTexture");
@@ -176,6 +180,8 @@ bool ResolveGLExtensions()
&& glDeleteBuffers
&& glBindBuffer
&& glBufferData
&& glMapBuffer
&& glUnmapBuffer
&& glBufferSubData
&& glBindBufferBase
&& glActiveTexture

View File

@@ -89,6 +89,11 @@
#define GL_EXTERNAL_VIRTUAL_MEMORY_BUFFER_AMD 0x9160
#define GL_SYNC_GPU_COMMANDS_COMPLETE 0x9117
#define GL_SYNC_FLUSH_COMMANDS_BIT 0x00000001
#define GL_ALREADY_SIGNALED 0x911A
#define GL_TIMEOUT_EXPIRED 0x911B
#define GL_CONDITION_SATISFIED 0x911C
#define GL_WAIT_FAILED 0x911D
#define GL_READ_ONLY 0x88B8
typedef struct __GLsync *GLsync;
typedef unsigned __int64 GLuint64;
@@ -100,6 +105,8 @@ typedef void (APIENTRYP PFNGLBINDBUFFERPROC) (GLenum target, GLuint buffer);
typedef void (APIENTRYP PFNGLDELETEBUFFERSPROC) (GLsizei n, const GLuint *buffers);
typedef void (APIENTRYP PFNGLGENBUFFERSPROC) (GLsizei n, GLuint *buffers);
typedef void (APIENTRYP PFNGLBUFFERDATAPROC) (GLenum target, GLsizeiptr size, const GLvoid *data, GLenum usage);
typedef GLvoid* (APIENTRYP PFNGLMAPBUFFERPROC) (GLenum target, GLenum access);
typedef GLboolean (APIENTRYP PFNGLUNMAPBUFFERPROC) (GLenum target);
typedef void (APIENTRYP PFNGLATTACHSHADERPROC) (GLuint program, GLuint shader);
typedef void (APIENTRYP PFNGLCOMPILESHADERPROC) (GLuint shader);
typedef GLuint (APIENTRYP PFNGLCREATEPROGRAMPROC) (void);
@@ -159,6 +166,8 @@ extern PFNGLGENBUFFERSPROC glGenBuffers;
extern PFNGLDELETEBUFFERSPROC glDeleteBuffers;
extern PFNGLBINDBUFFERPROC glBindBuffer;
extern PFNGLBUFFERDATAPROC glBufferData;
extern PFNGLMAPBUFFERPROC glMapBuffer;
extern PFNGLUNMAPBUFFERPROC glUnmapBuffer;
extern PFNGLBUFFERSUBDATAPROC glBufferSubData;
extern PFNGLBINDBUFFERBASEPROC glBindBufferBase;
extern PFNGLACTIVETEXTUREPROC glActiveTexture;

View File

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

View File

@@ -63,6 +63,7 @@ bool OpenGLRenderer::InitializeResources(unsigned inputFrameWidth, unsigned inpu
glBindBufferBase(GL_UNIFORM_BUFFER, kGlobalParamsBindingPoint, mGlobalParamsUBO);
glBindBuffer(GL_UNIFORM_BUFFER, 0);
mResourcesInitialized = true;
return true;
}
@@ -71,6 +72,9 @@ void OpenGLRenderer::SetDecodeShaderProgram(GLuint program, GLuint vertexShader,
mDecodeProgram = program;
mDecodeVertexShader = vertexShader;
mDecodeFragmentShader = fragmentShader;
mDecodePackedResolutionLocation = program != 0 ? glGetUniformLocation(program, "uPackedVideoResolution") : -1;
mDecodeDecodedResolutionLocation = program != 0 ? glGetUniformLocation(program, "uDecodedVideoResolution") : -1;
mDecodeInputPixelFormatLocation = program != 0 ? glGetUniformLocation(program, "uInputPixelFormat") : -1;
}
void OpenGLRenderer::SetOutputPackShaderProgram(GLuint program, GLuint vertexShader, GLuint fragmentShader)
@@ -78,6 +82,9 @@ void OpenGLRenderer::SetOutputPackShaderProgram(GLuint program, GLuint vertexSha
mOutputPackProgram = program;
mOutputPackVertexShader = vertexShader;
mOutputPackFragmentShader = fragmentShader;
mOutputPackResolutionLocation = program != 0 ? glGetUniformLocation(program, "uOutputVideoResolution") : -1;
mOutputPackActiveWordsLocation = program != 0 ? glGetUniformLocation(program, "uActiveV210Words") : -1;
mOutputPackFormatLocation = program != 0 ? glGetUniformLocation(program, "uOutputPackFormat") : -1;
}
bool OpenGLRenderer::ReserveTemporaryRenderTargets(std::size_t count, unsigned width, unsigned height, std::string& error)
@@ -151,8 +158,10 @@ void OpenGLRenderer::DestroyResources()
mCaptureTexture = 0;
mTextureUploadBuffer = 0;
mGlobalParamsUBOSize = 0;
mResourcesInitialized = false;
mTemporalHistory.DestroyResources();
mFeedbackBuffers.DestroyResources();
DestroyLayerPrograms();
DestroyDecodeShaderProgram();
DestroyOutputPackShaderProgram();
@@ -217,6 +226,9 @@ void OpenGLRenderer::DestroyDecodeShaderProgram()
glDeleteProgram(mDecodeProgram);
mDecodeProgram = 0;
}
mDecodePackedResolutionLocation = -1;
mDecodeDecodedResolutionLocation = -1;
mDecodeInputPixelFormatLocation = -1;
if (mDecodeFragmentShader != 0)
{
@@ -238,6 +250,9 @@ void OpenGLRenderer::DestroyOutputPackShaderProgram()
glDeleteProgram(mOutputPackProgram);
mOutputPackProgram = 0;
}
mOutputPackResolutionLocation = -1;
mOutputPackActiveWordsLocation = -1;
mOutputPackFormatLocation = -1;
if (mOutputPackFragmentShader != 0)
{

View File

@@ -2,6 +2,7 @@
#include "GLExtensions.h"
#include "RenderTargetPool.h"
#include "ShaderFeedbackBuffers.h"
#include "ShaderTypes.h"
#include "TemporalHistoryBuffers.h"
@@ -70,8 +71,15 @@ public:
GLuint GlobalParamsUBO() const { return mGlobalParamsUBO; }
GLuint DecodeProgram() const { return mDecodeProgram; }
GLuint OutputPackProgram() const { return mOutputPackProgram; }
GLint DecodePackedResolutionLocation() const { return mDecodePackedResolutionLocation; }
GLint DecodeDecodedResolutionLocation() const { return mDecodeDecodedResolutionLocation; }
GLint DecodeInputPixelFormatLocation() const { return mDecodeInputPixelFormatLocation; }
GLint OutputPackResolutionLocation() const { return mOutputPackResolutionLocation; }
GLint OutputPackActiveWordsLocation() const { return mOutputPackActiveWordsLocation; }
GLint OutputPackFormatLocation() const { return mOutputPackFormatLocation; }
GLsizeiptr GlobalParamsUBOSize() const { return mGlobalParamsUBOSize; }
void SetGlobalParamsUBOSize(GLsizeiptr size) { mGlobalParamsUBOSize = size; }
bool ResourcesInitialized() const { return mResourcesInitialized; }
void ReplaceLayerPrograms(std::vector<LayerProgram>& newPrograms) { mLayerPrograms.swap(newPrograms); }
std::vector<LayerProgram>& LayerPrograms() { return mLayerPrograms; }
const std::vector<LayerProgram>& LayerPrograms() const { return mLayerPrograms; }
@@ -80,6 +88,8 @@ public:
std::size_t TemporaryRenderTargetCount() const { return mRenderTargets.TemporaryTargetCount(); }
TemporalHistoryBuffers& TemporalHistory() { 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 SetOutputPackShaderProgram(GLuint program, GLuint vertexShader, GLuint fragmentShader);
bool InitializeResources(unsigned inputFrameWidth, unsigned inputFrameHeight, unsigned captureTextureWidth, unsigned outputFrameWidth, unsigned outputFrameHeight, unsigned outputPackTextureWidth, std::string& error);
@@ -101,13 +111,21 @@ private:
GLuint mDecodeProgram = 0;
GLuint mDecodeVertexShader = 0;
GLuint mDecodeFragmentShader = 0;
GLint mDecodePackedResolutionLocation = -1;
GLint mDecodeDecodedResolutionLocation = -1;
GLint mDecodeInputPixelFormatLocation = -1;
GLuint mOutputPackProgram = 0;
GLuint mOutputPackVertexShader = 0;
GLuint mOutputPackFragmentShader = 0;
GLint mOutputPackResolutionLocation = -1;
GLint mOutputPackActiveWordsLocation = -1;
GLint mOutputPackFormatLocation = -1;
GLsizeiptr mGlobalParamsUBOSize = 0;
bool mResourcesInitialized = false;
int mViewWidth = 0;
int mViewHeight = 0;
std::vector<LayerProgram> mLayerPrograms;
RenderTargetPool mRenderTargets;
TemporalHistoryBuffers mTemporalHistory;
ShaderFeedbackBuffers mFeedbackBuffers;
};

View File

@@ -10,9 +10,10 @@ 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;
std::vector<unsigned char>& buffer = mScratchBuffer;
buffer.clear();
buffer.reserve(512);
AppendStd140Float(buffer, static_cast<float>(state.timeSeconds));
@@ -32,6 +33,7 @@ bool GlobalParamsBuffer::Update(const RuntimeRenderState& state, unsigned availa
: 0u;
AppendStd140Int(buffer, static_cast<int>(effectiveSourceHistoryLength));
AppendStd140Int(buffer, static_cast<int>(effectiveTemporalHistoryLength));
AppendStd140Int(buffer, feedbackAvailable ? 1 : 0);
for (const ShaderParameterDefinition& definition : state.parameterDefinitions)
{

View File

@@ -3,13 +3,16 @@
#include "OpenGLRenderer.h"
#include "ShaderTypes.h"
#include <vector>
class GlobalParamsBuffer
{
public:
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:
OpenGLRenderer& mRenderer;
std::vector<unsigned char> mScratchBuffer;
};

View File

@@ -29,19 +29,21 @@ std::size_t RequiredTemporaryRenderTargets(const std::vector<OpenGLRenderer::Lay
}
}
OpenGLShaderPrograms::OpenGLShaderPrograms(OpenGLRenderer& renderer, RuntimeHost& runtimeHost) :
OpenGLShaderPrograms::OpenGLShaderPrograms(OpenGLRenderer& renderer, RuntimeSnapshotProvider& runtimeSnapshotProvider) :
mRenderer(renderer),
mRuntimeHost(runtimeHost),
mRuntimeSnapshotProvider(runtimeSnapshotProvider),
mGlobalParamsBuffer(renderer),
mCompiler(renderer, runtimeHost, mTextureBindings)
mCompiler(renderer, runtimeSnapshotProvider, mTextureBindings)
{
}
bool OpenGLShaderPrograms::CompileLayerPrograms(unsigned inputFrameWidth, unsigned inputFrameHeight, int errorMessageSize, char* errorMessage)
{
const std::vector<RuntimeRenderState> layerStates = mRuntimeHost.GetLayerRenderStates(inputFrameWidth, inputFrameHeight);
const RuntimeRenderStateSnapshot renderSnapshot =
mRuntimeSnapshotProvider.GetRenderStateSnapshot(inputFrameWidth, inputFrameHeight);
const std::vector<RuntimeRenderState>& layerStates = renderSnapshot.states;
std::string temporalError;
const unsigned historyCap = mRuntimeHost.GetMaxTemporalHistoryFrames();
const unsigned historyCap = mRuntimeSnapshotProvider.GetMaxTemporalHistoryFrames();
if (!mRenderer.TemporalHistory().ValidateTextureUnitBudget(layerStates, historyCap, temporalError))
{
CopyErrorMessage(temporalError, errorMessageSize, errorMessage);
@@ -52,6 +54,12 @@ bool OpenGLShaderPrograms::CompileLayerPrograms(unsigned inputFrameWidth, unsign
CopyErrorMessage(temporalError, errorMessageSize, errorMessage);
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
// queue so Slang/file work stays off the playback path.
@@ -81,10 +89,7 @@ bool OpenGLShaderPrograms::CompileLayerPrograms(unsigned inputFrameWidth, unsign
DestroyLayerPrograms();
mRenderer.ReplaceLayerPrograms(newPrograms);
mCommittedLayerStates = layerStates;
mRuntimeHost.SetCompileStatus(true, "Shader layers compiled successfully.");
mRuntimeHost.ClearReloadRequest();
mCommittedLayerStates = renderSnapshot.states;
return true;
}
@@ -98,13 +103,19 @@ bool OpenGLShaderPrograms::CommitPreparedLayerPrograms(const PreparedShaderBuild
}
std::string temporalError;
const unsigned historyCap = mRuntimeHost.GetMaxTemporalHistoryFrames();
if (!mRenderer.TemporalHistory().ValidateTextureUnitBudget(preparedBuild.layerStates, historyCap, temporalError))
const unsigned historyCap = mRuntimeSnapshotProvider.GetMaxTemporalHistoryFrames();
if (!mRenderer.TemporalHistory().ValidateTextureUnitBudget(preparedBuild.renderSnapshot.states, historyCap, temporalError))
{
CopyErrorMessage(temporalError, errorMessageSize, errorMessage);
return false;
}
if (!mRenderer.TemporalHistory().EnsureResources(preparedBuild.layerStates, historyCap, inputFrameWidth, inputFrameHeight, temporalError))
if (!mRenderer.TemporalHistory().EnsureResources(preparedBuild.renderSnapshot.states, historyCap, inputFrameWidth, inputFrameHeight, temporalError))
{
CopyErrorMessage(temporalError, errorMessageSize, errorMessage);
return false;
}
if (mRenderer.ResourcesInitialized() &&
!mRenderer.FeedbackBuffers().EnsureResources(preparedBuild.renderSnapshot.states, inputFrameWidth, inputFrameHeight, temporalError))
{
CopyErrorMessage(temporalError, errorMessageSize, errorMessage);
return false;
@@ -138,10 +149,7 @@ bool OpenGLShaderPrograms::CommitPreparedLayerPrograms(const PreparedShaderBuild
DestroyLayerPrograms();
mRenderer.ReplaceLayerPrograms(newPrograms);
mCommittedLayerStates = preparedBuild.layerStates;
mRuntimeHost.SetCompileStatus(true, "Shader layers compiled successfully.");
mRuntimeHost.ClearReloadRequest();
mCommittedLayerStates = preparedBuild.renderSnapshot.states;
return true;
}
@@ -176,12 +184,17 @@ void OpenGLShaderPrograms::ResetTemporalHistoryState()
mRenderer.TemporalHistory().ResetState();
}
void OpenGLShaderPrograms::ResetShaderFeedbackState()
{
mRenderer.FeedbackBuffers().ResetState();
}
bool OpenGLShaderPrograms::UpdateTextBindingTexture(const RuntimeRenderState& state, LayerProgram::TextBinding& textBinding, std::string& error)
{
return mTextureBindings.UpdateTextBindingTexture(state, textBinding, error);
}
bool OpenGLShaderPrograms::UpdateGlobalParamsBuffer(const RuntimeRenderState& state, unsigned availableSourceHistoryLength, unsigned availableTemporalHistoryLength)
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);
}

View File

@@ -2,7 +2,7 @@
#include "GlobalParamsBuffer.h"
#include "OpenGLRenderer.h"
#include "RuntimeHost.h"
#include "RuntimeSnapshotProvider.h"
#include "ShaderBuildQueue.h"
#include "ShaderTypes.h"
#include "ShaderProgramCompiler.h"
@@ -15,7 +15,7 @@ class OpenGLShaderPrograms
public:
using LayerProgram = OpenGLRenderer::LayerProgram;
OpenGLShaderPrograms(OpenGLRenderer& renderer, RuntimeHost& runtimeHost);
OpenGLShaderPrograms(OpenGLRenderer& renderer, RuntimeSnapshotProvider& runtimeSnapshotProvider);
bool CompileLayerPrograms(unsigned inputFrameWidth, unsigned inputFrameHeight, int errorMessageSize, char* errorMessage);
bool CommitPreparedLayerPrograms(const PreparedShaderBuild& preparedBuild, unsigned inputFrameWidth, unsigned inputFrameHeight, int errorMessageSize, char* errorMessage);
@@ -25,13 +25,14 @@ public:
void DestroySingleLayerProgram(LayerProgram& layerProgram);
void DestroyDecodeShaderProgram();
void ResetTemporalHistoryState();
void ResetShaderFeedbackState();
const std::vector<RuntimeRenderState>& CommittedLayerStates() const { return mCommittedLayerStates; }
bool UpdateTextBindingTexture(const RuntimeRenderState& state, LayerProgram::TextBinding& textBinding, std::string& error);
bool UpdateGlobalParamsBuffer(const RuntimeRenderState& state, unsigned availableSourceHistoryLength, unsigned availableTemporalHistoryLength);
bool UpdateGlobalParamsBuffer(const RuntimeRenderState& state, unsigned availableSourceHistoryLength, unsigned availableTemporalHistoryLength, bool feedbackAvailable);
private:
OpenGLRenderer& mRenderer;
RuntimeHost& mRuntimeHost;
RuntimeSnapshotProvider& mRuntimeSnapshotProvider;
ShaderTextureBindings mTextureBindings;
GlobalParamsBuffer mGlobalParamsBuffer;
ShaderProgramCompiler mCompiler;

View File

@@ -1,7 +1,5 @@
#include "ShaderBuildQueue.h"
#include "RuntimeHost.h"
#include <chrono>
#include <utility>
@@ -10,8 +8,8 @@ namespace
constexpr auto kShaderBuildDebounce = std::chrono::milliseconds(400);
}
ShaderBuildQueue::ShaderBuildQueue(RuntimeHost& runtimeHost) :
mRuntimeHost(runtimeHost),
ShaderBuildQueue::ShaderBuildQueue(RuntimeSnapshotProvider& runtimeSnapshotProvider) :
mRuntimeSnapshotProvider(runtimeSnapshotProvider),
mWorkerThread([this]() { WorkerLoop(); })
{
}
@@ -113,14 +111,14 @@ PreparedShaderBuild ShaderBuildQueue::Build(uint64_t generation, unsigned output
{
PreparedShaderBuild build;
build.generation = generation;
build.layerStates = mRuntimeHost.GetLayerRenderStates(outputWidth, outputHeight);
build.layers.reserve(build.layerStates.size());
build.renderSnapshot = mRuntimeSnapshotProvider.GetRenderStateSnapshot(outputWidth, outputHeight);
build.layers.reserve(build.renderSnapshot.states.size());
for (const RuntimeRenderState& state : build.layerStates)
for (const RuntimeRenderState& state : build.renderSnapshot.states)
{
PreparedLayerShader layer;
layer.state = state;
if (!mRuntimeHost.BuildLayerPassFragmentShaderSources(state.layerId, layer.passes, build.message))
if (!mRuntimeSnapshotProvider.BuildLayerPassFragmentShaderSources(state.layerId, layer.passes, build.message))
{
build.succeeded = false;
return build;

View File

@@ -1,5 +1,6 @@
#pragma once
#include "RuntimeSnapshotProvider.h"
#include "ShaderTypes.h"
#include <condition_variable>
@@ -9,8 +10,6 @@
#include <thread>
#include <vector>
class RuntimeHost;
struct PreparedLayerShader
{
RuntimeRenderState state;
@@ -22,14 +21,14 @@ struct PreparedShaderBuild
uint64_t generation = 0;
bool succeeded = false;
std::string message;
std::vector<RuntimeRenderState> layerStates;
RuntimeRenderStateSnapshot renderSnapshot;
std::vector<PreparedLayerShader> layers;
};
class ShaderBuildQueue
{
public:
explicit ShaderBuildQueue(RuntimeHost& runtimeHost);
explicit ShaderBuildQueue(RuntimeSnapshotProvider& runtimeSnapshotProvider);
~ShaderBuildQueue();
ShaderBuildQueue(const ShaderBuildQueue&) = delete;
@@ -43,7 +42,7 @@ private:
void WorkerLoop();
PreparedShaderBuild Build(uint64_t generation, unsigned outputWidth, unsigned outputHeight);
RuntimeHost& mRuntimeHost;
RuntimeSnapshotProvider& mRuntimeSnapshotProvider;
std::thread mWorkerThread;
std::mutex mMutex;
std::condition_variable mCondition;

View File

@@ -19,9 +19,9 @@ void CopyErrorMessage(const std::string& message, int errorMessageSize, char* er
}
}
ShaderProgramCompiler::ShaderProgramCompiler(OpenGLRenderer& renderer, RuntimeHost& runtimeHost, ShaderTextureBindings& textureBindings) :
ShaderProgramCompiler::ShaderProgramCompiler(OpenGLRenderer& renderer, RuntimeSnapshotProvider& runtimeSnapshotProvider, ShaderTextureBindings& textureBindings) :
mRenderer(renderer),
mRuntimeHost(runtimeHost),
mRuntimeSnapshotProvider(runtimeSnapshotProvider),
mTextureBindings(textureBindings)
{
}
@@ -31,7 +31,7 @@ bool ShaderProgramCompiler::CompileLayerProgram(const RuntimeRenderState& state,
std::vector<ShaderPassBuildSource> passSources;
std::string loadError;
if (!mRuntimeHost.BuildLayerPassFragmentShaderSources(state.layerId, passSources, loadError))
if (!mRuntimeSnapshotProvider.BuildLayerPassFragmentShaderSources(state.layerId, passSources, loadError))
{
CopyErrorMessage(loadError, errorMessageSize, errorMessage);
return false;
@@ -117,7 +117,7 @@ bool ShaderProgramCompiler::CompilePreparedLayerProgram(const RuntimeRenderState
passProgram.passId = passSource.passId;
passProgram.inputNames = passSource.inputNames;
passProgram.outputName = passSource.outputName;
passProgram.shaderTextureBase = mTextureBindings.ResolveShaderTextureBase(state, mRuntimeHost.GetMaxTemporalHistoryFrames());
passProgram.shaderTextureBase = mTextureBindings.ResolveShaderTextureBase(state, mRuntimeSnapshotProvider.GetMaxTemporalHistoryFrames());
passProgram.textureBindings.swap(textureBindings);
passProgram.textBindings.swap(textBindings);
@@ -125,7 +125,7 @@ bool ShaderProgramCompiler::CompilePreparedLayerProgram(const RuntimeRenderState
if (globalParamsIndex != GL_INVALID_INDEX)
glUniformBlockBinding(newProgram.get(), globalParamsIndex, kGlobalParamsBindingPoint);
const unsigned historyCap = mRuntimeHost.GetMaxTemporalHistoryFrames();
const unsigned historyCap = mRuntimeSnapshotProvider.GetMaxTemporalHistoryFrames();
glUseProgram(newProgram.get());
mTextureBindings.AssignLayerSamplerUniforms(newProgram.get(), state, passProgram, historyCap);
glUseProgram(0);

View File

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

View File

@@ -103,16 +103,25 @@ GLint ShaderTextureBindings::FindSamplerUniformLocation(GLuint program, const st
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;
}
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
{
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)
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));
}
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)
{
const GLint textureSamplerLocation = FindSamplerUniformLocation(program, passProgram.textureBindings[index].samplerName);
@@ -148,10 +164,14 @@ void ShaderTextureBindings::AssignLayerSamplerUniforms(GLuint program, const Run
ShaderTextureBindings::RuntimeTextureBindingPlan ShaderTextureBindings::BuildLayerRuntimeBindingPlan(
const PassProgram& passProgram,
GLuint layerInputTexture,
GLuint originalLayerInputTexture,
const RuntimeRenderState& state,
GLuint feedbackTexture,
const std::vector<GLuint>& sourceHistoryTextures,
const std::vector<GLuint>& temporalHistoryTextures) const
{
RuntimeTextureBindingPlan plan;
plan.bindings.push_back({ "originalLayerInput", "gLayerInput", originalLayerInputTexture, kLayerInputTextureUnit });
plan.bindings.push_back({ "layerInput", "gVideoInput", layerInputTexture, kDecodedVideoTextureUnit });
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)
{
const LayerProgram::TextureBinding& textureBinding = passProgram.textureBindings[index];

View File

@@ -29,11 +29,15 @@ public:
void CreateTextBindings(const RuntimeRenderState& state, std::vector<LayerProgram::TextBinding>& textBindings);
bool UpdateTextBindingTexture(const RuntimeRenderState& state, LayerProgram::TextBinding& textBinding, std::string& error);
GLint FindSamplerUniformLocation(GLuint program, const std::string& samplerName) const;
GLuint ResolveFeedbackTextureUnit(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;
RuntimeTextureBindingPlan BuildLayerRuntimeBindingPlan(
const PassProgram& passProgram,
GLuint layerInputTexture,
GLuint originalLayerInputTexture,
const RuntimeRenderState& state,
GLuint feedbackTexture,
const std::vector<GLuint>& sourceHistoryTextures,
const std::vector<GLuint>& temporalHistoryTextures) const;
void BindRuntimeTexturePlan(const RuntimeTextureBindingPlan& plan) const;

View File

@@ -0,0 +1,43 @@
#include "stdafx.h"
#include "HealthTelemetry.h"
#include "RuntimeHost.h"
HealthTelemetry::HealthTelemetry(RuntimeHost& runtimeHost) :
mRuntimeHost(runtimeHost)
{
}
void HealthTelemetry::ReportSignalStatus(bool hasSignal, unsigned width, unsigned height, const std::string& modeName)
{
mRuntimeHost.WriteSignalStatus(hasSignal, width, height, modeName);
}
bool HealthTelemetry::TryReportSignalStatus(bool hasSignal, unsigned width, unsigned height, const std::string& modeName)
{
return mRuntimeHost.TryWriteSignalStatus(hasSignal, width, height, modeName);
}
void HealthTelemetry::RecordPerformanceStats(double frameBudgetMilliseconds, double renderMilliseconds)
{
mRuntimeHost.WritePerformanceStats(frameBudgetMilliseconds, renderMilliseconds);
}
bool HealthTelemetry::TryRecordPerformanceStats(double frameBudgetMilliseconds, double renderMilliseconds)
{
return mRuntimeHost.TryWritePerformanceStats(frameBudgetMilliseconds, renderMilliseconds);
}
void HealthTelemetry::RecordFramePacingStats(double completionIntervalMilliseconds, double smoothedCompletionIntervalMilliseconds,
double maxCompletionIntervalMilliseconds, uint64_t lateFrameCount, uint64_t droppedFrameCount, uint64_t flushedFrameCount)
{
mRuntimeHost.WriteFramePacingStats(completionIntervalMilliseconds, smoothedCompletionIntervalMilliseconds,
maxCompletionIntervalMilliseconds, lateFrameCount, droppedFrameCount, flushedFrameCount);
}
bool HealthTelemetry::TryRecordFramePacingStats(double completionIntervalMilliseconds, double smoothedCompletionIntervalMilliseconds,
double maxCompletionIntervalMilliseconds, uint64_t lateFrameCount, uint64_t droppedFrameCount, uint64_t flushedFrameCount)
{
return mRuntimeHost.TryWriteFramePacingStats(completionIntervalMilliseconds, smoothedCompletionIntervalMilliseconds,
maxCompletionIntervalMilliseconds, lateFrameCount, droppedFrameCount, flushedFrameCount);
}

View File

@@ -0,0 +1,29 @@
#pragma once
#include <cstdint>
#include <string>
class RuntimeHost;
// Phase 1 compatibility seam for status and timing reporting. The current
// implementation still writes through RuntimeHost, but callers can now target
// HealthTelemetry as the home for operational visibility work.
class HealthTelemetry
{
public:
explicit HealthTelemetry(RuntimeHost& runtimeHost);
void ReportSignalStatus(bool hasSignal, unsigned width, unsigned height, const std::string& modeName);
bool TryReportSignalStatus(bool hasSignal, unsigned width, unsigned height, const std::string& modeName);
void RecordPerformanceStats(double frameBudgetMilliseconds, double renderMilliseconds);
bool TryRecordPerformanceStats(double frameBudgetMilliseconds, double renderMilliseconds);
void RecordFramePacingStats(double completionIntervalMilliseconds, double smoothedCompletionIntervalMilliseconds,
double maxCompletionIntervalMilliseconds, uint64_t lateFrameCount, uint64_t droppedFrameCount, uint64_t flushedFrameCount);
bool TryRecordFramePacingStats(double completionIntervalMilliseconds, double smoothedCompletionIntervalMilliseconds,
double maxCompletionIntervalMilliseconds, uint64_t lateFrameCount, uint64_t droppedFrameCount, uint64_t flushedFrameCount);
private:
RuntimeHost& mRuntimeHost;
};

View File

@@ -0,0 +1,173 @@
#include "RuntimeCoordinator.h"
#include "RuntimeStore.h"
RuntimeCoordinator::RuntimeCoordinator(RuntimeStore& runtimeStore) :
mRuntimeStore(runtimeStore)
{
}
RuntimeCoordinatorResult RuntimeCoordinator::AddLayer(const std::string& shaderId)
{
std::string error;
return ApplyStoreMutation(mRuntimeStore.CreateStoredLayer(shaderId, error), error, true, true);
}
RuntimeCoordinatorResult RuntimeCoordinator::RemoveLayer(const std::string& layerId)
{
std::string error;
return ApplyStoreMutation(mRuntimeStore.DeleteStoredLayer(layerId, error), error, true, true);
}
RuntimeCoordinatorResult RuntimeCoordinator::MoveLayer(const std::string& layerId, int direction)
{
std::string error;
return ApplyStoreMutation(mRuntimeStore.MoveStoredLayer(layerId, direction, error), error, true, true);
}
RuntimeCoordinatorResult RuntimeCoordinator::MoveLayerToIndex(const std::string& layerId, std::size_t targetIndex)
{
std::string error;
return ApplyStoreMutation(mRuntimeStore.MoveStoredLayerToIndex(layerId, targetIndex, error), error, true, true);
}
RuntimeCoordinatorResult RuntimeCoordinator::SetLayerBypass(const std::string& layerId, bool bypassed)
{
std::string error;
return ApplyStoreMutation(mRuntimeStore.SetStoredLayerBypassState(layerId, bypassed, error), error, true, false);
}
RuntimeCoordinatorResult RuntimeCoordinator::SetLayerShader(const std::string& layerId, const std::string& shaderId)
{
std::string error;
return ApplyStoreMutation(mRuntimeStore.SetStoredLayerShaderSelection(layerId, shaderId, error), error, true, false);
}
RuntimeCoordinatorResult RuntimeCoordinator::UpdateLayerParameter(const std::string& layerId, const std::string& parameterId, const JsonValue& newValue)
{
std::string error;
return ApplyStoreMutation(mRuntimeStore.SetStoredParameterValue(layerId, parameterId, newValue, error), error, false, false);
}
RuntimeCoordinatorResult RuntimeCoordinator::UpdateLayerParameterByControlKey(const std::string& layerKey, const std::string& parameterKey, const JsonValue& newValue)
{
std::string error;
return ApplyStoreMutation(mRuntimeStore.SetStoredParameterValueByControlKey(layerKey, parameterKey, newValue, error), error, false, false);
}
RuntimeCoordinatorResult RuntimeCoordinator::ResetLayerParameters(const std::string& layerId)
{
std::string error;
RuntimeCoordinatorResult result = ApplyStoreMutation(mRuntimeStore.ResetStoredLayerParameterValues(layerId, error), error, false, false);
if (!result.accepted)
return result;
result.clearTransientOscState = true;
result.renderResetScope = RuntimeCoordinatorRenderResetScope::TemporalHistoryAndFeedback;
return result;
}
RuntimeCoordinatorResult RuntimeCoordinator::SaveStackPreset(const std::string& presetName)
{
std::string error;
return ApplyStoreMutation(mRuntimeStore.SaveStackPresetSnapshot(presetName, error), error, false, false);
}
RuntimeCoordinatorResult RuntimeCoordinator::LoadStackPreset(const std::string& presetName)
{
std::string error;
return ApplyStoreMutation(mRuntimeStore.LoadStackPresetSnapshot(presetName, error), error, true, false);
}
RuntimeCoordinatorResult RuntimeCoordinator::RequestShaderReload(bool preserveFeedbackState)
{
return BuildQueuedReloadResult(preserveFeedbackState);
}
RuntimeCoordinatorResult RuntimeCoordinator::HandleRuntimePollFailure(const std::string& error)
{
RuntimeCoordinatorResult result;
result.accepted = true;
result.runtimeStateBroadcastRequired = true;
result.compileStatusChanged = true;
result.compileStatusSucceeded = false;
result.compileStatusMessage = error;
return result;
}
RuntimeCoordinatorResult RuntimeCoordinator::HandlePreparedShaderBuildFailure(const std::string& error)
{
mPreserveFeedbackOnNextShaderBuild = false;
RuntimeCoordinatorResult result;
result.accepted = true;
result.runtimeStateBroadcastRequired = true;
result.compileStatusChanged = true;
result.compileStatusSucceeded = false;
result.compileStatusMessage = error;
result.committedStateMode = RuntimeCoordinatorCommittedStateMode::UseCommittedStates;
return result;
}
RuntimeCoordinatorResult RuntimeCoordinator::HandlePreparedShaderBuildSuccess()
{
RuntimeCoordinatorResult result;
result.accepted = true;
result.runtimeStateBroadcastRequired = true;
result.compileStatusChanged = true;
result.compileStatusSucceeded = true;
result.compileStatusMessage = "Shader layers compiled successfully.";
result.committedStateMode = RuntimeCoordinatorCommittedStateMode::UseLiveSnapshots;
mPreserveFeedbackOnNextShaderBuild = false;
return result;
}
RuntimeCoordinatorResult RuntimeCoordinator::HandleRuntimeReloadRequest()
{
return BuildQueuedReloadResult(false);
}
bool RuntimeCoordinator::PreserveFeedbackOnNextShaderBuild() const
{
return mPreserveFeedbackOnNextShaderBuild;
}
RuntimeCoordinatorResult RuntimeCoordinator::ApplyStoreMutation(bool succeeded, const std::string& errorMessage, bool reloadRequired, bool preserveFeedbackState)
{
if (!succeeded)
{
RuntimeCoordinatorResult result;
result.accepted = false;
result.errorMessage = errorMessage;
return result;
}
if (reloadRequired)
return BuildQueuedReloadResult(preserveFeedbackState);
return BuildAcceptedNoReloadResult();
}
RuntimeCoordinatorResult RuntimeCoordinator::BuildQueuedReloadResult(bool preserveFeedbackState)
{
mPreserveFeedbackOnNextShaderBuild = preserveFeedbackState;
RuntimeCoordinatorResult result;
result.accepted = true;
result.runtimeStateBroadcastRequired = true;
result.shaderBuildRequested = true;
result.compileStatusChanged = true;
result.compileStatusSucceeded = true;
result.compileStatusMessage = "Shader rebuild queued.";
result.clearReloadRequest = true;
result.committedStateMode = RuntimeCoordinatorCommittedStateMode::UseCommittedStates;
return result;
}
RuntimeCoordinatorResult RuntimeCoordinator::BuildAcceptedNoReloadResult() const
{
RuntimeCoordinatorResult result;
result.accepted = true;
result.runtimeStateBroadcastRequired = true;
return result;
}

View File

@@ -0,0 +1,70 @@
#pragma once
#include "RuntimeJson.h"
#include <cstddef>
#include <string>
class RuntimeStore;
enum class RuntimeCoordinatorCommittedStateMode
{
Unchanged,
UseCommittedStates,
UseLiveSnapshots
};
enum class RuntimeCoordinatorRenderResetScope
{
None,
TemporalHistoryOnly,
TemporalHistoryAndFeedback
};
struct RuntimeCoordinatorResult
{
bool accepted = false;
bool runtimeStateBroadcastRequired = false;
bool shaderBuildRequested = false;
bool clearTransientOscState = false;
bool compileStatusChanged = false;
bool compileStatusSucceeded = false;
bool clearReloadRequest = false;
RuntimeCoordinatorCommittedStateMode committedStateMode = RuntimeCoordinatorCommittedStateMode::Unchanged;
RuntimeCoordinatorRenderResetScope renderResetScope = RuntimeCoordinatorRenderResetScope::None;
std::string compileStatusMessage;
std::string errorMessage;
};
class RuntimeCoordinator
{
public:
explicit RuntimeCoordinator(RuntimeStore& runtimeStore);
RuntimeCoordinatorResult AddLayer(const std::string& shaderId);
RuntimeCoordinatorResult RemoveLayer(const std::string& layerId);
RuntimeCoordinatorResult MoveLayer(const std::string& layerId, int direction);
RuntimeCoordinatorResult MoveLayerToIndex(const std::string& layerId, std::size_t targetIndex);
RuntimeCoordinatorResult SetLayerBypass(const std::string& layerId, bool bypassed);
RuntimeCoordinatorResult SetLayerShader(const std::string& layerId, const std::string& shaderId);
RuntimeCoordinatorResult UpdateLayerParameter(const std::string& layerId, const std::string& parameterId, const JsonValue& newValue);
RuntimeCoordinatorResult UpdateLayerParameterByControlKey(const std::string& layerKey, const std::string& parameterKey, const JsonValue& newValue);
RuntimeCoordinatorResult ResetLayerParameters(const std::string& layerId);
RuntimeCoordinatorResult SaveStackPreset(const std::string& presetName);
RuntimeCoordinatorResult LoadStackPreset(const std::string& presetName);
RuntimeCoordinatorResult RequestShaderReload(bool preserveFeedbackState = false);
RuntimeCoordinatorResult HandleRuntimePollFailure(const std::string& error);
RuntimeCoordinatorResult HandlePreparedShaderBuildFailure(const std::string& error);
RuntimeCoordinatorResult HandlePreparedShaderBuildSuccess();
RuntimeCoordinatorResult HandleRuntimeReloadRequest();
bool PreserveFeedbackOnNextShaderBuild() const;
private:
RuntimeCoordinatorResult ApplyStoreMutation(bool succeeded, const std::string& errorMessage, bool reloadRequired, bool preserveFeedbackState);
RuntimeCoordinatorResult BuildQueuedReloadResult(bool preserveFeedbackState);
RuntimeCoordinatorResult BuildAcceptedNoReloadResult() const;
RuntimeStore& mRuntimeStore;
bool mPreserveFeedbackOnNextShaderBuild = false;
};

View File

@@ -33,6 +33,11 @@ bool IsFiniteNumber(double value)
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::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);
}
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()
{
std::random_device randomDevice;
@@ -680,7 +699,8 @@ bool ParseParameterDefinitions(const JsonValue& manifestJson, ShaderPackage& sha
}
RuntimeHost::RuntimeHost()
: mReloadRequested(false),
: mHealthTelemetry(*this),
mReloadRequested(false),
mCompileSucceeded(false),
mHasSignal(false),
mSignalWidth(0),
@@ -841,6 +861,8 @@ bool RuntimeHost::PollFileChanges(bool& registryChanged, bool& reloadRequested,
}
reloadRequested = mReloadRequested;
if (registryChanged || reloadRequested)
MarkRenderStateDirtyLocked();
return true;
}
catch (const std::exception& exception)
@@ -884,6 +906,7 @@ bool RuntimeHost::AddLayer(const std::string& shaderId, std::string& error)
EnsureLayerDefaultsLocked(layer, shaderIt->second);
mPersistentState.layers.push_back(layer);
mReloadRequested = true;
MarkRenderStateDirtyLocked();
return SavePersistentState(error);
}
@@ -900,6 +923,7 @@ bool RuntimeHost::RemoveLayer(const std::string& layerId, std::string& error)
mPersistentState.layers.erase(it);
mReloadRequested = true;
MarkRenderStateDirtyLocked();
return SavePersistentState(error);
}
@@ -921,6 +945,7 @@ bool RuntimeHost::MoveLayer(const std::string& layerId, int direction, std::stri
std::swap(mPersistentState.layers[index], mPersistentState.layers[newIndex]);
mReloadRequested = true;
MarkRenderStateDirtyLocked();
return SavePersistentState(error);
}
@@ -949,6 +974,7 @@ bool RuntimeHost::MoveLayerToIndex(const std::string& layerId, std::size_t targe
mPersistentState.layers.erase(mPersistentState.layers.begin() + static_cast<std::ptrdiff_t>(sourceIndex));
mPersistentState.layers.insert(mPersistentState.layers.begin() + static_cast<std::ptrdiff_t>(targetIndex), movedLayer);
mReloadRequested = true;
MarkRenderStateDirtyLocked();
return SavePersistentState(error);
}
@@ -964,6 +990,7 @@ bool RuntimeHost::SetLayerBypass(const std::string& layerId, bool bypassed, std:
layer->bypass = bypassed;
mReloadRequested = true;
MarkParameterStateDirtyLocked();
return SavePersistentState(error);
}
@@ -988,6 +1015,7 @@ bool RuntimeHost::SetLayerShader(const std::string& layerId, const std::string&
layer->parameterValues.clear();
EnsureLayerDefaultsLocked(*layer, shaderIt->second);
mReloadRequested = true;
MarkRenderStateDirtyLocked();
return SavePersistentState(error);
}
@@ -1024,6 +1052,7 @@ bool RuntimeHost::UpdateLayerParameter(const std::string& layerId, const std::st
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();
return true;
}
@@ -1032,10 +1061,16 @@ bool RuntimeHost::UpdateLayerParameter(const std::string& layerId, const std::st
return false;
layer->parameterValues[parameterId] = normalized;
MarkParameterStateDirtyLocked();
return SavePersistentState(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);
@@ -1079,6 +1114,7 @@ bool RuntimeHost::UpdateLayerParameterByControlKey(const std::string& layerKey,
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();
return true;
}
@@ -1087,7 +1123,141 @@ bool RuntimeHost::UpdateLayerParameterByControlKey(const std::string& layerKey,
return false;
matchedLayer->parameterValues[parameterIt->id] = normalized;
return SavePersistentState(error);
MarkParameterStateDirtyLocked();
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(),
[&parameterKey](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)
@@ -1110,6 +1280,7 @@ bool RuntimeHost::ResetLayerParameters(const std::string& layerId, std::string&
layer->parameterValues.clear();
EnsureLayerDefaultsLocked(*layer, shaderIt->second);
MarkParameterStateDirtyLocked();
return SavePersistentState(error);
}
@@ -1169,6 +1340,7 @@ bool RuntimeHost::LoadStackPreset(const std::string& presetName, std::string& er
mPersistentState.layers = nextLayers;
mReloadRequested = true;
MarkRenderStateDirtyLocked();
return SavePersistentState(error);
}
@@ -1180,12 +1352,22 @@ void RuntimeHost::SetCompileStatus(bool succeeded, const std::string& message)
}
void RuntimeHost::SetSignalStatus(bool hasSignal, unsigned width, unsigned height, const std::string& modeName)
{
mHealthTelemetry.ReportSignalStatus(hasSignal, width, height, modeName);
}
bool RuntimeHost::TrySetSignalStatus(bool hasSignal, unsigned width, unsigned height, const std::string& modeName)
{
return mHealthTelemetry.TryReportSignalStatus(hasSignal, width, height, modeName);
}
void RuntimeHost::WriteSignalStatus(bool hasSignal, unsigned width, unsigned height, const std::string& modeName)
{
std::lock_guard<std::mutex> lock(mMutex);
SetSignalStatusLocked(hasSignal, width, height, modeName);
}
bool RuntimeHost::TrySetSignalStatus(bool hasSignal, unsigned width, unsigned height, const std::string& modeName)
bool RuntimeHost::TryWriteSignalStatus(bool hasSignal, unsigned width, unsigned height, const std::string& modeName)
{
std::unique_lock<std::mutex> lock(mMutex, std::try_to_lock);
if (!lock.owns_lock())
@@ -1197,10 +1379,27 @@ bool RuntimeHost::TrySetSignalStatus(bool hasSignal, unsigned width, unsigned he
void RuntimeHost::SetSignalStatusLocked(bool hasSignal, unsigned width, unsigned height, const std::string& modeName)
{
const bool changed = mHasSignal != hasSignal ||
mSignalWidth != width ||
mSignalHeight != height ||
mSignalModeName != modeName;
mHasSignal = hasSignal;
mSignalWidth = width;
mSignalHeight = height;
mSignalModeName = modeName;
if (changed)
MarkRenderStateDirtyLocked();
}
void RuntimeHost::MarkRenderStateDirtyLocked()
{
mRenderStateVersion.fetch_add(1, std::memory_order_relaxed);
mParameterStateVersion.fetch_add(1, std::memory_order_relaxed);
}
void RuntimeHost::MarkParameterStateDirtyLocked()
{
mParameterStateVersion.fetch_add(1, std::memory_order_relaxed);
}
void RuntimeHost::SetDeckLinkOutputStatus(const std::string& modelName, bool supportsInternalKeying, bool supportsExternalKeying,
@@ -1225,12 +1424,22 @@ void RuntimeHost::SetVideoIOStatus(const std::string& backendName, const std::st
}
void RuntimeHost::SetPerformanceStats(double frameBudgetMilliseconds, double renderMilliseconds)
{
mHealthTelemetry.RecordPerformanceStats(frameBudgetMilliseconds, renderMilliseconds);
}
bool RuntimeHost::TrySetPerformanceStats(double frameBudgetMilliseconds, double renderMilliseconds)
{
return mHealthTelemetry.TryRecordPerformanceStats(frameBudgetMilliseconds, renderMilliseconds);
}
void RuntimeHost::WritePerformanceStats(double frameBudgetMilliseconds, double renderMilliseconds)
{
std::lock_guard<std::mutex> lock(mMutex);
SetPerformanceStatsLocked(frameBudgetMilliseconds, renderMilliseconds);
}
bool RuntimeHost::TrySetPerformanceStats(double frameBudgetMilliseconds, double renderMilliseconds)
bool RuntimeHost::TryWritePerformanceStats(double frameBudgetMilliseconds, double renderMilliseconds)
{
std::unique_lock<std::mutex> lock(mMutex, std::try_to_lock);
if (!lock.owns_lock())
@@ -1252,13 +1461,27 @@ void RuntimeHost::SetPerformanceStatsLocked(double frameBudgetMilliseconds, doub
void RuntimeHost::SetFramePacingStats(double completionIntervalMilliseconds, double smoothedCompletionIntervalMilliseconds,
double maxCompletionIntervalMilliseconds, uint64_t lateFrameCount, uint64_t droppedFrameCount, uint64_t flushedFrameCount)
{
mHealthTelemetry.RecordFramePacingStats(completionIntervalMilliseconds, smoothedCompletionIntervalMilliseconds,
maxCompletionIntervalMilliseconds, lateFrameCount, droppedFrameCount, flushedFrameCount);
}
bool RuntimeHost::TrySetFramePacingStats(double completionIntervalMilliseconds, double smoothedCompletionIntervalMilliseconds,
double maxCompletionIntervalMilliseconds, uint64_t lateFrameCount, uint64_t droppedFrameCount, uint64_t flushedFrameCount)
{
return mHealthTelemetry.TryRecordFramePacingStats(completionIntervalMilliseconds, smoothedCompletionIntervalMilliseconds,
maxCompletionIntervalMilliseconds, lateFrameCount, droppedFrameCount, flushedFrameCount);
}
void RuntimeHost::WriteFramePacingStats(double completionIntervalMilliseconds, double smoothedCompletionIntervalMilliseconds,
double maxCompletionIntervalMilliseconds, uint64_t lateFrameCount, uint64_t droppedFrameCount, uint64_t flushedFrameCount)
{
std::lock_guard<std::mutex> lock(mMutex);
SetFramePacingStatsLocked(completionIntervalMilliseconds, smoothedCompletionIntervalMilliseconds,
maxCompletionIntervalMilliseconds, lateFrameCount, droppedFrameCount, flushedFrameCount);
}
bool RuntimeHost::TrySetFramePacingStats(double completionIntervalMilliseconds, double smoothedCompletionIntervalMilliseconds,
bool RuntimeHost::TryWriteFramePacingStats(double completionIntervalMilliseconds, double smoothedCompletionIntervalMilliseconds,
double maxCompletionIntervalMilliseconds, uint64_t lateFrameCount, uint64_t droppedFrameCount, uint64_t flushedFrameCount)
{
std::unique_lock<std::mutex> lock(mMutex, std::try_to_lock);
@@ -1363,6 +1586,34 @@ bool RuntimeHost::TryGetLayerRenderStates(unsigned outputWidth, unsigned outputH
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
{
const RuntimeClockSnapshot clock = GetRuntimeClockSnapshot();
@@ -1390,6 +1641,7 @@ void RuntimeHost::BuildLayerRenderStatesLocked(unsigned outputWidth, unsigned ou
RuntimeRenderState state;
state.layerId = layer.id;
state.shaderId = layer.shaderId;
state.shaderName = shaderIt->second.displayName;
state.mixAmount = 1.0;
state.bypass = layer.bypass ? 1.0 : 0.0;
state.inputWidth = mSignalWidth;
@@ -1403,6 +1655,7 @@ void RuntimeHost::BuildLayerRenderStatesLocked(unsigned outputWidth, unsigned ou
state.temporalHistorySource = shaderIt->second.temporal.historySource;
state.requestedTemporalHistoryLength = shaderIt->second.temporal.requestedHistoryLength;
state.effectiveTemporalHistoryLength = shaderIt->second.temporal.effectiveHistoryLength;
state.feedback = shaderIt->second.feedback;
for (const ShaderParameterDefinition& definition : shaderIt->second.parameters)
{
@@ -1449,6 +1702,10 @@ bool RuntimeHost::LoadConfig(std::string& error)
mConfig.serverPort = static_cast<unsigned short>(serverPortValue->asNumber(mConfig.serverPort));
if (const JsonValue* oscPortValue = configJson.find("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"))
mConfig.autoReload = autoReloadValue->asBoolean(mConfig.autoReload);
if (const JsonValue* maxTemporalHistoryFramesValue = configJson.find("maxTemporalHistoryFrames"))
@@ -1456,6 +1713,11 @@ bool RuntimeHost::LoadConfig(std::string& error)
const double configuredValue = maxTemporalHistoryFramesValue->asNumber(static_cast<double>(mConfig.maxTemporalHistoryFrames));
mConfig.maxTemporalHistoryFrames = configuredValue <= 0.0 ? 0u : static_cast<unsigned>(configuredValue);
}
if (const JsonValue* previewFpsValue = configJson.find("previewFps"))
{
const double configuredValue = previewFpsValue->asNumber(static_cast<double>(mConfig.previewFps));
mConfig.previewFps = configuredValue <= 0.0 ? 0u : static_cast<unsigned>(configuredValue);
}
if (const JsonValue* enableExternalKeyingValue = configJson.find("enableExternalKeying"))
mConfig.enableExternalKeying = enableExternalKeyingValue->asBoolean(mConfig.enableExternalKeying);
if (const JsonValue* videoFormatValue = configJson.find("videoFormat"))
@@ -1674,6 +1936,8 @@ bool RuntimeHost::ScanShaderPackages(std::string& error)
++it;
}
MarkRenderStateDirtyLocked();
return true;
}
@@ -1838,8 +2102,11 @@ JsonValue RuntimeHost::BuildStateValue() const
JsonValue app = JsonValue::MakeObject();
app.set("serverPort", JsonValue(static_cast<double>(mServerPort)));
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("maxTemporalHistoryFrames", JsonValue(static_cast<double>(mConfig.maxTemporalHistoryFrames)));
app.set("previewFps", JsonValue(static_cast<double>(mConfig.previewFps)));
app.set("enableExternalKeying", JsonValue(mConfig.enableExternalKeying));
app.set("inputVideoFormat", JsonValue(mConfig.inputVideoFormat));
app.set("inputFrameRate", JsonValue(mConfig.inputFrameRate));
@@ -1916,6 +2183,13 @@ JsonValue RuntimeHost::BuildStateValue() const
temporal.set("effectiveHistoryLength", JsonValue(static_cast<double>(shaderIt->second.temporal.effectiveHistoryLength)));
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);
}
root.set("shaders", shaderLibrary);
@@ -1953,6 +2227,13 @@ JsonValue RuntimeHost::SerializeLayerStackLocked() const
temporal.set("effectiveHistoryLength", JsonValue(static_cast<double>(shaderIt->second.temporal.effectiveHistoryLength)));
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();
for (const ShaderParameterDefinition& definition : shaderIt->second.parameters)

View File

@@ -1,5 +1,6 @@
#pragma once
#include "HealthTelemetry.h"
#include "RuntimeJson.h"
#include "ShaderTypes.h"
@@ -31,6 +32,8 @@ public:
bool SetLayerShader(const std::string& layerId, const std::string& shaderId, std::string& error);
bool UpdateLayerParameter(const std::string& layerId, const std::string& parameterId, const JsonValue& newValue, std::string& error);
bool UpdateLayerParameterByControlKey(const std::string& layerKey, const std::string& parameterKey, const JsonValue& newValue, std::string& error);
bool 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 SaveStackPreset(const std::string& presetName, std::string& error) const;
bool LoadStackPreset(const std::string& presetName, std::string& error);
@@ -50,12 +53,17 @@ public:
double maxCompletionIntervalMilliseconds, uint64_t lateFrameCount, uint64_t droppedFrameCount, uint64_t flushedFrameCount);
void AdvanceFrame();
bool TryAdvanceFrame();
HealthTelemetry& GetHealthTelemetry() { return mHealthTelemetry; }
const HealthTelemetry& GetHealthTelemetry() const { return mHealthTelemetry; }
bool BuildLayerPassFragmentShaderSources(const std::string& layerId, std::vector<ShaderPassBuildSource>& passSources, std::string& error);
std::vector<RuntimeRenderState> GetLayerRenderStates(unsigned outputWidth, unsigned outputHeight) const;
bool TryGetLayerRenderStates(unsigned outputWidth, unsigned outputHeight, std::vector<RuntimeRenderState>& states) const;
bool TryRefreshCachedLayerStates(std::vector<RuntimeRenderState>& states) const;
void RefreshDynamicRenderStateFields(std::vector<RuntimeRenderState>& states) const;
std::string BuildStateJson() const;
uint64_t GetRenderStateVersion() const { return mRenderStateVersion.load(std::memory_order_relaxed); }
uint64_t GetParameterStateVersion() const { return mParameterStateVersion.load(std::memory_order_relaxed); }
const std::filesystem::path& GetRepoRoot() const { return mRepoRoot; }
const std::filesystem::path& GetUiRoot() const { return mUiRoot; }
@@ -63,7 +71,10 @@ public:
const std::filesystem::path& GetRuntimeRoot() const { return mRuntimeRoot; }
unsigned short GetServerPort() const { return mServerPort; }
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 GetPreviewFps() const { return mConfig.previewFps; }
bool ExternalKeyingEnabled() const { return mConfig.enableExternalKeying; }
const std::string& GetInputVideoFormat() const { return mConfig.inputVideoFormat; }
const std::string& GetInputFrameRate() const { return mConfig.inputFrameRate; }
@@ -78,8 +89,11 @@ private:
std::string shaderLibrary = "shaders";
unsigned short serverPort = 8080;
unsigned short oscPort = 9000;
std::string oscBindAddress = "127.0.0.1";
double oscSmoothing = 0.18;
bool autoReload = true;
unsigned maxTemporalHistoryFrames = 4;
unsigned previewFps = 30;
bool enableExternalKeying = false;
std::string inputVideoFormat = "1080p";
std::string inputFrameRate = "59.94";
@@ -134,12 +148,24 @@ private:
LayerPersistentState* FindLayerById(const std::string& layerId);
const LayerPersistentState* FindLayerById(const std::string& layerId) const;
std::string GenerateLayerId();
void WriteSignalStatus(bool hasSignal, unsigned width, unsigned height, const std::string& modeName);
bool TryWriteSignalStatus(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 MarkParameterStateDirtyLocked();
void WritePerformanceStats(double frameBudgetMilliseconds, double renderMilliseconds);
bool TryWritePerformanceStats(double frameBudgetMilliseconds, double renderMilliseconds);
void SetPerformanceStatsLocked(double frameBudgetMilliseconds, double renderMilliseconds);
void WriteFramePacingStats(double completionIntervalMilliseconds, double smoothedCompletionIntervalMilliseconds,
double maxCompletionIntervalMilliseconds, uint64_t lateFrameCount, uint64_t droppedFrameCount, uint64_t flushedFrameCount);
bool TryWriteFramePacingStats(double completionIntervalMilliseconds, double smoothedCompletionIntervalMilliseconds,
double maxCompletionIntervalMilliseconds, uint64_t lateFrameCount, uint64_t droppedFrameCount, uint64_t flushedFrameCount);
void SetFramePacingStatsLocked(double completionIntervalMilliseconds, double smoothedCompletionIntervalMilliseconds,
double maxCompletionIntervalMilliseconds, uint64_t lateFrameCount, uint64_t droppedFrameCount, uint64_t flushedFrameCount);
private:
friend class HealthTelemetry;
HealthTelemetry mHealthTelemetry;
mutable std::mutex mMutex;
AppConfig mConfig;
PersistentState mPersistentState;
@@ -179,6 +205,8 @@ private:
bool mAutoReloadEnabled;
std::chrono::steady_clock::time_point mStartTime;
std::chrono::steady_clock::time_point mLastScanTime;
std::atomic<uint64_t> mFrameCounter;
std::atomic<uint64_t> mFrameCounter{ 0 };
std::atomic<uint64_t> mRenderStateVersion{ 0 };
std::atomic<uint64_t> mParameterStateVersion{ 0 };
uint64_t mNextLayerId;
};

View File

@@ -0,0 +1,168 @@
#include "RuntimeSnapshotProvider.h"
#include <utility>
RuntimeSnapshotProvider::RuntimeSnapshotProvider(RuntimeHost& runtimeHost) :
mRuntimeHost(runtimeHost)
{
}
bool RuntimeSnapshotProvider::BuildLayerPassFragmentShaderSources(const std::string& layerId, std::vector<ShaderPassBuildSource>& passSources, std::string& error) const
{
return mRuntimeHost.BuildLayerPassFragmentShaderSources(layerId, passSources, error);
}
unsigned RuntimeSnapshotProvider::GetMaxTemporalHistoryFrames() const
{
return mRuntimeHost.GetMaxTemporalHistoryFrames();
}
RuntimeSnapshotVersions RuntimeSnapshotProvider::GetVersions() const
{
RuntimeSnapshotVersions versions;
versions.renderStateVersion = mRuntimeHost.GetRenderStateVersion();
versions.parameterStateVersion = mRuntimeHost.GetParameterStateVersion();
return versions;
}
RuntimeRenderFrameContext RuntimeSnapshotProvider::GetFrameContext() const
{
std::vector<RuntimeRenderState> stateScratch(1);
mRuntimeHost.RefreshDynamicRenderStateFields(stateScratch);
RuntimeRenderFrameContext frameContext;
const RuntimeRenderState& state = stateScratch.front();
frameContext.timeSeconds = state.timeSeconds;
frameContext.utcTimeSeconds = state.utcTimeSeconds;
frameContext.utcOffsetSeconds = state.utcOffsetSeconds;
frameContext.startupRandom = state.startupRandom;
frameContext.frameCount = state.frameCount;
return frameContext;
}
void RuntimeSnapshotProvider::AdvanceFrame()
{
mRuntimeHost.AdvanceFrame();
}
bool RuntimeSnapshotProvider::TryAdvanceFrame()
{
return mRuntimeHost.TryAdvanceFrame();
}
RuntimeRenderStateSnapshot RuntimeSnapshotProvider::GetRenderStateSnapshot(unsigned outputWidth, unsigned outputHeight) const
{
for (;;)
{
const RuntimeSnapshotVersions versionsBefore = GetVersions();
RuntimeRenderStateSnapshot snapshot;
snapshot.outputWidth = outputWidth;
snapshot.outputHeight = outputHeight;
snapshot.states = mRuntimeHost.GetLayerRenderStates(outputWidth, outputHeight);
const RuntimeSnapshotVersions versionsAfter = GetVersions();
if (versionsBefore.renderStateVersion == versionsAfter.renderStateVersion &&
versionsBefore.parameterStateVersion == versionsAfter.parameterStateVersion)
{
snapshot.versions = versionsAfter;
return snapshot;
}
}
}
bool RuntimeSnapshotProvider::TryGetRenderStateSnapshot(unsigned outputWidth, unsigned outputHeight, RuntimeRenderStateSnapshot& snapshot) const
{
const RuntimeSnapshotVersions versionsBefore = GetVersions();
std::vector<RuntimeRenderState> states;
if (!mRuntimeHost.TryGetLayerRenderStates(outputWidth, outputHeight, states))
return false;
const RuntimeSnapshotVersions versionsAfter = GetVersions();
if (versionsBefore.renderStateVersion != versionsAfter.renderStateVersion ||
versionsBefore.parameterStateVersion != versionsAfter.parameterStateVersion)
{
return false;
}
snapshot.outputWidth = outputWidth;
snapshot.outputHeight = outputHeight;
snapshot.versions = versionsAfter;
snapshot.states = std::move(states);
return true;
}
bool RuntimeSnapshotProvider::TryRefreshSnapshotParameters(RuntimeRenderStateSnapshot& snapshot) const
{
const uint64_t expectedRenderStateVersion = snapshot.versions.renderStateVersion;
if (!mRuntimeHost.TryRefreshCachedLayerStates(snapshot.states))
return false;
const RuntimeSnapshotVersions versions = GetVersions();
if (versions.renderStateVersion != expectedRenderStateVersion)
return false;
snapshot.versions = versions;
return true;
}
void RuntimeSnapshotProvider::ApplyFrameContext(std::vector<RuntimeRenderState>& states, const RuntimeRenderFrameContext& frameContext) const
{
for (RuntimeRenderState& state : states)
{
state.timeSeconds = frameContext.timeSeconds;
state.utcTimeSeconds = frameContext.utcTimeSeconds;
state.utcOffsetSeconds = frameContext.utcOffsetSeconds;
state.startupRandom = frameContext.startupRandom;
state.frameCount = frameContext.frameCount;
}
}
void RuntimeSnapshotProvider::ApplyFrameContext(RuntimeRenderStateSnapshot& snapshot, const RuntimeRenderFrameContext& frameContext) const
{
ApplyFrameContext(snapshot.states, frameContext);
}
std::vector<RuntimeRenderState> RuntimeSnapshotProvider::GetLayerRenderStates(unsigned outputWidth, unsigned outputHeight) const
{
return GetRenderStateSnapshot(outputWidth, outputHeight).states;
}
bool RuntimeSnapshotProvider::TryGetLayerRenderStates(unsigned outputWidth, unsigned outputHeight, std::vector<RuntimeRenderState>& states) const
{
RuntimeRenderStateSnapshot snapshot;
if (!TryGetRenderStateSnapshot(outputWidth, outputHeight, snapshot))
return false;
states = std::move(snapshot.states);
return true;
}
bool RuntimeSnapshotProvider::TryRefreshCachedLayerStates(std::vector<RuntimeRenderState>& states) const
{
RuntimeRenderStateSnapshot snapshot;
snapshot.versions.renderStateVersion = mRuntimeHost.GetRenderStateVersion();
snapshot.versions.parameterStateVersion = mRuntimeHost.GetParameterStateVersion();
snapshot.states = states;
if (!TryRefreshSnapshotParameters(snapshot))
return false;
states = std::move(snapshot.states);
return true;
}
void RuntimeSnapshotProvider::RefreshDynamicRenderStateFields(std::vector<RuntimeRenderState>& states) const
{
ApplyFrameContext(states, GetFrameContext());
}
uint64_t RuntimeSnapshotProvider::GetRenderStateVersion() const
{
return GetVersions().renderStateVersion;
}
uint64_t RuntimeSnapshotProvider::GetParameterStateVersion() const
{
return GetVersions().parameterStateVersion;
}

View File

@@ -0,0 +1,58 @@
#pragma once
#include "RuntimeHost.h"
#include <cstdint>
#include <string>
#include <vector>
struct RuntimeSnapshotVersions
{
uint64_t renderStateVersion = 0;
uint64_t parameterStateVersion = 0;
};
struct RuntimeRenderFrameContext
{
double timeSeconds = 0.0;
double utcTimeSeconds = 0.0;
double utcOffsetSeconds = 0.0;
double startupRandom = 0.0;
double frameCount = 0.0;
};
struct RuntimeRenderStateSnapshot
{
RuntimeSnapshotVersions versions;
unsigned outputWidth = 0;
unsigned outputHeight = 0;
std::vector<RuntimeRenderState> states;
};
class RuntimeSnapshotProvider
{
public:
explicit RuntimeSnapshotProvider(RuntimeHost& runtimeHost);
bool BuildLayerPassFragmentShaderSources(const std::string& layerId, std::vector<ShaderPassBuildSource>& passSources, std::string& error) const;
unsigned GetMaxTemporalHistoryFrames() const;
RuntimeSnapshotVersions GetVersions() const;
RuntimeRenderFrameContext GetFrameContext() const;
void AdvanceFrame();
bool TryAdvanceFrame();
RuntimeRenderStateSnapshot GetRenderStateSnapshot(unsigned outputWidth, unsigned outputHeight) const;
bool TryGetRenderStateSnapshot(unsigned outputWidth, unsigned outputHeight, RuntimeRenderStateSnapshot& snapshot) const;
bool TryRefreshSnapshotParameters(RuntimeRenderStateSnapshot& snapshot) const;
void ApplyFrameContext(std::vector<RuntimeRenderState>& states, const RuntimeRenderFrameContext& frameContext) const;
void ApplyFrameContext(RuntimeRenderStateSnapshot& snapshot, const RuntimeRenderFrameContext& frameContext) const;
std::vector<RuntimeRenderState> GetLayerRenderStates(unsigned outputWidth, unsigned outputHeight) 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;
uint64_t GetRenderStateVersion() const;
uint64_t GetParameterStateVersion() const;
private:
RuntimeHost& mRuntimeHost;
};

View File

@@ -0,0 +1,161 @@
#include "RuntimeStore.h"
RuntimeStore::RuntimeStore(RuntimeHost& runtimeHost) :
mRuntimeHost(runtimeHost)
{
}
bool RuntimeStore::InitializeStore(std::string& error)
{
return mRuntimeHost.Initialize(error);
}
std::string RuntimeStore::BuildPersistentStateJson() const
{
return mRuntimeHost.BuildStateJson();
}
bool RuntimeStore::CreateStoredLayer(const std::string& shaderId, std::string& error)
{
return mRuntimeHost.AddLayer(shaderId, error);
}
bool RuntimeStore::DeleteStoredLayer(const std::string& layerId, std::string& error)
{
return mRuntimeHost.RemoveLayer(layerId, error);
}
bool RuntimeStore::MoveStoredLayer(const std::string& layerId, int direction, std::string& error)
{
return mRuntimeHost.MoveLayer(layerId, direction, error);
}
bool RuntimeStore::MoveStoredLayerToIndex(const std::string& layerId, std::size_t targetIndex, std::string& error)
{
return mRuntimeHost.MoveLayerToIndex(layerId, targetIndex, error);
}
bool RuntimeStore::SetStoredLayerBypassState(const std::string& layerId, bool bypassed, std::string& error)
{
return mRuntimeHost.SetLayerBypass(layerId, bypassed, error);
}
bool RuntimeStore::SetStoredLayerShaderSelection(const std::string& layerId, const std::string& shaderId, std::string& error)
{
return mRuntimeHost.SetLayerShader(layerId, shaderId, error);
}
bool RuntimeStore::SetStoredParameterValue(const std::string& layerId, const std::string& parameterId, const JsonValue& newValue, std::string& error)
{
return mRuntimeHost.UpdateLayerParameter(layerId, parameterId, newValue, error);
}
bool RuntimeStore::SetStoredParameterValueByControlKey(const std::string& layerKey, const std::string& parameterKey, const JsonValue& newValue, std::string& error)
{
return mRuntimeHost.UpdateLayerParameterByControlKey(layerKey, parameterKey, newValue, error);
}
bool RuntimeStore::SetStoredParameterValueByControlKey(const std::string& layerKey, const std::string& parameterKey, const JsonValue& newValue, bool persistState, std::string& error)
{
return mRuntimeHost.UpdateLayerParameterByControlKey(layerKey, parameterKey, newValue, persistState, error);
}
bool RuntimeStore::ResetStoredLayerParameterValues(const std::string& layerId, std::string& error)
{
return mRuntimeHost.ResetLayerParameters(layerId, error);
}
bool RuntimeStore::SaveStackPresetSnapshot(const std::string& presetName, std::string& error) const
{
return mRuntimeHost.SaveStackPreset(presetName, error);
}
bool RuntimeStore::LoadStackPresetSnapshot(const std::string& presetName, std::string& error)
{
return mRuntimeHost.LoadStackPreset(presetName, error);
}
const std::filesystem::path& RuntimeStore::GetRuntimeRepositoryRoot() const
{
return mRuntimeHost.GetRepoRoot();
}
const std::filesystem::path& RuntimeStore::GetRuntimeUiRoot() const
{
return mRuntimeHost.GetUiRoot();
}
const std::filesystem::path& RuntimeStore::GetRuntimeDocsRoot() const
{
return mRuntimeHost.GetDocsRoot();
}
const std::filesystem::path& RuntimeStore::GetRuntimeDataRoot() const
{
return mRuntimeHost.GetRuntimeRoot();
}
unsigned short RuntimeStore::GetConfiguredControlServerPort() const
{
return mRuntimeHost.GetServerPort();
}
unsigned short RuntimeStore::GetConfiguredOscPort() const
{
return mRuntimeHost.GetOscPort();
}
const std::string& RuntimeStore::GetConfiguredOscBindAddress() const
{
return mRuntimeHost.GetOscBindAddress();
}
double RuntimeStore::GetConfiguredOscSmoothing() const
{
return mRuntimeHost.GetOscSmoothing();
}
unsigned RuntimeStore::GetConfiguredMaxTemporalHistoryFrames() const
{
return mRuntimeHost.GetMaxTemporalHistoryFrames();
}
unsigned RuntimeStore::GetConfiguredPreviewFps() const
{
return mRuntimeHost.GetPreviewFps();
}
bool RuntimeStore::IsExternalKeyingConfigured() const
{
return mRuntimeHost.ExternalKeyingEnabled();
}
const std::string& RuntimeStore::GetConfiguredInputVideoFormat() const
{
return mRuntimeHost.GetInputVideoFormat();
}
const std::string& RuntimeStore::GetConfiguredInputFrameRate() const
{
return mRuntimeHost.GetInputFrameRate();
}
const std::string& RuntimeStore::GetConfiguredOutputVideoFormat() const
{
return mRuntimeHost.GetOutputVideoFormat();
}
const std::string& RuntimeStore::GetConfiguredOutputFrameRate() const
{
return mRuntimeHost.GetOutputFrameRate();
}
void RuntimeStore::SetCompileStatus(bool succeeded, const std::string& message)
{
mRuntimeHost.SetCompileStatus(succeeded, message);
}
void RuntimeStore::ClearReloadRequest()
{
mRuntimeHost.ClearReloadRequest();
}

View File

@@ -0,0 +1,50 @@
#pragma once
#include "RuntimeHost.h"
#include <filesystem>
#include <string>
class RuntimeStore
{
public:
explicit RuntimeStore(RuntimeHost& runtimeHost);
bool InitializeStore(std::string& error);
std::string BuildPersistentStateJson() const;
bool CreateStoredLayer(const std::string& shaderId, std::string& error);
bool DeleteStoredLayer(const std::string& layerId, std::string& error);
bool MoveStoredLayer(const std::string& layerId, int direction, std::string& error);
bool MoveStoredLayerToIndex(const std::string& layerId, std::size_t targetIndex, std::string& error);
bool SetStoredLayerBypassState(const std::string& layerId, bool bypassed, std::string& error);
bool SetStoredLayerShaderSelection(const std::string& layerId, const std::string& shaderId, std::string& error);
bool SetStoredParameterValue(const std::string& layerId, const std::string& parameterId, const JsonValue& newValue, std::string& error);
bool SetStoredParameterValueByControlKey(const std::string& layerKey, const std::string& parameterKey, const JsonValue& newValue, std::string& error);
bool SetStoredParameterValueByControlKey(const std::string& layerKey, const std::string& parameterKey, const JsonValue& newValue, bool persistState, std::string& error);
bool ResetStoredLayerParameterValues(const std::string& layerId, std::string& error);
bool SaveStackPresetSnapshot(const std::string& presetName, std::string& error) const;
bool LoadStackPresetSnapshot(const std::string& presetName, std::string& error);
const std::filesystem::path& GetRuntimeRepositoryRoot() const;
const std::filesystem::path& GetRuntimeUiRoot() const;
const std::filesystem::path& GetRuntimeDocsRoot() const;
const std::filesystem::path& GetRuntimeDataRoot() const;
unsigned short GetConfiguredControlServerPort() const;
unsigned short GetConfiguredOscPort() const;
const std::string& GetConfiguredOscBindAddress() const;
double GetConfiguredOscSmoothing() const;
unsigned GetConfiguredMaxTemporalHistoryFrames() const;
unsigned GetConfiguredPreviewFps() const;
bool IsExternalKeyingConfigured() const;
const std::string& GetConfiguredInputVideoFormat() const;
const std::string& GetConfiguredInputFrameRate() const;
const std::string& GetConfiguredOutputVideoFormat() const;
const std::string& GetConfiguredOutputFrameRate() const;
void SetCompileStatus(bool succeeded, const std::string& message);
void ClearReloadRequest();
private:
RuntimeHost& mRuntimeHost;
};

View File

@@ -178,6 +178,11 @@ bool ShaderCompiler::BuildWrapperSlangSource(const ShaderPackage& shaderPackage,
const unsigned historySamplerCount = shaderPackage.temporal.enabled ? mMaxTemporalHistoryFrames : 0;
wrapperSource = ReplaceAll(wrapperSource, "{{SOURCE_HISTORY_SAMPLERS}}", BuildHistorySamplerDeclarations("gSourceHistory", 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, "{{TEXT_SAMPLERS}}", BuildTextSamplerDeclarations(shaderPackage.parameters));
wrapperSource = ReplaceAll(wrapperSource, "{{TEXT_HELPERS}}", BuildTextHelpers(shaderPackage.parameters));

View File

@@ -473,6 +473,46 @@ bool ParseTemporalSettings(const JsonValue& manifestJson, ShaderPackage& shaderP
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)
{
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) &&
ParseFontAssets(manifestJson, shaderPackage, manifestPath, error) &&
ParseTemporalSettings(manifestJson, shaderPackage, mMaxTemporalHistoryFrames, manifestPath, error) &&
ParseFeedbackSettings(manifestJson, shaderPackage, manifestPath, error) &&
ParseParameterDefinitions(manifestJson, shaderPackage, manifestPath, error);
}

View File

@@ -63,6 +63,12 @@ struct TemporalSettings
unsigned effectiveHistoryLength = 0;
};
struct FeedbackSettings
{
bool enabled = false;
std::string writePassId;
};
struct ShaderTextureAsset
{
std::string id;
@@ -110,6 +116,7 @@ struct ShaderPackage
std::vector<ShaderTextureAsset> textureAssets;
std::vector<ShaderFontAsset> fontAssets;
TemporalSettings temporal;
FeedbackSettings feedback;
std::filesystem::file_time_type shaderWriteTime;
std::filesystem::file_time_type manifestWriteTime;
};
@@ -128,6 +135,7 @@ struct RuntimeRenderState
{
std::string layerId;
std::string shaderId;
std::string shaderName;
std::vector<ShaderParameterDefinition> parameterDefinitions;
std::map<std::string, ShaderParameterValue> parameterValues;
std::vector<ShaderTextureAsset> textureAssets;
@@ -147,4 +155,5 @@ struct RuntimeRenderState
TemporalHistorySource temporalHistorySource = TemporalHistorySource::None;
unsigned requestedTemporalHistoryLength = 0;
unsigned effectiveTemporalHistoryLength = 0;
FeedbackSettings feedback;
};

View File

@@ -0,0 +1,238 @@
#include "VideoBackend.h"
#include "DeckLinkSession.h"
#include "OpenGLVideoIOBridge.h"
#include "HealthTelemetry.h"
#include "RenderEngine.h"
#include <chrono>
VideoBackend::VideoBackend(RenderEngine& renderEngine, HealthTelemetry& healthTelemetry) :
mHealthTelemetry(healthTelemetry),
mVideoIODevice(std::make_unique<DeckLinkSession>()),
mBridge(std::make_unique<OpenGLVideoIOBridge>(renderEngine))
{
}
VideoBackend::~VideoBackend()
{
ReleaseResources();
}
void VideoBackend::ReleaseResources()
{
if (mVideoIODevice)
mVideoIODevice->ReleaseResources();
}
bool VideoBackend::DiscoverDevicesAndModes(const VideoFormatSelection& videoModes, std::string& error)
{
return mVideoIODevice->DiscoverDevicesAndModes(videoModes, error);
}
bool VideoBackend::SelectPreferredFormats(const VideoFormatSelection& videoModes, bool outputAlphaRequired, std::string& error)
{
return mVideoIODevice->SelectPreferredFormats(videoModes, outputAlphaRequired, error);
}
bool VideoBackend::ConfigureInput(const VideoFormat& inputVideoMode, std::string& error)
{
return mVideoIODevice->ConfigureInput(
[this](const VideoIOFrame& frame) { HandleInputFrame(frame); },
inputVideoMode,
error);
}
bool VideoBackend::ConfigureOutput(const VideoFormat& outputVideoMode, bool externalKeyingEnabled, std::string& error)
{
return mVideoIODevice->ConfigureOutput(
[this](const VideoIOCompletion& completion) { HandleOutputFrameCompletion(completion); },
outputVideoMode,
externalKeyingEnabled,
error);
}
bool VideoBackend::Start()
{
return mVideoIODevice->Start();
}
bool VideoBackend::Stop()
{
return mVideoIODevice->Stop();
}
const VideoIOState& VideoBackend::State() const
{
return mVideoIODevice->State();
}
VideoIOState& VideoBackend::MutableState()
{
return mVideoIODevice->MutableState();
}
bool VideoBackend::BeginOutputFrame(VideoIOOutputFrame& frame)
{
return mVideoIODevice->BeginOutputFrame(frame);
}
void VideoBackend::EndOutputFrame(VideoIOOutputFrame& frame)
{
mVideoIODevice->EndOutputFrame(frame);
}
bool VideoBackend::ScheduleOutputFrame(const VideoIOOutputFrame& frame)
{
return mVideoIODevice->ScheduleOutputFrame(frame);
}
void VideoBackend::AccountForCompletionResult(VideoIOCompletionResult result)
{
mVideoIODevice->AccountForCompletionResult(result);
}
bool VideoBackend::HasInputDevice() const
{
return mVideoIODevice->HasInputDevice();
}
bool VideoBackend::HasInputSource() const
{
return mVideoIODevice->HasInputSource();
}
unsigned VideoBackend::InputFrameWidth() const
{
return mVideoIODevice->InputFrameWidth();
}
unsigned VideoBackend::InputFrameHeight() const
{
return mVideoIODevice->InputFrameHeight();
}
unsigned VideoBackend::OutputFrameWidth() const
{
return mVideoIODevice->OutputFrameWidth();
}
unsigned VideoBackend::OutputFrameHeight() const
{
return mVideoIODevice->OutputFrameHeight();
}
unsigned VideoBackend::CaptureTextureWidth() const
{
return mVideoIODevice->CaptureTextureWidth();
}
unsigned VideoBackend::OutputPackTextureWidth() const
{
return mVideoIODevice->OutputPackTextureWidth();
}
VideoIOPixelFormat VideoBackend::InputPixelFormat() const
{
return mVideoIODevice->InputPixelFormat();
}
const std::string& VideoBackend::InputDisplayModeName() const
{
return mVideoIODevice->InputDisplayModeName();
}
const std::string& VideoBackend::OutputModelName() const
{
return mVideoIODevice->OutputModelName();
}
bool VideoBackend::SupportsInternalKeying() const
{
return mVideoIODevice->SupportsInternalKeying();
}
bool VideoBackend::SupportsExternalKeying() const
{
return mVideoIODevice->SupportsExternalKeying();
}
bool VideoBackend::KeyerInterfaceAvailable() const
{
return mVideoIODevice->KeyerInterfaceAvailable();
}
bool VideoBackend::ExternalKeyingActive() const
{
return mVideoIODevice->ExternalKeyingActive();
}
const std::string& VideoBackend::StatusMessage() const
{
return mVideoIODevice->StatusMessage();
}
void VideoBackend::SetStatusMessage(const std::string& message)
{
mVideoIODevice->SetStatusMessage(message);
}
void VideoBackend::HandleInputFrame(const VideoIOFrame& frame)
{
const VideoIOState& state = mVideoIODevice->State();
mHealthTelemetry.TryReportSignalStatus(!frame.hasNoInputSource, state.inputFrameSize.width, state.inputFrameSize.height, state.inputDisplayModeName);
if (mBridge)
mBridge->UploadInputFrame(frame, state);
}
void VideoBackend::HandleOutputFrameCompletion(const VideoIOCompletion& completion)
{
RecordFramePacing(completion.result);
VideoIOOutputFrame outputFrame;
if (!BeginOutputFrame(outputFrame))
return;
const VideoIOState& state = mVideoIODevice->State();
if (mBridge)
mBridge->RenderScheduledFrame(state, completion, outputFrame);
EndOutputFrame(outputFrame);
AccountForCompletionResult(completion.result);
// Schedule the next frame after render work is complete so device-side
// bookkeeping stays with the backend seam and the bridge stays render-only.
ScheduleOutputFrame(outputFrame);
}
void VideoBackend::RecordFramePacing(VideoIOCompletionResult completionResult)
{
const auto now = std::chrono::steady_clock::now();
if (mLastPlayoutCompletionTime != std::chrono::steady_clock::time_point())
{
mCompletionIntervalMilliseconds = std::chrono::duration_cast<std::chrono::duration<double, std::milli>>(now - mLastPlayoutCompletionTime).count();
if (mSmoothedCompletionIntervalMilliseconds <= 0.0)
mSmoothedCompletionIntervalMilliseconds = mCompletionIntervalMilliseconds;
else
mSmoothedCompletionIntervalMilliseconds = mSmoothedCompletionIntervalMilliseconds * 0.9 + mCompletionIntervalMilliseconds * 0.1;
if (mCompletionIntervalMilliseconds > mMaxCompletionIntervalMilliseconds)
mMaxCompletionIntervalMilliseconds = mCompletionIntervalMilliseconds;
}
mLastPlayoutCompletionTime = now;
if (completionResult == VideoIOCompletionResult::DisplayedLate)
++mLateFrameCount;
else if (completionResult == VideoIOCompletionResult::Dropped)
++mDroppedFrameCount;
else if (completionResult == VideoIOCompletionResult::Flushed)
++mFlushedFrameCount;
mHealthTelemetry.TryRecordFramePacingStats(
mCompletionIntervalMilliseconds,
mSmoothedCompletionIntervalMilliseconds,
mMaxCompletionIntervalMilliseconds,
mLateFrameCount,
mDroppedFrameCount,
mFlushedFrameCount);
}

View File

@@ -0,0 +1,69 @@
#pragma once
#include "VideoIOTypes.h"
#include <chrono>
#include <cstdint>
#include <memory>
#include <string>
class HealthTelemetry;
class OpenGLVideoIOBridge;
class RenderEngine;
class VideoIODevice;
class VideoBackend
{
public:
VideoBackend(RenderEngine& renderEngine, HealthTelemetry& healthTelemetry);
~VideoBackend();
void ReleaseResources();
bool DiscoverDevicesAndModes(const VideoFormatSelection& videoModes, std::string& error);
bool SelectPreferredFormats(const VideoFormatSelection& videoModes, bool outputAlphaRequired, std::string& error);
bool ConfigureInput(const VideoFormat& inputVideoMode, std::string& error);
bool ConfigureOutput(const VideoFormat& outputVideoMode, bool externalKeyingEnabled, std::string& error);
bool Start();
bool Stop();
const VideoIOState& State() const;
VideoIOState& MutableState();
bool BeginOutputFrame(VideoIOOutputFrame& frame);
void EndOutputFrame(VideoIOOutputFrame& frame);
bool ScheduleOutputFrame(const VideoIOOutputFrame& frame);
void AccountForCompletionResult(VideoIOCompletionResult result);
bool HasInputDevice() const;
bool HasInputSource() const;
unsigned InputFrameWidth() const;
unsigned InputFrameHeight() const;
unsigned OutputFrameWidth() const;
unsigned OutputFrameHeight() const;
unsigned CaptureTextureWidth() const;
unsigned OutputPackTextureWidth() const;
VideoIOPixelFormat InputPixelFormat() const;
const std::string& InputDisplayModeName() const;
const std::string& OutputModelName() const;
bool SupportsInternalKeying() const;
bool SupportsExternalKeying() const;
bool KeyerInterfaceAvailable() const;
bool ExternalKeyingActive() const;
const std::string& StatusMessage() const;
void SetStatusMessage(const std::string& message);
private:
void HandleInputFrame(const VideoIOFrame& frame);
void HandleOutputFrameCompletion(const VideoIOCompletion& completion);
void RecordFramePacing(VideoIOCompletionResult completionResult);
HealthTelemetry& mHealthTelemetry;
std::unique_ptr<VideoIODevice> mVideoIODevice;
std::unique_ptr<OpenGLVideoIOBridge> mBridge;
std::chrono::steady_clock::time_point mLastPlayoutCompletionTime;
double mCompletionIntervalMilliseconds = 0.0;
double mSmoothedCompletionIntervalMilliseconds = 0.0;
double mMaxCompletionIntervalMilliseconds = 0.0;
uint64_t mLateFrameCount = 0;
uint64_t mDroppedFrameCount = 0;
uint64_t mFlushedFrameCount = 0;
};

View File

@@ -417,11 +417,21 @@ double DeckLinkSession::FrameBudgetMilliseconds() const
return mScheduler.FrameBudgetMilliseconds();
}
bool DeckLinkSession::BeginOutputFrame(VideoIOOutputFrame& frame)
bool DeckLinkSession::AcquireNextOutputVideoFrame(CComPtr<IDeckLinkMutableVideoFrame>& outputVideoFrame)
{
CComPtr<IDeckLinkMutableVideoFrame> outputVideoFrame = outputVideoFrameQueue.front();
if (outputVideoFrameQueue.empty())
return false;
outputVideoFrame = outputVideoFrameQueue.front();
outputVideoFrameQueue.push_back(outputVideoFrame);
outputVideoFrameQueue.pop_front();
return outputVideoFrame != nullptr;
}
bool DeckLinkSession::PopulateOutputFrame(IDeckLinkMutableVideoFrame* outputVideoFrame, VideoIOOutputFrame& frame)
{
if (outputVideoFrame == nullptr)
return false;
CComPtr<IDeckLinkVideoBuffer> outputVideoFrameBuffer;
if (outputVideoFrame->QueryInterface(IID_IDeckLinkVideoBuffer, (void**)&outputVideoFrameBuffer) != S_OK)
@@ -438,11 +448,44 @@ bool DeckLinkSession::BeginOutputFrame(VideoIOOutputFrame& frame)
frame.width = mState.outputFrameSize.width;
frame.height = mState.outputFrameSize.height;
frame.pixelFormat = mState.outputPixelFormat;
frame.nativeFrame = outputVideoFrame.p;
frame.nativeFrame = outputVideoFrame;
frame.nativeBuffer = outputVideoFrameBuffer.Detach();
return true;
}
bool DeckLinkSession::ScheduleFrame(IDeckLinkMutableVideoFrame* outputVideoFrame)
{
const VideoIOScheduleTime scheduleTime = mScheduler.NextScheduleTime();
return outputVideoFrame != nullptr &&
output->ScheduleVideoFrame(outputVideoFrame, scheduleTime.streamTime, scheduleTime.duration, scheduleTime.timeScale) == S_OK;
}
bool DeckLinkSession::ScheduleBlackFrame(IDeckLinkMutableVideoFrame* outputVideoFrame)
{
if (outputVideoFrame == nullptr)
return false;
CComPtr<IDeckLinkVideoBuffer> outputVideoFrameBuffer;
if (outputVideoFrame->QueryInterface(IID_IDeckLinkVideoBuffer, (void**)&outputVideoFrameBuffer) != S_OK)
return false;
if (outputVideoFrameBuffer->StartAccess(bmdBufferAccessWrite) != S_OK)
return false;
void* pFrame = nullptr;
outputVideoFrameBuffer->GetBytes((void**)&pFrame);
memset(pFrame, 0, outputVideoFrame->GetRowBytes() * mState.outputFrameSize.height);
outputVideoFrameBuffer->EndAccess(bmdBufferAccessWrite);
return ScheduleFrame(outputVideoFrame);
}
bool DeckLinkSession::BeginOutputFrame(VideoIOOutputFrame& frame)
{
CComPtr<IDeckLinkMutableVideoFrame> outputVideoFrame;
return AcquireNextOutputVideoFrame(outputVideoFrame) && PopulateOutputFrame(outputVideoFrame, frame);
}
void DeckLinkSession::EndOutputFrame(VideoIOOutputFrame& frame)
{
IDeckLinkVideoBuffer* outputVideoFrameBuffer = static_cast<IDeckLinkVideoBuffer*>(frame.nativeBuffer);
@@ -463,11 +506,7 @@ void DeckLinkSession::AccountForCompletionResult(VideoIOCompletionResult complet
bool DeckLinkSession::ScheduleOutputFrame(const VideoIOOutputFrame& frame)
{
IDeckLinkMutableVideoFrame* outputVideoFrame = static_cast<IDeckLinkMutableVideoFrame*>(frame.nativeFrame);
const VideoIOScheduleTime scheduleTime = mScheduler.NextScheduleTime();
if (outputVideoFrame == nullptr || output->ScheduleVideoFrame(outputVideoFrame, scheduleTime.streamTime, scheduleTime.duration, scheduleTime.timeScale) != S_OK)
return false;
return true;
return ScheduleFrame(outputVideoFrame);
}
bool DeckLinkSession::Start()
@@ -486,31 +525,13 @@ bool DeckLinkSession::Start()
for (unsigned i = 0; i < kPrerollFrameCount; i++)
{
CComPtr<IDeckLinkMutableVideoFrame> outputVideoFrame = outputVideoFrameQueue.front();
outputVideoFrameQueue.push_back(outputVideoFrame);
outputVideoFrameQueue.pop_front();
CComPtr<IDeckLinkVideoBuffer> outputVideoFrameBuffer;
if (outputVideoFrame->QueryInterface(IID_IDeckLinkVideoBuffer, (void**)&outputVideoFrameBuffer) != S_OK)
CComPtr<IDeckLinkMutableVideoFrame> outputVideoFrame;
if (!AcquireNextOutputVideoFrame(outputVideoFrame))
{
MessageBoxA(NULL, "Could not query the preroll output frame buffer.", "DeckLink start failed", MB_OK | MB_ICONERROR);
MessageBoxA(NULL, "Could not acquire a preroll output frame.", "DeckLink start failed", MB_OK | MB_ICONERROR);
return false;
}
if (outputVideoFrameBuffer->StartAccess(bmdBufferAccessWrite) != S_OK)
{
MessageBoxA(NULL, "Could not write to the preroll output frame buffer.", "DeckLink start failed", MB_OK | MB_ICONERROR);
return false;
}
void* pFrame = nullptr;
outputVideoFrameBuffer->GetBytes((void**)&pFrame);
memset(pFrame, 0, outputVideoFrame->GetRowBytes() * mState.outputFrameSize.height);
outputVideoFrameBuffer->EndAccess(bmdBufferAccessWrite);
const VideoIOScheduleTime scheduleTime = mScheduler.NextScheduleTime();
if (output->ScheduleVideoFrame(outputVideoFrame, scheduleTime.streamTime, scheduleTime.duration, scheduleTime.timeScale) != S_OK)
if (!ScheduleBlackFrame(outputVideoFrame))
{
MessageBoxA(NULL, "Could not schedule a preroll output frame.", "DeckLink start failed", MB_OK | MB_ICONERROR);
return false;
@@ -599,23 +620,23 @@ void DeckLinkSession::HandlePlayoutFrameCompleted(IDeckLinkVideoFrame*, BMDOutpu
return;
VideoIOCompletion completion;
completion.result = TranslateCompletionResult(completionResult);
mOutputFrameCallback(completion);
}
VideoIOCompletionResult DeckLinkSession::TranslateCompletionResult(BMDOutputFrameCompletionResult completionResult)
{
switch (completionResult)
{
case bmdOutputFrameDisplayedLate:
completion.result = VideoIOCompletionResult::DisplayedLate;
break;
return VideoIOCompletionResult::DisplayedLate;
case bmdOutputFrameDropped:
completion.result = VideoIOCompletionResult::Dropped;
break;
return VideoIOCompletionResult::Dropped;
case bmdOutputFrameFlushed:
completion.result = VideoIOCompletionResult::Flushed;
break;
return VideoIOCompletionResult::Flushed;
case bmdOutputFrameCompleted:
completion.result = VideoIOCompletionResult::Completed;
break;
return VideoIOCompletionResult::Completed;
default:
completion.result = VideoIOCompletionResult::Unknown;
break;
return VideoIOCompletionResult::Unknown;
}
mOutputFrameCallback(completion);
}

View File

@@ -66,6 +66,12 @@ public:
void HandlePlayoutFrameCompleted(IDeckLinkVideoFrame* completedFrame, BMDOutputFrameCompletionResult completionResult);
private:
bool AcquireNextOutputVideoFrame(CComPtr<IDeckLinkMutableVideoFrame>& outputVideoFrame);
bool PopulateOutputFrame(IDeckLinkMutableVideoFrame* outputVideoFrame, VideoIOOutputFrame& frame);
bool ScheduleFrame(IDeckLinkMutableVideoFrame* outputVideoFrame);
bool ScheduleBlackFrame(IDeckLinkMutableVideoFrame* outputVideoFrame);
static VideoIOCompletionResult TranslateCompletionResult(BMDOutputFrameCompletionResult completionResult);
CComPtr<CaptureDelegate> captureDelegate;
CComPtr<PlayoutDelegate> playoutDelegate;
CComPtr<IDeckLinkInput> input;

View File

@@ -1,12 +1,15 @@
{
"shaderLibrary": "shaders",
"serverPort": 8080,
"oscBindAddress": "0.0.0.0",
"oscPort": 9000,
"oscSmoothing": 0.18,
"inputVideoFormat": "1080p",
"inputFrameRate": "59.94",
"outputVideoFormat": "1080p",
"outputFrameRate": "59.94",
"autoReload": true,
"maxTemporalHistoryFrames": 12,
"previewFps": 30,
"enableExternalKeying": true
}

View File

@@ -0,0 +1,646 @@
# 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
- `RuntimeCoordinator`
- mutation validation and classification
- committed-live versus transient policy
- snapshot and persistence requests
- `RuntimeSnapshotProvider`
- render-facing immutable or near-immutable snapshots
- parameter values prepared for the render path
- `ControlServices`
- OSC ingress
- web control ingress
- reload/file-watch requests
- commit/persist requests
- `RenderEngine`
- sole owner of live GL rendering
- sole consumer of render snapshots plus transient overlays
- `VideoBackend`
- DeckLink input/output lifecycle
- pacing and scheduling
- `HealthTelemetry`
- logging
- counters
- timing traces
- degraded-state reporting
Why this phase comes first:
- it prevents later refactors from reintroducing responsibility overlap
- it gives names to the seams the later phases will build around
- it reduces the risk of replacing one monolith with several poorly-defined ones
Suggested deliverables:
- a short architecture diagram
- a responsibility table for each subsystem
- a list of allowed dependencies between subsystems
- a dedicated Phase 1 design note:
- [PHASE_1_SUBSYSTEM_BOUNDARIES_DESIGN.md](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/docs/PHASE_1_SUBSYSTEM_BOUNDARIES_DESIGN.md)
- a subsystem design bundle index:
- [docs/subsystems/README.md](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/docs/subsystems/README.md)
### 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.

View File

@@ -8,11 +8,15 @@ Set the UDP port in `config/runtime-host.json`:
```json
{
"oscPort": 9000
"oscBindAddress": "127.0.0.1",
"oscPort": 9000,
"oscSmoothing": 0.18
}
```
Set `oscPort` to `0` to disable the OSC listener.
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
@@ -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.
OSC updates are coalesced by target route and applied once per render tick, so rapid controller motion does not force one runtime mutation, disk write, and UI push per incoming UDP packet. Numeric OSC controls can also be slightly smoothed with `oscSmoothing`.
Examples:
```text
@@ -72,6 +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.
OSC-driven parameter changes are not autosaved to `runtime/runtime_state.json`. Stack edits made through the UI and preset operations still persist as before. Smoothing only applies to numeric controls such as floats, `vec2`, and `color`; booleans, enums, text, and triggers stay immediate.
For `trigger` parameters, the OSC value is treated as a pulse. A simple integer or boolean message is enough:
```text
@@ -114,10 +122,21 @@ send('127.0.0.1:9000', '/VideoShaderToys/fisheye-reproject/tiltDegrees', {type:
## Network
The listener binds to localhost only:
By default the listener binds to localhost only:
```text
127.0.0.1:<oscPort>
```
This keeps the control surface local to the machine running Video Shader Toys.
To accept OSC from other machines on the network, set:
```json
{
"oscBindAddress": "0.0.0.0",
"oscPort": 9000
}
```
That listens on all IPv4 interfaces, so make sure your firewall and network are configured appropriately.

View File

@@ -0,0 +1,664 @@
# Phase 1 Design: Subsystem Boundaries and Target Architecture
This document expands Phase 1 of [ARCHITECTURE_RESILIENCE_REVIEW.md](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/docs/ARCHITECTURE_RESILIENCE_REVIEW.md) into a concrete target design. Its purpose is to define the long-term subsystem split before later phases introduce a full event model, split `RuntimeHost`, and move rendering onto a sole-owner render thread.
The main goal of Phase 1 is not to immediately rewrite the app. It is to establish clear ownership boundaries so later refactors all move toward the same architecture instead of solving local problems in conflicting ways.
## Why Phase 1 Exists
Today the app works, but too many responsibilities still converge in a few places:
- `RuntimeHost` owns persistence, live layer state, shader package access, status reporting, and mutation entrypoints.
- `OpenGLComposite` coordinates runtime setup, render state retrieval, shader rebuild handling, transient OSC overlay behavior, and video backend integration.
- DeckLink callback-driven playout still reaches directly into render-facing work.
- Background services rely on polling and shared mutable state more than explicit subsystem contracts.
Those are exactly the kinds of overlaps that make timing issues, state regressions, and recovery edge cases harder to solve cleanly.
Phase 1 creates a map for where each responsibility should eventually live.
## Design Goals
The target architecture should optimize for:
- live timing isolation
- explicit state ownership
- predictable recovery behavior
- clear boundaries between persistent state and transient live state
- easier testing of non-GL and non-hardware logic
- fewer cross-thread shared mutable objects
- a playout model that can evolve toward producer/consumer scheduling
## Non-Goals
Phase 1 does not itself require:
- replacing every direct call with events immediately
- moving all rendering to a new thread yet
- redesigning the shader contract again
- changing DeckLink behavior in place
- removing all existing classes before replacements exist
This phase is the target design and the dependency rules. Later phases perform the actual extraction.
## Current Pressure Points
The following current code paths are the strongest evidence for the split proposed here:
- `RuntimeHost` is both store and live authority:
- [RuntimeHost.h](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/runtime/RuntimeHost.h:15)
- [RuntimeHost.cpp](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/runtime/RuntimeHost.cpp:726)
- `OpenGLComposite` is both app orchestrator and render/runtime coordinator:
- [OpenGLComposite.cpp](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/gl/OpenGLComposite.cpp:106)
- [OpenGLComposite.cpp](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/gl/OpenGLComposite.cpp:283)
- `RuntimeServices` mixes service orchestration with polling and deferred state work:
- [RuntimeServices.h](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/control/RuntimeServices.h:46)
- [RuntimeServices.cpp](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/control/RuntimeServices.cpp:194)
- Playout is still callback-coupled to render-facing work:
- [OpenGLVideoIOBridge.cpp](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/gl/pipeline/OpenGLVideoIOBridge.cpp:68)
## Target Subsystems
The long-term architecture should converge on seven primary subsystems:
1. `RuntimeStore`
2. `RuntimeCoordinator`
3. `RuntimeSnapshotProvider`
4. `ControlServices`
5. `RenderEngine`
6. `VideoBackend`
7. `HealthTelemetry`
The split below is intentionally sharper than the current code. The point is to make ownership obvious.
Subsystem-specific design notes that elaborate these boundaries live under [docs/subsystems](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/docs/subsystems).
## Phase 1 Document Set
This document is the parent note for the Phase 1 subsystem package. The bundle index and subsystem notes live here:
- [Subsystem Design Index](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/docs/subsystems/README.md)
- [RuntimeStore.md](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/docs/subsystems/RuntimeStore.md)
- [RuntimeCoordinator.md](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/docs/subsystems/RuntimeCoordinator.md)
- [RuntimeSnapshotProvider.md](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/docs/subsystems/RuntimeSnapshotProvider.md)
- [ControlServices.md](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/docs/subsystems/ControlServices.md)
- [RenderEngine.md](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/docs/subsystems/RenderEngine.md)
- [VideoBackend.md](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/docs/subsystems/VideoBackend.md)
- [HealthTelemetry.md](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/docs/subsystems/HealthTelemetry.md)
## Current Implementation Foothold
The codebase now has an initial Phase 1 compatibility split in place:
- `RuntimeStore`
- [RuntimeStore.h](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/runtime/RuntimeStore.h)
- [RuntimeStore.cpp](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/runtime/RuntimeStore.cpp)
- `RuntimeCoordinator`
- [RuntimeCoordinator.h](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/runtime/RuntimeCoordinator.h)
- [RuntimeCoordinator.cpp](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/runtime/RuntimeCoordinator.cpp)
- `RuntimeSnapshotProvider`
- [RuntimeSnapshotProvider.h](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/runtime/RuntimeSnapshotProvider.h)
- [RuntimeSnapshotProvider.cpp](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/runtime/RuntimeSnapshotProvider.cpp)
- `ControlServices`
- [ControlServices.h](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/control/ControlServices.h)
- [ControlServices.cpp](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/control/ControlServices.cpp)
- `HealthTelemetry`
- [HealthTelemetry.h](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/runtime/HealthTelemetry.h)
- [HealthTelemetry.cpp](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/runtime/HealthTelemetry.cpp)
- `RenderEngine`
- [RenderEngine.h](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/gl/RenderEngine.h)
- [RenderEngine.cpp](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/gl/RenderEngine.cpp)
- `VideoBackend`
- [VideoBackend.h](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/videoio/VideoBackend.h)
- [VideoBackend.cpp](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/videoio/VideoBackend.cpp)
These are still compatibility seams, not a completed subsystem extraction. Most of them continue to delegate heavily to `RuntimeHost`, `OpenGLComposite`, `DeckLinkSession`, and the existing bridge/pipeline classes. Their purpose is to give later Phase 1 work real code boundaries that can be expanded in parallel:
- store-facing UI/runtime control calls in `OpenGLCompositeRuntimeControls.cpp` now route through `RuntimeStore`
- mutation and reload policy now routes through `RuntimeCoordinator`
- render-state and shader-build reads in `OpenGLComposite.cpp`, `OpenGLShaderPrograms.cpp`, and `ShaderBuildQueue.cpp` now route through `RuntimeSnapshotProvider`
- service ingress and polling coordination now route through `ControlServices`
- timing and status writes now route through `HealthTelemetry`
- render-side frame advancement and render-performance reporting now flow through `RuntimeSnapshotProvider` and `HealthTelemetry` instead of directly through `RuntimeHost`
- `OpenGLComposite` now owns a `RenderEngine` seam for renderer, pipeline, render-pass, and shader-program responsibilities
- `OpenGLComposite` now owns a `VideoBackend` seam for device/session ownership and callback wiring
- `OpenGLVideoIOBridge` now acts as an explicit compatibility adapter between `VideoBackend` and `RenderEngine`, instead of `OpenGLComposite` directly owning both sides
That means the next parallel Phase 1 work can focus on moving responsibility behind these seams instead of first inventing them.
## Subsystem Responsibilities
### `RuntimeStore`
`RuntimeStore` owns persisted and operator-authored state.
It is the source of truth for:
- runtime config loaded from disk
- persisted layer stack structure
- persisted parameter values
- stack preset serialization/deserialization
- shader/package metadata that must survive across renders
It should not be responsible for:
- render-thread timing
- GL resource lifetime
- live transient overlays
- hardware callback coordination
- UI/websocket broadcasting policy
Design rules:
- disk I/O belongs here or in its dedicated writer helper
- values here are authoritative for saved state
- writes may be debounced later, but the data model itself belongs here
### `RuntimeCoordinator`
`RuntimeCoordinator` is the mutation and policy layer.
It is responsible for:
- receiving valid mutation requests from controls, services, or automation
- validating requested changes against shader definitions and config rules
- resolving how persisted state, committed live state, and transient overlays should interact
- requesting snapshot publication when state changes affect render
- requesting persistence when stored state changes
It should not be responsible for:
- direct disk serialization details
- direct GL work
- hardware device lifecycle
- polling loops
Design rules:
- all non-render mutations should eventually flow through this layer
- this layer decides whether a change is persisted, transient, or both
- this layer owns state policy, not device policy
### `RuntimeSnapshotProvider`
`RuntimeSnapshotProvider` publishes render-facing snapshots.
It is responsible for:
- building immutable or near-immutable render snapshots
- translating runtime state into render-ready structures
- publishing versioned snapshots
- serving the render side without large mutable shared locks
It should not be responsible for:
- deciding whether a mutation is allowed
- directly applying UI/OSC requests
- persistence
- shader compilation orchestration
Design rules:
- render consumes snapshots, not live mutable store objects
- snapshots should be cheap to read and explicit about version changes
- dynamic frame-only values may still be attached later, but the snapshot shape should stay stable
### `ControlServices`
`ControlServices` is the ingress boundary for non-render control sources.
It is responsible for:
- OSC receive and route resolution
- REST/websocket/control UI ingress
- file-watch or reload request ingress
- translating external inputs into typed internal actions/events
- low-cost buffering/coalescing where appropriate
It should not be responsible for:
- persistence decisions
- render snapshot building
- hardware playout policy
- direct long-lived state ownership beyond ingress-specific queues
Design rules:
- external inputs enter here and are normalized before they touch core state
- service-specific timing concerns stay here unless they affect whole-app policy
- no service should directly mutate render-facing state structures
### `RenderEngine`
`RenderEngine` is the owner of live rendering behavior.
It is responsible for:
- sole ownership of GL work in the target architecture
- shader program lifecycle once compilation outputs are available
- texture upload scheduling
- render-pass execution
- temporal history and shader feedback resources
- transient render-only overlays
- preview production as a subordinate output
- output-frame production for the video backend
It should not be responsible for:
- persistence
- user-facing control normalization
- hardware discovery/configuration
- high-level runtime mutation policy
Design rules:
- render consumes snapshots plus render-local transient state
- render-local state is allowed if it stays render-local
- preview must be treated as best-effort relative to playout
### `VideoBackend`
`VideoBackend` owns input/output device lifecycle and playout policy.
It is responsible for:
- input device configuration and callbacks
- output device configuration and callbacks
- frame scheduling policy
- buffer-pool ownership
- playout headroom policy
- input signal status
- backend state transitions and recovery logic
It should not be responsible for:
- composing frames
- owning GL contexts long-term
- validating shader parameter changes
- persistence
Design rules:
- this subsystem is the consumer of rendered output frames, not the owner of frame composition policy
- it should evolve toward producer/consumer playout rather than callback-driven rendering
- backend state should be explicit and reportable
### `HealthTelemetry`
`HealthTelemetry` owns structured operational visibility.
It is responsible for:
- logging
- warning/error counters
- timing traces
- subsystem health state
- degraded-mode reporting
- operator-visible health summaries
It should not be responsible for:
- deciding core app behavior
- owning render or backend state
- persistence policy
Design rules:
- all major subsystems publish health information here
- health visibility should outlive UI connection state
- modal dialogs should not be the main operational surface
## Target Dependency Rules
The architecture should follow these rules as closely as possible.
Allowed dependency directions:
- `ControlServices -> RuntimeCoordinator`
- `RuntimeCoordinator -> RuntimeStore`
- `RuntimeCoordinator -> RuntimeSnapshotProvider`
- `RuntimeCoordinator -> HealthTelemetry`
- `RuntimeSnapshotProvider -> RuntimeStore`
- `RenderEngine -> RuntimeSnapshotProvider`
- `RenderEngine -> HealthTelemetry`
- `VideoBackend -> RenderEngine`
- `VideoBackend -> HealthTelemetry`
Conditionally allowed during migration:
- `ControlServices -> HealthTelemetry`
- `ControlServices -> RuntimeStore` only through temporary compatibility shims
Not allowed in the target design:
- `RenderEngine -> RuntimeStore`
- `RenderEngine -> ControlServices`
- `VideoBackend -> RuntimeStore`
- `ControlServices -> RenderEngine` for direct mutation
- `RuntimeStore -> RenderEngine`
- `HealthTelemetry -> any subsystem` for control flow
The key principle is:
- store owns durable data
- coordinator owns mutation policy
- snapshot provider owns render-facing state publication
- render owns live GPU execution
- backend owns device timing
- telemetry observes all of them
## State Ownership Model
The app has several different kinds of state, and Phase 1 should name them explicitly.
### Persisted State
Owned by `RuntimeStore`.
Examples:
- layer stack structure
- selected shader ids
- saved parameter values
- runtime host config
- stack presets
### Committed Live State
Owned logically by `RuntimeCoordinator`, stored in the store or a live-state companion depending on future implementation.
Examples:
- current operator-selected parameter values
- current bypass state
- current selected shader for each layer
This is state that should normally survive until explicitly changed and can be persisted if policy says so.
### Transient Live Overlay State
Owned by the subsystem that consumes it, not by the persisted store.
Examples:
- active OSC overlay targets while automation is flowing
- shader feedback buffers
- temporal history textures
- queued input frames
- in-flight preview state
- playout queue state
This is where many current issues come from. The design rule is:
- transient state may influence output
- transient state should not masquerade as persisted truth
### Health and Timing State
Owned by `HealthTelemetry`.
Examples:
- frame pacing stats
- render timing
- late/dropped frame counters
- queue depths
- warning states
## Target Runtime Flow
This section describes the intended long-term flow once later phases are in place.
### Control Mutation Flow
1. OSC/UI/file-watch input enters `ControlServices`.
2. `ControlServices` normalizes it into an internal action or event.
3. `RuntimeCoordinator` validates and classifies the action.
4. If the action changes durable state, `RuntimeStore` is updated.
5. If the action changes render-facing state, `RuntimeSnapshotProvider` publishes a new snapshot.
6. If the action requires persistence, a persistence request is queued.
7. Health/timing observations are emitted separately.
### Render Flow
1. `RenderEngine` consumes the latest published snapshot.
2. `RenderEngine` combines that snapshot with render-local transient state.
3. `RenderEngine` performs uploads, pass execution, feedback/history maintenance, and output production.
4. `RenderEngine` produces:
- preview-ready output
- video-backend-ready output frames
- render timing and warning signals
### Video Output Flow
Target long-term flow:
1. `RenderEngine` produces completed output frames ahead of demand.
2. `VideoBackend` consumes those frames from a bounded queue or ring buffer.
3. Device callbacks only drive dequeue/schedule/accounting behavior.
4. `HealthTelemetry` records queue depth, lateness, underruns, and recovery events.
### Reload / Shader Rebuild Flow
1. file-watch or manual reload enters through `ControlServices`
2. `RuntimeCoordinator` classifies the reload request
3. `RuntimeStore` and shader/package metadata are refreshed if needed
4. `RuntimeSnapshotProvider` republishes affected snapshot state
5. `RenderEngine` rebuilds render-local resources from the new snapshot/build outputs
The important boundary here is that reload is not "a render concern that also touches persistence." It is a coordinated runtime concern with a render-local execution phase.
## Suggested Public Interfaces
These are not final class signatures, but they show the shape the architecture should move toward.
### `RuntimeStore`
Core responsibilities:
- `LoadConfig()`
- `LoadPersistentState()`
- `SavePersistentStateSnapshot(...)`
- `GetStoredLayerStack()`
- `SetStoredLayerStack(...)`
- `GetStackPresetNames()`
- `SaveStackPreset(...)`
- `LoadStackPreset(...)`
### `RuntimeCoordinator`
Core responsibilities:
- `ApplyControlMutation(...)`
- `ApplyAutomationTarget(...)`
- `ResetLayer(...)`
- `RequestReload(...)`
- `CommitOverlayState(...)`
- `PublishSnapshotIfNeeded()`
- `RequestPersistenceIfNeeded()`
### `RuntimeSnapshotProvider`
Core responsibilities:
- `BuildSnapshot(...)`
- `GetLatestSnapshot()`
- `GetSnapshotVersion()`
- `PublishSnapshot(...)`
### `ControlServices`
Core responsibilities:
- `StartOscIngress(...)`
- `StartWebControlIngress(...)`
- `StartFileWatchIngress(...)`
- `EnqueueControlAction(...)`
- `DrainServiceEvents(...)`
### `RenderEngine`
Core responsibilities:
- `StartRenderLoop(...)`
- `ConsumeSnapshot(...)`
- `EnqueueInputFrame(...)`
- `ProduceOutputFrame(...)`
- `ResetRenderLocalState(...)`
- `HandleRebuildOutputs(...)`
### `VideoBackend`
Core responsibilities:
- `ConfigureInput(...)`
- `ConfigureOutput(...)`
- `StartPlayout(...)`
- `StopPlayout(...)`
- `ConsumeRenderedFrame(...)`
- `ReportBackendState(...)`
### `HealthTelemetry`
Core responsibilities:
- `RecordTimingSample(...)`
- `RecordCounterDelta(...)`
- `RaiseWarning(...)`
- `ClearWarning(...)`
- `AppendLogEntry(...)`
- `BuildHealthSnapshot()`
## Mapping From Current Code to Target Subsystems
This is not a one-to-one rename plan. It is a responsibility migration map.
### Current `RuntimeHost`
Should eventually split across:
- `RuntimeStore`
- `RuntimeCoordinator`
- `RuntimeSnapshotProvider`
- parts of `HealthTelemetry`
Likely examples:
- config loading/saving -> `RuntimeStore`
- layer stack mutation validation -> `RuntimeCoordinator`
- render state building/versioning -> `RuntimeSnapshotProvider`
- timing/status setters -> `HealthTelemetry`
### Current `RuntimeServices`
Should eventually become mostly:
- `ControlServices`
- a small service-hosting shell
Likely examples:
- OSC ingress/coalescing -> `ControlServices`
- file-watch ingress -> `ControlServices`
- deferred service coordination now done by polling -> split between `ControlServices` and event-driven coordinator calls
### Current `OpenGLComposite`
Should eventually split across:
- application bootstrap shell
- `RenderEngine`
- orchestration glue that wires subsystems together
Likely examples:
- render-pass facing code -> `RenderEngine`
- app/service/backend bootstrap -> composition root
- runtime mutation API surface -> coordinator-facing adapter, not render owner
### Current `OpenGLVideoIOBridge` and `DeckLinkSession`
Should eventually align more clearly under:
- `VideoBackend`
- `RenderEngine`
Likely examples:
- device callback and scheduling policy -> `VideoBackend`
- GL upload/readback/render work -> `RenderEngine`
## Architectural Guardrails
As later phases begin, these rules should be treated as guardrails.
### 1. No new cross-cutting state should be added to `RuntimeHost`
If a new feature needs durable state, place it conceptually under `RuntimeStore`.
If it needs render-local transient state, place it conceptually under `RenderEngine`.
If it needs timing/status counters, place it conceptually under `HealthTelemetry`.
### 2. Render-local state should stay render-local
Do not push shader feedback, temporal history, preview caches, or playout queues back into the store just to make them easy to reach from other systems.
### 3. Device callbacks should not become a dumping ground for app work
Callback threads should converge toward signaling and queue management, not core rendering, persistence, or control mutation.
### 4. Persistence should not be used as a control synchronization mechanism
Saving state is not how subsystems discover changes. Published snapshots and explicit events should handle that.
### 5. Health reporting should observe, not coordinate
Telemetry systems may record warnings and degraded states, but they should not become the hidden control plane for the app.
## Migration Strategy
Phase 1 is a design phase, but it should support incremental migration.
Recommended order after this document:
1. Introduce names and interfaces before moving logic.
2. Create compatibility adapters around `RuntimeHost` rather than forcing a flag day.
3. Move read-only render snapshot publication out before moving all mutation logic.
4. Move service ingress boundaries out before removing the old polling shell.
5. Isolate timing/health setters from the core store as early as practical.
This keeps progress measurable while reducing rewrite risk.
## Suggested Deliverables for Completing Phase 1
Phase 1 can reasonably be considered complete once the project has:
- this subsystem-boundary design document
- agreed subsystem names and responsibilities
- agreed allowed dependency directions
- explicit state categories: persisted, committed live, transient overlay, health/timing
- a current-to-target responsibility map for `RuntimeHost`, `RuntimeServices`, `OpenGLComposite`, and backend/render bridge code
- a decision that later phases will build against this target rather than inventing new boundaries ad hoc
## Open Questions For Later Phases
These do not block Phase 1, but they should remain visible.
- Should shader package registry ownership live entirely in `RuntimeStore`, or should compile-ready derived registry data move into the snapshot provider?
- Should committed live state be stored directly in `RuntimeStore`, or split into store plus live-session state owned by the coordinator?
- How much of shader build orchestration belongs to `RenderEngine` versus a separate build service?
- At what phase should preview become fully decoupled from playout cadence?
- Should persistence become its own `PersistenceWriter` subsystem in Phase 6, or remain an implementation detail under `RuntimeStore`?
## Short Version
Phase 1 should establish one simple rule for the rest of the refactor:
- durable state lives in the store
- mutation policy lives in the coordinator
- render-facing state is published as snapshots
- external control sources enter through services
- GL work belongs to render
- hardware pacing belongs to the backend
- health visibility belongs to telemetry
If later phases keep to that rule, the architecture will become materially more resilient without needing another round of foundational boundary changes.

View File

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

View File

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

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

@@ -0,0 +1,62 @@
# Phase 1 Subsystem Design Index
This directory contains the subsystem-specific design notes for Phase 1 of the architecture roadmap.
Start here if you want the Phase 1 package to read as one coherent deliverable rather than as separate subsystem writeups.
Parent documents:
- [Architecture Resilience Review](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/docs/ARCHITECTURE_RESILIENCE_REVIEW.md)
- [Phase 1: Subsystem Boundaries and Target Architecture](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/docs/PHASE_1_SUBSYSTEM_BOUNDARIES_DESIGN.md)
## How This Set Fits Together
- [PHASE_1_SUBSYSTEM_BOUNDARIES_DESIGN.md](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/docs/PHASE_1_SUBSYSTEM_BOUNDARIES_DESIGN.md) defines the top-level subsystem split, dependency rules, state categories, and migration guardrails.
- The notes in this directory expand each subsystem boundary without changing the parent Phase 1 design.
- The subsystem notes are meant to be read as design companions, not as independent alternate architectures.
## Recommended Reading Order
1. [PHASE_1_SUBSYSTEM_BOUNDARIES_DESIGN.md](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/docs/PHASE_1_SUBSYSTEM_BOUNDARIES_DESIGN.md)
2. [RuntimeStore.md](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/docs/subsystems/RuntimeStore.md)
3. [RuntimeCoordinator.md](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/docs/subsystems/RuntimeCoordinator.md)
4. [RuntimeSnapshotProvider.md](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/docs/subsystems/RuntimeSnapshotProvider.md)
5. [ControlServices.md](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/docs/subsystems/ControlServices.md)
6. [RenderEngine.md](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/docs/subsystems/RenderEngine.md)
7. [VideoBackend.md](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/docs/subsystems/VideoBackend.md)
8. [HealthTelemetry.md](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/docs/subsystems/HealthTelemetry.md)
That order mirrors the intended dependency story:
- durable state first
- mutation and publication next
- ingress and render boundaries after that
- device timing and operational visibility last
## Subsystem Notes
- [RuntimeStore.md](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/docs/subsystems/RuntimeStore.md)
Durable runtime config, persisted layer state, presets, and package metadata ownership.
- [RuntimeCoordinator.md](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/docs/subsystems/RuntimeCoordinator.md)
Mutation validation, state classification, reset/reload policy, and publication/persistence requests.
- [RuntimeSnapshotProvider.md](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/docs/subsystems/RuntimeSnapshotProvider.md)
Render-facing snapshot build, publication, and versioning boundaries.
- [ControlServices.md](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/docs/subsystems/ControlServices.md)
OSC, HTTP/WebSocket, and file-watch ingress plus normalization and service-local buffering.
- [RenderEngine.md](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/docs/subsystems/RenderEngine.md)
Sole-owner render/GL boundary, render-local transient state, preview, and playout-ready frame production.
- [VideoBackend.md](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/docs/subsystems/VideoBackend.md)
Device lifecycle, input/output pacing, buffer policy, and producer/consumer playout direction.
- [HealthTelemetry.md](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/docs/subsystems/HealthTelemetry.md)
Logs, warnings, counters, timing traces, and subsystem health snapshots.
## What Phase 1 Should Settle
Phase 1 should leave the project with:
- one agreed subsystem vocabulary
- one agreed dependency direction map
- one agreed state-category model
- one agreed current-to-target migration story
Phase 1 does not need to settle every later implementation detail. The subsystem notes intentionally leave some questions open where later phases need room to choose concrete mechanics.

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -19,6 +19,7 @@ struct ShaderContext
float bypass;
int sourceHistoryLength;
int temporalHistoryLength;
int feedbackAvailable;
};
cbuffer GlobalParams
@@ -34,16 +35,23 @@ cbuffer GlobalParams
float gBypass;
int gSourceHistoryLength;
int gTemporalHistoryLength;
int gFeedbackAvailable;
{{PARAMETER_UNIFORMS}}};
Sampler2D<float4> gVideoInput;
{{SOURCE_HISTORY_SAMPLERS}}{{TEMPORAL_HISTORY_SAMPLERS}}{{TEXTURE_SAMPLERS}}
Sampler2D<float4> gLayerInput;
{{SOURCE_HISTORY_SAMPLERS}}{{TEMPORAL_HISTORY_SAMPLERS}}{{FEEDBACK_SAMPLER}}{{TEXTURE_SAMPLERS}}
{{TEXT_SAMPLERS}}
float4 sampleVideo(float2 tc)
{
return gVideoInput.Sample(tc);
}
float4 sampleLayerInput(float2 tc)
{
return gLayerInput.Sample(tc);
}
float4 sampleSourceHistory(int framesAgo, float2 tc)
{
if (gSourceHistoryLength <= 0)
@@ -74,6 +82,8 @@ float4 sampleTemporalHistory(int framesAgo, float2 tc)
}
}
{{FEEDBACK_HELPER}}
{{TEXT_HELPERS}}
#include "{{USER_SHADER_INCLUDE}}"
@@ -94,6 +104,7 @@ float4 fragmentMain(FragmentInput input) : SV_Target
context.bypass = gBypass;
context.sourceHistoryLength = gSourceHistoryLength;
context.temporalHistoryLength = gTemporalHistoryLength;
context.feedbackAvailable = gFeedbackAvailable;
float4 effectedColor = {{ENTRY_POINT_CALL}};
float mixValue = clamp(gBypass > 0.5 ? 0.0 : gMixAmount, 0.0, 1.0);
return lerp(context.sourceColor, effectedColor, mixValue);

View File

@@ -101,9 +101,17 @@ Optional fields:
- `textures`: texture assets to load and expose as samplers.
- `fonts`: packaged font assets for live text parameters.
- `temporal`: history-buffer requirements.
- `feedback`: optional previous-frame shader-local feedback surface.
Parameter objects may also include an optional `description` string. The control UI displays it as one-line helper text with the full text available on hover, so use it for short operational guidance rather than long documentation.
Metadata conventions:
- Keep `name` short, human-facing, and in title case.
- Keep `category` consistent with existing library groups such as `Color`, `Transform`, `Projection`, `Temporal`, `Scopes & Guides`, `Utility`, `Feedback`, and `Calibration`.
- Keep `description` to one clear sentence in present tense that explains what the shader does for an operator.
- Avoid placeholder, joke, or overly implementation-heavy descriptions unless the shader is intentionally a diagnostic or broken example.
Shader-visible identifiers must be valid Slang-style identifiers:
- `entryPoint`
@@ -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.
## Feedback Surface
Shaders may opt in to a persistent previous-frame feedback surface:
```json
{
"feedback": {
"enabled": true,
"writePass": "final"
}
}
```
Fields:
- `enabled`: when `true`, the runtime allocates one persistent `RGBA16F` feedback surface for this shader at the current render resolution.
- `writePass`: optional pass `id` whose output should become next frame's feedback surface. If omitted, the runtime uses the final declared pass, or the implicit `main` pass for single-pass shaders.
Behavior:
- all passes may sample the same previous-frame feedback surface
- one designated pass writes the next feedback surface
- feedback is previous-frame state, not same-frame pass chaining
Guardrails:
- Feedback is best suited to image-like state such as trails, masks, luminance fields, decay maps, and shader-local analysis buffers.
- Feedback is not a precise long-term data store. The surface uses `RGBA16F`, so repeated accumulation, exact counters, and tightly packed metadata can drift or clamp over time.
- The feedback surface is currently filtered like an image, not configured as strict texel-addressed storage. If you reserve texels as data slots, sample them carefully and do not assume exact CPU-style array semantics.
- Each feedback-enabled layer allocates two full-resolution feedback textures for ping-pong state. This increases VRAM use and adds one extra full-frame feedback copy per rendered frame.
- In multipass shaders, feedback remains previous-frame state even when a pass also consumes same-frame pass outputs. Do not treat feedback as another same-frame intermediate buffer.
Single-pass example:
```json
{
"id": "feedback-glow",
"name": "Feedback Glow",
"feedback": {
"enabled": true
},
"parameters": []
}
```
Multipass example:
```json
{
"passes": [
{
"id": "analysis",
"source": "shader.slang",
"entryPoint": "analyzeFrame",
"output": "analysisBuffer"
},
{
"id": "final",
"source": "shader.slang",
"entryPoint": "finishFrame",
"inputs": ["analysisBuffer"],
"output": "layerOutput"
}
],
"feedback": {
"enabled": true,
"writePass": "final"
}
}
```
The wrapper exposes:
```slang
float4 sampleFeedback(float2 uv);
```
On the first frame, or after a reset, `sampleFeedback` returns transparent black.
Feedback resets when:
- a layer bypass state changes
- a layer changes shader
- the layer itself is removed
- a shader is reloaded or recompiled
- render dimensions change
- the app restarts
Ordinary stack add/remove/reorder operations on other layers are intended to preserve feedback state for unchanged feedback-enabled layers.
So feedback should be treated as live runtime state, not durable saved state.
## Slang Entry Point
Your shader file must implement the manifest `entryPoint`.
@@ -239,6 +339,7 @@ struct ShaderContext
float bypass;
int sourceHistoryLength;
int temporalHistoryLength;
int feedbackAvailable;
};
```
@@ -257,6 +358,7 @@ Fields:
- `bypass`: `1.0` when the layer is bypassed, otherwise `0.0`.
- `sourceHistoryLength`: number of usable source-history frames currently available.
- `temporalHistoryLength`: number of usable temporal frames currently available for this layer.
- `feedbackAvailable`: `1` when previous-frame feedback exists for this layer, otherwise `0`.
Color/precision notes:
@@ -270,17 +372,23 @@ Color/precision notes:
The wrapper provides:
```slang
float4 sampleLayerInput(float2 uv);
float4 sampleVideo(float2 uv);
float4 sampleSourceHistory(int framesAgo, float2 uv);
float4 sampleTemporalHistory(int framesAgo, float2 uv);
float4 sampleFeedback(float2 uv);
```
`sampleVideo` samples the live decoded source video.
`sampleLayerInput` samples the input arriving at this shader layer before any of the layer's own passes run. If this layer follows another shader, it sees that previous shader's output. If this is the first shader layer, it sees the decoded source image.
`sampleVideo` samples the current pass input texture. In single-pass shaders this is usually the layer input. In multipass shaders it may instead be a named pass output or `previousPass`, depending on the manifest routing for that pass.
`sampleSourceHistory` samples previous decoded source frames. `framesAgo` is clamped into the available range. If no history is available, it falls back to `sampleVideo`.
`sampleTemporalHistory` samples previous pre-layer input frames for temporal shaders that request `preLayerInput` history. `framesAgo` is clamped into the available range. If no temporal history is available, it falls back to `sampleVideo`.
`sampleFeedback` samples the shader-local previous-frame feedback surface. If feedback has not been written yet, it returns transparent black.
Example:
```slang
@@ -291,6 +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
Manifest parameters are exposed to Slang as global values with the same `id`.

View File

@@ -6,6 +6,8 @@ float4 balatroSwirl(float2 screenSize, float2 screenCoords, float time, float se
float2 uv = (screenCoords - 0.5 * screenSize) / safeScreenLength - offset - seedOffset;
float uvLength = length(uv);
// First warp: convert to polar space and twist the angle more near the
// center, creating the large spiral motion.
float speed = spinRotation * spinEase * 0.2;
if (isRotate)
speed = time * speed;
@@ -19,6 +21,8 @@ float4 balatroSwirl(float2 screenSize, float2 screenCoords, float time, float se
speed = (time + seed * 17.0) * spinSpeed;
float2 uv2 = float2(uv.x + uv.y, uv.x + uv.y);
// Second warp: a short iterative feedback loop turns the spiral into
// painterly bands while preserving a fixed compile-time loop bound.
for (int i = 0; i < 5; ++i)
{
uv2 += float2(sin(max(uv.x, uv.y)), sin(max(uv.x, uv.y))) + uv;
@@ -32,6 +36,8 @@ float4 balatroSwirl(float2 screenSize, float2 screenCoords, float time, float se
float c1p = max(0.0, 1.0 - contrastMod * abs(1.0 - paintRes));
float c2p = max(0.0, 1.0 - contrastMod * abs(paintRes));
float c3p = 1.0 - min(1.0, c1p + c2p);
// Three soft band weights drive the palette; lighting rides on the brightest
// bands so the swirl keeps dimensional highlights.
float light = (lighting - 0.2) * max(c1p * 5.0 - 4.0, 0.0) + lighting * max(c2p * 5.0 - 4.0, 0.0);
float safeContrast = max(contrast, 0.001);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -31,6 +31,8 @@ float normalizedFisheyeRadius(float theta, float halfFov)
{
float safeHalfFov = max(halfFov, 0.0001);
// Match common fisheye projection families while keeping the selected FOV
// normalized to the same source-image radius.
if (fisheyeModel == 1)
{
return sin(theta * 0.5) / max(sin(safeHalfFov * 0.5), 0.0001);
@@ -49,6 +51,7 @@ float normalizedFisheyeRadius(float theta, float halfFov)
float3 equirectangularRay(float2 uv)
{
// Convert equirectangular UVs into longitude/latitude on the unit sphere.
float longitude = (uv.x - 0.5) * TWO_PI;
float latitude = (0.5 - uv.y) * PI;
float latitudeCos = cos(latitude);
@@ -82,6 +85,8 @@ float4 sampleEdgeFilledVideo(float2 sourceUv, ShaderContext context)
float inwardLength = max(length(inward), 0.000001);
inward /= inwardLength;
// Outside the fisheye image, sample back inward from the nearest edge so the
// fill looks like stretched lens content instead of a hard color plate.
float blurDistance = max(edgeBlur, 0.0);
float4 color = sampleVideo(clampedUv) * 0.32;
color += sampleVideo(saturate(clampedUv + inward * blurDistance * 0.35)) * 0.26;
@@ -114,6 +119,7 @@ float4 shadeVideo(ShaderContext context)
float phi = atan2(ray.y, ray.x);
float fisheyeRadius = normalizedFisheyeRadius(theta, halfFov);
// Project the mirrored sphere ray back into the circular fisheye source.
float2 sourceUv = float2(
center.x + cos(phi) * fisheyeRadius * radius.x,
center.y - sin(phi) * fisheyeRadius * radius.y

View File

@@ -59,6 +59,26 @@
],
"description": "Normalized fisheye radius; adjust X/Y when the image is cropped or squeezed."
},
{
"id": "sourceEdgeCut",
"label": "Source Edge Cut",
"type": "float",
"default": 0.01,
"min": 0,
"max": 0.2,
"step": 0.001,
"description": "Cuts slightly inward from all four source-frame edges before sampling to hide empty border regions."
},
{
"id": "sourceEdgeFeather",
"label": "Source Edge Feather",
"type": "float",
"default": 0.02,
"min": 0,
"max": 0.2,
"step": 0.001,
"description": "Softens the trimmed source edges into the outside color for easier background blending."
},
{
"id": "virtualFovDegrees",
"label": "Virtual FOV",

View File

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

View File

@@ -23,6 +23,8 @@ float3 matteSampleColor(float2 uv, ShaderContext context)
if (blur <= 0.0001)
return center;
// Pre-blur only the color used for screen comparison; the final image keeps
// its original detail and alpha is refined in a later pass.
float2 radius = pixel * blur;
float3 color = center * 0.36;
color += saturate(sampleVideo(saturate(uv + float2(radius.x, 0.0))).rgb) * 0.16;
@@ -37,6 +39,8 @@ float keyDistanceAt(float2 uv, ShaderContext context)
float3 color = matteSampleColor(uv, context);
float3 keyColor = saturate(screenColor.rgb);
float chromaDistance = distance(chroma709(color), chroma709(keyColor)) * 2.65;
// Direction distance is less sensitive to brightness, while chroma distance
// follows broadcast-style color difference; screenBalance blends the two.
float directionDistance = length(safeNormalize(max(color, float3(0.0001, 0.0001, 0.0001))) - safeNormalize(max(keyColor, float3(0.0001, 0.0001, 0.0001)))) * 0.55;
return lerp(directionDistance, chromaDistance, saturate(screenBalance));
}
@@ -65,6 +69,8 @@ float refinedAlphaFromMatte(float2 uv, ShaderContext context)
if (aaRadius > 0.0001)
{
// A small fixed kernel smooths edges and collects min/max alpha for
// black/white cleanup without needing dynamic loops or arrays.
float2 radius = pixel * aaRadius;
float2 halfRadius = radius * 0.5;
float alphaMin = centerAlpha;
@@ -126,6 +132,8 @@ float refinedAlphaFromMatte(float2 uv, ShaderContext context)
alpha = centerAlpha;
}
// Final matte shaping happens after blur/cleanup so clip and contrast affect
// the refined edge rather than the raw screen-distance estimate.
alpha = saturate((alpha - clipBlack) / max(clipWhite - clipBlack, 0.0001));
alpha = saturate((alpha - 0.5) * max(matteContrast, 0.0001) + 0.5);
alpha = pow(max(alpha, 0.0), max(matteGamma, 0.0001));
@@ -135,6 +143,8 @@ float refinedAlphaFromMatte(float2 uv, ShaderContext context)
float spillAmountForColor(float3 color)
{
float3 keyColor = saturate(screenColor.rgb);
// Measure spill as color energy aligned with the screen color minus the
// strongest opposing channel, leaving neutral highlights mostly intact.
float keyComponent = dot(color, safeNormalize(max(keyColor, float3(0.0001, 0.0001, 0.0001))));
float opposingComponent = max(max(color.r * (1.0 - keyColor.r), color.g * (1.0 - keyColor.g)), color.b * (1.0 - keyColor.b));
return saturate(keyComponent - opposingComponent + despillBias);
@@ -187,6 +197,8 @@ float4 applyKey(ShaderContext context)
float cropMask = cropMaskAt(context.uv, context);
alpha *= cropMask;
// Edge recovery is strongest around 50% alpha, where fringing usually lives,
// and fades away for solid foreground/background pixels.
float edgeAmount = saturate(1.0 - abs(alpha * 2.0 - 1.0));
despilled = lerp(despilled, despilled * saturate(edgeColor.rgb), edgeAmount * saturate(edgeRecover));

View File

@@ -36,6 +36,8 @@ float4 shadeVideo(ShaderContext context)
float4 accumulated = float4(0.0, 0.0, 0.0, 0.0);
float clampedSteps = clamp(raySteps, 1.0, 77.0);
// Ray-march a folded procedural field. distanceToSurface advances the ray,
// while inverse-distance accumulation creates the glowing filaments.
for (int i = 0; i < 77; ++i)
{
if (float(i) >= clampedSteps)
@@ -49,11 +51,14 @@ float4 shadeVideo(ShaderContext context)
position.xy = mul(rotateAroundZ(2.0 + originalPosition.z), position.xy);
position.xy = mul(happyAccidentMatrix(originalPosition, timeCos), position.xy);
// Color comes from pre-fold space so the palette varies smoothly even as
// the geometry folds into repeated cells.
float colorSeed = 0.5 * originalPosition.z + length(position - originalPosition);
float4 palette = 1.0 + sin(colorSeed + float4(0.0, 4.0, 3.0, 6.0));
palette /= 0.55 + 1.55 * dot(originalPosition.xy, originalPosition.xy);
position = abs(frac(position) - 0.5);
// Distance to a tiny box/cross primitive inside each repeated cell.
distanceToSurface = abs(min(length(position.xy) - 0.125, min(position.x, position.y) + 0.001)) + 0.001;
accumulated += palette.w * palette / distanceToSurface;
}

View File

@@ -7,6 +7,8 @@ float3 sampleLutCell(float3 index)
float g = floor(index.g + 0.5);
float b = floor(index.b + 0.5);
// The 33^3 cube is packed as blue slices laid horizontally, with red across
// each slice and green down the atlas.
float atlasWidth = LUT_SIZE * LUT_SIZE;
float2 lutUv;
lutUv.x = (r + b * LUT_SIZE + 0.5) / atlasWidth;
@@ -30,6 +32,9 @@ float3 applyLut33(float3 color)
float3 c011 = sampleLutCell(float3(baseIndex.r, nextIndex.g, nextIndex.b));
float3 c111 = sampleLutCell(float3(nextIndex.r, nextIndex.g, nextIndex.b));
// Tetrahedral interpolation chooses one of six paths through the cube.
// This avoids the muddy diagonals that simple trilinear LUT sampling can
// introduce for strong grades.
if (blend.r > blend.g)
{
if (blend.g > blend.b)
@@ -55,6 +60,8 @@ float hash12(float2 value)
float3 outputDither(float2 pixel)
{
// Subtract paired hashes to center the dither around zero, then scale to
// roughly one 8-bit code value.
float r = hash12(pixel + float2(17.0, 31.0)) - hash12(pixel + float2(83.0, 47.0));
float g = hash12(pixel + float2(29.0, 71.0)) - hash12(pixel + float2(53.0, 19.0));
float b = hash12(pixel + float2(61.0, 11.0)) - hash12(pixel + float2(7.0, 97.0));

View File

@@ -20,6 +20,8 @@ float4 shadeVideo(ShaderContext context)
float2 p = (fragCoord + fragCoord - resolution) / resolution.y / safeScale;
p -= center + float2(sin(seed * 6.2831853), cos(seed * 6.2831853)) * 0.035;
// Build a skewed coordinate system around an offset "black hole" so the
// waves pinch and stretch instead of staying radially symmetric.
float iterator = 0.2;
float2 diagonal = normalize(float2(-1.0 + seed * 0.5, 1.0 - seed * 0.35));
float2 blackholeCenter = p - iterator * diagonal;
@@ -30,6 +32,8 @@ float4 shadeVideo(ShaderContext context)
float2 v = singularitySpiral(c, time, iterator);
float2 waves = float2(0.0001, 0.0001);
// Iterative sine feedback creates the accretion texture; the iterator value
// also damps later steps to keep the pattern stable.
for (; iterator < 9.0; iterator += 1.0)
{
waves += 1.0 + sin(v);
@@ -40,6 +44,8 @@ float4 shadeVideo(ShaderContext context)
float disk = 2.0 + diskRadius * diskRadius * (0.25 * safeTightness) - diskRadius;
float centerDarkness = 0.5 + 1.0 / max(dot(c, c), 0.0001);
float rim = 0.025 + abs(length(p) - safeRingRadius) * safeTightness;
// Exponential falloff turns the accumulated wave field into bright rims and
// a darker center without hard thresholds.
float4 redBlueGradient = exp(c.x * float4(0.6, -0.4, -1.0, 0.0) * colorShift);
float4 waveColor = waves.xyyx;

View File

@@ -69,7 +69,7 @@
"id": "vignetteAmount",
"label": "Vignette",
"type": "float",
"default": 0.18,
"default": 0.3,
"min": 0,
"max": 0.6,
"step": 0.01,
@@ -154,6 +154,46 @@
"max": 6,
"step": 0.05,
"description": "Scale of the generated noise pattern."
},
{
"id": "scanlineAmount",
"label": "Scanlines",
"type": "float",
"default": 0.08,
"min": 0,
"max": 0.35,
"step": 0.005,
"description": "Subtle alternating-field luma modulation."
},
{
"id": "chromaCrawlAmount",
"label": "Chroma Crawl",
"type": "float",
"default": 0.035,
"min": 0,
"max": 0.2,
"step": 0.005,
"description": "Moving color shimmer around high-contrast edges."
},
{
"id": "generationLoss",
"label": "Generation Loss",
"type": "float",
"default": 0.18,
"min": 0,
"max": 1,
"step": 0.01,
"description": "Raises blacks, softens detail, lowers contrast, and desaturates chroma like copied tape."
},
{
"id": "sharpnessDrift",
"label": "Sharpness Drift",
"type": "float",
"default": 0.12,
"min": 0,
"max": 0.6,
"step": 0.01,
"description": "Slowly varies picture softness to mimic unstable tape focus."
}
]
}

View File

@@ -8,6 +8,8 @@ float2 jumpy(float2 uv, float framecount)
float2 look = uv;
float m = frac(framecount / 4.0);
float dy = look.y - m;
// Localize the horizontal tear to a moving scanline window instead of
// bending the whole frame equally.
float window = 1.0 / (1.0 + 80.0 * dy * dy);
look.x += 0.05 * sin(look.y * 10.0 + framecount) / 20.0 * onOff(4.0, 4.0, 0.3, framecount) * (0.5 + cos(framecount * 20.0)) * window;
float vShift = (0.1 * wiggle) * 0.4 * onOff(2.0, 3.0, 0.9, framecount) * (sin(framecount) * sin(framecount * 20.0) + (0.5 + 0.1 * sin(framecount * 200.0) * cos(framecount)));
@@ -44,11 +46,16 @@ float noiseHash(float2 p)
return frac(sin(dot(p, float2(127.1, 311.7))) * 43758.5453123);
}
// Gold Noise (c)2015 dcerisano@standard3d.com, adapted for Slang.
float goldNoise(float2 xy, float seed)
float staticHash(float2 p)
{
const float phi = 1.61803398874989484820459;
return frac(tan(distance(xy * phi, xy) * seed) * xy.x);
float3 p3 = frac(float3(p.x, p.y, p.x) * 0.1031);
p3 += dot(p3, p3.yzx + 33.33);
return frac((p3.x + p3.y) * p3.z);
}
float seededStaticHash(float2 p, float seed)
{
return staticHash(p + float2(seed * 37.13, seed * 17.71));
}
float grainScalar(float2 uv)
@@ -59,13 +66,17 @@ float grainScalar(float2 uv)
float3 animatedChromaGrain(float2 uv, float time, float2 outputResolution, float grainSize)
{
float safeGrainSize = max(grainSize, 0.001);
// Quantize the coordinates first so larger grain sizes become visible
// chroma blocks rather than simply lower-frequency smooth noise.
float2 baseUv = uv * outputResolution * float2(0.85, 0.95) / safeGrainSize;
float2 grainUv = floor(baseUv) + 0.5;
float2 drift = float2(time * 19.7, time * 23.3);
float frame = floor(time * 59.94);
float r = grainScalar(grainUv + drift + float2(13.1, 71.7));
float g = grainScalar(grainUv * float2(1.03, 0.97) + drift * 1.11 + float2(47.2, 19.4));
float b = grainScalar(grainUv * float2(0.96, 1.05) + drift * 0.91 + float2(83.6, 53.8));
// Change the grain field per frame instead of drifting it through UV space;
// continuous drift can alias into horizontal bands that march down-frame.
float r = staticHash(grainUv + float2(frame * 17.0 + 13.1, frame * 3.0 + 71.7));
float g = staticHash(grainUv * float2(1.03, 0.97) + float2(frame * 11.0 + 47.2, frame * 5.0 + 19.4));
float b = staticHash(grainUv * float2(0.96, 1.05) + float2(frame * 7.0 + 83.6, frame * 13.0 + 53.8));
return float3(r, g, b) * 2.0 - 1.0;
}
@@ -87,6 +98,8 @@ float valueNoise2(float2 p)
float tapeLineNoise(float2 uv, float time, float2 outputResolution)
{
float y = floor(uv.y * outputResolution.y);
// Combine stable per-line noise with frame-rate noise so bands have both
// slow tape wander and fast electronic shimmer.
float slowLine = valueNoise2(float2(y * 0.021, floor(time * 10.0)));
float fastLine = noiseHash(float2(y * 1.73, floor(time * 59.94)));
float line = (slowLine * 0.7 + fastLine * 0.3) * 2.0 - 1.0;
@@ -102,16 +115,19 @@ float3 analogStatic(float2 uv, float time, float2 outputResolution)
float frame = floor(time * 59.94);
float seed = frac(time);
// Several differently skewed hashes keep the snow from forming obvious
// diagonal or grid patterns at broadcast frame cadence.
float2 goldPixel = pixel + float2(0.37, 0.61) + frame;
float snowA = goldNoise(goldPixel, seed + 0.1);
float snowB = goldNoise(goldPixel * float2(0.37, 2.11) + float2(19.0, 41.0), seed + 0.2);
float snowC = goldNoise(goldPixel * float2(1.73, 0.81) + float2(53.0, 7.0), seed + 0.3);
float snowA = seededStaticHash(goldPixel, seed + 0.1);
float snowB = seededStaticHash(goldPixel * float2(0.37, 2.11) + float2(19.0, 41.0), seed + 0.2);
float snowC = seededStaticHash(goldPixel * float2(1.73, 0.81) + float2(53.0, 7.0), seed + 0.3);
float snow = (snowA * 0.72 + snowB * 0.28) * 2.0 - 1.0;
float lineNoise = tapeLineNoise(uv, time, safeResolution);
float dropoutSeed = goldNoise(float2(floor(uv.y * safeResolution.y * 0.25) + 1.0, frame + 2.0), seed + 0.4);
float dropoutSeed = seededStaticHash(float2(floor(uv.y * safeResolution.y * 0.25) + 1.0, frame + 2.0), seed + 0.4);
float dropout = smoothstep(0.965, 1.0, dropoutSeed);
float fleck = smoothstep(0.988, 1.0, snowA) - smoothstep(0.0, 0.012, snowC);
float fleckSeed = seededStaticHash(pixel + float2(frame * 13.0, -frame * 7.0), seed + 0.5);
float fleck = smoothstep(0.992, 1.0, fleckSeed) - smoothstep(0.0, 0.008, snowC);
float scan = sin(uv.y * safeResolution.y * 3.14159265);
float scanMask = 0.55 + 0.45 * scan * scan;
@@ -138,6 +154,85 @@ float3 softBloom(float2 uv, float2 outputResolution, float radius)
return sum;
}
float3 softCrossBlur(float2 uv, float2 outputResolution, float radius)
{
float2 pixel = 1.0 / max(outputResolution, float2(1.0, 1.0));
float2 offset = pixel * radius;
float3 sum = sampleVideo(frac(uv)).rgb * 0.40;
sum += sampleVideo(frac(uv + float2(offset.x, 0.0))).rgb * 0.15;
sum += sampleVideo(frac(uv - float2(offset.x, 0.0))).rgb * 0.15;
sum += sampleVideo(frac(uv + float2(0.0, offset.y))).rgb * 0.15;
sum += sampleVideo(frac(uv - float2(0.0, offset.y))).rgb * 0.15;
return sum;
}
float3 applyChromaCrawl(float3 color, float2 uv, float time, float2 outputResolution)
{
float amount = saturate(chromaCrawlAmount);
if (amount <= 0.0001)
return color;
float2 pixel = 1.0 / max(outputResolution, float2(1.0, 1.0));
float lumaCenter = dot(color, float3(0.299, 0.587, 0.114));
float lumaX = dot(sampleVideo(frac(uv + float2(pixel.x, 0.0))).rgb, float3(0.299, 0.587, 0.114));
float lumaY = dot(sampleVideo(frac(uv + float2(0.0, pixel.y))).rgb, float3(0.299, 0.587, 0.114));
float edge = saturate((abs(lumaX - lumaCenter) + abs(lumaY - lumaCenter)) * 6.0);
float phase = sin(uv.y * outputResolution.y * 1.35 + time * 36.0) * cos(uv.x * outputResolution.x * 0.55 - time * 21.0);
float2 crawlOffset = float2(phase, -phase * 0.35) * pixel * (1.0 + amount * 8.0);
float3 shiftedA = sampleVideo(frac(uv + crawlOffset)).rgb;
float3 shiftedB = sampleVideo(frac(uv - crawlOffset * 0.75)).rgb;
float3 crawled = color;
crawled.r = lerp(color.r, shiftedA.r, edge * amount);
crawled.b = lerp(color.b, shiftedB.b, edge * amount);
return crawled;
}
float3 applyGenerationLoss(float3 color, float2 uv, float2 outputResolution)
{
float loss = saturate(generationLoss);
if (loss <= 0.0001)
return color;
float3 softened = softCrossBlur(uv, outputResolution, 0.85 + loss * 2.2);
color = lerp(color, softened, loss * 0.42);
float luma = dot(color, float3(0.299, 0.587, 0.114));
float3 gray = float3(luma, luma, luma);
color = lerp(color, gray, loss * 0.32);
color = (color - 0.5) * (1.0 - loss * 0.18) + 0.5;
color = color * (1.0 - loss * 0.08) + float3(0.035, 0.035, 0.04) * loss;
return color;
}
float3 applySharpnessDrift(float3 color, float2 uv, float time, float2 outputResolution)
{
float drift = saturate(sharpnessDrift);
if (drift <= 0.0001)
return color;
float wobble = 0.5 + 0.5 * sin(time * 1.7 + sin(time * 0.37) * 2.0);
float radius = 0.35 + wobble * 2.25;
float3 softened = softCrossBlur(uv, outputResolution, radius);
return lerp(color, softened, drift * (0.35 + 0.65 * wobble));
}
float3 applySubtleScanlines(float3 color, float2 uv, float time, float2 outputResolution)
{
float amount = saturate(scanlineAmount);
if (amount <= 0.0001)
return color;
float scan = sin((uv.y * outputResolution.y + floor(time * 59.94) * 0.5) * 3.14159265);
float field = 0.5 + 0.5 * scan;
float luma = dot(color, float3(0.299, 0.587, 0.114));
float visibility = lerp(1.0, 0.45, saturate(luma));
float modulation = 1.0 - amount * visibility * (0.35 + 0.65 * field);
color.rgb *= modulation;
color.rgb += amount * 0.015 * (1.0 - field);
return color;
}
float3 blurVhs(float2 uv, float d, int sampleCount)
{
float3 sum = float3(0.0, 0.0, 0.0);
@@ -146,6 +241,8 @@ float3 blurVhs(float2 uv, float d, int sampleCount)
float2 pixelOffset = float2(d, 0.0);
float2 scale = 0.66 * 8.0 * pixelOffset;
// The circular tap pattern approximates soft tape smear while keeping the
// maximum loop bound fixed for shader compilation.
for (int i = 0; i < 15; ++i)
{
if (i >= sampleCount)
@@ -170,6 +267,8 @@ float4 buildTapeSmear(ShaderContext context)
float framecount = frac(time * wiggleSpeed / 7.0) * 7.0;
int sampleCount = int(clamp(blurSamples, 3.0, 15.0) + 0.5);
// Split the source into YIQ, smear each component by a different amount,
// then recombine to mimic luma/chroma bandwidth mismatch on tape.
float d = 0.1 - round(frac(time / 3.0)) * 0.1;
uv = jumpy(uv, framecount);
float s = 0.0001 * -d + 0.0001 * wiggle * sin(time * wiggleSpeed);
@@ -202,6 +301,8 @@ float4 finishVhs(ShaderContext context)
float time = distortedTapeTime(context);
float3 color = sampleVideo(context.uv).rgb;
// Radial red/blue offsets create lens and deck misregistration before the
// wider tape effects are layered in.
float2 centered = context.uv * 2.0 - 1.0;
centered.x *= context.outputResolution.x / max(context.outputResolution.y, 1.0);
float2 aberrationOffset = centered * (aberrationAmount * 0.0015);
@@ -219,16 +320,24 @@ float4 finishVhs(ShaderContext context)
float halationMask = smoothstep(0.45, 1.0, halationLuma) * halationAmount;
color += halationSource * float3(1.0, 0.38, 0.24) * halationMask * 0.35;
// Bloom and fade are applied as separate layers so highlights glow without
// flattening the full picture into the faded black level.
float3 bloomSource = softBloom(context.uv, context.outputResolution, 2.0 + smear * 2.5);
float bloomLuma = dot(bloomSource, float3(0.299, 0.587, 0.114));
float bloomMask = smoothstep(0.32, 1.0, bloomLuma) * bloomAmount;
color = lerp(color, bloomSource, bloomAmount * 0.18);
color += bloomSource * float3(1.0, 0.96, 0.92) * bloomMask * 0.24;
color = applySharpnessDrift(color, context.uv, time, context.outputResolution);
color = applyGenerationLoss(color, context.uv, context.outputResolution);
color = applyChromaCrawl(color, context.uv, time, context.outputResolution);
float3 speckle = animatedChromaGrain(context.uv, time, context.outputResolution, noiseSize);
float luma = dot(color, float3(0.299, 0.587, 0.114));
float noiseMask = lerp(0.65, 1.0, 1.0 - saturate(luma));
float chunkiness = lerp(1.0, 2.4, saturate((noiseSize - 1.0) / 5.0));
// Push darker regions harder: analog noise reads most naturally in shadows
// and avoids washing out bright highlights.
float3 chromaNoise = float3(speckle.x * 1.2, speckle.y * 0.28, speckle.z * 1.35);
color += chromaNoise * noiseAmount * noiseMask * chunkiness;
color.rg = lerp(color.rg, float2(color.r, color.g) + speckle.xy * noiseAmount * 0.2 * chunkiness, 0.35);
@@ -244,6 +353,8 @@ float4 finishVhs(ShaderContext context)
color = color * (1.0 - fadeAmount * 0.08) + float3(0.055, 0.055, 0.065) * fadeAmount;
color = lerp(color, softBloom(context.uv, context.outputResolution, 1.0 + smear), fadeAmount * 0.12);
color = applySubtleScanlines(color, context.uv, time, context.outputResolution);
float vignetteBase = context.uv.x * (1.0 - context.uv.x) * context.uv.y * (1.0 - context.uv.y);
float vignette = saturate(pow(vignetteBase * 16.0, 0.22));
color *= lerp(1.0 - vignetteAmount, 1.0, vignette);

View File

@@ -17,6 +17,8 @@ bool intersectCube(float3 rayOrigin, float3 rayDirection, float halfExtent, out
float3 boxMin = float3(-halfExtent, -halfExtent, -halfExtent);
float3 boxMax = float3(halfExtent, halfExtent, halfExtent);
// Slab intersection: find the ray interval that overlaps all three box
// axes, then keep the nearest positive hit.
float3 invDir = 1.0 / rayDirection;
float3 t0 = (boxMin - rayOrigin) * invDir;
float3 t1 = (boxMax - rayOrigin) * invDir;
@@ -43,6 +45,8 @@ float2 cubeFaceUv(float3 hitPoint, float halfExtent, float zoom)
float2 uv = float2(0.5, 0.5);
float safeZoom = max(zoom, 0.001);
// The dominant coordinate tells which face was hit; the other two axes
// become that face's local UVs.
if (face.x >= face.y && face.x >= face.z)
{
uv = hitPoint.x > 0.0
@@ -79,6 +83,8 @@ float4 shadeVideo(ShaderContext context)
float yaw = spin;
float pitch = spin * 0.61 + 0.35;
// Rotate the camera ray into cube-local space instead of rotating the cube
// geometry, which keeps the intersection math axis-aligned.
float3 localOrigin = rotateY(rotateX(rayOrigin, -pitch), -yaw);
float3 localDirection = rotateY(rotateX(rayDirection, -pitch), -yaw);
@@ -96,6 +102,8 @@ float4 shadeVideo(ShaderContext context)
float3 normal;
float3 face = abs(localHit);
// Reconstruct the face normal from the hit point so lighting follows the
// same face choice used for UV lookup.
if (face.x >= face.y && face.x >= face.z)
normal = float3(sign(localHit.x), 0.0, 0.0);
else if (face.y >= face.x && face.y >= face.z)

View File

@@ -0,0 +1,121 @@
{
"id": "video-plane-3d",
"name": "Video Plane 3D",
"description": "Places the video on a perspective 2D plane in 3D space with camera FOV, XYZ position, and pan/tilt/roll controls.",
"category": "Projection",
"entryPoint": "shadeVideo",
"parameters": [
{
"id": "fovDegrees",
"label": "FOV",
"type": "float",
"default": 45,
"min": 5,
"max": 150,
"step": 0.1,
"description": "Virtual camera vertical field of view in degrees."
},
{
"id": "positionX",
"label": "X",
"type": "float",
"default": 0,
"min": -4,
"max": 4,
"step": 0.01,
"description": "Horizontal plane position in world units."
},
{
"id": "positionY",
"label": "Y",
"type": "float",
"default": 0,
"min": -4,
"max": 4,
"step": 0.01,
"description": "Vertical plane position in world units."
},
{
"id": "positionZ",
"label": "Z",
"type": "float",
"default": 2.2,
"min": 0.1,
"max": 10,
"step": 0.01,
"description": "Depth of the plane in front of the virtual camera."
},
{
"id": "panDegrees",
"label": "Pan",
"type": "float",
"default": 0,
"min": -180,
"max": 180,
"step": 0.1,
"description": "Rotates the plane left/right around its vertical axis."
},
{
"id": "tiltDegrees",
"label": "Tilt",
"type": "float",
"default": 0,
"min": -120,
"max": 120,
"step": 0.1,
"description": "Rotates the plane up/down around its horizontal axis."
},
{
"id": "rollDegrees",
"label": "Roll",
"type": "float",
"default": 0,
"min": -180,
"max": 180,
"step": 0.1,
"description": "Rotates the plane around its face normal."
},
{
"id": "planeScale",
"label": "Plane Scale",
"type": "float",
"default": 1.4,
"min": 0.05,
"max": 6,
"step": 0.01,
"description": "Height of the video plane in world units; width follows the source aspect ratio."
},
{
"id": "edgeFeather",
"label": "Edge Feather",
"type": "float",
"default": 1.5,
"min": 0,
"max": 24,
"step": 0.1,
"description": "Softens the plane edge in source pixels."
},
{
"id": "backgroundMix",
"label": "Background Mix",
"type": "float",
"default": 0,
"min": 0,
"max": 1,
"step": 0.01,
"description": "Mixes the original video behind the projected plane."
},
{
"id": "outsideColor",
"label": "Outside Color",
"type": "color",
"default": [
0,
0,
0,
1
],
"description": "Color used where the camera ray misses the plane."
}
]
}

View File

@@ -0,0 +1,84 @@
static const float PI = 3.14159265358979323846;
float radiansFromDegrees(float degrees)
{
return degrees * (PI / 180.0);
}
float3 rotateX(float3 p, float angle)
{
float s = sin(angle);
float c = cos(angle);
return float3(p.x, c * p.y - s * p.z, s * p.y + c * p.z);
}
float3 rotateY(float3 p, float angle)
{
float s = sin(angle);
float c = cos(angle);
return float3(c * p.x + s * p.z, p.y, -s * p.x + c * p.z);
}
float3 rotateZ(float3 p, float angle)
{
float s = sin(angle);
float c = cos(angle);
return float3(c * p.x - s * p.y, s * p.x + c * p.y, p.z);
}
float3 rotateWorldToPlane(float3 value)
{
float pan = radiansFromDegrees(panDegrees);
float tilt = radiansFromDegrees(tiltDegrees);
float roll = radiansFromDegrees(rollDegrees);
return rotateZ(rotateX(rotateY(value, -pan), -tilt), -roll);
}
float planeEdgeMask(float2 uv, float2 inputResolution)
{
float2 feather = max(edgeFeather, 0.0) / max(inputResolution, float2(1.0, 1.0));
feather = max(feather, float2(0.00001, 0.00001));
float left = smoothstep(0.0, feather.x, uv.x);
float right = 1.0 - smoothstep(1.0 - feather.x, 1.0, uv.x);
float top = smoothstep(0.0, feather.y, uv.y);
float bottom = 1.0 - smoothstep(1.0 - feather.y, 1.0, uv.y);
return saturate(left * right * top * bottom);
}
float4 shadeVideo(ShaderContext context)
{
float2 outputResolution = max(context.outputResolution, float2(1.0, 1.0));
float outputAspect = outputResolution.x / outputResolution.y;
float sourceAspect = context.inputResolution.x / max(context.inputResolution.y, 1.0);
float tanHalfFov = tan(radiansFromDegrees(clamp(fovDegrees, 5.0, 150.0)) * 0.5);
float2 screen = float2(context.uv.x * 2.0 - 1.0, 1.0 - context.uv.y * 2.0);
float3 rayOrigin = float3(0.0, 0.0, 0.0);
float3 rayDirection = normalize(float3(screen.x * outputAspect * tanHalfFov, screen.y * tanHalfFov, 1.0));
float3 planePosition = float3(positionX, positionY, max(positionZ, 0.001));
float3 localOrigin = rotateWorldToPlane(rayOrigin - planePosition);
float3 localDirection = rotateWorldToPlane(rayDirection);
float backgroundAmount = saturate(backgroundMix);
float4 background = float4(lerp(outsideColor.rgb, context.sourceColor.rgb, backgroundAmount), 1.0);
if (abs(localDirection.z) < 0.00001)
return background;
float hitDistance = -localOrigin.z / localDirection.z;
if (hitDistance <= 0.0)
return background;
float3 localHit = localOrigin + localDirection * hitDistance;
float halfHeight = max(planeScale, 0.001) * 0.5;
float halfWidth = halfHeight * sourceAspect;
float2 planeUv = float2(
localHit.x / max(halfWidth * 2.0, 0.0001) + 0.5,
0.5 - localHit.y / max(halfHeight * 2.0, 0.0001)
);
float mask = planeEdgeMask(planeUv, max(context.inputResolution, float2(1.0, 1.0)));
float4 planeColor = sampleVideo(clamp(planeUv, 0.0, 1.0));
return saturate(lerp(background, planeColor, mask));
}

View File

@@ -47,6 +47,35 @@
"step": 0.1,
"description": "Rotates the source image around the frame center."
},
{
"id": "cropAspect",
"label": "Crop Aspect",
"type": "enum",
"default": "none",
"options": [
{
"value": "none",
"label": "None"
},
{
"value": "4x3",
"label": "4:3"
},
{
"value": "3x2",
"label": "3:2"
},
{
"value": "1x1",
"label": "1:1"
},
{
"value": "9x16",
"label": "9:16"
}
],
"description": "Crops the visible image to a centered preset aspect ratio without squeezing the source."
},
{
"id": "edgeMode",
"label": "Edge Mode",

View File

@@ -28,8 +28,42 @@ float2 applyEdgeMode(float2 uv, out bool inside)
return uv;
}
float selectedCropAspect()
{
if (cropAspect == 1)
return 4.0 / 3.0;
if (cropAspect == 2)
return 3.0 / 2.0;
if (cropAspect == 3)
return 1.0;
if (cropAspect == 4)
return 9.0 / 16.0;
return 0.0;
}
bool insideCropWindow(float2 uv, float2 resolution)
{
float targetAspect = selectedCropAspect();
if (targetAspect <= 0.0)
return true;
float outputAspect = resolution.x / max(resolution.y, 1.0);
float2 cropSize = float2(1.0, 1.0);
if (outputAspect > targetAspect)
cropSize.x = targetAspect / outputAspect;
else
cropSize.y = outputAspect / targetAspect;
float2 cropMin = (1.0 - cropSize) * 0.5;
float2 cropMax = cropMin + cropSize;
return uv.x >= cropMin.x && uv.x <= cropMax.x && uv.y >= cropMin.y && uv.y <= cropMax.y;
}
float4 shadeVideo(ShaderContext context)
{
if (!insideCropWindow(context.uv, max(context.outputResolution, float2(1.0, 1.0))))
return outsideColor;
float safeZoom = max(zoom, 0.001);
float2 sourceUv = (context.uv - 0.5) / safeZoom + 0.5;
sourceUv -= pan;

View File

@@ -18,6 +18,8 @@ float4 shadeVideo(ShaderContext context)
float resolutionAspect = max(context.outputResolution.x, 1.0) / max(context.outputResolution.y, 1.0);
float width = saturate(overlayScale);
float height = width * resolutionAspect / targetAspect;
// Keep the scope in a 16:9 frame, then shrink it if the requested scale
// would push the overlay beyond the screen bounds.
float fitScale = min(1.0 / max(width, 0.001), 1.0 / max(height, 0.001));
width *= min(fitScale, 1.0);
height *= min(fitScale, 1.0);
@@ -36,6 +38,8 @@ float4 shadeVideo(ShaderContext context)
float3 bg = lerp(color.rgb, float3(0.0, 0.0, 0.0), saturate(backgroundOpacity));
float labelHeight = min(max(pad.x * 0.95, 0.048), 0.12);
// Label textures are authored in UV space, so compensate for the overlay
// and output aspect ratios to keep the glyphs from stretching.
float labelWidth = labelHeight * height * max(context.outputResolution.y, 1.0) / max(width * max(context.outputResolution.x, 1.0), 0.001);
float labelX = max(pad.x * 0.5, labelWidth * 0.55);
float y0 = pad.y;
@@ -63,6 +67,8 @@ float4 shadeVideo(ShaderContext context)
float requestedSamples = clamp(waveformSamples, 1.0, 96.0);
float density = 0.0;
// For each output pixel, march through source rows at the same X coordinate
// and accumulate hits where sampled luma lands near this pixel's Y level.
for (int sampleIndex = 0; sampleIndex < 96; sampleIndex++)
{
float samplePosition = float(sampleIndex);

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