Compare commits
47 Commits
v0.0.5
...
ff10b66d1d
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ff10b66d1d | ||
|
|
fdcc38c6ae | ||
|
|
718e4dcadd | ||
|
|
7740fe209c | ||
|
|
77590f4a62 | ||
|
|
e8a3805fff | ||
|
|
99fd903144 | ||
|
|
761df3b2d0 | ||
|
|
f141d20026 | ||
|
|
bfc32c4a1e | ||
|
|
20476bdf63 | ||
|
|
0ec5a4cfed | ||
|
|
539fcd3351 | ||
|
|
ebc10a9925 | ||
|
|
e5c5920ccd | ||
|
|
3b641dd07a | ||
|
|
e00e2574ed | ||
|
|
e459155d51 | ||
|
|
06f3dd4942 | ||
|
|
0808171677 | ||
|
|
00b6ad4c36 | ||
|
|
d4f6a4a268 | ||
|
|
6e600be112 | ||
|
|
a9b08f7f27 | ||
|
|
ccfc0237fd | ||
|
|
b3705d96cc | ||
|
|
5503ce85a9 | ||
|
|
41677b71ec | ||
|
|
9cbb5d8004 | ||
|
|
cbf1b541dc | ||
|
|
5cbdbd6813 | ||
|
|
b2369c418b | ||
|
|
c4883d3413 | ||
|
|
53e78890a8 | ||
|
|
36b398ea95 | ||
|
|
ba4643dfa3 | ||
|
|
27dbb55f7b | ||
|
|
f6b26bf28b | ||
|
|
861593123d | ||
|
|
34c145e80b | ||
|
|
a24cdc0630 | ||
|
|
120f899b0d | ||
|
|
41075bbc61 | ||
|
|
7f0f60c0e3 | ||
|
|
739231d5a1 | ||
|
|
3629227aa9 | ||
|
|
618831d578 |
@@ -57,11 +57,11 @@ jobs:
|
||||
|
||||
- name: Build Debug
|
||||
shell: powershell
|
||||
run: cmake --build --preset build-debug
|
||||
run: cmake --build --preset build-debug --parallel
|
||||
|
||||
- name: Run Native Tests And Shader Validation
|
||||
shell: powershell
|
||||
run: cmake --build --preset build-debug --target RUN_TESTS
|
||||
run: cmake --build --preset build-debug --target RUN_TESTS --parallel
|
||||
|
||||
ui-ubuntu:
|
||||
name: React UI Build
|
||||
@@ -136,7 +136,7 @@ jobs:
|
||||
|
||||
- name: Build Release
|
||||
shell: powershell
|
||||
run: cmake --build --preset build-release
|
||||
run: cmake --build --preset build-release --parallel
|
||||
|
||||
- name: Install Runtime Package
|
||||
shell: powershell
|
||||
|
||||
5
.vscode/launch.json
vendored
5
.vscode/launch.json
vendored
@@ -11,6 +11,11 @@
|
||||
"cwd": "${workspaceFolder}\\build\\vs2022-x64-debug\\Debug",
|
||||
"environment": [],
|
||||
"console": "internalConsole",
|
||||
"symbolSearchPath": "${workspaceFolder}\\build\\vs2022-x64-debug\\Debug",
|
||||
"requireExactSource": true,
|
||||
"logging": {
|
||||
"moduleLoad": true
|
||||
},
|
||||
"preLaunchTask": "Build LoopThroughWithOpenGLCompositing Debug x64"
|
||||
}
|
||||
]
|
||||
|
||||
6
.vscode/tasks.json
vendored
6
.vscode/tasks.json
vendored
@@ -11,7 +11,8 @@
|
||||
"--config",
|
||||
"Debug",
|
||||
"--target",
|
||||
"LoopThroughWithOpenGLCompositing"
|
||||
"LoopThroughWithOpenGLCompositing",
|
||||
"--parallel"
|
||||
],
|
||||
"group": {
|
||||
"kind": "build",
|
||||
@@ -29,7 +30,8 @@
|
||||
"--config",
|
||||
"Release",
|
||||
"--target",
|
||||
"LoopThroughWithOpenGLCompositing"
|
||||
"LoopThroughWithOpenGLCompositing",
|
||||
"--parallel"
|
||||
],
|
||||
"group": "build",
|
||||
"problemMatcher": "$msCompile"
|
||||
|
||||
215
CMakeLists.txt
215
CMakeLists.txt
@@ -34,10 +34,14 @@ 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"
|
||||
"${APP_DIR}/control/RuntimeControlBridge.h"
|
||||
"${APP_DIR}/control/RuntimeServiceLiveBridge.cpp"
|
||||
"${APP_DIR}/control/RuntimeServiceLiveBridge.h"
|
||||
"${APP_DIR}/control/RuntimeServices.cpp"
|
||||
"${APP_DIR}/control/RuntimeServices.h"
|
||||
"${APP_DIR}/videoio/decklink/DeckLinkAPI_h.h"
|
||||
@@ -60,6 +64,15 @@ set(APP_SOURCES
|
||||
"${APP_DIR}/gl/OpenGLComposite.cpp"
|
||||
"${APP_DIR}/gl/OpenGLComposite.h"
|
||||
"${APP_DIR}/gl/OpenGLCompositeRuntimeControls.cpp"
|
||||
"${APP_DIR}/gl/RenderCommandQueue.cpp"
|
||||
"${APP_DIR}/gl/RenderCommandQueue.h"
|
||||
"${APP_DIR}/gl/RenderEngine.cpp"
|
||||
"${APP_DIR}/gl/RenderEngine.h"
|
||||
"${APP_DIR}/gl/RenderFrameState.h"
|
||||
"${APP_DIR}/gl/RenderFrameStateResolver.cpp"
|
||||
"${APP_DIR}/gl/RenderFrameStateResolver.h"
|
||||
"${APP_DIR}/gl/RuntimeUpdateController.cpp"
|
||||
"${APP_DIR}/gl/RuntimeUpdateController.h"
|
||||
"${APP_DIR}/gl/pipeline/OpenGLRenderPass.cpp"
|
||||
"${APP_DIR}/gl/pipeline/OpenGLRenderPass.h"
|
||||
"${APP_DIR}/gl/pipeline/OpenGLRenderPipeline.cpp"
|
||||
@@ -96,14 +109,45 @@ set(APP_SOURCES
|
||||
"${APP_DIR}/platform/NativeHandles.h"
|
||||
"${APP_DIR}/platform/NativeSockets.h"
|
||||
"${APP_DIR}/resource.h"
|
||||
"${APP_DIR}/runtime/RuntimeHost.cpp"
|
||||
"${APP_DIR}/runtime/RuntimeHost.h"
|
||||
"${APP_DIR}/runtime/RuntimeClock.cpp"
|
||||
"${APP_DIR}/runtime/RuntimeClock.h"
|
||||
"${APP_DIR}/runtime/RuntimeJson.cpp"
|
||||
"${APP_DIR}/runtime/RuntimeJson.h"
|
||||
"${APP_DIR}/runtime/RuntimeParameterUtils.cpp"
|
||||
"${APP_DIR}/runtime/RuntimeParameterUtils.h"
|
||||
"${APP_DIR}/runtime/coordination/RuntimeCoordinator.cpp"
|
||||
"${APP_DIR}/runtime/coordination/RuntimeCoordinator.h"
|
||||
"${APP_DIR}/runtime/events/RuntimeEventCoalescingQueue.h"
|
||||
"${APP_DIR}/runtime/events/RuntimeEventDispatcher.h"
|
||||
"${APP_DIR}/runtime/events/RuntimeEvent.h"
|
||||
"${APP_DIR}/runtime/events/RuntimeEventPayloads.h"
|
||||
"${APP_DIR}/runtime/events/RuntimeEventQueue.h"
|
||||
"${APP_DIR}/runtime/events/RuntimeEventType.h"
|
||||
"${APP_DIR}/runtime/live/RenderStateComposer.cpp"
|
||||
"${APP_DIR}/runtime/live/RenderStateComposer.h"
|
||||
"${APP_DIR}/runtime/live/RuntimeStateLayerModel.cpp"
|
||||
"${APP_DIR}/runtime/live/RuntimeStateLayerModel.h"
|
||||
"${APP_DIR}/runtime/live/RuntimeLiveState.cpp"
|
||||
"${APP_DIR}/runtime/live/RuntimeLiveState.h"
|
||||
"${APP_DIR}/runtime/presentation/RuntimeStateJson.cpp"
|
||||
"${APP_DIR}/runtime/presentation/RuntimeStateJson.h"
|
||||
"${APP_DIR}/runtime/presentation/RuntimeStatePresenter.cpp"
|
||||
"${APP_DIR}/runtime/presentation/RuntimeStatePresenter.h"
|
||||
"${APP_DIR}/runtime/snapshot/RenderSnapshotBuilder.cpp"
|
||||
"${APP_DIR}/runtime/snapshot/RenderSnapshotBuilder.h"
|
||||
"${APP_DIR}/runtime/snapshot/RuntimeSnapshotProvider.cpp"
|
||||
"${APP_DIR}/runtime/snapshot/RuntimeSnapshotProvider.h"
|
||||
"${APP_DIR}/runtime/store/LayerStackStore.cpp"
|
||||
"${APP_DIR}/runtime/store/LayerStackStore.h"
|
||||
"${APP_DIR}/runtime/store/RuntimeConfigStore.cpp"
|
||||
"${APP_DIR}/runtime/store/RuntimeConfigStore.h"
|
||||
"${APP_DIR}/runtime/store/RuntimeStore.cpp"
|
||||
"${APP_DIR}/runtime/store/RuntimeStore.h"
|
||||
"${APP_DIR}/runtime/store/RuntimeStoreReadModels.h"
|
||||
"${APP_DIR}/runtime/store/ShaderPackageCatalog.cpp"
|
||||
"${APP_DIR}/runtime/store/ShaderPackageCatalog.h"
|
||||
"${APP_DIR}/runtime/support/RuntimeJson.cpp"
|
||||
"${APP_DIR}/runtime/support/RuntimeJson.h"
|
||||
"${APP_DIR}/runtime/support/RuntimeParameterUtils.cpp"
|
||||
"${APP_DIR}/runtime/support/RuntimeParameterUtils.h"
|
||||
"${APP_DIR}/runtime/telemetry/HealthTelemetry.cpp"
|
||||
"${APP_DIR}/runtime/telemetry/HealthTelemetry.h"
|
||||
"${APP_DIR}/runtime/telemetry/RuntimeClock.cpp"
|
||||
"${APP_DIR}/runtime/telemetry/RuntimeClock.h"
|
||||
"${APP_DIR}/shader/ShaderCompiler.cpp"
|
||||
"${APP_DIR}/shader/ShaderCompiler.h"
|
||||
"${APP_DIR}/shader/ShaderPackageRegistry.cpp"
|
||||
@@ -114,6 +158,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"
|
||||
@@ -130,6 +176,14 @@ target_include_directories(LoopThroughWithOpenGLCompositing PRIVATE
|
||||
"${APP_DIR}/gl/shader"
|
||||
"${APP_DIR}/platform"
|
||||
"${APP_DIR}/runtime"
|
||||
"${APP_DIR}/runtime/coordination"
|
||||
"${APP_DIR}/runtime/events"
|
||||
"${APP_DIR}/runtime/live"
|
||||
"${APP_DIR}/runtime/presentation"
|
||||
"${APP_DIR}/runtime/snapshot"
|
||||
"${APP_DIR}/runtime/store"
|
||||
"${APP_DIR}/runtime/support"
|
||||
"${APP_DIR}/runtime/telemetry"
|
||||
"${APP_DIR}/shader"
|
||||
"${APP_DIR}/videoio"
|
||||
"${APP_DIR}/videoio/decklink"
|
||||
@@ -156,13 +210,14 @@ if(MSVC)
|
||||
endif()
|
||||
|
||||
add_executable(RuntimeJsonTests
|
||||
"${APP_DIR}/runtime/RuntimeJson.cpp"
|
||||
"${APP_DIR}/runtime/support/RuntimeJson.cpp"
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/tests/RuntimeJsonTests.cpp"
|
||||
)
|
||||
|
||||
target_include_directories(RuntimeJsonTests PRIVATE
|
||||
"${APP_DIR}"
|
||||
"${APP_DIR}/runtime"
|
||||
"${APP_DIR}/runtime/support"
|
||||
)
|
||||
|
||||
if(MSVC)
|
||||
@@ -173,13 +228,14 @@ enable_testing()
|
||||
add_test(NAME RuntimeJsonTests COMMAND RuntimeJsonTests)
|
||||
|
||||
add_executable(RuntimeClockTests
|
||||
"${APP_DIR}/runtime/RuntimeClock.cpp"
|
||||
"${APP_DIR}/runtime/telemetry/RuntimeClock.cpp"
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/tests/RuntimeClockTests.cpp"
|
||||
)
|
||||
|
||||
target_include_directories(RuntimeClockTests PRIVATE
|
||||
"${APP_DIR}"
|
||||
"${APP_DIR}/runtime"
|
||||
"${APP_DIR}/runtime/telemetry"
|
||||
)
|
||||
|
||||
if(MSVC)
|
||||
@@ -188,15 +244,33 @@ endif()
|
||||
|
||||
add_test(NAME RuntimeClockTests COMMAND RuntimeClockTests)
|
||||
|
||||
add_executable(HealthTelemetryTests
|
||||
"${APP_DIR}/runtime/telemetry/HealthTelemetry.cpp"
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/tests/HealthTelemetryTests.cpp"
|
||||
)
|
||||
|
||||
target_include_directories(HealthTelemetryTests PRIVATE
|
||||
"${APP_DIR}"
|
||||
"${APP_DIR}/runtime"
|
||||
"${APP_DIR}/runtime/telemetry"
|
||||
)
|
||||
|
||||
if(MSVC)
|
||||
target_compile_options(HealthTelemetryTests PRIVATE /W3)
|
||||
endif()
|
||||
|
||||
add_test(NAME HealthTelemetryTests COMMAND HealthTelemetryTests)
|
||||
|
||||
add_executable(RuntimeParameterUtilsTests
|
||||
"${APP_DIR}/runtime/RuntimeJson.cpp"
|
||||
"${APP_DIR}/runtime/RuntimeParameterUtils.cpp"
|
||||
"${APP_DIR}/runtime/support/RuntimeJson.cpp"
|
||||
"${APP_DIR}/runtime/support/RuntimeParameterUtils.cpp"
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/tests/RuntimeParameterUtilsTests.cpp"
|
||||
)
|
||||
|
||||
target_include_directories(RuntimeParameterUtilsTests PRIVATE
|
||||
"${APP_DIR}"
|
||||
"${APP_DIR}/runtime"
|
||||
"${APP_DIR}/runtime/support"
|
||||
"${APP_DIR}/shader"
|
||||
)
|
||||
|
||||
@@ -206,6 +280,99 @@ endif()
|
||||
|
||||
add_test(NAME RuntimeParameterUtilsTests COMMAND RuntimeParameterUtilsTests)
|
||||
|
||||
add_executable(RuntimeEventTypeTests
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/tests/RuntimeEventTypeTests.cpp"
|
||||
)
|
||||
|
||||
target_include_directories(RuntimeEventTypeTests PRIVATE
|
||||
"${APP_DIR}"
|
||||
"${APP_DIR}/runtime"
|
||||
"${APP_DIR}/runtime/events"
|
||||
)
|
||||
|
||||
if(MSVC)
|
||||
target_compile_options(RuntimeEventTypeTests PRIVATE /W3)
|
||||
endif()
|
||||
|
||||
add_test(NAME RuntimeEventTypeTests COMMAND RuntimeEventTypeTests)
|
||||
|
||||
add_executable(RuntimeLiveStateTests
|
||||
"${APP_DIR}/runtime/live/RenderStateComposer.cpp"
|
||||
"${APP_DIR}/runtime/live/RuntimeLiveState.cpp"
|
||||
"${APP_DIR}/runtime/support/RuntimeJson.cpp"
|
||||
"${APP_DIR}/runtime/support/RuntimeParameterUtils.cpp"
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/tests/RuntimeLiveStateTests.cpp"
|
||||
)
|
||||
|
||||
target_include_directories(RuntimeLiveStateTests PRIVATE
|
||||
"${APP_DIR}"
|
||||
"${APP_DIR}/runtime"
|
||||
"${APP_DIR}/runtime/live"
|
||||
"${APP_DIR}/runtime/support"
|
||||
"${APP_DIR}/shader"
|
||||
)
|
||||
|
||||
if(MSVC)
|
||||
target_compile_options(RuntimeLiveStateTests PRIVATE /W3)
|
||||
endif()
|
||||
|
||||
add_test(NAME RuntimeLiveStateTests COMMAND RuntimeLiveStateTests)
|
||||
|
||||
add_executable(RuntimeStateLayerModelTests
|
||||
"${APP_DIR}/runtime/live/RuntimeStateLayerModel.cpp"
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/tests/RuntimeStateLayerModelTests.cpp"
|
||||
)
|
||||
|
||||
target_include_directories(RuntimeStateLayerModelTests PRIVATE
|
||||
"${APP_DIR}"
|
||||
"${APP_DIR}/runtime"
|
||||
"${APP_DIR}/runtime/live"
|
||||
)
|
||||
|
||||
if(MSVC)
|
||||
target_compile_options(RuntimeStateLayerModelTests PRIVATE /W3)
|
||||
endif()
|
||||
|
||||
add_test(NAME RuntimeStateLayerModelTests COMMAND RuntimeStateLayerModelTests)
|
||||
|
||||
add_executable(RuntimeSubsystemTests
|
||||
"${APP_DIR}/runtime/coordination/RuntimeCoordinator.cpp"
|
||||
"${APP_DIR}/runtime/snapshot/RenderSnapshotBuilder.cpp"
|
||||
"${APP_DIR}/runtime/store/LayerStackStore.cpp"
|
||||
"${APP_DIR}/runtime/store/RuntimeConfigStore.cpp"
|
||||
"${APP_DIR}/runtime/store/RuntimeStore.cpp"
|
||||
"${APP_DIR}/runtime/store/ShaderPackageCatalog.cpp"
|
||||
"${APP_DIR}/runtime/presentation/RuntimeStateJson.cpp"
|
||||
"${APP_DIR}/runtime/presentation/RuntimeStatePresenter.cpp"
|
||||
"${APP_DIR}/runtime/support/RuntimeJson.cpp"
|
||||
"${APP_DIR}/runtime/support/RuntimeParameterUtils.cpp"
|
||||
"${APP_DIR}/runtime/telemetry/HealthTelemetry.cpp"
|
||||
"${APP_DIR}/runtime/telemetry/RuntimeClock.cpp"
|
||||
"${APP_DIR}/shader/ShaderCompiler.cpp"
|
||||
"${APP_DIR}/shader/ShaderPackageRegistry.cpp"
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/tests/RuntimeSubsystemTests.cpp"
|
||||
)
|
||||
|
||||
target_include_directories(RuntimeSubsystemTests PRIVATE
|
||||
"${APP_DIR}"
|
||||
"${APP_DIR}/platform"
|
||||
"${APP_DIR}/runtime"
|
||||
"${APP_DIR}/runtime/coordination"
|
||||
"${APP_DIR}/runtime/events"
|
||||
"${APP_DIR}/runtime/presentation"
|
||||
"${APP_DIR}/runtime/snapshot"
|
||||
"${APP_DIR}/runtime/store"
|
||||
"${APP_DIR}/runtime/support"
|
||||
"${APP_DIR}/runtime/telemetry"
|
||||
"${APP_DIR}/shader"
|
||||
)
|
||||
|
||||
if(MSVC)
|
||||
target_compile_options(RuntimeSubsystemTests PRIVATE /W3)
|
||||
endif()
|
||||
|
||||
add_test(NAME RuntimeSubsystemTests COMMAND RuntimeSubsystemTests)
|
||||
|
||||
add_executable(Std140BufferTests
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/tests/Std140BufferTests.cpp"
|
||||
)
|
||||
@@ -222,8 +389,26 @@ endif()
|
||||
|
||||
add_test(NAME Std140BufferTests COMMAND Std140BufferTests)
|
||||
|
||||
add_executable(RenderCommandQueueTests
|
||||
"${APP_DIR}/gl/RenderCommandQueue.cpp"
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/tests/RenderCommandQueueTests.cpp"
|
||||
)
|
||||
|
||||
target_include_directories(RenderCommandQueueTests PRIVATE
|
||||
"${APP_DIR}"
|
||||
"${APP_DIR}/gl"
|
||||
"${APP_DIR}/videoio"
|
||||
"${APP_DIR}/videoio/decklink"
|
||||
)
|
||||
|
||||
if(MSVC)
|
||||
target_compile_options(RenderCommandQueueTests PRIVATE /W3)
|
||||
endif()
|
||||
|
||||
add_test(NAME RenderCommandQueueTests COMMAND RenderCommandQueueTests)
|
||||
|
||||
add_executable(ShaderPackageRegistryTests
|
||||
"${APP_DIR}/runtime/RuntimeJson.cpp"
|
||||
"${APP_DIR}/runtime/support/RuntimeJson.cpp"
|
||||
"${APP_DIR}/shader/ShaderPackageRegistry.cpp"
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/tests/ShaderPackageRegistryTests.cpp"
|
||||
)
|
||||
@@ -231,6 +416,7 @@ add_executable(ShaderPackageRegistryTests
|
||||
target_include_directories(ShaderPackageRegistryTests PRIVATE
|
||||
"${APP_DIR}"
|
||||
"${APP_DIR}/runtime"
|
||||
"${APP_DIR}/runtime/support"
|
||||
"${APP_DIR}/shader"
|
||||
)
|
||||
|
||||
@@ -241,7 +427,7 @@ endif()
|
||||
add_test(NAME ShaderPackageRegistryTests COMMAND ShaderPackageRegistryTests)
|
||||
|
||||
add_executable(ShaderSlangValidationTests
|
||||
"${APP_DIR}/runtime/RuntimeJson.cpp"
|
||||
"${APP_DIR}/runtime/support/RuntimeJson.cpp"
|
||||
"${APP_DIR}/shader/ShaderCompiler.cpp"
|
||||
"${APP_DIR}/shader/ShaderPackageRegistry.cpp"
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/tests/ShaderSlangValidationTests.cpp"
|
||||
@@ -251,6 +437,7 @@ target_include_directories(ShaderSlangValidationTests PRIVATE
|
||||
"${APP_DIR}"
|
||||
"${APP_DIR}/platform"
|
||||
"${APP_DIR}/runtime"
|
||||
"${APP_DIR}/runtime/support"
|
||||
"${APP_DIR}/shader"
|
||||
)
|
||||
|
||||
|
||||
@@ -49,7 +49,7 @@ Configure and build the native app:
|
||||
|
||||
```powershell
|
||||
cmake --preset vs2022-x64-debug
|
||||
cmake --build --preset build-debug
|
||||
cmake --build --preset build-debug --parallel
|
||||
```
|
||||
|
||||
Build the React control UI:
|
||||
@@ -80,7 +80,7 @@ npm ci
|
||||
npm run build
|
||||
cd ..
|
||||
cmake --preset vs2022-x64-release
|
||||
cmake --build --preset build-release
|
||||
cmake --build --preset build-release --parallel
|
||||
cmake --install build/vs2022-x64-release --config Release --prefix dist/VideoShader
|
||||
```
|
||||
|
||||
@@ -114,7 +114,7 @@ Compress-Archive -Path dist/VideoShader/* -DestinationPath dist/VideoShader.zip
|
||||
Run native tests:
|
||||
|
||||
```powershell
|
||||
cmake --build --preset build-debug --target RUN_TESTS
|
||||
cmake --build --preset build-debug --target RUN_TESTS --parallel
|
||||
```
|
||||
|
||||
Run the UI production build check:
|
||||
|
||||
@@ -1,47 +1,3 @@
|
||||
/* -LICENSE-START-
|
||||
** Copyright (c) 2012 Blackmagic Design
|
||||
**
|
||||
** Permission is hereby granted, free of charge, to any person or organization
|
||||
** obtaining a copy of the software and accompanying documentation (the
|
||||
** "Software") to use, reproduce, display, distribute, sub-license, execute,
|
||||
** and transmit the Software, and to prepare derivative works of the Software,
|
||||
** and to permit third-parties to whom the Software is furnished to do so, in
|
||||
** accordance with:
|
||||
**
|
||||
** (1) if the Software is obtained from Blackmagic Design, the End User License
|
||||
** Agreement for the Software Development Kit ("EULA") available at
|
||||
** https://www.blackmagicdesign.com/EULA/DeckLinkSDK; or
|
||||
**
|
||||
** (2) if the Software is obtained from any third party, such licensing terms
|
||||
** as notified by that third party,
|
||||
**
|
||||
** and all subject to the following:
|
||||
**
|
||||
** (3) the copyright notices in the Software and this entire statement,
|
||||
** including the above license grant, this restriction and the following
|
||||
** disclaimer, must be included in all copies of the Software, in whole or in
|
||||
** part, and all derivative works of the Software, unless such copies or
|
||||
** derivative works are solely in the form of machine-executable object code
|
||||
** generated by a source language processor.
|
||||
**
|
||||
** (4) THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
||||
** OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
** FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT
|
||||
** SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE
|
||||
** FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE,
|
||||
** ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
** DEALINGS IN THE SOFTWARE.
|
||||
**
|
||||
** A copy of the Software is available free of charge at
|
||||
** https://www.blackmagicdesign.com/desktopvideo_sdk under the EULA.
|
||||
**
|
||||
** -LICENSE-END-
|
||||
*/
|
||||
//
|
||||
// LoopThroughWithOpenGLCompositing.cpp
|
||||
// LoopThroughWithOpenGLCompositing
|
||||
//
|
||||
|
||||
#include "stdafx.h"
|
||||
#include "resource.h"
|
||||
#include "OpenGLComposite.h"
|
||||
@@ -478,7 +434,7 @@ LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
|
||||
}
|
||||
|
||||
// Deselect the current rendering context and delete it
|
||||
wglMakeCurrent(hDC, NULL);
|
||||
wglMakeCurrent(NULL, NULL);
|
||||
wglDeleteContext(hRC);
|
||||
|
||||
// Tell the application to terminate after the window is gone
|
||||
@@ -530,15 +486,12 @@ LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
|
||||
|
||||
if (!sInteractiveResize && pOpenGLComposite)
|
||||
{
|
||||
wglMakeCurrent(hDC, hRC);
|
||||
pOpenGLComposite->paintGL(true);
|
||||
wglMakeCurrent( NULL, NULL );
|
||||
RaiseStatusControls(sStatusStrip);
|
||||
}
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
wglMakeCurrent( NULL, NULL );
|
||||
ShowUnhandledExceptionMessage("Paint failed inside the OpenGL runtime.");
|
||||
}
|
||||
break;
|
||||
|
||||
@@ -1,47 +1,3 @@
|
||||
/* -LICENSE-START-
|
||||
** Copyright (c) 2012 Blackmagic Design
|
||||
**
|
||||
** Permission is hereby granted, free of charge, to any person or organization
|
||||
** obtaining a copy of the software and accompanying documentation (the
|
||||
** "Software") to use, reproduce, display, distribute, sub-license, execute,
|
||||
** and transmit the Software, and to prepare derivative works of the Software,
|
||||
** and to permit third-parties to whom the Software is furnished to do so, in
|
||||
** accordance with:
|
||||
**
|
||||
** (1) if the Software is obtained from Blackmagic Design, the End User License
|
||||
** Agreement for the Software Development Kit ("EULA") available at
|
||||
** https://www.blackmagicdesign.com/EULA/DeckLinkSDK; or
|
||||
**
|
||||
** (2) if the Software is obtained from any third party, such licensing terms
|
||||
** as notified by that third party,
|
||||
**
|
||||
** and all subject to the following:
|
||||
**
|
||||
** (3) the copyright notices in the Software and this entire statement,
|
||||
** including the above license grant, this restriction and the following
|
||||
** disclaimer, must be included in all copies of the Software, in whole or in
|
||||
** part, and all derivative works of the Software, unless such copies or
|
||||
** derivative works are solely in the form of machine-executable object code
|
||||
** generated by a source language processor.
|
||||
**
|
||||
** (4) THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
||||
** OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
** FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT
|
||||
** SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE
|
||||
** FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE,
|
||||
** ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
** DEALINGS IN THE SOFTWARE.
|
||||
**
|
||||
** A copy of the Software is available free of charge at
|
||||
** https://www.blackmagicdesign.com/desktopvideo_sdk under the EULA.
|
||||
**
|
||||
** -LICENSE-END-
|
||||
*/
|
||||
//
|
||||
// LoopThroughWithOpenGLCompositing.h
|
||||
// LoopThroughWithOpenGLCompositing
|
||||
//
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "resource.h"
|
||||
|
||||
@@ -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
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -0,0 +1,343 @@
|
||||
#include "ControlServices.h"
|
||||
|
||||
#include "ControlServer.h"
|
||||
#include "OscServer.h"
|
||||
#include "RuntimeControlBridge.h"
|
||||
#include "RuntimeEventDispatcher.h"
|
||||
#include "RuntimeStore.h"
|
||||
#include <windows.h>
|
||||
|
||||
namespace
|
||||
{
|
||||
constexpr auto kCompatibilityPollFallbackInterval = std::chrono::milliseconds(250);
|
||||
}
|
||||
|
||||
ControlServices::ControlServices(RuntimeEventDispatcher& runtimeEventDispatcher) :
|
||||
mControlServer(std::make_unique<ControlServer>()),
|
||||
mOscServer(std::make_unique<OscServer>()),
|
||||
mRuntimeEventDispatcher(runtimeEventDispatcher),
|
||||
mPollRunning(false)
|
||||
{
|
||||
}
|
||||
|
||||
ControlServices::~ControlServices()
|
||||
{
|
||||
Stop();
|
||||
}
|
||||
|
||||
bool ControlServices::Start(OpenGLComposite& composite, RuntimeStore& runtimeStore, std::string& error)
|
||||
{
|
||||
Stop();
|
||||
|
||||
if (!StartControlServicesBoundary(composite, runtimeStore, *this, *mControlServer, *mOscServer, error))
|
||||
{
|
||||
Stop();
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void ControlServices::BeginPolling(RuntimeCoordinator& runtimeCoordinator)
|
||||
{
|
||||
StartPolling(runtimeCoordinator);
|
||||
}
|
||||
|
||||
void ControlServices::Stop()
|
||||
{
|
||||
StopPolling();
|
||||
|
||||
if (mOscServer)
|
||||
mOscServer->Stop();
|
||||
|
||||
if (mControlServer)
|
||||
mControlServer->Stop();
|
||||
}
|
||||
|
||||
void ControlServices::BroadcastState()
|
||||
{
|
||||
if (mControlServer)
|
||||
mControlServer->BroadcastState();
|
||||
}
|
||||
|
||||
void ControlServices::RequestBroadcastState()
|
||||
{
|
||||
PublishRuntimeStateBroadcastRequested("control-service-request");
|
||||
|
||||
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));
|
||||
PublishOscValueReceived(entry.second, entry.first);
|
||||
}
|
||||
|
||||
(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);
|
||||
}
|
||||
WakePolling();
|
||||
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::ClearOscStateForLayerKey(const std::string& layerKey)
|
||||
{
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mPendingOscMutex);
|
||||
for (auto it = mPendingOscUpdates.begin(); it != mPendingOscUpdates.end();)
|
||||
{
|
||||
if (it->second.layerKey == layerKey)
|
||||
it = mPendingOscUpdates.erase(it);
|
||||
else
|
||||
++it;
|
||||
}
|
||||
}
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mPendingOscCommitMutex);
|
||||
for (auto it = mPendingOscCommits.begin(); it != mPendingOscCommits.end();)
|
||||
{
|
||||
if (it->second.layerKey == layerKey)
|
||||
it = mPendingOscCommits.erase(it);
|
||||
else
|
||||
++it;
|
||||
}
|
||||
}
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mCompletedOscCommitMutex);
|
||||
for (auto it = mCompletedOscCommits.begin(); it != mCompletedOscCommits.end();)
|
||||
{
|
||||
if (it->routeKey.rfind(layerKey + "\n", 0) == 0)
|
||||
it = mCompletedOscCommits.erase(it);
|
||||
else
|
||||
++it;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ControlServices::ConsumeCompletedOscCommits(std::vector<CompletedOscCommit>& completedCommits)
|
||||
{
|
||||
completedCommits.clear();
|
||||
|
||||
std::lock_guard<std::mutex> lock(mCompletedOscCommitMutex);
|
||||
if (mCompletedOscCommits.empty())
|
||||
return;
|
||||
|
||||
completedCommits.swap(mCompletedOscCommits);
|
||||
}
|
||||
|
||||
void ControlServices::StartPolling(RuntimeCoordinator& runtimeCoordinator)
|
||||
{
|
||||
if (mPollRunning.exchange(true))
|
||||
return;
|
||||
|
||||
mPollThread = std::thread([this, &runtimeCoordinator]() { PollLoop(runtimeCoordinator); });
|
||||
}
|
||||
|
||||
void ControlServices::StopPolling()
|
||||
{
|
||||
if (!mPollRunning.exchange(false))
|
||||
return;
|
||||
|
||||
WakePolling();
|
||||
if (mPollThread.joinable())
|
||||
mPollThread.join();
|
||||
}
|
||||
|
||||
void ControlServices::PollLoop(RuntimeCoordinator& runtimeCoordinator)
|
||||
{
|
||||
while (mPollRunning)
|
||||
{
|
||||
std::map<std::string, PendingOscCommit> pendingCommits;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mPendingOscCommitMutex);
|
||||
pendingCommits.swap(mPendingOscCommits);
|
||||
}
|
||||
for (const auto& entry : pendingCommits)
|
||||
{
|
||||
PublishOscCommitRequested(entry.second);
|
||||
const RuntimeCoordinatorResult result = runtimeCoordinator.CommitOscParameterByControlKey(
|
||||
entry.second.layerKey,
|
||||
entry.second.parameterKey,
|
||||
entry.second.value);
|
||||
if (result.accepted)
|
||||
{
|
||||
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));
|
||||
PublishOscOverlaySettled(entry.second);
|
||||
}
|
||||
else if (!result.errorMessage.empty())
|
||||
{
|
||||
OutputDebugStringA(("OSC commit failed: " + result.errorMessage + "\n").c_str());
|
||||
}
|
||||
}
|
||||
|
||||
bool registryChanged = false;
|
||||
const RuntimeCoordinatorResult pollResult = runtimeCoordinator.PollRuntimeStoreChanges(registryChanged);
|
||||
if (pollResult.compileStatusChanged && !pollResult.compileStatusSucceeded && !pollResult.compileStatusMessage.empty())
|
||||
OutputDebugStringA(("Runtime poll failed: " + pollResult.compileStatusMessage + "\n").c_str());
|
||||
|
||||
std::unique_lock<std::mutex> wakeLock(mPollWakeMutex);
|
||||
mPollWakeCondition.wait_for(wakeLock, kCompatibilityPollFallbackInterval, [this]() {
|
||||
return !mPollRunning.load() || mPollWakeRequested;
|
||||
});
|
||||
mPollWakeRequested = false;
|
||||
}
|
||||
}
|
||||
|
||||
void ControlServices::WakePolling()
|
||||
{
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mPollWakeMutex);
|
||||
mPollWakeRequested = true;
|
||||
}
|
||||
mPollWakeCondition.notify_one();
|
||||
}
|
||||
|
||||
void ControlServices::PublishRuntimeStateBroadcastRequested(const std::string& reason)
|
||||
{
|
||||
try
|
||||
{
|
||||
RuntimeStateBroadcastRequestedEvent event;
|
||||
event.reason = reason;
|
||||
if (!mRuntimeEventDispatcher.PublishPayload(event, "ControlServices"))
|
||||
OutputDebugStringA("RuntimeStateBroadcastRequested event publish failed.\n");
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
OutputDebugStringA("RuntimeStateBroadcastRequested event publish threw.\n");
|
||||
}
|
||||
}
|
||||
|
||||
void ControlServices::PublishOscValueReceived(const PendingOscUpdate& update, const std::string& routeKey)
|
||||
{
|
||||
try
|
||||
{
|
||||
OscValueReceivedEvent event;
|
||||
event.routeKey = routeKey;
|
||||
event.layerKey = update.layerKey;
|
||||
event.parameterKey = update.parameterKey;
|
||||
event.valueJson = update.valueJson;
|
||||
if (!mRuntimeEventDispatcher.PublishPayload(event, "ControlServices"))
|
||||
OutputDebugStringA("OscValueReceived event publish failed.\n");
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
OutputDebugStringA("OscValueReceived event publish threw.\n");
|
||||
}
|
||||
}
|
||||
|
||||
void ControlServices::PublishOscCommitRequested(const PendingOscCommit& commit)
|
||||
{
|
||||
try
|
||||
{
|
||||
OscCommitRequestedEvent event;
|
||||
event.routeKey = commit.routeKey;
|
||||
event.layerKey = commit.layerKey;
|
||||
event.parameterKey = commit.parameterKey;
|
||||
event.valueJson = SerializeJson(commit.value, false);
|
||||
event.generation = commit.generation;
|
||||
if (!mRuntimeEventDispatcher.PublishPayload(event, "ControlServices"))
|
||||
OutputDebugStringA("OscCommitRequested event publish failed.\n");
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
OutputDebugStringA("OscCommitRequested event publish threw.\n");
|
||||
}
|
||||
}
|
||||
|
||||
void ControlServices::PublishOscOverlaySettled(const PendingOscCommit& commit)
|
||||
{
|
||||
try
|
||||
{
|
||||
OscOverlayEvent event;
|
||||
event.routeKey = commit.routeKey;
|
||||
event.layerKey = commit.layerKey;
|
||||
event.parameterKey = commit.parameterKey;
|
||||
event.generation = commit.generation;
|
||||
event.settled = true;
|
||||
if (!mRuntimeEventDispatcher.PublishPayload(event, "ControlServices"))
|
||||
OutputDebugStringA("OscOverlaySettled event publish failed.\n");
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
OutputDebugStringA("OscOverlaySettled event publish threw.\n");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
#pragma once
|
||||
|
||||
#include "RuntimeJson.h"
|
||||
#include "RuntimeCoordinator.h"
|
||||
#include "ShaderTypes.h"
|
||||
|
||||
#include <atomic>
|
||||
#include <condition_variable>
|
||||
#include <chrono>
|
||||
#include <map>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
#include <vector>
|
||||
|
||||
class ControlServer;
|
||||
class OpenGLComposite;
|
||||
class OscServer;
|
||||
class RuntimeEventDispatcher;
|
||||
class RuntimeStore;
|
||||
|
||||
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;
|
||||
};
|
||||
|
||||
explicit ControlServices(RuntimeEventDispatcher& runtimeEventDispatcher);
|
||||
~ControlServices();
|
||||
|
||||
bool Start(OpenGLComposite& composite, RuntimeStore& runtimeStore, std::string& error);
|
||||
void BeginPolling(RuntimeCoordinator& runtimeCoordinator);
|
||||
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 ClearOscStateForLayerKey(const std::string& layerKey);
|
||||
void ConsumeCompletedOscCommits(std::vector<CompletedOscCommit>& completedCommits);
|
||||
|
||||
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(RuntimeCoordinator& runtimeCoordinator);
|
||||
void StopPolling();
|
||||
void PollLoop(RuntimeCoordinator& runtimeCoordinator);
|
||||
void WakePolling();
|
||||
void PublishRuntimeStateBroadcastRequested(const std::string& reason);
|
||||
void PublishOscValueReceived(const PendingOscUpdate& update, const std::string& routeKey);
|
||||
void PublishOscCommitRequested(const PendingOscCommit& commit);
|
||||
void PublishOscOverlaySettled(const PendingOscCommit& commit);
|
||||
|
||||
std::unique_ptr<ControlServer> mControlServer;
|
||||
std::unique_ptr<OscServer> mOscServer;
|
||||
RuntimeEventDispatcher& mRuntimeEventDispatcher;
|
||||
std::thread mPollThread;
|
||||
std::atomic<bool> mPollRunning;
|
||||
std::mutex mPollWakeMutex;
|
||||
std::condition_variable mPollWakeCondition;
|
||||
bool mPollWakeRequested = false;
|
||||
std::mutex mPendingOscMutex;
|
||||
std::map<std::string, PendingOscUpdate> mPendingOscUpdates;
|
||||
std::mutex mPendingOscCommitMutex;
|
||||
std::map<std::string, PendingOscCommit> mPendingOscCommits;
|
||||
std::mutex mCompletedOscCommitMutex;
|
||||
std::vector<CompletedOscCommit> mCompletedOscCommits;
|
||||
};
|
||||
@@ -1,15 +1,15 @@
|
||||
#include "RuntimeControlBridge.h"
|
||||
|
||||
#include "ControlServices.h"
|
||||
#include "ControlServer.h"
|
||||
#include "OpenGLComposite.h"
|
||||
#include "OscServer.h"
|
||||
#include "RuntimeHost.h"
|
||||
#include "RuntimeServices.h"
|
||||
#include "RuntimeStore.h"
|
||||
|
||||
bool StartRuntimeControlServices(
|
||||
bool StartControlServicesBoundary(
|
||||
OpenGLComposite& composite,
|
||||
RuntimeHost& runtimeHost,
|
||||
RuntimeServices& runtimeServices,
|
||||
RuntimeStore& runtimeStore,
|
||||
ControlServices& controlServices,
|
||||
ControlServer& controlServer,
|
||||
OscServer& oscServer,
|
||||
std::string& error)
|
||||
@@ -38,15 +38,16 @@ bool StartRuntimeControlServices(
|
||||
return true;
|
||||
};
|
||||
|
||||
if (!controlServer.Start(runtimeHost.GetUiRoot(), runtimeHost.GetDocsRoot(), runtimeHost.GetServerPort(), callbacks, error))
|
||||
if (!controlServer.Start(runtimeStore.GetRuntimeUiRoot(), runtimeStore.GetRuntimeDocsRoot(), runtimeStore.GetConfiguredControlServerPort(), callbacks, error))
|
||||
return false;
|
||||
runtimeHost.SetServerPort(controlServer.GetPort());
|
||||
runtimeStore.SetBoundControlServerPort(controlServer.GetPort());
|
||||
|
||||
OscServer::Callbacks oscCallbacks;
|
||||
oscCallbacks.updateParameter = [&runtimeServices](const std::string& layerKey, const std::string& parameterKey, const std::string& valueJson, std::string& actionError) {
|
||||
return runtimeServices.QueueOscUpdate(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.GetOscBindAddress(), runtimeHost.GetOscPort(), oscCallbacks, error))
|
||||
if (runtimeStore.GetConfiguredOscPort() > 0 &&
|
||||
!oscServer.Start(runtimeStore.GetConfiguredOscBindAddress(), runtimeStore.GetConfiguredOscPort(), oscCallbacks, error))
|
||||
return false;
|
||||
|
||||
return true;
|
||||
|
||||
@@ -3,15 +3,15 @@
|
||||
#include <string>
|
||||
|
||||
class ControlServer;
|
||||
class ControlServices;
|
||||
class OpenGLComposite;
|
||||
class OscServer;
|
||||
class RuntimeHost;
|
||||
class RuntimeServices;
|
||||
class RuntimeStore;
|
||||
|
||||
bool StartRuntimeControlServices(
|
||||
bool StartControlServicesBoundary(
|
||||
OpenGLComposite& composite,
|
||||
RuntimeHost& runtimeHost,
|
||||
RuntimeServices& runtimeServices,
|
||||
RuntimeStore& runtimeStore,
|
||||
ControlServices& controlServices,
|
||||
ControlServer& controlServer,
|
||||
OscServer& oscServer,
|
||||
std::string& error);
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
#include "RuntimeServiceLiveBridge.h"
|
||||
|
||||
#include "RenderEngine.h"
|
||||
#include "RuntimeServices.h"
|
||||
|
||||
#include <windows.h>
|
||||
|
||||
namespace
|
||||
{
|
||||
void DrainServiceEvents(RuntimeServices& runtimeServices, RenderEngine& renderEngine)
|
||||
{
|
||||
std::vector<RuntimeServices::AppliedOscUpdate> appliedOscUpdates;
|
||||
std::vector<RuntimeServices::CompletedOscCommit> completedOscCommits;
|
||||
|
||||
std::string oscError;
|
||||
if (!runtimeServices.ApplyPendingOscUpdates(appliedOscUpdates, oscError) && !oscError.empty())
|
||||
OutputDebugStringA(("OSC apply failed: " + oscError + "\n").c_str());
|
||||
runtimeServices.ConsumeCompletedOscCommits(completedOscCommits);
|
||||
|
||||
std::vector<RenderEngine::OscOverlayUpdate> overlayUpdates;
|
||||
overlayUpdates.reserve(appliedOscUpdates.size());
|
||||
for (const RuntimeServices::AppliedOscUpdate& update : appliedOscUpdates)
|
||||
{
|
||||
overlayUpdates.push_back({ update.routeKey, update.layerKey, update.parameterKey, update.targetValue });
|
||||
}
|
||||
|
||||
std::vector<RenderEngine::OscOverlayCommitCompletion> overlayCommitCompletions;
|
||||
overlayCommitCompletions.reserve(completedOscCommits.size());
|
||||
for (const RuntimeServices::CompletedOscCommit& completedCommit : completedOscCommits)
|
||||
{
|
||||
overlayCommitCompletions.push_back({ completedCommit.routeKey, completedCommit.generation });
|
||||
}
|
||||
|
||||
renderEngine.UpdateOscOverlayState(overlayUpdates, overlayCommitCompletions);
|
||||
}
|
||||
|
||||
void QueueServiceCommitRequests(
|
||||
RuntimeServices& runtimeServices,
|
||||
const std::vector<RenderEngine::OscOverlayCommitRequest>& commitRequests)
|
||||
{
|
||||
for (const RenderEngine::OscOverlayCommitRequest& commitRequest : commitRequests)
|
||||
{
|
||||
std::string commitError;
|
||||
if (!runtimeServices.QueueOscCommit(
|
||||
commitRequest.routeKey,
|
||||
commitRequest.layerKey,
|
||||
commitRequest.parameterKey,
|
||||
commitRequest.value,
|
||||
commitRequest.generation,
|
||||
commitError) &&
|
||||
!commitError.empty())
|
||||
{
|
||||
OutputDebugStringA(("OSC commit queue failed: " + commitError + "\n").c_str());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool RuntimeServiceLiveBridge::PrepareLiveRenderFrameState(
|
||||
RuntimeServices& runtimeServices,
|
||||
RenderEngine& renderEngine,
|
||||
const RenderFrameInput& input,
|
||||
RenderFrameState& frameState)
|
||||
{
|
||||
DrainServiceEvents(runtimeServices, renderEngine);
|
||||
|
||||
std::vector<RenderEngine::OscOverlayCommitRequest> commitRequests;
|
||||
const bool resolved = renderEngine.ResolveRenderFrameState(input, &commitRequests, frameState);
|
||||
|
||||
QueueServiceCommitRequests(runtimeServices, commitRequests);
|
||||
return resolved;
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
#pragma once
|
||||
|
||||
#include "RenderFrameState.h"
|
||||
|
||||
#include <vector>
|
||||
|
||||
class RenderEngine;
|
||||
class RuntimeServices;
|
||||
|
||||
class RuntimeServiceLiveBridge
|
||||
{
|
||||
public:
|
||||
static bool PrepareLiveRenderFrameState(
|
||||
RuntimeServices& runtimeServices,
|
||||
RenderEngine& renderEngine,
|
||||
const RenderFrameInput& input,
|
||||
RenderFrameState& frameState);
|
||||
};
|
||||
@@ -1,18 +1,9 @@
|
||||
#include "RuntimeServices.h"
|
||||
|
||||
#include "ControlServer.h"
|
||||
#include "OscServer.h"
|
||||
#include "RuntimeControlBridge.h"
|
||||
#include "RuntimeHost.h"
|
||||
#include <windows.h>
|
||||
#include "RuntimeStore.h"
|
||||
|
||||
RuntimeServices::RuntimeServices() :
|
||||
mControlServer(std::make_unique<ControlServer>()),
|
||||
mOscServer(std::make_unique<OscServer>()),
|
||||
mPollRunning(false),
|
||||
mRegistryChanged(false),
|
||||
mReloadRequested(false),
|
||||
mPollFailed(false)
|
||||
RuntimeServices::RuntimeServices(RuntimeEventDispatcher& runtimeEventDispatcher) :
|
||||
mControlServices(std::make_unique<ControlServices>(runtimeEventDispatcher))
|
||||
{
|
||||
}
|
||||
|
||||
@@ -21,227 +12,75 @@ RuntimeServices::~RuntimeServices()
|
||||
Stop();
|
||||
}
|
||||
|
||||
bool RuntimeServices::Start(OpenGLComposite& composite, RuntimeHost& runtimeHost, std::string& error)
|
||||
bool RuntimeServices::Start(OpenGLComposite& composite, RuntimeStore& runtimeStore, std::string& error)
|
||||
{
|
||||
Stop();
|
||||
|
||||
if (!StartRuntimeControlServices(composite, runtimeHost, *this, *mControlServer, *mOscServer, error))
|
||||
{
|
||||
Stop();
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
return mControlServices && mControlServices->Start(composite, runtimeStore, error);
|
||||
}
|
||||
|
||||
void RuntimeServices::BeginPolling(RuntimeHost& runtimeHost)
|
||||
void RuntimeServices::BeginPolling(RuntimeCoordinator& runtimeCoordinator)
|
||||
{
|
||||
StartPolling(runtimeHost);
|
||||
if (mControlServices)
|
||||
mControlServices->BeginPolling(runtimeCoordinator);
|
||||
}
|
||||
|
||||
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 (mControlServer)
|
||||
mControlServer->RequestBroadcastState();
|
||||
if (mControlServices)
|
||||
mControlServices->RequestBroadcastState();
|
||||
}
|
||||
|
||||
bool RuntimeServices::QueueOscUpdate(const std::string& layerKey, const std::string& parameterKey, const std::string& valueJson, std::string& error)
|
||||
{
|
||||
(void)error;
|
||||
|
||||
PendingOscUpdate update;
|
||||
update.layerKey = layerKey;
|
||||
update.parameterKey = parameterKey;
|
||||
update.valueJson = valueJson;
|
||||
|
||||
const std::string routeKey = layerKey + "\n" + parameterKey;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mPendingOscMutex);
|
||||
mPendingOscUpdates[routeKey] = std::move(update);
|
||||
}
|
||||
return true;
|
||||
return mControlServices && mControlServices->QueueOscUpdate(layerKey, parameterKey, valueJson, error);
|
||||
}
|
||||
|
||||
bool RuntimeServices::ApplyPendingOscUpdates(std::vector<AppliedOscUpdate>& appliedUpdates, std::string& error)
|
||||
{
|
||||
appliedUpdates.clear();
|
||||
|
||||
std::map<std::string, PendingOscUpdate> pending;
|
||||
if (!mControlServices)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mPendingOscMutex);
|
||||
if (mPendingOscUpdates.empty())
|
||||
return true;
|
||||
pending.swap(mPendingOscUpdates);
|
||||
appliedUpdates.clear();
|
||||
return true;
|
||||
}
|
||||
|
||||
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;
|
||||
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)
|
||||
{
|
||||
(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;
|
||||
return mControlServices && mControlServices->QueueOscCommit(routeKey, layerKey, parameterKey, value, generation, error);
|
||||
}
|
||||
|
||||
void RuntimeServices::ClearOscState()
|
||||
{
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mPendingOscMutex);
|
||||
mPendingOscUpdates.clear();
|
||||
}
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mPendingOscCommitMutex);
|
||||
mPendingOscCommits.clear();
|
||||
}
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mCompletedOscCommitMutex);
|
||||
mCompletedOscCommits.clear();
|
||||
}
|
||||
if (mControlServices)
|
||||
mControlServices->ClearOscState();
|
||||
}
|
||||
|
||||
void RuntimeServices::ClearOscStateForLayerKey(const std::string& layerKey)
|
||||
{
|
||||
if (mControlServices)
|
||||
mControlServices->ClearOscStateForLayerKey(layerKey);
|
||||
}
|
||||
|
||||
void RuntimeServices::ConsumeCompletedOscCommits(std::vector<CompletedOscCommit>& completedCommits)
|
||||
{
|
||||
completedCommits.clear();
|
||||
|
||||
std::lock_guard<std::mutex> lock(mCompletedOscCommitMutex);
|
||||
if (mCompletedOscCommits.empty())
|
||||
return;
|
||||
|
||||
completedCommits.swap(mCompletedOscCommits);
|
||||
}
|
||||
|
||||
RuntimePollEvents RuntimeServices::ConsumePollEvents()
|
||||
{
|
||||
RuntimePollEvents events;
|
||||
events.registryChanged = mRegistryChanged.exchange(false);
|
||||
events.reloadRequested = mReloadRequested.exchange(false);
|
||||
events.failed = mPollFailed.exchange(false);
|
||||
|
||||
if (events.failed)
|
||||
if (!mControlServices)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mPollErrorMutex);
|
||||
events.error = mPollError;
|
||||
completedCommits.clear();
|
||||
return;
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
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);
|
||||
}
|
||||
mControlServices->ConsumeCompletedOscCommits(completedCommits);
|
||||
}
|
||||
|
||||
@@ -1,50 +1,25 @@
|
||||
#pragma once
|
||||
|
||||
#include "RuntimeJson.h"
|
||||
#include "ShaderTypes.h"
|
||||
#include "ControlServices.h"
|
||||
|
||||
#include <atomic>
|
||||
#include <map>
|
||||
#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 RuntimeCoordinator;
|
||||
class RuntimeEventDispatcher;
|
||||
class RuntimeStore;
|
||||
|
||||
class RuntimeServices
|
||||
{
|
||||
public:
|
||||
struct AppliedOscUpdate
|
||||
{
|
||||
std::string routeKey;
|
||||
std::string layerKey;
|
||||
std::string parameterKey;
|
||||
JsonValue targetValue;
|
||||
};
|
||||
using AppliedOscUpdate = ControlServices::AppliedOscUpdate;
|
||||
using CompletedOscCommit = ControlServices::CompletedOscCommit;
|
||||
|
||||
struct CompletedOscCommit
|
||||
{
|
||||
std::string routeKey;
|
||||
uint64_t generation = 0;
|
||||
};
|
||||
|
||||
RuntimeServices();
|
||||
explicit RuntimeServices(RuntimeEventDispatcher& runtimeEventDispatcher);
|
||||
~RuntimeServices();
|
||||
|
||||
bool Start(OpenGLComposite& composite, RuntimeHost& runtimeHost, std::string& error);
|
||||
void BeginPolling(RuntimeHost& runtimeHost);
|
||||
bool Start(OpenGLComposite& composite, RuntimeStore& runtimeStore, std::string& error);
|
||||
void BeginPolling(RuntimeCoordinator& runtimeCoordinator);
|
||||
void Stop();
|
||||
void BroadcastState();
|
||||
void RequestBroadcastState();
|
||||
@@ -52,43 +27,9 @@ public:
|
||||
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 ClearOscStateForLayerKey(const std::string& layerKey);
|
||||
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;
|
||||
std::unique_ptr<ControlServices> mControlServices;
|
||||
};
|
||||
|
||||
@@ -1,127 +1,53 @@
|
||||
#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 "RuntimeParameterUtils.h"
|
||||
#include "RenderEngine.h"
|
||||
#include "RuntimeCoordinator.h"
|
||||
#include "RuntimeEventDispatcher.h"
|
||||
#include "RuntimeServiceLiveBridge.h"
|
||||
#include "RuntimeServices.h"
|
||||
#include "RuntimeSnapshotProvider.h"
|
||||
#include "RuntimeStore.h"
|
||||
#include "RuntimeUpdateController.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)
|
||||
hGLWnd(hWnd), hGLDC(hDC), hGLRC(hRC)
|
||||
{
|
||||
InitializeCriticalSection(&pMutex);
|
||||
mRuntimeHost = std::make_unique<RuntimeHost>();
|
||||
mRenderPipeline = std::make_unique<OpenGLRenderPipeline>(
|
||||
*mRenderer,
|
||||
*mRuntimeHost,
|
||||
[this]() { renderEffect(); },
|
||||
[this]() { ProcessScreenshotRequest(); },
|
||||
[this]() { paintGL(false); });
|
||||
mVideoIOBridge = std::make_unique<OpenGLVideoIOBridge>(
|
||||
*mVideoIO,
|
||||
*mRenderer,
|
||||
*mRenderPipeline,
|
||||
*mRuntimeHost,
|
||||
pMutex,
|
||||
mRuntimeStore = std::make_unique<RuntimeStore>();
|
||||
mRuntimeEventDispatcher = std::make_unique<RuntimeEventDispatcher>();
|
||||
mRuntimeSnapshotProvider = std::make_unique<RuntimeSnapshotProvider>(mRuntimeStore->GetRenderSnapshotBuilder(), *mRuntimeEventDispatcher);
|
||||
mRuntimeCoordinator = std::make_unique<RuntimeCoordinator>(*mRuntimeStore, *mRuntimeEventDispatcher);
|
||||
mRenderEngine = std::make_unique<RenderEngine>(
|
||||
*mRuntimeSnapshotProvider,
|
||||
mRuntimeStore->GetHealthTelemetry(),
|
||||
hGLDC,
|
||||
hGLRC);
|
||||
mRenderPass = std::make_unique<OpenGLRenderPass>(*mRenderer);
|
||||
mShaderPrograms = std::make_unique<OpenGLShaderPrograms>(*mRenderer, *mRuntimeHost);
|
||||
mShaderBuildQueue = std::make_unique<ShaderBuildQueue>(*mRuntimeHost);
|
||||
mRuntimeServices = std::make_unique<RuntimeServices>();
|
||||
hGLRC,
|
||||
[this]() { renderEffect(); },
|
||||
[]() {},
|
||||
[this]() { paintGL(false); });
|
||||
mVideoBackend = std::make_unique<VideoBackend>(*mRenderEngine, mRuntimeStore->GetHealthTelemetry(), *mRuntimeEventDispatcher);
|
||||
mShaderBuildQueue = std::make_unique<ShaderBuildQueue>(*mRuntimeSnapshotProvider, *mRuntimeEventDispatcher);
|
||||
mRuntimeServices = std::make_unique<RuntimeServices>(*mRuntimeEventDispatcher);
|
||||
mRuntimeUpdateController = std::make_unique<RuntimeUpdateController>(
|
||||
*mRuntimeStore,
|
||||
*mRuntimeCoordinator,
|
||||
*mRuntimeEventDispatcher,
|
||||
*mRuntimeServices,
|
||||
*mRenderEngine,
|
||||
*mShaderBuildQueue,
|
||||
*mVideoBackend);
|
||||
}
|
||||
|
||||
OpenGLComposite::~OpenGLComposite()
|
||||
@@ -130,10 +56,8 @@ OpenGLComposite::~OpenGLComposite()
|
||||
mRuntimeServices->Stop();
|
||||
if (mShaderBuildQueue)
|
||||
mShaderBuildQueue->Stop();
|
||||
mVideoIO->ReleaseResources();
|
||||
mRenderer->DestroyResources();
|
||||
|
||||
DeleteCriticalSection(&pMutex);
|
||||
if (mVideoBackend)
|
||||
mVideoBackend->ReleaseResources();
|
||||
}
|
||||
|
||||
bool OpenGLComposite::InitDeckLink()
|
||||
@@ -146,23 +70,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))
|
||||
{
|
||||
@@ -171,7 +95,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."
|
||||
@@ -179,8 +103,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())
|
||||
@@ -195,38 +119,40 @@ bool OpenGLComposite::InitVideoIO()
|
||||
goto error;
|
||||
}
|
||||
|
||||
PublishVideoIOStatus(mVideoIO->OutputModelName().empty()
|
||||
? "DeckLink output device selected."
|
||||
: ("Selected output device: " + mVideoIO->OutputModelName()));
|
||||
mVideoBackend->PublishStatus(
|
||||
mRuntimeStore && mRuntimeStore->IsExternalKeyingConfigured(),
|
||||
mVideoBackend->OutputModelName().empty()
|
||||
? "DeckLink output device selected."
|
||||
: ("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)
|
||||
{
|
||||
mRuntimeHost->SetSignalStatus(false, mVideoIO->InputFrameWidth(), mVideoIO->InputFrameHeight(), mVideoIO->InputDisplayModeName());
|
||||
}
|
||||
if (!mVideoBackend->HasInputDevice())
|
||||
mVideoBackend->ReportNoInputDeviceSignalStatus();
|
||||
|
||||
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());
|
||||
mVideoBackend->PublishStatus(
|
||||
mRuntimeStore && mRuntimeStore->IsExternalKeyingConfigured(),
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -236,37 +162,23 @@ void OpenGLComposite::paintGL(bool force)
|
||||
{
|
||||
if (IsIconic(hGLWnd))
|
||||
return;
|
||||
|
||||
const unsigned previewFps = mRuntimeHost ? mRuntimeHost->GetPreviewFps() : 30u;
|
||||
if (previewFps == 0)
|
||||
return;
|
||||
|
||||
const auto now = std::chrono::steady_clock::now();
|
||||
const auto minimumInterval = std::chrono::microseconds(1000000 / (previewFps == 0 ? 1u : previewFps));
|
||||
if (mLastPreviewPresentTime != std::chrono::steady_clock::time_point() &&
|
||||
now - mLastPreviewPresentTime < minimumInterval)
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (!TryEnterCriticalSection(&pMutex))
|
||||
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());
|
||||
mLastPreviewPresentTime = std::chrono::steady_clock::now();
|
||||
ValidateRect(hGLWnd, NULL);
|
||||
LeaveCriticalSection(&pMutex);
|
||||
}
|
||||
|
||||
void OpenGLComposite::resizeGL(WORD width, WORD height)
|
||||
{
|
||||
// We don't set the project or model matrices here since the window data is copied directly from
|
||||
// an off-screen FBO in paintGL(). Just save the width and height for use in paintGL().
|
||||
mRenderer->ResizeView(width, height);
|
||||
mRenderEngine->ResizeView(width, height);
|
||||
}
|
||||
|
||||
void OpenGLComposite::resizeWindow(int width, int height)
|
||||
@@ -278,38 +190,19 @@ void OpenGLComposite::resizeWindow(int width, int height)
|
||||
}
|
||||
}
|
||||
|
||||
void OpenGLComposite::PublishVideoIOStatus(const std::string& statusMessage)
|
||||
{
|
||||
if (!mRuntimeHost)
|
||||
return;
|
||||
|
||||
if (!statusMessage.empty())
|
||||
mVideoIO->SetStatusMessage(statusMessage);
|
||||
|
||||
mRuntimeHost->SetVideoIOStatus(
|
||||
"decklink",
|
||||
mVideoIO->OutputModelName(),
|
||||
mVideoIO->SupportsInternalKeying(),
|
||||
mVideoIO->SupportsExternalKeying(),
|
||||
mVideoIO->KeyerInterfaceAvailable(),
|
||||
mRuntimeHost->ExternalKeyingEnabled(),
|
||||
mVideoIO->ExternalKeyingActive(),
|
||||
mVideoIO->StatusMessage());
|
||||
}
|
||||
|
||||
bool OpenGLComposite::InitOpenGLState()
|
||||
{
|
||||
if (! ResolveGLExtensions())
|
||||
return false;
|
||||
|
||||
std::string runtimeError;
|
||||
if (mRuntimeHost->GetRepoRoot().empty() && !mRuntimeHost->Initialize(runtimeError))
|
||||
if (mRuntimeStore->GetRuntimeRepositoryRoot().empty() && !mRuntimeStore->InitializeStore(runtimeError))
|
||||
{
|
||||
MessageBoxA(NULL, runtimeError.c_str(), "Runtime host failed to initialize", MB_OK);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!mRuntimeServices->Start(*this, *mRuntimeHost, runtimeError))
|
||||
if (!mRuntimeServices->Start(*this, *mRuntimeStore, runtimeError))
|
||||
{
|
||||
MessageBoxA(NULL, runtimeError.c_str(), "Runtime control services failed to start", MB_OK);
|
||||
return false;
|
||||
@@ -317,50 +210,56 @@ 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;
|
||||
}
|
||||
|
||||
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 (!mShaderPrograms->CompileLayerPrograms(mVideoIO->InputFrameWidth(), mVideoIO->InputFrameHeight(), sizeof(compilerErrorMessage), compilerErrorMessage))
|
||||
if (!mRenderEngine->CompileLayerPrograms(mVideoBackend->InputFrameWidth(), mVideoBackend->InputFrameHeight(), sizeof(compilerErrorMessage), compilerErrorMessage))
|
||||
{
|
||||
MessageBoxA(NULL, compilerErrorMessage, "OpenGL shader failed to load or compile", MB_OK);
|
||||
return false;
|
||||
}
|
||||
mCachedLayerRenderStates = mShaderPrograms->CommittedLayerStates();
|
||||
mUseCommittedLayerStates = false;
|
||||
mRuntimeStore->SetCompileStatus(true, "Shader layers compiled successfully.");
|
||||
|
||||
mShaderPrograms->ResetTemporalHistoryState();
|
||||
mShaderPrograms->ResetShaderFeedbackState();
|
||||
mRenderEngine->ResetTemporalHistoryState();
|
||||
mRenderEngine->ResetShaderFeedbackState();
|
||||
|
||||
broadcastRuntimeState();
|
||||
mRuntimeServices->BeginPolling(*mRuntimeHost);
|
||||
mRuntimeUpdateController->BroadcastRuntimeState();
|
||||
mRuntimeServices->BeginPolling(*mRuntimeCoordinator);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool OpenGLComposite::Start()
|
||||
{
|
||||
return mVideoIO->Start();
|
||||
if (!mRenderEngine->StartRenderThread())
|
||||
return false;
|
||||
|
||||
if (mVideoBackend->Start())
|
||||
return true;
|
||||
|
||||
mRenderEngine->StopRenderThread();
|
||||
return false;
|
||||
}
|
||||
|
||||
bool OpenGLComposite::Stop()
|
||||
@@ -368,339 +267,122 @@ 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.");
|
||||
mVideoBackend->PublishStatus(
|
||||
mRuntimeStore && mRuntimeStore->IsExternalKeyingConfigured(),
|
||||
"External keying has been disabled.");
|
||||
|
||||
if (mRenderEngine)
|
||||
mRenderEngine->StopRenderThread();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool OpenGLComposite::ReloadShader(bool preserveFeedbackState)
|
||||
{
|
||||
mPreserveFeedbackOnNextShaderBuild = preserveFeedbackState;
|
||||
if (mRuntimeHost)
|
||||
{
|
||||
mRuntimeHost->SetCompileStatus(true, "Shader rebuild queued.");
|
||||
mRuntimeHost->ClearReloadRequest();
|
||||
}
|
||||
RequestShaderBuild();
|
||||
broadcastRuntimeState();
|
||||
return true;
|
||||
return mRuntimeCoordinator &&
|
||||
mRuntimeUpdateController &&
|
||||
mRuntimeUpdateController->ApplyRuntimeCoordinatorResult(mRuntimeCoordinator->RequestShaderReload(preserveFeedbackState));
|
||||
}
|
||||
|
||||
bool OpenGLComposite::RequestScreenshot(std::string& error)
|
||||
{
|
||||
(void)error;
|
||||
mScreenshotRequested.store(true);
|
||||
if (!mRenderEngine || !mVideoBackend)
|
||||
{
|
||||
error = "The render engine is not ready.";
|
||||
return false;
|
||||
}
|
||||
|
||||
const unsigned width = mVideoBackend->OutputFrameWidth();
|
||||
const unsigned height = mVideoBackend->OutputFrameHeight();
|
||||
if (width == 0 || height == 0)
|
||||
{
|
||||
error = "The output frame size is not available.";
|
||||
return false;
|
||||
}
|
||||
|
||||
std::filesystem::path outputPath;
|
||||
try
|
||||
{
|
||||
outputPath = BuildScreenshotPath();
|
||||
std::filesystem::create_directories(outputPath.parent_path());
|
||||
}
|
||||
catch (const std::exception& exception)
|
||||
{
|
||||
error = exception.what();
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!mRenderEngine->RequestScreenshotCapture(
|
||||
width,
|
||||
height,
|
||||
[outputPath](unsigned captureWidth, unsigned captureHeight, std::vector<unsigned char> topDownPixels) {
|
||||
try
|
||||
{
|
||||
WritePngFileAsync(outputPath, captureWidth, captureHeight, std::move(topDownPixels));
|
||||
}
|
||||
catch (const std::exception& exception)
|
||||
{
|
||||
OutputDebugStringA((std::string("Screenshot request failed: ") + exception.what() + "\n").c_str());
|
||||
}
|
||||
}))
|
||||
{
|
||||
error = "Screenshot capture request failed.";
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void OpenGLComposite::renderEffect()
|
||||
{
|
||||
ProcessRuntimePollResults();
|
||||
std::vector<RuntimeServices::AppliedOscUpdate> appliedOscUpdates;
|
||||
std::vector<RuntimeServices::CompletedOscCommit> completedOscCommits;
|
||||
if (mRuntimeHost && mRuntimeServices)
|
||||
{
|
||||
std::string oscError;
|
||||
if (!mRuntimeServices->ApplyPendingOscUpdates(appliedOscUpdates, oscError) && !oscError.empty())
|
||||
OutputDebugStringA(("OSC apply failed: " + oscError + "\n").c_str());
|
||||
mRuntimeServices->ConsumeCompletedOscCommits(completedOscCommits);
|
||||
}
|
||||
if (mRuntimeUpdateController)
|
||||
mRuntimeUpdateController->ProcessRuntimeWork();
|
||||
|
||||
for (const RuntimeServices::CompletedOscCommit& completedCommit : completedOscCommits)
|
||||
{
|
||||
auto overlayIt = mOscOverlayStates.find(completedCommit.routeKey);
|
||||
if (overlayIt == mOscOverlayStates.end())
|
||||
continue;
|
||||
|
||||
OscOverlayState& overlay = overlayIt->second;
|
||||
if (overlay.commitQueued &&
|
||||
overlay.pendingCommitGeneration == completedCommit.generation &&
|
||||
overlay.generation == completedCommit.generation)
|
||||
{
|
||||
mOscOverlayStates.erase(overlayIt);
|
||||
}
|
||||
}
|
||||
|
||||
std::set<std::string> pendingOscRouteKeys;
|
||||
const auto oscNow = std::chrono::steady_clock::now();
|
||||
for (const RuntimeServices::AppliedOscUpdate& update : appliedOscUpdates)
|
||||
{
|
||||
const std::string routeKey = update.routeKey;
|
||||
auto overlayIt = mOscOverlayStates.find(routeKey);
|
||||
if (overlayIt == mOscOverlayStates.end())
|
||||
{
|
||||
OscOverlayState overlay;
|
||||
overlay.layerKey = update.layerKey;
|
||||
overlay.parameterKey = update.parameterKey;
|
||||
overlay.targetValue = update.targetValue;
|
||||
overlay.lastUpdatedTime = oscNow;
|
||||
overlay.lastAppliedTime = oscNow;
|
||||
overlay.generation = 1;
|
||||
mOscOverlayStates[routeKey] = std::move(overlay);
|
||||
}
|
||||
else
|
||||
{
|
||||
overlayIt->second.targetValue = update.targetValue;
|
||||
overlayIt->second.lastUpdatedTime = oscNow;
|
||||
overlayIt->second.generation += 1;
|
||||
overlayIt->second.commitQueued = false;
|
||||
}
|
||||
pendingOscRouteKeys.insert(routeKey);
|
||||
}
|
||||
|
||||
const auto applyOscOverlays = [&](std::vector<RuntimeRenderState>& states, bool allowCommit)
|
||||
{
|
||||
if (states.empty() || mOscOverlayStates.empty() || !mRuntimeHost)
|
||||
return;
|
||||
|
||||
const double smoothing = ClampOscAlpha(mRuntimeHost->GetOscSmoothing());
|
||||
std::vector<std::string> overlayKeysToRemove;
|
||||
for (auto& item : mOscOverlayStates)
|
||||
{
|
||||
OscOverlayState& overlay = item.second;
|
||||
auto stateIt = std::find_if(states.begin(), states.end(),
|
||||
[&overlay](const RuntimeRenderState& state)
|
||||
{
|
||||
return MatchesOscControlKey(state.layerId, overlay.layerKey) ||
|
||||
MatchesOscControlKey(state.shaderId, overlay.layerKey) ||
|
||||
MatchesOscControlKey(state.shaderName, overlay.layerKey);
|
||||
});
|
||||
if (stateIt == states.end())
|
||||
continue;
|
||||
|
||||
auto definitionIt = std::find_if(stateIt->parameterDefinitions.begin(), stateIt->parameterDefinitions.end(),
|
||||
[&overlay](const ShaderParameterDefinition& definition)
|
||||
{
|
||||
return MatchesOscControlKey(definition.id, overlay.parameterKey) ||
|
||||
MatchesOscControlKey(definition.label, overlay.parameterKey);
|
||||
});
|
||||
if (definitionIt == stateIt->parameterDefinitions.end())
|
||||
continue;
|
||||
|
||||
if (definitionIt->type == ShaderParameterType::Trigger)
|
||||
{
|
||||
if (pendingOscRouteKeys.find(item.first) == pendingOscRouteKeys.end())
|
||||
continue;
|
||||
|
||||
ShaderParameterValue& value = stateIt->parameterValues[definitionIt->id];
|
||||
const double previousCount = value.numberValues.empty() ? 0.0 : value.numberValues[0];
|
||||
const double triggerTime = stateIt->timeSeconds;
|
||||
value.numberValues = { previousCount + 1.0, triggerTime };
|
||||
overlayKeysToRemove.push_back(item.first);
|
||||
continue;
|
||||
}
|
||||
|
||||
ShaderParameterValue targetValue;
|
||||
std::string normalizeError;
|
||||
if (!NormalizeAndValidateParameterValue(*definitionIt, overlay.targetValue, targetValue, normalizeError))
|
||||
continue;
|
||||
|
||||
const bool smoothable =
|
||||
smoothing > 0.0 &&
|
||||
(definitionIt->type == ShaderParameterType::Float ||
|
||||
definitionIt->type == ShaderParameterType::Vec2 ||
|
||||
definitionIt->type == ShaderParameterType::Color);
|
||||
if (!smoothable)
|
||||
{
|
||||
overlay.currentValue = targetValue;
|
||||
overlay.hasCurrentValue = true;
|
||||
stateIt->parameterValues[definitionIt->id] = overlay.currentValue;
|
||||
if (allowCommit &&
|
||||
!overlay.commitQueued &&
|
||||
oscNow - overlay.lastUpdatedTime >= kOscOverlayCommitDelay &&
|
||||
mRuntimeServices)
|
||||
{
|
||||
std::string commitError;
|
||||
if (mRuntimeServices->QueueOscCommit(item.first, overlay.layerKey, overlay.parameterKey, overlay.targetValue, overlay.generation, commitError))
|
||||
{
|
||||
overlay.pendingCommitGeneration = overlay.generation;
|
||||
overlay.commitQueued = true;
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!overlay.hasCurrentValue)
|
||||
{
|
||||
overlay.currentValue = DefaultValueForDefinition(*definitionIt);
|
||||
auto currentIt = stateIt->parameterValues.find(definitionIt->id);
|
||||
if (currentIt != stateIt->parameterValues.end())
|
||||
overlay.currentValue = currentIt->second;
|
||||
overlay.hasCurrentValue = true;
|
||||
}
|
||||
|
||||
if (overlay.currentValue.numberValues.size() != targetValue.numberValues.size())
|
||||
overlay.currentValue.numberValues = targetValue.numberValues;
|
||||
|
||||
double smoothingAlpha = smoothing;
|
||||
if (overlay.lastAppliedTime != std::chrono::steady_clock::time_point())
|
||||
{
|
||||
const double deltaSeconds =
|
||||
std::chrono::duration_cast<std::chrono::duration<double>>(oscNow - overlay.lastAppliedTime).count();
|
||||
smoothingAlpha = ComputeTimeBasedOscAlpha(smoothing, deltaSeconds);
|
||||
}
|
||||
overlay.lastAppliedTime = oscNow;
|
||||
|
||||
ShaderParameterValue nextValue = targetValue;
|
||||
bool converged = true;
|
||||
for (std::size_t index = 0; index < targetValue.numberValues.size(); ++index)
|
||||
{
|
||||
const double currentNumber = overlay.currentValue.numberValues[index];
|
||||
const double targetNumber = targetValue.numberValues[index];
|
||||
const double delta = targetNumber - currentNumber;
|
||||
double nextNumber = currentNumber + delta * smoothingAlpha;
|
||||
if (std::fabs(delta) <= 0.0005)
|
||||
nextNumber = targetNumber;
|
||||
else
|
||||
converged = false;
|
||||
nextValue.numberValues[index] = nextNumber;
|
||||
}
|
||||
|
||||
if (converged)
|
||||
nextValue.numberValues = targetValue.numberValues;
|
||||
|
||||
overlay.currentValue = nextValue;
|
||||
overlay.hasCurrentValue = true;
|
||||
stateIt->parameterValues[definitionIt->id] = overlay.currentValue;
|
||||
if (allowCommit &&
|
||||
converged &&
|
||||
!overlay.commitQueued &&
|
||||
oscNow - overlay.lastUpdatedTime >= kOscOverlayCommitDelay &&
|
||||
mRuntimeServices)
|
||||
{
|
||||
std::string commitError;
|
||||
JsonValue committedValue = BuildOscCommitValue(*definitionIt, overlay.currentValue);
|
||||
if (mRuntimeServices->QueueOscCommit(item.first, overlay.layerKey, overlay.parameterKey, committedValue, overlay.generation, commitError))
|
||||
{
|
||||
overlay.pendingCommitGeneration = overlay.generation;
|
||||
overlay.commitQueued = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const std::string& overlayKey : overlayKeysToRemove)
|
||||
mOscOverlayStates.erase(overlayKey);
|
||||
};
|
||||
|
||||
const bool hasInputSource = mVideoIO->HasInputSource();
|
||||
std::vector<RuntimeRenderState> layerStates;
|
||||
if (mUseCommittedLayerStates)
|
||||
{
|
||||
layerStates = mShaderPrograms->CommittedLayerStates();
|
||||
applyOscOverlays(layerStates, false);
|
||||
if (mRuntimeHost)
|
||||
mRuntimeHost->RefreshDynamicRenderStateFields(layerStates);
|
||||
}
|
||||
else if (mRuntimeHost)
|
||||
{
|
||||
const unsigned renderWidth = mVideoIO->InputFrameWidth();
|
||||
const unsigned renderHeight = mVideoIO->InputFrameHeight();
|
||||
const uint64_t renderStateVersion = mRuntimeHost->GetRenderStateVersion();
|
||||
const uint64_t parameterStateVersion = mRuntimeHost->GetParameterStateVersion();
|
||||
const bool renderStateCacheValid =
|
||||
!mCachedLayerRenderStates.empty() &&
|
||||
mCachedRenderStateVersion == renderStateVersion &&
|
||||
mCachedRenderStateWidth == renderWidth &&
|
||||
mCachedRenderStateHeight == renderHeight;
|
||||
|
||||
if (renderStateCacheValid)
|
||||
{
|
||||
applyOscOverlays(mCachedLayerRenderStates, true);
|
||||
if (mCachedParameterStateVersion != parameterStateVersion &&
|
||||
mRuntimeHost->TryRefreshCachedLayerStates(mCachedLayerRenderStates))
|
||||
{
|
||||
mCachedParameterStateVersion = parameterStateVersion;
|
||||
applyOscOverlays(mCachedLayerRenderStates, true);
|
||||
}
|
||||
layerStates = mCachedLayerRenderStates;
|
||||
mRuntimeHost->RefreshDynamicRenderStateFields(layerStates);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (mRuntimeHost->TryGetLayerRenderStates(renderWidth, renderHeight, layerStates))
|
||||
{
|
||||
mCachedLayerRenderStates = layerStates;
|
||||
mCachedRenderStateVersion = renderStateVersion;
|
||||
mCachedParameterStateVersion = parameterStateVersion;
|
||||
mCachedRenderStateWidth = renderWidth;
|
||||
mCachedRenderStateHeight = renderHeight;
|
||||
applyOscOverlays(mCachedLayerRenderStates, true);
|
||||
layerStates = mCachedLayerRenderStates;
|
||||
}
|
||||
else
|
||||
{
|
||||
applyOscOverlays(mCachedLayerRenderStates, true);
|
||||
layerStates = mCachedLayerRenderStates;
|
||||
mRuntimeHost->RefreshDynamicRenderStateFields(layerStates);
|
||||
}
|
||||
}
|
||||
}
|
||||
const unsigned historyCap = mRuntimeHost ? mRuntimeHost->GetMaxTemporalHistoryFrames() : 0;
|
||||
mRenderPass->Render(
|
||||
hasInputSource,
|
||||
layerStates,
|
||||
mVideoIO->InputFrameWidth(),
|
||||
mVideoIO->InputFrameHeight(),
|
||||
mVideoIO->CaptureTextureWidth(),
|
||||
mVideoIO->InputPixelFormat(),
|
||||
historyCap,
|
||||
[this](const RuntimeRenderState& state, LayerProgram::TextBinding& textBinding, std::string& error) {
|
||||
return mShaderPrograms->UpdateTextBindingTexture(state, textBinding, error);
|
||||
},
|
||||
[this](const RuntimeRenderState& state, unsigned availableSourceHistoryLength, unsigned availableTemporalHistoryLength, bool feedbackAvailable) {
|
||||
return mShaderPrograms->UpdateGlobalParamsBuffer(state, availableSourceHistoryLength, availableTemporalHistoryLength, feedbackAvailable);
|
||||
});
|
||||
const RenderFrameInput frameInput = BuildRenderFrameInput();
|
||||
RenderFrame(frameInput);
|
||||
}
|
||||
|
||||
void OpenGLComposite::ProcessScreenshotRequest()
|
||||
RenderFrameInput OpenGLComposite::BuildRenderFrameInput() const
|
||||
{
|
||||
if (!mScreenshotRequested.exchange(false))
|
||||
return;
|
||||
RenderFrameInput frameInput;
|
||||
frameInput.useCommittedLayerStates = mRuntimeCoordinator && mRuntimeCoordinator->UseCommittedLayerStates();
|
||||
frameInput.hasInputSource = mVideoBackend->HasInputSource();
|
||||
frameInput.renderWidth = mVideoBackend->InputFrameWidth();
|
||||
frameInput.renderHeight = mVideoBackend->InputFrameHeight();
|
||||
frameInput.inputFrameWidth = mVideoBackend->InputFrameWidth();
|
||||
frameInput.inputFrameHeight = mVideoBackend->InputFrameHeight();
|
||||
frameInput.captureTextureWidth = mVideoBackend->CaptureTextureWidth();
|
||||
frameInput.inputPixelFormat = mVideoBackend->InputPixelFormat();
|
||||
frameInput.historyCap = mRuntimeStore ? mRuntimeStore->GetConfiguredMaxTemporalHistoryFrames() : 0;
|
||||
frameInput.oscSmoothing = mRuntimeStore ? mRuntimeStore->GetConfiguredOscSmoothing() : 0.0;
|
||||
return frameInput;
|
||||
}
|
||||
|
||||
const unsigned width = mVideoIO ? mVideoIO->OutputFrameWidth() : 0;
|
||||
const unsigned height = mVideoIO ? mVideoIO->OutputFrameHeight() : 0;
|
||||
if (width == 0 || height == 0)
|
||||
return;
|
||||
|
||||
std::vector<unsigned char> bottomUpPixels(static_cast<std::size_t>(width) * height * 4);
|
||||
std::vector<unsigned char> topDownPixels(bottomUpPixels.size());
|
||||
|
||||
glBindFramebuffer(GL_READ_FRAMEBUFFER, mRenderer->OutputFramebuffer());
|
||||
glReadBuffer(GL_COLOR_ATTACHMENT0);
|
||||
glPixelStorei(GL_PACK_ALIGNMENT, 1);
|
||||
glPixelStorei(GL_PACK_ROW_LENGTH, 0);
|
||||
glReadPixels(0, 0, width, height, GL_RGBA, GL_UNSIGNED_BYTE, bottomUpPixels.data());
|
||||
glPixelStorei(GL_PACK_ALIGNMENT, 4);
|
||||
|
||||
const std::size_t rowBytes = static_cast<std::size_t>(width) * 4;
|
||||
for (unsigned y = 0; y < height; ++y)
|
||||
void OpenGLComposite::RenderFrame(const RenderFrameInput& frameInput)
|
||||
{
|
||||
RenderFrameState frameState;
|
||||
if (mRuntimeServices)
|
||||
{
|
||||
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));
|
||||
RuntimeServiceLiveBridge::PrepareLiveRenderFrameState(
|
||||
*mRuntimeServices,
|
||||
*mRenderEngine,
|
||||
frameInput,
|
||||
frameState);
|
||||
}
|
||||
|
||||
try
|
||||
else
|
||||
{
|
||||
const std::filesystem::path outputPath = BuildScreenshotPath();
|
||||
std::filesystem::create_directories(outputPath.parent_path());
|
||||
WritePngFileAsync(outputPath, width, height, std::move(topDownPixels));
|
||||
}
|
||||
catch (const std::exception& exception)
|
||||
{
|
||||
OutputDebugStringA((std::string("Screenshot request failed: ") + exception.what() + "\n").c_str());
|
||||
mRenderEngine->ResolveRenderFrameState(frameInput, nullptr, frameState);
|
||||
}
|
||||
mRenderEngine->RenderPreparedFrame(frameState);
|
||||
}
|
||||
|
||||
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();
|
||||
@@ -718,83 +400,7 @@ std::filesystem::path OpenGLComposite::BuildScreenshotPath() const
|
||||
return root / "screenshots" / filename.str();
|
||||
}
|
||||
|
||||
bool OpenGLComposite::ProcessRuntimePollResults()
|
||||
{
|
||||
if (!mRuntimeHost || !mRuntimeServices)
|
||||
return true;
|
||||
|
||||
const RuntimePollEvents events = mRuntimeServices->ConsumePollEvents();
|
||||
if (events.failed)
|
||||
{
|
||||
mRuntimeHost->SetCompileStatus(false, events.error);
|
||||
broadcastRuntimeState();
|
||||
return false;
|
||||
}
|
||||
|
||||
if (events.registryChanged)
|
||||
broadcastRuntimeState();
|
||||
|
||||
if (!events.reloadRequested)
|
||||
{
|
||||
PreparedShaderBuild readyBuild;
|
||||
if (!mShaderBuildQueue || !mShaderBuildQueue->TryConsumeReadyBuild(readyBuild))
|
||||
return true;
|
||||
|
||||
char compilerErrorMessage[1024] = {};
|
||||
if (!mShaderPrograms->CommitPreparedLayerPrograms(readyBuild, mVideoIO->InputFrameWidth(), mVideoIO->InputFrameHeight(), sizeof(compilerErrorMessage), compilerErrorMessage))
|
||||
{
|
||||
mRuntimeHost->SetCompileStatus(false, compilerErrorMessage);
|
||||
mUseCommittedLayerStates = true;
|
||||
mPreserveFeedbackOnNextShaderBuild = false;
|
||||
broadcastRuntimeState();
|
||||
return false;
|
||||
}
|
||||
|
||||
mUseCommittedLayerStates = false;
|
||||
mCachedLayerRenderStates = mShaderPrograms->CommittedLayerStates();
|
||||
mShaderPrograms->ResetTemporalHistoryState();
|
||||
if (!mPreserveFeedbackOnNextShaderBuild)
|
||||
mShaderPrograms->ResetShaderFeedbackState();
|
||||
mPreserveFeedbackOnNextShaderBuild = false;
|
||||
broadcastRuntimeState();
|
||||
return true;
|
||||
}
|
||||
|
||||
mRuntimeHost->SetCompileStatus(true, "Shader rebuild queued.");
|
||||
mPreserveFeedbackOnNextShaderBuild = false;
|
||||
RequestShaderBuild();
|
||||
broadcastRuntimeState();
|
||||
return true;
|
||||
}
|
||||
|
||||
void OpenGLComposite::RequestShaderBuild()
|
||||
{
|
||||
if (!mShaderBuildQueue || !mVideoIO)
|
||||
return;
|
||||
|
||||
mUseCommittedLayerStates = true;
|
||||
if (mRuntimeHost)
|
||||
mRuntimeHost->ClearReloadRequest();
|
||||
mShaderBuildQueue->RequestBuild(mVideoIO->InputFrameWidth(), mVideoIO->InputFrameHeight());
|
||||
}
|
||||
|
||||
void OpenGLComposite::broadcastRuntimeState()
|
||||
{
|
||||
if (mRuntimeServices)
|
||||
mRuntimeServices->BroadcastState();
|
||||
}
|
||||
|
||||
void OpenGLComposite::resetTemporalHistoryState()
|
||||
{
|
||||
mShaderPrograms->ResetTemporalHistoryState();
|
||||
mShaderPrograms->ResetShaderFeedbackState();
|
||||
}
|
||||
|
||||
bool OpenGLComposite::CheckOpenGLExtensions()
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
////////////////////////////////////////////
|
||||
|
||||
|
||||
|
||||
@@ -12,26 +12,23 @@
|
||||
#include <comutil.h>
|
||||
|
||||
#include "GLExtensions.h"
|
||||
#include "OpenGLRenderer.h"
|
||||
#include "RuntimeHost.h"
|
||||
#include "RenderFrameState.h"
|
||||
|
||||
#include <functional>
|
||||
#include <atomic>
|
||||
#include <filesystem>
|
||||
#include <map>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <deque>
|
||||
#include <chrono>
|
||||
|
||||
class VideoIODevice;
|
||||
class OpenGLVideoIOBridge;
|
||||
class OpenGLRenderPass;
|
||||
class OpenGLRenderPipeline;
|
||||
class OpenGLShaderPrograms;
|
||||
class RenderEngine;
|
||||
class RuntimeCoordinator;
|
||||
class RuntimeEventDispatcher;
|
||||
class RuntimeSnapshotProvider;
|
||||
class RuntimeServices;
|
||||
class RuntimeStore;
|
||||
class RuntimeUpdateController;
|
||||
class ShaderBuildQueue;
|
||||
class VideoBackend;
|
||||
|
||||
|
||||
class OpenGLComposite
|
||||
@@ -71,55 +68,26 @@ public:
|
||||
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<RuntimeEventDispatcher> mRuntimeEventDispatcher;
|
||||
std::unique_ptr<RenderEngine> mRenderEngine;
|
||||
std::unique_ptr<ShaderBuildQueue> mShaderBuildQueue;
|
||||
std::unique_ptr<RuntimeServices> mRuntimeServices;
|
||||
std::vector<RuntimeRenderState> mCachedLayerRenderStates;
|
||||
uint64_t mCachedRenderStateVersion = 0;
|
||||
uint64_t mCachedParameterStateVersion = 0;
|
||||
unsigned mCachedRenderStateWidth = 0;
|
||||
unsigned mCachedRenderStateHeight = 0;
|
||||
std::map<std::string, OscOverlayState> mOscOverlayStates;
|
||||
std::atomic<bool> mUseCommittedLayerStates;
|
||||
std::atomic<bool> mScreenshotRequested;
|
||||
std::chrono::steady_clock::time_point mLastPreviewPresentTime;
|
||||
bool mPreserveFeedbackOnNextShaderBuild = false;
|
||||
std::unique_ptr<RuntimeUpdateController> mRuntimeUpdateController;
|
||||
std::unique_ptr<VideoBackend> mVideoBackend;
|
||||
|
||||
bool InitOpenGLState();
|
||||
void renderEffect();
|
||||
bool ProcessRuntimePollResults();
|
||||
void RequestShaderBuild();
|
||||
void ProcessScreenshotRequest();
|
||||
RenderFrameInput BuildRenderFrameInput() const;
|
||||
void RenderFrame(const RenderFrameInput& frameInput);
|
||||
std::filesystem::path BuildScreenshotPath() const;
|
||||
void broadcastRuntimeState();
|
||||
void resetTemporalHistoryState();
|
||||
};
|
||||
|
||||
#endif // __OPENGL_COMPOSITE_H__
|
||||
|
||||
@@ -1,24 +1,28 @@
|
||||
#include "OpenGLComposite.h"
|
||||
#include "RuntimeCoordinator.h"
|
||||
#include "RuntimeJson.h"
|
||||
#include "RuntimeServices.h"
|
||||
#include "RuntimeStore.h"
|
||||
#include "RuntimeUpdateController.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 mRuntimeHost ? mRuntimeHost->GetOscBindAddress() : "127.0.0.1";
|
||||
return mRuntimeStore ? mRuntimeStore->GetConfiguredOscBindAddress() : "127.0.0.1";
|
||||
}
|
||||
|
||||
std::string OpenGLComposite::GetControlUrl() const
|
||||
@@ -38,62 +42,44 @@ std::string OpenGLComposite::GetOscAddress() const
|
||||
|
||||
bool OpenGLComposite::AddLayer(const std::string& shaderId, std::string& error)
|
||||
{
|
||||
if (!mRuntimeHost->AddLayer(shaderId, error))
|
||||
return false;
|
||||
|
||||
ReloadShader(true);
|
||||
broadcastRuntimeState();
|
||||
return true;
|
||||
return mRuntimeCoordinator &&
|
||||
mRuntimeUpdateController &&
|
||||
mRuntimeUpdateController->ApplyRuntimeCoordinatorResult(mRuntimeCoordinator->AddLayer(shaderId), &error);
|
||||
}
|
||||
|
||||
bool OpenGLComposite::RemoveLayer(const std::string& layerId, std::string& error)
|
||||
{
|
||||
if (!mRuntimeHost->RemoveLayer(layerId, error))
|
||||
return false;
|
||||
|
||||
ReloadShader(true);
|
||||
broadcastRuntimeState();
|
||||
return true;
|
||||
return mRuntimeCoordinator &&
|
||||
mRuntimeUpdateController &&
|
||||
mRuntimeUpdateController->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(true);
|
||||
broadcastRuntimeState();
|
||||
return true;
|
||||
return mRuntimeCoordinator &&
|
||||
mRuntimeUpdateController &&
|
||||
mRuntimeUpdateController->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(true);
|
||||
broadcastRuntimeState();
|
||||
return true;
|
||||
return mRuntimeCoordinator &&
|
||||
mRuntimeUpdateController &&
|
||||
mRuntimeUpdateController->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 &&
|
||||
mRuntimeUpdateController &&
|
||||
mRuntimeUpdateController->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 &&
|
||||
mRuntimeUpdateController &&
|
||||
mRuntimeUpdateController->ApplyRuntimeCoordinatorResult(mRuntimeCoordinator->SetLayerShader(layerId, shaderId), &error);
|
||||
}
|
||||
|
||||
bool OpenGLComposite::UpdateLayerParameterJson(const std::string& layerId, const std::string& parameterId, const std::string& valueJson, std::string& error)
|
||||
@@ -102,11 +88,9 @@ 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 &&
|
||||
mRuntimeUpdateController &&
|
||||
mRuntimeUpdateController->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)
|
||||
@@ -115,42 +99,28 @@ 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 &&
|
||||
mRuntimeUpdateController &&
|
||||
mRuntimeUpdateController->ApplyRuntimeCoordinatorResult(mRuntimeCoordinator->UpdateLayerParameterByControlKey(layerKey, parameterKey, parsedValue), &error);
|
||||
}
|
||||
|
||||
bool OpenGLComposite::ResetLayerParameters(const std::string& layerId, std::string& error)
|
||||
{
|
||||
if (!mRuntimeHost->ResetLayerParameters(layerId, error))
|
||||
return false;
|
||||
|
||||
mOscOverlayStates.clear();
|
||||
if (mRuntimeServices)
|
||||
mRuntimeServices->ClearOscState();
|
||||
resetTemporalHistoryState();
|
||||
|
||||
broadcastRuntimeState();
|
||||
return true;
|
||||
return mRuntimeCoordinator &&
|
||||
mRuntimeUpdateController &&
|
||||
mRuntimeUpdateController->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 &&
|
||||
mRuntimeUpdateController &&
|
||||
mRuntimeUpdateController->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 &&
|
||||
mRuntimeUpdateController &&
|
||||
mRuntimeUpdateController->ApplyRuntimeCoordinatorResult(mRuntimeCoordinator->LoadStackPreset(presetName), &error);
|
||||
}
|
||||
|
||||
160
apps/LoopThroughWithOpenGLCompositing/gl/RenderCommandQueue.cpp
Normal file
160
apps/LoopThroughWithOpenGLCompositing/gl/RenderCommandQueue.cpp
Normal file
@@ -0,0 +1,160 @@
|
||||
#include "RenderCommandQueue.h"
|
||||
|
||||
void RenderCommandQueue::RequestPreviewPresent(const RenderPreviewPresentRequest& request)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mMutex);
|
||||
if (mHasPreviewPresentRequest)
|
||||
++mCoalescedCount;
|
||||
else
|
||||
++mEnqueuedCount;
|
||||
|
||||
mPreviewPresentRequest = request;
|
||||
mHasPreviewPresentRequest = true;
|
||||
}
|
||||
|
||||
bool RenderCommandQueue::TryTakePreviewPresent(RenderPreviewPresentRequest& request)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mMutex);
|
||||
if (!mHasPreviewPresentRequest)
|
||||
return false;
|
||||
|
||||
request = mPreviewPresentRequest;
|
||||
mPreviewPresentRequest = {};
|
||||
mHasPreviewPresentRequest = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
void RenderCommandQueue::RequestScreenshotCapture(const RenderScreenshotCaptureRequest& request)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mMutex);
|
||||
if (mHasScreenshotCaptureRequest)
|
||||
++mCoalescedCount;
|
||||
else
|
||||
++mEnqueuedCount;
|
||||
|
||||
mScreenshotCaptureRequest = request;
|
||||
mHasScreenshotCaptureRequest = true;
|
||||
}
|
||||
|
||||
bool RenderCommandQueue::TryTakeScreenshotCapture(RenderScreenshotCaptureRequest& request)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mMutex);
|
||||
if (!mHasScreenshotCaptureRequest)
|
||||
return false;
|
||||
|
||||
request = mScreenshotCaptureRequest;
|
||||
mScreenshotCaptureRequest = {};
|
||||
mHasScreenshotCaptureRequest = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
void RenderCommandQueue::RequestInputUpload(const RenderInputUploadRequest& request)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mMutex);
|
||||
if (mHasInputUploadRequest)
|
||||
++mCoalescedCount;
|
||||
else
|
||||
++mEnqueuedCount;
|
||||
|
||||
mInputUploadRequest = request;
|
||||
mHasInputUploadRequest = true;
|
||||
}
|
||||
|
||||
bool RenderCommandQueue::TryTakeInputUpload(RenderInputUploadRequest& request)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mMutex);
|
||||
if (!mHasInputUploadRequest)
|
||||
return false;
|
||||
|
||||
request = mInputUploadRequest;
|
||||
mInputUploadRequest = {};
|
||||
mHasInputUploadRequest = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
void RenderCommandQueue::RequestOutputFrame(const RenderOutputFrameRequest& request)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mMutex);
|
||||
mOutputFrameRequests.push_back(request);
|
||||
++mEnqueuedCount;
|
||||
}
|
||||
|
||||
bool RenderCommandQueue::TryTakeOutputFrame(RenderOutputFrameRequest& request)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mMutex);
|
||||
if (mOutputFrameRequests.empty())
|
||||
return false;
|
||||
|
||||
request = mOutputFrameRequests.front();
|
||||
mOutputFrameRequests.pop_front();
|
||||
return true;
|
||||
}
|
||||
|
||||
void RenderCommandQueue::RequestRenderReset(RenderCommandResetScope scope)
|
||||
{
|
||||
if (scope == RenderCommandResetScope::None)
|
||||
return;
|
||||
|
||||
std::lock_guard<std::mutex> lock(mMutex);
|
||||
if (mRenderResetScope != RenderCommandResetScope::None)
|
||||
++mCoalescedCount;
|
||||
else
|
||||
++mEnqueuedCount;
|
||||
|
||||
mRenderResetScope = MergeResetScopes(mRenderResetScope, scope);
|
||||
}
|
||||
|
||||
bool RenderCommandQueue::TryTakeRenderReset(RenderCommandResetScope& scope)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mMutex);
|
||||
if (mRenderResetScope == RenderCommandResetScope::None)
|
||||
return false;
|
||||
|
||||
scope = mRenderResetScope;
|
||||
mRenderResetScope = RenderCommandResetScope::None;
|
||||
return true;
|
||||
}
|
||||
|
||||
RenderCommandQueueMetrics RenderCommandQueue::GetMetrics() const
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mMutex);
|
||||
RenderCommandQueueMetrics metrics;
|
||||
metrics.depth =
|
||||
(mHasPreviewPresentRequest ? 1u : 0u) +
|
||||
(mHasScreenshotCaptureRequest ? 1u : 0u) +
|
||||
(mHasInputUploadRequest ? 1u : 0u) +
|
||||
mOutputFrameRequests.size() +
|
||||
(mRenderResetScope != RenderCommandResetScope::None ? 1u : 0u);
|
||||
metrics.enqueuedCount = mEnqueuedCount;
|
||||
metrics.coalescedCount = mCoalescedCount;
|
||||
return metrics;
|
||||
}
|
||||
|
||||
RenderCommandResetScope RenderCommandQueue::MergeResetScopes(RenderCommandResetScope current, RenderCommandResetScope requested)
|
||||
{
|
||||
if (current == RenderCommandResetScope::TemporalHistoryAndFeedback ||
|
||||
requested == RenderCommandResetScope::TemporalHistoryAndFeedback)
|
||||
{
|
||||
return RenderCommandResetScope::TemporalHistoryAndFeedback;
|
||||
}
|
||||
|
||||
if ((current == RenderCommandResetScope::TemporalHistoryOnly && requested == RenderCommandResetScope::ShaderFeedbackOnly) ||
|
||||
(current == RenderCommandResetScope::ShaderFeedbackOnly && requested == RenderCommandResetScope::TemporalHistoryOnly))
|
||||
{
|
||||
return RenderCommandResetScope::TemporalHistoryAndFeedback;
|
||||
}
|
||||
|
||||
if (current == RenderCommandResetScope::TemporalHistoryOnly ||
|
||||
requested == RenderCommandResetScope::TemporalHistoryOnly)
|
||||
{
|
||||
return RenderCommandResetScope::TemporalHistoryOnly;
|
||||
}
|
||||
|
||||
if (current == RenderCommandResetScope::ShaderFeedbackOnly ||
|
||||
requested == RenderCommandResetScope::ShaderFeedbackOnly)
|
||||
{
|
||||
return RenderCommandResetScope::ShaderFeedbackOnly;
|
||||
}
|
||||
|
||||
return RenderCommandResetScope::None;
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
#pragma once
|
||||
|
||||
#include "VideoIOTypes.h"
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <deque>
|
||||
#include <mutex>
|
||||
#include <vector>
|
||||
|
||||
enum class RenderCommandResetScope
|
||||
{
|
||||
None,
|
||||
ShaderFeedbackOnly,
|
||||
TemporalHistoryOnly,
|
||||
TemporalHistoryAndFeedback
|
||||
};
|
||||
|
||||
struct RenderPreviewPresentRequest
|
||||
{
|
||||
unsigned outputFrameWidth = 0;
|
||||
unsigned outputFrameHeight = 0;
|
||||
};
|
||||
|
||||
struct RenderScreenshotCaptureRequest
|
||||
{
|
||||
unsigned width = 0;
|
||||
unsigned height = 0;
|
||||
};
|
||||
|
||||
struct RenderInputUploadRequest
|
||||
{
|
||||
VideoIOFrame inputFrame;
|
||||
VideoIOState videoState;
|
||||
std::vector<unsigned char> ownedBytes;
|
||||
};
|
||||
|
||||
struct RenderOutputFrameRequest
|
||||
{
|
||||
VideoIOState videoState;
|
||||
VideoIOCompletion completion;
|
||||
};
|
||||
|
||||
struct RenderCommandQueueMetrics
|
||||
{
|
||||
std::size_t depth = 0;
|
||||
uint64_t enqueuedCount = 0;
|
||||
uint64_t coalescedCount = 0;
|
||||
};
|
||||
|
||||
class RenderCommandQueue
|
||||
{
|
||||
public:
|
||||
void RequestPreviewPresent(const RenderPreviewPresentRequest& request);
|
||||
bool TryTakePreviewPresent(RenderPreviewPresentRequest& request);
|
||||
|
||||
void RequestScreenshotCapture(const RenderScreenshotCaptureRequest& request);
|
||||
bool TryTakeScreenshotCapture(RenderScreenshotCaptureRequest& request);
|
||||
|
||||
void RequestInputUpload(const RenderInputUploadRequest& request);
|
||||
bool TryTakeInputUpload(RenderInputUploadRequest& request);
|
||||
|
||||
void RequestOutputFrame(const RenderOutputFrameRequest& request);
|
||||
bool TryTakeOutputFrame(RenderOutputFrameRequest& request);
|
||||
|
||||
void RequestRenderReset(RenderCommandResetScope scope);
|
||||
bool TryTakeRenderReset(RenderCommandResetScope& scope);
|
||||
|
||||
RenderCommandQueueMetrics GetMetrics() const;
|
||||
|
||||
private:
|
||||
static RenderCommandResetScope MergeResetScopes(RenderCommandResetScope current, RenderCommandResetScope requested);
|
||||
|
||||
mutable std::mutex mMutex;
|
||||
bool mHasPreviewPresentRequest = false;
|
||||
RenderPreviewPresentRequest mPreviewPresentRequest;
|
||||
bool mHasScreenshotCaptureRequest = false;
|
||||
RenderScreenshotCaptureRequest mScreenshotCaptureRequest;
|
||||
bool mHasInputUploadRequest = false;
|
||||
RenderInputUploadRequest mInputUploadRequest;
|
||||
std::deque<RenderOutputFrameRequest> mOutputFrameRequests;
|
||||
RenderCommandResetScope mRenderResetScope = RenderCommandResetScope::None;
|
||||
uint64_t mEnqueuedCount = 0;
|
||||
uint64_t mCoalescedCount = 0;
|
||||
};
|
||||
662
apps/LoopThroughWithOpenGLCompositing/gl/RenderEngine.cpp
Normal file
662
apps/LoopThroughWithOpenGLCompositing/gl/RenderEngine.cpp
Normal file
@@ -0,0 +1,662 @@
|
||||
#include "RenderEngine.h"
|
||||
|
||||
#include <gl/gl.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstring>
|
||||
|
||||
RenderEngine::RenderEngine(
|
||||
RuntimeSnapshotProvider& runtimeSnapshotProvider,
|
||||
HealthTelemetry& healthTelemetry,
|
||||
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),
|
||||
mHdc(hdc),
|
||||
mHglrc(hglrc),
|
||||
mFrameStateResolver(runtimeSnapshotProvider)
|
||||
{
|
||||
}
|
||||
|
||||
RenderEngine::~RenderEngine()
|
||||
{
|
||||
StopRenderThread();
|
||||
if (!mResourcesDestroyed)
|
||||
{
|
||||
mRenderer.DestroyResources();
|
||||
mResourcesDestroyed = true;
|
||||
}
|
||||
}
|
||||
|
||||
bool RenderEngine::StartRenderThread()
|
||||
{
|
||||
if (mRenderThreadRunning)
|
||||
return true;
|
||||
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mRenderThreadMutex);
|
||||
mRenderThreadStopping = false;
|
||||
}
|
||||
|
||||
std::promise<bool> ready;
|
||||
std::future<bool> readyResult = ready.get_future();
|
||||
mRenderThread = std::thread(&RenderEngine::RenderThreadMain, this, std::move(ready));
|
||||
if (!readyResult.get())
|
||||
{
|
||||
if (mRenderThread.joinable())
|
||||
mRenderThread.join();
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void RenderEngine::StopRenderThread()
|
||||
{
|
||||
if (mRenderThreadRunning)
|
||||
{
|
||||
InvokeOnRenderThread([this]() {
|
||||
if (!mResourcesDestroyed)
|
||||
{
|
||||
mRenderer.DestroyResources();
|
||||
mResourcesDestroyed = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mRenderThreadMutex);
|
||||
mRenderThreadStopping = true;
|
||||
}
|
||||
mRenderThreadCondition.notify_one();
|
||||
|
||||
if (mRenderThread.joinable())
|
||||
mRenderThread.join();
|
||||
}
|
||||
|
||||
void RenderEngine::RenderThreadMain(std::promise<bool> ready)
|
||||
{
|
||||
mRenderThreadId = GetCurrentThreadId();
|
||||
if (!wglMakeCurrent(mHdc, mHglrc))
|
||||
{
|
||||
mRenderThreadId = 0;
|
||||
ready.set_value(false);
|
||||
return;
|
||||
}
|
||||
|
||||
mRenderThreadRunning = true;
|
||||
ready.set_value(true);
|
||||
|
||||
for (;;)
|
||||
{
|
||||
std::function<void()> task;
|
||||
{
|
||||
std::unique_lock<std::mutex> lock(mRenderThreadMutex);
|
||||
mRenderThreadCondition.wait(lock, [this]() {
|
||||
return mRenderThreadStopping || !mRenderThreadTasks.empty();
|
||||
});
|
||||
|
||||
if (mRenderThreadStopping && mRenderThreadTasks.empty())
|
||||
break;
|
||||
|
||||
task = std::move(mRenderThreadTasks.front());
|
||||
mRenderThreadTasks.pop();
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
task();
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
OutputDebugStringA("Render thread task failed with an unhandled exception.\n");
|
||||
}
|
||||
}
|
||||
|
||||
wglMakeCurrent(NULL, NULL);
|
||||
mRenderThreadRunning = false;
|
||||
mRenderThreadId = 0;
|
||||
}
|
||||
|
||||
void RenderEngine::ReportRenderThreadRequestFailure(const char* operationName, const char* reason)
|
||||
{
|
||||
std::ostringstream message;
|
||||
message << "Render thread request failed";
|
||||
if (operationName && operationName[0] != '\0')
|
||||
message << " [" << operationName << "]";
|
||||
if (reason && reason[0] != '\0')
|
||||
message << ": " << reason;
|
||||
message << ".\n";
|
||||
OutputDebugStringA(message.str().c_str());
|
||||
}
|
||||
|
||||
bool RenderEngine::IsRenderThreadAccessExpected() const
|
||||
{
|
||||
return !mRenderThreadRunning || GetCurrentThreadId() == mRenderThreadId;
|
||||
}
|
||||
|
||||
void RenderEngine::ReportWrongThreadRenderAccess(const char* operationName) const
|
||||
{
|
||||
if (IsRenderThreadAccessExpected())
|
||||
return;
|
||||
|
||||
std::ostringstream message;
|
||||
message << "Wrong-thread render access detected";
|
||||
if (operationName && operationName[0] != '\0')
|
||||
message << " [" << operationName << "]";
|
||||
message << ".\n";
|
||||
OutputDebugStringA(message.str().c_str());
|
||||
}
|
||||
|
||||
bool RenderEngine::CompileDecodeShader(int errorMessageSize, char* errorMessage)
|
||||
{
|
||||
return InvokeOnRenderThread([this, errorMessageSize, errorMessage]() {
|
||||
return mShaderPrograms.CompileDecodeShader(errorMessageSize, errorMessage);
|
||||
});
|
||||
}
|
||||
|
||||
bool RenderEngine::CompileOutputPackShader(int errorMessageSize, char* errorMessage)
|
||||
{
|
||||
return InvokeOnRenderThread([this, errorMessageSize, 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 InvokeOnRenderThread([this, inputFrameWidth, inputFrameHeight, captureTextureWidth, outputFrameWidth, outputFrameHeight, outputPackTextureWidth, &error]() {
|
||||
return mRenderer.InitializeResources(
|
||||
inputFrameWidth,
|
||||
inputFrameHeight,
|
||||
captureTextureWidth,
|
||||
outputFrameWidth,
|
||||
outputFrameHeight,
|
||||
outputPackTextureWidth,
|
||||
error);
|
||||
});
|
||||
}
|
||||
|
||||
bool RenderEngine::CompileLayerPrograms(unsigned inputFrameWidth, unsigned inputFrameHeight, int errorMessageSize, char* errorMessage)
|
||||
{
|
||||
return InvokeOnRenderThread([this, inputFrameWidth, inputFrameHeight, errorMessageSize, errorMessage]() {
|
||||
return mShaderPrograms.CompileLayerPrograms(inputFrameWidth, inputFrameHeight, errorMessageSize, errorMessage);
|
||||
});
|
||||
}
|
||||
|
||||
bool RenderEngine::CommitPreparedLayerPrograms(const PreparedShaderBuild& preparedBuild, unsigned inputFrameWidth, unsigned inputFrameHeight, int errorMessageSize, char* errorMessage)
|
||||
{
|
||||
return InvokeOnRenderThread([this, &preparedBuild, inputFrameWidth, inputFrameHeight, errorMessageSize, 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;
|
||||
|
||||
mFrameStateResolver.StoreCommittedSnapshot(preparedBuild.renderSnapshot, mShaderPrograms.CommittedLayerStates());
|
||||
ResetTemporalHistoryState();
|
||||
if (!preserveFeedbackState)
|
||||
ResetShaderFeedbackState();
|
||||
return true;
|
||||
}
|
||||
|
||||
void RenderEngine::ResetTemporalHistoryState()
|
||||
{
|
||||
InvokeOnRenderThread([this]() {
|
||||
mRenderCommandQueue.RequestRenderReset(RenderCommandResetScope::TemporalHistoryOnly);
|
||||
ProcessRenderResetCommandsOnRenderThread();
|
||||
});
|
||||
}
|
||||
|
||||
void RenderEngine::ResetShaderFeedbackState()
|
||||
{
|
||||
InvokeOnRenderThread([this]() {
|
||||
mRenderCommandQueue.RequestRenderReset(RenderCommandResetScope::ShaderFeedbackOnly);
|
||||
ProcessRenderResetCommandsOnRenderThread();
|
||||
});
|
||||
}
|
||||
|
||||
void RenderEngine::ApplyRuntimeCoordinatorRenderReset(RuntimeCoordinatorRenderResetScope resetScope)
|
||||
{
|
||||
InvokeOnRenderThread([this, resetScope]() {
|
||||
switch (resetScope)
|
||||
{
|
||||
case RuntimeCoordinatorRenderResetScope::TemporalHistoryOnly:
|
||||
mRenderCommandQueue.RequestRenderReset(RenderCommandResetScope::TemporalHistoryOnly);
|
||||
ProcessRenderResetCommandsOnRenderThread();
|
||||
break;
|
||||
case RuntimeCoordinatorRenderResetScope::TemporalHistoryAndFeedback:
|
||||
mRenderCommandQueue.RequestRenderReset(RenderCommandResetScope::TemporalHistoryAndFeedback);
|
||||
ProcessRenderResetCommandsOnRenderThread();
|
||||
break;
|
||||
case RuntimeCoordinatorRenderResetScope::None:
|
||||
default:
|
||||
break;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void RenderEngine::ResetTemporalHistoryStateOnRenderThread()
|
||||
{
|
||||
ReportWrongThreadRenderAccess("reset-temporal-history");
|
||||
mShaderPrograms.ResetTemporalHistoryState();
|
||||
}
|
||||
|
||||
void RenderEngine::ResetShaderFeedbackStateOnRenderThread()
|
||||
{
|
||||
ReportWrongThreadRenderAccess("reset-shader-feedback");
|
||||
mShaderPrograms.ResetShaderFeedbackState();
|
||||
}
|
||||
|
||||
void RenderEngine::ApplyRenderResetOnRenderThread(RenderCommandResetScope resetScope)
|
||||
{
|
||||
switch (resetScope)
|
||||
{
|
||||
case RenderCommandResetScope::ShaderFeedbackOnly:
|
||||
ResetShaderFeedbackStateOnRenderThread();
|
||||
break;
|
||||
case RenderCommandResetScope::TemporalHistoryOnly:
|
||||
ResetTemporalHistoryStateOnRenderThread();
|
||||
break;
|
||||
case RenderCommandResetScope::TemporalHistoryAndFeedback:
|
||||
ResetTemporalHistoryStateOnRenderThread();
|
||||
ResetShaderFeedbackStateOnRenderThread();
|
||||
break;
|
||||
case RenderCommandResetScope::None:
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void RenderEngine::ProcessRenderResetCommandsOnRenderThread()
|
||||
{
|
||||
RenderCommandResetScope resetScope = RenderCommandResetScope::None;
|
||||
while (mRenderCommandQueue.TryTakeRenderReset(resetScope))
|
||||
ApplyRenderResetOnRenderThread(resetScope);
|
||||
}
|
||||
|
||||
void RenderEngine::EnqueuePreviewPresentWake()
|
||||
{
|
||||
if (!mRenderThreadRunning)
|
||||
return;
|
||||
|
||||
bool shouldNotify = false;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mRenderThreadMutex);
|
||||
if (!mRenderThreadStopping && !mPreviewPresentWakePending)
|
||||
{
|
||||
mPreviewPresentWakePending = true;
|
||||
mRenderThreadTasks.push([this]() {
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mRenderThreadMutex);
|
||||
mPreviewPresentWakePending = false;
|
||||
}
|
||||
ProcessPreviewPresentCommandsOnRenderThread();
|
||||
});
|
||||
shouldNotify = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (shouldNotify)
|
||||
mRenderThreadCondition.notify_one();
|
||||
}
|
||||
|
||||
void RenderEngine::ProcessPreviewPresentCommandsOnRenderThread()
|
||||
{
|
||||
RenderPreviewPresentRequest request;
|
||||
if (mRenderCommandQueue.TryTakePreviewPresent(request))
|
||||
PresentPreviewOnRenderThread(request.outputFrameWidth, request.outputFrameHeight);
|
||||
}
|
||||
|
||||
void RenderEngine::EnqueueInputUploadWake()
|
||||
{
|
||||
if (!mRenderThreadRunning || GetCurrentThreadId() == mRenderThreadId)
|
||||
return;
|
||||
|
||||
bool shouldNotify = false;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mRenderThreadMutex);
|
||||
if (!mRenderThreadStopping && !mInputUploadWakePending)
|
||||
{
|
||||
mInputUploadWakePending = true;
|
||||
mRenderThreadTasks.push([this]() {
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mRenderThreadMutex);
|
||||
mInputUploadWakePending = false;
|
||||
}
|
||||
ProcessInputUploadCommandsOnRenderThread();
|
||||
});
|
||||
shouldNotify = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (shouldNotify)
|
||||
mRenderThreadCondition.notify_one();
|
||||
}
|
||||
|
||||
void RenderEngine::ProcessInputUploadCommandsOnRenderThread()
|
||||
{
|
||||
RenderInputUploadRequest request;
|
||||
while (mRenderCommandQueue.TryTakeInputUpload(request))
|
||||
{
|
||||
if (request.ownedBytes.empty())
|
||||
continue;
|
||||
|
||||
request.inputFrame.bytes = request.ownedBytes.data();
|
||||
UploadInputFrameOnRenderThread(request.inputFrame, request.videoState);
|
||||
}
|
||||
}
|
||||
|
||||
void RenderEngine::EnqueueScreenshotCaptureWake()
|
||||
{
|
||||
if (!mRenderThreadRunning || GetCurrentThreadId() == mRenderThreadId)
|
||||
return;
|
||||
|
||||
bool shouldNotify = false;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mRenderThreadMutex);
|
||||
if (!mRenderThreadStopping && !mScreenshotCaptureWakePending)
|
||||
{
|
||||
mScreenshotCaptureWakePending = true;
|
||||
mRenderThreadTasks.push([this]() {
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mRenderThreadMutex);
|
||||
mScreenshotCaptureWakePending = false;
|
||||
}
|
||||
ProcessScreenshotCaptureCommandsOnRenderThread();
|
||||
});
|
||||
shouldNotify = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (shouldNotify)
|
||||
mRenderThreadCondition.notify_one();
|
||||
}
|
||||
|
||||
void RenderEngine::ProcessScreenshotCaptureCommandsOnRenderThread()
|
||||
{
|
||||
RenderScreenshotCaptureRequest request;
|
||||
ScreenshotCaptureCallback completion;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mRenderThreadMutex);
|
||||
completion = mScreenshotCaptureCompletion;
|
||||
}
|
||||
|
||||
while (mRenderCommandQueue.TryTakeScreenshotCapture(request))
|
||||
{
|
||||
if (!completion)
|
||||
continue;
|
||||
|
||||
std::vector<unsigned char> topDownPixels;
|
||||
if (CaptureOutputFrameRgbaTopDownOnRenderThread(request.width, request.height, topDownPixels))
|
||||
completion(request.width, request.height, std::move(topDownPixels));
|
||||
}
|
||||
}
|
||||
|
||||
void RenderEngine::ClearOscOverlayState()
|
||||
{
|
||||
InvokeOnRenderThread([this]() {
|
||||
mRuntimeLiveState.Clear();
|
||||
});
|
||||
}
|
||||
|
||||
void RenderEngine::ClearOscOverlayStateForLayerKey(const std::string& layerKey)
|
||||
{
|
||||
InvokeOnRenderThread([this, layerKey]() {
|
||||
mRuntimeLiveState.ClearForLayerKey(layerKey);
|
||||
});
|
||||
}
|
||||
|
||||
void RenderEngine::UpdateOscOverlayState(
|
||||
const std::vector<OscOverlayUpdate>& updates,
|
||||
const std::vector<OscOverlayCommitCompletion>& completedCommits)
|
||||
{
|
||||
std::vector<RuntimeLiveOscCommitCompletion> liveCompletions;
|
||||
liveCompletions.reserve(completedCommits.size());
|
||||
for (const OscOverlayCommitCompletion& completedCommit : completedCommits)
|
||||
liveCompletions.push_back({ completedCommit.routeKey, completedCommit.generation });
|
||||
mRuntimeLiveState.ApplyOscCommitCompletions(liveCompletions);
|
||||
|
||||
std::vector<RuntimeLiveOscUpdate> liveUpdates;
|
||||
liveUpdates.reserve(updates.size());
|
||||
for (const OscOverlayUpdate& update : updates)
|
||||
liveUpdates.push_back({ update.routeKey, update.layerKey, update.parameterKey, update.targetValue });
|
||||
mRuntimeLiveState.ApplyOscUpdates(liveUpdates);
|
||||
}
|
||||
|
||||
void RenderEngine::ResizeView(int width, int height)
|
||||
{
|
||||
InvokeOnRenderThread([this, width, 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 (mRenderThreadRunning)
|
||||
{
|
||||
mRenderCommandQueue.RequestPreviewPresent({ outputFrameWidth, outputFrameHeight });
|
||||
EnqueuePreviewPresentWake();
|
||||
return true;
|
||||
}
|
||||
|
||||
ReportRenderThreadRequestFailure("preview-present", "render thread is not running");
|
||||
return false;
|
||||
}
|
||||
|
||||
bool RenderEngine::PresentPreviewOnRenderThread(unsigned outputFrameWidth, unsigned outputFrameHeight)
|
||||
{
|
||||
ReportWrongThreadRenderAccess("preview-present");
|
||||
mRenderer.PresentToWindow(mHdc, outputFrameWidth, outputFrameHeight);
|
||||
mLastPreviewPresentTime = std::chrono::steady_clock::now();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool RenderEngine::RequestScreenshotCapture(unsigned width, unsigned height, ScreenshotCaptureCallback completion)
|
||||
{
|
||||
if (width == 0 || height == 0 || !completion)
|
||||
return false;
|
||||
if (!mRenderThreadRunning)
|
||||
return false;
|
||||
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mRenderThreadMutex);
|
||||
mScreenshotCaptureCompletion = std::move(completion);
|
||||
}
|
||||
mRenderCommandQueue.RequestScreenshotCapture({ width, height });
|
||||
EnqueueScreenshotCaptureWake();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool RenderEngine::QueueInputFrame(const VideoIOFrame& inputFrame, const VideoIOState& videoState)
|
||||
{
|
||||
if (inputFrame.hasNoInputSource || inputFrame.bytes == nullptr)
|
||||
return true;
|
||||
if (inputFrame.rowBytes <= 0 || inputFrame.height == 0)
|
||||
return false;
|
||||
|
||||
const std::size_t byteCount = static_cast<std::size_t>(inputFrame.rowBytes) * inputFrame.height;
|
||||
RenderInputUploadRequest request;
|
||||
request.inputFrame = inputFrame;
|
||||
request.videoState = videoState;
|
||||
request.ownedBytes.resize(byteCount);
|
||||
std::memcpy(request.ownedBytes.data(), inputFrame.bytes, byteCount);
|
||||
request.inputFrame.bytes = nullptr;
|
||||
|
||||
mRenderCommandQueue.RequestInputUpload(request);
|
||||
EnqueueInputUploadWake();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool RenderEngine::UploadInputFrameOnRenderThread(const VideoIOFrame& inputFrame, const VideoIOState& videoState)
|
||||
{
|
||||
ReportWrongThreadRenderAccess("input-upload");
|
||||
const long textureSize = inputFrame.rowBytes * static_cast<long>(inputFrame.height);
|
||||
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);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool RenderEngine::RequestOutputFrame(const RenderPipelineFrameContext& context, VideoIOOutputFrame& outputFrame)
|
||||
{
|
||||
if (mRenderThreadRunning)
|
||||
{
|
||||
return TryInvokeOnRenderThread("output-render", [this, &context, &outputFrame]() {
|
||||
mRenderCommandQueue.RequestOutputFrame({ context.videoState, context.completion });
|
||||
RenderOutputFrameRequest request;
|
||||
return mRenderCommandQueue.TryTakeOutputFrame(request) &&
|
||||
RenderOutputFrameOnRenderThread({ request.videoState, request.completion }, outputFrame);
|
||||
});
|
||||
}
|
||||
|
||||
ReportRenderThreadRequestFailure("output-render", "render thread is not running");
|
||||
return false;
|
||||
}
|
||||
|
||||
bool RenderEngine::RenderOutputFrameOnRenderThread(const RenderPipelineFrameContext& context, VideoIOOutputFrame& outputFrame)
|
||||
{
|
||||
ReportWrongThreadRenderAccess("output-render");
|
||||
ProcessRenderResetCommandsOnRenderThread();
|
||||
ProcessInputUploadCommandsOnRenderThread();
|
||||
return mRenderPipeline.RenderFrame(context, outputFrame);
|
||||
}
|
||||
|
||||
bool RenderEngine::ResolveRenderFrameState(
|
||||
const RenderFrameInput& input,
|
||||
std::vector<OscOverlayCommitRequest>* commitRequests,
|
||||
RenderFrameState& frameState)
|
||||
{
|
||||
std::vector<RuntimeLiveOscCommitRequest> liveCommitRequests;
|
||||
const bool resolved = mFrameStateResolver.Resolve(
|
||||
input,
|
||||
mShaderPrograms.CommittedLayerStates(),
|
||||
mRuntimeLiveState,
|
||||
commitRequests ? &liveCommitRequests : nullptr,
|
||||
frameState);
|
||||
|
||||
if (commitRequests)
|
||||
{
|
||||
for (const RuntimeLiveOscCommitRequest& request : liveCommitRequests)
|
||||
commitRequests->push_back({ request.routeKey, request.layerKey, request.parameterKey, request.value, request.generation });
|
||||
}
|
||||
return resolved;
|
||||
}
|
||||
|
||||
void RenderEngine::RenderPreparedFrame(const RenderFrameState& frameState)
|
||||
{
|
||||
RenderLayerStack(
|
||||
frameState.hasInputSource,
|
||||
frameState.layerStates,
|
||||
frameState.inputFrameWidth,
|
||||
frameState.inputFrameHeight,
|
||||
frameState.captureTextureWidth,
|
||||
frameState.inputPixelFormat,
|
||||
frameState.historyCap);
|
||||
}
|
||||
|
||||
void RenderEngine::RenderLayerStack(
|
||||
bool hasInputSource,
|
||||
const std::vector<RuntimeRenderState>& layerStates,
|
||||
unsigned inputFrameWidth,
|
||||
unsigned inputFrameHeight,
|
||||
unsigned captureTextureWidth,
|
||||
VideoIOPixelFormat inputPixelFormat,
|
||||
unsigned historyCap)
|
||||
{
|
||||
ReportWrongThreadRenderAccess("render-layer-stack");
|
||||
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::ReadOutputFrameRgbaOnRenderThread(unsigned width, unsigned height, std::vector<unsigned char>& bottomUpPixels)
|
||||
{
|
||||
ReportWrongThreadRenderAccess("read-output-frame-rgba");
|
||||
if (width == 0 || height == 0)
|
||||
return false;
|
||||
|
||||
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);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool RenderEngine::CaptureOutputFrameRgbaTopDownOnRenderThread(unsigned width, unsigned height, std::vector<unsigned char>& topDownPixels)
|
||||
{
|
||||
std::vector<unsigned char> bottomUpPixels;
|
||||
if (!ReadOutputFrameRgbaOnRenderThread(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;
|
||||
}
|
||||
232
apps/LoopThroughWithOpenGLCompositing/gl/RenderEngine.h
Normal file
232
apps/LoopThroughWithOpenGLCompositing/gl/RenderEngine.h
Normal file
@@ -0,0 +1,232 @@
|
||||
#pragma once
|
||||
|
||||
#include "OpenGLRenderPass.h"
|
||||
#include "OpenGLRenderPipeline.h"
|
||||
#include "OpenGLRenderer.h"
|
||||
#include "OpenGLShaderPrograms.h"
|
||||
#include "RenderCommandQueue.h"
|
||||
#include "RenderFrameState.h"
|
||||
#include "RenderFrameStateResolver.h"
|
||||
#include "HealthTelemetry.h"
|
||||
#include "RuntimeCoordinator.h"
|
||||
#include "RuntimeSnapshotProvider.h"
|
||||
|
||||
#include <windows.h>
|
||||
|
||||
#include <atomic>
|
||||
#include <cstdint>
|
||||
#include <chrono>
|
||||
#include <condition_variable>
|
||||
#include <functional>
|
||||
#include <future>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <queue>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
class RenderEngine
|
||||
{
|
||||
public:
|
||||
using RenderEffectCallback = std::function<void()>;
|
||||
using ScreenshotCallback = std::function<void()>;
|
||||
using ScreenshotCaptureCallback = std::function<void(unsigned, unsigned, std::vector<unsigned char>)>;
|
||||
using PreviewPaintCallback = std::function<void()>;
|
||||
|
||||
struct OscOverlayUpdate
|
||||
{
|
||||
std::string routeKey;
|
||||
std::string layerKey;
|
||||
std::string parameterKey;
|
||||
JsonValue targetValue;
|
||||
};
|
||||
|
||||
struct OscOverlayCommitCompletion
|
||||
{
|
||||
std::string routeKey;
|
||||
uint64_t generation = 0;
|
||||
};
|
||||
|
||||
struct OscOverlayCommitRequest
|
||||
{
|
||||
std::string routeKey;
|
||||
std::string layerKey;
|
||||
std::string parameterKey;
|
||||
JsonValue value;
|
||||
uint64_t generation = 0;
|
||||
};
|
||||
|
||||
RenderEngine(
|
||||
RuntimeSnapshotProvider& runtimeSnapshotProvider,
|
||||
HealthTelemetry& healthTelemetry,
|
||||
HDC hdc,
|
||||
HGLRC hglrc,
|
||||
RenderEffectCallback renderEffect,
|
||||
ScreenshotCallback screenshotReady,
|
||||
PreviewPaintCallback previewPaint);
|
||||
~RenderEngine();
|
||||
|
||||
bool StartRenderThread();
|
||||
void StopRenderThread();
|
||||
|
||||
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 ApplyPreparedShaderBuild(
|
||||
const PreparedShaderBuild& preparedBuild,
|
||||
unsigned inputFrameWidth,
|
||||
unsigned inputFrameHeight,
|
||||
bool preserveFeedbackState,
|
||||
int errorMessageSize,
|
||||
char* errorMessage);
|
||||
|
||||
void ResetTemporalHistoryState();
|
||||
void ResetShaderFeedbackState();
|
||||
void ApplyRuntimeCoordinatorRenderReset(RuntimeCoordinatorRenderResetScope resetScope);
|
||||
void ClearOscOverlayState();
|
||||
void ClearOscOverlayStateForLayerKey(const std::string& layerKey);
|
||||
void UpdateOscOverlayState(
|
||||
const std::vector<OscOverlayUpdate>& updates,
|
||||
const std::vector<OscOverlayCommitCompletion>& completedCommits);
|
||||
void ResizeView(int width, int height);
|
||||
bool TryPresentPreview(bool force, unsigned previewFps, unsigned outputFrameWidth, unsigned outputFrameHeight);
|
||||
bool RequestScreenshotCapture(unsigned width, unsigned height, ScreenshotCaptureCallback completion);
|
||||
bool QueueInputFrame(const VideoIOFrame& inputFrame, const VideoIOState& videoState);
|
||||
bool RequestOutputFrame(const RenderPipelineFrameContext& context, VideoIOOutputFrame& outputFrame);
|
||||
bool ResolveRenderFrameState(
|
||||
const RenderFrameInput& input,
|
||||
std::vector<OscOverlayCommitRequest>* commitRequests,
|
||||
RenderFrameState& frameState);
|
||||
void RenderPreparedFrame(const RenderFrameState& frameState);
|
||||
|
||||
private:
|
||||
static constexpr std::chrono::milliseconds kRenderThreadRequestTimeout{ 250 };
|
||||
|
||||
struct RenderThreadTaskState
|
||||
{
|
||||
std::atomic<bool> started = false;
|
||||
std::atomic<bool> cancelled = false;
|
||||
};
|
||||
|
||||
template<typename Func>
|
||||
auto InvokeOnRenderThread(Func&& func) -> decltype(func())
|
||||
{
|
||||
using Result = decltype(func());
|
||||
if (!mRenderThreadRunning || GetCurrentThreadId() == mRenderThreadId)
|
||||
return func();
|
||||
|
||||
auto task = std::make_shared<std::packaged_task<Result()>>(std::forward<Func>(func));
|
||||
std::future<Result> result = task->get_future();
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mRenderThreadMutex);
|
||||
mRenderThreadTasks.push([task]() { (*task)(); });
|
||||
}
|
||||
mRenderThreadCondition.notify_one();
|
||||
return result.get();
|
||||
}
|
||||
|
||||
template<typename Func>
|
||||
bool TryInvokeOnRenderThread(const char* operationName, Func&& func)
|
||||
{
|
||||
if (!mRenderThreadRunning || GetCurrentThreadId() == mRenderThreadId)
|
||||
return func();
|
||||
|
||||
auto state = std::make_shared<RenderThreadTaskState>();
|
||||
auto task = std::make_shared<std::packaged_task<bool()>>(
|
||||
[state, func = std::forward<Func>(func)]() mutable {
|
||||
state->started = true;
|
||||
if (state->cancelled)
|
||||
return false;
|
||||
|
||||
return func();
|
||||
});
|
||||
std::future<bool> result = task->get_future();
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mRenderThreadMutex);
|
||||
if (mRenderThreadStopping)
|
||||
{
|
||||
ReportRenderThreadRequestFailure(operationName, "render thread is stopping");
|
||||
return false;
|
||||
}
|
||||
mRenderThreadTasks.push([task]() { (*task)(); });
|
||||
}
|
||||
mRenderThreadCondition.notify_one();
|
||||
|
||||
if (result.wait_for(kRenderThreadRequestTimeout) == std::future_status::ready)
|
||||
return result.get();
|
||||
|
||||
if (!state->started)
|
||||
{
|
||||
state->cancelled = true;
|
||||
ReportRenderThreadRequestFailure(operationName, "timed out before execution");
|
||||
return false;
|
||||
}
|
||||
|
||||
ReportRenderThreadRequestFailure(operationName, "exceeded timeout while executing; waiting for safe completion");
|
||||
return result.get();
|
||||
}
|
||||
|
||||
void RenderThreadMain(std::promise<bool> ready);
|
||||
void ReportRenderThreadRequestFailure(const char* operationName, const char* reason);
|
||||
bool IsRenderThreadAccessExpected() const;
|
||||
void ReportWrongThreadRenderAccess(const char* operationName) const;
|
||||
bool CommitPreparedLayerPrograms(const PreparedShaderBuild& preparedBuild, unsigned inputFrameWidth, unsigned inputFrameHeight, int errorMessageSize, char* errorMessage);
|
||||
void RenderLayerStack(
|
||||
bool hasInputSource,
|
||||
const std::vector<RuntimeRenderState>& layerStates,
|
||||
unsigned inputFrameWidth,
|
||||
unsigned inputFrameHeight,
|
||||
unsigned captureTextureWidth,
|
||||
VideoIOPixelFormat inputPixelFormat,
|
||||
unsigned historyCap);
|
||||
void ResetTemporalHistoryStateOnRenderThread();
|
||||
void ResetShaderFeedbackStateOnRenderThread();
|
||||
void ApplyRenderResetOnRenderThread(RenderCommandResetScope resetScope);
|
||||
void ProcessRenderResetCommandsOnRenderThread();
|
||||
void EnqueuePreviewPresentWake();
|
||||
void ProcessPreviewPresentCommandsOnRenderThread();
|
||||
void EnqueueInputUploadWake();
|
||||
void ProcessInputUploadCommandsOnRenderThread();
|
||||
void EnqueueScreenshotCaptureWake();
|
||||
void ProcessScreenshotCaptureCommandsOnRenderThread();
|
||||
bool PresentPreviewOnRenderThread(unsigned outputFrameWidth, unsigned outputFrameHeight);
|
||||
bool UploadInputFrameOnRenderThread(const VideoIOFrame& inputFrame, const VideoIOState& videoState);
|
||||
bool RenderOutputFrameOnRenderThread(const RenderPipelineFrameContext& context, VideoIOOutputFrame& outputFrame);
|
||||
bool ReadOutputFrameRgbaOnRenderThread(unsigned width, unsigned height, std::vector<unsigned char>& bottomUpPixels);
|
||||
bool CaptureOutputFrameRgbaTopDownOnRenderThread(unsigned width, unsigned height, std::vector<unsigned char>& topDownPixels);
|
||||
|
||||
OpenGLRenderer mRenderer;
|
||||
OpenGLRenderPass mRenderPass;
|
||||
OpenGLRenderPipeline mRenderPipeline;
|
||||
OpenGLShaderPrograms mShaderPrograms;
|
||||
HDC mHdc;
|
||||
HGLRC mHglrc;
|
||||
|
||||
std::chrono::steady_clock::time_point mLastPreviewPresentTime;
|
||||
RenderCommandQueue mRenderCommandQueue;
|
||||
RenderFrameStateResolver mFrameStateResolver;
|
||||
RuntimeLiveState mRuntimeLiveState;
|
||||
std::thread mRenderThread;
|
||||
std::atomic<DWORD> mRenderThreadId = 0;
|
||||
std::mutex mRenderThreadMutex;
|
||||
std::condition_variable mRenderThreadCondition;
|
||||
std::queue<std::function<void()>> mRenderThreadTasks;
|
||||
std::atomic<bool> mRenderThreadRunning = false;
|
||||
bool mRenderThreadStopping = false;
|
||||
bool mPreviewPresentWakePending = false;
|
||||
bool mInputUploadWakePending = false;
|
||||
bool mScreenshotCaptureWakePending = false;
|
||||
ScreenshotCaptureCallback mScreenshotCaptureCompletion;
|
||||
bool mResourcesDestroyed = false;
|
||||
};
|
||||
31
apps/LoopThroughWithOpenGLCompositing/gl/RenderFrameState.h
Normal file
31
apps/LoopThroughWithOpenGLCompositing/gl/RenderFrameState.h
Normal file
@@ -0,0 +1,31 @@
|
||||
#pragma once
|
||||
|
||||
#include "ShaderTypes.h"
|
||||
#include "VideoIOTypes.h"
|
||||
|
||||
#include <vector>
|
||||
|
||||
struct RenderFrameInput
|
||||
{
|
||||
bool useCommittedLayerStates = false;
|
||||
bool hasInputSource = false;
|
||||
unsigned renderWidth = 0;
|
||||
unsigned renderHeight = 0;
|
||||
unsigned inputFrameWidth = 0;
|
||||
unsigned inputFrameHeight = 0;
|
||||
unsigned captureTextureWidth = 0;
|
||||
VideoIOPixelFormat inputPixelFormat = VideoIOPixelFormat::Uyvy8;
|
||||
unsigned historyCap = 0;
|
||||
double oscSmoothing = 0.0;
|
||||
};
|
||||
|
||||
struct RenderFrameState
|
||||
{
|
||||
bool hasInputSource = false;
|
||||
unsigned inputFrameWidth = 0;
|
||||
unsigned inputFrameHeight = 0;
|
||||
unsigned captureTextureWidth = 0;
|
||||
VideoIOPixelFormat inputPixelFormat = VideoIOPixelFormat::Uyvy8;
|
||||
unsigned historyCap = 0;
|
||||
std::vector<RuntimeRenderState> layerStates;
|
||||
};
|
||||
@@ -0,0 +1,119 @@
|
||||
#include "RenderFrameStateResolver.h"
|
||||
|
||||
#include <chrono>
|
||||
|
||||
namespace
|
||||
{
|
||||
constexpr auto kOscOverlayCommitDelay = std::chrono::milliseconds(150);
|
||||
}
|
||||
|
||||
RenderFrameStateResolver::RenderFrameStateResolver(RuntimeSnapshotProvider& runtimeSnapshotProvider) :
|
||||
mRuntimeSnapshotProvider(runtimeSnapshotProvider)
|
||||
{
|
||||
}
|
||||
|
||||
void RenderFrameStateResolver::StoreCommittedSnapshot(
|
||||
const RuntimeRenderStateSnapshot& snapshot,
|
||||
const std::vector<RuntimeRenderState>& committedLayerStates)
|
||||
{
|
||||
mCachedLayerRenderStates = committedLayerStates;
|
||||
mCachedRenderStateVersion = snapshot.versions.renderStateVersion;
|
||||
mCachedParameterStateVersion = snapshot.versions.parameterStateVersion;
|
||||
mCachedRenderStateWidth = snapshot.outputWidth;
|
||||
mCachedRenderStateHeight = snapshot.outputHeight;
|
||||
}
|
||||
|
||||
bool RenderFrameStateResolver::Resolve(
|
||||
const RenderFrameInput& input,
|
||||
const std::vector<RuntimeRenderState>& committedLayerStates,
|
||||
RuntimeLiveState& liveState,
|
||||
std::vector<RuntimeLiveOscCommitRequest>* commitRequests,
|
||||
RenderFrameState& frameState)
|
||||
{
|
||||
frameState.hasInputSource = input.hasInputSource;
|
||||
frameState.inputFrameWidth = input.inputFrameWidth;
|
||||
frameState.inputFrameHeight = input.inputFrameHeight;
|
||||
frameState.captureTextureWidth = input.captureTextureWidth;
|
||||
frameState.inputPixelFormat = input.inputPixelFormat;
|
||||
frameState.historyCap = input.historyCap;
|
||||
frameState.layerStates.clear();
|
||||
|
||||
if (input.useCommittedLayerStates)
|
||||
{
|
||||
frameState.layerStates = ComposeLayerStates(committedLayerStates, liveState, false, input.oscSmoothing, commitRequests);
|
||||
mRuntimeSnapshotProvider.RefreshDynamicRenderStateFields(frameState.layerStates);
|
||||
return true;
|
||||
}
|
||||
|
||||
const RuntimeSnapshotVersions versions = mRuntimeSnapshotProvider.GetVersions();
|
||||
const bool renderStateCacheValid =
|
||||
!mCachedLayerRenderStates.empty() &&
|
||||
mCachedRenderStateVersion == versions.renderStateVersion &&
|
||||
mCachedRenderStateWidth == input.renderWidth &&
|
||||
mCachedRenderStateHeight == input.renderHeight;
|
||||
|
||||
if (renderStateCacheValid)
|
||||
{
|
||||
RuntimeRenderStateSnapshot renderSnapshot;
|
||||
renderSnapshot.outputWidth = input.renderWidth;
|
||||
renderSnapshot.outputHeight = input.renderHeight;
|
||||
renderSnapshot.versions.renderStateVersion = mCachedRenderStateVersion;
|
||||
renderSnapshot.versions.parameterStateVersion = mCachedParameterStateVersion;
|
||||
renderSnapshot.states = mCachedLayerRenderStates;
|
||||
|
||||
renderSnapshot.states = ComposeLayerStates(renderSnapshot.states, liveState, true, input.oscSmoothing, commitRequests);
|
||||
if (mCachedParameterStateVersion != versions.parameterStateVersion &&
|
||||
mRuntimeSnapshotProvider.TryRefreshPublishedSnapshotParameters(renderSnapshot))
|
||||
{
|
||||
mCachedParameterStateVersion = renderSnapshot.versions.parameterStateVersion;
|
||||
renderSnapshot.states = ComposeLayerStates(renderSnapshot.states, liveState, true, input.oscSmoothing, commitRequests);
|
||||
}
|
||||
|
||||
mCachedLayerRenderStates = renderSnapshot.states;
|
||||
frameState.layerStates = renderSnapshot.states;
|
||||
mRuntimeSnapshotProvider.RefreshDynamicRenderStateFields(frameState.layerStates);
|
||||
return true;
|
||||
}
|
||||
|
||||
RuntimeRenderStateSnapshot renderSnapshot;
|
||||
if (mRuntimeSnapshotProvider.TryPublishRenderStateSnapshot(input.renderWidth, input.renderHeight, renderSnapshot))
|
||||
{
|
||||
mCachedLayerRenderStates = renderSnapshot.states;
|
||||
mCachedRenderStateVersion = renderSnapshot.versions.renderStateVersion;
|
||||
mCachedParameterStateVersion = renderSnapshot.versions.parameterStateVersion;
|
||||
mCachedRenderStateWidth = renderSnapshot.outputWidth;
|
||||
mCachedRenderStateHeight = renderSnapshot.outputHeight;
|
||||
mCachedLayerRenderStates = ComposeLayerStates(mCachedLayerRenderStates, liveState, true, input.oscSmoothing, commitRequests);
|
||||
frameState.layerStates = mCachedLayerRenderStates;
|
||||
return true;
|
||||
}
|
||||
|
||||
frameState.layerStates = ComposeLayerStates(mCachedLayerRenderStates, liveState, true, input.oscSmoothing, commitRequests);
|
||||
mRuntimeSnapshotProvider.RefreshDynamicRenderStateFields(frameState.layerStates);
|
||||
return !frameState.layerStates.empty();
|
||||
}
|
||||
|
||||
std::vector<RuntimeRenderState> RenderFrameStateResolver::ComposeLayerStates(
|
||||
const std::vector<RuntimeRenderState>& baseStates,
|
||||
RuntimeLiveState& liveState,
|
||||
bool allowCommit,
|
||||
double smoothing,
|
||||
std::vector<RuntimeLiveOscCommitRequest>* commitRequests) const
|
||||
{
|
||||
LayeredRenderStateInput input;
|
||||
input.committedLiveLayerStates = &baseStates;
|
||||
input.transientAutomationOverlay = &liveState;
|
||||
input.allowTransientAutomationCommits = allowCommit;
|
||||
input.collectTransientAutomationCommitRequests = commitRequests != nullptr;
|
||||
input.transientAutomationSmoothing = smoothing;
|
||||
input.transientAutomationCommitDelay = kOscOverlayCommitDelay;
|
||||
input.now = std::chrono::steady_clock::now();
|
||||
const RenderStateCompositionResult result = mRenderStateComposer.BuildFrameState(input);
|
||||
|
||||
if (commitRequests)
|
||||
{
|
||||
for (const RuntimeLiveOscCommitRequest& request : result.commitRequests)
|
||||
commitRequests->push_back(request);
|
||||
}
|
||||
return result.layerStates;
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
#pragma once
|
||||
|
||||
#include "RenderFrameState.h"
|
||||
#include "RenderStateComposer.h"
|
||||
#include "RuntimeSnapshotProvider.h"
|
||||
|
||||
#include <cstdint>
|
||||
#include <vector>
|
||||
|
||||
class RenderFrameStateResolver
|
||||
{
|
||||
public:
|
||||
explicit RenderFrameStateResolver(RuntimeSnapshotProvider& runtimeSnapshotProvider);
|
||||
|
||||
void StoreCommittedSnapshot(
|
||||
const RuntimeRenderStateSnapshot& snapshot,
|
||||
const std::vector<RuntimeRenderState>& committedLayerStates);
|
||||
bool Resolve(
|
||||
const RenderFrameInput& input,
|
||||
const std::vector<RuntimeRenderState>& committedLayerStates,
|
||||
RuntimeLiveState& liveState,
|
||||
std::vector<RuntimeLiveOscCommitRequest>* commitRequests,
|
||||
RenderFrameState& frameState);
|
||||
|
||||
private:
|
||||
std::vector<RuntimeRenderState> ComposeLayerStates(
|
||||
const std::vector<RuntimeRenderState>& baseStates,
|
||||
RuntimeLiveState& liveState,
|
||||
bool allowCommit,
|
||||
double smoothing,
|
||||
std::vector<RuntimeLiveOscCommitRequest>* commitRequests) const;
|
||||
|
||||
RuntimeSnapshotProvider& mRuntimeSnapshotProvider;
|
||||
RenderStateComposer mRenderStateComposer;
|
||||
std::vector<RuntimeRenderState> mCachedLayerRenderStates;
|
||||
uint64_t mCachedRenderStateVersion = 0;
|
||||
uint64_t mCachedParameterStateVersion = 0;
|
||||
unsigned mCachedRenderStateWidth = 0;
|
||||
unsigned mCachedRenderStateHeight = 0;
|
||||
};
|
||||
@@ -0,0 +1,365 @@
|
||||
#include "RuntimeUpdateController.h"
|
||||
|
||||
#include "RenderEngine.h"
|
||||
#include "RuntimeEventDispatcher.h"
|
||||
#include "RuntimeServices.h"
|
||||
#include "RuntimeStore.h"
|
||||
#include "ShaderBuildQueue.h"
|
||||
#include "VideoBackend.h"
|
||||
|
||||
#include <variant>
|
||||
|
||||
namespace
|
||||
{
|
||||
RuntimeCoordinatorRenderResetScope ToRuntimeCoordinatorRenderResetScope(RuntimeEventRenderResetScope scope)
|
||||
{
|
||||
switch (scope)
|
||||
{
|
||||
case RuntimeEventRenderResetScope::TemporalHistoryOnly:
|
||||
return RuntimeCoordinatorRenderResetScope::TemporalHistoryOnly;
|
||||
case RuntimeEventRenderResetScope::TemporalHistoryAndFeedback:
|
||||
return RuntimeCoordinatorRenderResetScope::TemporalHistoryAndFeedback;
|
||||
case RuntimeEventRenderResetScope::None:
|
||||
default:
|
||||
return RuntimeCoordinatorRenderResetScope::None;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
RuntimeUpdateController::RuntimeUpdateController(
|
||||
RuntimeStore& runtimeStore,
|
||||
RuntimeCoordinator& runtimeCoordinator,
|
||||
RuntimeEventDispatcher& runtimeEventDispatcher,
|
||||
RuntimeServices& runtimeServices,
|
||||
RenderEngine& renderEngine,
|
||||
ShaderBuildQueue& shaderBuildQueue,
|
||||
VideoBackend& videoBackend) :
|
||||
mRuntimeStore(runtimeStore),
|
||||
mRuntimeCoordinator(runtimeCoordinator),
|
||||
mRuntimeEventDispatcher(runtimeEventDispatcher),
|
||||
mRuntimeServices(runtimeServices),
|
||||
mRenderEngine(renderEngine),
|
||||
mShaderBuildQueue(shaderBuildQueue),
|
||||
mVideoBackend(videoBackend)
|
||||
{
|
||||
mRuntimeEventDispatcher.Subscribe(
|
||||
RuntimeEventType::RuntimeStateBroadcastRequested,
|
||||
[this](const RuntimeEvent& event) { HandleRuntimeStateBroadcastRequested(event); });
|
||||
mRuntimeEventDispatcher.Subscribe(
|
||||
RuntimeEventType::RuntimeReloadRequested,
|
||||
[this](const RuntimeEvent& event) { HandleRuntimeReloadRequested(event); });
|
||||
mRuntimeEventDispatcher.Subscribe(
|
||||
RuntimeEventType::ShaderBuildRequested,
|
||||
[this](const RuntimeEvent& event) { HandleShaderBuildRequested(event); });
|
||||
mRuntimeEventDispatcher.Subscribe(
|
||||
RuntimeEventType::ShaderBuildPrepared,
|
||||
[this](const RuntimeEvent& event) { HandleShaderBuildPrepared(event); });
|
||||
mRuntimeEventDispatcher.Subscribe(
|
||||
RuntimeEventType::ShaderBuildFailed,
|
||||
[this](const RuntimeEvent& event) { HandleShaderBuildFailed(event); });
|
||||
mRuntimeEventDispatcher.Subscribe(
|
||||
RuntimeEventType::CompileStatusChanged,
|
||||
[this](const RuntimeEvent& event) { HandleCompileStatusChanged(event); });
|
||||
mRuntimeEventDispatcher.Subscribe(
|
||||
RuntimeEventType::RenderResetRequested,
|
||||
[this](const RuntimeEvent& event) { HandleRenderResetRequested(event); });
|
||||
}
|
||||
|
||||
bool RuntimeUpdateController::ApplyRuntimeCoordinatorResult(const RuntimeCoordinatorResult& result, std::string* error)
|
||||
{
|
||||
if (!result.accepted)
|
||||
{
|
||||
if (error)
|
||||
*error = result.errorMessage;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (result.compileStatusChanged)
|
||||
{
|
||||
mRuntimeStore.SetCompileStatus(result.compileStatusSucceeded, result.compileStatusMessage);
|
||||
++mPendingCoordinatorCompileStatusEvents;
|
||||
}
|
||||
|
||||
if (result.clearReloadRequest)
|
||||
mRuntimeStore.ClearReloadRequest();
|
||||
|
||||
mRuntimeCoordinator.ApplyCommittedStateMode(result.committedStateMode);
|
||||
|
||||
switch (result.transientOscInvalidation)
|
||||
{
|
||||
case RuntimeCoordinatorTransientOscInvalidation::All:
|
||||
mRenderEngine.ClearOscOverlayState();
|
||||
mRuntimeServices.ClearOscState();
|
||||
break;
|
||||
case RuntimeCoordinatorTransientOscInvalidation::Layer:
|
||||
mRenderEngine.ClearOscOverlayStateForLayerKey(result.transientOscLayerKey);
|
||||
mRuntimeServices.ClearOscStateForLayerKey(result.transientOscLayerKey);
|
||||
break;
|
||||
case RuntimeCoordinatorTransientOscInvalidation::None:
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
mRenderEngine.ApplyRuntimeCoordinatorRenderReset(result.renderResetScope);
|
||||
if (result.renderResetScope != RuntimeCoordinatorRenderResetScope::None)
|
||||
++mPendingCoordinatorRenderResetEvents;
|
||||
|
||||
if (result.shaderBuildRequested)
|
||||
{
|
||||
RequestShaderBuild();
|
||||
++mPendingCoordinatorShaderBuildEvents;
|
||||
}
|
||||
|
||||
if (result.runtimeStateBroadcastRequired)
|
||||
BroadcastRuntimeState();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool RuntimeUpdateController::ProcessRuntimeWork()
|
||||
{
|
||||
DispatchRuntimeEvents();
|
||||
|
||||
return ConsumeReadyShaderBuild(0, true, true);
|
||||
}
|
||||
|
||||
void RuntimeUpdateController::RequestShaderBuild()
|
||||
{
|
||||
mShaderBuildQueue.RequestBuild(mVideoBackend.InputFrameWidth(), mVideoBackend.InputFrameHeight());
|
||||
}
|
||||
|
||||
void RuntimeUpdateController::BroadcastRuntimeState()
|
||||
{
|
||||
RuntimeStateBroadcastRequestedEvent event;
|
||||
event.reason = "runtime-state-changed";
|
||||
if (!mRuntimeEventDispatcher.PublishPayload(event, "RuntimeUpdateController"))
|
||||
{
|
||||
mRuntimeServices.BroadcastState();
|
||||
return;
|
||||
}
|
||||
|
||||
DispatchRuntimeEvents();
|
||||
}
|
||||
|
||||
void RuntimeUpdateController::HandleRuntimeStateBroadcastRequested(const RuntimeEvent& event)
|
||||
{
|
||||
if (event.source == "ControlServices")
|
||||
return;
|
||||
|
||||
mRuntimeServices.BroadcastState();
|
||||
}
|
||||
|
||||
void RuntimeUpdateController::HandleRuntimeReloadRequested(const RuntimeEvent& event)
|
||||
{
|
||||
const RuntimeReloadRequestedEvent* payload = std::get_if<RuntimeReloadRequestedEvent>(&event.payload);
|
||||
if (!payload)
|
||||
return;
|
||||
|
||||
mRuntimeStore.ClearReloadRequest();
|
||||
}
|
||||
|
||||
void RuntimeUpdateController::HandleShaderBuildRequested(const RuntimeEvent& event)
|
||||
{
|
||||
const ShaderBuildEvent* payload = std::get_if<ShaderBuildEvent>(&event.payload);
|
||||
if (!payload || payload->phase != RuntimeEventShaderBuildPhase::Requested)
|
||||
return;
|
||||
if (ShouldSuppressCoordinatorFollowUp(event, mPendingCoordinatorShaderBuildEvents))
|
||||
return;
|
||||
|
||||
RequestShaderBuild();
|
||||
}
|
||||
|
||||
void RuntimeUpdateController::HandleShaderBuildPrepared(const RuntimeEvent& event)
|
||||
{
|
||||
const ShaderBuildEvent* payload = std::get_if<ShaderBuildEvent>(&event.payload);
|
||||
if (!payload || payload->phase != RuntimeEventShaderBuildPhase::Prepared)
|
||||
return;
|
||||
|
||||
ConsumeReadyShaderBuild(payload->generation, false, true);
|
||||
}
|
||||
|
||||
void RuntimeUpdateController::HandleShaderBuildFailed(const RuntimeEvent& event)
|
||||
{
|
||||
const ShaderBuildEvent* payload = std::get_if<ShaderBuildEvent>(&event.payload);
|
||||
if (!payload || payload->phase != RuntimeEventShaderBuildPhase::Failed)
|
||||
return;
|
||||
|
||||
ConsumeReadyShaderBuild(payload->generation, false, false);
|
||||
}
|
||||
|
||||
void RuntimeUpdateController::HandleCompileStatusChanged(const RuntimeEvent& event)
|
||||
{
|
||||
const CompileStatusChangedEvent* payload = std::get_if<CompileStatusChangedEvent>(&event.payload);
|
||||
if (!payload)
|
||||
return;
|
||||
if (ShouldSuppressCoordinatorFollowUp(event, mPendingCoordinatorCompileStatusEvents))
|
||||
return;
|
||||
|
||||
mRuntimeStore.SetCompileStatus(payload->succeeded, payload->message);
|
||||
}
|
||||
|
||||
void RuntimeUpdateController::HandleRenderResetRequested(const RuntimeEvent& event)
|
||||
{
|
||||
const RenderResetEvent* payload = std::get_if<RenderResetEvent>(&event.payload);
|
||||
if (!payload || payload->applied)
|
||||
return;
|
||||
if (ShouldSuppressCoordinatorFollowUp(event, mPendingCoordinatorRenderResetEvents))
|
||||
return;
|
||||
|
||||
mRenderEngine.ApplyRuntimeCoordinatorRenderReset(ToRuntimeCoordinatorRenderResetScope(payload->scope));
|
||||
}
|
||||
|
||||
bool RuntimeUpdateController::ConsumeReadyShaderBuild(uint64_t expectedGeneration, bool publishPreparedEvent, bool publishFailureEvent)
|
||||
{
|
||||
PreparedShaderBuild readyBuild;
|
||||
const bool consumed = expectedGeneration == 0
|
||||
? mShaderBuildQueue.TryConsumeReadyBuild(readyBuild)
|
||||
: mShaderBuildQueue.TryConsumeReadyBuild(expectedGeneration, readyBuild);
|
||||
if (!consumed)
|
||||
return true;
|
||||
|
||||
const unsigned inputWidth = mVideoBackend.InputFrameWidth();
|
||||
const unsigned inputHeight = mVideoBackend.InputFrameHeight();
|
||||
if (!readyBuild.succeeded)
|
||||
{
|
||||
if (publishFailureEvent)
|
||||
{
|
||||
PublishShaderBuildLifecycleEvent(
|
||||
RuntimeEventShaderBuildPhase::Failed,
|
||||
readyBuild.generation,
|
||||
inputWidth,
|
||||
inputHeight,
|
||||
false,
|
||||
readyBuild.message);
|
||||
DispatchRuntimeEvents();
|
||||
}
|
||||
ApplyRuntimeCoordinatorResult(mRuntimeCoordinator.HandlePreparedShaderBuildFailure(readyBuild.message));
|
||||
return false;
|
||||
}
|
||||
|
||||
if (publishPreparedEvent)
|
||||
{
|
||||
PublishShaderBuildLifecycleEvent(
|
||||
RuntimeEventShaderBuildPhase::Prepared,
|
||||
readyBuild.generation,
|
||||
inputWidth,
|
||||
inputHeight,
|
||||
true,
|
||||
readyBuild.message);
|
||||
DispatchRuntimeEvents();
|
||||
}
|
||||
|
||||
char compilerErrorMessage[1024] = {};
|
||||
if (!mRenderEngine.ApplyPreparedShaderBuild(
|
||||
readyBuild,
|
||||
inputWidth,
|
||||
inputHeight,
|
||||
mRuntimeCoordinator.PreserveFeedbackOnNextShaderBuild(),
|
||||
sizeof(compilerErrorMessage),
|
||||
compilerErrorMessage))
|
||||
{
|
||||
const std::string errorMessage = compilerErrorMessage;
|
||||
if (publishFailureEvent)
|
||||
{
|
||||
PublishShaderBuildLifecycleEvent(
|
||||
RuntimeEventShaderBuildPhase::Failed,
|
||||
readyBuild.generation,
|
||||
inputWidth,
|
||||
inputHeight,
|
||||
false,
|
||||
errorMessage);
|
||||
DispatchRuntimeEvents();
|
||||
}
|
||||
ApplyRuntimeCoordinatorResult(mRuntimeCoordinator.HandlePreparedShaderBuildFailure(errorMessage));
|
||||
return false;
|
||||
}
|
||||
|
||||
PublishShaderBuildLifecycleEvent(
|
||||
RuntimeEventShaderBuildPhase::Applied,
|
||||
readyBuild.generation,
|
||||
inputWidth,
|
||||
inputHeight,
|
||||
true,
|
||||
"Shader layers applied successfully.");
|
||||
ApplyRuntimeCoordinatorResult(mRuntimeCoordinator.HandlePreparedShaderBuildSuccess());
|
||||
return true;
|
||||
}
|
||||
|
||||
void RuntimeUpdateController::PublishShaderBuildLifecycleEvent(
|
||||
RuntimeEventShaderBuildPhase phase,
|
||||
uint64_t generation,
|
||||
unsigned inputWidth,
|
||||
unsigned inputHeight,
|
||||
bool succeeded,
|
||||
const std::string& message)
|
||||
{
|
||||
ShaderBuildEvent event;
|
||||
event.phase = phase;
|
||||
event.generation = generation;
|
||||
event.inputWidth = inputWidth;
|
||||
event.inputHeight = inputHeight;
|
||||
event.preserveFeedbackState = mRuntimeCoordinator.PreserveFeedbackOnNextShaderBuild();
|
||||
event.succeeded = succeeded;
|
||||
event.message = message;
|
||||
mRuntimeEventDispatcher.PublishPayload(event, "RuntimeUpdateController");
|
||||
}
|
||||
|
||||
bool RuntimeUpdateController::ShouldSuppressCoordinatorFollowUp(const RuntimeEvent& event, std::size_t& pendingSuppressions)
|
||||
{
|
||||
if (event.source != "RuntimeCoordinator")
|
||||
return false;
|
||||
|
||||
if (pendingSuppressions > 0)
|
||||
--pendingSuppressions;
|
||||
return true;
|
||||
}
|
||||
|
||||
RuntimeEventDispatchResult RuntimeUpdateController::DispatchRuntimeEvents(std::size_t maxEvents)
|
||||
{
|
||||
RuntimeEventDispatchResult result = mRuntimeEventDispatcher.DispatchPending(maxEvents);
|
||||
const RuntimeEventQueueMetrics queueMetrics = mRuntimeEventDispatcher.GetQueueMetrics();
|
||||
HealthTelemetry& telemetry = mRuntimeStore.GetHealthTelemetry();
|
||||
telemetry.TryRecordRuntimeEventDispatchStats(
|
||||
result.dispatchedEvents,
|
||||
result.handlerInvocations,
|
||||
result.handlerFailures,
|
||||
result.dispatchDurationMilliseconds);
|
||||
telemetry.TryRecordRuntimeEventQueueMetrics(
|
||||
"runtime-events",
|
||||
queueMetrics.depth,
|
||||
queueMetrics.capacity,
|
||||
static_cast<uint64_t>(queueMetrics.droppedCount),
|
||||
queueMetrics.oldestEventAgeMilliseconds);
|
||||
PublishRuntimeEventHealthObservations(result);
|
||||
return result;
|
||||
}
|
||||
|
||||
void RuntimeUpdateController::PublishRuntimeEventHealthObservations(const RuntimeEventDispatchResult& result)
|
||||
{
|
||||
const RuntimeEventQueueMetrics queueMetrics = mRuntimeEventDispatcher.GetQueueMetrics();
|
||||
if (queueMetrics.depth != mLastReportedRuntimeEventQueueDepth ||
|
||||
queueMetrics.droppedCount != mLastReportedRuntimeEventDroppedCount ||
|
||||
queueMetrics.coalescedCount != mLastReportedRuntimeEventCoalescedCount)
|
||||
{
|
||||
QueueDepthChangedEvent queueDepth;
|
||||
queueDepth.queueName = "runtime-events";
|
||||
queueDepth.depth = queueMetrics.depth;
|
||||
queueDepth.capacity = queueMetrics.capacity;
|
||||
queueDepth.droppedCount = queueMetrics.droppedCount;
|
||||
queueDepth.coalescedCount = queueMetrics.coalescedCount;
|
||||
mRuntimeEventDispatcher.PublishPayload(queueDepth, "HealthTelemetry");
|
||||
mLastReportedRuntimeEventQueueDepth = queueMetrics.depth;
|
||||
mLastReportedRuntimeEventDroppedCount = queueMetrics.droppedCount;
|
||||
mLastReportedRuntimeEventCoalescedCount = queueMetrics.coalescedCount;
|
||||
}
|
||||
|
||||
if (result.handlerInvocations == 0 && result.handlerFailures == 0)
|
||||
return;
|
||||
|
||||
TimingSampleRecordedEvent timing;
|
||||
timing.subsystem = "RuntimeEventDispatcher";
|
||||
timing.metric = "dispatchDuration";
|
||||
timing.value = result.dispatchDurationMilliseconds;
|
||||
timing.unit = "ms";
|
||||
mRuntimeEventDispatcher.PublishPayload(timing, "HealthTelemetry");
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
#pragma once
|
||||
|
||||
#include "RuntimeCoordinator.h"
|
||||
#include "RuntimeEventPayloads.h"
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
|
||||
class RenderEngine;
|
||||
struct RuntimeEvent;
|
||||
struct RuntimeEventDispatchResult;
|
||||
class RuntimeEventDispatcher;
|
||||
class RuntimeServices;
|
||||
class RuntimeStore;
|
||||
class ShaderBuildQueue;
|
||||
class VideoBackend;
|
||||
|
||||
class RuntimeUpdateController
|
||||
{
|
||||
public:
|
||||
RuntimeUpdateController(
|
||||
RuntimeStore& runtimeStore,
|
||||
RuntimeCoordinator& runtimeCoordinator,
|
||||
RuntimeEventDispatcher& runtimeEventDispatcher,
|
||||
RuntimeServices& runtimeServices,
|
||||
RenderEngine& renderEngine,
|
||||
ShaderBuildQueue& shaderBuildQueue,
|
||||
VideoBackend& videoBackend);
|
||||
|
||||
bool ApplyRuntimeCoordinatorResult(const RuntimeCoordinatorResult& result, std::string* error = nullptr);
|
||||
bool ProcessRuntimeWork();
|
||||
void RequestShaderBuild();
|
||||
void BroadcastRuntimeState();
|
||||
|
||||
private:
|
||||
void HandleRuntimeStateBroadcastRequested(const RuntimeEvent& event);
|
||||
void HandleRuntimeReloadRequested(const RuntimeEvent& event);
|
||||
void HandleShaderBuildRequested(const RuntimeEvent& event);
|
||||
void HandleShaderBuildPrepared(const RuntimeEvent& event);
|
||||
void HandleShaderBuildFailed(const RuntimeEvent& event);
|
||||
void HandleCompileStatusChanged(const RuntimeEvent& event);
|
||||
void HandleRenderResetRequested(const RuntimeEvent& event);
|
||||
bool ConsumeReadyShaderBuild(uint64_t expectedGeneration, bool publishPreparedEvent, bool publishFailureEvent);
|
||||
void PublishShaderBuildLifecycleEvent(
|
||||
RuntimeEventShaderBuildPhase phase,
|
||||
uint64_t generation,
|
||||
unsigned inputWidth,
|
||||
unsigned inputHeight,
|
||||
bool succeeded,
|
||||
const std::string& message);
|
||||
bool ShouldSuppressCoordinatorFollowUp(const RuntimeEvent& event, std::size_t& pendingSuppressions);
|
||||
RuntimeEventDispatchResult DispatchRuntimeEvents(std::size_t maxEvents = 0);
|
||||
void PublishRuntimeEventHealthObservations(const RuntimeEventDispatchResult& result);
|
||||
|
||||
RuntimeStore& mRuntimeStore;
|
||||
RuntimeCoordinator& mRuntimeCoordinator;
|
||||
RuntimeEventDispatcher& mRuntimeEventDispatcher;
|
||||
RuntimeServices& mRuntimeServices;
|
||||
RenderEngine& mRenderEngine;
|
||||
ShaderBuildQueue& mShaderBuildQueue;
|
||||
VideoBackend& mVideoBackend;
|
||||
std::size_t mPendingCoordinatorShaderBuildEvents = 0;
|
||||
std::size_t mPendingCoordinatorCompileStatusEvents = 0;
|
||||
std::size_t mPendingCoordinatorRenderResetEvents = 0;
|
||||
std::size_t mLastReportedRuntimeEventQueueDepth = static_cast<std::size_t>(-1);
|
||||
std::size_t mLastReportedRuntimeEventDroppedCount = static_cast<std::size_t>(-1);
|
||||
std::size_t mLastReportedRuntimeEventCoalescedCount = static_cast<std::size_t>(-1);
|
||||
};
|
||||
@@ -1,7 +1,8 @@
|
||||
#include "OpenGLRenderPipeline.h"
|
||||
|
||||
#include "HealthTelemetry.h"
|
||||
#include "OpenGLRenderer.h"
|
||||
#include "RuntimeHost.h"
|
||||
#include "RuntimeSnapshotProvider.h"
|
||||
#include "VideoIOFormat.h"
|
||||
|
||||
#include <cstring>
|
||||
@@ -11,12 +12,14 @@
|
||||
|
||||
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)
|
||||
@@ -47,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.AdvanceFrame();
|
||||
|
||||
ReadOutputFrame(state, outputFrame);
|
||||
if (mPaint)
|
||||
|
||||
@@ -8,7 +8,8 @@
|
||||
#include <vector>
|
||||
|
||||
class OpenGLRenderer;
|
||||
class RuntimeHost;
|
||||
class HealthTelemetry;
|
||||
class RuntimeSnapshotProvider;
|
||||
|
||||
struct RenderPipelineFrameContext
|
||||
{
|
||||
@@ -25,7 +26,8 @@ public:
|
||||
|
||||
OpenGLRenderPipeline(
|
||||
OpenGLRenderer& renderer,
|
||||
RuntimeHost& runtimeHost,
|
||||
RuntimeSnapshotProvider& runtimeSnapshotProvider,
|
||||
HealthTelemetry& healthTelemetry,
|
||||
RenderEffectCallback renderEffect,
|
||||
OutputReadyCallback outputReady,
|
||||
PaintCallback paint);
|
||||
@@ -53,7 +55,8 @@ private:
|
||||
void ReadOutputFrame(const VideoIOState& state, VideoIOOutputFrame& outputFrame);
|
||||
|
||||
OpenGLRenderer& mRenderer;
|
||||
RuntimeHost& mRuntimeHost;
|
||||
RuntimeSnapshotProvider& mRuntimeSnapshotProvider;
|
||||
HealthTelemetry& mHealthTelemetry;
|
||||
RenderEffectCallback mRenderEffect;
|
||||
OutputReadyCallback mOutputReady;
|
||||
PaintCallback mPaint;
|
||||
|
||||
@@ -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);
|
||||
|
||||
// Never let input upload stall the playout/render callback. If the GL bridge
|
||||
// is busy producing an output frame, skip this upload and use the next input.
|
||||
if (!TryEnterCriticalSection(&mMutex))
|
||||
return;
|
||||
|
||||
wglMakeCurrent(mHdc, mHglrc); // make OpenGL context current in this thread
|
||||
|
||||
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.QueueInputFrame(inputFrame, state);
|
||||
}
|
||||
|
||||
void OpenGLVideoIOBridge::PlayoutFrameCompleted(const VideoIOCompletion& completion)
|
||||
bool OpenGLVideoIOBridge::RenderScheduledFrame(const VideoIOState& state, const VideoIOCompletion& completion, VideoIOOutputFrame& outputFrame)
|
||||
{
|
||||
RecordFramePacing(completion.result);
|
||||
|
||||
VideoIOOutputFrame outputFrame;
|
||||
if (!mVideoIO.BeginOutputFrame(outputFrame))
|
||||
return;
|
||||
const VideoIOState& state = mVideoIO.State();
|
||||
RenderPipelineFrameContext frameContext;
|
||||
frameContext.videoState = state;
|
||||
frameContext.completion = completion;
|
||||
|
||||
EnterCriticalSection(&mMutex);
|
||||
|
||||
// make GL context current in this thread
|
||||
wglMakeCurrent(mHdc, mHglrc);
|
||||
|
||||
mRenderPipeline.RenderFrame(frameContext, outputFrame);
|
||||
wglMakeCurrent(NULL, NULL);
|
||||
|
||||
LeaveCriticalSection(&mMutex);
|
||||
|
||||
mVideoIO.EndOutputFrame(outputFrame);
|
||||
|
||||
mVideoIO.AccountForCompletionResult(completion.result);
|
||||
|
||||
// Schedule the next frame for playout after the GL bridge is released so
|
||||
// input uploads are not blocked by non-GL output bookkeeping.
|
||||
mVideoIO.ScheduleOutputFrame(outputFrame);
|
||||
return mRenderEngine.RequestOutputFrame(frameContext, outputFrame);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
bool 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;
|
||||
};
|
||||
|
||||
@@ -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.PublishRenderStateSnapshot(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);
|
||||
@@ -87,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;
|
||||
}
|
||||
@@ -104,19 +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.layerStates, inputFrameWidth, inputFrameHeight, temporalError))
|
||||
!mRenderer.FeedbackBuffers().EnsureResources(preparedBuild.renderSnapshot.states, inputFrameWidth, inputFrameHeight, temporalError))
|
||||
{
|
||||
CopyErrorMessage(temporalError, errorMessageSize, errorMessage);
|
||||
return false;
|
||||
@@ -150,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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
@@ -32,7 +32,7 @@ public:
|
||||
|
||||
private:
|
||||
OpenGLRenderer& mRenderer;
|
||||
RuntimeHost& mRuntimeHost;
|
||||
RuntimeSnapshotProvider& mRuntimeSnapshotProvider;
|
||||
ShaderTextureBindings mTextureBindings;
|
||||
GlobalParamsBuffer mGlobalParamsBuffer;
|
||||
ShaderProgramCompiler mCompiler;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
#include "ShaderBuildQueue.h"
|
||||
|
||||
#include "RuntimeHost.h"
|
||||
#include "RuntimeEventDispatcher.h"
|
||||
|
||||
#include <chrono>
|
||||
#include <utility>
|
||||
@@ -10,8 +10,9 @@ namespace
|
||||
constexpr auto kShaderBuildDebounce = std::chrono::milliseconds(400);
|
||||
}
|
||||
|
||||
ShaderBuildQueue::ShaderBuildQueue(RuntimeHost& runtimeHost) :
|
||||
mRuntimeHost(runtimeHost),
|
||||
ShaderBuildQueue::ShaderBuildQueue(RuntimeSnapshotProvider& runtimeSnapshotProvider, RuntimeEventDispatcher& runtimeEventDispatcher) :
|
||||
mRuntimeSnapshotProvider(runtimeSnapshotProvider),
|
||||
mRuntimeEventDispatcher(runtimeEventDispatcher),
|
||||
mWorkerThread([this]() { WorkerLoop(); })
|
||||
{
|
||||
}
|
||||
@@ -46,6 +47,18 @@ bool ShaderBuildQueue::TryConsumeReadyBuild(PreparedShaderBuild& build)
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ShaderBuildQueue::TryConsumeReadyBuild(uint64_t expectedGeneration, PreparedShaderBuild& build)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mMutex);
|
||||
if (!mHasReadyBuild || mReadyBuild.generation != expectedGeneration)
|
||||
return false;
|
||||
|
||||
build = std::move(mReadyBuild);
|
||||
mReadyBuild = PreparedShaderBuild();
|
||||
mHasReadyBuild = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
void ShaderBuildQueue::Stop()
|
||||
{
|
||||
{
|
||||
@@ -99,13 +112,20 @@ void ShaderBuildQueue::WorkerLoop()
|
||||
|
||||
PreparedShaderBuild build = Build(generation, outputWidth, outputHeight);
|
||||
|
||||
std::lock_guard<std::mutex> lock(mMutex);
|
||||
if (mStopping)
|
||||
return;
|
||||
if (generation != mRequestedGeneration)
|
||||
continue;
|
||||
mReadyBuild = std::move(build);
|
||||
mHasReadyBuild = true;
|
||||
bool shouldPublish = false;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mMutex);
|
||||
if (mStopping)
|
||||
return;
|
||||
if (generation != mRequestedGeneration)
|
||||
continue;
|
||||
mReadyBuild = build;
|
||||
mHasReadyBuild = true;
|
||||
shouldPublish = true;
|
||||
}
|
||||
|
||||
if (shouldPublish)
|
||||
PublishBuildLifecycleEvent(build, outputWidth, outputHeight);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -113,14 +133,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.PublishRenderStateSnapshot(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;
|
||||
@@ -132,3 +152,15 @@ PreparedShaderBuild ShaderBuildQueue::Build(uint64_t generation, unsigned output
|
||||
build.message = "Shader layers prepared successfully.";
|
||||
return build;
|
||||
}
|
||||
|
||||
void ShaderBuildQueue::PublishBuildLifecycleEvent(const PreparedShaderBuild& build, unsigned outputWidth, unsigned outputHeight) const
|
||||
{
|
||||
ShaderBuildEvent event;
|
||||
event.phase = build.succeeded ? RuntimeEventShaderBuildPhase::Prepared : RuntimeEventShaderBuildPhase::Failed;
|
||||
event.generation = build.generation;
|
||||
event.inputWidth = outputWidth;
|
||||
event.inputHeight = outputHeight;
|
||||
event.succeeded = build.succeeded;
|
||||
event.message = build.message;
|
||||
mRuntimeEventDispatcher.PublishPayload(event, "ShaderBuildQueue");
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#pragma once
|
||||
|
||||
#include "RuntimeSnapshotProvider.h"
|
||||
#include "ShaderTypes.h"
|
||||
|
||||
#include <condition_variable>
|
||||
@@ -9,7 +10,7 @@
|
||||
#include <thread>
|
||||
#include <vector>
|
||||
|
||||
class RuntimeHost;
|
||||
class RuntimeEventDispatcher;
|
||||
|
||||
struct PreparedLayerShader
|
||||
{
|
||||
@@ -22,14 +23,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);
|
||||
ShaderBuildQueue(RuntimeSnapshotProvider& runtimeSnapshotProvider, RuntimeEventDispatcher& runtimeEventDispatcher);
|
||||
~ShaderBuildQueue();
|
||||
|
||||
ShaderBuildQueue(const ShaderBuildQueue&) = delete;
|
||||
@@ -37,13 +38,16 @@ public:
|
||||
|
||||
void RequestBuild(unsigned outputWidth, unsigned outputHeight);
|
||||
bool TryConsumeReadyBuild(PreparedShaderBuild& build);
|
||||
bool TryConsumeReadyBuild(uint64_t expectedGeneration, PreparedShaderBuild& build);
|
||||
void Stop();
|
||||
|
||||
private:
|
||||
void WorkerLoop();
|
||||
PreparedShaderBuild Build(uint64_t generation, unsigned outputWidth, unsigned outputHeight);
|
||||
void PublishBuildLifecycleEvent(const PreparedShaderBuild& build, unsigned outputWidth, unsigned outputHeight) const;
|
||||
|
||||
RuntimeHost& mRuntimeHost;
|
||||
RuntimeSnapshotProvider& mRuntimeSnapshotProvider;
|
||||
RuntimeEventDispatcher& mRuntimeEventDispatcher;
|
||||
std::thread mWorkerThread;
|
||||
std::mutex mMutex;
|
||||
std::condition_variable mCondition;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,199 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "RuntimeJson.h"
|
||||
#include "ShaderTypes.h"
|
||||
|
||||
#include <atomic>
|
||||
#include <chrono>
|
||||
#include <filesystem>
|
||||
#include <map>
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
class RuntimeHost
|
||||
{
|
||||
public:
|
||||
RuntimeHost();
|
||||
|
||||
bool Initialize(std::string& error);
|
||||
|
||||
bool PollFileChanges(bool& registryChanged, bool& reloadRequested, std::string& error);
|
||||
bool ManualReloadRequested();
|
||||
void ClearReloadRequest();
|
||||
|
||||
bool AddLayer(const std::string& shaderId, std::string& error);
|
||||
bool RemoveLayer(const std::string& layerId, std::string& error);
|
||||
bool MoveLayer(const std::string& layerId, int direction, std::string& error);
|
||||
bool MoveLayerToIndex(const std::string& layerId, std::size_t targetIndex, std::string& error);
|
||||
bool SetLayerBypass(const std::string& layerId, bool bypassed, std::string& error);
|
||||
bool SetLayerShader(const std::string& layerId, const std::string& shaderId, std::string& error);
|
||||
bool UpdateLayerParameter(const std::string& layerId, const std::string& parameterId, const JsonValue& newValue, std::string& error);
|
||||
bool UpdateLayerParameterByControlKey(const std::string& layerKey, const std::string& parameterKey, const JsonValue& newValue, std::string& error);
|
||||
bool 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);
|
||||
|
||||
void SetCompileStatus(bool succeeded, const std::string& message);
|
||||
void SetSignalStatus(bool hasSignal, unsigned width, unsigned height, const std::string& modeName);
|
||||
bool TrySetSignalStatus(bool hasSignal, unsigned width, unsigned height, const std::string& modeName);
|
||||
void SetDeckLinkOutputStatus(const std::string& modelName, bool supportsInternalKeying, bool supportsExternalKeying,
|
||||
bool keyerInterfaceAvailable, bool externalKeyingRequested, bool externalKeyingActive, const std::string& statusMessage);
|
||||
void SetVideoIOStatus(const std::string& backendName, const std::string& modelName, bool supportsInternalKeying, bool supportsExternalKeying,
|
||||
bool keyerInterfaceAvailable, bool externalKeyingRequested, bool externalKeyingActive, const std::string& statusMessage);
|
||||
void SetPerformanceStats(double frameBudgetMilliseconds, double renderMilliseconds);
|
||||
bool TrySetPerformanceStats(double frameBudgetMilliseconds, double renderMilliseconds);
|
||||
void SetFramePacingStats(double completionIntervalMilliseconds, double smoothedCompletionIntervalMilliseconds,
|
||||
double maxCompletionIntervalMilliseconds, uint64_t lateFrameCount, uint64_t droppedFrameCount, uint64_t flushedFrameCount);
|
||||
bool TrySetFramePacingStats(double completionIntervalMilliseconds, double smoothedCompletionIntervalMilliseconds,
|
||||
double maxCompletionIntervalMilliseconds, uint64_t lateFrameCount, uint64_t droppedFrameCount, uint64_t flushedFrameCount);
|
||||
void AdvanceFrame();
|
||||
bool TryAdvanceFrame();
|
||||
|
||||
bool BuildLayerPassFragmentShaderSources(const std::string& layerId, std::vector<ShaderPassBuildSource>& passSources, std::string& error);
|
||||
std::vector<RuntimeRenderState> GetLayerRenderStates(unsigned outputWidth, unsigned outputHeight) const;
|
||||
bool TryGetLayerRenderStates(unsigned outputWidth, unsigned outputHeight, std::vector<RuntimeRenderState>& states) const;
|
||||
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; }
|
||||
const std::filesystem::path& GetDocsRoot() const { return mDocsRoot; }
|
||||
const std::filesystem::path& GetRuntimeRoot() const { return mRuntimeRoot; }
|
||||
unsigned short GetServerPort() const { return mServerPort; }
|
||||
unsigned short GetOscPort() const { return mConfig.oscPort; }
|
||||
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; }
|
||||
const std::string& GetOutputVideoFormat() const { return mConfig.outputVideoFormat; }
|
||||
const std::string& GetOutputFrameRate() const { return mConfig.outputFrameRate; }
|
||||
void SetServerPort(unsigned short port);
|
||||
bool AutoReloadEnabled() const { return mAutoReloadEnabled; }
|
||||
|
||||
private:
|
||||
struct AppConfig
|
||||
{
|
||||
std::string shaderLibrary = "shaders";
|
||||
unsigned short serverPort = 8080;
|
||||
unsigned short oscPort = 9000;
|
||||
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";
|
||||
std::string outputVideoFormat = "1080p";
|
||||
std::string outputFrameRate = "59.94";
|
||||
};
|
||||
|
||||
struct DeckLinkOutputStatus
|
||||
{
|
||||
std::string backendName = "decklink";
|
||||
std::string modelName;
|
||||
bool supportsInternalKeying = false;
|
||||
bool supportsExternalKeying = false;
|
||||
bool keyerInterfaceAvailable = false;
|
||||
bool externalKeyingRequested = false;
|
||||
bool externalKeyingActive = false;
|
||||
std::string statusMessage;
|
||||
};
|
||||
|
||||
struct LayerPersistentState
|
||||
{
|
||||
std::string id;
|
||||
std::string shaderId;
|
||||
bool bypass = false;
|
||||
std::map<std::string, ShaderParameterValue> parameterValues;
|
||||
};
|
||||
|
||||
struct PersistentState
|
||||
{
|
||||
std::vector<LayerPersistentState> layers;
|
||||
};
|
||||
|
||||
bool LoadConfig(std::string& error);
|
||||
bool LoadPersistentState(std::string& error);
|
||||
bool SavePersistentState(std::string& error) const;
|
||||
bool ScanShaderPackages(std::string& error);
|
||||
bool NormalizeAndValidateValue(const ShaderParameterDefinition& definition, const JsonValue& value, ShaderParameterValue& normalizedValue, std::string& error) const;
|
||||
ShaderParameterValue DefaultValueForDefinition(const ShaderParameterDefinition& definition) const;
|
||||
void EnsureLayerDefaultsLocked(LayerPersistentState& layerState, const ShaderPackage& shaderPackage) const;
|
||||
std::string ReadTextFile(const std::filesystem::path& path, std::string& error) const;
|
||||
bool WriteTextFile(const std::filesystem::path& path, const std::string& contents, std::string& error) const;
|
||||
bool ResolvePaths(std::string& error);
|
||||
void BuildLayerRenderStatesLocked(unsigned outputWidth, unsigned outputHeight, std::vector<RuntimeRenderState>& states) const;
|
||||
JsonValue BuildStateValue() const;
|
||||
JsonValue SerializeLayerStackLocked() const;
|
||||
bool DeserializeLayerStackLocked(const JsonValue& layersValue, std::vector<LayerPersistentState>& layers, std::string& error);
|
||||
void NormalizePersistentLayerIdsLocked();
|
||||
std::vector<std::string> GetStackPresetNamesLocked() const;
|
||||
std::string MakeSafePresetFileStem(const std::string& presetName) const;
|
||||
JsonValue SerializeParameterValue(const ShaderParameterDefinition& definition, const ShaderParameterValue& value) const;
|
||||
std::string TemporalHistorySourceToString(TemporalHistorySource source) const;
|
||||
LayerPersistentState* FindLayerById(const std::string& layerId);
|
||||
const LayerPersistentState* FindLayerById(const std::string& layerId) const;
|
||||
std::string GenerateLayerId();
|
||||
void SetSignalStatusLocked(bool hasSignal, unsigned width, unsigned height, const std::string& modeName);
|
||||
void MarkRenderStateDirtyLocked();
|
||||
void MarkParameterStateDirtyLocked();
|
||||
void SetPerformanceStatsLocked(double frameBudgetMilliseconds, double renderMilliseconds);
|
||||
void SetFramePacingStatsLocked(double completionIntervalMilliseconds, double smoothedCompletionIntervalMilliseconds,
|
||||
double maxCompletionIntervalMilliseconds, uint64_t lateFrameCount, uint64_t droppedFrameCount, uint64_t flushedFrameCount);
|
||||
|
||||
private:
|
||||
mutable std::mutex mMutex;
|
||||
AppConfig mConfig;
|
||||
PersistentState mPersistentState;
|
||||
std::filesystem::path mRepoRoot;
|
||||
std::filesystem::path mUiRoot;
|
||||
std::filesystem::path mDocsRoot;
|
||||
std::filesystem::path mShaderRoot;
|
||||
std::filesystem::path mRuntimeRoot;
|
||||
std::filesystem::path mPresetRoot;
|
||||
std::filesystem::path mRuntimeStatePath;
|
||||
std::filesystem::path mConfigPath;
|
||||
std::filesystem::path mWrapperPath;
|
||||
std::filesystem::path mGeneratedGlslPath;
|
||||
std::filesystem::path mPatchedGlslPath;
|
||||
std::map<std::string, ShaderPackage> mPackagesById;
|
||||
std::vector<std::string> mPackageOrder;
|
||||
std::vector<ShaderPackageStatus> mPackageStatuses;
|
||||
bool mReloadRequested;
|
||||
bool mCompileSucceeded;
|
||||
std::string mCompileMessage;
|
||||
bool mHasSignal;
|
||||
unsigned mSignalWidth;
|
||||
unsigned mSignalHeight;
|
||||
std::string mSignalModeName;
|
||||
double mFrameBudgetMilliseconds;
|
||||
double mRenderMilliseconds;
|
||||
double mSmoothedRenderMilliseconds;
|
||||
double mCompletionIntervalMilliseconds;
|
||||
double mSmoothedCompletionIntervalMilliseconds;
|
||||
double mMaxCompletionIntervalMilliseconds;
|
||||
double mStartupRandom;
|
||||
uint64_t mLateFrameCount;
|
||||
uint64_t mDroppedFrameCount;
|
||||
uint64_t mFlushedFrameCount;
|
||||
DeckLinkOutputStatus mDeckLinkOutputStatus;
|
||||
unsigned short mServerPort;
|
||||
bool mAutoReloadEnabled;
|
||||
std::chrono::steady_clock::time_point mStartTime;
|
||||
std::chrono::steady_clock::time_point mLastScanTime;
|
||||
std::atomic<uint64_t> mFrameCounter{ 0 };
|
||||
std::atomic<uint64_t> mRenderStateVersion{ 0 };
|
||||
std::atomic<uint64_t> mParameterStateVersion{ 0 };
|
||||
uint64_t mNextLayerId;
|
||||
};
|
||||
@@ -0,0 +1,614 @@
|
||||
#include "RuntimeCoordinator.h"
|
||||
|
||||
#include "RuntimeEventDispatcher.h"
|
||||
#include "RuntimeEventPayloads.h"
|
||||
#include "RuntimeParameterUtils.h"
|
||||
#include "RuntimeStore.h"
|
||||
|
||||
namespace
|
||||
{
|
||||
RuntimeEventRenderResetScope ToRuntimeEventRenderResetScope(RuntimeCoordinatorRenderResetScope scope)
|
||||
{
|
||||
switch (scope)
|
||||
{
|
||||
case RuntimeCoordinatorRenderResetScope::TemporalHistoryOnly:
|
||||
return RuntimeEventRenderResetScope::TemporalHistoryOnly;
|
||||
case RuntimeCoordinatorRenderResetScope::TemporalHistoryAndFeedback:
|
||||
return RuntimeEventRenderResetScope::TemporalHistoryAndFeedback;
|
||||
case RuntimeCoordinatorRenderResetScope::None:
|
||||
default:
|
||||
return RuntimeEventRenderResetScope::None;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
RuntimeCoordinator::RuntimeCoordinator(RuntimeStore& runtimeStore, RuntimeEventDispatcher& runtimeEventDispatcher) :
|
||||
mRuntimeStore(runtimeStore),
|
||||
mRuntimeEventDispatcher(runtimeEventDispatcher)
|
||||
{
|
||||
}
|
||||
|
||||
RuntimeCoordinatorResult RuntimeCoordinator::AddLayer(const std::string& shaderId)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mMutex);
|
||||
std::string error;
|
||||
if (!ValidateShaderExists(shaderId, error))
|
||||
{
|
||||
RuntimeCoordinatorResult result = ApplyStoreMutation(false, error, false, false, false);
|
||||
PublishCoordinatorResult("AddLayer", result);
|
||||
return result;
|
||||
}
|
||||
|
||||
RuntimeCoordinatorResult result = ApplyStoreMutation(mRuntimeStore.CreateStoredLayer(shaderId, error), error, true, true, true);
|
||||
PublishCoordinatorResult("AddLayer", result);
|
||||
return result;
|
||||
}
|
||||
|
||||
RuntimeCoordinatorResult RuntimeCoordinator::RemoveLayer(const std::string& layerId)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mMutex);
|
||||
std::string error;
|
||||
if (!ValidateLayerExists(layerId, error))
|
||||
{
|
||||
RuntimeCoordinatorResult result = ApplyStoreMutation(false, error, false, false, false);
|
||||
PublishCoordinatorResult("RemoveLayer", result);
|
||||
return result;
|
||||
}
|
||||
|
||||
RuntimeCoordinatorResult result = ApplyStoreMutation(mRuntimeStore.DeleteStoredLayer(layerId, error), error, true, true, true);
|
||||
if (result.accepted)
|
||||
{
|
||||
result.transientOscInvalidation = RuntimeCoordinatorTransientOscInvalidation::Layer;
|
||||
result.transientOscLayerKey = layerId;
|
||||
}
|
||||
PublishCoordinatorResult("RemoveLayer", result);
|
||||
return result;
|
||||
}
|
||||
|
||||
RuntimeCoordinatorResult RuntimeCoordinator::MoveLayer(const std::string& layerId, int direction)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mMutex);
|
||||
std::string error;
|
||||
bool shouldMove = false;
|
||||
if (!ResolveLayerMove(layerId, direction, shouldMove, error))
|
||||
{
|
||||
RuntimeCoordinatorResult result = ApplyStoreMutation(false, error, false, false, false);
|
||||
PublishCoordinatorResult("MoveLayer", result);
|
||||
return result;
|
||||
}
|
||||
if (!shouldMove)
|
||||
{
|
||||
RuntimeCoordinatorResult result = BuildAcceptedNoReloadResult();
|
||||
return result;
|
||||
}
|
||||
|
||||
RuntimeCoordinatorResult result = ApplyStoreMutation(mRuntimeStore.MoveStoredLayer(layerId, direction, error), error, true, true, true);
|
||||
PublishCoordinatorResult("MoveLayer", result);
|
||||
return result;
|
||||
}
|
||||
|
||||
RuntimeCoordinatorResult RuntimeCoordinator::MoveLayerToIndex(const std::string& layerId, std::size_t targetIndex)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mMutex);
|
||||
std::string error;
|
||||
bool shouldMove = false;
|
||||
if (!ResolveLayerMoveToIndex(layerId, targetIndex, shouldMove, error))
|
||||
{
|
||||
RuntimeCoordinatorResult result = ApplyStoreMutation(false, error, false, false, false);
|
||||
PublishCoordinatorResult("MoveLayerToIndex", result);
|
||||
return result;
|
||||
}
|
||||
if (!shouldMove)
|
||||
{
|
||||
RuntimeCoordinatorResult result = BuildAcceptedNoReloadResult();
|
||||
return result;
|
||||
}
|
||||
|
||||
RuntimeCoordinatorResult result = ApplyStoreMutation(mRuntimeStore.MoveStoredLayerToIndex(layerId, targetIndex, error), error, true, true, true);
|
||||
PublishCoordinatorResult("MoveLayerToIndex", result);
|
||||
return result;
|
||||
}
|
||||
|
||||
RuntimeCoordinatorResult RuntimeCoordinator::SetLayerBypass(const std::string& layerId, bool bypassed)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mMutex);
|
||||
std::string error;
|
||||
if (!ValidateLayerExists(layerId, error))
|
||||
{
|
||||
RuntimeCoordinatorResult result = ApplyStoreMutation(false, error, false, false, false);
|
||||
PublishCoordinatorResult("SetLayerBypass", result);
|
||||
return result;
|
||||
}
|
||||
|
||||
RuntimeCoordinatorResult result = ApplyStoreMutation(mRuntimeStore.SetStoredLayerBypassState(layerId, bypassed, error), error, true, false, true);
|
||||
PublishCoordinatorResult("SetLayerBypass", result);
|
||||
return result;
|
||||
}
|
||||
|
||||
RuntimeCoordinatorResult RuntimeCoordinator::SetLayerShader(const std::string& layerId, const std::string& shaderId)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mMutex);
|
||||
std::string error;
|
||||
if (!ValidateLayerExists(layerId, error) || !ValidateShaderExists(shaderId, error))
|
||||
{
|
||||
RuntimeCoordinatorResult result = ApplyStoreMutation(false, error, false, false, false);
|
||||
PublishCoordinatorResult("SetLayerShader", result);
|
||||
return result;
|
||||
}
|
||||
|
||||
RuntimeCoordinatorResult result = ApplyStoreMutation(mRuntimeStore.SetStoredLayerShaderSelection(layerId, shaderId, error), error, true, false, true);
|
||||
PublishCoordinatorResult("SetLayerShader", result);
|
||||
return result;
|
||||
}
|
||||
|
||||
RuntimeCoordinatorResult RuntimeCoordinator::UpdateLayerParameter(const std::string& layerId, const std::string& parameterId, const JsonValue& newValue)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mMutex);
|
||||
std::string error;
|
||||
ResolvedParameterMutation mutation;
|
||||
if (!BuildParameterMutationById(layerId, parameterId, newValue, true, mutation, error))
|
||||
{
|
||||
RuntimeCoordinatorResult result = ApplyStoreMutation(false, error, false, false, false);
|
||||
PublishCoordinatorResult("UpdateLayerParameter", result);
|
||||
return result;
|
||||
}
|
||||
|
||||
RuntimeCoordinatorResult result = ApplyStoreMutation(mRuntimeStore.SetStoredParameterValue(mutation.layerId, mutation.parameterId, mutation.value, mutation.persistState, error), error, false, false, mutation.persistState);
|
||||
PublishCoordinatorResult("UpdateLayerParameter", result);
|
||||
return result;
|
||||
}
|
||||
|
||||
RuntimeCoordinatorResult RuntimeCoordinator::UpdateLayerParameterByControlKey(const std::string& layerKey, const std::string& parameterKey, const JsonValue& newValue)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mMutex);
|
||||
std::string error;
|
||||
ResolvedParameterMutation mutation;
|
||||
if (!BuildParameterMutationByControlKey(layerKey, parameterKey, newValue, true, mutation, error))
|
||||
{
|
||||
RuntimeCoordinatorResult result = ApplyStoreMutation(false, error, false, false, false);
|
||||
PublishCoordinatorResult("UpdateLayerParameterByControlKey", result);
|
||||
return result;
|
||||
}
|
||||
|
||||
RuntimeCoordinatorResult result = ApplyStoreMutation(mRuntimeStore.SetStoredParameterValue(mutation.layerId, mutation.parameterId, mutation.value, mutation.persistState, error), error, false, false, mutation.persistState);
|
||||
PublishCoordinatorResult("UpdateLayerParameterByControlKey", result);
|
||||
return result;
|
||||
}
|
||||
|
||||
RuntimeCoordinatorResult RuntimeCoordinator::CommitOscParameterByControlKey(const std::string& layerKey, const std::string& parameterKey, const JsonValue& newValue)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mMutex);
|
||||
constexpr RuntimeCoordinatorOscCommitPersistence kDefaultOscCommitPersistence =
|
||||
RuntimeCoordinatorOscCommitPersistence::SessionOnly;
|
||||
constexpr bool kPersistSettledOscCommits =
|
||||
kDefaultOscCommitPersistence == RuntimeCoordinatorOscCommitPersistence::Persistent;
|
||||
|
||||
std::string error;
|
||||
ResolvedParameterMutation mutation;
|
||||
if (!BuildParameterMutationByControlKey(layerKey, parameterKey, newValue, kPersistSettledOscCommits, mutation, error))
|
||||
{
|
||||
RuntimeCoordinatorResult result = ApplyStoreMutation(false, error, false, false, false);
|
||||
PublishCoordinatorResult("CommitOscParameterByControlKey", result);
|
||||
return result;
|
||||
}
|
||||
|
||||
RuntimeCoordinatorResult result = ApplyStoreMutation(mRuntimeStore.SetStoredParameterValue(mutation.layerId, mutation.parameterId, mutation.value, mutation.persistState, error), error, false, false, mutation.persistState);
|
||||
PublishCoordinatorResult("CommitOscParameterByControlKey", result);
|
||||
return result;
|
||||
}
|
||||
|
||||
RuntimeCoordinatorResult RuntimeCoordinator::ResetLayerParameters(const std::string& layerId)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mMutex);
|
||||
std::string error;
|
||||
if (!ValidateLayerExists(layerId, error))
|
||||
{
|
||||
RuntimeCoordinatorResult result = ApplyStoreMutation(false, error, false, false, false);
|
||||
PublishCoordinatorResult("ResetLayerParameters", result);
|
||||
return result;
|
||||
}
|
||||
|
||||
RuntimeCoordinatorResult result = ApplyStoreMutation(mRuntimeStore.ResetStoredLayerParameterValues(layerId, error), error, false, false, true);
|
||||
if (!result.accepted)
|
||||
{
|
||||
PublishCoordinatorResult("ResetLayerParameters", result);
|
||||
return result;
|
||||
}
|
||||
|
||||
result.transientOscInvalidation = RuntimeCoordinatorTransientOscInvalidation::Layer;
|
||||
result.transientOscLayerKey = layerId;
|
||||
result.renderResetScope = RuntimeCoordinatorRenderResetScope::TemporalHistoryAndFeedback;
|
||||
PublishCoordinatorResult("ResetLayerParameters", result);
|
||||
return result;
|
||||
}
|
||||
|
||||
RuntimeCoordinatorResult RuntimeCoordinator::SaveStackPreset(const std::string& presetName)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mMutex);
|
||||
std::string error;
|
||||
if (!ValidatePresetName(presetName, error))
|
||||
{
|
||||
RuntimeCoordinatorResult result = ApplyStoreMutation(false, error, false, false, false);
|
||||
PublishCoordinatorResult("SaveStackPreset", result);
|
||||
return result;
|
||||
}
|
||||
|
||||
RuntimeCoordinatorResult result = ApplyStoreMutation(mRuntimeStore.SaveStackPresetSnapshot(presetName, error), error, false, false, true);
|
||||
PublishCoordinatorResult("SaveStackPreset", result);
|
||||
return result;
|
||||
}
|
||||
|
||||
RuntimeCoordinatorResult RuntimeCoordinator::LoadStackPreset(const std::string& presetName)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mMutex);
|
||||
std::string error;
|
||||
if (!ValidatePresetName(presetName, error))
|
||||
{
|
||||
RuntimeCoordinatorResult result = ApplyStoreMutation(false, error, false, false, false);
|
||||
PublishCoordinatorResult("LoadStackPreset", result);
|
||||
return result;
|
||||
}
|
||||
|
||||
RuntimeCoordinatorResult result = ApplyStoreMutation(mRuntimeStore.LoadStackPresetSnapshot(presetName, error), error, true, false, true);
|
||||
PublishCoordinatorResult("LoadStackPreset", result);
|
||||
return result;
|
||||
}
|
||||
|
||||
RuntimeCoordinatorResult RuntimeCoordinator::RequestShaderReload(bool preserveFeedbackState)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mMutex);
|
||||
PublishManualReloadRequested(preserveFeedbackState, "RequestShaderReload");
|
||||
RuntimeCoordinatorResult result = BuildQueuedReloadResult(preserveFeedbackState);
|
||||
PublishCoordinatorFollowUpEvents("RequestShaderReload", result);
|
||||
return result;
|
||||
}
|
||||
|
||||
RuntimeCoordinatorResult RuntimeCoordinator::PollRuntimeStoreChanges(bool& registryChanged)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mMutex);
|
||||
|
||||
registryChanged = false;
|
||||
bool reloadRequested = false;
|
||||
std::string error;
|
||||
if (!mRuntimeStore.PollStoredFileChanges(registryChanged, reloadRequested, error))
|
||||
{
|
||||
RuntimeCoordinatorResult result = HandleRuntimePollFailure(error);
|
||||
return result;
|
||||
}
|
||||
|
||||
if (reloadRequested)
|
||||
{
|
||||
PublishFileChangeDetected("PollRuntimeStoreChanges", registryChanged, reloadRequested);
|
||||
RuntimeCoordinatorResult result = BuildQueuedReloadResult(false);
|
||||
PublishCoordinatorFollowUpEvents("PollRuntimeStoreChanges", result);
|
||||
return result;
|
||||
}
|
||||
|
||||
if (registryChanged)
|
||||
{
|
||||
PublishFileChangeDetected("PollRuntimeStoreChanges", registryChanged, reloadRequested);
|
||||
RuntimeCoordinatorResult result = BuildAcceptedNoReloadResult();
|
||||
PublishCoordinatorFollowUpEvents("PollRuntimeStoreChanges", result);
|
||||
return result;
|
||||
}
|
||||
|
||||
RuntimeCoordinatorResult result;
|
||||
result.accepted = true;
|
||||
return result;
|
||||
}
|
||||
|
||||
RuntimeCoordinatorResult RuntimeCoordinator::HandleRuntimePollFailure(const std::string& error)
|
||||
{
|
||||
RuntimeCoordinatorResult result;
|
||||
result.accepted = true;
|
||||
result.runtimeStateBroadcastRequired = true;
|
||||
result.compileStatusChanged = true;
|
||||
result.compileStatusSucceeded = false;
|
||||
result.compileStatusMessage = error;
|
||||
PublishCoordinatorFollowUpEvents("HandleRuntimePollFailure", result);
|
||||
return result;
|
||||
}
|
||||
|
||||
RuntimeCoordinatorResult RuntimeCoordinator::HandlePreparedShaderBuildFailure(const std::string& error)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mMutex);
|
||||
mPreserveFeedbackOnNextShaderBuild = false;
|
||||
mUseCommittedLayerStates = true;
|
||||
|
||||
RuntimeCoordinatorResult result;
|
||||
result.accepted = true;
|
||||
result.runtimeStateBroadcastRequired = true;
|
||||
result.compileStatusChanged = true;
|
||||
result.compileStatusSucceeded = false;
|
||||
result.compileStatusMessage = error;
|
||||
result.committedStateMode = RuntimeCoordinatorCommittedStateMode::UseCommittedStates;
|
||||
PublishCoordinatorFollowUpEvents("HandlePreparedShaderBuildFailure", result);
|
||||
return result;
|
||||
}
|
||||
|
||||
RuntimeCoordinatorResult RuntimeCoordinator::HandlePreparedShaderBuildSuccess()
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mMutex);
|
||||
mUseCommittedLayerStates = false;
|
||||
|
||||
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;
|
||||
PublishCoordinatorFollowUpEvents("HandlePreparedShaderBuildSuccess", result);
|
||||
return result;
|
||||
}
|
||||
|
||||
RuntimeCoordinatorResult RuntimeCoordinator::HandleRuntimeReloadRequest()
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mMutex);
|
||||
PublishManualReloadRequested(false, "HandleRuntimeReloadRequest");
|
||||
RuntimeCoordinatorResult result = BuildQueuedReloadResult(false);
|
||||
PublishCoordinatorFollowUpEvents("HandleRuntimeReloadRequest", result);
|
||||
return result;
|
||||
}
|
||||
|
||||
void RuntimeCoordinator::ApplyCommittedStateMode(RuntimeCoordinatorCommittedStateMode mode)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mMutex);
|
||||
switch (mode)
|
||||
{
|
||||
case RuntimeCoordinatorCommittedStateMode::UseCommittedStates:
|
||||
mUseCommittedLayerStates = true;
|
||||
break;
|
||||
case RuntimeCoordinatorCommittedStateMode::UseLiveSnapshots:
|
||||
mUseCommittedLayerStates = false;
|
||||
break;
|
||||
case RuntimeCoordinatorCommittedStateMode::Unchanged:
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
bool RuntimeCoordinator::UseCommittedLayerStates() const
|
||||
{
|
||||
return mUseCommittedLayerStates.load();
|
||||
}
|
||||
|
||||
bool RuntimeCoordinator::PreserveFeedbackOnNextShaderBuild() const
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mMutex);
|
||||
return mPreserveFeedbackOnNextShaderBuild;
|
||||
}
|
||||
|
||||
bool RuntimeCoordinator::BuildParameterMutationById(const std::string& layerId, const std::string& parameterId, const JsonValue& newValue,
|
||||
bool persistState, ResolvedParameterMutation& mutation, std::string& error) const
|
||||
{
|
||||
RuntimeStore::StoredParameterSnapshot snapshot;
|
||||
if (!mRuntimeStore.TryGetStoredParameterById(layerId, parameterId, snapshot, error))
|
||||
return false;
|
||||
|
||||
return BuildParameterMutationFromSnapshot(snapshot.layerId, snapshot.definition, snapshot.currentValue, snapshot.hasCurrentValue,
|
||||
newValue, persistState, mutation, error);
|
||||
}
|
||||
|
||||
bool RuntimeCoordinator::BuildParameterMutationByControlKey(const std::string& layerKey, const std::string& parameterKey, const JsonValue& newValue,
|
||||
bool persistState, ResolvedParameterMutation& mutation, std::string& error) const
|
||||
{
|
||||
RuntimeStore::StoredParameterSnapshot snapshot;
|
||||
if (!mRuntimeStore.TryGetStoredParameterByControlKey(layerKey, parameterKey, snapshot, error))
|
||||
return false;
|
||||
|
||||
return BuildParameterMutationFromSnapshot(snapshot.layerId, snapshot.definition, snapshot.currentValue, snapshot.hasCurrentValue,
|
||||
newValue, persistState, mutation, error);
|
||||
}
|
||||
|
||||
bool RuntimeCoordinator::BuildParameterMutationFromSnapshot(const std::string& layerId, const ShaderParameterDefinition& definition,
|
||||
const ShaderParameterValue& currentValue, bool hasCurrentValue, const JsonValue& newValue,
|
||||
bool persistState, ResolvedParameterMutation& mutation, std::string& error) const
|
||||
{
|
||||
mutation.layerId = layerId;
|
||||
mutation.parameterId = definition.id;
|
||||
mutation.persistState = persistState;
|
||||
|
||||
if (definition.type == ShaderParameterType::Trigger)
|
||||
{
|
||||
const double previousCount = !hasCurrentValue || currentValue.numberValues.empty()
|
||||
? 0.0
|
||||
: currentValue.numberValues[0];
|
||||
const double triggerTime = mRuntimeStore.GetRuntimeElapsedSeconds();
|
||||
mutation.value.numberValues = { previousCount + 1.0, triggerTime };
|
||||
mutation.persistState = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
return NormalizeAndValidateParameterValue(definition, newValue, mutation.value, error);
|
||||
}
|
||||
|
||||
bool RuntimeCoordinator::ValidateLayerExists(const std::string& layerId, std::string& error) const
|
||||
{
|
||||
if (mRuntimeStore.HasStoredLayer(layerId))
|
||||
return true;
|
||||
|
||||
error = "Unknown layer id: " + layerId;
|
||||
return false;
|
||||
}
|
||||
|
||||
bool RuntimeCoordinator::ValidateShaderExists(const std::string& shaderId, std::string& error) const
|
||||
{
|
||||
if (mRuntimeStore.HasStoredShader(shaderId))
|
||||
return true;
|
||||
|
||||
error = "Unknown shader id: " + shaderId;
|
||||
return false;
|
||||
}
|
||||
|
||||
bool RuntimeCoordinator::ResolveLayerMove(const std::string& layerId, int direction, bool& shouldMove, std::string& error) const
|
||||
{
|
||||
return mRuntimeStore.ResolveStoredLayerMove(layerId, direction, shouldMove, error);
|
||||
}
|
||||
|
||||
bool RuntimeCoordinator::ResolveLayerMoveToIndex(const std::string& layerId, std::size_t targetIndex, bool& shouldMove, std::string& error) const
|
||||
{
|
||||
return mRuntimeStore.ResolveStoredLayerMoveToIndex(layerId, targetIndex, shouldMove, error);
|
||||
}
|
||||
|
||||
bool RuntimeCoordinator::ValidatePresetName(const std::string& presetName, std::string& error) const
|
||||
{
|
||||
if (mRuntimeStore.IsValidStackPresetName(presetName))
|
||||
return true;
|
||||
|
||||
error = "Preset name must include at least one letter or number.";
|
||||
return false;
|
||||
}
|
||||
|
||||
RuntimeCoordinatorResult RuntimeCoordinator::ApplyStoreMutation(bool succeeded, const std::string& errorMessage, bool reloadRequired, bool preserveFeedbackState, bool persistenceRequested)
|
||||
{
|
||||
if (!succeeded)
|
||||
{
|
||||
RuntimeCoordinatorResult result;
|
||||
result.accepted = false;
|
||||
result.errorMessage = errorMessage;
|
||||
return result;
|
||||
}
|
||||
|
||||
if (reloadRequired)
|
||||
{
|
||||
RuntimeCoordinatorResult result = BuildQueuedReloadResult(preserveFeedbackState);
|
||||
result.persistenceRequested = persistenceRequested;
|
||||
return result;
|
||||
}
|
||||
|
||||
RuntimeCoordinatorResult result = BuildAcceptedNoReloadResult();
|
||||
result.persistenceRequested = persistenceRequested;
|
||||
return result;
|
||||
}
|
||||
|
||||
RuntimeCoordinatorResult RuntimeCoordinator::BuildQueuedReloadResult(bool preserveFeedbackState)
|
||||
{
|
||||
mPreserveFeedbackOnNextShaderBuild = preserveFeedbackState;
|
||||
mUseCommittedLayerStates = true;
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
void RuntimeCoordinator::PublishFileChangeDetected(const std::string& reason, bool registryChanged, bool reloadRequested) const
|
||||
{
|
||||
try
|
||||
{
|
||||
FileChangeDetectedEvent event;
|
||||
event.path = reason;
|
||||
event.shaderPackageCandidate = registryChanged || reloadRequested;
|
||||
event.runtimeConfigCandidate = false;
|
||||
event.presetCandidate = false;
|
||||
mRuntimeEventDispatcher.PublishPayload(event, "RuntimeCoordinator");
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
void RuntimeCoordinator::PublishManualReloadRequested(bool preserveFeedbackState, const std::string& reason) const
|
||||
{
|
||||
try
|
||||
{
|
||||
ManualReloadRequestedEvent event;
|
||||
event.preserveFeedbackState = preserveFeedbackState;
|
||||
event.reason = reason;
|
||||
mRuntimeEventDispatcher.PublishPayload(event, "RuntimeCoordinator");
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
void RuntimeCoordinator::PublishCoordinatorResult(const std::string& action, const RuntimeCoordinatorResult& result) const
|
||||
{
|
||||
try
|
||||
{
|
||||
RuntimeMutationEvent mutation;
|
||||
mutation.action = action;
|
||||
mutation.accepted = result.accepted;
|
||||
mutation.runtimeStateChanged = result.accepted && result.runtimeStateBroadcastRequired;
|
||||
mutation.runtimeStateBroadcastRequired = result.runtimeStateBroadcastRequired;
|
||||
mutation.shaderBuildRequested = result.shaderBuildRequested;
|
||||
mutation.persistenceRequested = result.persistenceRequested;
|
||||
mutation.clearTransientOscState = result.transientOscInvalidation != RuntimeCoordinatorTransientOscInvalidation::None;
|
||||
mutation.renderResetScope = ToRuntimeEventRenderResetScope(result.renderResetScope);
|
||||
mutation.errorMessage = result.errorMessage;
|
||||
mRuntimeEventDispatcher.PublishPayload(mutation, "RuntimeCoordinator");
|
||||
|
||||
PublishCoordinatorFollowUpEvents(action, result);
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
void RuntimeCoordinator::PublishCoordinatorFollowUpEvents(const std::string& action, const RuntimeCoordinatorResult& result) const
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!result.accepted)
|
||||
return;
|
||||
|
||||
if (result.runtimeStateBroadcastRequired)
|
||||
{
|
||||
RuntimeStateChangedEvent stateChanged;
|
||||
stateChanged.reason = action;
|
||||
stateChanged.renderVisible = result.renderResetScope != RuntimeCoordinatorRenderResetScope::None;
|
||||
stateChanged.persistenceRequested = result.persistenceRequested;
|
||||
mRuntimeEventDispatcher.PublishPayload(stateChanged, "RuntimeCoordinator");
|
||||
}
|
||||
|
||||
if (result.persistenceRequested)
|
||||
{
|
||||
RuntimePersistenceRequestedEvent persistenceRequested;
|
||||
persistenceRequested.reason = action;
|
||||
persistenceRequested.debounceAllowed = true;
|
||||
mRuntimeEventDispatcher.PublishPayload(persistenceRequested, "RuntimeCoordinator");
|
||||
}
|
||||
|
||||
if (result.shaderBuildRequested)
|
||||
{
|
||||
RuntimeReloadRequestedEvent reloadRequested;
|
||||
reloadRequested.preserveFeedbackState = mPreserveFeedbackOnNextShaderBuild;
|
||||
reloadRequested.reason = action;
|
||||
mRuntimeEventDispatcher.PublishPayload(reloadRequested, "RuntimeCoordinator");
|
||||
|
||||
ShaderBuildEvent shaderBuild;
|
||||
shaderBuild.phase = RuntimeEventShaderBuildPhase::Requested;
|
||||
shaderBuild.preserveFeedbackState = mPreserveFeedbackOnNextShaderBuild;
|
||||
shaderBuild.succeeded = true;
|
||||
shaderBuild.message = result.compileStatusMessage;
|
||||
mRuntimeEventDispatcher.PublishPayload(shaderBuild, "RuntimeCoordinator");
|
||||
}
|
||||
|
||||
if (result.compileStatusChanged)
|
||||
{
|
||||
CompileStatusChangedEvent compileStatus;
|
||||
compileStatus.succeeded = result.compileStatusSucceeded;
|
||||
compileStatus.message = result.compileStatusMessage;
|
||||
mRuntimeEventDispatcher.PublishPayload(compileStatus, "RuntimeCoordinator");
|
||||
}
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
#pragma once
|
||||
|
||||
#include "RuntimeJson.h"
|
||||
#include "ShaderTypes.h"
|
||||
|
||||
#include <atomic>
|
||||
#include <cstddef>
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
|
||||
class RuntimeStore;
|
||||
class RuntimeEventDispatcher;
|
||||
|
||||
enum class RuntimeCoordinatorCommittedStateMode
|
||||
{
|
||||
Unchanged,
|
||||
UseCommittedStates,
|
||||
UseLiveSnapshots
|
||||
};
|
||||
|
||||
enum class RuntimeCoordinatorRenderResetScope
|
||||
{
|
||||
None,
|
||||
TemporalHistoryOnly,
|
||||
TemporalHistoryAndFeedback
|
||||
};
|
||||
|
||||
enum class RuntimeCoordinatorTransientOscInvalidation
|
||||
{
|
||||
None,
|
||||
Layer,
|
||||
All
|
||||
};
|
||||
|
||||
enum class RuntimeCoordinatorOscCommitPersistence
|
||||
{
|
||||
SessionOnly,
|
||||
Persistent
|
||||
};
|
||||
|
||||
struct RuntimeCoordinatorResult
|
||||
{
|
||||
bool accepted = false;
|
||||
bool runtimeStateBroadcastRequired = false;
|
||||
bool shaderBuildRequested = false;
|
||||
bool persistenceRequested = false;
|
||||
bool compileStatusChanged = false;
|
||||
bool compileStatusSucceeded = false;
|
||||
bool clearReloadRequest = false;
|
||||
RuntimeCoordinatorCommittedStateMode committedStateMode = RuntimeCoordinatorCommittedStateMode::Unchanged;
|
||||
RuntimeCoordinatorRenderResetScope renderResetScope = RuntimeCoordinatorRenderResetScope::None;
|
||||
RuntimeCoordinatorTransientOscInvalidation transientOscInvalidation = RuntimeCoordinatorTransientOscInvalidation::None;
|
||||
std::string transientOscLayerKey;
|
||||
std::string compileStatusMessage;
|
||||
std::string errorMessage;
|
||||
};
|
||||
|
||||
class RuntimeCoordinator
|
||||
{
|
||||
public:
|
||||
RuntimeCoordinator(RuntimeStore& runtimeStore, RuntimeEventDispatcher& runtimeEventDispatcher);
|
||||
|
||||
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 CommitOscParameterByControlKey(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 PollRuntimeStoreChanges(bool& registryChanged);
|
||||
RuntimeCoordinatorResult HandleRuntimePollFailure(const std::string& error);
|
||||
RuntimeCoordinatorResult HandlePreparedShaderBuildFailure(const std::string& error);
|
||||
RuntimeCoordinatorResult HandlePreparedShaderBuildSuccess();
|
||||
RuntimeCoordinatorResult HandleRuntimeReloadRequest();
|
||||
void ApplyCommittedStateMode(RuntimeCoordinatorCommittedStateMode mode);
|
||||
bool UseCommittedLayerStates() const;
|
||||
bool PreserveFeedbackOnNextShaderBuild() const;
|
||||
|
||||
private:
|
||||
struct ResolvedParameterMutation
|
||||
{
|
||||
std::string layerId;
|
||||
std::string parameterId;
|
||||
ShaderParameterValue value;
|
||||
bool persistState = true;
|
||||
};
|
||||
|
||||
bool BuildParameterMutationById(const std::string& layerId, const std::string& parameterId, const JsonValue& newValue,
|
||||
bool persistState, ResolvedParameterMutation& mutation, std::string& error) const;
|
||||
bool BuildParameterMutationByControlKey(const std::string& layerKey, const std::string& parameterKey, const JsonValue& newValue,
|
||||
bool persistState, ResolvedParameterMutation& mutation, std::string& error) const;
|
||||
bool BuildParameterMutationFromSnapshot(const std::string& layerId, const ShaderParameterDefinition& definition,
|
||||
const ShaderParameterValue& currentValue, bool hasCurrentValue, const JsonValue& newValue,
|
||||
bool persistState, ResolvedParameterMutation& mutation, std::string& error) const;
|
||||
bool ValidateLayerExists(const std::string& layerId, std::string& error) const;
|
||||
bool ValidateShaderExists(const std::string& shaderId, std::string& error) const;
|
||||
bool ResolveLayerMove(const std::string& layerId, int direction, bool& shouldMove, std::string& error) const;
|
||||
bool ResolveLayerMoveToIndex(const std::string& layerId, std::size_t targetIndex, bool& shouldMove, std::string& error) const;
|
||||
bool ValidatePresetName(const std::string& presetName, std::string& error) const;
|
||||
RuntimeCoordinatorResult ApplyStoreMutation(bool succeeded, const std::string& errorMessage, bool reloadRequired, bool preserveFeedbackState, bool persistenceRequested);
|
||||
RuntimeCoordinatorResult BuildQueuedReloadResult(bool preserveFeedbackState);
|
||||
RuntimeCoordinatorResult BuildAcceptedNoReloadResult() const;
|
||||
void PublishFileChangeDetected(const std::string& reason, bool registryChanged, bool reloadRequested) const;
|
||||
void PublishManualReloadRequested(bool preserveFeedbackState, const std::string& reason) const;
|
||||
void PublishCoordinatorResult(const std::string& action, const RuntimeCoordinatorResult& result) const;
|
||||
void PublishCoordinatorFollowUpEvents(const std::string& action, const RuntimeCoordinatorResult& result) const;
|
||||
|
||||
RuntimeStore& mRuntimeStore;
|
||||
RuntimeEventDispatcher& mRuntimeEventDispatcher;
|
||||
mutable std::mutex mMutex;
|
||||
bool mPreserveFeedbackOnNextShaderBuild = false;
|
||||
std::atomic<bool> mUseCommittedLayerStates{ false };
|
||||
};
|
||||
@@ -0,0 +1,87 @@
|
||||
#pragma once
|
||||
|
||||
#include "RuntimeEventPayloads.h"
|
||||
|
||||
#include <chrono>
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <type_traits>
|
||||
#include <utility>
|
||||
#include <variant>
|
||||
|
||||
using RuntimeEventPayload = std::variant<
|
||||
std::monostate,
|
||||
OscValueReceivedEvent,
|
||||
OscValueCoalescedEvent,
|
||||
OscCommitRequestedEvent,
|
||||
HttpControlMutationRequestedEvent,
|
||||
WebSocketClientConnectedEvent,
|
||||
RuntimeStateBroadcastRequestedEvent,
|
||||
FileChangeDetectedEvent,
|
||||
ManualReloadRequestedEvent,
|
||||
RuntimeMutationEvent,
|
||||
RuntimeStateChangedEvent,
|
||||
RuntimePersistenceRequestedEvent,
|
||||
RuntimeReloadRequestedEvent,
|
||||
ShaderPackagesChangedEvent,
|
||||
RenderSnapshotPublishRequestedEvent,
|
||||
RuntimeStatePresentationChangedEvent,
|
||||
ShaderBuildEvent,
|
||||
CompileStatusChangedEvent,
|
||||
RenderSnapshotPublishedEvent,
|
||||
RenderResetEvent,
|
||||
OscOverlayEvent,
|
||||
FrameRenderedEvent,
|
||||
PreviewFrameAvailableEvent,
|
||||
InputSignalChangedEvent,
|
||||
InputFrameArrivedEvent,
|
||||
OutputFrameScheduledEvent,
|
||||
OutputFrameCompletedEvent,
|
||||
BackendStateChangedEvent,
|
||||
SubsystemWarningEvent,
|
||||
SubsystemRecoveredEvent,
|
||||
TimingSampleRecordedEvent,
|
||||
QueueDepthChangedEvent>;
|
||||
|
||||
inline RuntimeEventType RuntimeEventPayloadType(const RuntimeEventPayload& payload)
|
||||
{
|
||||
return std::visit([](const auto& value) -> RuntimeEventType {
|
||||
using PayloadType = std::decay_t<decltype(value)>;
|
||||
if constexpr (std::is_same_v<PayloadType, std::monostate>)
|
||||
return RuntimeEventType::Unknown;
|
||||
else
|
||||
return RuntimeEventPayloadType(value);
|
||||
}, payload);
|
||||
}
|
||||
|
||||
struct RuntimeEvent
|
||||
{
|
||||
RuntimeEventType type = RuntimeEventType::Unknown;
|
||||
uint64_t sequence = 0;
|
||||
std::chrono::steady_clock::time_point createdAt = std::chrono::steady_clock::now();
|
||||
std::string source;
|
||||
RuntimeEventPayload payload;
|
||||
|
||||
bool HasPayload() const
|
||||
{
|
||||
return !std::holds_alternative<std::monostate>(payload);
|
||||
}
|
||||
|
||||
bool PayloadMatchesType() const
|
||||
{
|
||||
return RuntimeEventPayloadType(payload) == type;
|
||||
}
|
||||
};
|
||||
|
||||
template <typename Payload>
|
||||
RuntimeEvent MakeRuntimeEvent(Payload payload, std::string source = {}, uint64_t sequence = 0,
|
||||
std::chrono::steady_clock::time_point createdAt = std::chrono::steady_clock::now())
|
||||
{
|
||||
RuntimeEvent event;
|
||||
event.type = RuntimeEventPayloadType(payload);
|
||||
event.sequence = sequence;
|
||||
event.createdAt = createdAt;
|
||||
event.source = std::move(source);
|
||||
event.payload = std::move(payload);
|
||||
return event;
|
||||
}
|
||||
@@ -0,0 +1,158 @@
|
||||
#pragma once
|
||||
|
||||
#include "RuntimeEvent.h"
|
||||
|
||||
#include <chrono>
|
||||
#include <cstddef>
|
||||
#include <deque>
|
||||
#include <functional>
|
||||
#include <map>
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
struct RuntimeEventCoalescingQueueMetrics
|
||||
{
|
||||
std::size_t depth = 0;
|
||||
std::size_t capacity = 0;
|
||||
std::size_t droppedCount = 0;
|
||||
std::size_t coalescedCount = 0;
|
||||
double oldestEventAgeMilliseconds = 0.0;
|
||||
};
|
||||
|
||||
inline std::string RuntimeEventDefaultCoalescingKey(const RuntimeEvent& event)
|
||||
{
|
||||
if (const auto* payload = std::get_if<OscValueReceivedEvent>(&event.payload))
|
||||
return std::string(RuntimeEventTypeName(event.type)) + ":" + payload->routeKey;
|
||||
if (const auto* payload = std::get_if<OscCommitRequestedEvent>(&event.payload))
|
||||
return std::string(RuntimeEventTypeName(event.type)) + ":" + payload->routeKey;
|
||||
if (const auto* payload = std::get_if<FileChangeDetectedEvent>(&event.payload))
|
||||
return std::string(RuntimeEventTypeName(event.type)) + ":" + payload->path;
|
||||
if (const auto* payload = std::get_if<ShaderBuildEvent>(&event.payload))
|
||||
return std::string(RuntimeEventTypeName(event.type)) + ":" +
|
||||
std::to_string(payload->inputWidth) + "x" +
|
||||
std::to_string(payload->inputHeight) + ":" +
|
||||
(payload->preserveFeedbackState ? "preserve" : "reset");
|
||||
if (const auto* payload = std::get_if<RenderSnapshotPublishRequestedEvent>(&event.payload))
|
||||
return std::string(RuntimeEventTypeName(event.type)) + ":" +
|
||||
std::to_string(payload->outputWidth) + "x" +
|
||||
std::to_string(payload->outputHeight);
|
||||
if (const auto* payload = std::get_if<TimingSampleRecordedEvent>(&event.payload))
|
||||
return std::string(RuntimeEventTypeName(event.type)) + ":" + payload->subsystem + ":" + payload->metric;
|
||||
if (const auto* payload = std::get_if<QueueDepthChangedEvent>(&event.payload))
|
||||
return std::string(RuntimeEventTypeName(event.type)) + ":" + payload->queueName;
|
||||
|
||||
return std::string(RuntimeEventTypeName(event.type));
|
||||
}
|
||||
|
||||
class RuntimeEventCoalescingQueue
|
||||
{
|
||||
public:
|
||||
using KeySelector = std::function<std::string(const RuntimeEvent&)>;
|
||||
|
||||
explicit RuntimeEventCoalescingQueue(std::size_t capacity = 256, KeySelector keySelector = RuntimeEventDefaultCoalescingKey) :
|
||||
mCapacity(capacity),
|
||||
mKeySelector(std::move(keySelector))
|
||||
{
|
||||
}
|
||||
|
||||
bool Push(RuntimeEvent event)
|
||||
{
|
||||
const std::string key = mKeySelector(event);
|
||||
if (key.empty())
|
||||
return false;
|
||||
|
||||
std::lock_guard<std::mutex> lock(mMutex);
|
||||
auto found = mEntries.find(key);
|
||||
if (found != mEntries.end())
|
||||
{
|
||||
const auto firstCreatedAt = found->second.event.createdAt;
|
||||
found->second.event = std::move(event);
|
||||
found->second.event.createdAt = firstCreatedAt;
|
||||
++found->second.coalescedCount;
|
||||
++mCoalescedCount;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (mEntries.size() >= mCapacity)
|
||||
{
|
||||
++mDroppedCount;
|
||||
return false;
|
||||
}
|
||||
|
||||
mOrder.push_back(key);
|
||||
Entry entry;
|
||||
entry.event = std::move(event);
|
||||
mEntries.emplace(key, std::move(entry));
|
||||
return true;
|
||||
}
|
||||
|
||||
std::vector<RuntimeEvent> Drain(std::size_t maxEvents = 0)
|
||||
{
|
||||
std::vector<RuntimeEvent> events;
|
||||
|
||||
std::lock_guard<std::mutex> lock(mMutex);
|
||||
const std::size_t count = maxEvents == 0 || maxEvents > mOrder.size() ? mOrder.size() : maxEvents;
|
||||
events.reserve(count);
|
||||
|
||||
for (std::size_t index = 0; index < count; ++index)
|
||||
{
|
||||
const std::string key = std::move(mOrder.front());
|
||||
mOrder.pop_front();
|
||||
|
||||
auto found = mEntries.find(key);
|
||||
if (found == mEntries.end())
|
||||
continue;
|
||||
|
||||
events.push_back(std::move(found->second.event));
|
||||
mEntries.erase(found);
|
||||
}
|
||||
|
||||
return events;
|
||||
}
|
||||
|
||||
RuntimeEventCoalescingQueueMetrics GetMetrics(std::chrono::steady_clock::time_point now = std::chrono::steady_clock::now()) const
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mMutex);
|
||||
|
||||
RuntimeEventCoalescingQueueMetrics metrics;
|
||||
metrics.depth = mEntries.size();
|
||||
metrics.capacity = mCapacity;
|
||||
metrics.droppedCount = mDroppedCount;
|
||||
metrics.coalescedCount = mCoalescedCount;
|
||||
|
||||
if (!mOrder.empty())
|
||||
{
|
||||
const auto found = mEntries.find(mOrder.front());
|
||||
if (found != mEntries.end())
|
||||
{
|
||||
const auto age = now - found->second.event.createdAt;
|
||||
metrics.oldestEventAgeMilliseconds = std::chrono::duration<double, std::milli>(age).count();
|
||||
}
|
||||
}
|
||||
|
||||
return metrics;
|
||||
}
|
||||
|
||||
std::size_t Depth() const
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mMutex);
|
||||
return mEntries.size();
|
||||
}
|
||||
|
||||
private:
|
||||
struct Entry
|
||||
{
|
||||
RuntimeEvent event;
|
||||
std::size_t coalescedCount = 0;
|
||||
};
|
||||
|
||||
mutable std::mutex mMutex;
|
||||
std::size_t mCapacity = 0;
|
||||
KeySelector mKeySelector;
|
||||
std::deque<std::string> mOrder;
|
||||
std::map<std::string, Entry> mEntries;
|
||||
std::size_t mDroppedCount = 0;
|
||||
std::size_t mCoalescedCount = 0;
|
||||
};
|
||||
@@ -0,0 +1,170 @@
|
||||
#pragma once
|
||||
|
||||
#include "RuntimeEventCoalescingQueue.h"
|
||||
#include "RuntimeEventQueue.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <atomic>
|
||||
#include <functional>
|
||||
#include <map>
|
||||
#include <mutex>
|
||||
#include <vector>
|
||||
|
||||
struct RuntimeEventDispatchResult
|
||||
{
|
||||
std::size_t dispatchedEvents = 0;
|
||||
std::size_t handlerInvocations = 0;
|
||||
std::size_t handlerFailures = 0;
|
||||
double dispatchDurationMilliseconds = 0.0;
|
||||
};
|
||||
|
||||
class RuntimeEventDispatcher
|
||||
{
|
||||
public:
|
||||
using Handler = std::function<void(const RuntimeEvent&)>;
|
||||
|
||||
explicit RuntimeEventDispatcher(std::size_t queueCapacity = 1024) :
|
||||
mQueue(queueCapacity),
|
||||
mCoalescingQueue(queueCapacity)
|
||||
{
|
||||
}
|
||||
|
||||
bool Publish(RuntimeEvent event)
|
||||
{
|
||||
if (!event.PayloadMatchesType())
|
||||
return false;
|
||||
|
||||
if (event.sequence == 0)
|
||||
event.sequence = mNextSequence.fetch_add(1);
|
||||
|
||||
if (ShouldCoalesce(event))
|
||||
return mCoalescingQueue.Push(std::move(event));
|
||||
|
||||
return mQueue.Push(std::move(event));
|
||||
}
|
||||
|
||||
template <typename Payload>
|
||||
bool PublishPayload(Payload payload, std::string source = {})
|
||||
{
|
||||
return Publish(MakeRuntimeEvent(std::move(payload), std::move(source)));
|
||||
}
|
||||
|
||||
void Subscribe(RuntimeEventType type, Handler handler)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mHandlerMutex);
|
||||
mHandlers[type].push_back(std::move(handler));
|
||||
}
|
||||
|
||||
void SubscribeAll(Handler handler)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mHandlerMutex);
|
||||
mAllHandlers.push_back(std::move(handler));
|
||||
}
|
||||
|
||||
RuntimeEventDispatchResult DispatchPending(std::size_t maxEvents = 0)
|
||||
{
|
||||
const auto startedAt = std::chrono::steady_clock::now();
|
||||
RuntimeEventDispatchResult result;
|
||||
FlushCoalescedToFifo(maxEvents);
|
||||
std::vector<RuntimeEvent> events = mQueue.Drain(maxEvents);
|
||||
result.dispatchedEvents = events.size();
|
||||
|
||||
for (const RuntimeEvent& event : events)
|
||||
{
|
||||
std::vector<Handler> handlers = HandlersFor(event.type);
|
||||
result.handlerInvocations += handlers.size();
|
||||
|
||||
for (const Handler& handler : handlers)
|
||||
{
|
||||
try
|
||||
{
|
||||
handler(event);
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
++result.handlerFailures;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
result.dispatchDurationMilliseconds =
|
||||
std::chrono::duration<double, std::milli>(std::chrono::steady_clock::now() - startedAt).count();
|
||||
return result;
|
||||
}
|
||||
|
||||
bool TryPop(RuntimeEvent& event)
|
||||
{
|
||||
return mQueue.TryPop(event);
|
||||
}
|
||||
|
||||
RuntimeEventQueueMetrics GetQueueMetrics(std::chrono::steady_clock::time_point now = std::chrono::steady_clock::now()) const
|
||||
{
|
||||
RuntimeEventQueueMetrics metrics = mQueue.GetMetrics(now);
|
||||
const RuntimeEventCoalescingQueueMetrics coalescingMetrics = mCoalescingQueue.GetMetrics(now);
|
||||
if (metrics.depth == 0)
|
||||
metrics.oldestEventAgeMilliseconds = coalescingMetrics.oldestEventAgeMilliseconds;
|
||||
else if (coalescingMetrics.depth > 0)
|
||||
metrics.oldestEventAgeMilliseconds = (std::max)(metrics.oldestEventAgeMilliseconds, coalescingMetrics.oldestEventAgeMilliseconds);
|
||||
metrics.depth += coalescingMetrics.depth;
|
||||
metrics.capacity += coalescingMetrics.capacity;
|
||||
metrics.droppedCount += coalescingMetrics.droppedCount;
|
||||
metrics.coalescedCount = coalescingMetrics.coalescedCount;
|
||||
return metrics;
|
||||
}
|
||||
|
||||
std::size_t QueueDepth() const
|
||||
{
|
||||
return mQueue.Depth() + mCoalescingQueue.Depth();
|
||||
}
|
||||
|
||||
private:
|
||||
static bool ShouldCoalesce(const RuntimeEvent& event)
|
||||
{
|
||||
switch (event.type)
|
||||
{
|
||||
case RuntimeEventType::OscValueReceived:
|
||||
case RuntimeEventType::OscCommitRequested:
|
||||
case RuntimeEventType::RuntimeStateBroadcastRequested:
|
||||
case RuntimeEventType::FileChangeDetected:
|
||||
case RuntimeEventType::RuntimeReloadRequested:
|
||||
case RuntimeEventType::ShaderBuildRequested:
|
||||
case RuntimeEventType::RenderSnapshotPublishRequested:
|
||||
case RuntimeEventType::TimingSampleRecorded:
|
||||
case RuntimeEventType::QueueDepthChanged:
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
void FlushCoalescedToFifo(std::size_t maxEvents)
|
||||
{
|
||||
const std::size_t fifoDepth = mQueue.Depth();
|
||||
if (maxEvents != 0 && fifoDepth >= maxEvents)
|
||||
return;
|
||||
|
||||
const std::size_t flushLimit = maxEvents == 0 ? 0 : maxEvents - fifoDepth;
|
||||
std::vector<RuntimeEvent> events = mCoalescingQueue.Drain(flushLimit);
|
||||
for (RuntimeEvent& event : events)
|
||||
mQueue.Push(std::move(event));
|
||||
}
|
||||
|
||||
std::vector<Handler> HandlersFor(RuntimeEventType type) const
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mHandlerMutex);
|
||||
std::vector<Handler> handlers = mAllHandlers;
|
||||
|
||||
const auto found = mHandlers.find(type);
|
||||
if (found != mHandlers.end())
|
||||
handlers.insert(handlers.end(), found->second.begin(), found->second.end());
|
||||
|
||||
return handlers;
|
||||
}
|
||||
|
||||
RuntimeEventQueue mQueue;
|
||||
RuntimeEventCoalescingQueue mCoalescingQueue;
|
||||
std::atomic<uint64_t> mNextSequence{ 1 };
|
||||
mutable std::mutex mHandlerMutex;
|
||||
std::map<RuntimeEventType, std::vector<Handler>> mHandlers;
|
||||
std::vector<Handler> mAllHandlers;
|
||||
};
|
||||
@@ -0,0 +1,441 @@
|
||||
#pragma once
|
||||
|
||||
#include "RuntimeEventType.h"
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
|
||||
enum class RuntimeEventSeverity
|
||||
{
|
||||
Debug,
|
||||
Info,
|
||||
Warning,
|
||||
Error
|
||||
};
|
||||
|
||||
enum class RuntimeEventRenderResetScope
|
||||
{
|
||||
None,
|
||||
TemporalHistoryOnly,
|
||||
TemporalHistoryAndFeedback
|
||||
};
|
||||
|
||||
enum class RuntimeEventShaderBuildPhase
|
||||
{
|
||||
Requested,
|
||||
Prepared,
|
||||
Applied,
|
||||
Failed
|
||||
};
|
||||
|
||||
struct OscValueReceivedEvent
|
||||
{
|
||||
std::string routeKey;
|
||||
std::string layerKey;
|
||||
std::string parameterKey;
|
||||
std::string valueJson;
|
||||
uint64_t generation = 0;
|
||||
};
|
||||
|
||||
struct OscValueCoalescedEvent
|
||||
{
|
||||
std::string routeKey;
|
||||
std::size_t coalescedCount = 0;
|
||||
uint64_t latestGeneration = 0;
|
||||
};
|
||||
|
||||
struct OscCommitRequestedEvent
|
||||
{
|
||||
std::string routeKey;
|
||||
std::string layerKey;
|
||||
std::string parameterKey;
|
||||
std::string valueJson;
|
||||
uint64_t generation = 0;
|
||||
};
|
||||
|
||||
struct HttpControlMutationRequestedEvent
|
||||
{
|
||||
std::string method;
|
||||
std::string path;
|
||||
std::string bodyJson;
|
||||
};
|
||||
|
||||
struct WebSocketClientConnectedEvent
|
||||
{
|
||||
std::string clientId;
|
||||
std::size_t connectedClientCount = 0;
|
||||
};
|
||||
|
||||
struct RuntimeStateBroadcastRequestedEvent
|
||||
{
|
||||
std::string reason;
|
||||
bool coalescable = true;
|
||||
};
|
||||
|
||||
struct FileChangeDetectedEvent
|
||||
{
|
||||
std::string path;
|
||||
bool shaderPackageCandidate = false;
|
||||
bool runtimeConfigCandidate = false;
|
||||
bool presetCandidate = false;
|
||||
};
|
||||
|
||||
struct ManualReloadRequestedEvent
|
||||
{
|
||||
bool preserveFeedbackState = false;
|
||||
std::string reason;
|
||||
};
|
||||
|
||||
struct RuntimeMutationEvent
|
||||
{
|
||||
std::string action;
|
||||
bool accepted = false;
|
||||
bool runtimeStateChanged = false;
|
||||
bool runtimeStateBroadcastRequired = false;
|
||||
bool shaderBuildRequested = false;
|
||||
bool persistenceRequested = false;
|
||||
bool clearTransientOscState = false;
|
||||
RuntimeEventRenderResetScope renderResetScope = RuntimeEventRenderResetScope::None;
|
||||
std::string errorMessage;
|
||||
};
|
||||
|
||||
struct RuntimeStateChangedEvent
|
||||
{
|
||||
std::string reason;
|
||||
bool renderVisible = false;
|
||||
bool persistenceRequested = false;
|
||||
};
|
||||
|
||||
struct RuntimePersistenceRequestedEvent
|
||||
{
|
||||
std::string reason;
|
||||
bool debounceAllowed = true;
|
||||
};
|
||||
|
||||
struct RuntimeReloadRequestedEvent
|
||||
{
|
||||
bool preserveFeedbackState = false;
|
||||
std::string reason;
|
||||
};
|
||||
|
||||
struct ShaderPackagesChangedEvent
|
||||
{
|
||||
bool registryChanged = false;
|
||||
std::size_t packageCount = 0;
|
||||
std::string reason;
|
||||
};
|
||||
|
||||
struct RenderSnapshotPublishRequestedEvent
|
||||
{
|
||||
unsigned inputWidth = 0;
|
||||
unsigned inputHeight = 0;
|
||||
unsigned outputWidth = 0;
|
||||
unsigned outputHeight = 0;
|
||||
std::string reason;
|
||||
};
|
||||
|
||||
struct RuntimeStatePresentationChangedEvent
|
||||
{
|
||||
std::string reason;
|
||||
};
|
||||
|
||||
struct ShaderBuildEvent
|
||||
{
|
||||
RuntimeEventShaderBuildPhase phase = RuntimeEventShaderBuildPhase::Requested;
|
||||
uint64_t generation = 0;
|
||||
unsigned inputWidth = 0;
|
||||
unsigned inputHeight = 0;
|
||||
bool preserveFeedbackState = false;
|
||||
bool succeeded = false;
|
||||
std::string message;
|
||||
};
|
||||
|
||||
struct CompileStatusChangedEvent
|
||||
{
|
||||
bool succeeded = false;
|
||||
std::string message;
|
||||
};
|
||||
|
||||
struct RenderSnapshotPublishedEvent
|
||||
{
|
||||
uint64_t snapshotVersion = 0;
|
||||
uint64_t structureVersion = 0;
|
||||
uint64_t parameterVersion = 0;
|
||||
uint64_t packageVersion = 0;
|
||||
unsigned outputWidth = 0;
|
||||
unsigned outputHeight = 0;
|
||||
std::size_t layerCount = 0;
|
||||
};
|
||||
|
||||
struct RenderResetEvent
|
||||
{
|
||||
RuntimeEventRenderResetScope scope = RuntimeEventRenderResetScope::None;
|
||||
bool applied = false;
|
||||
std::string reason;
|
||||
};
|
||||
|
||||
struct OscOverlayEvent
|
||||
{
|
||||
std::string routeKey;
|
||||
std::string layerKey;
|
||||
std::string parameterKey;
|
||||
uint64_t generation = 0;
|
||||
bool settled = false;
|
||||
};
|
||||
|
||||
struct FrameRenderedEvent
|
||||
{
|
||||
uint64_t frameIndex = 0;
|
||||
double renderMilliseconds = 0.0;
|
||||
};
|
||||
|
||||
struct PreviewFrameAvailableEvent
|
||||
{
|
||||
uint64_t frameIndex = 0;
|
||||
unsigned width = 0;
|
||||
unsigned height = 0;
|
||||
};
|
||||
|
||||
struct InputSignalChangedEvent
|
||||
{
|
||||
bool hasSignal = false;
|
||||
unsigned width = 0;
|
||||
unsigned height = 0;
|
||||
std::string modeName;
|
||||
};
|
||||
|
||||
struct InputFrameArrivedEvent
|
||||
{
|
||||
uint64_t frameIndex = 0;
|
||||
unsigned width = 0;
|
||||
unsigned height = 0;
|
||||
long rowBytes = 0;
|
||||
std::string pixelFormat;
|
||||
bool hasNoInputSource = false;
|
||||
};
|
||||
|
||||
struct OutputFrameScheduledEvent
|
||||
{
|
||||
uint64_t frameIndex = 0;
|
||||
int64_t streamTime = 0;
|
||||
int64_t duration = 0;
|
||||
int64_t timeScale = 0;
|
||||
};
|
||||
|
||||
struct OutputFrameCompletedEvent
|
||||
{
|
||||
uint64_t frameIndex = 0;
|
||||
std::string result;
|
||||
};
|
||||
|
||||
struct BackendStateChangedEvent
|
||||
{
|
||||
std::string backendName;
|
||||
std::string state;
|
||||
std::string message;
|
||||
};
|
||||
|
||||
struct SubsystemWarningEvent
|
||||
{
|
||||
std::string subsystem;
|
||||
std::string warningKey;
|
||||
RuntimeEventSeverity severity = RuntimeEventSeverity::Warning;
|
||||
std::string message;
|
||||
bool cleared = false;
|
||||
};
|
||||
|
||||
struct SubsystemRecoveredEvent
|
||||
{
|
||||
std::string subsystem;
|
||||
std::string recoveryKey;
|
||||
std::string message;
|
||||
};
|
||||
|
||||
struct TimingSampleRecordedEvent
|
||||
{
|
||||
std::string subsystem;
|
||||
std::string metric;
|
||||
double value = 0.0;
|
||||
std::string unit;
|
||||
};
|
||||
|
||||
struct QueueDepthChangedEvent
|
||||
{
|
||||
std::string queueName;
|
||||
std::size_t depth = 0;
|
||||
std::size_t capacity = 0;
|
||||
std::size_t droppedCount = 0;
|
||||
std::size_t coalescedCount = 0;
|
||||
};
|
||||
|
||||
constexpr RuntimeEventType RuntimeEventPayloadType(const OscValueReceivedEvent&)
|
||||
{
|
||||
return RuntimeEventType::OscValueReceived;
|
||||
}
|
||||
|
||||
constexpr RuntimeEventType RuntimeEventPayloadType(const OscValueCoalescedEvent&)
|
||||
{
|
||||
return RuntimeEventType::OscValueCoalesced;
|
||||
}
|
||||
|
||||
constexpr RuntimeEventType RuntimeEventPayloadType(const OscCommitRequestedEvent&)
|
||||
{
|
||||
return RuntimeEventType::OscCommitRequested;
|
||||
}
|
||||
|
||||
constexpr RuntimeEventType RuntimeEventPayloadType(const HttpControlMutationRequestedEvent&)
|
||||
{
|
||||
return RuntimeEventType::HttpControlMutationRequested;
|
||||
}
|
||||
|
||||
constexpr RuntimeEventType RuntimeEventPayloadType(const WebSocketClientConnectedEvent&)
|
||||
{
|
||||
return RuntimeEventType::WebSocketClientConnected;
|
||||
}
|
||||
|
||||
constexpr RuntimeEventType RuntimeEventPayloadType(const RuntimeStateBroadcastRequestedEvent&)
|
||||
{
|
||||
return RuntimeEventType::RuntimeStateBroadcastRequested;
|
||||
}
|
||||
|
||||
constexpr RuntimeEventType RuntimeEventPayloadType(const FileChangeDetectedEvent&)
|
||||
{
|
||||
return RuntimeEventType::FileChangeDetected;
|
||||
}
|
||||
|
||||
constexpr RuntimeEventType RuntimeEventPayloadType(const ManualReloadRequestedEvent&)
|
||||
{
|
||||
return RuntimeEventType::ManualReloadRequested;
|
||||
}
|
||||
|
||||
inline RuntimeEventType RuntimeEventPayloadType(const RuntimeMutationEvent& event)
|
||||
{
|
||||
return event.accepted ? RuntimeEventType::RuntimeMutationAccepted : RuntimeEventType::RuntimeMutationRejected;
|
||||
}
|
||||
|
||||
constexpr RuntimeEventType RuntimeEventPayloadType(const RuntimeStateChangedEvent&)
|
||||
{
|
||||
return RuntimeEventType::RuntimeStateChanged;
|
||||
}
|
||||
|
||||
constexpr RuntimeEventType RuntimeEventPayloadType(const RuntimePersistenceRequestedEvent&)
|
||||
{
|
||||
return RuntimeEventType::RuntimePersistenceRequested;
|
||||
}
|
||||
|
||||
constexpr RuntimeEventType RuntimeEventPayloadType(const RuntimeReloadRequestedEvent&)
|
||||
{
|
||||
return RuntimeEventType::RuntimeReloadRequested;
|
||||
}
|
||||
|
||||
constexpr RuntimeEventType RuntimeEventPayloadType(const ShaderPackagesChangedEvent&)
|
||||
{
|
||||
return RuntimeEventType::ShaderPackagesChanged;
|
||||
}
|
||||
|
||||
constexpr RuntimeEventType RuntimeEventPayloadType(const RenderSnapshotPublishRequestedEvent&)
|
||||
{
|
||||
return RuntimeEventType::RenderSnapshotPublishRequested;
|
||||
}
|
||||
|
||||
constexpr RuntimeEventType RuntimeEventPayloadType(const RuntimeStatePresentationChangedEvent&)
|
||||
{
|
||||
return RuntimeEventType::RuntimeStatePresentationChanged;
|
||||
}
|
||||
|
||||
inline RuntimeEventType RuntimeEventPayloadType(const ShaderBuildEvent& event)
|
||||
{
|
||||
switch (event.phase)
|
||||
{
|
||||
case RuntimeEventShaderBuildPhase::Requested:
|
||||
return RuntimeEventType::ShaderBuildRequested;
|
||||
case RuntimeEventShaderBuildPhase::Prepared:
|
||||
return RuntimeEventType::ShaderBuildPrepared;
|
||||
case RuntimeEventShaderBuildPhase::Applied:
|
||||
return RuntimeEventType::ShaderBuildApplied;
|
||||
case RuntimeEventShaderBuildPhase::Failed:
|
||||
return RuntimeEventType::ShaderBuildFailed;
|
||||
}
|
||||
|
||||
return RuntimeEventType::ShaderBuildRequested;
|
||||
}
|
||||
|
||||
constexpr RuntimeEventType RuntimeEventPayloadType(const CompileStatusChangedEvent&)
|
||||
{
|
||||
return RuntimeEventType::CompileStatusChanged;
|
||||
}
|
||||
|
||||
constexpr RuntimeEventType RuntimeEventPayloadType(const RenderSnapshotPublishedEvent&)
|
||||
{
|
||||
return RuntimeEventType::RenderSnapshotPublished;
|
||||
}
|
||||
|
||||
inline RuntimeEventType RuntimeEventPayloadType(const RenderResetEvent& event)
|
||||
{
|
||||
return event.applied ? RuntimeEventType::RenderResetApplied : RuntimeEventType::RenderResetRequested;
|
||||
}
|
||||
|
||||
inline RuntimeEventType RuntimeEventPayloadType(const OscOverlayEvent& event)
|
||||
{
|
||||
return event.settled ? RuntimeEventType::OscOverlaySettled : RuntimeEventType::OscOverlayApplied;
|
||||
}
|
||||
|
||||
constexpr RuntimeEventType RuntimeEventPayloadType(const FrameRenderedEvent&)
|
||||
{
|
||||
return RuntimeEventType::FrameRendered;
|
||||
}
|
||||
|
||||
constexpr RuntimeEventType RuntimeEventPayloadType(const PreviewFrameAvailableEvent&)
|
||||
{
|
||||
return RuntimeEventType::PreviewFrameAvailable;
|
||||
}
|
||||
|
||||
constexpr RuntimeEventType RuntimeEventPayloadType(const InputSignalChangedEvent&)
|
||||
{
|
||||
return RuntimeEventType::InputSignalChanged;
|
||||
}
|
||||
|
||||
constexpr RuntimeEventType RuntimeEventPayloadType(const InputFrameArrivedEvent&)
|
||||
{
|
||||
return RuntimeEventType::InputFrameArrived;
|
||||
}
|
||||
|
||||
constexpr RuntimeEventType RuntimeEventPayloadType(const OutputFrameScheduledEvent&)
|
||||
{
|
||||
return RuntimeEventType::OutputFrameScheduled;
|
||||
}
|
||||
|
||||
inline RuntimeEventType RuntimeEventPayloadType(const OutputFrameCompletedEvent& event)
|
||||
{
|
||||
if (event.result == "DisplayedLate")
|
||||
return RuntimeEventType::OutputLateFrameDetected;
|
||||
if (event.result == "Dropped")
|
||||
return RuntimeEventType::OutputDroppedFrameDetected;
|
||||
return RuntimeEventType::OutputFrameCompleted;
|
||||
}
|
||||
|
||||
constexpr RuntimeEventType RuntimeEventPayloadType(const BackendStateChangedEvent&)
|
||||
{
|
||||
return RuntimeEventType::BackendStateChanged;
|
||||
}
|
||||
|
||||
inline RuntimeEventType RuntimeEventPayloadType(const SubsystemWarningEvent& event)
|
||||
{
|
||||
return event.cleared ? RuntimeEventType::SubsystemWarningCleared : RuntimeEventType::SubsystemWarningRaised;
|
||||
}
|
||||
|
||||
constexpr RuntimeEventType RuntimeEventPayloadType(const SubsystemRecoveredEvent&)
|
||||
{
|
||||
return RuntimeEventType::SubsystemRecovered;
|
||||
}
|
||||
|
||||
constexpr RuntimeEventType RuntimeEventPayloadType(const TimingSampleRecordedEvent&)
|
||||
{
|
||||
return RuntimeEventType::TimingSampleRecorded;
|
||||
}
|
||||
|
||||
constexpr RuntimeEventType RuntimeEventPayloadType(const QueueDepthChangedEvent&)
|
||||
{
|
||||
return RuntimeEventType::QueueDepthChanged;
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
#pragma once
|
||||
|
||||
#include "RuntimeEvent.h"
|
||||
|
||||
#include <chrono>
|
||||
#include <cstddef>
|
||||
#include <deque>
|
||||
#include <mutex>
|
||||
#include <vector>
|
||||
|
||||
struct RuntimeEventQueueMetrics
|
||||
{
|
||||
std::size_t depth = 0;
|
||||
std::size_t capacity = 0;
|
||||
std::size_t droppedCount = 0;
|
||||
std::size_t coalescedCount = 0;
|
||||
double oldestEventAgeMilliseconds = 0.0;
|
||||
};
|
||||
|
||||
class RuntimeEventQueue
|
||||
{
|
||||
public:
|
||||
explicit RuntimeEventQueue(std::size_t capacity = 1024) :
|
||||
mCapacity(capacity)
|
||||
{
|
||||
}
|
||||
|
||||
bool Push(RuntimeEvent event)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mMutex);
|
||||
if (mEvents.size() >= mCapacity)
|
||||
{
|
||||
++mDroppedCount;
|
||||
return false;
|
||||
}
|
||||
|
||||
mEvents.push_back(std::move(event));
|
||||
return true;
|
||||
}
|
||||
|
||||
bool TryPop(RuntimeEvent& event)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mMutex);
|
||||
if (mEvents.empty())
|
||||
return false;
|
||||
|
||||
event = std::move(mEvents.front());
|
||||
mEvents.pop_front();
|
||||
return true;
|
||||
}
|
||||
|
||||
std::vector<RuntimeEvent> Drain(std::size_t maxEvents = 0)
|
||||
{
|
||||
std::vector<RuntimeEvent> events;
|
||||
|
||||
std::lock_guard<std::mutex> lock(mMutex);
|
||||
const std::size_t count = maxEvents == 0 || maxEvents > mEvents.size() ? mEvents.size() : maxEvents;
|
||||
events.reserve(count);
|
||||
for (std::size_t index = 0; index < count; ++index)
|
||||
{
|
||||
events.push_back(std::move(mEvents.front()));
|
||||
mEvents.pop_front();
|
||||
}
|
||||
|
||||
return events;
|
||||
}
|
||||
|
||||
RuntimeEventQueueMetrics GetMetrics(std::chrono::steady_clock::time_point now = std::chrono::steady_clock::now()) const
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mMutex);
|
||||
|
||||
RuntimeEventQueueMetrics metrics;
|
||||
metrics.depth = mEvents.size();
|
||||
metrics.capacity = mCapacity;
|
||||
metrics.droppedCount = mDroppedCount;
|
||||
if (!mEvents.empty())
|
||||
{
|
||||
const auto age = now - mEvents.front().createdAt;
|
||||
metrics.oldestEventAgeMilliseconds = std::chrono::duration<double, std::milli>(age).count();
|
||||
}
|
||||
return metrics;
|
||||
}
|
||||
|
||||
std::size_t Depth() const
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mMutex);
|
||||
return mEvents.size();
|
||||
}
|
||||
|
||||
std::size_t Capacity() const
|
||||
{
|
||||
return mCapacity;
|
||||
}
|
||||
|
||||
private:
|
||||
mutable std::mutex mMutex;
|
||||
std::deque<RuntimeEvent> mEvents;
|
||||
std::size_t mCapacity = 0;
|
||||
std::size_t mDroppedCount = 0;
|
||||
};
|
||||
@@ -0,0 +1,151 @@
|
||||
#pragma once
|
||||
|
||||
#include <string_view>
|
||||
|
||||
enum class RuntimeEventType
|
||||
{
|
||||
Unknown = 0,
|
||||
|
||||
// Control ingress.
|
||||
OscValueReceived,
|
||||
OscValueCoalesced,
|
||||
OscCommitRequested,
|
||||
HttpControlMutationRequested,
|
||||
WebSocketClientConnected,
|
||||
RuntimeStateBroadcastRequested,
|
||||
FileChangeDetected,
|
||||
ManualReloadRequested,
|
||||
|
||||
// Runtime policy and state.
|
||||
RuntimeMutationAccepted,
|
||||
RuntimeMutationRejected,
|
||||
RuntimeStateChanged,
|
||||
RuntimePersistenceRequested,
|
||||
RuntimeReloadRequested,
|
||||
ShaderPackagesChanged,
|
||||
RenderSnapshotPublishRequested,
|
||||
RuntimeStatePresentationChanged,
|
||||
|
||||
// Shader build lifecycle.
|
||||
ShaderBuildRequested,
|
||||
ShaderBuildPrepared,
|
||||
ShaderBuildApplied,
|
||||
ShaderBuildFailed,
|
||||
CompileStatusChanged,
|
||||
|
||||
// Render lifecycle.
|
||||
RenderSnapshotPublished,
|
||||
RenderResetRequested,
|
||||
RenderResetApplied,
|
||||
OscOverlayApplied,
|
||||
OscOverlaySettled,
|
||||
FrameRendered,
|
||||
PreviewFrameAvailable,
|
||||
|
||||
// Video backend lifecycle.
|
||||
InputSignalChanged,
|
||||
InputFrameArrived,
|
||||
OutputFrameScheduled,
|
||||
OutputFrameCompleted,
|
||||
OutputLateFrameDetected,
|
||||
OutputDroppedFrameDetected,
|
||||
BackendStateChanged,
|
||||
|
||||
// Health and telemetry.
|
||||
SubsystemWarningRaised,
|
||||
SubsystemWarningCleared,
|
||||
SubsystemRecovered,
|
||||
TimingSampleRecorded,
|
||||
QueueDepthChanged
|
||||
};
|
||||
|
||||
constexpr std::string_view RuntimeEventTypeName(RuntimeEventType type)
|
||||
{
|
||||
switch (type)
|
||||
{
|
||||
case RuntimeEventType::Unknown:
|
||||
return "Unknown";
|
||||
case RuntimeEventType::OscValueReceived:
|
||||
return "OscValueReceived";
|
||||
case RuntimeEventType::OscValueCoalesced:
|
||||
return "OscValueCoalesced";
|
||||
case RuntimeEventType::OscCommitRequested:
|
||||
return "OscCommitRequested";
|
||||
case RuntimeEventType::HttpControlMutationRequested:
|
||||
return "HttpControlMutationRequested";
|
||||
case RuntimeEventType::WebSocketClientConnected:
|
||||
return "WebSocketClientConnected";
|
||||
case RuntimeEventType::RuntimeStateBroadcastRequested:
|
||||
return "RuntimeStateBroadcastRequested";
|
||||
case RuntimeEventType::FileChangeDetected:
|
||||
return "FileChangeDetected";
|
||||
case RuntimeEventType::ManualReloadRequested:
|
||||
return "ManualReloadRequested";
|
||||
case RuntimeEventType::RuntimeMutationAccepted:
|
||||
return "RuntimeMutationAccepted";
|
||||
case RuntimeEventType::RuntimeMutationRejected:
|
||||
return "RuntimeMutationRejected";
|
||||
case RuntimeEventType::RuntimeStateChanged:
|
||||
return "RuntimeStateChanged";
|
||||
case RuntimeEventType::RuntimePersistenceRequested:
|
||||
return "RuntimePersistenceRequested";
|
||||
case RuntimeEventType::RuntimeReloadRequested:
|
||||
return "RuntimeReloadRequested";
|
||||
case RuntimeEventType::ShaderPackagesChanged:
|
||||
return "ShaderPackagesChanged";
|
||||
case RuntimeEventType::RenderSnapshotPublishRequested:
|
||||
return "RenderSnapshotPublishRequested";
|
||||
case RuntimeEventType::RuntimeStatePresentationChanged:
|
||||
return "RuntimeStatePresentationChanged";
|
||||
case RuntimeEventType::ShaderBuildRequested:
|
||||
return "ShaderBuildRequested";
|
||||
case RuntimeEventType::ShaderBuildPrepared:
|
||||
return "ShaderBuildPrepared";
|
||||
case RuntimeEventType::ShaderBuildApplied:
|
||||
return "ShaderBuildApplied";
|
||||
case RuntimeEventType::ShaderBuildFailed:
|
||||
return "ShaderBuildFailed";
|
||||
case RuntimeEventType::CompileStatusChanged:
|
||||
return "CompileStatusChanged";
|
||||
case RuntimeEventType::RenderSnapshotPublished:
|
||||
return "RenderSnapshotPublished";
|
||||
case RuntimeEventType::RenderResetRequested:
|
||||
return "RenderResetRequested";
|
||||
case RuntimeEventType::RenderResetApplied:
|
||||
return "RenderResetApplied";
|
||||
case RuntimeEventType::OscOverlayApplied:
|
||||
return "OscOverlayApplied";
|
||||
case RuntimeEventType::OscOverlaySettled:
|
||||
return "OscOverlaySettled";
|
||||
case RuntimeEventType::FrameRendered:
|
||||
return "FrameRendered";
|
||||
case RuntimeEventType::PreviewFrameAvailable:
|
||||
return "PreviewFrameAvailable";
|
||||
case RuntimeEventType::InputSignalChanged:
|
||||
return "InputSignalChanged";
|
||||
case RuntimeEventType::InputFrameArrived:
|
||||
return "InputFrameArrived";
|
||||
case RuntimeEventType::OutputFrameScheduled:
|
||||
return "OutputFrameScheduled";
|
||||
case RuntimeEventType::OutputFrameCompleted:
|
||||
return "OutputFrameCompleted";
|
||||
case RuntimeEventType::OutputLateFrameDetected:
|
||||
return "OutputLateFrameDetected";
|
||||
case RuntimeEventType::OutputDroppedFrameDetected:
|
||||
return "OutputDroppedFrameDetected";
|
||||
case RuntimeEventType::BackendStateChanged:
|
||||
return "BackendStateChanged";
|
||||
case RuntimeEventType::SubsystemWarningRaised:
|
||||
return "SubsystemWarningRaised";
|
||||
case RuntimeEventType::SubsystemWarningCleared:
|
||||
return "SubsystemWarningCleared";
|
||||
case RuntimeEventType::SubsystemRecovered:
|
||||
return "SubsystemRecovered";
|
||||
case RuntimeEventType::TimingSampleRecorded:
|
||||
return "TimingSampleRecorded";
|
||||
case RuntimeEventType::QueueDepthChanged:
|
||||
return "QueueDepthChanged";
|
||||
}
|
||||
|
||||
return "Unknown";
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
#include "RenderStateComposer.h"
|
||||
|
||||
RenderStateCompositionResult RenderStateComposer::BuildFrameState(const LayeredRenderStateInput& input) const
|
||||
{
|
||||
RenderStateCompositionResult result;
|
||||
const std::vector<RuntimeRenderState>* layerStates =
|
||||
input.committedLiveLayerStates ? input.committedLiveLayerStates : input.basePersistedLayerStates;
|
||||
if (!layerStates)
|
||||
return result;
|
||||
|
||||
result.layerStates = *layerStates;
|
||||
result.hasLayerStates = !result.layerStates.empty();
|
||||
if (input.transientAutomationOverlay)
|
||||
{
|
||||
RuntimeLiveStateApplyOptions options;
|
||||
options.allowCommit = input.allowTransientAutomationCommits;
|
||||
options.smoothing = input.transientAutomationSmoothing;
|
||||
options.commitDelay = input.transientAutomationCommitDelay;
|
||||
options.now = input.now;
|
||||
input.transientAutomationOverlay->ApplyToLayerStates(
|
||||
result.layerStates,
|
||||
options,
|
||||
input.collectTransientAutomationCommitRequests ? &result.commitRequests : nullptr);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
#pragma once
|
||||
|
||||
#include "RuntimeLiveState.h"
|
||||
|
||||
#include <chrono>
|
||||
#include <vector>
|
||||
|
||||
struct LayeredRenderStateInput
|
||||
{
|
||||
const std::vector<RuntimeRenderState>* basePersistedLayerStates = nullptr;
|
||||
const std::vector<RuntimeRenderState>* committedLiveLayerStates = nullptr;
|
||||
RuntimeLiveState* transientAutomationOverlay = nullptr;
|
||||
bool allowTransientAutomationCommits = false;
|
||||
bool collectTransientAutomationCommitRequests = true;
|
||||
double transientAutomationSmoothing = 0.0;
|
||||
std::chrono::milliseconds transientAutomationCommitDelay = std::chrono::milliseconds(150);
|
||||
std::chrono::steady_clock::time_point now = std::chrono::steady_clock::now();
|
||||
};
|
||||
|
||||
struct RenderStateCompositionResult
|
||||
{
|
||||
std::vector<RuntimeRenderState> layerStates;
|
||||
std::vector<RuntimeLiveOscCommitRequest> commitRequests;
|
||||
bool hasLayerStates = false;
|
||||
};
|
||||
|
||||
class RenderStateComposer
|
||||
{
|
||||
public:
|
||||
RenderStateCompositionResult BuildFrameState(const LayeredRenderStateInput& input) const;
|
||||
};
|
||||
@@ -0,0 +1,329 @@
|
||||
#include "RuntimeLiveState.h"
|
||||
|
||||
#include "RuntimeParameterUtils.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
#include <cmath>
|
||||
#include <cstddef>
|
||||
|
||||
namespace
|
||||
{
|
||||
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();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void RuntimeLiveState::Clear()
|
||||
{
|
||||
mOscOverlayStates.clear();
|
||||
}
|
||||
|
||||
void RuntimeLiveState::ClearForLayerKey(const std::string& layerKey)
|
||||
{
|
||||
for (auto it = mOscOverlayStates.begin(); it != mOscOverlayStates.end();)
|
||||
{
|
||||
if (OverlayMatchesLayerKey(it->second, layerKey))
|
||||
it = mOscOverlayStates.erase(it);
|
||||
else
|
||||
++it;
|
||||
}
|
||||
}
|
||||
|
||||
bool RuntimeLiveState::OverlayMatchesLayerKey(const OscOverlayState& overlay, const std::string& layerKey)
|
||||
{
|
||||
return MatchesOscControlKey(overlay.layerKey, layerKey);
|
||||
}
|
||||
|
||||
bool RuntimeLiveState::TryResolveOverlayTarget(
|
||||
const OscOverlayState& overlay,
|
||||
const std::vector<RuntimeRenderState>& states,
|
||||
std::vector<RuntimeRenderState>::const_iterator& stateIt,
|
||||
std::vector<ShaderParameterDefinition>::const_iterator& definitionIt)
|
||||
{
|
||||
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())
|
||||
return false;
|
||||
|
||||
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);
|
||||
});
|
||||
return definitionIt != stateIt->parameterDefinitions.end();
|
||||
}
|
||||
|
||||
std::size_t RuntimeLiveState::OverlayCount() const
|
||||
{
|
||||
return mOscOverlayStates.size();
|
||||
}
|
||||
|
||||
void RuntimeLiveState::ApplyOscUpdates(const std::vector<RuntimeLiveOscUpdate>& updates)
|
||||
{
|
||||
const auto now = std::chrono::steady_clock::now();
|
||||
for (const RuntimeLiveOscUpdate& update : updates)
|
||||
{
|
||||
auto overlayIt = mOscOverlayStates.find(update.routeKey);
|
||||
if (overlayIt == mOscOverlayStates.end())
|
||||
{
|
||||
OscOverlayState overlay;
|
||||
overlay.layerKey = update.layerKey;
|
||||
overlay.parameterKey = update.parameterKey;
|
||||
overlay.targetValue = update.targetValue;
|
||||
overlay.lastUpdatedTime = now;
|
||||
overlay.lastAppliedTime = now;
|
||||
overlay.generation = 1;
|
||||
mOscOverlayStates[update.routeKey] = std::move(overlay);
|
||||
}
|
||||
else
|
||||
{
|
||||
overlayIt->second.targetValue = update.targetValue;
|
||||
overlayIt->second.lastUpdatedTime = now;
|
||||
overlayIt->second.generation += 1;
|
||||
overlayIt->second.commitQueued = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void RuntimeLiveState::ApplyOscCommitCompletions(const std::vector<RuntimeLiveOscCommitCompletion>& completedCommits)
|
||||
{
|
||||
for (const RuntimeLiveOscCommitCompletion& completedCommit : completedCommits)
|
||||
{
|
||||
auto overlayIt = mOscOverlayStates.find(completedCommit.routeKey);
|
||||
if (overlayIt == mOscOverlayStates.end())
|
||||
continue;
|
||||
|
||||
OscOverlayState& overlay = overlayIt->second;
|
||||
if (overlay.commitQueued &&
|
||||
overlay.pendingCommitGeneration == completedCommit.generation &&
|
||||
overlay.generation == completedCommit.generation)
|
||||
{
|
||||
mOscOverlayStates.erase(overlayIt);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void RuntimeLiveState::PruneIncompatibleOverlays(const std::vector<RuntimeRenderState>& states)
|
||||
{
|
||||
for (auto it = mOscOverlayStates.begin(); it != mOscOverlayStates.end();)
|
||||
{
|
||||
std::vector<RuntimeRenderState>::const_iterator stateIt;
|
||||
std::vector<ShaderParameterDefinition>::const_iterator definitionIt;
|
||||
if (TryResolveOverlayTarget(it->second, states, stateIt, definitionIt))
|
||||
{
|
||||
ShaderParameterValue targetValue;
|
||||
std::string normalizeError;
|
||||
if (NormalizeAndValidateParameterValue(*definitionIt, it->second.targetValue, targetValue, normalizeError))
|
||||
{
|
||||
++it;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
it = mOscOverlayStates.erase(it);
|
||||
}
|
||||
}
|
||||
|
||||
void RuntimeLiveState::ApplyToLayerStates(
|
||||
std::vector<RuntimeRenderState>& states,
|
||||
const RuntimeLiveStateApplyOptions& options,
|
||||
std::vector<RuntimeLiveOscCommitRequest>* commitRequests)
|
||||
{
|
||||
if (states.empty() || mOscOverlayStates.empty())
|
||||
return;
|
||||
|
||||
PruneIncompatibleOverlays(states);
|
||||
if (mOscOverlayStates.empty())
|
||||
return;
|
||||
|
||||
const auto now = options.now;
|
||||
const double clampedSmoothing = ClampOscAlpha(options.smoothing);
|
||||
std::vector<std::string> overlayKeysToRemove;
|
||||
|
||||
for (auto& item : mOscOverlayStates)
|
||||
{
|
||||
const std::string& routeKey = item.first;
|
||||
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;
|
||||
|
||||
ShaderParameterValue targetValue;
|
||||
std::string normalizeError;
|
||||
if (!NormalizeAndValidateParameterValue(*definitionIt, overlay.targetValue, targetValue, normalizeError))
|
||||
continue;
|
||||
|
||||
if (definitionIt->type == ShaderParameterType::Trigger)
|
||||
{
|
||||
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(routeKey);
|
||||
continue;
|
||||
}
|
||||
|
||||
const bool smoothable =
|
||||
clampedSmoothing > 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 (options.allowCommit &&
|
||||
!overlay.commitQueued &&
|
||||
now - overlay.lastUpdatedTime >= options.commitDelay &&
|
||||
commitRequests)
|
||||
{
|
||||
commitRequests->push_back({ routeKey, overlay.layerKey, overlay.parameterKey, overlay.targetValue, overlay.generation });
|
||||
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 = clampedSmoothing;
|
||||
if (overlay.lastAppliedTime != std::chrono::steady_clock::time_point())
|
||||
{
|
||||
const double deltaSeconds =
|
||||
std::chrono::duration_cast<std::chrono::duration<double>>(now - overlay.lastAppliedTime).count();
|
||||
smoothingAlpha = ComputeTimeBasedOscAlpha(clampedSmoothing, deltaSeconds);
|
||||
}
|
||||
overlay.lastAppliedTime = now;
|
||||
|
||||
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 (options.allowCommit &&
|
||||
converged &&
|
||||
!overlay.commitQueued &&
|
||||
now - overlay.lastUpdatedTime >= options.commitDelay &&
|
||||
commitRequests)
|
||||
{
|
||||
commitRequests->push_back({ routeKey, overlay.layerKey, overlay.parameterKey, BuildOscCommitValue(*definitionIt, overlay.currentValue), overlay.generation });
|
||||
overlay.pendingCommitGeneration = overlay.generation;
|
||||
overlay.commitQueued = true;
|
||||
}
|
||||
}
|
||||
|
||||
for (const std::string& overlayKey : overlayKeysToRemove)
|
||||
mOscOverlayStates.erase(overlayKey);
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
#pragma once
|
||||
|
||||
#include "RuntimeJson.h"
|
||||
#include "ShaderTypes.h"
|
||||
|
||||
#include <chrono>
|
||||
#include <cstdint>
|
||||
#include <map>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
struct RuntimeLiveOscUpdate
|
||||
{
|
||||
std::string routeKey;
|
||||
std::string layerKey;
|
||||
std::string parameterKey;
|
||||
JsonValue targetValue;
|
||||
};
|
||||
|
||||
struct RuntimeLiveOscCommitCompletion
|
||||
{
|
||||
std::string routeKey;
|
||||
uint64_t generation = 0;
|
||||
};
|
||||
|
||||
struct RuntimeLiveOscCommitRequest
|
||||
{
|
||||
std::string routeKey;
|
||||
std::string layerKey;
|
||||
std::string parameterKey;
|
||||
JsonValue value;
|
||||
uint64_t generation = 0;
|
||||
};
|
||||
|
||||
struct RuntimeLiveStateApplyOptions
|
||||
{
|
||||
bool allowCommit = false;
|
||||
double smoothing = 0.0;
|
||||
std::chrono::milliseconds commitDelay = std::chrono::milliseconds(150);
|
||||
std::chrono::steady_clock::time_point now = std::chrono::steady_clock::now();
|
||||
};
|
||||
|
||||
class RuntimeLiveState
|
||||
{
|
||||
public:
|
||||
void Clear();
|
||||
void ClearForLayerKey(const std::string& layerKey);
|
||||
std::size_t OverlayCount() const;
|
||||
void ApplyOscUpdates(const std::vector<RuntimeLiveOscUpdate>& updates);
|
||||
void ApplyOscCommitCompletions(const std::vector<RuntimeLiveOscCommitCompletion>& completedCommits);
|
||||
void PruneIncompatibleOverlays(const std::vector<RuntimeRenderState>& states);
|
||||
void ApplyToLayerStates(
|
||||
std::vector<RuntimeRenderState>& states,
|
||||
const RuntimeLiveStateApplyOptions& options,
|
||||
std::vector<RuntimeLiveOscCommitRequest>* commitRequests);
|
||||
|
||||
private:
|
||||
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;
|
||||
};
|
||||
|
||||
static bool OverlayMatchesLayerKey(const OscOverlayState& overlay, const std::string& layerKey);
|
||||
static bool TryResolveOverlayTarget(
|
||||
const OscOverlayState& overlay,
|
||||
const std::vector<RuntimeRenderState>& states,
|
||||
std::vector<RuntimeRenderState>::const_iterator& stateIt,
|
||||
std::vector<ShaderParameterDefinition>::const_iterator& definitionIt);
|
||||
|
||||
std::map<std::string, OscOverlayState> mOscOverlayStates;
|
||||
};
|
||||
@@ -0,0 +1,230 @@
|
||||
#include "RuntimeStateLayerModel.h"
|
||||
|
||||
const char* RuntimeStateLayerKindName(RuntimeStateLayerKind kind)
|
||||
{
|
||||
switch (kind)
|
||||
{
|
||||
case RuntimeStateLayerKind::BasePersisted:
|
||||
return "base persisted";
|
||||
case RuntimeStateLayerKind::CommittedLive:
|
||||
return "committed live";
|
||||
case RuntimeStateLayerKind::TransientAutomation:
|
||||
return "transient automation";
|
||||
case RuntimeStateLayerKind::RenderLocal:
|
||||
return "render local";
|
||||
case RuntimeStateLayerKind::HealthConfig:
|
||||
return "health/config";
|
||||
default:
|
||||
return "unknown";
|
||||
}
|
||||
}
|
||||
|
||||
int RuntimeStateLayerCompositionPrecedence(RuntimeStateLayerKind kind)
|
||||
{
|
||||
switch (kind)
|
||||
{
|
||||
case RuntimeStateLayerKind::BasePersisted:
|
||||
return 0;
|
||||
case RuntimeStateLayerKind::CommittedLive:
|
||||
return 1;
|
||||
case RuntimeStateLayerKind::TransientAutomation:
|
||||
return 2;
|
||||
case RuntimeStateLayerKind::RenderLocal:
|
||||
case RuntimeStateLayerKind::HealthConfig:
|
||||
default:
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
bool RuntimeStateLayerIsDurable(RuntimeStateLayerKind kind)
|
||||
{
|
||||
return kind == RuntimeStateLayerKind::BasePersisted;
|
||||
}
|
||||
|
||||
bool RuntimeStateLayerParticipatesInParameterComposition(RuntimeStateLayerKind kind)
|
||||
{
|
||||
return RuntimeStateLayerCompositionPrecedence(kind) >= 0;
|
||||
}
|
||||
|
||||
bool RuntimeStateLayerIsRenderLocal(RuntimeStateLayerKind kind)
|
||||
{
|
||||
return kind == RuntimeStateLayerKind::RenderLocal;
|
||||
}
|
||||
|
||||
RuntimeStateLayerKind ClassifyRuntimeStateField(RuntimeStateField field)
|
||||
{
|
||||
switch (field)
|
||||
{
|
||||
case RuntimeStateField::PersistedLayerStack:
|
||||
case RuntimeStateField::PersistedParameterValues:
|
||||
case RuntimeStateField::StackPresets:
|
||||
return RuntimeStateLayerKind::BasePersisted;
|
||||
case RuntimeStateField::CommittedSessionParameterValues:
|
||||
case RuntimeStateField::CommittedLayerBypass:
|
||||
case RuntimeStateField::RuntimeCompileReloadFlags:
|
||||
return RuntimeStateLayerKind::CommittedLive;
|
||||
case RuntimeStateField::TransientOscOverlay:
|
||||
case RuntimeStateField::TransientAutomationCommitState:
|
||||
return RuntimeStateLayerKind::TransientAutomation;
|
||||
case RuntimeStateField::RenderLocalTemporalHistory:
|
||||
case RuntimeStateField::RenderLocalFeedbackState:
|
||||
case RuntimeStateField::RenderLocalInputFrames:
|
||||
case RuntimeStateField::RenderLocalOutputFrames:
|
||||
return RuntimeStateLayerKind::RenderLocal;
|
||||
case RuntimeStateField::RuntimeConfiguration:
|
||||
case RuntimeStateField::HealthTelemetry:
|
||||
default:
|
||||
return RuntimeStateLayerKind::HealthConfig;
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<RuntimeStateLayerDescriptor> GetRuntimeStateLayerInventory()
|
||||
{
|
||||
return {
|
||||
{
|
||||
RuntimeStateLayerKind::BasePersisted,
|
||||
"Base persisted state",
|
||||
"RuntimeStore / LayerStackStore",
|
||||
"Survives restart",
|
||||
"Written to disk",
|
||||
"Default layer stack, shader selections, saved parameter values"
|
||||
},
|
||||
{
|
||||
RuntimeStateLayerKind::CommittedLive,
|
||||
"Committed live state",
|
||||
"RuntimeCoordinator, physically backed by RuntimeStore during migration",
|
||||
"Current running session",
|
||||
"May request persistence depending on mutation policy",
|
||||
"Operator/session truth until changed again"
|
||||
},
|
||||
{
|
||||
RuntimeStateLayerKind::TransientAutomation,
|
||||
"Transient automation overlay",
|
||||
"RuntimeLiveState / RuntimeServiceLiveBridge",
|
||||
"High-rate and short-lived",
|
||||
"Not persisted directly",
|
||||
"Temporary OSC/automation target applied over committed truth"
|
||||
},
|
||||
{
|
||||
RuntimeStateLayerKind::RenderLocal,
|
||||
"Render-local state",
|
||||
"RenderEngine",
|
||||
"Render-thread/resource lifetime",
|
||||
"Not persisted",
|
||||
"Temporal history, feedback, input/output queues, and GL-local caches"
|
||||
},
|
||||
{
|
||||
RuntimeStateLayerKind::HealthConfig,
|
||||
"Health/config state",
|
||||
"RuntimeConfigStore / HealthTelemetry",
|
||||
"Config survives restart; health is observational",
|
||||
"Config is file-backed; health is reported, not composed",
|
||||
"Does not participate in parameter composition"
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
std::vector<RuntimeStateFieldDescriptor> GetRuntimeStateFieldInventory()
|
||||
{
|
||||
return {
|
||||
{
|
||||
RuntimeStateField::PersistedLayerStack,
|
||||
ClassifyRuntimeStateField(RuntimeStateField::PersistedLayerStack),
|
||||
"persisted layer stack",
|
||||
"LayerStackStore",
|
||||
"Durable layer order, ids, shader selections, and bypass flags"
|
||||
},
|
||||
{
|
||||
RuntimeStateField::PersistedParameterValues,
|
||||
ClassifyRuntimeStateField(RuntimeStateField::PersistedParameterValues),
|
||||
"persisted parameter values",
|
||||
"LayerStackStore",
|
||||
"Saved parameter values used as the baseline for snapshots and presets"
|
||||
},
|
||||
{
|
||||
RuntimeStateField::StackPresets,
|
||||
ClassifyRuntimeStateField(RuntimeStateField::StackPresets),
|
||||
"stack presets",
|
||||
"RuntimeStore / LayerStackStore",
|
||||
"Durable preset files and preset serialization shape"
|
||||
},
|
||||
{
|
||||
RuntimeStateField::CommittedSessionParameterValues,
|
||||
ClassifyRuntimeStateField(RuntimeStateField::CommittedSessionParameterValues),
|
||||
"committed session parameter values",
|
||||
"RuntimeCoordinator policy, RuntimeStore backing during migration",
|
||||
"Operator/API truth after accepted mutations"
|
||||
},
|
||||
{
|
||||
RuntimeStateField::CommittedLayerBypass,
|
||||
ClassifyRuntimeStateField(RuntimeStateField::CommittedLayerBypass),
|
||||
"committed layer bypass",
|
||||
"RuntimeCoordinator policy, RuntimeStore backing during migration",
|
||||
"Current operator/API bypass state"
|
||||
},
|
||||
{
|
||||
RuntimeStateField::RuntimeCompileReloadFlags,
|
||||
ClassifyRuntimeStateField(RuntimeStateField::RuntimeCompileReloadFlags),
|
||||
"runtime compile/reload flags",
|
||||
"RuntimeCoordinator / RuntimeUpdateController",
|
||||
"Session coordination state used to request snapshot or render rebuild work"
|
||||
},
|
||||
{
|
||||
RuntimeStateField::TransientOscOverlay,
|
||||
ClassifyRuntimeStateField(RuntimeStateField::TransientOscOverlay),
|
||||
"transient OSC overlays",
|
||||
"RuntimeLiveState",
|
||||
"High-rate automation values applied above committed state"
|
||||
},
|
||||
{
|
||||
RuntimeStateField::TransientAutomationCommitState,
|
||||
ClassifyRuntimeStateField(RuntimeStateField::TransientAutomationCommitState),
|
||||
"transient automation commit state",
|
||||
"RuntimeLiveState / RuntimeServiceLiveBridge",
|
||||
"Generation and completion bookkeeping for settled overlay commits"
|
||||
},
|
||||
{
|
||||
RuntimeStateField::RenderLocalTemporalHistory,
|
||||
ClassifyRuntimeStateField(RuntimeStateField::RenderLocalTemporalHistory),
|
||||
"render-local temporal history",
|
||||
"RenderEngine",
|
||||
"GL/resource history that must stay out of parameter layering"
|
||||
},
|
||||
{
|
||||
RuntimeStateField::RenderLocalFeedbackState,
|
||||
ClassifyRuntimeStateField(RuntimeStateField::RenderLocalFeedbackState),
|
||||
"render-local feedback state",
|
||||
"RenderEngine",
|
||||
"Feedback buffers and ping-pong resources"
|
||||
},
|
||||
{
|
||||
RuntimeStateField::RenderLocalInputFrames,
|
||||
ClassifyRuntimeStateField(RuntimeStateField::RenderLocalInputFrames),
|
||||
"render-local input frames",
|
||||
"RenderEngine",
|
||||
"Latest accepted input frame payloads and upload staging"
|
||||
},
|
||||
{
|
||||
RuntimeStateField::RenderLocalOutputFrames,
|
||||
ClassifyRuntimeStateField(RuntimeStateField::RenderLocalOutputFrames),
|
||||
"render-local output frames",
|
||||
"RenderEngine",
|
||||
"Readback, packed output, screenshot, and preview staging"
|
||||
},
|
||||
{
|
||||
RuntimeStateField::RuntimeConfiguration,
|
||||
ClassifyRuntimeStateField(RuntimeStateField::RuntimeConfiguration),
|
||||
"runtime configuration",
|
||||
"RuntimeConfigStore",
|
||||
"File-backed config, not a live parameter layer"
|
||||
},
|
||||
{
|
||||
RuntimeStateField::HealthTelemetry,
|
||||
ClassifyRuntimeStateField(RuntimeStateField::HealthTelemetry),
|
||||
"health telemetry",
|
||||
"HealthTelemetry",
|
||||
"Operational observations, not source state for render values"
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
enum class RuntimeStateLayerKind
|
||||
{
|
||||
BasePersisted,
|
||||
CommittedLive,
|
||||
TransientAutomation,
|
||||
RenderLocal,
|
||||
HealthConfig
|
||||
};
|
||||
|
||||
enum class RuntimeStateField
|
||||
{
|
||||
PersistedLayerStack,
|
||||
PersistedParameterValues,
|
||||
StackPresets,
|
||||
CommittedSessionParameterValues,
|
||||
CommittedLayerBypass,
|
||||
RuntimeCompileReloadFlags,
|
||||
TransientOscOverlay,
|
||||
TransientAutomationCommitState,
|
||||
RenderLocalTemporalHistory,
|
||||
RenderLocalFeedbackState,
|
||||
RenderLocalInputFrames,
|
||||
RenderLocalOutputFrames,
|
||||
RuntimeConfiguration,
|
||||
HealthTelemetry
|
||||
};
|
||||
|
||||
struct RuntimeStateLayerDescriptor
|
||||
{
|
||||
RuntimeStateLayerKind kind = RuntimeStateLayerKind::BasePersisted;
|
||||
const char* name = "";
|
||||
const char* owner = "";
|
||||
const char* lifetime = "";
|
||||
const char* persistence = "";
|
||||
const char* renderRole = "";
|
||||
};
|
||||
|
||||
struct RuntimeStateFieldDescriptor
|
||||
{
|
||||
RuntimeStateField field = RuntimeStateField::PersistedLayerStack;
|
||||
RuntimeStateLayerKind layerKind = RuntimeStateLayerKind::BasePersisted;
|
||||
const char* name = "";
|
||||
const char* currentOwner = "";
|
||||
const char* notes = "";
|
||||
};
|
||||
|
||||
const char* RuntimeStateLayerKindName(RuntimeStateLayerKind kind);
|
||||
int RuntimeStateLayerCompositionPrecedence(RuntimeStateLayerKind kind);
|
||||
bool RuntimeStateLayerIsDurable(RuntimeStateLayerKind kind);
|
||||
bool RuntimeStateLayerParticipatesInParameterComposition(RuntimeStateLayerKind kind);
|
||||
bool RuntimeStateLayerIsRenderLocal(RuntimeStateLayerKind kind);
|
||||
|
||||
RuntimeStateLayerKind ClassifyRuntimeStateField(RuntimeStateField field);
|
||||
std::vector<RuntimeStateLayerDescriptor> GetRuntimeStateLayerInventory();
|
||||
std::vector<RuntimeStateFieldDescriptor> GetRuntimeStateFieldInventory();
|
||||
|
||||
@@ -0,0 +1,170 @@
|
||||
#include "RuntimeStateJson.h"
|
||||
|
||||
#include "RuntimeParameterUtils.h"
|
||||
|
||||
namespace
|
||||
{
|
||||
std::string ShaderParameterTypeToString(ShaderParameterType type)
|
||||
{
|
||||
switch (type)
|
||||
{
|
||||
case ShaderParameterType::Float: return "float";
|
||||
case ShaderParameterType::Vec2: return "vec2";
|
||||
case ShaderParameterType::Color: return "color";
|
||||
case ShaderParameterType::Boolean: return "bool";
|
||||
case ShaderParameterType::Enum: return "enum";
|
||||
case ShaderParameterType::Text: return "text";
|
||||
case ShaderParameterType::Trigger: return "trigger";
|
||||
}
|
||||
return "unknown";
|
||||
}
|
||||
}
|
||||
|
||||
JsonValue RuntimeStateJson::SerializeLayerStack(const LayerStackStore& layerStack, const ShaderPackageCatalog& shaderCatalog)
|
||||
{
|
||||
std::map<std::string, ShaderPackage> packagesById;
|
||||
for (const std::string& packageId : shaderCatalog.PackageOrder())
|
||||
{
|
||||
ShaderPackage shaderPackage;
|
||||
if (shaderCatalog.CopyPackage(packageId, shaderPackage))
|
||||
packagesById[packageId] = shaderPackage;
|
||||
}
|
||||
return SerializeLayerStack(layerStack.Layers(), packagesById);
|
||||
}
|
||||
|
||||
JsonValue RuntimeStateJson::SerializeLayerStack(const std::vector<LayerStackStore::LayerPersistentState>& layerStates, const std::map<std::string, ShaderPackage>& packagesById)
|
||||
{
|
||||
JsonValue layersValue = JsonValue::MakeArray();
|
||||
for (const LayerStackStore::LayerPersistentState& layer : layerStates)
|
||||
{
|
||||
auto shaderIt = packagesById.find(layer.shaderId);
|
||||
if (shaderIt == packagesById.end())
|
||||
continue;
|
||||
const ShaderPackage& shaderPackage = shaderIt->second;
|
||||
|
||||
JsonValue layerValue = JsonValue::MakeObject();
|
||||
layerValue.set("id", JsonValue(layer.id));
|
||||
layerValue.set("shaderId", JsonValue(layer.shaderId));
|
||||
layerValue.set("shaderName", JsonValue(shaderPackage.displayName));
|
||||
layerValue.set("bypass", JsonValue(layer.bypass));
|
||||
if (shaderPackage.temporal.enabled)
|
||||
{
|
||||
JsonValue temporal = JsonValue::MakeObject();
|
||||
temporal.set("enabled", JsonValue(true));
|
||||
temporal.set("historySource", JsonValue(TemporalHistorySourceToString(shaderPackage.temporal.historySource)));
|
||||
temporal.set("requestedHistoryLength", JsonValue(static_cast<double>(shaderPackage.temporal.requestedHistoryLength)));
|
||||
temporal.set("effectiveHistoryLength", JsonValue(static_cast<double>(shaderPackage.temporal.effectiveHistoryLength)));
|
||||
layerValue.set("temporal", temporal);
|
||||
}
|
||||
if (shaderPackage.feedback.enabled)
|
||||
{
|
||||
JsonValue feedback = JsonValue::MakeObject();
|
||||
feedback.set("enabled", JsonValue(true));
|
||||
feedback.set("writePass", JsonValue(shaderPackage.feedback.writePassId));
|
||||
layerValue.set("feedback", feedback);
|
||||
}
|
||||
|
||||
JsonValue parameters = JsonValue::MakeArray();
|
||||
for (const ShaderParameterDefinition& definition : shaderPackage.parameters)
|
||||
{
|
||||
JsonValue parameter = JsonValue::MakeObject();
|
||||
parameter.set("id", JsonValue(definition.id));
|
||||
parameter.set("label", JsonValue(definition.label));
|
||||
if (!definition.description.empty())
|
||||
parameter.set("description", JsonValue(definition.description));
|
||||
parameter.set("type", JsonValue(ShaderParameterTypeToString(definition.type)));
|
||||
parameter.set("defaultValue", SerializeParameterValue(definition, DefaultValueForDefinition(definition)));
|
||||
|
||||
if (!definition.minNumbers.empty())
|
||||
{
|
||||
JsonValue minValue = JsonValue::MakeArray();
|
||||
for (double number : definition.minNumbers)
|
||||
minValue.pushBack(JsonValue(number));
|
||||
parameter.set("min", minValue);
|
||||
}
|
||||
if (!definition.maxNumbers.empty())
|
||||
{
|
||||
JsonValue maxValue = JsonValue::MakeArray();
|
||||
for (double number : definition.maxNumbers)
|
||||
maxValue.pushBack(JsonValue(number));
|
||||
parameter.set("max", maxValue);
|
||||
}
|
||||
if (!definition.stepNumbers.empty())
|
||||
{
|
||||
JsonValue stepValue = JsonValue::MakeArray();
|
||||
for (double number : definition.stepNumbers)
|
||||
stepValue.pushBack(JsonValue(number));
|
||||
parameter.set("step", stepValue);
|
||||
}
|
||||
if (definition.type == ShaderParameterType::Enum)
|
||||
{
|
||||
JsonValue options = JsonValue::MakeArray();
|
||||
for (const ShaderParameterOption& option : definition.enumOptions)
|
||||
{
|
||||
JsonValue optionValue = JsonValue::MakeObject();
|
||||
optionValue.set("value", JsonValue(option.value));
|
||||
optionValue.set("label", JsonValue(option.label));
|
||||
options.pushBack(optionValue);
|
||||
}
|
||||
parameter.set("options", options);
|
||||
}
|
||||
if (definition.type == ShaderParameterType::Text)
|
||||
{
|
||||
parameter.set("maxLength", JsonValue(static_cast<double>(definition.maxLength)));
|
||||
if (!definition.fontId.empty())
|
||||
parameter.set("font", JsonValue(definition.fontId));
|
||||
}
|
||||
|
||||
ShaderParameterValue value = DefaultValueForDefinition(definition);
|
||||
auto valueIt = layer.parameterValues.find(definition.id);
|
||||
if (valueIt != layer.parameterValues.end())
|
||||
value = valueIt->second;
|
||||
parameter.set("value", SerializeParameterValue(definition, value));
|
||||
parameters.pushBack(parameter);
|
||||
}
|
||||
|
||||
layerValue.set("parameters", parameters);
|
||||
layersValue.pushBack(layerValue);
|
||||
}
|
||||
return layersValue;
|
||||
}
|
||||
|
||||
JsonValue RuntimeStateJson::SerializeParameterValue(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:
|
||||
return JsonValue(value.numberValues.empty() ? 0.0 : value.numberValues.front());
|
||||
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();
|
||||
}
|
||||
|
||||
std::string RuntimeStateJson::TemporalHistorySourceToString(TemporalHistorySource source)
|
||||
{
|
||||
switch (source)
|
||||
{
|
||||
case TemporalHistorySource::Source:
|
||||
return "source";
|
||||
case TemporalHistorySource::PreLayerInput:
|
||||
return "preLayerInput";
|
||||
case TemporalHistorySource::None:
|
||||
default:
|
||||
return "none";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
#pragma once
|
||||
|
||||
#include "LayerStackStore.h"
|
||||
#include "RuntimeJson.h"
|
||||
#include "ShaderPackageCatalog.h"
|
||||
#include "ShaderTypes.h"
|
||||
|
||||
#include <map>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
class RuntimeStateJson
|
||||
{
|
||||
public:
|
||||
static JsonValue SerializeLayerStack(const LayerStackStore& layerStack, const ShaderPackageCatalog& shaderCatalog);
|
||||
static JsonValue SerializeLayerStack(const std::vector<LayerStackStore::LayerPersistentState>& layers, const std::map<std::string, ShaderPackage>& packagesById);
|
||||
static JsonValue SerializeParameterValue(const ShaderParameterDefinition& definition, const ShaderParameterValue& value);
|
||||
static std::string TemporalHistorySourceToString(TemporalHistorySource source);
|
||||
};
|
||||
@@ -0,0 +1,145 @@
|
||||
#include "RuntimeStatePresenter.h"
|
||||
|
||||
#include "RuntimeStateJson.h"
|
||||
#include "RuntimeStore.h"
|
||||
|
||||
std::string RuntimeStatePresenter::BuildRuntimeStateJson(const RuntimeStore& runtimeStore)
|
||||
{
|
||||
return SerializeJson(BuildRuntimeStateValue(runtimeStore), true);
|
||||
}
|
||||
|
||||
JsonValue RuntimeStatePresenter::BuildRuntimeStateValue(const RuntimeStore& runtimeStore)
|
||||
{
|
||||
const RuntimeStatePresentationReadModel model = runtimeStore.BuildRuntimeStatePresentationReadModel();
|
||||
const HealthTelemetry::Snapshot& telemetrySnapshot = model.telemetry;
|
||||
|
||||
JsonValue root = JsonValue::MakeObject();
|
||||
|
||||
JsonValue app = JsonValue::MakeObject();
|
||||
app.set("serverPort", JsonValue(static_cast<double>(model.serverPort)));
|
||||
app.set("oscPort", JsonValue(static_cast<double>(model.config.oscPort)));
|
||||
app.set("oscBindAddress", JsonValue(model.config.oscBindAddress));
|
||||
app.set("oscSmoothing", JsonValue(model.config.oscSmoothing));
|
||||
app.set("autoReload", JsonValue(model.autoReloadEnabled));
|
||||
app.set("maxTemporalHistoryFrames", JsonValue(static_cast<double>(model.config.maxTemporalHistoryFrames)));
|
||||
app.set("previewFps", JsonValue(static_cast<double>(model.config.previewFps)));
|
||||
app.set("enableExternalKeying", JsonValue(model.config.enableExternalKeying));
|
||||
app.set("inputVideoFormat", JsonValue(model.config.inputVideoFormat));
|
||||
app.set("inputFrameRate", JsonValue(model.config.inputFrameRate));
|
||||
app.set("outputVideoFormat", JsonValue(model.config.outputVideoFormat));
|
||||
app.set("outputFrameRate", JsonValue(model.config.outputFrameRate));
|
||||
root.set("app", app);
|
||||
|
||||
JsonValue runtime = JsonValue::MakeObject();
|
||||
runtime.set("layerCount", JsonValue(static_cast<double>(model.layerStack.LayerCount())));
|
||||
runtime.set("compileSucceeded", JsonValue(model.compileSucceeded));
|
||||
runtime.set("compileMessage", JsonValue(model.compileMessage));
|
||||
root.set("runtime", runtime);
|
||||
|
||||
JsonValue video = JsonValue::MakeObject();
|
||||
video.set("hasSignal", JsonValue(telemetrySnapshot.signal.hasSignal));
|
||||
video.set("width", JsonValue(static_cast<double>(telemetrySnapshot.signal.width)));
|
||||
video.set("height", JsonValue(static_cast<double>(telemetrySnapshot.signal.height)));
|
||||
video.set("modeName", JsonValue(telemetrySnapshot.signal.modeName));
|
||||
root.set("video", video);
|
||||
|
||||
JsonValue deckLink = JsonValue::MakeObject();
|
||||
deckLink.set("modelName", JsonValue(telemetrySnapshot.videoIO.modelName));
|
||||
deckLink.set("supportsInternalKeying", JsonValue(telemetrySnapshot.videoIO.supportsInternalKeying));
|
||||
deckLink.set("supportsExternalKeying", JsonValue(telemetrySnapshot.videoIO.supportsExternalKeying));
|
||||
deckLink.set("keyerInterfaceAvailable", JsonValue(telemetrySnapshot.videoIO.keyerInterfaceAvailable));
|
||||
deckLink.set("externalKeyingRequested", JsonValue(telemetrySnapshot.videoIO.externalKeyingRequested));
|
||||
deckLink.set("externalKeyingActive", JsonValue(telemetrySnapshot.videoIO.externalKeyingActive));
|
||||
deckLink.set("statusMessage", JsonValue(telemetrySnapshot.videoIO.statusMessage));
|
||||
root.set("decklink", deckLink);
|
||||
|
||||
JsonValue videoIO = JsonValue::MakeObject();
|
||||
videoIO.set("backend", JsonValue(telemetrySnapshot.videoIO.backendName));
|
||||
videoIO.set("modelName", JsonValue(telemetrySnapshot.videoIO.modelName));
|
||||
videoIO.set("supportsInternalKeying", JsonValue(telemetrySnapshot.videoIO.supportsInternalKeying));
|
||||
videoIO.set("supportsExternalKeying", JsonValue(telemetrySnapshot.videoIO.supportsExternalKeying));
|
||||
videoIO.set("keyerInterfaceAvailable", JsonValue(telemetrySnapshot.videoIO.keyerInterfaceAvailable));
|
||||
videoIO.set("externalKeyingRequested", JsonValue(telemetrySnapshot.videoIO.externalKeyingRequested));
|
||||
videoIO.set("externalKeyingActive", JsonValue(telemetrySnapshot.videoIO.externalKeyingActive));
|
||||
videoIO.set("statusMessage", JsonValue(telemetrySnapshot.videoIO.statusMessage));
|
||||
root.set("videoIO", videoIO);
|
||||
|
||||
JsonValue performance = JsonValue::MakeObject();
|
||||
performance.set("frameBudgetMs", JsonValue(telemetrySnapshot.performance.frameBudgetMilliseconds));
|
||||
performance.set("renderMs", JsonValue(telemetrySnapshot.performance.renderMilliseconds));
|
||||
performance.set("smoothedRenderMs", JsonValue(telemetrySnapshot.performance.smoothedRenderMilliseconds));
|
||||
performance.set("budgetUsedPercent", JsonValue(
|
||||
telemetrySnapshot.performance.frameBudgetMilliseconds > 0.0
|
||||
? (telemetrySnapshot.performance.smoothedRenderMilliseconds / telemetrySnapshot.performance.frameBudgetMilliseconds) * 100.0
|
||||
: 0.0));
|
||||
performance.set("completionIntervalMs", JsonValue(telemetrySnapshot.performance.completionIntervalMilliseconds));
|
||||
performance.set("smoothedCompletionIntervalMs", JsonValue(telemetrySnapshot.performance.smoothedCompletionIntervalMilliseconds));
|
||||
performance.set("maxCompletionIntervalMs", JsonValue(telemetrySnapshot.performance.maxCompletionIntervalMilliseconds));
|
||||
performance.set("lateFrameCount", JsonValue(static_cast<double>(telemetrySnapshot.performance.lateFrameCount)));
|
||||
performance.set("droppedFrameCount", JsonValue(static_cast<double>(telemetrySnapshot.performance.droppedFrameCount)));
|
||||
performance.set("flushedFrameCount", JsonValue(static_cast<double>(telemetrySnapshot.performance.flushedFrameCount)));
|
||||
root.set("performance", performance);
|
||||
|
||||
JsonValue eventQueue = JsonValue::MakeObject();
|
||||
eventQueue.set("name", JsonValue(telemetrySnapshot.runtimeEvents.queue.queueName));
|
||||
eventQueue.set("depth", JsonValue(static_cast<double>(telemetrySnapshot.runtimeEvents.queue.depth)));
|
||||
eventQueue.set("capacity", JsonValue(static_cast<double>(telemetrySnapshot.runtimeEvents.queue.capacity)));
|
||||
eventQueue.set("droppedCount", JsonValue(static_cast<double>(telemetrySnapshot.runtimeEvents.queue.droppedCount)));
|
||||
eventQueue.set("oldestEventAgeMs", JsonValue(telemetrySnapshot.runtimeEvents.queue.oldestEventAgeMilliseconds));
|
||||
|
||||
JsonValue eventDispatch = JsonValue::MakeObject();
|
||||
eventDispatch.set("dispatchCallCount", JsonValue(static_cast<double>(telemetrySnapshot.runtimeEvents.dispatch.dispatchCallCount)));
|
||||
eventDispatch.set("dispatchedEventCount", JsonValue(static_cast<double>(telemetrySnapshot.runtimeEvents.dispatch.dispatchedEventCount)));
|
||||
eventDispatch.set("handlerInvocationCount", JsonValue(static_cast<double>(telemetrySnapshot.runtimeEvents.dispatch.handlerInvocationCount)));
|
||||
eventDispatch.set("handlerFailureCount", JsonValue(static_cast<double>(telemetrySnapshot.runtimeEvents.dispatch.handlerFailureCount)));
|
||||
eventDispatch.set("lastDispatchDurationMs", JsonValue(telemetrySnapshot.runtimeEvents.dispatch.lastDispatchDurationMilliseconds));
|
||||
eventDispatch.set("maxDispatchDurationMs", JsonValue(telemetrySnapshot.runtimeEvents.dispatch.maxDispatchDurationMilliseconds));
|
||||
|
||||
JsonValue runtimeEvents = JsonValue::MakeObject();
|
||||
runtimeEvents.set("queue", eventQueue);
|
||||
runtimeEvents.set("dispatch", eventDispatch);
|
||||
root.set("runtimeEvents", runtimeEvents);
|
||||
|
||||
JsonValue shaderLibrary = JsonValue::MakeArray();
|
||||
for (const ShaderPackageStatus& status : model.packageStatuses)
|
||||
{
|
||||
JsonValue shader = JsonValue::MakeObject();
|
||||
shader.set("id", JsonValue(status.id));
|
||||
shader.set("name", JsonValue(status.displayName));
|
||||
shader.set("description", JsonValue(status.description));
|
||||
shader.set("category", JsonValue(status.category));
|
||||
shader.set("available", JsonValue(status.available));
|
||||
if (!status.available)
|
||||
shader.set("error", JsonValue(status.error));
|
||||
|
||||
auto shaderIt = model.shaderCatalog.packagesById.find(status.id);
|
||||
if (status.available && shaderIt != model.shaderCatalog.packagesById.end() && shaderIt->second.temporal.enabled)
|
||||
{
|
||||
const ShaderPackage& shaderPackage = shaderIt->second;
|
||||
JsonValue temporal = JsonValue::MakeObject();
|
||||
temporal.set("enabled", JsonValue(true));
|
||||
temporal.set("historySource", JsonValue(RuntimeStateJson::TemporalHistorySourceToString(shaderPackage.temporal.historySource)));
|
||||
temporal.set("requestedHistoryLength", JsonValue(static_cast<double>(shaderPackage.temporal.requestedHistoryLength)));
|
||||
temporal.set("effectiveHistoryLength", JsonValue(static_cast<double>(shaderPackage.temporal.effectiveHistoryLength)));
|
||||
shader.set("temporal", temporal);
|
||||
}
|
||||
if (status.available && shaderIt != model.shaderCatalog.packagesById.end() && shaderIt->second.feedback.enabled)
|
||||
{
|
||||
const ShaderPackage& shaderPackage = shaderIt->second;
|
||||
JsonValue feedback = JsonValue::MakeObject();
|
||||
feedback.set("enabled", JsonValue(true));
|
||||
feedback.set("writePass", JsonValue(shaderPackage.feedback.writePassId));
|
||||
shader.set("feedback", feedback);
|
||||
}
|
||||
shaderLibrary.pushBack(shader);
|
||||
}
|
||||
root.set("shaders", shaderLibrary);
|
||||
|
||||
JsonValue stackPresets = JsonValue::MakeArray();
|
||||
for (const std::string& presetName : model.stackPresetNames)
|
||||
stackPresets.pushBack(JsonValue(presetName));
|
||||
root.set("stackPresets", stackPresets);
|
||||
|
||||
root.set("layers", RuntimeStateJson::SerializeLayerStack(model.layerStack.Layers(), model.shaderCatalog.packagesById));
|
||||
return root;
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
#pragma once
|
||||
|
||||
#include "RuntimeJson.h"
|
||||
|
||||
#include <string>
|
||||
|
||||
class RuntimeStore;
|
||||
|
||||
class RuntimeStatePresenter
|
||||
{
|
||||
public:
|
||||
static std::string BuildRuntimeStateJson(const RuntimeStore& runtimeStore);
|
||||
static JsonValue BuildRuntimeStateValue(const RuntimeStore& runtimeStore);
|
||||
};
|
||||
@@ -0,0 +1,193 @@
|
||||
#include "RenderSnapshotBuilder.h"
|
||||
|
||||
#include "RuntimeClock.h"
|
||||
#include "RuntimeParameterUtils.h"
|
||||
#include "RuntimeStore.h"
|
||||
#include "ShaderCompiler.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <chrono>
|
||||
#include <filesystem>
|
||||
#include <mutex>
|
||||
#include <utility>
|
||||
|
||||
RenderSnapshotBuilder::RenderSnapshotBuilder(RuntimeStore& runtimeStore) :
|
||||
mRuntimeStore(runtimeStore)
|
||||
{
|
||||
}
|
||||
|
||||
bool RenderSnapshotBuilder::BuildLayerPassFragmentShaderSources(const std::string& layerId, std::vector<ShaderPassBuildSource>& passSources, std::string& error) const
|
||||
{
|
||||
try
|
||||
{
|
||||
ShaderPackage shaderPackage;
|
||||
if (!mRuntimeStore.CopyShaderPackageForStoredLayer(layerId, shaderPackage, error))
|
||||
return false;
|
||||
|
||||
const ShaderCompilerInputs inputs = mRuntimeStore.GetShaderCompilerInputs();
|
||||
|
||||
ShaderCompiler compiler(
|
||||
inputs.repoRoot,
|
||||
inputs.wrapperPath,
|
||||
inputs.generatedGlslPath,
|
||||
inputs.patchedGlslPath,
|
||||
inputs.maxTemporalHistoryFrames);
|
||||
passSources.clear();
|
||||
passSources.reserve(shaderPackage.passes.size());
|
||||
for (const ShaderPassDefinition& pass : shaderPackage.passes)
|
||||
{
|
||||
ShaderPassBuildSource passSource;
|
||||
passSource.passId = pass.id;
|
||||
passSource.inputNames = pass.inputNames;
|
||||
passSource.outputName = pass.outputName;
|
||||
if (!compiler.BuildPassFragmentShaderSource(shaderPackage, pass, passSource.fragmentShaderSource, error))
|
||||
return false;
|
||||
passSources.push_back(std::move(passSource));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
catch (const std::exception& exception)
|
||||
{
|
||||
error = std::string("RenderSnapshotBuilder::BuildLayerPassFragmentShaderSources exception: ") + exception.what();
|
||||
return false;
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
error = "RenderSnapshotBuilder::BuildLayerPassFragmentShaderSources threw a non-standard exception.";
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
unsigned RenderSnapshotBuilder::GetMaxTemporalHistoryFrames() const
|
||||
{
|
||||
return mRuntimeStore.GetConfiguredMaxTemporalHistoryFrames();
|
||||
}
|
||||
|
||||
RuntimeSnapshotVersions RenderSnapshotBuilder::GetVersions() const
|
||||
{
|
||||
RuntimeSnapshotVersions versions;
|
||||
versions.renderStateVersion = mRenderStateVersion.load(std::memory_order_relaxed);
|
||||
versions.parameterStateVersion = mParameterStateVersion.load(std::memory_order_relaxed);
|
||||
return versions;
|
||||
}
|
||||
|
||||
void RenderSnapshotBuilder::AdvanceFrame()
|
||||
{
|
||||
++mFrameCounter;
|
||||
}
|
||||
|
||||
void RenderSnapshotBuilder::BuildLayerRenderStates(unsigned outputWidth, unsigned outputHeight, std::vector<RuntimeRenderState>& states) const
|
||||
{
|
||||
BuildLayerRenderStates(outputWidth, outputHeight, mRuntimeStore.BuildRenderSnapshotReadModel(), states);
|
||||
}
|
||||
|
||||
bool RenderSnapshotBuilder::TryBuildLayerRenderStates(unsigned outputWidth, unsigned outputHeight, std::vector<RuntimeRenderState>& states) const
|
||||
{
|
||||
BuildLayerRenderStates(outputWidth, outputHeight, mRuntimeStore.BuildRenderSnapshotReadModel(), states);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool RenderSnapshotBuilder::TryRefreshLayerParameters(std::vector<RuntimeRenderState>& states) const
|
||||
{
|
||||
RefreshLayerParameters(mRuntimeStore.CopyCommittedLiveLayerStates(), states);
|
||||
return true;
|
||||
}
|
||||
|
||||
void RenderSnapshotBuilder::RefreshDynamicRenderStateFields(std::vector<RuntimeRenderState>& states) const
|
||||
{
|
||||
RefreshDynamicRenderStateFields(mRuntimeStore.GetRenderTimingSnapshot(), states);
|
||||
}
|
||||
|
||||
void RenderSnapshotBuilder::MarkRenderStateDirty()
|
||||
{
|
||||
mRenderStateVersion.fetch_add(1, std::memory_order_relaxed);
|
||||
mParameterStateVersion.fetch_add(1, std::memory_order_relaxed);
|
||||
}
|
||||
|
||||
void RenderSnapshotBuilder::MarkParameterStateDirty()
|
||||
{
|
||||
mParameterStateVersion.fetch_add(1, std::memory_order_relaxed);
|
||||
}
|
||||
|
||||
void RenderSnapshotBuilder::BuildLayerRenderStates(unsigned outputWidth, unsigned outputHeight, const RenderSnapshotReadModel& readModel, std::vector<RuntimeRenderState>& states) const
|
||||
{
|
||||
states.clear();
|
||||
|
||||
for (const LayerStackStore::LayerPersistentState& layer : readModel.committedLiveState.layers)
|
||||
{
|
||||
auto shaderIt = readModel.committedLiveState.packagesById.find(layer.shaderId);
|
||||
if (shaderIt == readModel.committedLiveState.packagesById.end())
|
||||
continue;
|
||||
const ShaderPackage& shaderPackage = shaderIt->second;
|
||||
|
||||
RuntimeRenderState state;
|
||||
state.layerId = layer.id;
|
||||
state.shaderId = layer.shaderId;
|
||||
state.shaderName = shaderPackage.displayName;
|
||||
state.mixAmount = 1.0;
|
||||
state.bypass = layer.bypass ? 1.0 : 0.0;
|
||||
state.inputWidth = readModel.signalStatus.width;
|
||||
state.inputHeight = readModel.signalStatus.height;
|
||||
state.outputWidth = outputWidth;
|
||||
state.outputHeight = outputHeight;
|
||||
state.parameterDefinitions = shaderPackage.parameters;
|
||||
state.textureAssets = shaderPackage.textureAssets;
|
||||
state.fontAssets = shaderPackage.fontAssets;
|
||||
state.isTemporal = shaderPackage.temporal.enabled;
|
||||
state.temporalHistorySource = shaderPackage.temporal.historySource;
|
||||
state.requestedTemporalHistoryLength = shaderPackage.temporal.requestedHistoryLength;
|
||||
state.effectiveTemporalHistoryLength = shaderPackage.temporal.effectiveHistoryLength;
|
||||
state.feedback = shaderPackage.feedback;
|
||||
|
||||
for (const ShaderParameterDefinition& definition : shaderPackage.parameters)
|
||||
{
|
||||
ShaderParameterValue value = DefaultValueForDefinition(definition);
|
||||
auto valueIt = layer.parameterValues.find(definition.id);
|
||||
if (valueIt != layer.parameterValues.end())
|
||||
value = valueIt->second;
|
||||
state.parameterValues[definition.id] = value;
|
||||
}
|
||||
|
||||
states.push_back(state);
|
||||
}
|
||||
|
||||
RefreshDynamicRenderStateFields(readModel.timing, states);
|
||||
}
|
||||
|
||||
void RenderSnapshotBuilder::RefreshLayerParameters(const std::vector<LayerStackStore::LayerPersistentState>& layers, std::vector<RuntimeRenderState>& states) const
|
||||
{
|
||||
for (RuntimeRenderState& state : states)
|
||||
{
|
||||
const auto layerIt = std::find_if(layers.begin(), layers.end(),
|
||||
[&state](const LayerStackStore::LayerPersistentState& layer) { return layer.id == state.layerId; });
|
||||
if (layerIt == 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void RenderSnapshotBuilder::RefreshDynamicRenderStateFields(const RenderTimingSnapshot& timing, std::vector<RuntimeRenderState>& states) const
|
||||
{
|
||||
const RuntimeClockSnapshot clock = GetRuntimeClockSnapshot();
|
||||
const double timeSeconds = std::chrono::duration_cast<std::chrono::duration<double>>(std::chrono::steady_clock::now() - timing.startTime).count();
|
||||
const double frameCount = static_cast<double>(mFrameCounter.load(std::memory_order_relaxed));
|
||||
|
||||
for (RuntimeRenderState& state : states)
|
||||
{
|
||||
state.timeSeconds = timeSeconds;
|
||||
state.utcTimeSeconds = clock.utcTimeSeconds;
|
||||
state.utcOffsetSeconds = clock.utcOffsetSeconds;
|
||||
state.startupRandom = timing.startupRandom;
|
||||
state.frameCount = frameCount;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
#pragma once
|
||||
|
||||
#include "RuntimeStoreReadModels.h"
|
||||
#include "ShaderTypes.h"
|
||||
|
||||
#include <atomic>
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
class RuntimeStore;
|
||||
|
||||
struct RuntimeSnapshotVersions
|
||||
{
|
||||
uint64_t renderStateVersion = 0;
|
||||
uint64_t parameterStateVersion = 0;
|
||||
};
|
||||
|
||||
class RenderSnapshotBuilder
|
||||
{
|
||||
public:
|
||||
explicit RenderSnapshotBuilder(RuntimeStore& runtimeStore);
|
||||
|
||||
bool BuildLayerPassFragmentShaderSources(const std::string& layerId, std::vector<ShaderPassBuildSource>& passSources, std::string& error) const;
|
||||
unsigned GetMaxTemporalHistoryFrames() const;
|
||||
RuntimeSnapshotVersions GetVersions() const;
|
||||
void AdvanceFrame();
|
||||
void BuildLayerRenderStates(unsigned outputWidth, unsigned outputHeight, std::vector<RuntimeRenderState>& states) const;
|
||||
bool TryBuildLayerRenderStates(unsigned outputWidth, unsigned outputHeight, std::vector<RuntimeRenderState>& states) const;
|
||||
bool TryRefreshLayerParameters(std::vector<RuntimeRenderState>& states) const;
|
||||
void RefreshDynamicRenderStateFields(std::vector<RuntimeRenderState>& states) const;
|
||||
void MarkRenderStateDirty();
|
||||
void MarkParameterStateDirty();
|
||||
|
||||
private:
|
||||
void BuildLayerRenderStates(unsigned outputWidth, unsigned outputHeight, const RenderSnapshotReadModel& readModel, std::vector<RuntimeRenderState>& states) const;
|
||||
void RefreshLayerParameters(const std::vector<LayerStackStore::LayerPersistentState>& layers, std::vector<RuntimeRenderState>& states) const;
|
||||
void RefreshDynamicRenderStateFields(const RenderTimingSnapshot& timing, std::vector<RuntimeRenderState>& states) const;
|
||||
|
||||
RuntimeStore& mRuntimeStore;
|
||||
std::atomic<uint64_t> mFrameCounter{ 0 };
|
||||
std::atomic<uint64_t> mRenderStateVersion{ 0 };
|
||||
std::atomic<uint64_t> mParameterStateVersion{ 0 };
|
||||
};
|
||||
@@ -0,0 +1,196 @@
|
||||
#include "RuntimeSnapshotProvider.h"
|
||||
|
||||
#include "RuntimeEventDispatcher.h"
|
||||
|
||||
#include <windows.h>
|
||||
|
||||
#include <utility>
|
||||
|
||||
RuntimeSnapshotProvider::RuntimeSnapshotProvider(RenderSnapshotBuilder& renderSnapshotBuilder, RuntimeEventDispatcher& runtimeEventDispatcher) :
|
||||
mRenderSnapshotBuilder(renderSnapshotBuilder),
|
||||
mRuntimeEventDispatcher(runtimeEventDispatcher)
|
||||
{
|
||||
}
|
||||
|
||||
bool RuntimeSnapshotProvider::BuildLayerPassFragmentShaderSources(const std::string& layerId, std::vector<ShaderPassBuildSource>& passSources, std::string& error) const
|
||||
{
|
||||
try
|
||||
{
|
||||
return mRenderSnapshotBuilder.BuildLayerPassFragmentShaderSources(layerId, passSources, error);
|
||||
}
|
||||
catch (const std::exception& exception)
|
||||
{
|
||||
error = std::string("RuntimeSnapshotProvider::BuildLayerPassFragmentShaderSources exception: ") + exception.what();
|
||||
return false;
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
error = "RuntimeSnapshotProvider::BuildLayerPassFragmentShaderSources threw a non-standard exception.";
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
unsigned RuntimeSnapshotProvider::GetMaxTemporalHistoryFrames() const
|
||||
{
|
||||
return mRenderSnapshotBuilder.GetMaxTemporalHistoryFrames();
|
||||
}
|
||||
|
||||
RuntimeSnapshotVersions RuntimeSnapshotProvider::GetVersions() const
|
||||
{
|
||||
return mRenderSnapshotBuilder.GetVersions();
|
||||
}
|
||||
|
||||
void RuntimeSnapshotProvider::AdvanceFrame()
|
||||
{
|
||||
mRenderSnapshotBuilder.AdvanceFrame();
|
||||
}
|
||||
|
||||
RuntimeRenderStateSnapshot RuntimeSnapshotProvider::PublishRenderStateSnapshot(unsigned outputWidth, unsigned outputHeight) const
|
||||
{
|
||||
PublishRenderSnapshotPublishRequested(outputWidth, outputHeight, "publish-render-state-snapshot");
|
||||
|
||||
for (;;)
|
||||
{
|
||||
const RuntimeSnapshotVersions versionsBefore = GetVersions();
|
||||
RuntimeRenderStateSnapshot publishedSnapshot;
|
||||
if (TryGetPublishedRenderStateSnapshot(outputWidth, outputHeight, versionsBefore, publishedSnapshot))
|
||||
{
|
||||
PublishRenderSnapshotPublished(publishedSnapshot);
|
||||
return publishedSnapshot;
|
||||
}
|
||||
|
||||
RuntimeRenderStateSnapshot snapshot;
|
||||
snapshot.outputWidth = outputWidth;
|
||||
snapshot.outputHeight = outputHeight;
|
||||
mRenderSnapshotBuilder.BuildLayerRenderStates(outputWidth, outputHeight, snapshot.states);
|
||||
|
||||
const RuntimeSnapshotVersions versionsAfter = GetVersions();
|
||||
if (versionsBefore.renderStateVersion == versionsAfter.renderStateVersion &&
|
||||
versionsBefore.parameterStateVersion == versionsAfter.parameterStateVersion)
|
||||
{
|
||||
snapshot.versions = versionsAfter;
|
||||
StorePublishedRenderStateSnapshot(snapshot);
|
||||
PublishRenderSnapshotPublished(snapshot);
|
||||
return snapshot;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool RuntimeSnapshotProvider::TryPublishRenderStateSnapshot(unsigned outputWidth, unsigned outputHeight, RuntimeRenderStateSnapshot& snapshot) const
|
||||
{
|
||||
PublishRenderSnapshotPublishRequested(outputWidth, outputHeight, "try-publish-render-state-snapshot");
|
||||
|
||||
const RuntimeSnapshotVersions versionsBefore = GetVersions();
|
||||
if (TryGetPublishedRenderStateSnapshot(outputWidth, outputHeight, versionsBefore, snapshot))
|
||||
{
|
||||
PublishRenderSnapshotPublished(snapshot);
|
||||
return true;
|
||||
}
|
||||
|
||||
std::vector<RuntimeRenderState> states;
|
||||
if (!mRenderSnapshotBuilder.TryBuildLayerRenderStates(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);
|
||||
StorePublishedRenderStateSnapshot(snapshot);
|
||||
PublishRenderSnapshotPublished(snapshot);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool RuntimeSnapshotProvider::TryRefreshPublishedSnapshotParameters(RuntimeRenderStateSnapshot& snapshot) const
|
||||
{
|
||||
const uint64_t expectedRenderStateVersion = snapshot.versions.renderStateVersion;
|
||||
if (!mRenderSnapshotBuilder.TryRefreshLayerParameters(snapshot.states))
|
||||
return false;
|
||||
|
||||
const RuntimeSnapshotVersions versions = GetVersions();
|
||||
if (versions.renderStateVersion != expectedRenderStateVersion)
|
||||
return false;
|
||||
|
||||
snapshot.versions = versions;
|
||||
StorePublishedRenderStateSnapshot(snapshot);
|
||||
PublishRenderSnapshotPublished(snapshot);
|
||||
return true;
|
||||
}
|
||||
|
||||
void RuntimeSnapshotProvider::RefreshDynamicRenderStateFields(std::vector<RuntimeRenderState>& states) const
|
||||
{
|
||||
mRenderSnapshotBuilder.RefreshDynamicRenderStateFields(states);
|
||||
}
|
||||
|
||||
bool RuntimeSnapshotProvider::TryGetPublishedRenderStateSnapshot(unsigned outputWidth, unsigned outputHeight,
|
||||
const RuntimeSnapshotVersions& versions, RuntimeRenderStateSnapshot& snapshot) const
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mPublishedSnapshotMutex);
|
||||
if (!mHasPublishedRenderStateSnapshot ||
|
||||
!SnapshotMatches(mPublishedRenderStateSnapshot, outputWidth, outputHeight, versions))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
snapshot = mPublishedRenderStateSnapshot;
|
||||
return true;
|
||||
}
|
||||
|
||||
void RuntimeSnapshotProvider::StorePublishedRenderStateSnapshot(const RuntimeRenderStateSnapshot& snapshot) const
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mPublishedSnapshotMutex);
|
||||
mPublishedRenderStateSnapshot = snapshot;
|
||||
mHasPublishedRenderStateSnapshot = true;
|
||||
}
|
||||
|
||||
bool RuntimeSnapshotProvider::SnapshotMatches(const RuntimeRenderStateSnapshot& snapshot, unsigned outputWidth, unsigned outputHeight,
|
||||
const RuntimeSnapshotVersions& versions)
|
||||
{
|
||||
return snapshot.outputWidth == outputWidth &&
|
||||
snapshot.outputHeight == outputHeight &&
|
||||
snapshot.versions.renderStateVersion == versions.renderStateVersion &&
|
||||
snapshot.versions.parameterStateVersion == versions.parameterStateVersion;
|
||||
}
|
||||
|
||||
void RuntimeSnapshotProvider::PublishRenderSnapshotPublishRequested(unsigned outputWidth, unsigned outputHeight, const std::string& reason) const
|
||||
{
|
||||
try
|
||||
{
|
||||
RenderSnapshotPublishRequestedEvent event;
|
||||
event.outputWidth = outputWidth;
|
||||
event.outputHeight = outputHeight;
|
||||
event.reason = reason;
|
||||
if (!mRuntimeEventDispatcher.PublishPayload(event, "RuntimeSnapshotProvider"))
|
||||
OutputDebugStringA("RenderSnapshotPublishRequested event publish failed.\n");
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
OutputDebugStringA("RenderSnapshotPublishRequested event publish threw.\n");
|
||||
}
|
||||
}
|
||||
|
||||
void RuntimeSnapshotProvider::PublishRenderSnapshotPublished(const RuntimeRenderStateSnapshot& snapshot) const
|
||||
{
|
||||
try
|
||||
{
|
||||
RenderSnapshotPublishedEvent event;
|
||||
event.snapshotVersion = snapshot.versions.renderStateVersion;
|
||||
event.structureVersion = snapshot.versions.renderStateVersion;
|
||||
event.parameterVersion = snapshot.versions.parameterStateVersion;
|
||||
event.outputWidth = snapshot.outputWidth;
|
||||
event.outputHeight = snapshot.outputHeight;
|
||||
event.layerCount = snapshot.states.size();
|
||||
if (!mRuntimeEventDispatcher.PublishPayload(event, "RuntimeSnapshotProvider"))
|
||||
OutputDebugStringA("RenderSnapshotPublished event publish failed.\n");
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
OutputDebugStringA("RenderSnapshotPublished event publish threw.\n");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
#pragma once
|
||||
|
||||
#include "RenderSnapshotBuilder.h"
|
||||
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
class RuntimeEventDispatcher;
|
||||
|
||||
struct RuntimeRenderStateSnapshot
|
||||
{
|
||||
RuntimeSnapshotVersions versions;
|
||||
unsigned outputWidth = 0;
|
||||
unsigned outputHeight = 0;
|
||||
std::vector<RuntimeRenderState> states;
|
||||
};
|
||||
|
||||
class RuntimeSnapshotProvider
|
||||
{
|
||||
public:
|
||||
RuntimeSnapshotProvider(RenderSnapshotBuilder& renderSnapshotBuilder, RuntimeEventDispatcher& runtimeEventDispatcher);
|
||||
|
||||
bool BuildLayerPassFragmentShaderSources(const std::string& layerId, std::vector<ShaderPassBuildSource>& passSources, std::string& error) const;
|
||||
unsigned GetMaxTemporalHistoryFrames() const;
|
||||
RuntimeSnapshotVersions GetVersions() const;
|
||||
void AdvanceFrame();
|
||||
RuntimeRenderStateSnapshot PublishRenderStateSnapshot(unsigned outputWidth, unsigned outputHeight) const;
|
||||
bool TryPublishRenderStateSnapshot(unsigned outputWidth, unsigned outputHeight, RuntimeRenderStateSnapshot& snapshot) const;
|
||||
bool TryRefreshPublishedSnapshotParameters(RuntimeRenderStateSnapshot& snapshot) const;
|
||||
void RefreshDynamicRenderStateFields(std::vector<RuntimeRenderState>& states) const;
|
||||
|
||||
private:
|
||||
bool TryGetPublishedRenderStateSnapshot(unsigned outputWidth, unsigned outputHeight,
|
||||
const RuntimeSnapshotVersions& versions, RuntimeRenderStateSnapshot& snapshot) const;
|
||||
void StorePublishedRenderStateSnapshot(const RuntimeRenderStateSnapshot& snapshot) const;
|
||||
static bool SnapshotMatches(const RuntimeRenderStateSnapshot& snapshot, unsigned outputWidth, unsigned outputHeight,
|
||||
const RuntimeSnapshotVersions& versions);
|
||||
void PublishRenderSnapshotPublishRequested(unsigned outputWidth, unsigned outputHeight, const std::string& reason) const;
|
||||
void PublishRenderSnapshotPublished(const RuntimeRenderStateSnapshot& snapshot) const;
|
||||
|
||||
RenderSnapshotBuilder& mRenderSnapshotBuilder;
|
||||
RuntimeEventDispatcher& mRuntimeEventDispatcher;
|
||||
mutable std::mutex mPublishedSnapshotMutex;
|
||||
mutable bool mHasPublishedRenderStateSnapshot = false;
|
||||
mutable RuntimeRenderStateSnapshot mPublishedRenderStateSnapshot;
|
||||
};
|
||||
@@ -0,0 +1,738 @@
|
||||
#include "LayerStackStore.h"
|
||||
|
||||
#include "RuntimeParameterUtils.h"
|
||||
#include "RuntimeStateJson.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
#include <cmath>
|
||||
#include <set>
|
||||
#include <sstream>
|
||||
#include <utility>
|
||||
|
||||
namespace
|
||||
{
|
||||
std::string TrimCopy(const std::string& text)
|
||||
{
|
||||
std::size_t start = 0;
|
||||
while (start < text.size() && std::isspace(static_cast<unsigned char>(text[start])))
|
||||
++start;
|
||||
|
||||
std::size_t end = text.size();
|
||||
while (end > start && std::isspace(static_cast<unsigned char>(text[end - 1])))
|
||||
--end;
|
||||
|
||||
return text.substr(start, end - start);
|
||||
}
|
||||
|
||||
std::string SimplifyControlKey(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 MatchesControlKey(const std::string& candidate, const std::string& key)
|
||||
{
|
||||
return candidate == key || SimplifyControlKey(candidate) == SimplifyControlKey(key);
|
||||
}
|
||||
|
||||
bool TryParseLayerIdNumber(const std::string& layerId, uint64_t& number)
|
||||
{
|
||||
const std::string prefix = "layer-";
|
||||
if (layerId.rfind(prefix, 0) != 0 || layerId.size() == prefix.size())
|
||||
return false;
|
||||
|
||||
uint64_t parsed = 0;
|
||||
for (std::size_t index = prefix.size(); index < layerId.size(); ++index)
|
||||
{
|
||||
const unsigned char ch = static_cast<unsigned char>(layerId[index]);
|
||||
if (!std::isdigit(ch))
|
||||
return false;
|
||||
parsed = parsed * 10 + static_cast<uint64_t>(ch - '0');
|
||||
}
|
||||
|
||||
number = parsed;
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
bool LayerStackStore::LoadPersistentStateValue(const JsonValue& root)
|
||||
{
|
||||
if (const JsonValue* layersValue = root.find("layers"))
|
||||
{
|
||||
for (const JsonValue& layerValue : layersValue->asArray())
|
||||
{
|
||||
if (!layerValue.isObject())
|
||||
continue;
|
||||
LayerPersistentState layer;
|
||||
if (const JsonValue* idValue = layerValue.find("id"))
|
||||
layer.id = idValue->asString();
|
||||
if (const JsonValue* shaderIdValue = layerValue.find("shaderId"))
|
||||
layer.shaderId = shaderIdValue->asString();
|
||||
if (const JsonValue* bypassValue = layerValue.find("bypass"))
|
||||
layer.bypass = bypassValue->asBoolean(false);
|
||||
else if (const JsonValue* enabledValue = layerValue.find("enabled"))
|
||||
layer.bypass = !enabledValue->asBoolean(true);
|
||||
|
||||
if (const JsonValue* parameterValues = layerValue.find("parameterValues"))
|
||||
{
|
||||
for (const auto& parameterItem : parameterValues->asObject())
|
||||
{
|
||||
ShaderParameterValue value;
|
||||
const JsonValue& jsonValue = parameterItem.second;
|
||||
if (jsonValue.isBoolean())
|
||||
value.booleanValue = jsonValue.asBoolean();
|
||||
else if (jsonValue.isString())
|
||||
value.enumValue = jsonValue.asString();
|
||||
else if (jsonValue.isNumber())
|
||||
value.numberValues.push_back(jsonValue.asNumber());
|
||||
else if (jsonValue.isArray())
|
||||
value.numberValues = JsonArrayToNumbers(jsonValue);
|
||||
layer.parameterValues[parameterItem.first] = value;
|
||||
}
|
||||
}
|
||||
|
||||
if (!layer.shaderId.empty())
|
||||
mLayers.push_back(layer);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
std::string activeShaderId;
|
||||
if (const JsonValue* activeShaderValue = root.find("activeShaderId"))
|
||||
activeShaderId = activeShaderValue->asString();
|
||||
|
||||
if (!activeShaderId.empty())
|
||||
{
|
||||
LayerPersistentState layer;
|
||||
layer.id = GenerateLayerId(mLayers, mNextLayerId);
|
||||
layer.shaderId = activeShaderId;
|
||||
layer.bypass = false;
|
||||
|
||||
if (const JsonValue* valuesByShader = root.find("parameterValuesByShader"))
|
||||
{
|
||||
const JsonValue* shaderValues = valuesByShader->find(activeShaderId);
|
||||
if (shaderValues)
|
||||
{
|
||||
for (const auto& parameterItem : shaderValues->asObject())
|
||||
{
|
||||
ShaderParameterValue value;
|
||||
const JsonValue& jsonValue = parameterItem.second;
|
||||
if (jsonValue.isBoolean())
|
||||
value.booleanValue = jsonValue.asBoolean();
|
||||
else if (jsonValue.isString())
|
||||
value.enumValue = jsonValue.asString();
|
||||
else if (jsonValue.isNumber())
|
||||
value.numberValues.push_back(jsonValue.asNumber());
|
||||
else if (jsonValue.isArray())
|
||||
value.numberValues = JsonArrayToNumbers(jsonValue);
|
||||
layer.parameterValues[parameterItem.first] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mLayers.push_back(layer);
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
JsonValue LayerStackStore::BuildPersistentStateValue(const ShaderPackageCatalog& shaderCatalog) const
|
||||
{
|
||||
JsonValue root = JsonValue::MakeObject();
|
||||
JsonValue layers = JsonValue::MakeArray();
|
||||
for (const LayerPersistentState& layer : mLayers)
|
||||
{
|
||||
JsonValue layerValue = JsonValue::MakeObject();
|
||||
layerValue.set("id", JsonValue(layer.id));
|
||||
layerValue.set("shaderId", JsonValue(layer.shaderId));
|
||||
layerValue.set("bypass", JsonValue(layer.bypass));
|
||||
|
||||
JsonValue parameterValues = JsonValue::MakeObject();
|
||||
const ShaderPackage* shaderPackage = shaderCatalog.FindPackage(layer.shaderId);
|
||||
for (const auto& parameterItem : layer.parameterValues)
|
||||
{
|
||||
const ShaderParameterDefinition* definition = nullptr;
|
||||
if (shaderPackage)
|
||||
{
|
||||
for (const ShaderParameterDefinition& candidate : shaderPackage->parameters)
|
||||
{
|
||||
if (candidate.id == parameterItem.first)
|
||||
{
|
||||
definition = &candidate;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (definition)
|
||||
parameterValues.set(parameterItem.first, RuntimeStateJson::SerializeParameterValue(*definition, parameterItem.second));
|
||||
}
|
||||
|
||||
layerValue.set("parameterValues", parameterValues);
|
||||
layers.pushBack(layerValue);
|
||||
}
|
||||
root.set("layers", layers);
|
||||
return root;
|
||||
}
|
||||
|
||||
void LayerStackStore::NormalizeLayerIds()
|
||||
{
|
||||
std::set<std::string> usedIds;
|
||||
uint64_t maxLayerNumber = mNextLayerId;
|
||||
|
||||
for (LayerPersistentState& layer : mLayers)
|
||||
{
|
||||
uint64_t layerNumber = 0;
|
||||
const bool hasReusableId = !layer.id.empty() &&
|
||||
usedIds.find(layer.id) == usedIds.end() &&
|
||||
TryParseLayerIdNumber(layer.id, layerNumber);
|
||||
|
||||
if (hasReusableId)
|
||||
{
|
||||
usedIds.insert(layer.id);
|
||||
maxLayerNumber = (std::max)(maxLayerNumber, layerNumber);
|
||||
continue;
|
||||
}
|
||||
|
||||
do
|
||||
{
|
||||
++maxLayerNumber;
|
||||
layer.id = "layer-" + std::to_string(maxLayerNumber);
|
||||
}
|
||||
while (usedIds.find(layer.id) != usedIds.end());
|
||||
|
||||
usedIds.insert(layer.id);
|
||||
}
|
||||
|
||||
mNextLayerId = maxLayerNumber;
|
||||
}
|
||||
|
||||
void LayerStackStore::EnsureDefaultsForAllLayers(const ShaderPackageCatalog& shaderCatalog)
|
||||
{
|
||||
for (LayerPersistentState& layer : mLayers)
|
||||
{
|
||||
const ShaderPackage* shaderPackage = shaderCatalog.FindPackage(layer.shaderId);
|
||||
if (shaderPackage)
|
||||
EnsureLayerDefaults(layer, *shaderPackage);
|
||||
}
|
||||
}
|
||||
|
||||
void LayerStackStore::EnsureDefaultLayer(const ShaderPackageCatalog& shaderCatalog)
|
||||
{
|
||||
if (!mLayers.empty() || shaderCatalog.PackageOrder().empty())
|
||||
return;
|
||||
|
||||
LayerPersistentState layer;
|
||||
layer.id = GenerateLayerId(mLayers, mNextLayerId);
|
||||
layer.shaderId = shaderCatalog.PackageOrder().front();
|
||||
layer.bypass = false;
|
||||
if (const ShaderPackage* shaderPackage = shaderCatalog.FindPackage(layer.shaderId))
|
||||
EnsureLayerDefaults(layer, *shaderPackage);
|
||||
mLayers.push_back(layer);
|
||||
}
|
||||
|
||||
void LayerStackStore::RemoveLayersWithMissingPackages(const ShaderPackageCatalog& shaderCatalog)
|
||||
{
|
||||
for (auto it = mLayers.begin(); it != mLayers.end();)
|
||||
{
|
||||
if (!shaderCatalog.HasPackage(it->shaderId))
|
||||
it = mLayers.erase(it);
|
||||
else
|
||||
++it;
|
||||
}
|
||||
}
|
||||
|
||||
bool LayerStackStore::CreateLayer(const ShaderPackageCatalog& shaderCatalog, const std::string& shaderId, std::string& error)
|
||||
{
|
||||
const ShaderPackage* shaderPackage = shaderCatalog.FindPackage(shaderId);
|
||||
if (!shaderPackage)
|
||||
{
|
||||
error = "Unknown shader id: " + shaderId;
|
||||
return false;
|
||||
}
|
||||
|
||||
LayerPersistentState layer;
|
||||
layer.id = GenerateLayerId(mLayers, mNextLayerId);
|
||||
layer.shaderId = shaderId;
|
||||
layer.bypass = false;
|
||||
EnsureLayerDefaults(layer, *shaderPackage);
|
||||
mLayers.push_back(layer);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool LayerStackStore::DeleteLayer(const std::string& layerId, std::string& error)
|
||||
{
|
||||
auto it = std::find_if(mLayers.begin(), mLayers.end(),
|
||||
[&layerId](const LayerPersistentState& layer) { return layer.id == layerId; });
|
||||
if (it == mLayers.end())
|
||||
{
|
||||
error = "Unknown layer id: " + layerId;
|
||||
return false;
|
||||
}
|
||||
|
||||
mLayers.erase(it);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool LayerStackStore::MoveLayer(const std::string& layerId, int direction, std::string& error)
|
||||
{
|
||||
auto it = std::find_if(mLayers.begin(), mLayers.end(),
|
||||
[&layerId](const LayerPersistentState& layer) { return layer.id == layerId; });
|
||||
if (it == mLayers.end())
|
||||
{
|
||||
error = "Unknown layer id: " + layerId;
|
||||
return false;
|
||||
}
|
||||
|
||||
const std::ptrdiff_t index = std::distance(mLayers.begin(), it);
|
||||
const std::ptrdiff_t newIndex = index + direction;
|
||||
if (newIndex < 0 || newIndex >= static_cast<std::ptrdiff_t>(mLayers.size()))
|
||||
return true;
|
||||
|
||||
std::swap(mLayers[index], mLayers[newIndex]);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool LayerStackStore::MoveLayerToIndex(const std::string& layerId, std::size_t targetIndex, std::string& error)
|
||||
{
|
||||
auto it = std::find_if(mLayers.begin(), mLayers.end(),
|
||||
[&layerId](const LayerPersistentState& layer) { return layer.id == layerId; });
|
||||
if (it == mLayers.end())
|
||||
{
|
||||
error = "Unknown layer id: " + layerId;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (mLayers.empty())
|
||||
return true;
|
||||
|
||||
if (targetIndex >= mLayers.size())
|
||||
targetIndex = mLayers.size() - 1;
|
||||
|
||||
const std::size_t sourceIndex = static_cast<std::size_t>(std::distance(mLayers.begin(), it));
|
||||
if (sourceIndex == targetIndex)
|
||||
return true;
|
||||
|
||||
LayerPersistentState movedLayer = *it;
|
||||
mLayers.erase(mLayers.begin() + static_cast<std::ptrdiff_t>(sourceIndex));
|
||||
mLayers.insert(mLayers.begin() + static_cast<std::ptrdiff_t>(targetIndex), movedLayer);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool LayerStackStore::SetLayerBypassState(const std::string& layerId, bool bypassed, std::string& error)
|
||||
{
|
||||
LayerPersistentState* layer = FindLayerById(layerId);
|
||||
if (!layer)
|
||||
{
|
||||
error = "Unknown layer id: " + layerId;
|
||||
return false;
|
||||
}
|
||||
|
||||
layer->bypass = bypassed;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool LayerStackStore::SetLayerShaderSelection(const ShaderPackageCatalog& shaderCatalog, const std::string& layerId, const std::string& shaderId, std::string& error)
|
||||
{
|
||||
LayerPersistentState* layer = FindLayerById(layerId);
|
||||
if (!layer)
|
||||
{
|
||||
error = "Unknown layer id: " + layerId;
|
||||
return false;
|
||||
}
|
||||
|
||||
const ShaderPackage* shaderPackage = shaderCatalog.FindPackage(shaderId);
|
||||
if (!shaderPackage)
|
||||
{
|
||||
error = "Unknown shader id: " + shaderId;
|
||||
return false;
|
||||
}
|
||||
|
||||
layer->shaderId = shaderId;
|
||||
layer->parameterValues.clear();
|
||||
EnsureLayerDefaults(*layer, *shaderPackage);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool LayerStackStore::SetParameterValue(const std::string& layerId, const std::string& parameterId, const ShaderParameterValue& value, std::string& error)
|
||||
{
|
||||
LayerPersistentState* layer = FindLayerById(layerId);
|
||||
if (!layer)
|
||||
{
|
||||
error = "Unknown layer id: " + layerId;
|
||||
return false;
|
||||
}
|
||||
|
||||
layer->parameterValues[parameterId] = value;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool LayerStackStore::ResetLayerParameterValues(const ShaderPackageCatalog& shaderCatalog, const std::string& layerId, std::string& error)
|
||||
{
|
||||
LayerPersistentState* layer = FindLayerById(layerId);
|
||||
if (!layer)
|
||||
{
|
||||
error = "Unknown layer id: " + layerId;
|
||||
return false;
|
||||
}
|
||||
|
||||
const ShaderPackage* shaderPackage = shaderCatalog.FindPackage(layer->shaderId);
|
||||
if (!shaderPackage)
|
||||
{
|
||||
error = "Unknown shader id: " + layer->shaderId;
|
||||
return false;
|
||||
}
|
||||
|
||||
layer->parameterValues.clear();
|
||||
EnsureLayerDefaults(*layer, *shaderPackage);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool LayerStackStore::HasLayer(const std::string& layerId) const
|
||||
{
|
||||
return FindLayerById(layerId) != nullptr;
|
||||
}
|
||||
|
||||
bool LayerStackStore::TryGetParameterById(const ShaderPackageCatalog& shaderCatalog, const std::string& layerId, const std::string& parameterId, StoredParameterSnapshot& snapshot, std::string& error) const
|
||||
{
|
||||
const LayerPersistentState* layer = FindLayerById(layerId);
|
||||
if (!layer)
|
||||
{
|
||||
error = "Unknown layer id: " + layerId;
|
||||
return false;
|
||||
}
|
||||
|
||||
const ShaderPackage* shaderPackage = shaderCatalog.FindPackage(layer->shaderId);
|
||||
if (!shaderPackage)
|
||||
{
|
||||
error = "Unknown shader id: " + layer->shaderId;
|
||||
return false;
|
||||
}
|
||||
|
||||
auto parameterIt = std::find_if(shaderPackage->parameters.begin(), shaderPackage->parameters.end(),
|
||||
[¶meterId](const ShaderParameterDefinition& definition) { return definition.id == parameterId; });
|
||||
if (parameterIt == shaderPackage->parameters.end())
|
||||
{
|
||||
error = "Unknown parameter id: " + parameterId;
|
||||
return false;
|
||||
}
|
||||
|
||||
snapshot = StoredParameterSnapshot();
|
||||
snapshot.layerId = layer->id;
|
||||
snapshot.definition = *parameterIt;
|
||||
auto valueIt = layer->parameterValues.find(parameterIt->id);
|
||||
if (valueIt != layer->parameterValues.end())
|
||||
{
|
||||
snapshot.currentValue = valueIt->second;
|
||||
snapshot.hasCurrentValue = true;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool LayerStackStore::TryGetParameterByControlKey(const ShaderPackageCatalog& shaderCatalog, const std::string& layerKey, const std::string& parameterKey, StoredParameterSnapshot& snapshot, std::string& error) const
|
||||
{
|
||||
const LayerPersistentState* matchedLayer = nullptr;
|
||||
const ShaderPackage* matchedPackage = nullptr;
|
||||
|
||||
for (const LayerPersistentState& layer : mLayers)
|
||||
{
|
||||
const ShaderPackage* shaderPackage = shaderCatalog.FindPackage(layer.shaderId);
|
||||
if (!shaderPackage)
|
||||
continue;
|
||||
|
||||
if (MatchesControlKey(layer.id, layerKey) || MatchesControlKey(shaderPackage->id, layerKey) ||
|
||||
MatchesControlKey(shaderPackage->displayName, layerKey))
|
||||
{
|
||||
matchedLayer = &layer;
|
||||
matchedPackage = shaderPackage;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!matchedLayer || !matchedPackage)
|
||||
{
|
||||
error = "Unknown OSC layer key: " + layerKey;
|
||||
return false;
|
||||
}
|
||||
|
||||
auto parameterIt = std::find_if(matchedPackage->parameters.begin(), matchedPackage->parameters.end(),
|
||||
[¶meterKey](const ShaderParameterDefinition& definition)
|
||||
{
|
||||
return MatchesControlKey(definition.id, parameterKey) || MatchesControlKey(definition.label, parameterKey);
|
||||
});
|
||||
if (parameterIt == matchedPackage->parameters.end())
|
||||
{
|
||||
error = "Unknown OSC parameter key: " + parameterKey;
|
||||
return false;
|
||||
}
|
||||
|
||||
snapshot = StoredParameterSnapshot();
|
||||
snapshot.layerId = matchedLayer->id;
|
||||
snapshot.definition = *parameterIt;
|
||||
auto valueIt = matchedLayer->parameterValues.find(parameterIt->id);
|
||||
if (valueIt != matchedLayer->parameterValues.end())
|
||||
{
|
||||
snapshot.currentValue = valueIt->second;
|
||||
snapshot.hasCurrentValue = true;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool LayerStackStore::ResolveLayerMove(const std::string& layerId, int direction, bool& shouldMove, std::string& error) const
|
||||
{
|
||||
auto it = std::find_if(mLayers.begin(), mLayers.end(),
|
||||
[&layerId](const LayerPersistentState& layer) { return layer.id == layerId; });
|
||||
if (it == mLayers.end())
|
||||
{
|
||||
error = "Unknown layer id: " + layerId;
|
||||
return false;
|
||||
}
|
||||
|
||||
const std::ptrdiff_t index = std::distance(mLayers.begin(), it);
|
||||
const std::ptrdiff_t newIndex = index + direction;
|
||||
shouldMove = newIndex >= 0 && newIndex < static_cast<std::ptrdiff_t>(mLayers.size()) && newIndex != index;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool LayerStackStore::ResolveLayerMoveToIndex(const std::string& layerId, std::size_t targetIndex, bool& shouldMove, std::string& error) const
|
||||
{
|
||||
auto it = std::find_if(mLayers.begin(), mLayers.end(),
|
||||
[&layerId](const LayerPersistentState& layer) { return layer.id == layerId; });
|
||||
if (it == mLayers.end())
|
||||
{
|
||||
error = "Unknown layer id: " + layerId;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (mLayers.empty())
|
||||
{
|
||||
shouldMove = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
const std::size_t clampedTargetIndex = (std::min)(targetIndex, mLayers.size() - 1);
|
||||
const std::size_t sourceIndex = static_cast<std::size_t>(std::distance(mLayers.begin(), it));
|
||||
shouldMove = sourceIndex != clampedTargetIndex;
|
||||
return true;
|
||||
}
|
||||
|
||||
JsonValue LayerStackStore::BuildStackPresetValue(const ShaderPackageCatalog& shaderCatalog, const std::string& presetName) const
|
||||
{
|
||||
JsonValue root = JsonValue::MakeObject();
|
||||
root.set("version", JsonValue(1.0));
|
||||
root.set("name", JsonValue(TrimCopy(presetName)));
|
||||
root.set("layers", RuntimeStateJson::SerializeLayerStack(*this, shaderCatalog));
|
||||
return root;
|
||||
}
|
||||
|
||||
bool LayerStackStore::LoadStackPresetValue(const ShaderPackageCatalog& shaderCatalog, const JsonValue& root, std::string& error)
|
||||
{
|
||||
const JsonValue* layersValue = root.find("layers");
|
||||
if (!layersValue || !layersValue->isArray())
|
||||
{
|
||||
error = "Preset file is missing a valid 'layers' array.";
|
||||
return false;
|
||||
}
|
||||
|
||||
std::vector<LayerPersistentState> nextLayers;
|
||||
uint64_t nextLayerId = mNextLayerId;
|
||||
if (!DeserializeLayerStack(shaderCatalog, *layersValue, nextLayers, nextLayerId, error))
|
||||
return false;
|
||||
|
||||
if (nextLayers.empty())
|
||||
{
|
||||
error = "Preset does not contain any valid layers.";
|
||||
return false;
|
||||
}
|
||||
|
||||
mLayers = std::move(nextLayers);
|
||||
mNextLayerId = nextLayerId;
|
||||
return true;
|
||||
}
|
||||
|
||||
std::string LayerStackStore::MakeSafePresetFileStem(const std::string& presetName)
|
||||
{
|
||||
return ::MakeSafePresetFileStem(presetName);
|
||||
}
|
||||
|
||||
const std::vector<LayerStackStore::LayerPersistentState>& LayerStackStore::Layers() const
|
||||
{
|
||||
return mLayers;
|
||||
}
|
||||
|
||||
std::vector<LayerStackStore::LayerPersistentState>& LayerStackStore::Layers()
|
||||
{
|
||||
return mLayers;
|
||||
}
|
||||
|
||||
std::size_t LayerStackStore::LayerCount() const
|
||||
{
|
||||
return mLayers.size();
|
||||
}
|
||||
|
||||
const LayerStackStore::LayerPersistentState* LayerStackStore::FindLayerById(const std::string& layerId) const
|
||||
{
|
||||
auto it = std::find_if(mLayers.begin(), mLayers.end(),
|
||||
[&layerId](const LayerPersistentState& layer) { return layer.id == layerId; });
|
||||
return it == mLayers.end() ? nullptr : &*it;
|
||||
}
|
||||
|
||||
LayerStackStore::LayerPersistentState* LayerStackStore::FindLayerById(const std::string& layerId)
|
||||
{
|
||||
auto it = std::find_if(mLayers.begin(), mLayers.end(),
|
||||
[&layerId](const LayerPersistentState& layer) { return layer.id == layerId; });
|
||||
return it == mLayers.end() ? nullptr : &*it;
|
||||
}
|
||||
|
||||
ShaderParameterValue LayerStackStore::DefaultValueForDefinition(const ShaderParameterDefinition& definition)
|
||||
{
|
||||
return ::DefaultValueForDefinition(definition);
|
||||
}
|
||||
|
||||
void LayerStackStore::EnsureLayerDefaults(LayerPersistentState& layerState, const ShaderPackage& shaderPackage)
|
||||
{
|
||||
for (const ShaderParameterDefinition& definition : shaderPackage.parameters)
|
||||
{
|
||||
auto valueIt = layerState.parameterValues.find(definition.id);
|
||||
if (valueIt == layerState.parameterValues.end())
|
||||
{
|
||||
layerState.parameterValues[definition.id] = DefaultValueForDefinition(definition);
|
||||
continue;
|
||||
}
|
||||
|
||||
JsonValue valueJson;
|
||||
bool shouldNormalize = true;
|
||||
switch (definition.type)
|
||||
{
|
||||
case ShaderParameterType::Float:
|
||||
if (valueIt->second.numberValues.empty())
|
||||
shouldNormalize = false;
|
||||
else
|
||||
valueJson = JsonValue(valueIt->second.numberValues.front());
|
||||
break;
|
||||
case ShaderParameterType::Vec2:
|
||||
case ShaderParameterType::Color:
|
||||
valueJson = JsonValue::MakeArray();
|
||||
for (double number : valueIt->second.numberValues)
|
||||
valueJson.pushBack(JsonValue(number));
|
||||
break;
|
||||
case ShaderParameterType::Boolean:
|
||||
valueJson = JsonValue(valueIt->second.booleanValue);
|
||||
break;
|
||||
case ShaderParameterType::Enum:
|
||||
valueJson = JsonValue(valueIt->second.enumValue);
|
||||
break;
|
||||
case ShaderParameterType::Text:
|
||||
{
|
||||
const std::string textValue = !valueIt->second.textValue.empty()
|
||||
? valueIt->second.textValue
|
||||
: valueIt->second.enumValue;
|
||||
if (textValue.empty())
|
||||
{
|
||||
valueIt->second = DefaultValueForDefinition(definition);
|
||||
shouldNormalize = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
valueJson = JsonValue(textValue);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case ShaderParameterType::Trigger:
|
||||
if (valueIt->second.numberValues.empty())
|
||||
valueJson = JsonValue(0.0);
|
||||
else
|
||||
valueJson = JsonValue((std::max)(0.0, std::floor(valueIt->second.numberValues.front())));
|
||||
break;
|
||||
}
|
||||
|
||||
if (!shouldNormalize)
|
||||
continue;
|
||||
|
||||
ShaderParameterValue normalizedValue;
|
||||
std::string normalizeError;
|
||||
if (NormalizeAndValidateParameterValue(definition, valueJson, normalizedValue, normalizeError))
|
||||
valueIt->second = normalizedValue;
|
||||
else
|
||||
valueIt->second = DefaultValueForDefinition(definition);
|
||||
}
|
||||
}
|
||||
|
||||
bool LayerStackStore::DeserializeLayerStack(const ShaderPackageCatalog& shaderCatalog, const JsonValue& layersValue, std::vector<LayerPersistentState>& layers, uint64_t& nextLayerId, std::string& error)
|
||||
{
|
||||
for (const JsonValue& layerValue : layersValue.asArray())
|
||||
{
|
||||
if (!layerValue.isObject())
|
||||
continue;
|
||||
|
||||
const JsonValue* shaderIdValue = layerValue.find("shaderId");
|
||||
if (!shaderIdValue)
|
||||
continue;
|
||||
|
||||
const std::string shaderId = shaderIdValue->asString();
|
||||
const ShaderPackage* shaderPackage = shaderCatalog.FindPackage(shaderId);
|
||||
if (!shaderPackage)
|
||||
{
|
||||
error = "Preset references unknown shader id: " + shaderId;
|
||||
return false;
|
||||
}
|
||||
|
||||
LayerPersistentState layer;
|
||||
layer.id = GenerateLayerId(layers, nextLayerId);
|
||||
layer.shaderId = shaderId;
|
||||
if (const JsonValue* bypassValue = layerValue.find("bypass"))
|
||||
layer.bypass = bypassValue->asBoolean(false);
|
||||
|
||||
if (const JsonValue* parametersValue = layerValue.find("parameters"))
|
||||
{
|
||||
for (const JsonValue& parameterValue : parametersValue->asArray())
|
||||
{
|
||||
if (!parameterValue.isObject())
|
||||
continue;
|
||||
|
||||
const JsonValue* parameterIdValue = parameterValue.find("id");
|
||||
const JsonValue* valueValue = parameterValue.find("value");
|
||||
if (!parameterIdValue || !valueValue)
|
||||
continue;
|
||||
|
||||
const std::string parameterId = parameterIdValue->asString();
|
||||
auto definitionIt = std::find_if(shaderPackage->parameters.begin(), shaderPackage->parameters.end(),
|
||||
[¶meterId](const ShaderParameterDefinition& definition) { return definition.id == parameterId; });
|
||||
if (definitionIt == shaderPackage->parameters.end())
|
||||
continue;
|
||||
|
||||
ShaderParameterValue normalizedValue;
|
||||
if (!NormalizeAndValidateParameterValue(*definitionIt, *valueValue, normalizedValue, error))
|
||||
return false;
|
||||
|
||||
layer.parameterValues[parameterId] = normalizedValue;
|
||||
}
|
||||
}
|
||||
|
||||
EnsureLayerDefaults(layer, *shaderPackage);
|
||||
layers.push_back(layer);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
std::string LayerStackStore::GenerateLayerId(std::vector<LayerPersistentState>& layers, uint64_t& nextLayerId)
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
++nextLayerId;
|
||||
const std::string candidate = "layer-" + std::to_string(nextLayerId);
|
||||
auto it = std::find_if(layers.begin(), layers.end(),
|
||||
[&candidate](const LayerPersistentState& layer) { return layer.id == candidate; });
|
||||
if (it == layers.end())
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
#pragma once
|
||||
|
||||
#include "RuntimeJson.h"
|
||||
#include "ShaderPackageCatalog.h"
|
||||
#include "ShaderTypes.h"
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <map>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
class LayerStackStore
|
||||
{
|
||||
public:
|
||||
struct LayerPersistentState
|
||||
{
|
||||
std::string id;
|
||||
std::string shaderId;
|
||||
bool bypass = false;
|
||||
std::map<std::string, ShaderParameterValue> parameterValues;
|
||||
};
|
||||
|
||||
struct StoredParameterSnapshot
|
||||
{
|
||||
std::string layerId;
|
||||
ShaderParameterDefinition definition;
|
||||
ShaderParameterValue currentValue;
|
||||
bool hasCurrentValue = false;
|
||||
};
|
||||
|
||||
bool LoadPersistentStateValue(const JsonValue& root);
|
||||
JsonValue BuildPersistentStateValue(const ShaderPackageCatalog& shaderCatalog) const;
|
||||
void NormalizeLayerIds();
|
||||
void EnsureDefaultsForAllLayers(const ShaderPackageCatalog& shaderCatalog);
|
||||
void EnsureDefaultLayer(const ShaderPackageCatalog& shaderCatalog);
|
||||
void RemoveLayersWithMissingPackages(const ShaderPackageCatalog& shaderCatalog);
|
||||
|
||||
bool CreateLayer(const ShaderPackageCatalog& shaderCatalog, const std::string& shaderId, std::string& error);
|
||||
bool DeleteLayer(const std::string& layerId, std::string& error);
|
||||
bool MoveLayer(const std::string& layerId, int direction, std::string& error);
|
||||
bool MoveLayerToIndex(const std::string& layerId, std::size_t targetIndex, std::string& error);
|
||||
bool SetLayerBypassState(const std::string& layerId, bool bypassed, std::string& error);
|
||||
bool SetLayerShaderSelection(const ShaderPackageCatalog& shaderCatalog, const std::string& layerId, const std::string& shaderId, std::string& error);
|
||||
bool SetParameterValue(const std::string& layerId, const std::string& parameterId, const ShaderParameterValue& value, std::string& error);
|
||||
bool ResetLayerParameterValues(const ShaderPackageCatalog& shaderCatalog, const std::string& layerId, std::string& error);
|
||||
|
||||
bool HasLayer(const std::string& layerId) const;
|
||||
bool TryGetParameterById(const ShaderPackageCatalog& shaderCatalog, const std::string& layerId, const std::string& parameterId, StoredParameterSnapshot& snapshot, std::string& error) const;
|
||||
bool TryGetParameterByControlKey(const ShaderPackageCatalog& shaderCatalog, const std::string& layerKey, const std::string& parameterKey, StoredParameterSnapshot& snapshot, std::string& error) const;
|
||||
bool ResolveLayerMove(const std::string& layerId, int direction, bool& shouldMove, std::string& error) const;
|
||||
bool ResolveLayerMoveToIndex(const std::string& layerId, std::size_t targetIndex, bool& shouldMove, std::string& error) const;
|
||||
|
||||
JsonValue BuildStackPresetValue(const ShaderPackageCatalog& shaderCatalog, const std::string& presetName) const;
|
||||
bool LoadStackPresetValue(const ShaderPackageCatalog& shaderCatalog, const JsonValue& root, std::string& error);
|
||||
static std::string MakeSafePresetFileStem(const std::string& presetName);
|
||||
|
||||
const std::vector<LayerPersistentState>& Layers() const;
|
||||
std::vector<LayerPersistentState>& Layers();
|
||||
std::size_t LayerCount() const;
|
||||
const LayerPersistentState* FindLayerById(const std::string& layerId) const;
|
||||
LayerPersistentState* FindLayerById(const std::string& layerId);
|
||||
|
||||
private:
|
||||
static ShaderParameterValue DefaultValueForDefinition(const ShaderParameterDefinition& definition);
|
||||
static void EnsureLayerDefaults(LayerPersistentState& layerState, const ShaderPackage& shaderPackage);
|
||||
static bool DeserializeLayerStack(const ShaderPackageCatalog& shaderCatalog, const JsonValue& layersValue, std::vector<LayerPersistentState>& layers, uint64_t& nextLayerId, std::string& error);
|
||||
static std::string GenerateLayerId(std::vector<LayerPersistentState>& layers, uint64_t& nextLayerId);
|
||||
|
||||
std::vector<LayerPersistentState> mLayers;
|
||||
uint64_t mNextLayerId = 0;
|
||||
};
|
||||
@@ -0,0 +1,270 @@
|
||||
#include "RuntimeConfigStore.h"
|
||||
|
||||
#include "RuntimeJson.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <fstream>
|
||||
#include <sstream>
|
||||
#include <vector>
|
||||
#include <windows.h>
|
||||
|
||||
namespace
|
||||
{
|
||||
double Clamp01(double value)
|
||||
{
|
||||
return (std::max)(0.0, (std::min)(1.0, value));
|
||||
}
|
||||
|
||||
bool LooksLikePackagedRuntimeRoot(const std::filesystem::path& candidate)
|
||||
{
|
||||
return std::filesystem::exists(candidate / "config" / "runtime-host.json") &&
|
||||
std::filesystem::exists(candidate / "runtime" / "templates" / "shader_wrapper.slang.in") &&
|
||||
std::filesystem::exists(candidate / "shaders");
|
||||
}
|
||||
|
||||
bool LooksLikeRepoRoot(const std::filesystem::path& candidate)
|
||||
{
|
||||
return std::filesystem::exists(candidate / "CMakeLists.txt") &&
|
||||
std::filesystem::exists(candidate / "apps" / "LoopThroughWithOpenGLCompositing");
|
||||
}
|
||||
|
||||
std::filesystem::path FindRepoRootCandidate()
|
||||
{
|
||||
std::vector<std::filesystem::path> rootsToTry;
|
||||
|
||||
char currentDirectory[MAX_PATH] = {};
|
||||
if (GetCurrentDirectoryA(MAX_PATH, currentDirectory) > 0)
|
||||
rootsToTry.push_back(std::filesystem::path(currentDirectory));
|
||||
|
||||
char modulePath[MAX_PATH] = {};
|
||||
DWORD moduleLength = GetModuleFileNameA(NULL, modulePath, MAX_PATH);
|
||||
if (moduleLength > 0 && moduleLength < MAX_PATH)
|
||||
rootsToTry.push_back(std::filesystem::path(modulePath).parent_path());
|
||||
|
||||
for (const std::filesystem::path& startPath : rootsToTry)
|
||||
{
|
||||
std::filesystem::path candidate = startPath;
|
||||
for (int depth = 0; depth < 10 && !candidate.empty(); ++depth)
|
||||
{
|
||||
if (LooksLikePackagedRuntimeRoot(candidate) || LooksLikeRepoRoot(candidate))
|
||||
return candidate;
|
||||
|
||||
candidate = candidate.parent_path();
|
||||
}
|
||||
}
|
||||
|
||||
return std::filesystem::path();
|
||||
}
|
||||
}
|
||||
|
||||
bool RuntimeConfigStore::Initialize(std::string& error)
|
||||
{
|
||||
if (!ResolvePaths(error))
|
||||
return false;
|
||||
if (!LoadConfig(error))
|
||||
return false;
|
||||
RefreshConfigDependentPaths();
|
||||
return true;
|
||||
}
|
||||
|
||||
const RuntimeConfigStore::AppConfig& RuntimeConfigStore::GetConfig() const
|
||||
{
|
||||
return mConfig;
|
||||
}
|
||||
|
||||
const std::filesystem::path& RuntimeConfigStore::GetRepoRoot() const
|
||||
{
|
||||
return mRepoRoot;
|
||||
}
|
||||
|
||||
const std::filesystem::path& RuntimeConfigStore::GetUiRoot() const
|
||||
{
|
||||
return mUiRoot;
|
||||
}
|
||||
|
||||
const std::filesystem::path& RuntimeConfigStore::GetDocsRoot() const
|
||||
{
|
||||
return mDocsRoot;
|
||||
}
|
||||
|
||||
const std::filesystem::path& RuntimeConfigStore::GetShaderRoot() const
|
||||
{
|
||||
return mShaderRoot;
|
||||
}
|
||||
|
||||
const std::filesystem::path& RuntimeConfigStore::GetRuntimeRoot() const
|
||||
{
|
||||
return mRuntimeRoot;
|
||||
}
|
||||
|
||||
const std::filesystem::path& RuntimeConfigStore::GetPresetRoot() const
|
||||
{
|
||||
return mPresetRoot;
|
||||
}
|
||||
|
||||
const std::filesystem::path& RuntimeConfigStore::GetRuntimeStatePath() const
|
||||
{
|
||||
return mRuntimeStatePath;
|
||||
}
|
||||
|
||||
const std::filesystem::path& RuntimeConfigStore::GetWrapperPath() const
|
||||
{
|
||||
return mWrapperPath;
|
||||
}
|
||||
|
||||
const std::filesystem::path& RuntimeConfigStore::GetGeneratedGlslPath() const
|
||||
{
|
||||
return mGeneratedGlslPath;
|
||||
}
|
||||
|
||||
const std::filesystem::path& RuntimeConfigStore::GetPatchedGlslPath() const
|
||||
{
|
||||
return mPatchedGlslPath;
|
||||
}
|
||||
|
||||
void RuntimeConfigStore::SetBoundControlServerPort(unsigned short port)
|
||||
{
|
||||
mConfig.serverPort = port;
|
||||
}
|
||||
|
||||
bool RuntimeConfigStore::ResolvePaths(std::string& error)
|
||||
{
|
||||
mRepoRoot = FindRepoRootCandidate();
|
||||
if (mRepoRoot.empty())
|
||||
{
|
||||
error = "Could not locate the repository root from the current runtime path.";
|
||||
return false;
|
||||
}
|
||||
|
||||
const std::filesystem::path builtUiRoot = mRepoRoot / "ui" / "dist";
|
||||
mUiRoot = std::filesystem::exists(builtUiRoot) ? builtUiRoot : (mRepoRoot / "ui");
|
||||
mDocsRoot = mRepoRoot / "docs";
|
||||
mConfigPath = mRepoRoot / "config" / "runtime-host.json";
|
||||
mRuntimeRoot = mRepoRoot / "runtime";
|
||||
mPresetRoot = mRuntimeRoot / "stack_presets";
|
||||
mRuntimeStatePath = mRuntimeRoot / "runtime_state.json";
|
||||
RefreshConfigDependentPaths();
|
||||
|
||||
std::error_code fsError;
|
||||
std::filesystem::create_directories(mRuntimeRoot / "shader_cache", fsError);
|
||||
std::filesystem::create_directories(mPresetRoot, fsError);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool RuntimeConfigStore::LoadConfig(std::string& error)
|
||||
{
|
||||
if (!std::filesystem::exists(mConfigPath))
|
||||
return true;
|
||||
|
||||
std::string configText = ReadTextFile(mConfigPath, error);
|
||||
if (configText.empty())
|
||||
return false;
|
||||
|
||||
JsonValue configJson;
|
||||
if (!ParseJson(configText, configJson, error))
|
||||
return false;
|
||||
|
||||
if (const JsonValue* shaderLibraryValue = configJson.find("shaderLibrary"))
|
||||
mConfig.shaderLibrary = shaderLibraryValue->asString();
|
||||
if (const JsonValue* serverPortValue = configJson.find("serverPort"))
|
||||
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"))
|
||||
{
|
||||
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"))
|
||||
{
|
||||
if (videoFormatValue->isString() && !videoFormatValue->asString().empty())
|
||||
{
|
||||
mConfig.inputVideoFormat = videoFormatValue->asString();
|
||||
mConfig.outputVideoFormat = videoFormatValue->asString();
|
||||
}
|
||||
}
|
||||
if (const JsonValue* frameRateValue = configJson.find("frameRate"))
|
||||
{
|
||||
if (frameRateValue->isString() && !frameRateValue->asString().empty())
|
||||
{
|
||||
mConfig.inputFrameRate = frameRateValue->asString();
|
||||
mConfig.outputFrameRate = frameRateValue->asString();
|
||||
}
|
||||
else if (frameRateValue->isNumber())
|
||||
{
|
||||
std::ostringstream stream;
|
||||
stream << frameRateValue->asNumber();
|
||||
mConfig.inputFrameRate = stream.str();
|
||||
mConfig.outputFrameRate = stream.str();
|
||||
}
|
||||
}
|
||||
if (const JsonValue* inputVideoFormatValue = configJson.find("inputVideoFormat"))
|
||||
{
|
||||
if (inputVideoFormatValue->isString() && !inputVideoFormatValue->asString().empty())
|
||||
mConfig.inputVideoFormat = inputVideoFormatValue->asString();
|
||||
}
|
||||
if (const JsonValue* inputFrameRateValue = configJson.find("inputFrameRate"))
|
||||
{
|
||||
if (inputFrameRateValue->isString() && !inputFrameRateValue->asString().empty())
|
||||
mConfig.inputFrameRate = inputFrameRateValue->asString();
|
||||
else if (inputFrameRateValue->isNumber())
|
||||
{
|
||||
std::ostringstream stream;
|
||||
stream << inputFrameRateValue->asNumber();
|
||||
mConfig.inputFrameRate = stream.str();
|
||||
}
|
||||
}
|
||||
if (const JsonValue* outputVideoFormatValue = configJson.find("outputVideoFormat"))
|
||||
{
|
||||
if (outputVideoFormatValue->isString() && !outputVideoFormatValue->asString().empty())
|
||||
mConfig.outputVideoFormat = outputVideoFormatValue->asString();
|
||||
}
|
||||
if (const JsonValue* outputFrameRateValue = configJson.find("outputFrameRate"))
|
||||
{
|
||||
if (outputFrameRateValue->isString() && !outputFrameRateValue->asString().empty())
|
||||
mConfig.outputFrameRate = outputFrameRateValue->asString();
|
||||
else if (outputFrameRateValue->isNumber())
|
||||
{
|
||||
std::ostringstream stream;
|
||||
stream << outputFrameRateValue->asNumber();
|
||||
mConfig.outputFrameRate = stream.str();
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
std::string RuntimeConfigStore::ReadTextFile(const std::filesystem::path& path, std::string& error) const
|
||||
{
|
||||
std::ifstream input(path, std::ios::binary);
|
||||
if (!input)
|
||||
{
|
||||
error = "Could not open file: " + path.string();
|
||||
return std::string();
|
||||
}
|
||||
|
||||
std::ostringstream buffer;
|
||||
buffer << input.rdbuf();
|
||||
return buffer.str();
|
||||
}
|
||||
|
||||
void RuntimeConfigStore::RefreshConfigDependentPaths()
|
||||
{
|
||||
mShaderRoot = mRepoRoot / mConfig.shaderLibrary;
|
||||
mWrapperPath = mRuntimeRoot / "shader_cache" / "active_shader_wrapper.slang";
|
||||
mGeneratedGlslPath = mRuntimeRoot / "shader_cache" / "active_shader.raw.frag";
|
||||
mPatchedGlslPath = mRuntimeRoot / "shader_cache" / "active_shader.frag";
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
#pragma once
|
||||
|
||||
#include <filesystem>
|
||||
#include <string>
|
||||
|
||||
class RuntimeConfigStore
|
||||
{
|
||||
public:
|
||||
struct AppConfig
|
||||
{
|
||||
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";
|
||||
std::string outputVideoFormat = "1080p";
|
||||
std::string outputFrameRate = "59.94";
|
||||
};
|
||||
|
||||
bool Initialize(std::string& error);
|
||||
|
||||
const AppConfig& GetConfig() const;
|
||||
const std::filesystem::path& GetRepoRoot() const;
|
||||
const std::filesystem::path& GetUiRoot() const;
|
||||
const std::filesystem::path& GetDocsRoot() const;
|
||||
const std::filesystem::path& GetShaderRoot() const;
|
||||
const std::filesystem::path& GetRuntimeRoot() const;
|
||||
const std::filesystem::path& GetPresetRoot() const;
|
||||
const std::filesystem::path& GetRuntimeStatePath() const;
|
||||
const std::filesystem::path& GetWrapperPath() const;
|
||||
const std::filesystem::path& GetGeneratedGlslPath() const;
|
||||
const std::filesystem::path& GetPatchedGlslPath() const;
|
||||
void SetBoundControlServerPort(unsigned short port);
|
||||
|
||||
private:
|
||||
bool ResolvePaths(std::string& error);
|
||||
bool LoadConfig(std::string& error);
|
||||
std::string ReadTextFile(const std::filesystem::path& path, std::string& error) const;
|
||||
void RefreshConfigDependentPaths();
|
||||
|
||||
AppConfig mConfig;
|
||||
std::filesystem::path mRepoRoot;
|
||||
std::filesystem::path mUiRoot;
|
||||
std::filesystem::path mDocsRoot;
|
||||
std::filesystem::path mShaderRoot;
|
||||
std::filesystem::path mRuntimeRoot;
|
||||
std::filesystem::path mPresetRoot;
|
||||
std::filesystem::path mRuntimeStatePath;
|
||||
std::filesystem::path mConfigPath;
|
||||
std::filesystem::path mWrapperPath;
|
||||
std::filesystem::path mGeneratedGlslPath;
|
||||
std::filesystem::path mPatchedGlslPath;
|
||||
};
|
||||
@@ -0,0 +1,644 @@
|
||||
#include "RuntimeStore.h"
|
||||
|
||||
#include "RuntimeStatePresenter.h"
|
||||
|
||||
#include <cctype>
|
||||
#include <fstream>
|
||||
#include <mutex>
|
||||
#include <random>
|
||||
#include <sstream>
|
||||
#include <windows.h>
|
||||
|
||||
namespace
|
||||
{
|
||||
std::string ToLowerCopy(std::string text)
|
||||
{
|
||||
std::transform(text.begin(), text.end(), text.begin(),
|
||||
[](unsigned char ch) { return static_cast<char>(std::tolower(ch)); });
|
||||
return text;
|
||||
}
|
||||
|
||||
double GenerateStartupRandom()
|
||||
{
|
||||
std::random_device randomDevice;
|
||||
std::uniform_real_distribution<double> distribution(0.0, 1.0);
|
||||
return distribution(randomDevice);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
RuntimeStore::RuntimeStore() :
|
||||
mRenderSnapshotBuilder(*this),
|
||||
mHealthTelemetry(),
|
||||
mReloadRequested(false),
|
||||
mCompileSucceeded(false),
|
||||
mStartupRandom(GenerateStartupRandom()),
|
||||
mServerPort(8080),
|
||||
mAutoReloadEnabled(true),
|
||||
mStartTime(std::chrono::steady_clock::now()),
|
||||
mLastScanTime((std::chrono::steady_clock::time_point::min)())
|
||||
{
|
||||
}
|
||||
|
||||
HealthTelemetry& RuntimeStore::GetHealthTelemetry()
|
||||
{
|
||||
return mHealthTelemetry;
|
||||
}
|
||||
|
||||
const HealthTelemetry& RuntimeStore::GetHealthTelemetry() const
|
||||
{
|
||||
return mHealthTelemetry;
|
||||
}
|
||||
|
||||
RenderSnapshotBuilder& RuntimeStore::GetRenderSnapshotBuilder()
|
||||
{
|
||||
return mRenderSnapshotBuilder;
|
||||
}
|
||||
|
||||
const RenderSnapshotBuilder& RuntimeStore::GetRenderSnapshotBuilder() const
|
||||
{
|
||||
return mRenderSnapshotBuilder;
|
||||
}
|
||||
|
||||
bool RuntimeStore::InitializeStore(std::string& error)
|
||||
{
|
||||
try
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mMutex);
|
||||
|
||||
if (!mConfigStore.Initialize(error))
|
||||
return false;
|
||||
if (!LoadPersistentState(error))
|
||||
return false;
|
||||
if (!ScanShaderPackages(error))
|
||||
return false;
|
||||
mLayerStack.NormalizeLayerIds();
|
||||
mLayerStack.EnsureDefaultsForAllLayers(mShaderCatalog);
|
||||
mLayerStack.EnsureDefaultLayer(mShaderCatalog);
|
||||
|
||||
mServerPort = mConfigStore.GetConfig().serverPort;
|
||||
mAutoReloadEnabled = mConfigStore.GetConfig().autoReload;
|
||||
mReloadRequested = true;
|
||||
mCompileMessage = "Waiting for shader compile.";
|
||||
return true;
|
||||
}
|
||||
catch (const std::exception& exception)
|
||||
{
|
||||
error = std::string("RuntimeStore::InitializeStore exception: ") + exception.what();
|
||||
return false;
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
error = "RuntimeStore::InitializeStore threw a non-standard exception.";
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
std::string RuntimeStore::BuildPersistentStateJson() const
|
||||
{
|
||||
return RuntimeStatePresenter::BuildRuntimeStateJson(*this);
|
||||
}
|
||||
|
||||
bool RuntimeStore::PollStoredFileChanges(bool& registryChanged, bool& reloadRequested, std::string& error)
|
||||
{
|
||||
try
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mMutex);
|
||||
registryChanged = false;
|
||||
reloadRequested = false;
|
||||
|
||||
if (!mAutoReloadEnabled)
|
||||
{
|
||||
reloadRequested = mReloadRequested;
|
||||
return true;
|
||||
}
|
||||
|
||||
const auto now = std::chrono::steady_clock::now();
|
||||
if (mLastScanTime != (std::chrono::steady_clock::time_point::min)() &&
|
||||
std::chrono::duration_cast<std::chrono::milliseconds>(now - mLastScanTime).count() < 250)
|
||||
{
|
||||
reloadRequested = mReloadRequested;
|
||||
return true;
|
||||
}
|
||||
|
||||
mLastScanTime = now;
|
||||
|
||||
std::string scanError;
|
||||
const ShaderPackageCatalog::Snapshot previousCatalog = mShaderCatalog.CaptureSnapshot();
|
||||
if (!ScanShaderPackages(scanError))
|
||||
{
|
||||
error = scanError;
|
||||
return false;
|
||||
}
|
||||
|
||||
registryChanged = mShaderCatalog.HasCatalogChangedSince(previousCatalog);
|
||||
|
||||
mLayerStack.EnsureDefaultsForAllLayers(mShaderCatalog);
|
||||
for (RuntimeStore::LayerPersistentState& layer : mLayerStack.Layers())
|
||||
{
|
||||
const ShaderPackage* active = mShaderCatalog.FindPackage(layer.shaderId);
|
||||
if (!active)
|
||||
continue;
|
||||
if (mShaderCatalog.HasPackageChangedSince(previousCatalog, layer.shaderId))
|
||||
mReloadRequested = true;
|
||||
}
|
||||
|
||||
reloadRequested = mReloadRequested;
|
||||
if (registryChanged || reloadRequested)
|
||||
MarkRenderStateDirtyLocked();
|
||||
return true;
|
||||
}
|
||||
catch (const std::exception& exception)
|
||||
{
|
||||
error = std::string("RuntimeStore::PollStoredFileChanges exception: ") + exception.what();
|
||||
return false;
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
error = "RuntimeStore::PollStoredFileChanges threw a non-standard exception.";
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
bool RuntimeStore::CreateStoredLayer(const std::string& shaderId, std::string& error)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mMutex);
|
||||
if (!mLayerStack.CreateLayer(mShaderCatalog, shaderId, error))
|
||||
return false;
|
||||
|
||||
mReloadRequested = true;
|
||||
MarkRenderStateDirtyLocked();
|
||||
return SavePersistentState(error);
|
||||
}
|
||||
|
||||
bool RuntimeStore::DeleteStoredLayer(const std::string& layerId, std::string& error)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mMutex);
|
||||
if (!mLayerStack.DeleteLayer(layerId, error))
|
||||
return false;
|
||||
|
||||
mReloadRequested = true;
|
||||
MarkRenderStateDirtyLocked();
|
||||
return SavePersistentState(error);
|
||||
}
|
||||
|
||||
bool RuntimeStore::MoveStoredLayer(const std::string& layerId, int direction, std::string& error)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mMutex);
|
||||
bool shouldMove = false;
|
||||
if (!mLayerStack.ResolveLayerMove(layerId, direction, shouldMove, error))
|
||||
return false;
|
||||
if (!shouldMove)
|
||||
return true;
|
||||
|
||||
if (!mLayerStack.MoveLayer(layerId, direction, error))
|
||||
return false;
|
||||
|
||||
mReloadRequested = true;
|
||||
MarkRenderStateDirtyLocked();
|
||||
return SavePersistentState(error);
|
||||
}
|
||||
|
||||
bool RuntimeStore::MoveStoredLayerToIndex(const std::string& layerId, std::size_t targetIndex, std::string& error)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mMutex);
|
||||
bool shouldMove = false;
|
||||
if (!mLayerStack.ResolveLayerMoveToIndex(layerId, targetIndex, shouldMove, error))
|
||||
return false;
|
||||
if (!shouldMove)
|
||||
return true;
|
||||
|
||||
if (!mLayerStack.MoveLayerToIndex(layerId, targetIndex, error))
|
||||
return false;
|
||||
|
||||
mReloadRequested = true;
|
||||
MarkRenderStateDirtyLocked();
|
||||
return SavePersistentState(error);
|
||||
}
|
||||
|
||||
bool RuntimeStore::SetStoredLayerBypassState(const std::string& layerId, bool bypassed, std::string& error)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mMutex);
|
||||
if (!mLayerStack.SetLayerBypassState(layerId, bypassed, error))
|
||||
return false;
|
||||
|
||||
mReloadRequested = true;
|
||||
MarkParameterStateDirtyLocked();
|
||||
return SavePersistentState(error);
|
||||
}
|
||||
|
||||
bool RuntimeStore::SetStoredLayerShaderSelection(const std::string& layerId, const std::string& shaderId, std::string& error)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mMutex);
|
||||
if (!mLayerStack.SetLayerShaderSelection(mShaderCatalog, layerId, shaderId, error))
|
||||
return false;
|
||||
|
||||
mReloadRequested = true;
|
||||
MarkRenderStateDirtyLocked();
|
||||
return SavePersistentState(error);
|
||||
}
|
||||
|
||||
bool RuntimeStore::SetStoredParameterValue(const std::string& layerId, const std::string& parameterId, const ShaderParameterValue& value, bool persistState, std::string& error)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mMutex);
|
||||
|
||||
if (!mLayerStack.SetParameterValue(layerId, parameterId, value, error))
|
||||
return false;
|
||||
|
||||
MarkParameterStateDirtyLocked();
|
||||
return !persistState || SavePersistentState(error);
|
||||
}
|
||||
|
||||
bool RuntimeStore::ResetStoredLayerParameterValues(const std::string& layerId, std::string& error)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mMutex);
|
||||
|
||||
if (!mLayerStack.ResetLayerParameterValues(mShaderCatalog, layerId, error))
|
||||
return false;
|
||||
|
||||
MarkParameterStateDirtyLocked();
|
||||
return SavePersistentState(error);
|
||||
}
|
||||
|
||||
bool RuntimeStore::SaveStackPresetSnapshot(const std::string& presetName, std::string& error) const
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mMutex);
|
||||
const std::string safeStem = LayerStackStore::MakeSafePresetFileStem(presetName);
|
||||
if (safeStem.empty())
|
||||
{
|
||||
error = "Preset name must include at least one letter or number.";
|
||||
return false;
|
||||
}
|
||||
|
||||
JsonValue root = JsonValue::MakeObject();
|
||||
root = mLayerStack.BuildStackPresetValue(mShaderCatalog, presetName);
|
||||
|
||||
return WriteTextFile(mConfigStore.GetPresetRoot() / (safeStem + ".json"), SerializeJson(root, true), error);
|
||||
}
|
||||
|
||||
bool RuntimeStore::LoadStackPresetSnapshot(const std::string& presetName, std::string& error)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mMutex);
|
||||
const std::string safeStem = LayerStackStore::MakeSafePresetFileStem(presetName);
|
||||
if (safeStem.empty())
|
||||
{
|
||||
error = "Preset name must include at least one letter or number.";
|
||||
return false;
|
||||
}
|
||||
|
||||
const std::filesystem::path presetPath = mConfigStore.GetPresetRoot() / (safeStem + ".json");
|
||||
std::string presetText = ReadTextFile(presetPath, error);
|
||||
if (presetText.empty())
|
||||
return false;
|
||||
|
||||
JsonValue root;
|
||||
if (!ParseJson(presetText, root, error))
|
||||
return false;
|
||||
|
||||
if (!mLayerStack.LoadStackPresetValue(mShaderCatalog, root, error))
|
||||
return false;
|
||||
|
||||
mReloadRequested = true;
|
||||
MarkRenderStateDirtyLocked();
|
||||
return SavePersistentState(error);
|
||||
}
|
||||
|
||||
bool RuntimeStore::HasStoredLayer(const std::string& layerId) const
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mMutex);
|
||||
return mLayerStack.HasLayer(layerId);
|
||||
}
|
||||
|
||||
bool RuntimeStore::HasStoredShader(const std::string& shaderId) const
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mMutex);
|
||||
return mShaderCatalog.HasPackage(shaderId);
|
||||
}
|
||||
|
||||
bool RuntimeStore::TryGetStoredParameterById(const std::string& layerId, const std::string& parameterId, StoredParameterSnapshot& snapshot, std::string& error) const
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mMutex);
|
||||
|
||||
return mLayerStack.TryGetParameterById(mShaderCatalog, layerId, parameterId, snapshot, error);
|
||||
}
|
||||
|
||||
bool RuntimeStore::TryGetStoredParameterByControlKey(const std::string& layerKey, const std::string& parameterKey, StoredParameterSnapshot& snapshot, std::string& error) const
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mMutex);
|
||||
|
||||
return mLayerStack.TryGetParameterByControlKey(mShaderCatalog, layerKey, parameterKey, snapshot, error);
|
||||
}
|
||||
|
||||
bool RuntimeStore::ResolveStoredLayerMove(const std::string& layerId, int direction, bool& shouldMove, std::string& error) const
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mMutex);
|
||||
return mLayerStack.ResolveLayerMove(layerId, direction, shouldMove, error);
|
||||
}
|
||||
|
||||
bool RuntimeStore::ResolveStoredLayerMoveToIndex(const std::string& layerId, std::size_t targetIndex, bool& shouldMove, std::string& error) const
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mMutex);
|
||||
return mLayerStack.ResolveLayerMoveToIndex(layerId, targetIndex, shouldMove, error);
|
||||
}
|
||||
|
||||
bool RuntimeStore::IsValidStackPresetName(const std::string& presetName) const
|
||||
{
|
||||
return !LayerStackStore::MakeSafePresetFileStem(presetName).empty();
|
||||
}
|
||||
|
||||
double RuntimeStore::GetRuntimeElapsedSeconds() const
|
||||
{
|
||||
return std::chrono::duration_cast<std::chrono::duration<double>>(
|
||||
std::chrono::steady_clock::now() - mStartTime).count();
|
||||
}
|
||||
|
||||
const std::filesystem::path& RuntimeStore::GetRuntimeRepositoryRoot() const
|
||||
{
|
||||
return mConfigStore.GetRepoRoot();
|
||||
}
|
||||
|
||||
const std::filesystem::path& RuntimeStore::GetRuntimeUiRoot() const
|
||||
{
|
||||
return mConfigStore.GetUiRoot();
|
||||
}
|
||||
|
||||
const std::filesystem::path& RuntimeStore::GetRuntimeDocsRoot() const
|
||||
{
|
||||
return mConfigStore.GetDocsRoot();
|
||||
}
|
||||
|
||||
const std::filesystem::path& RuntimeStore::GetRuntimeDataRoot() const
|
||||
{
|
||||
return mConfigStore.GetRuntimeRoot();
|
||||
}
|
||||
|
||||
unsigned short RuntimeStore::GetConfiguredControlServerPort() const
|
||||
{
|
||||
return mServerPort;
|
||||
}
|
||||
|
||||
unsigned short RuntimeStore::GetConfiguredOscPort() const
|
||||
{
|
||||
return mConfigStore.GetConfig().oscPort;
|
||||
}
|
||||
|
||||
const std::string& RuntimeStore::GetConfiguredOscBindAddress() const
|
||||
{
|
||||
return mConfigStore.GetConfig().oscBindAddress;
|
||||
}
|
||||
|
||||
double RuntimeStore::GetConfiguredOscSmoothing() const
|
||||
{
|
||||
return mConfigStore.GetConfig().oscSmoothing;
|
||||
}
|
||||
|
||||
unsigned RuntimeStore::GetConfiguredMaxTemporalHistoryFrames() const
|
||||
{
|
||||
return mConfigStore.GetConfig().maxTemporalHistoryFrames;
|
||||
}
|
||||
|
||||
unsigned RuntimeStore::GetConfiguredPreviewFps() const
|
||||
{
|
||||
return mConfigStore.GetConfig().previewFps;
|
||||
}
|
||||
|
||||
bool RuntimeStore::IsExternalKeyingConfigured() const
|
||||
{
|
||||
return mConfigStore.GetConfig().enableExternalKeying;
|
||||
}
|
||||
|
||||
const std::string& RuntimeStore::GetConfiguredInputVideoFormat() const
|
||||
{
|
||||
return mConfigStore.GetConfig().inputVideoFormat;
|
||||
}
|
||||
|
||||
const std::string& RuntimeStore::GetConfiguredInputFrameRate() const
|
||||
{
|
||||
return mConfigStore.GetConfig().inputFrameRate;
|
||||
}
|
||||
|
||||
const std::string& RuntimeStore::GetConfiguredOutputVideoFormat() const
|
||||
{
|
||||
return mConfigStore.GetConfig().outputVideoFormat;
|
||||
}
|
||||
|
||||
const std::string& RuntimeStore::GetConfiguredOutputFrameRate() const
|
||||
{
|
||||
return mConfigStore.GetConfig().outputFrameRate;
|
||||
}
|
||||
|
||||
void RuntimeStore::SetBoundControlServerPort(unsigned short port)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mMutex);
|
||||
mServerPort = port;
|
||||
mConfigStore.SetBoundControlServerPort(port);
|
||||
}
|
||||
|
||||
void RuntimeStore::SetCompileStatus(bool succeeded, const std::string& message)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mMutex);
|
||||
mCompileSucceeded = succeeded;
|
||||
mCompileMessage = message;
|
||||
}
|
||||
|
||||
void RuntimeStore::ClearReloadRequest()
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mMutex);
|
||||
mReloadRequested = false;
|
||||
}
|
||||
|
||||
bool RuntimeStore::LoadPersistentState(std::string& error)
|
||||
{
|
||||
if (!std::filesystem::exists(mConfigStore.GetRuntimeStatePath()))
|
||||
return true;
|
||||
|
||||
std::string stateText = ReadTextFile(mConfigStore.GetRuntimeStatePath(), error);
|
||||
if (stateText.empty())
|
||||
return false;
|
||||
|
||||
JsonValue root;
|
||||
if (!ParseJson(stateText, root, error))
|
||||
return false;
|
||||
|
||||
return mLayerStack.LoadPersistentStateValue(root);
|
||||
}
|
||||
|
||||
bool RuntimeStore::SavePersistentState(std::string& error) const
|
||||
{
|
||||
return WriteTextFile(mConfigStore.GetRuntimeStatePath(), SerializeJson(mLayerStack.BuildPersistentStateValue(mShaderCatalog), true), error);
|
||||
}
|
||||
|
||||
bool RuntimeStore::ScanShaderPackages(std::string& error)
|
||||
{
|
||||
if (!mShaderCatalog.Scan(mConfigStore.GetShaderRoot(), mConfigStore.GetConfig().maxTemporalHistoryFrames, error))
|
||||
return false;
|
||||
|
||||
mLayerStack.RemoveLayersWithMissingPackages(mShaderCatalog);
|
||||
|
||||
MarkRenderStateDirtyLocked();
|
||||
return true;
|
||||
}
|
||||
|
||||
std::string RuntimeStore::ReadTextFile(const std::filesystem::path& path, std::string& error) const
|
||||
{
|
||||
std::ifstream input(path, std::ios::binary);
|
||||
if (!input)
|
||||
{
|
||||
error = "Could not open file: " + path.string();
|
||||
return std::string();
|
||||
}
|
||||
|
||||
std::ostringstream buffer;
|
||||
buffer << input.rdbuf();
|
||||
return buffer.str();
|
||||
}
|
||||
|
||||
bool RuntimeStore::WriteTextFile(const std::filesystem::path& path, const std::string& contents, std::string& error) const
|
||||
{
|
||||
std::error_code fsError;
|
||||
std::filesystem::create_directories(path.parent_path(), fsError);
|
||||
|
||||
const std::filesystem::path temporaryPath = path.string() + ".tmp";
|
||||
std::ofstream output(temporaryPath, std::ios::binary | std::ios::trunc);
|
||||
if (!output)
|
||||
{
|
||||
error = "Could not write file: " + temporaryPath.string();
|
||||
return false;
|
||||
}
|
||||
|
||||
output << contents;
|
||||
output.close();
|
||||
if (!output.good())
|
||||
{
|
||||
error = "Could not finish writing file: " + temporaryPath.string();
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!MoveFileExA(temporaryPath.string().c_str(), path.string().c_str(), MOVEFILE_REPLACE_EXISTING | MOVEFILE_WRITE_THROUGH))
|
||||
{
|
||||
const DWORD lastError = GetLastError();
|
||||
std::filesystem::remove(temporaryPath, fsError);
|
||||
error = "Could not replace file: " + path.string() + " (Win32 error " + std::to_string(lastError) + ")";
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
std::vector<std::string> RuntimeStore::GetStackPresetNamesLocked() const
|
||||
{
|
||||
std::vector<std::string> presetNames;
|
||||
std::error_code fsError;
|
||||
if (!std::filesystem::exists(mConfigStore.GetPresetRoot(), fsError))
|
||||
return presetNames;
|
||||
|
||||
for (const auto& entry : std::filesystem::directory_iterator(mConfigStore.GetPresetRoot(), fsError))
|
||||
{
|
||||
if (!entry.is_regular_file())
|
||||
continue;
|
||||
if (ToLowerCopy(entry.path().extension().string()) != ".json")
|
||||
continue;
|
||||
presetNames.push_back(entry.path().stem().string());
|
||||
}
|
||||
|
||||
std::sort(presetNames.begin(), presetNames.end());
|
||||
return presetNames;
|
||||
}
|
||||
|
||||
bool RuntimeStore::CopyShaderPackageForStoredLayer(const std::string& layerId, ShaderPackage& shaderPackage, std::string& error) const
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mMutex);
|
||||
const RuntimeStore::LayerPersistentState* layer = mLayerStack.FindLayerById(layerId);
|
||||
if (!layer)
|
||||
{
|
||||
error = "Unknown layer id: " + layerId;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!mShaderCatalog.CopyPackage(layer->shaderId, shaderPackage))
|
||||
{
|
||||
error = "Unknown shader id: " + layer->shaderId;
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
ShaderCompilerInputs RuntimeStore::GetShaderCompilerInputs() const
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mMutex);
|
||||
ShaderCompilerInputs inputs;
|
||||
inputs.repoRoot = mConfigStore.GetRepoRoot();
|
||||
inputs.wrapperPath = mConfigStore.GetWrapperPath();
|
||||
inputs.generatedGlslPath = mConfigStore.GetGeneratedGlslPath();
|
||||
inputs.patchedGlslPath = mConfigStore.GetPatchedGlslPath();
|
||||
inputs.maxTemporalHistoryFrames = mConfigStore.GetConfig().maxTemporalHistoryFrames;
|
||||
return inputs;
|
||||
}
|
||||
|
||||
CommittedLiveStateReadModel RuntimeStore::BuildCommittedLiveStateReadModel() const
|
||||
{
|
||||
CommittedLiveStateReadModel model;
|
||||
std::lock_guard<std::mutex> lock(mMutex);
|
||||
model.layers = mLayerStack.Layers();
|
||||
model.packagesById = mShaderCatalog.CaptureSnapshot().packagesById;
|
||||
return model;
|
||||
}
|
||||
|
||||
RenderSnapshotReadModel RuntimeStore::BuildRenderSnapshotReadModel() const
|
||||
{
|
||||
RenderSnapshotReadModel model;
|
||||
model.signalStatus = mHealthTelemetry.GetSignalStatusSnapshot();
|
||||
model.committedLiveState = BuildCommittedLiveStateReadModel();
|
||||
std::lock_guard<std::mutex> lock(mMutex);
|
||||
model.timing.startTime = mStartTime;
|
||||
model.timing.startupRandom = mStartupRandom;
|
||||
return model;
|
||||
}
|
||||
|
||||
std::vector<RuntimeStore::LayerPersistentState> RuntimeStore::CopyCommittedLiveLayerStates() const
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mMutex);
|
||||
return mLayerStack.Layers();
|
||||
}
|
||||
|
||||
std::vector<RuntimeStore::LayerPersistentState> RuntimeStore::CopyLayerStates() const
|
||||
{
|
||||
return CopyCommittedLiveLayerStates();
|
||||
}
|
||||
|
||||
RenderTimingSnapshot RuntimeStore::GetRenderTimingSnapshot() const
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mMutex);
|
||||
RenderTimingSnapshot snapshot;
|
||||
snapshot.startTime = mStartTime;
|
||||
snapshot.startupRandom = mStartupRandom;
|
||||
return snapshot;
|
||||
}
|
||||
|
||||
RuntimeStatePresentationReadModel RuntimeStore::BuildRuntimeStatePresentationReadModel() const
|
||||
{
|
||||
RuntimeStatePresentationReadModel model;
|
||||
model.telemetry = mHealthTelemetry.GetSnapshot();
|
||||
std::lock_guard<std::mutex> lock(mMutex);
|
||||
model.config = mConfigStore.GetConfig();
|
||||
model.layerStack = mLayerStack;
|
||||
model.shaderCatalog = mShaderCatalog.CaptureSnapshot();
|
||||
model.packageStatuses = mShaderCatalog.PackageStatuses();
|
||||
model.stackPresetNames = GetStackPresetNamesLocked();
|
||||
model.serverPort = mServerPort;
|
||||
model.autoReloadEnabled = mAutoReloadEnabled;
|
||||
model.compileSucceeded = mCompileSucceeded;
|
||||
model.compileMessage = mCompileMessage;
|
||||
return model;
|
||||
}
|
||||
|
||||
void RuntimeStore::MarkRenderStateDirtyLocked()
|
||||
{
|
||||
mRenderSnapshotBuilder.MarkRenderStateDirty();
|
||||
}
|
||||
|
||||
void RuntimeStore::MarkParameterStateDirtyLocked()
|
||||
{
|
||||
mRenderSnapshotBuilder.MarkParameterStateDirty();
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
#pragma once
|
||||
|
||||
#include "HealthTelemetry.h"
|
||||
#include "LayerStackStore.h"
|
||||
#include "RenderSnapshotBuilder.h"
|
||||
#include "RuntimeConfigStore.h"
|
||||
#include "RuntimeJson.h"
|
||||
#include "RuntimeStoreReadModels.h"
|
||||
#include "ShaderPackageCatalog.h"
|
||||
#include "ShaderTypes.h"
|
||||
|
||||
#include <chrono>
|
||||
#include <filesystem>
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
class RuntimeStore
|
||||
{
|
||||
public:
|
||||
using StoredParameterSnapshot = LayerStackStore::StoredParameterSnapshot;
|
||||
using LayerPersistentState = LayerStackStore::LayerPersistentState;
|
||||
|
||||
RuntimeStore();
|
||||
HealthTelemetry& GetHealthTelemetry();
|
||||
const HealthTelemetry& GetHealthTelemetry() const;
|
||||
RenderSnapshotBuilder& GetRenderSnapshotBuilder();
|
||||
const RenderSnapshotBuilder& GetRenderSnapshotBuilder() const;
|
||||
|
||||
bool InitializeStore(std::string& error);
|
||||
std::string BuildPersistentStateJson() const;
|
||||
bool PollStoredFileChanges(bool& registryChanged, bool& reloadRequested, std::string& error);
|
||||
|
||||
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 ShaderParameterValue& value, 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);
|
||||
bool HasStoredLayer(const std::string& layerId) const;
|
||||
bool HasStoredShader(const std::string& shaderId) const;
|
||||
bool TryGetStoredParameterById(const std::string& layerId, const std::string& parameterId, StoredParameterSnapshot& snapshot, std::string& error) const;
|
||||
bool TryGetStoredParameterByControlKey(const std::string& layerKey, const std::string& parameterKey, StoredParameterSnapshot& snapshot, std::string& error) const;
|
||||
bool ResolveStoredLayerMove(const std::string& layerId, int direction, bool& shouldMove, std::string& error) const;
|
||||
bool ResolveStoredLayerMoveToIndex(const std::string& layerId, std::size_t targetIndex, bool& shouldMove, std::string& error) const;
|
||||
bool IsValidStackPresetName(const std::string& presetName) const;
|
||||
double GetRuntimeElapsedSeconds() const;
|
||||
|
||||
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 SetBoundControlServerPort(unsigned short port);
|
||||
|
||||
void SetCompileStatus(bool succeeded, const std::string& message);
|
||||
void ClearReloadRequest();
|
||||
bool CopyShaderPackageForStoredLayer(const std::string& layerId, ShaderPackage& shaderPackage, std::string& error) const;
|
||||
::ShaderCompilerInputs GetShaderCompilerInputs() const;
|
||||
::CommittedLiveStateReadModel BuildCommittedLiveStateReadModel() const;
|
||||
::RenderSnapshotReadModel BuildRenderSnapshotReadModel() const;
|
||||
std::vector<LayerPersistentState> CopyCommittedLiveLayerStates() const;
|
||||
std::vector<LayerPersistentState> CopyLayerStates() const;
|
||||
::RenderTimingSnapshot GetRenderTimingSnapshot() const;
|
||||
::RuntimeStatePresentationReadModel BuildRuntimeStatePresentationReadModel() const;
|
||||
|
||||
private:
|
||||
bool LoadPersistentState(std::string& error);
|
||||
bool SavePersistentState(std::string& error) const;
|
||||
bool ScanShaderPackages(std::string& error);
|
||||
std::string ReadTextFile(const std::filesystem::path& path, std::string& error) const;
|
||||
bool WriteTextFile(const std::filesystem::path& path, const std::string& contents, std::string& error) const;
|
||||
std::vector<std::string> GetStackPresetNamesLocked() const;
|
||||
void MarkRenderStateDirtyLocked();
|
||||
void MarkParameterStateDirtyLocked();
|
||||
|
||||
RenderSnapshotBuilder mRenderSnapshotBuilder;
|
||||
RuntimeConfigStore mConfigStore;
|
||||
ShaderPackageCatalog mShaderCatalog;
|
||||
LayerStackStore mLayerStack;
|
||||
HealthTelemetry mHealthTelemetry;
|
||||
mutable std::mutex mMutex;
|
||||
bool mReloadRequested;
|
||||
bool mCompileSucceeded;
|
||||
std::string mCompileMessage;
|
||||
double mStartupRandom;
|
||||
unsigned short mServerPort;
|
||||
bool mAutoReloadEnabled;
|
||||
std::chrono::steady_clock::time_point mStartTime;
|
||||
std::chrono::steady_clock::time_point mLastScanTime;
|
||||
};
|
||||
@@ -0,0 +1,55 @@
|
||||
#pragma once
|
||||
|
||||
#include "HealthTelemetry.h"
|
||||
#include "LayerStackStore.h"
|
||||
#include "RuntimeConfigStore.h"
|
||||
#include "ShaderPackageCatalog.h"
|
||||
#include "ShaderTypes.h"
|
||||
|
||||
#include <chrono>
|
||||
#include <filesystem>
|
||||
#include <map>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
struct ShaderCompilerInputs
|
||||
{
|
||||
std::filesystem::path repoRoot;
|
||||
std::filesystem::path wrapperPath;
|
||||
std::filesystem::path generatedGlslPath;
|
||||
std::filesystem::path patchedGlslPath;
|
||||
unsigned maxTemporalHistoryFrames = 0;
|
||||
};
|
||||
|
||||
struct RenderTimingSnapshot
|
||||
{
|
||||
std::chrono::steady_clock::time_point startTime;
|
||||
double startupRandom = 0.0;
|
||||
};
|
||||
|
||||
struct CommittedLiveStateReadModel
|
||||
{
|
||||
std::vector<LayerStackStore::LayerPersistentState> layers;
|
||||
std::map<std::string, ShaderPackage> packagesById;
|
||||
};
|
||||
|
||||
struct RenderSnapshotReadModel
|
||||
{
|
||||
CommittedLiveStateReadModel committedLiveState;
|
||||
HealthTelemetry::SignalStatusSnapshot signalStatus;
|
||||
RenderTimingSnapshot timing;
|
||||
};
|
||||
|
||||
struct RuntimeStatePresentationReadModel
|
||||
{
|
||||
RuntimeConfigStore::AppConfig config;
|
||||
HealthTelemetry::Snapshot telemetry;
|
||||
LayerStackStore layerStack;
|
||||
ShaderPackageCatalog::Snapshot shaderCatalog;
|
||||
std::vector<ShaderPackageStatus> packageStatuses;
|
||||
std::vector<std::string> stackPresetNames;
|
||||
unsigned short serverPort = 0;
|
||||
bool autoReloadEnabled = false;
|
||||
bool compileSucceeded = false;
|
||||
std::string compileMessage;
|
||||
};
|
||||
@@ -0,0 +1,127 @@
|
||||
#include "ShaderPackageCatalog.h"
|
||||
|
||||
#include "ShaderPackageRegistry.h"
|
||||
|
||||
bool ShaderPackageCatalog::Scan(const std::filesystem::path& shaderRoot, unsigned maxTemporalHistoryFrames, std::string& error)
|
||||
{
|
||||
std::map<std::string, ShaderPackage> packagesById;
|
||||
std::vector<std::string> packageOrder;
|
||||
std::vector<ShaderPackageStatus> packageStatuses;
|
||||
|
||||
ShaderPackageRegistry registry(maxTemporalHistoryFrames);
|
||||
if (!registry.Scan(shaderRoot, packagesById, packageOrder, packageStatuses, error))
|
||||
return false;
|
||||
|
||||
mPackagesById.swap(packagesById);
|
||||
mPackageOrder.swap(packageOrder);
|
||||
mPackageStatuses.swap(packageStatuses);
|
||||
return true;
|
||||
}
|
||||
|
||||
ShaderPackageCatalog::Snapshot ShaderPackageCatalog::CaptureSnapshot() const
|
||||
{
|
||||
Snapshot snapshot;
|
||||
snapshot.packagesById = mPackagesById;
|
||||
snapshot.packageOrder = mPackageOrder;
|
||||
return snapshot;
|
||||
}
|
||||
|
||||
bool ShaderPackageCatalog::HasCatalogChangedSince(const Snapshot& snapshot) const
|
||||
{
|
||||
if (snapshot.packageOrder != mPackageOrder || snapshot.packagesById.size() != mPackagesById.size())
|
||||
return true;
|
||||
|
||||
for (const auto& item : mPackagesById)
|
||||
{
|
||||
auto previous = snapshot.packagesById.find(item.first);
|
||||
if (previous == snapshot.packagesById.end() || !PackagesEquivalent(previous->second, item.second))
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
bool ShaderPackageCatalog::HasPackageChangedSince(const Snapshot& snapshot, const std::string& shaderId) const
|
||||
{
|
||||
auto previous = snapshot.packagesById.find(shaderId);
|
||||
auto current = mPackagesById.find(shaderId);
|
||||
if (previous == snapshot.packagesById.end() || current == mPackagesById.end())
|
||||
return previous != snapshot.packagesById.end() || current != mPackagesById.end();
|
||||
|
||||
return !PackagesEquivalent(previous->second, current->second);
|
||||
}
|
||||
|
||||
bool ShaderPackageCatalog::HasPackage(const std::string& shaderId) const
|
||||
{
|
||||
return mPackagesById.find(shaderId) != mPackagesById.end();
|
||||
}
|
||||
|
||||
const ShaderPackage* ShaderPackageCatalog::FindPackage(const std::string& shaderId) const
|
||||
{
|
||||
auto it = mPackagesById.find(shaderId);
|
||||
return it == mPackagesById.end() ? nullptr : &it->second;
|
||||
}
|
||||
|
||||
bool ShaderPackageCatalog::CopyPackage(const std::string& shaderId, ShaderPackage& shaderPackage) const
|
||||
{
|
||||
const ShaderPackage* package = FindPackage(shaderId);
|
||||
if (!package)
|
||||
return false;
|
||||
|
||||
shaderPackage = *package;
|
||||
return true;
|
||||
}
|
||||
|
||||
const std::vector<std::string>& ShaderPackageCatalog::PackageOrder() const
|
||||
{
|
||||
return mPackageOrder;
|
||||
}
|
||||
|
||||
const std::vector<ShaderPackageStatus>& ShaderPackageCatalog::PackageStatuses() const
|
||||
{
|
||||
return mPackageStatuses;
|
||||
}
|
||||
|
||||
bool ShaderPackageCatalog::PackagesEquivalent(const ShaderPackage& left, const ShaderPackage& right)
|
||||
{
|
||||
return left.shaderWriteTime == right.shaderWriteTime &&
|
||||
left.manifestWriteTime == right.manifestWriteTime &&
|
||||
TextureAssetsEqual(left.textureAssets, right.textureAssets) &&
|
||||
FontAssetsEqual(left.fontAssets, right.fontAssets);
|
||||
}
|
||||
|
||||
bool ShaderPackageCatalog::TextureAssetsEqual(const std::vector<ShaderTextureAsset>& left, const std::vector<ShaderTextureAsset>& right)
|
||||
{
|
||||
if (left.size() != right.size())
|
||||
return false;
|
||||
|
||||
for (std::size_t index = 0; index < left.size(); ++index)
|
||||
{
|
||||
if (left[index].id != right[index].id ||
|
||||
left[index].path != right[index].path ||
|
||||
left[index].writeTime != right[index].writeTime)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ShaderPackageCatalog::FontAssetsEqual(const std::vector<ShaderFontAsset>& left, const std::vector<ShaderFontAsset>& right)
|
||||
{
|
||||
if (left.size() != right.size())
|
||||
return false;
|
||||
|
||||
for (std::size_t index = 0; index < left.size(); ++index)
|
||||
{
|
||||
if (left[index].id != right[index].id ||
|
||||
left[index].path != right[index].path ||
|
||||
left[index].writeTime != right[index].writeTime)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
#pragma once
|
||||
|
||||
#include "ShaderTypes.h"
|
||||
|
||||
#include <filesystem>
|
||||
#include <map>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
class ShaderPackageCatalog
|
||||
{
|
||||
public:
|
||||
struct Snapshot
|
||||
{
|
||||
std::map<std::string, ShaderPackage> packagesById;
|
||||
std::vector<std::string> packageOrder;
|
||||
};
|
||||
|
||||
bool Scan(const std::filesystem::path& shaderRoot, unsigned maxTemporalHistoryFrames, std::string& error);
|
||||
Snapshot CaptureSnapshot() const;
|
||||
bool HasCatalogChangedSince(const Snapshot& snapshot) const;
|
||||
bool HasPackageChangedSince(const Snapshot& snapshot, const std::string& shaderId) const;
|
||||
bool HasPackage(const std::string& shaderId) const;
|
||||
const ShaderPackage* FindPackage(const std::string& shaderId) const;
|
||||
bool CopyPackage(const std::string& shaderId, ShaderPackage& shaderPackage) const;
|
||||
const std::vector<std::string>& PackageOrder() const;
|
||||
const std::vector<ShaderPackageStatus>& PackageStatuses() const;
|
||||
|
||||
private:
|
||||
static bool PackagesEquivalent(const ShaderPackage& left, const ShaderPackage& right);
|
||||
static bool TextureAssetsEqual(const std::vector<ShaderTextureAsset>& left, const std::vector<ShaderTextureAsset>& right);
|
||||
static bool FontAssetsEqual(const std::vector<ShaderFontAsset>& left, const std::vector<ShaderFontAsset>& right);
|
||||
|
||||
std::map<std::string, ShaderPackage> mPackagesById;
|
||||
std::vector<std::string> mPackageOrder;
|
||||
std::vector<ShaderPackageStatus> mPackageStatuses;
|
||||
};
|
||||
@@ -0,0 +1,206 @@
|
||||
#include "stdafx.h"
|
||||
#include "HealthTelemetry.h"
|
||||
|
||||
void HealthTelemetry::ReportSignalStatus(bool hasSignal, unsigned width, unsigned height, const std::string& modeName)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mMutex);
|
||||
mSignalStatus.hasSignal = hasSignal;
|
||||
mSignalStatus.width = width;
|
||||
mSignalStatus.height = height;
|
||||
mSignalStatus.modeName = modeName;
|
||||
}
|
||||
|
||||
bool HealthTelemetry::TryReportSignalStatus(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())
|
||||
return false;
|
||||
|
||||
mSignalStatus.hasSignal = hasSignal;
|
||||
mSignalStatus.width = width;
|
||||
mSignalStatus.height = height;
|
||||
mSignalStatus.modeName = modeName;
|
||||
return true;
|
||||
}
|
||||
|
||||
void HealthTelemetry::ReportVideoIOStatus(const std::string& backendName, const std::string& modelName,
|
||||
bool supportsInternalKeying, bool supportsExternalKeying, bool keyerInterfaceAvailable,
|
||||
bool externalKeyingRequested, bool externalKeyingActive, const std::string& statusMessage)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mMutex);
|
||||
mVideoIOStatus.backendName = backendName;
|
||||
mVideoIOStatus.modelName = modelName;
|
||||
mVideoIOStatus.supportsInternalKeying = supportsInternalKeying;
|
||||
mVideoIOStatus.supportsExternalKeying = supportsExternalKeying;
|
||||
mVideoIOStatus.keyerInterfaceAvailable = keyerInterfaceAvailable;
|
||||
mVideoIOStatus.externalKeyingRequested = externalKeyingRequested;
|
||||
mVideoIOStatus.externalKeyingActive = externalKeyingActive;
|
||||
mVideoIOStatus.statusMessage = statusMessage;
|
||||
}
|
||||
|
||||
bool HealthTelemetry::TryReportVideoIOStatus(const std::string& backendName, const std::string& modelName,
|
||||
bool supportsInternalKeying, bool supportsExternalKeying, bool keyerInterfaceAvailable,
|
||||
bool externalKeyingRequested, bool externalKeyingActive, const std::string& statusMessage)
|
||||
{
|
||||
std::unique_lock<std::mutex> lock(mMutex, std::try_to_lock);
|
||||
if (!lock.owns_lock())
|
||||
return false;
|
||||
|
||||
mVideoIOStatus.backendName = backendName;
|
||||
mVideoIOStatus.modelName = modelName;
|
||||
mVideoIOStatus.supportsInternalKeying = supportsInternalKeying;
|
||||
mVideoIOStatus.supportsExternalKeying = supportsExternalKeying;
|
||||
mVideoIOStatus.keyerInterfaceAvailable = keyerInterfaceAvailable;
|
||||
mVideoIOStatus.externalKeyingRequested = externalKeyingRequested;
|
||||
mVideoIOStatus.externalKeyingActive = externalKeyingActive;
|
||||
mVideoIOStatus.statusMessage = statusMessage;
|
||||
return true;
|
||||
}
|
||||
|
||||
void HealthTelemetry::RecordPerformanceStats(double frameBudgetMilliseconds, double renderMilliseconds)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mMutex);
|
||||
mPerformance.frameBudgetMilliseconds = std::max(frameBudgetMilliseconds, 0.0);
|
||||
mPerformance.renderMilliseconds = std::max(renderMilliseconds, 0.0);
|
||||
if (mPerformance.smoothedRenderMilliseconds <= 0.0)
|
||||
mPerformance.smoothedRenderMilliseconds = mPerformance.renderMilliseconds;
|
||||
else
|
||||
mPerformance.smoothedRenderMilliseconds = mPerformance.smoothedRenderMilliseconds * 0.9 + mPerformance.renderMilliseconds * 0.1;
|
||||
}
|
||||
|
||||
bool HealthTelemetry::TryRecordPerformanceStats(double frameBudgetMilliseconds, double renderMilliseconds)
|
||||
{
|
||||
std::unique_lock<std::mutex> lock(mMutex, std::try_to_lock);
|
||||
if (!lock.owns_lock())
|
||||
return false;
|
||||
|
||||
mPerformance.frameBudgetMilliseconds = std::max(frameBudgetMilliseconds, 0.0);
|
||||
mPerformance.renderMilliseconds = std::max(renderMilliseconds, 0.0);
|
||||
if (mPerformance.smoothedRenderMilliseconds <= 0.0)
|
||||
mPerformance.smoothedRenderMilliseconds = mPerformance.renderMilliseconds;
|
||||
else
|
||||
mPerformance.smoothedRenderMilliseconds = mPerformance.smoothedRenderMilliseconds * 0.9 + mPerformance.renderMilliseconds * 0.1;
|
||||
return true;
|
||||
}
|
||||
|
||||
void HealthTelemetry::RecordFramePacingStats(double completionIntervalMilliseconds, double smoothedCompletionIntervalMilliseconds,
|
||||
double maxCompletionIntervalMilliseconds, uint64_t lateFrameCount, uint64_t droppedFrameCount, uint64_t flushedFrameCount)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mMutex);
|
||||
mPerformance.completionIntervalMilliseconds = std::max(completionIntervalMilliseconds, 0.0);
|
||||
mPerformance.smoothedCompletionIntervalMilliseconds = std::max(smoothedCompletionIntervalMilliseconds, 0.0);
|
||||
mPerformance.maxCompletionIntervalMilliseconds = std::max(maxCompletionIntervalMilliseconds, 0.0);
|
||||
mPerformance.lateFrameCount = lateFrameCount;
|
||||
mPerformance.droppedFrameCount = droppedFrameCount;
|
||||
mPerformance.flushedFrameCount = flushedFrameCount;
|
||||
}
|
||||
|
||||
bool HealthTelemetry::TryRecordFramePacingStats(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);
|
||||
if (!lock.owns_lock())
|
||||
return false;
|
||||
|
||||
mPerformance.completionIntervalMilliseconds = std::max(completionIntervalMilliseconds, 0.0);
|
||||
mPerformance.smoothedCompletionIntervalMilliseconds = std::max(smoothedCompletionIntervalMilliseconds, 0.0);
|
||||
mPerformance.maxCompletionIntervalMilliseconds = std::max(maxCompletionIntervalMilliseconds, 0.0);
|
||||
mPerformance.lateFrameCount = lateFrameCount;
|
||||
mPerformance.droppedFrameCount = droppedFrameCount;
|
||||
mPerformance.flushedFrameCount = flushedFrameCount;
|
||||
return true;
|
||||
}
|
||||
|
||||
void HealthTelemetry::RecordRuntimeEventQueueMetrics(const std::string& queueName, std::size_t depth, std::size_t capacity,
|
||||
uint64_t droppedCount, double oldestEventAgeMilliseconds)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mMutex);
|
||||
mRuntimeEvents.queue.queueName = queueName;
|
||||
mRuntimeEvents.queue.depth = depth;
|
||||
mRuntimeEvents.queue.capacity = capacity;
|
||||
mRuntimeEvents.queue.droppedCount = droppedCount;
|
||||
mRuntimeEvents.queue.oldestEventAgeMilliseconds = std::max(oldestEventAgeMilliseconds, 0.0);
|
||||
}
|
||||
|
||||
bool HealthTelemetry::TryRecordRuntimeEventQueueMetrics(const std::string& queueName, std::size_t depth, std::size_t capacity,
|
||||
uint64_t droppedCount, double oldestEventAgeMilliseconds)
|
||||
{
|
||||
std::unique_lock<std::mutex> lock(mMutex, std::try_to_lock);
|
||||
if (!lock.owns_lock())
|
||||
return false;
|
||||
|
||||
mRuntimeEvents.queue.queueName = queueName;
|
||||
mRuntimeEvents.queue.depth = depth;
|
||||
mRuntimeEvents.queue.capacity = capacity;
|
||||
mRuntimeEvents.queue.droppedCount = droppedCount;
|
||||
mRuntimeEvents.queue.oldestEventAgeMilliseconds = std::max(oldestEventAgeMilliseconds, 0.0);
|
||||
return true;
|
||||
}
|
||||
|
||||
void HealthTelemetry::RecordRuntimeEventDispatchStats(std::size_t dispatchedEvents, std::size_t handlerInvocations,
|
||||
std::size_t handlerFailures, double dispatchDurationMilliseconds)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mMutex);
|
||||
++mRuntimeEvents.dispatch.dispatchCallCount;
|
||||
mRuntimeEvents.dispatch.dispatchedEventCount += static_cast<uint64_t>(dispatchedEvents);
|
||||
mRuntimeEvents.dispatch.handlerInvocationCount += static_cast<uint64_t>(handlerInvocations);
|
||||
mRuntimeEvents.dispatch.handlerFailureCount += static_cast<uint64_t>(handlerFailures);
|
||||
mRuntimeEvents.dispatch.lastDispatchDurationMilliseconds = std::max(dispatchDurationMilliseconds, 0.0);
|
||||
mRuntimeEvents.dispatch.maxDispatchDurationMilliseconds = std::max(
|
||||
mRuntimeEvents.dispatch.maxDispatchDurationMilliseconds,
|
||||
mRuntimeEvents.dispatch.lastDispatchDurationMilliseconds);
|
||||
}
|
||||
|
||||
bool HealthTelemetry::TryRecordRuntimeEventDispatchStats(std::size_t dispatchedEvents, std::size_t handlerInvocations,
|
||||
std::size_t handlerFailures, double dispatchDurationMilliseconds)
|
||||
{
|
||||
std::unique_lock<std::mutex> lock(mMutex, std::try_to_lock);
|
||||
if (!lock.owns_lock())
|
||||
return false;
|
||||
|
||||
++mRuntimeEvents.dispatch.dispatchCallCount;
|
||||
mRuntimeEvents.dispatch.dispatchedEventCount += static_cast<uint64_t>(dispatchedEvents);
|
||||
mRuntimeEvents.dispatch.handlerInvocationCount += static_cast<uint64_t>(handlerInvocations);
|
||||
mRuntimeEvents.dispatch.handlerFailureCount += static_cast<uint64_t>(handlerFailures);
|
||||
mRuntimeEvents.dispatch.lastDispatchDurationMilliseconds = std::max(dispatchDurationMilliseconds, 0.0);
|
||||
mRuntimeEvents.dispatch.maxDispatchDurationMilliseconds = std::max(
|
||||
mRuntimeEvents.dispatch.maxDispatchDurationMilliseconds,
|
||||
mRuntimeEvents.dispatch.lastDispatchDurationMilliseconds);
|
||||
return true;
|
||||
}
|
||||
|
||||
HealthTelemetry::SignalStatusSnapshot HealthTelemetry::GetSignalStatusSnapshot() const
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mMutex);
|
||||
return mSignalStatus;
|
||||
}
|
||||
|
||||
HealthTelemetry::VideoIOStatusSnapshot HealthTelemetry::GetVideoIOStatusSnapshot() const
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mMutex);
|
||||
return mVideoIOStatus;
|
||||
}
|
||||
|
||||
HealthTelemetry::PerformanceSnapshot HealthTelemetry::GetPerformanceSnapshot() const
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mMutex);
|
||||
return mPerformance;
|
||||
}
|
||||
|
||||
HealthTelemetry::RuntimeEventMetricsSnapshot HealthTelemetry::GetRuntimeEventMetricsSnapshot() const
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mMutex);
|
||||
return mRuntimeEvents;
|
||||
}
|
||||
|
||||
HealthTelemetry::Snapshot HealthTelemetry::GetSnapshot() const
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mMutex);
|
||||
|
||||
Snapshot snapshot;
|
||||
snapshot.signal = mSignalStatus;
|
||||
snapshot.videoIO = mVideoIOStatus;
|
||||
snapshot.performance = mPerformance;
|
||||
snapshot.runtimeEvents = mRuntimeEvents;
|
||||
return snapshot;
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
#pragma once
|
||||
|
||||
#include <mutex>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
|
||||
// Phase 1 compatibility seam for status and timing reporting. HealthTelemetry
|
||||
// owns the current operational status snapshot directly, so callers can report
|
||||
// health without sharing runtime-store state.
|
||||
class HealthTelemetry
|
||||
{
|
||||
public:
|
||||
struct SignalStatusSnapshot
|
||||
{
|
||||
bool hasSignal = false;
|
||||
unsigned width = 0;
|
||||
unsigned height = 0;
|
||||
std::string modeName;
|
||||
};
|
||||
|
||||
struct VideoIOStatusSnapshot
|
||||
{
|
||||
std::string backendName = "decklink";
|
||||
std::string modelName;
|
||||
bool supportsInternalKeying = false;
|
||||
bool supportsExternalKeying = false;
|
||||
bool keyerInterfaceAvailable = false;
|
||||
bool externalKeyingRequested = false;
|
||||
bool externalKeyingActive = false;
|
||||
std::string statusMessage;
|
||||
};
|
||||
|
||||
struct PerformanceSnapshot
|
||||
{
|
||||
double frameBudgetMilliseconds = 0.0;
|
||||
double renderMilliseconds = 0.0;
|
||||
double smoothedRenderMilliseconds = 0.0;
|
||||
double completionIntervalMilliseconds = 0.0;
|
||||
double smoothedCompletionIntervalMilliseconds = 0.0;
|
||||
double maxCompletionIntervalMilliseconds = 0.0;
|
||||
uint64_t lateFrameCount = 0;
|
||||
uint64_t droppedFrameCount = 0;
|
||||
uint64_t flushedFrameCount = 0;
|
||||
};
|
||||
|
||||
struct RuntimeEventQueueSnapshot
|
||||
{
|
||||
std::string queueName = "runtime-events";
|
||||
std::size_t depth = 0;
|
||||
std::size_t capacity = 0;
|
||||
uint64_t droppedCount = 0;
|
||||
double oldestEventAgeMilliseconds = 0.0;
|
||||
};
|
||||
|
||||
struct RuntimeEventDispatchSnapshot
|
||||
{
|
||||
uint64_t dispatchCallCount = 0;
|
||||
uint64_t dispatchedEventCount = 0;
|
||||
uint64_t handlerInvocationCount = 0;
|
||||
uint64_t handlerFailureCount = 0;
|
||||
double lastDispatchDurationMilliseconds = 0.0;
|
||||
double maxDispatchDurationMilliseconds = 0.0;
|
||||
};
|
||||
|
||||
struct RuntimeEventMetricsSnapshot
|
||||
{
|
||||
RuntimeEventQueueSnapshot queue;
|
||||
RuntimeEventDispatchSnapshot dispatch;
|
||||
};
|
||||
|
||||
struct Snapshot
|
||||
{
|
||||
SignalStatusSnapshot signal;
|
||||
VideoIOStatusSnapshot videoIO;
|
||||
PerformanceSnapshot performance;
|
||||
RuntimeEventMetricsSnapshot runtimeEvents;
|
||||
};
|
||||
|
||||
HealthTelemetry() = default;
|
||||
|
||||
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 ReportVideoIOStatus(const std::string& backendName, const std::string& modelName,
|
||||
bool supportsInternalKeying, bool supportsExternalKeying, bool keyerInterfaceAvailable,
|
||||
bool externalKeyingRequested, bool externalKeyingActive, const std::string& statusMessage);
|
||||
bool TryReportVideoIOStatus(const std::string& backendName, const std::string& modelName,
|
||||
bool supportsInternalKeying, bool supportsExternalKeying, bool keyerInterfaceAvailable,
|
||||
bool externalKeyingRequested, bool externalKeyingActive, const std::string& statusMessage);
|
||||
|
||||
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);
|
||||
|
||||
void RecordRuntimeEventQueueMetrics(const std::string& queueName, std::size_t depth, std::size_t capacity,
|
||||
uint64_t droppedCount, double oldestEventAgeMilliseconds);
|
||||
bool TryRecordRuntimeEventQueueMetrics(const std::string& queueName, std::size_t depth, std::size_t capacity,
|
||||
uint64_t droppedCount, double oldestEventAgeMilliseconds);
|
||||
|
||||
void RecordRuntimeEventDispatchStats(std::size_t dispatchedEvents, std::size_t handlerInvocations,
|
||||
std::size_t handlerFailures, double dispatchDurationMilliseconds);
|
||||
bool TryRecordRuntimeEventDispatchStats(std::size_t dispatchedEvents, std::size_t handlerInvocations,
|
||||
std::size_t handlerFailures, double dispatchDurationMilliseconds);
|
||||
|
||||
SignalStatusSnapshot GetSignalStatusSnapshot() const;
|
||||
VideoIOStatusSnapshot GetVideoIOStatusSnapshot() const;
|
||||
PerformanceSnapshot GetPerformanceSnapshot() const;
|
||||
RuntimeEventMetricsSnapshot GetRuntimeEventMetricsSnapshot() const;
|
||||
Snapshot GetSnapshot() const;
|
||||
|
||||
private:
|
||||
mutable std::mutex mMutex;
|
||||
SignalStatusSnapshot mSignalStatus;
|
||||
VideoIOStatusSnapshot mVideoIOStatus;
|
||||
PerformanceSnapshot mPerformance;
|
||||
RuntimeEventMetricsSnapshot mRuntimeEvents;
|
||||
};
|
||||
430
apps/LoopThroughWithOpenGLCompositing/videoio/VideoBackend.cpp
Normal file
430
apps/LoopThroughWithOpenGLCompositing/videoio/VideoBackend.cpp
Normal file
@@ -0,0 +1,430 @@
|
||||
#include "VideoBackend.h"
|
||||
|
||||
#include "DeckLinkSession.h"
|
||||
#include "OpenGLVideoIOBridge.h"
|
||||
#include "HealthTelemetry.h"
|
||||
#include "RenderEngine.h"
|
||||
#include "RuntimeEventDispatcher.h"
|
||||
|
||||
#include <chrono>
|
||||
#include <windows.h>
|
||||
|
||||
VideoBackend::VideoBackend(RenderEngine& renderEngine, HealthTelemetry& healthTelemetry, RuntimeEventDispatcher& runtimeEventDispatcher) :
|
||||
mHealthTelemetry(healthTelemetry),
|
||||
mRuntimeEventDispatcher(runtimeEventDispatcher),
|
||||
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()
|
||||
{
|
||||
const bool started = mVideoIODevice->Start();
|
||||
PublishBackendStateChanged(started ? "started" : "start-failed", started ? "Video backend started." : StatusMessage());
|
||||
return started;
|
||||
}
|
||||
|
||||
bool VideoBackend::Stop()
|
||||
{
|
||||
const bool stopped = mVideoIODevice->Stop();
|
||||
PublishBackendStateChanged(stopped ? "stopped" : "stop-failed", stopped ? "Video backend stopped." : StatusMessage());
|
||||
return stopped;
|
||||
}
|
||||
|
||||
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::PublishStatus(bool externalKeyingConfigured, const std::string& statusMessage)
|
||||
{
|
||||
if (!statusMessage.empty())
|
||||
SetStatusMessage(statusMessage);
|
||||
|
||||
mHealthTelemetry.ReportVideoIOStatus(
|
||||
"decklink",
|
||||
OutputModelName(),
|
||||
SupportsInternalKeying(),
|
||||
SupportsExternalKeying(),
|
||||
KeyerInterfaceAvailable(),
|
||||
externalKeyingConfigured,
|
||||
ExternalKeyingActive(),
|
||||
StatusMessage());
|
||||
PublishBackendStateChanged("status", StatusMessage());
|
||||
}
|
||||
|
||||
void VideoBackend::ReportNoInputDeviceSignalStatus()
|
||||
{
|
||||
mHealthTelemetry.ReportSignalStatus(
|
||||
false,
|
||||
InputFrameWidth(),
|
||||
InputFrameHeight(),
|
||||
InputDisplayModeName());
|
||||
PublishBackendStateChanged("no-input-device", "No input device is available.");
|
||||
}
|
||||
|
||||
void VideoBackend::HandleInputFrame(const VideoIOFrame& frame)
|
||||
{
|
||||
const VideoIOState& state = mVideoIODevice->State();
|
||||
mHealthTelemetry.TryReportSignalStatus(!frame.hasNoInputSource, state.inputFrameSize.width, state.inputFrameSize.height, state.inputDisplayModeName);
|
||||
PublishInputSignalChanged(frame, state);
|
||||
PublishInputFrameArrived(frame);
|
||||
|
||||
if (mBridge)
|
||||
mBridge->UploadInputFrame(frame, state);
|
||||
}
|
||||
|
||||
void VideoBackend::HandleOutputFrameCompletion(const VideoIOCompletion& completion)
|
||||
{
|
||||
RecordFramePacing(completion.result);
|
||||
PublishOutputFrameCompleted(completion);
|
||||
|
||||
VideoIOOutputFrame outputFrame;
|
||||
if (!BeginOutputFrame(outputFrame))
|
||||
return;
|
||||
|
||||
const VideoIOState& state = mVideoIODevice->State();
|
||||
bool rendered = true;
|
||||
if (mBridge)
|
||||
rendered = mBridge->RenderScheduledFrame(state, completion, outputFrame);
|
||||
|
||||
EndOutputFrame(outputFrame);
|
||||
AccountForCompletionResult(completion.result);
|
||||
if (!rendered)
|
||||
{
|
||||
PublishBackendStateChanged("output-render-failed", "Output frame render request failed; skipping schedule for this frame.");
|
||||
return;
|
||||
}
|
||||
|
||||
// Schedule the next frame after render work is complete so device-side
|
||||
// bookkeeping stays with the backend seam and the bridge stays render-only.
|
||||
if (ScheduleOutputFrame(outputFrame))
|
||||
PublishOutputFrameScheduled(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);
|
||||
PublishTimingSample("VideoBackend", "completionInterval", mCompletionIntervalMilliseconds, "ms");
|
||||
PublishTimingSample("VideoBackend", "smoothedCompletionInterval", mSmoothedCompletionIntervalMilliseconds, "ms");
|
||||
}
|
||||
|
||||
void VideoBackend::PublishBackendStateChanged(const std::string& state, const std::string& message)
|
||||
{
|
||||
try
|
||||
{
|
||||
BackendStateChangedEvent event;
|
||||
event.backendName = "decklink";
|
||||
event.state = state;
|
||||
event.message = message;
|
||||
if (!mRuntimeEventDispatcher.PublishPayload(event, "VideoBackend"))
|
||||
OutputDebugStringA("BackendStateChanged event publish failed.\n");
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
OutputDebugStringA("BackendStateChanged event publish threw.\n");
|
||||
}
|
||||
}
|
||||
|
||||
void VideoBackend::PublishInputSignalChanged(const VideoIOFrame& frame, const VideoIOState& state)
|
||||
{
|
||||
const bool hasSignal = !frame.hasNoInputSource;
|
||||
const unsigned width = state.inputFrameSize.width;
|
||||
const unsigned height = state.inputFrameSize.height;
|
||||
if (mHasLastInputSignal &&
|
||||
mLastInputSignal == hasSignal &&
|
||||
mLastInputSignalWidth == width &&
|
||||
mLastInputSignalHeight == height &&
|
||||
mLastInputSignalModeName == state.inputDisplayModeName)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
mHasLastInputSignal = true;
|
||||
mLastInputSignal = hasSignal;
|
||||
mLastInputSignalWidth = width;
|
||||
mLastInputSignalHeight = height;
|
||||
mLastInputSignalModeName = state.inputDisplayModeName;
|
||||
|
||||
try
|
||||
{
|
||||
InputSignalChangedEvent event;
|
||||
event.hasSignal = hasSignal;
|
||||
event.width = width;
|
||||
event.height = height;
|
||||
event.modeName = state.inputDisplayModeName;
|
||||
if (!mRuntimeEventDispatcher.PublishPayload(event, "VideoBackend"))
|
||||
OutputDebugStringA("InputSignalChanged event publish failed.\n");
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
OutputDebugStringA("InputSignalChanged event publish threw.\n");
|
||||
}
|
||||
}
|
||||
|
||||
void VideoBackend::PublishInputFrameArrived(const VideoIOFrame& frame)
|
||||
{
|
||||
try
|
||||
{
|
||||
InputFrameArrivedEvent event;
|
||||
event.frameIndex = ++mInputFrameIndex;
|
||||
event.width = frame.width;
|
||||
event.height = frame.height;
|
||||
event.rowBytes = frame.rowBytes;
|
||||
event.pixelFormat = PixelFormatName(frame.pixelFormat);
|
||||
event.hasNoInputSource = frame.hasNoInputSource;
|
||||
if (!mRuntimeEventDispatcher.PublishPayload(event, "VideoBackend"))
|
||||
OutputDebugStringA("InputFrameArrived event publish failed.\n");
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
OutputDebugStringA("InputFrameArrived event publish threw.\n");
|
||||
}
|
||||
}
|
||||
|
||||
void VideoBackend::PublishOutputFrameScheduled(const VideoIOOutputFrame& frame)
|
||||
{
|
||||
try
|
||||
{
|
||||
OutputFrameScheduledEvent event;
|
||||
event.frameIndex = ++mOutputFrameScheduleIndex;
|
||||
(void)frame;
|
||||
if (!mRuntimeEventDispatcher.PublishPayload(event, "VideoBackend"))
|
||||
OutputDebugStringA("OutputFrameScheduled event publish failed.\n");
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
OutputDebugStringA("OutputFrameScheduled event publish threw.\n");
|
||||
}
|
||||
}
|
||||
|
||||
void VideoBackend::PublishOutputFrameCompleted(const VideoIOCompletion& completion)
|
||||
{
|
||||
try
|
||||
{
|
||||
OutputFrameCompletedEvent event;
|
||||
event.frameIndex = ++mOutputFrameCompletionIndex;
|
||||
event.result = CompletionResultName(completion.result);
|
||||
if (!mRuntimeEventDispatcher.PublishPayload(event, "VideoBackend"))
|
||||
OutputDebugStringA("OutputFrameCompleted event publish failed.\n");
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
OutputDebugStringA("OutputFrameCompleted event publish threw.\n");
|
||||
}
|
||||
}
|
||||
|
||||
void VideoBackend::PublishTimingSample(const std::string& subsystem, const std::string& metric, double value, const std::string& unit)
|
||||
{
|
||||
try
|
||||
{
|
||||
TimingSampleRecordedEvent event;
|
||||
event.subsystem = subsystem;
|
||||
event.metric = metric;
|
||||
event.value = value;
|
||||
event.unit = unit;
|
||||
if (!mRuntimeEventDispatcher.PublishPayload(event, "HealthTelemetry"))
|
||||
OutputDebugStringA("TimingSampleRecorded event publish failed.\n");
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
OutputDebugStringA("TimingSampleRecorded event publish threw.\n");
|
||||
}
|
||||
}
|
||||
|
||||
std::string VideoBackend::CompletionResultName(VideoIOCompletionResult result)
|
||||
{
|
||||
switch (result)
|
||||
{
|
||||
case VideoIOCompletionResult::Completed:
|
||||
return "Completed";
|
||||
case VideoIOCompletionResult::DisplayedLate:
|
||||
return "DisplayedLate";
|
||||
case VideoIOCompletionResult::Dropped:
|
||||
return "Dropped";
|
||||
case VideoIOCompletionResult::Flushed:
|
||||
return "Flushed";
|
||||
case VideoIOCompletionResult::Unknown:
|
||||
default:
|
||||
return "Unknown";
|
||||
}
|
||||
}
|
||||
|
||||
std::string VideoBackend::PixelFormatName(VideoIOPixelFormat pixelFormat)
|
||||
{
|
||||
return std::string(VideoIOPixelFormatName(pixelFormat));
|
||||
}
|
||||
89
apps/LoopThroughWithOpenGLCompositing/videoio/VideoBackend.h
Normal file
89
apps/LoopThroughWithOpenGLCompositing/videoio/VideoBackend.h
Normal file
@@ -0,0 +1,89 @@
|
||||
#pragma once
|
||||
|
||||
#include "VideoIOTypes.h"
|
||||
|
||||
#include <chrono>
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
|
||||
class HealthTelemetry;
|
||||
class OpenGLVideoIOBridge;
|
||||
class RenderEngine;
|
||||
class RuntimeEventDispatcher;
|
||||
class VideoIODevice;
|
||||
|
||||
class VideoBackend
|
||||
{
|
||||
public:
|
||||
VideoBackend(RenderEngine& renderEngine, HealthTelemetry& healthTelemetry, RuntimeEventDispatcher& runtimeEventDispatcher);
|
||||
~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);
|
||||
void PublishStatus(bool externalKeyingConfigured, const std::string& statusMessage = std::string());
|
||||
void ReportNoInputDeviceSignalStatus();
|
||||
|
||||
private:
|
||||
void HandleInputFrame(const VideoIOFrame& frame);
|
||||
void HandleOutputFrameCompletion(const VideoIOCompletion& completion);
|
||||
void RecordFramePacing(VideoIOCompletionResult completionResult);
|
||||
void PublishBackendStateChanged(const std::string& state, const std::string& message);
|
||||
void PublishInputSignalChanged(const VideoIOFrame& frame, const VideoIOState& state);
|
||||
void PublishInputFrameArrived(const VideoIOFrame& frame);
|
||||
void PublishOutputFrameScheduled(const VideoIOOutputFrame& frame);
|
||||
void PublishOutputFrameCompleted(const VideoIOCompletion& completion);
|
||||
void PublishTimingSample(const std::string& subsystem, const std::string& metric, double value, const std::string& unit);
|
||||
static std::string CompletionResultName(VideoIOCompletionResult result);
|
||||
static std::string PixelFormatName(VideoIOPixelFormat pixelFormat);
|
||||
|
||||
HealthTelemetry& mHealthTelemetry;
|
||||
RuntimeEventDispatcher& mRuntimeEventDispatcher;
|
||||
std::unique_ptr<VideoIODevice> mVideoIODevice;
|
||||
std::unique_ptr<OpenGLVideoIOBridge> mBridge;
|
||||
uint64_t mInputFrameIndex = 0;
|
||||
uint64_t mOutputFrameScheduleIndex = 0;
|
||||
uint64_t mOutputFrameCompletionIndex = 0;
|
||||
bool mHasLastInputSignal = false;
|
||||
bool mLastInputSignal = false;
|
||||
unsigned mLastInputSignalWidth = 0;
|
||||
unsigned mLastInputSignalHeight = 0;
|
||||
std::string mLastInputSignalModeName;
|
||||
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;
|
||||
};
|
||||
@@ -81,14 +81,10 @@ HRESULT PlayoutDelegate::ScheduledFrameCompleted(IDeckLinkVideoFrame* completedF
|
||||
switch (result)
|
||||
{
|
||||
case bmdOutputFrameDisplayedLate:
|
||||
OutputDebugStringA("ScheduledFrameCompleted() frame did not complete: Frame Displayed Late\n");
|
||||
break;
|
||||
case bmdOutputFrameDropped:
|
||||
OutputDebugStringA("ScheduledFrameCompleted() frame did not complete: Frame Dropped\n");
|
||||
break;
|
||||
case bmdOutputFrameCompleted:
|
||||
case bmdOutputFrameFlushed:
|
||||
// Don't log bmdOutputFrameFlushed result since it is expected when Stop() is called
|
||||
// Late/drop counts are recorded by VideoBackend; keep this callback lean.
|
||||
break;
|
||||
default:
|
||||
OutputDebugStringA("ScheduledFrameCompleted() frame did not complete: Unknown error\n");
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -4,15 +4,24 @@ This note summarizes the main architectural improvements that would make the app
|
||||
|
||||
Phase checklist:
|
||||
|
||||
- [ ] Define subsystem boundaries and target architecture
|
||||
- [ ] Introduce an internal event model
|
||||
- [ ] Split `RuntimeHost`
|
||||
- [ ] Make the render thread the sole GL owner
|
||||
- [x] Define subsystem boundaries and target architecture
|
||||
- [x] Introduce an internal event model
|
||||
- [x] Split `RuntimeHost`
|
||||
- [x] Finish live-state and service-facing coordination
|
||||
- [x] Make the render thread the sole GL owner
|
||||
- [ ] 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
|
||||
|
||||
Checklist note:
|
||||
|
||||
- The checked Phase 1 item means the subsystem vocabulary, dependency direction, state categories, design package, and runtime implementation foothold are in place.
|
||||
- The checked Phase 2 item means the internal event model substrate is complete enough for later phases: the typed event vocabulary, app-owned dispatcher, coalesced event pump, reload bridge events, production bridges, and pure event tests are in place. Remaining items in [PHASE_2_INTERNAL_EVENT_MODEL_DESIGN.md](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/docs/PHASE_2_INTERNAL_EVENT_MODEL_DESIGN.md) are narrow follow-ups, mainly completion/failure observations and later replacement of the runtime-store poll fallback with real file-watch events.
|
||||
- The checked Phase 3 item means the render-facing state path now has named live-state, composition, frame-state, resolver, and service-bridge boundaries. `OpenGLComposite::renderEffect()` is reduced to runtime work, frame input construction, and frame rendering.
|
||||
- The checked Phase 4 item means normal runtime GL work is now owned by a dedicated `RenderEngine` render thread. Input upload, output render, preview, screenshot capture, render-local resets, and shader application enter through render-thread queue/request paths instead of caller-thread context borrowing. The remaining output timing risk is callback-coupled synchronous output production, which is intentionally tracked for the later DeckLink/backend lifecycle and playout-queue work.
|
||||
- It does not mean the whole app is fully extracted. Deeper live-state layering, background persistence, backend lifecycle/playout queue policy, and richer telemetry continue through later phases.
|
||||
|
||||
## Timing Review
|
||||
|
||||
The recent OSC work removed several control-path stalls, but the app still has a few deeper timing characteristics that matter for live resilience:
|
||||
@@ -20,16 +29,16 @@ The recent OSC work removed several control-path stalls, but the app still has a
|
||||
- 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
|
||||
- preview presentation is best-effort and render-thread queued, but still shares the same render-thread budget as playout
|
||||
- background service timing is partially event-driven; runtime-store scanning still uses a bounded compatibility poll fallback
|
||||
|
||||
Those points are important because they affect not just average performance, but how the app behaves under brief spikes, device jitter, or load bursts.
|
||||
|
||||
## Key Findings
|
||||
|
||||
### 1. `RuntimeHost` is carrying too many responsibilities
|
||||
### 1. The original runtime host carried too many responsibilities
|
||||
|
||||
`RuntimeHost` currently acts as:
|
||||
The original `RuntimeHost` acted as:
|
||||
|
||||
- config store
|
||||
- persistent state store
|
||||
@@ -42,7 +51,7 @@ That makes it a single contention and failure domain. It is also why OSC and ren
|
||||
|
||||
Relevant code:
|
||||
|
||||
- [RuntimeHost.h](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/runtime/RuntimeHost.h:15)
|
||||
- `RuntimeHost.h`
|
||||
|
||||
Recommended direction:
|
||||
|
||||
@@ -50,23 +59,23 @@ Recommended direction:
|
||||
- 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
|
||||
### 2. OpenGL ownership has moved to the render thread
|
||||
|
||||
Even after recent timing improvements, preview, input upload, and playout rendering still rely on one shared GL context protected by one `CRITICAL_SECTION`.
|
||||
Phase 4 removed normal runtime dependence on the old shared GL `CRITICAL_SECTION`. `RenderEngine` now owns a dedicated render thread and binds the GL context there for normal input upload, output rendering, preview presentation, screenshot capture, shader application, and render-local reset work.
|
||||
|
||||
Relevant code:
|
||||
|
||||
- [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)
|
||||
- [RenderEngine.cpp](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/gl/RenderEngine.cpp:36)
|
||||
- [OpenGLVideoIOBridge.cpp](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/gl/pipeline/OpenGLVideoIOBridge.cpp:11)
|
||||
- [OpenGLComposite.cpp](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/gl/OpenGLComposite.cpp:168)
|
||||
|
||||
This is still a central choke point and limits timing isolation.
|
||||
This removes cross-thread GL context borrowing as the central correctness model. The remaining timing risk is that output frame production is still synchronous from the DeckLink completion path, so a render/readback spike can still reduce playout headroom.
|
||||
|
||||
Recommended direction:
|
||||
|
||||
- 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
|
||||
- keep the render thread as the sole GL owner
|
||||
- replace synchronous output request/response with a bounded producer/consumer playout queue
|
||||
- keep preview and screenshot subordinate to output deadline pressure
|
||||
|
||||
### 3. Control flow is spread across polling and shared-memory patterns
|
||||
|
||||
@@ -120,12 +129,12 @@ Recommended direction:
|
||||
|
||||
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`
|
||||
- persisted/committed parameter state in `RuntimeStore`
|
||||
- transient OSC overlay state in `RenderEngine`
|
||||
|
||||
Relevant code:
|
||||
|
||||
- [OpenGLComposite.h](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/gl/OpenGLComposite.h:66)
|
||||
- [RenderEngine.h](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/gl/RenderEngine.h:18)
|
||||
|
||||
Recommended direction:
|
||||
|
||||
@@ -171,7 +180,7 @@ Relevant timing code:
|
||||
|
||||
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.
|
||||
- the output completion path currently requests a scheduled render through `OpenGLVideoIOBridge::RenderScheduledFrame()`, which asks the render thread to render/read back synchronously and then schedules the next frame in one callback-driven flow.
|
||||
- `VideoPlayoutScheduler::AccountForCompletionResult()` currently reacts to both late and dropped frames by blindly advancing the schedule index by `2`, which is simple but not especially robust.
|
||||
- `kPrerollFrameCount` is now `12`, but `DeckLinkSession::ConfigureOutput()` still creates a fixed pool of `10` mutable output frames. That mismatch suggests the buffering model is not being sized from one coherent source of truth.
|
||||
|
||||
@@ -195,8 +204,8 @@ 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
|
||||
- requesting render-thread output production
|
||||
- waiting for render/readback completion
|
||||
- performing output readback
|
||||
- scheduling the next frame
|
||||
|
||||
@@ -253,7 +262,7 @@ Recommended direction:
|
||||
|
||||
Relevant code:
|
||||
|
||||
- [RuntimeHost.cpp](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/runtime/RuntimeHost.cpp:1841)
|
||||
- `RuntimeHost.cpp`
|
||||
|
||||
Recent OSC work already reduced this problem for live automation, but the broader architecture would still benefit from:
|
||||
|
||||
@@ -278,7 +287,7 @@ Add lightweight tracing for:
|
||||
|
||||
- input callback latency
|
||||
- input upload skip count
|
||||
- GL lock wait time
|
||||
- render-thread request latency
|
||||
- render queue depth
|
||||
- render time
|
||||
- pass build/compile latency
|
||||
@@ -288,7 +297,7 @@ Add lightweight tracing for:
|
||||
- preroll depth versus spare-buffer depth
|
||||
- preview present cost and skipped-preview count
|
||||
- control queue depth
|
||||
- `RuntimeHost` lock contention
|
||||
- runtime state lock contention
|
||||
|
||||
That would make future tuning and failure diagnosis much easier.
|
||||
|
||||
@@ -332,19 +341,19 @@ Recommended direction:
|
||||
- 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
|
||||
### 8c. Background control and file-watch timing are partially event-driven
|
||||
|
||||
`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.
|
||||
`ControlServices::PollLoop()` now uses a condition-variable wakeup for queued OSC commit work and a fallback timer for compatibility polling. That removes the old fixed `25 x Sleep(10)` cadence as the default OSC commit timing model, but file-watch/runtime-store refresh work still relies on a compatibility poll path.
|
||||
|
||||
Relevant code:
|
||||
|
||||
- [RuntimeServices.cpp](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/control/RuntimeServices.cpp:245)
|
||||
- [ControlServices.cpp](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/control/ControlServices.cpp:217)
|
||||
|
||||
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.
|
||||
That is acceptable as transitional non-critical background work. The Phase 2 bridge now publishes typed reload/file-change events when changes are detected; a later file-watch implementation can replace scanning as the source.
|
||||
|
||||
Recommended direction:
|
||||
|
||||
- replace coarse sleep polling with waitable events or condition-variable driven wakeups where practical
|
||||
- replace runtime-store scanning with true file-watch events when practical
|
||||
- isolate truly background work from latency-sensitive control reconciliation
|
||||
- add separate metrics for queue age, not just queue depth
|
||||
|
||||
@@ -356,13 +365,23 @@ This roadmap is ordered by architectural dependency rather than by “quick wins
|
||||
|
||||
Before changing major internals, formalize the target responsibilities for each major part of the app.
|
||||
|
||||
Status:
|
||||
|
||||
- Design deliverable: complete.
|
||||
- Runtime implementation foothold: complete.
|
||||
- Target boundary extraction: not complete across the whole app; remaining work is tracked by later phases, especially the event model, render ownership, live-state layering, backend lifecycle, telemetry, and persistence work.
|
||||
|
||||
Target split:
|
||||
|
||||
- `RuntimeStore`
|
||||
- persisted config
|
||||
- persisted layer stack
|
||||
- preset persistence
|
||||
- `RuntimeSnapshot`
|
||||
- `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`
|
||||
@@ -376,7 +395,7 @@ Target split:
|
||||
- `VideoBackend`
|
||||
- DeckLink input/output lifecycle
|
||||
- pacing and scheduling
|
||||
- `Health/Telemetry`
|
||||
- `HealthTelemetry`
|
||||
- logging
|
||||
- counters
|
||||
- timing traces
|
||||
@@ -393,11 +412,23 @@ Suggested deliverables:
|
||||
- a short architecture diagram
|
||||
- a responsibility table for each subsystem
|
||||
- a list of allowed dependencies between subsystems
|
||||
- a dedicated Phase 1 design note:
|
||||
- [PHASE_1_SUBSYSTEM_BOUNDARIES_DESIGN.md](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/docs/PHASE_1_SUBSYSTEM_BOUNDARIES_DESIGN.md)
|
||||
- a subsystem design bundle index:
|
||||
- [docs/subsystems/README.md](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/docs/subsystems/README.md)
|
||||
|
||||
Current implementation note:
|
||||
|
||||
The repo now has concrete runtime classes, folders, read models, and subsystem tests for the Phase 1 names. These classes are the runtime foothold for later phases; app-wide extraction still continues around eventing, render ownership, backend lifecycle, persistence, and telemetry.
|
||||
|
||||
### Phase 2. Introduce an internal event model
|
||||
|
||||
Once subsystem boundaries are defined, introduce a typed event pipeline between them. This should happen before large state splits so the app has a stable coordination model.
|
||||
|
||||
Dedicated design note:
|
||||
|
||||
- [PHASE_2_INTERNAL_EVENT_MODEL_DESIGN.md](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/docs/PHASE_2_INTERNAL_EVENT_MODEL_DESIGN.md)
|
||||
|
||||
Example event families:
|
||||
|
||||
- control events
|
||||
@@ -430,9 +461,13 @@ 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
|
||||
### Phase 3. Finish live-state and service-facing coordination
|
||||
|
||||
After the event model exists, break apart `RuntimeHost`.
|
||||
After the event model exists, finish separating live committed state and service-facing coordination from the runtime facades.
|
||||
|
||||
Dedicated design note:
|
||||
|
||||
- [PHASE_3_LIVE_STATE_SERVICE_COORDINATION_DESIGN.md](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/docs/PHASE_3_LIVE_STATE_SERVICE_COORDINATION_DESIGN.md)
|
||||
|
||||
Recommended split:
|
||||
|
||||
@@ -464,6 +499,15 @@ Primary design rule:
|
||||
|
||||
With state and coordination cleaner, move to a dedicated render-thread model.
|
||||
|
||||
Dedicated design note:
|
||||
|
||||
- [PHASE_4_RENDER_THREAD_OWNERSHIP_DESIGN.md](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/docs/PHASE_4_RENDER_THREAD_OWNERSHIP_DESIGN.md)
|
||||
|
||||
Status:
|
||||
|
||||
- complete for GL ownership
|
||||
- remaining playout-headroom work is tracked under Phase 7/backend lifecycle
|
||||
|
||||
Target behavior:
|
||||
|
||||
- one thread owns the GL context
|
||||
@@ -480,7 +524,7 @@ Other threads should only:
|
||||
|
||||
Why this phase comes here:
|
||||
|
||||
- it is much safer once state access and control coordination are no longer centered on `RuntimeHost`
|
||||
- it is much safer once state access and control coordination are no longer centered on one shared runtime object
|
||||
- it avoids coupling the render-thread refactor to storage and service refactors at the same time
|
||||
|
||||
Expected benefits:
|
||||
@@ -494,6 +538,10 @@ Expected benefits:
|
||||
|
||||
Once rendering and snapshots are isolated, formalize how final parameter values are derived.
|
||||
|
||||
Dedicated design note:
|
||||
|
||||
- [PHASE_5_LIVE_STATE_LAYERING_DESIGN.md](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/docs/PHASE_5_LIVE_STATE_LAYERING_DESIGN.md)
|
||||
|
||||
Recommended layers:
|
||||
|
||||
- base persisted state
|
||||
@@ -519,6 +567,10 @@ Expected benefits:
|
||||
|
||||
After the state model is explicit, persistence should become a background concern rather than a synchronous side effect of mutations.
|
||||
|
||||
Dedicated design note:
|
||||
|
||||
- [PHASE_6_BACKGROUND_PERSISTENCE_DESIGN.md](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/docs/PHASE_6_BACKGROUND_PERSISTENCE_DESIGN.md)
|
||||
|
||||
Target behavior:
|
||||
|
||||
- mutations update authoritative in-memory stored state
|
||||
@@ -529,7 +581,7 @@ Target behavior:
|
||||
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
|
||||
- it should operate on the new `RuntimeStore` model, not on a mixed-responsibility runtime object
|
||||
|
||||
Expected benefits:
|
||||
|
||||
@@ -541,6 +593,10 @@ Expected benefits:
|
||||
|
||||
Once the render and state layers are cleaner, refactor the video backend into an explicit lifecycle model.
|
||||
|
||||
Dedicated design note:
|
||||
|
||||
- [PHASE_7_BACKEND_LIFECYCLE_PLAYOUT_DESIGN.md](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/docs/PHASE_7_BACKEND_LIFECYCLE_PLAYOUT_DESIGN.md)
|
||||
|
||||
Suggested states:
|
||||
|
||||
- uninitialized
|
||||
@@ -569,10 +625,13 @@ Expected benefits:
|
||||
|
||||
This phase should happen after the main ownership changes so the telemetry can reflect the final architecture instead of a transient one.
|
||||
|
||||
Dedicated design note:
|
||||
|
||||
- [PHASE_8_HEALTH_TELEMETRY_DESIGN.md](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/docs/PHASE_8_HEALTH_TELEMETRY_DESIGN.md)
|
||||
|
||||
Recommended coverage:
|
||||
|
||||
- render queue depth
|
||||
- GL lock wait time, if any shared lock remains
|
||||
- input callback latency
|
||||
- input upload skip count
|
||||
- output scheduling lag
|
||||
@@ -605,7 +664,7 @@ If this is approached as a serious architecture program rather than opportunisti
|
||||
|
||||
1. Define subsystem boundaries and target architecture.
|
||||
2. Introduce the internal event model.
|
||||
3. Split `RuntimeHost`.
|
||||
3. Finish runtime live-state/service coordination.
|
||||
4. Make the render thread the sole GL owner.
|
||||
5. Formalize live state layering and composition.
|
||||
6. Move persistence to a background snapshot writer.
|
||||
@@ -617,14 +676,14 @@ If this is approached as a serious architecture program rather than opportunisti
|
||||
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.
|
||||
- runtime state ownership is split before render isolation so the render thread does not inherit a monolithic state model.
|
||||
- Live state layering is formalized only after render ownership is clearer.
|
||||
- Persistence 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:
|
||||
The app is in a much better place than it was before the OSC timing work. The shared-GL ownership risk has now been addressed by Phase 4; the main remaining live-resilience risk is output playout headroom because DeckLink callbacks still synchronously request render-thread output production. The most sensible path forward is:
|
||||
|
||||
1. define boundaries
|
||||
2. establish an event model
|
||||
|
||||
714
docs/PHASE_1_SUBSYSTEM_BOUNDARIES_DESIGN.md
Normal file
714
docs/PHASE_1_SUBSYSTEM_BOUNDARIES_DESIGN.md
Normal file
@@ -0,0 +1,714 @@
|
||||
# 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 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.
|
||||
|
||||
## Status
|
||||
|
||||
Phase 1 has two different meanings in this repo, and they should not be collapsed:
|
||||
|
||||
- Phase 1 design package: complete.
|
||||
- Phase 1 runtime implementation foothold: complete.
|
||||
|
||||
The completed design package includes the agreed subsystem names, responsibilities, dependency rules, state categories, and current-to-target migration map. The runtime code now has concrete subsystem folders, collaborators, read models, and tests for those boundaries, and the compiled runtime path no longer depends on `RuntimeHost`. That is different from saying every target boundary is fully extracted across the whole app: later roadmap phases are still responsible for the event model, sole-owner render thread, explicit live-state layering, background persistence, backend state machine, and fuller telemetry.
|
||||
|
||||
## Why Phase 1 Exists
|
||||
|
||||
At the start of this phase the app worked, but too many responsibilities converged in a few places:
|
||||
|
||||
- `RuntimeHost` owned 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` was both store and live authority:
|
||||
- `RuntimeHost.h`
|
||||
- `RuntimeHost.cpp`
|
||||
- `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 a Phase 1 runtime implementation foothold in place:
|
||||
|
||||
- `RuntimeStore`
|
||||
- [RuntimeStore.h](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/runtime/store/RuntimeStore.h)
|
||||
- [RuntimeStore.cpp](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/runtime/store/RuntimeStore.cpp)
|
||||
- `RuntimeConfigStore`
|
||||
- [RuntimeConfigStore.h](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/runtime/store/RuntimeConfigStore.h)
|
||||
- [RuntimeConfigStore.cpp](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/runtime/store/RuntimeConfigStore.cpp)
|
||||
- `ShaderPackageCatalog`
|
||||
- [ShaderPackageCatalog.h](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/runtime/store/ShaderPackageCatalog.h)
|
||||
- [ShaderPackageCatalog.cpp](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/runtime/store/ShaderPackageCatalog.cpp)
|
||||
- `RuntimeCoordinator`
|
||||
- [RuntimeCoordinator.h](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/runtime/coordination/RuntimeCoordinator.h)
|
||||
- [RuntimeCoordinator.cpp](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/runtime/coordination/RuntimeCoordinator.cpp)
|
||||
- `RuntimeSnapshotProvider`
|
||||
- [RuntimeSnapshotProvider.h](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/runtime/snapshot/RuntimeSnapshotProvider.h)
|
||||
- [RuntimeSnapshotProvider.cpp](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/runtime/snapshot/RuntimeSnapshotProvider.cpp)
|
||||
- `RenderSnapshotBuilder`
|
||||
- [RenderSnapshotBuilder.h](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/runtime/snapshot/RenderSnapshotBuilder.h)
|
||||
- [RenderSnapshotBuilder.cpp](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/runtime/snapshot/RenderSnapshotBuilder.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/telemetry/HealthTelemetry.h)
|
||||
- [HealthTelemetry.cpp](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/runtime/telemetry/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)
|
||||
|
||||
The runtime seams are now concrete code boundaries. Some app-level flows still delegate through compatibility helpers, `OpenGLComposite`, `DeckLinkSession`, and the existing bridge/pipeline classes, but runtime responsibilities have moved behind named collaborators:
|
||||
|
||||
- UI/runtime control calls in `OpenGLCompositeRuntimeControls.cpp` now route through `RuntimeCoordinator`
|
||||
- runtime startup now initializes path resolution and config loading through `RuntimeConfigStore`, with shader package scan and lookup delegated to `ShaderPackageCatalog`
|
||||
- runtime/UI state JSON composition now routes through `RuntimeStatePresenter` and `RuntimeStateJson` instead of living in `RuntimeHost` or `RuntimeStore`
|
||||
- regular stored layer mutations and stack preset save/load now route through `RuntimeStore` into `LayerStackStore` instead of `RuntimeHost` public APIs
|
||||
- persisted OSC-by-control-key commits now route through `RuntimeCoordinator` before applying store changes
|
||||
- mutation and reload policy now routes through `RuntimeCoordinator`
|
||||
- parameter target resolution, value normalization, trigger classification, and move no-op classification now live under `RuntimeCoordinator`
|
||||
- render-state and shader-build reads in `OpenGLComposite.cpp`, `OpenGLShaderPrograms.cpp`, and `ShaderBuildQueue.cpp` now route through `RuntimeSnapshotProvider`
|
||||
- `RuntimeSnapshotProvider` now depends on `RenderSnapshotBuilder` rather than on `RuntimeStore` friendship or shared `RuntimeHost` access
|
||||
- render-state assembly, cached parameter refresh, dynamic frame-field application, and render snapshot versions now live in `RenderSnapshotBuilder` instead of `RuntimeStore`
|
||||
- `RuntimeSnapshotProvider` now publishes versioned render snapshot objects and serves matching consumers from the last published snapshot
|
||||
- service ingress and polling coordination now route through `ControlServices`
|
||||
- `ControlServices` now queues coordinator results for OSC commit and file-poll outcomes instead of directly deciding runtime/store policy
|
||||
- timing and status writes now route through `HealthTelemetry`
|
||||
- `HealthTelemetry` now owns the live signal, video-I/O, and performance snapshots directly instead of `RuntimeHost` keeping those backing fields
|
||||
- render-side frame advancement and render-performance reporting now flow through `RuntimeSnapshotProvider` and `HealthTelemetry` instead of directly through `RuntimeHost`
|
||||
- `RuntimeStore` now owns its durable/session backing fields directly instead of wrapping a compatibility `RuntimeHost` object
|
||||
- `RuntimeConfigStore` now owns runtime config parsing, path resolution, configured ports/formats, runtime roots, and shader compiler paths instead of leaving those responsibilities inside `RuntimeStore`
|
||||
- `ShaderPackageCatalog` now owns shader package scanning, package status/order/lookup, and package asset/source change comparison instead of leaving those responsibilities inside `RuntimeStore`
|
||||
- `LayerStackStore` now owns durable layer state, layer CRUD/reorder, parameter persistence, and stack preset value serialization/load instead of leaving those responsibilities inside `RuntimeStore`
|
||||
- `RuntimeStatePresenter` and `RuntimeStateJson` now own runtime-state JSON assembly and layer-stack presentation serialization instead of leaving those responsibilities inside storage classes
|
||||
- `RuntimeCoordinator` now uses explicit `RuntimeStore` query APIs/read models instead of friendship or direct store-internal access
|
||||
- live OSC overlay state and smoothing/commit decisions now live under `RenderEngine` instead of `OpenGLComposite`
|
||||
- coordinator result application, shader-build requests, ready-build application, and runtime-state broadcasts now route through `RuntimeUpdateController` instead of being interpreted directly by `OpenGLComposite`
|
||||
- `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
|
||||
- `RuntimeSubsystemTests` now cover the new runtime seams around layer-stack storage, preset round-trips, mutation classification, and runtime-state JSON serialization
|
||||
|
||||
That means Phase 2 can focus on eventing and coordination mechanics rather than inventing the runtime boundary vocabulary.
|
||||
|
||||
Later-phase extraction work includes:
|
||||
|
||||
- moving persistence to an asynchronous writer in a later phase
|
||||
- replacing polling/shared-object coordination with the planned internal event model
|
||||
- making the render thread the sole GL owner
|
||||
- formalizing committed-live versus transient-overlay layering
|
||||
- making backend lifecycle and telemetry richer and more explicit
|
||||
|
||||
## 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 -> RenderSnapshotBuilder`
|
||||
- `RenderSnapshotBuilder -> 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.
|
||||
|
||||
### Previous `RuntimeHost`
|
||||
|
||||
Should eventually split across:
|
||||
|
||||
- `RuntimeStore`
|
||||
- `RuntimeCoordinator`
|
||||
- `RuntimeSnapshotProvider`
|
||||
- parts of `HealthTelemetry`
|
||||
|
||||
Likely examples:
|
||||
|
||||
- config loading/path resolution -> `RuntimeConfigStore`
|
||||
- persistent state saving -> `RuntimeStore`
|
||||
- layer stack mutation validation -> `RuntimeCoordinator`
|
||||
- render state building/versioning -> `RenderSnapshotBuilder`
|
||||
- render snapshot publication/cache -> `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 runtime object should be introduced
|
||||
|
||||
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 the subsystem facades 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 runtime services, `OpenGLComposite`, and backend/render bridge code
|
||||
- a decision that later phases will build against this target rather than inventing new boundaries ad hoc
|
||||
|
||||
By that definition, Phase 1 is complete for runtime: the design package is complete, `RuntimeHost` is retired from the compiled runtime path, runtime seams are represented in code, and runtime subsystem tests cover the new boundaries. App-wide ownership work continues in later phases.
|
||||
|
||||
## 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.
|
||||
660
docs/PHASE_2_INTERNAL_EVENT_MODEL_DESIGN.md
Normal file
660
docs/PHASE_2_INTERNAL_EVENT_MODEL_DESIGN.md
Normal file
@@ -0,0 +1,660 @@
|
||||
# Phase 2 Design: Internal Event Model
|
||||
|
||||
This document expands Phase 2 of [ARCHITECTURE_RESILIENCE_REVIEW.md](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/docs/ARCHITECTURE_RESILIENCE_REVIEW.md) into a concrete design target.
|
||||
|
||||
Phase 1 established the subsystem vocabulary and moved the runtime path behind clearer collaborators. Phase 2 should now give those subsystems a safer way to coordinate than direct cross-calls, shared mutable result queues, and coarse polling loops.
|
||||
|
||||
## Status
|
||||
|
||||
- Phase 2 design package: accepted.
|
||||
- Phase 2 implementation: substantially complete for the coordination substrate.
|
||||
- Current alignment: the typed event substrate, app-owned dispatcher, coalesced app pump, reload bridge events, production bridges, and event behavior tests are in place. Remaining items are narrow follow-ups rather than foundation work.
|
||||
|
||||
The current repo now has concrete Phase 2 implementation footholds:
|
||||
|
||||
- `RuntimeEventType`, typed payload structs, `RuntimeEvent`, `RuntimeEventQueue`, `RuntimeEventDispatcher`, and `RuntimeEventCoalescingQueue` define the event substrate.
|
||||
- `OpenGLComposite` owns one app-level `RuntimeEventDispatcher` and passes it into `RuntimeServices`, `RuntimeCoordinator`, `RuntimeUpdateController`, `RuntimeSnapshotProvider`, `ShaderBuildQueue`, and `VideoBackend`.
|
||||
- `ControlServices` publishes typed OSC and runtime-state broadcast events and uses condition-variable wakeups with a fallback compatibility timer.
|
||||
- `RuntimeCoordinator` publishes accepted, rejected, state-changed, persistence, reload, shader-build, and compile-status follow-up events.
|
||||
- `RuntimeUpdateController` subscribes to event families for broadcast, shader build, compile status, render reset, and dispatcher health observations.
|
||||
- `RuntimeSnapshotProvider` publishes render snapshot request/published events.
|
||||
- `ShaderBuildQueue` and `RuntimeUpdateController` publish shader build lifecycle events with generation matching.
|
||||
- `VideoBackend` publishes backend observation events and timing samples.
|
||||
- `HealthTelemetry` receives dispatcher metrics directly and the event vocabulary now includes health observation events.
|
||||
- Tests cover event type stability, payload mapping, FIFO dispatch, coalescing infrastructure, app-level coalesced broadcast/build behavior, handler failures, mutation follow-up behavior, reload bridge behavior, and shader-build generation behavior.
|
||||
|
||||
The implementation is now established in the repo. The remaining Phase 2 follow-up work is small: add completion/failure observations where useful and keep the runtime-store poll fallback explicitly transitional until a later file-watch implementation replaces it.
|
||||
|
||||
## Why Phase 2 Exists
|
||||
|
||||
The resilience review originally called out three timing and ownership problems that an event model could directly improve:
|
||||
|
||||
- background service timing relied on coarse sleeps and polling
|
||||
- control, reload, persistence, and render-update work traveled through mixed shared state and result queues
|
||||
- later render/backend refactors need a stable coordination model before they move more work across threads
|
||||
|
||||
The goal is not to make the app fully asynchronous in one pass. It is to introduce typed internal events so each subsystem can publish what happened without knowing who will react or how many downstream effects are needed.
|
||||
|
||||
## Goals
|
||||
|
||||
Phase 2 should establish:
|
||||
|
||||
- a small typed event vocabulary for control, runtime, render, backend, persistence, and health coordination
|
||||
- one app-owned event pump or dispatcher that can route events deterministically
|
||||
- bounded queues with clear ownership and no unbounded background growth
|
||||
- wakeup-driven service coordination where practical, replacing coarse polling as the default shape
|
||||
- explicit event-to-command boundaries so events do not become hidden global mutation APIs
|
||||
- tests for event ordering, coalescing, rejection, and dispatch side effects
|
||||
|
||||
## Non-Goals
|
||||
|
||||
Phase 2 should not require:
|
||||
|
||||
- a dedicated render thread yet
|
||||
- a full actor system
|
||||
- lock-free queues everywhere
|
||||
- background persistence implementation
|
||||
- a complete DeckLink state machine
|
||||
- final live-state layering
|
||||
- replacing every direct call in one change
|
||||
|
||||
Those are later phases. Phase 2 provides the coordination substrate they can build on.
|
||||
|
||||
## Current Coordination Shape
|
||||
|
||||
The current runtime is much cleaner than before Phase 1, and Phase 2 has moved the main coordination model toward typed publication and app-owned dispatch:
|
||||
|
||||
- `ControlServices` publishes OSC value, OSC commit, and runtime-state broadcast events.
|
||||
- `ControlServices::PollLoop(...)` is wakeup-driven for queued OSC commit work, with a bounded fallback timer for compatibility polling.
|
||||
- `RuntimeCoordinator` still returns `RuntimeCoordinatorResult` for synchronous callers, but also publishes accepted/rejected/follow-up events.
|
||||
- `RuntimeUpdateController` subscribes to event families and applies many effects from events rather than only from drained result objects.
|
||||
- shader-build request, readiness, failure, and application are represented by typed events.
|
||||
- render snapshot publication and backend observations are represented by typed events.
|
||||
- dispatcher queue metrics and handler failures feed telemetry and health observation events.
|
||||
|
||||
There is still transitional bridge-state:
|
||||
|
||||
- `ControlServices` still exposes completed OSC commit notifications for render overlay settlement.
|
||||
- `RuntimeEventCoalescingQueue` is now wired into the app-owned dispatcher for latest-value event types.
|
||||
- `FileChangeDetected` and `ManualReloadRequested` are now published as reload ingress bridge events before coordinator reload follow-ups.
|
||||
- runtime-state broadcast completion/failure events are still a target, not current behavior.
|
||||
|
||||
That means Phase 2 is complete enough as the coordination substrate for later phases. The remaining items are refinement work and should not block moving to render ownership, live-state layering, or persistence work.
|
||||
|
||||
## Event Model Principles
|
||||
|
||||
### Events say what happened
|
||||
|
||||
Events should describe facts:
|
||||
|
||||
- `OscValueReceived`
|
||||
- `RuntimeMutationAccepted`
|
||||
- `RuntimeMutationRejected`
|
||||
- `ShaderReloadRequested`
|
||||
- `ShaderBuildPrepared`
|
||||
- `ShaderBuildFailed`
|
||||
- `RenderSnapshotPublished`
|
||||
- `RuntimeStateBroadcastRequested`
|
||||
|
||||
They should not be vague commands like "do everything needed now."
|
||||
|
||||
### Commands request intent
|
||||
|
||||
Some work is still naturally command-shaped:
|
||||
|
||||
- "apply this parameter mutation"
|
||||
- "request shader reload"
|
||||
- "save this stack preset"
|
||||
- "start backend output"
|
||||
|
||||
Commands enter an owner subsystem. Events leave a subsystem after the owner has accepted, rejected, or completed work.
|
||||
|
||||
### One owner mutates each state category
|
||||
|
||||
Events must not become a way to bypass Phase 1 ownership:
|
||||
|
||||
- `RuntimeCoordinator` remains the owner of mutation policy.
|
||||
- `RuntimeStore` remains the owner of durable state.
|
||||
- `RuntimeSnapshotProvider` remains the owner of render snapshot publication.
|
||||
- `RenderEngine` remains the owner of render-local transient state.
|
||||
- `VideoBackend` remains the owner of device lifecycle and pacing.
|
||||
- `HealthTelemetry` observes and reports, but does not coordinate behavior.
|
||||
|
||||
### Event handlers should be small
|
||||
|
||||
Handlers should translate events into owner calls or follow-up events. They should not accumulate hidden long-lived state unless that state belongs to the handler's subsystem.
|
||||
|
||||
### Queues must be bounded or coalesced
|
||||
|
||||
High-rate control traffic can arrive faster than the app should process every individual sample. Phase 2 should preserve the useful current behavior of coalescing OSC updates by route, but make the coalescing policy explicit.
|
||||
|
||||
## Event Families
|
||||
|
||||
### Control Events
|
||||
|
||||
Produced by `ControlServices`.
|
||||
|
||||
Examples:
|
||||
|
||||
- `OscValueReceived`
|
||||
- `OscValueCoalesced`
|
||||
- `OscCommitRequested`
|
||||
- `HttpControlMutationRequested`
|
||||
- `WebSocketClientConnected`
|
||||
- `RuntimeStateBroadcastRequested`
|
||||
- `FileChangeDetected`
|
||||
- `ManualReloadRequested`
|
||||
|
||||
Primary consumers:
|
||||
|
||||
- `RuntimeCoordinator`
|
||||
- `HealthTelemetry`
|
||||
- later, a persistence writer or diagnostics publisher
|
||||
|
||||
### Runtime Events
|
||||
|
||||
Produced by `RuntimeCoordinator`, `RuntimeStore`, and snapshot publication code.
|
||||
|
||||
Examples:
|
||||
|
||||
- `RuntimeMutationAccepted`
|
||||
- `RuntimeMutationRejected`
|
||||
- `RuntimeStateChanged`
|
||||
- `RuntimePersistenceRequested`
|
||||
- `RuntimeReloadRequested`
|
||||
- `ShaderPackagesChanged`
|
||||
- `RenderSnapshotPublishRequested`
|
||||
- `RuntimeStatePresentationChanged`
|
||||
|
||||
Primary consumers:
|
||||
|
||||
- `RuntimeSnapshotProvider`
|
||||
- `RenderEngine`
|
||||
- `ControlServices`
|
||||
- `HealthTelemetry`
|
||||
- later, `PersistenceWriter`
|
||||
|
||||
### Shader Build Events
|
||||
|
||||
Produced by shader build orchestration and render-side build application.
|
||||
|
||||
Examples:
|
||||
|
||||
- `ShaderBuildRequested`
|
||||
- `ShaderBuildPrepared`
|
||||
- `ShaderBuildApplied`
|
||||
- `ShaderBuildFailed`
|
||||
- `CompileStatusChanged`
|
||||
|
||||
Primary consumers:
|
||||
|
||||
- `RenderEngine`
|
||||
- `RuntimeCoordinator`
|
||||
- `ControlServices`
|
||||
- `HealthTelemetry`
|
||||
|
||||
### Render Events
|
||||
|
||||
Produced by `RenderEngine` and `RuntimeSnapshotProvider`.
|
||||
|
||||
Examples:
|
||||
|
||||
- `RenderSnapshotPublished`
|
||||
- `RenderResetRequested`
|
||||
- `RenderResetApplied`
|
||||
- `OscOverlayApplied`
|
||||
- `OscOverlaySettled`
|
||||
- `FrameRendered`
|
||||
- `PreviewFrameAvailable`
|
||||
|
||||
Primary consumers:
|
||||
|
||||
- `RenderEngine`
|
||||
- `ControlServices`
|
||||
- `VideoBackend`
|
||||
- `HealthTelemetry`
|
||||
|
||||
### Backend Events
|
||||
|
||||
Produced by `VideoBackend` and backend adapters.
|
||||
|
||||
Examples:
|
||||
|
||||
- `InputSignalChanged`
|
||||
- `InputFrameArrived`
|
||||
- `OutputFrameScheduled`
|
||||
- `OutputFrameCompleted`
|
||||
- `OutputLateFrameDetected`
|
||||
- `OutputDroppedFrameDetected`
|
||||
- `BackendStateChanged`
|
||||
|
||||
Primary consumers:
|
||||
|
||||
- `RenderEngine`
|
||||
- `HealthTelemetry`
|
||||
- later, backend lifecycle state machine handlers
|
||||
|
||||
### Health Events
|
||||
|
||||
Produced by all major subsystems.
|
||||
|
||||
Examples:
|
||||
|
||||
- `SubsystemWarningRaised`
|
||||
- `SubsystemWarningCleared`
|
||||
- `SubsystemRecovered`
|
||||
- `TimingSampleRecorded`
|
||||
- `QueueDepthChanged`
|
||||
|
||||
Primary consumer:
|
||||
|
||||
- `HealthTelemetry`
|
||||
|
||||
Health events should be observational. They should not be required for core behavior to proceed.
|
||||
|
||||
## Event Envelope
|
||||
|
||||
A practical initial event envelope can stay simple:
|
||||
|
||||
```cpp
|
||||
enum class RuntimeEventType
|
||||
{
|
||||
OscCommitRequested,
|
||||
RuntimeMutationAccepted,
|
||||
RuntimeMutationRejected,
|
||||
RuntimeReloadRequested,
|
||||
ShaderBuildRequested,
|
||||
ShaderBuildPrepared,
|
||||
ShaderBuildFailed,
|
||||
RenderSnapshotPublishRequested,
|
||||
RenderSnapshotPublished,
|
||||
RuntimeStateBroadcastRequested,
|
||||
BackendStateChanged,
|
||||
SubsystemWarningRaised
|
||||
};
|
||||
|
||||
struct RuntimeEvent
|
||||
{
|
||||
RuntimeEventType type;
|
||||
uint64_t sequence = 0;
|
||||
std::chrono::steady_clock::time_point createdAt;
|
||||
std::string source;
|
||||
std::variant<
|
||||
OscCommitRequestedEvent,
|
||||
RuntimeMutationEvent,
|
||||
ShaderBuildEvent,
|
||||
RenderSnapshotEvent,
|
||||
BackendEvent,
|
||||
HealthEvent> payload;
|
||||
};
|
||||
```
|
||||
|
||||
The exact C++ names can change. The key design requirements are:
|
||||
|
||||
- event type is explicit
|
||||
- event order is observable
|
||||
- source subsystem is recorded
|
||||
- payload is typed, not a bag of optional strings
|
||||
- timestamps exist for queue-age telemetry
|
||||
- failures are events too, not just debug strings
|
||||
|
||||
## Event Bus Shape
|
||||
|
||||
Phase 2 does not need a large framework. A small app-owned dispatcher is enough.
|
||||
|
||||
Suggested components:
|
||||
|
||||
- `RuntimeEventDispatcher`
|
||||
- owns queues
|
||||
- assigns sequence numbers
|
||||
- exposes `Publish(...)`
|
||||
- exposes `DispatchPending(...)`
|
||||
- event handlers
|
||||
- narrow handler interface or function callback
|
||||
- registered by subsystem/composition root
|
||||
- `RuntimeEventQueue`
|
||||
- bounded FIFO for ordinary events
|
||||
- `RuntimeEventCoalescingQueue`
|
||||
- bounded keyed latest-value queue for flows such as high-rate OSC, broadcast requests, file/reload bursts, and queue-depth telemetry
|
||||
- queue and dispatch metrics
|
||||
- queue depth
|
||||
- oldest event age
|
||||
- dropped/coalesced counts
|
||||
|
||||
Initial implementation is single-process and mostly single-dispatch-thread. The important part is that event publication and event handling are explicit.
|
||||
|
||||
### Dispatcher Ownership Decision
|
||||
|
||||
The first concrete implementation uses one app-owned `RuntimeEventDispatcher`.
|
||||
|
||||
Ownership:
|
||||
|
||||
- `OpenGLComposite` owns the dispatcher as part of the current composition root.
|
||||
|
||||
References:
|
||||
|
||||
- `RuntimeServices` receives the dispatcher and passes it to `ControlServices`.
|
||||
- `RuntimeCoordinator` receives the dispatcher so coordinator outcomes can become explicit events.
|
||||
- `RuntimeUpdateController` receives the dispatcher so it can become the first effect/apply handler.
|
||||
- `RuntimeSnapshotProvider`, `ShaderBuildQueue`, and `VideoBackend` receive the dispatcher so snapshot, shader lifecycle, and backend observation events are visible.
|
||||
|
||||
This is intentionally a composition-root dependency, not a new subsystem dependency. Subsystems should not construct their own dispatchers, and future tests should use `RuntimeEventTestHarness` rather than creating ad hoc event plumbing.
|
||||
|
||||
The dispatcher should move out of `OpenGLComposite` only if a later application-shell/composition-root object replaces `OpenGLComposite` as the owner of subsystem wiring.
|
||||
|
||||
## Queue Policy
|
||||
|
||||
Not every event deserves the same queue semantics.
|
||||
|
||||
### FIFO Events
|
||||
|
||||
Use FIFO for events where every item matters:
|
||||
|
||||
- mutation accepted/rejected
|
||||
- shader build completed/failed
|
||||
- backend state changed
|
||||
- warning raised/cleared
|
||||
|
||||
### Coalesced Events
|
||||
|
||||
Use coalescing for high-rate latest-value flows:
|
||||
|
||||
- OSC parameter target updates by route
|
||||
- runtime-state broadcast requests
|
||||
- file-change reload requests during a burst
|
||||
- queue-depth telemetry
|
||||
|
||||
Coalesced events should record how many updates were collapsed so telemetry can show pressure.
|
||||
|
||||
### Synchronous Boundaries
|
||||
|
||||
Some calls may remain synchronous during Phase 2:
|
||||
|
||||
- UI/API mutation calls that need an immediate success/error response
|
||||
- startup configuration failures
|
||||
- shutdown ordering
|
||||
- tests
|
||||
|
||||
The rule is that synchronous calls should still publish events for accepted/rejected/completed work, so the rest of the app does not need to infer side effects from the call path.
|
||||
|
||||
## Event Bridge Policy
|
||||
|
||||
This section is the implementation rulebook for converting existing direct calls and result queues into events. Future Phase 2 lanes should use this table unless they deliberately update the policy here first.
|
||||
|
||||
### Bridge Categories
|
||||
|
||||
| Bridge category | Use when | Queue shape | Handler expectation |
|
||||
| --- | --- | --- | --- |
|
||||
| `fifo-fact` | every occurrence matters and must be observed in order | bounded FIFO | handler consumes each event exactly once |
|
||||
| `coalesced-latest` | only the latest value per key matters | bounded coalescing queue | handler consumes the latest event and telemetry records collapsed count |
|
||||
| `sync-command-with-event` | caller needs an immediate success/error result | direct owner call plus follow-up event publication | handler must not be required for the caller's response |
|
||||
| `observation-only` | event is telemetry/diagnostic and must not drive core behavior | FIFO or coalesced depending on rate | handler failure must never block app behavior |
|
||||
| `compatibility-poll` | source cannot yet publish an event directly | temporary poll adapter publishes typed events | poll interval is wakeup-driven with a fallback timer until a later file-watch implementation replaces it |
|
||||
|
||||
### Current Bridge Decisions
|
||||
|
||||
| Current flow | Phase 2 bridge | Event(s) | Current status |
|
||||
| --- | --- | --- | --- |
|
||||
| OSC latest-value updates | `ControlServices` ingress bridge | `OscValueReceived`, optional `OscValueCoalesced` | Event publication exists; source-side pending map and app-level dispatcher coalescing both provide latest-value behavior. |
|
||||
| OSC commit after settle | `ControlServices -> RuntimeCoordinator` bridge | `OscCommitRequested`, then `RuntimeMutationAccepted` or `RuntimeMutationRejected` | Event publication exists. Coordinator follow-up work now reaches the app path through events rather than a service-result queue. |
|
||||
| HTTP/UI mutation needing response | direct call into `RuntimeCoordinator` | `RuntimeMutationAccepted` or `RuntimeMutationRejected` after the synchronous response path | Implemented as `sync-command-with-event`; synchronous response remains supported. |
|
||||
| runtime-state broadcast request | presentation/broadcast bridge | `RuntimeStatePresentationChanged`, `RuntimeStateBroadcastRequested` | Request event exists, is handled, and is coalesced by the app dispatcher. Completion/failure events remain follow-ups. |
|
||||
| manual reload button | control ingress bridge | `ManualReloadRequested`, then `RuntimeReloadRequested` | Ingress and follow-up events exist and are covered by tests. |
|
||||
| file watcher changes | file-watch bridge | `FileChangeDetected`, then `RuntimeReloadRequested` | Poll fallback remains, but detected changes now publish ingress and follow-up events and are covered by tests. |
|
||||
| runtime store poll fallback | compatibility poll adapter | `FileChangeDetected`, `RuntimeReloadRequested`, or warning/compile-status event | Still present by design as a transitional bridge with a condition-variable fallback timer. Detected changes publish ingress and follow-up events. |
|
||||
| shader build request | runtime/render bridge | `ShaderBuildRequested` | Event publication, handler, and app dispatcher coalescing exist. |
|
||||
| shader build ready/failure/apply | shader build lifecycle bridge | `ShaderBuildPrepared`, `ShaderBuildFailed`, `ShaderBuildApplied`, `CompileStatusChanged` | Implemented with generation matching. |
|
||||
| render snapshot publication | snapshot bridge | `RenderSnapshotPublishRequested`, `RenderSnapshotPublished` | Implemented. Publish requests are coalesced by output dimensions in the app dispatcher. |
|
||||
| render reset request/application | render bridge | `RenderResetRequested`, `RenderResetApplied` | Request handling exists; applied event coverage can be expanded in later render work. |
|
||||
| input signal changes | backend observation bridge | `InputSignalChanged` | Implemented as backend observation publication. |
|
||||
| output late/dropped/completed frames | backend timing bridge | `OutputFrameCompleted`, `OutputLateFrameDetected`, `OutputDroppedFrameDetected` | Implemented at the vocabulary and backend publication level. High-rate policy may be refined during backend lifecycle work. |
|
||||
| warnings and recovery | telemetry bridge | `SubsystemWarningRaised`, `SubsystemWarningCleared`, `SubsystemRecovered` | Vocabulary exists; direct telemetry writes still coexist with event observations. |
|
||||
| queue depth/timing samples | telemetry metrics bridge | `QueueDepthChanged`, `TimingSampleRecorded` | Implemented for dispatcher/backend observations and coalesced by metric key in the app dispatcher. |
|
||||
|
||||
### Bridge Rules
|
||||
|
||||
- A bridge may translate an old direct call into an owner command, but it must publish the accepted/rejected/completed event that describes the outcome.
|
||||
- A bridge must not mutate state owned by another subsystem just because it handles that subsystem's event.
|
||||
- A coalesced event must have a stable key in code and a documented policy here.
|
||||
- A FIFO event should be cheap enough that retaining every occurrence is useful. If not, turn it into a coalesced metric before putting it on a hot path.
|
||||
- A synchronous bridge must treat event publication as a side effect of the owner decision, not as the mechanism that produces the direct caller's response.
|
||||
- A compatibility poll adapter should be named as temporary in code so it does not become the new long-term coordination model.
|
||||
- Handler failure should be reported through telemetry and dispatch metrics. It should not throw back across subsystem boundaries.
|
||||
|
||||
### First Integration Recommendation
|
||||
|
||||
The safest first behavior-changing bridge is `RuntimeStateBroadcastRequested`.
|
||||
|
||||
It is low risk because:
|
||||
|
||||
- it is already a side effect of many coordinator outcomes
|
||||
- duplicate requests are naturally coalescable
|
||||
- the handler can call the existing `ControlServices::BroadcastState()` path
|
||||
- success can be verified through existing UI behavior and event tests
|
||||
|
||||
After that, the next bridge should be `ShaderBuildRequested`, because it already behaves like a queued side effect and has clear follow-up events.
|
||||
|
||||
## Target Flow Examples
|
||||
|
||||
### OSC Parameter Update
|
||||
|
||||
1. `OscServer` decodes a packet.
|
||||
2. `ControlServices` publishes or coalesces `OscValueReceived`.
|
||||
3. The dispatcher routes the event to the render-overlay path or coordinator policy, depending on whether the value is transient or committing.
|
||||
4. `RuntimeCoordinator` publishes `RuntimeMutationAccepted` or `RuntimeMutationRejected` for committed changes.
|
||||
5. Accepted committed changes publish `RenderSnapshotPublishRequested` and `RuntimePersistenceRequested` as needed.
|
||||
6. `ControlServices` receives `RuntimeStateBroadcastRequested` or a presentation-changed event and broadcasts at its own cadence.
|
||||
|
||||
### File Reload
|
||||
|
||||
1. File-watch or manual reload produces `FileChangeDetected` or `ManualReloadRequested`.
|
||||
2. `ControlServices` coalesces reload bursts into one `RuntimeReloadRequested`.
|
||||
3. `RuntimeCoordinator` classifies the reload.
|
||||
4. Package/store refresh produces `ShaderPackagesChanged` if package metadata changed.
|
||||
5. Coordinator publishes `ShaderBuildRequested`.
|
||||
6. Shader build completion publishes `ShaderBuildPrepared` or `ShaderBuildFailed`.
|
||||
7. Render applies the ready build and publishes `ShaderBuildApplied`.
|
||||
|
||||
### Runtime State Broadcast
|
||||
|
||||
1. A mutation or reload publishes `RuntimeStatePresentationChanged`.
|
||||
2. `ControlServices` coalesces this into a broadcast request.
|
||||
3. The broadcast path asks `RuntimeStatePresenter` for the current presentation read model.
|
||||
4. `HealthTelemetry` records broadcast count, failures, and queue age.
|
||||
|
||||
### Backend Signal Change
|
||||
|
||||
1. Backend adapter detects input signal change.
|
||||
2. `VideoBackend` publishes `InputSignalChanged`.
|
||||
3. `HealthTelemetry` records the new signal status.
|
||||
4. Later phases may let the backend lifecycle state machine react to the same event.
|
||||
|
||||
## Migration Plan
|
||||
|
||||
### Step 1. Add Event Types And A Minimal Dispatcher
|
||||
|
||||
Status: complete.
|
||||
|
||||
Introduce:
|
||||
|
||||
- `RuntimeEvent`
|
||||
- `RuntimeEventType`
|
||||
- typed payload structs for the smallest useful event family
|
||||
- `RuntimeEventBus` or equivalent dispatcher
|
||||
|
||||
Start with events that do not change behavior:
|
||||
|
||||
- `RuntimeStateBroadcastRequested`
|
||||
- `ShaderBuildRequested`
|
||||
- `RuntimeMutationRejected`
|
||||
- simple health/log observations
|
||||
|
||||
### Step 2. Convert `RuntimeUpdateController` Into An Event Handler
|
||||
|
||||
Status: complete for the Phase 2 target, with synchronous API helpers retained.
|
||||
|
||||
`RuntimeUpdateController` is already close to an event effect applier. Phase 2 should narrow it into a handler for:
|
||||
|
||||
- coordinator outcome events
|
||||
- shader build readiness events
|
||||
- snapshot publication requests
|
||||
- broadcast requests
|
||||
|
||||
The class should stop being the place that polls every source of work.
|
||||
|
||||
Current note: `RuntimeUpdateController` now subscribes to the dispatcher and handles broadcast, reload, shader build, compile status, render reset, and health observation paths. It still accepts synchronous `RuntimeCoordinatorResult` values for UI/API calls that need immediate success or error responses.
|
||||
|
||||
### Step 3. Replace `ControlServices::PollLoop` Sleep With Wakeups
|
||||
|
||||
Status: complete for OSC commit wakeups; runtime-store compatibility polling remains explicitly transitional.
|
||||
|
||||
Keep coalescing, but replace the fixed `25 x Sleep(10)` cadence with:
|
||||
|
||||
- a condition variable or waitable event
|
||||
- wakeups when OSC commit work arrives
|
||||
- wakeups when file/reload work arrives
|
||||
- a fallback timer only for compatibility polling that cannot yet be evented
|
||||
|
||||
This is the most direct Phase 2 timing win.
|
||||
|
||||
Current note: `ControlServices` now uses a condition variable and fallback timer. The fallback exists for runtime-store polling until a later file-watch implementation can replace scanning as the change source. Detected reload/file changes publish typed ingress and follow-up events.
|
||||
|
||||
### Step 4. Route Shader Build Lifecycle Through Events
|
||||
|
||||
Status: mostly complete.
|
||||
|
||||
Turn the current request/apply/failure/success path into explicit events:
|
||||
|
||||
- `ShaderBuildRequested`
|
||||
- `ShaderBuildPrepared`
|
||||
- `ShaderBuildFailed`
|
||||
- `ShaderBuildApplied`
|
||||
- `CompileStatusChanged`
|
||||
|
||||
This should preserve the current off-frame-path compile behavior while making readiness visible.
|
||||
|
||||
Current note: request, prepared, failed, applied, and compile-status events exist. Generation-aware consumption is covered by tests. Request events are coalesced by build dimensions and preserve-feedback policy in the app dispatcher.
|
||||
|
||||
### Step 5. Route Runtime Broadcasts Through Events
|
||||
|
||||
Status: partially complete.
|
||||
|
||||
Replace direct "broadcast now" decisions with:
|
||||
|
||||
- `RuntimeStatePresentationChanged`
|
||||
- `RuntimeStateBroadcastRequested`
|
||||
- `RuntimeStateBroadcastCompleted`
|
||||
- `RuntimeStateBroadcastFailed`
|
||||
|
||||
This keeps UI delivery in `ControlServices` while keeping presentation ownership in the runtime presentation layer.
|
||||
|
||||
Current note: `RuntimeStateBroadcastRequested` exists, is coalesced by the app dispatcher, and is handled. Broadcast completion/failure events have not been added yet.
|
||||
|
||||
### Step 6. Add Event Metrics
|
||||
|
||||
Status: mostly complete for dispatcher metrics; broader health-event observation continues.
|
||||
|
||||
Before using the event system for hotter paths, add metrics:
|
||||
|
||||
- event queue depth
|
||||
- oldest event age
|
||||
- event dispatch duration
|
||||
- coalesced event count
|
||||
- dropped event count
|
||||
- handler failure count
|
||||
|
||||
These should feed `HealthTelemetry`.
|
||||
|
||||
Current note: queue depth, oldest-event age, dispatch duration, dropped count, coalesced count, and handler failure counts feed telemetry. Queue/timing events are also published and coalesced by metric key.
|
||||
|
||||
## Dependency Rules
|
||||
|
||||
Allowed:
|
||||
|
||||
- producers publish events to the bus
|
||||
- the composition root registers handlers
|
||||
- handlers call owner subsystem APIs
|
||||
- `HealthTelemetry` observes event metrics and failures
|
||||
|
||||
Avoid:
|
||||
|
||||
- subsystems subscribing directly to each other in constructors
|
||||
- event handlers mutating state outside their owner subsystem
|
||||
- using one global event payload with many nullable fields
|
||||
- making render hot paths block on the event bus
|
||||
- requiring health/telemetry event delivery for core behavior
|
||||
|
||||
The dispatcher is coordination infrastructure, not a new domain owner.
|
||||
|
||||
## Testing Strategy
|
||||
|
||||
Phase 2 should add tests that do not require GL, DeckLink, or network sockets.
|
||||
|
||||
Implemented tests:
|
||||
|
||||
- FIFO events dispatch in sequence order
|
||||
- coalesced events keep the latest payload and count collapsed updates
|
||||
- rejected mutations publish rejection events without downstream snapshot/build events
|
||||
- accepted parameter mutations publish the expected follow-up event set
|
||||
- handler failures are reported as health/log events
|
||||
- queue depth and oldest-event-age metrics update predictably
|
||||
- typed payload mapping covers persistence, render snapshot, backend, timing, queue-depth, and late/dropped output-frame events
|
||||
- shader build generation matching applies only the expected prepared build
|
||||
|
||||
Remaining useful tests before deeper file-watch work:
|
||||
|
||||
- file reload bursts collapse into one reload request across a real poll burst
|
||||
- broadcast completion/failure events are observable once those payloads exist
|
||||
|
||||
The existing `RuntimeEventTypeTests` target is now the main pure event behavior harness. `RuntimeEventTestHarness` should remain the shared test helper so future lanes do not invent their own dispatcher plumbing.
|
||||
|
||||
## Phase 2 Exit Criteria
|
||||
|
||||
Phase 2 can be considered complete once the project can say:
|
||||
|
||||
- [x] there is a typed internal event envelope and dispatcher
|
||||
- [x] `OpenGLComposite` owns the dispatcher as the current composition root
|
||||
- [x] `ControlServices` emits typed events for OSC commits and broadcast requests
|
||||
- [x] reload/file-change work publishes typed ingress and follow-up events
|
||||
- [x] `RuntimeCoordinator` publishes explicit accepted/rejected/follow-up events
|
||||
- [x] callers no longer need broad compatibility result queues for normal runtime side effects
|
||||
- [x] `RuntimeUpdateController` handles event-driven broadcast, shader build, compile status, render reset, and health observation paths
|
||||
- [x] `RuntimeUpdateController` no longer needs compatibility result draining for ordinary service work
|
||||
- [x] shader build request/readiness/failure/application is represented as events
|
||||
- [x] shader build requests are coalesced by dimensions and preserve-feedback policy in the app path
|
||||
- [x] render snapshot publication is represented as request/published events
|
||||
- [x] render snapshot publish requests are coalesced in the app path where needed
|
||||
- [x] backend observations publish typed events
|
||||
- [x] event queues expose depth, age, dropped, coalescing, and failure metrics
|
||||
- [x] production event paths use coalescing for broadcast requests, shader-build requests, and high-rate metrics
|
||||
- [x] coarse sleep polling is no longer the default coordination model for OSC commit service work
|
||||
- [x] runtime-store/file-change compatibility polling is explicitly contained and publishes event-first reload bridge events when changes are detected
|
||||
|
||||
Phase 2 closure note:
|
||||
|
||||
- The checklist above is complete for the internal event model substrate.
|
||||
- Broadcast completion/failure events and real file-watch burst tests are useful follow-ups, but they are no longer foundation blockers.
|
||||
- `RuntimeCoordinatorResult` may remain as a synchronous return type for command APIs; the Phase 2 requirement is that accepted/rejected/follow-up behavior is also published as typed events, which is now true.
|
||||
|
||||
## Open Questions For Implementation
|
||||
|
||||
- Resolved: the first dispatcher is single-process, app-owned, and pumped through the current app/update path.
|
||||
- Resolved: event payloads use typed structs carried by `std::variant`.
|
||||
- Resolved: persistence requests are represented in Phase 2 even though background persistence lands later.
|
||||
- Resolved: backend callback events are introduced now as observation-only events.
|
||||
- Still open: should high-rate OSC transient overlay events enter the app dispatcher, or should they remain source-local until the live-state layering phase?
|
||||
- Resolved for Phase 2: `RuntimeCoordinatorResult` can survive as a synchronous helper for command APIs, as long as event publication remains the coordination path for downstream effects.
|
||||
- Resolved: app-level coalescing lives inside `RuntimeEventDispatcher`; source-specific bridges can still coalesce before publication when they own useful domain-specific collapse policy.
|
||||
|
||||
## Short Version
|
||||
|
||||
Phase 2 should give the app a typed nervous system.
|
||||
|
||||
- external inputs become typed events
|
||||
- owner subsystems still make decisions
|
||||
- decisions publish explicit outcomes
|
||||
- follow-up work is routed by handlers, not inferred from scattered call paths
|
||||
- high-rate work is bounded or coalesced
|
||||
- timing and queue pressure become observable
|
||||
|
||||
If this boundary holds, later render-thread, persistence, backend, and telemetry work can move independently without returning to shared-object polling as the default coordination model.
|
||||
383
docs/PHASE_3_LIVE_STATE_SERVICE_COORDINATION_DESIGN.md
Normal file
383
docs/PHASE_3_LIVE_STATE_SERVICE_COORDINATION_DESIGN.md
Normal file
@@ -0,0 +1,383 @@
|
||||
# Phase 3 Design: Live State And Service Coordination
|
||||
|
||||
This document expands Phase 3 of [ARCHITECTURE_RESILIENCE_REVIEW.md](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/docs/ARCHITECTURE_RESILIENCE_REVIEW.md) into a concrete design target.
|
||||
|
||||
Phase 1 split runtime responsibilities into named subsystems. Phase 2 added the typed internal event model those subsystems can coordinate through. Phase 3 should now finish the service-facing and live-state cleanup needed before the app attempts sole-owner GL rendering.
|
||||
|
||||
## Status
|
||||
|
||||
- Phase 3 design package: accepted.
|
||||
- Phase 3 implementation: exit criteria satisfied for the current architecture.
|
||||
- Current alignment: the repo now has the live-state/composer building blocks, a service bridge, and a named frame-state handoff. `OpenGLComposite::renderEffect()` still remains the app-level frame entrypoint, but the service drain, layer-state resolution, and OSC commit handoff now sit behind named helpers and frame-state data.
|
||||
|
||||
Current footholds:
|
||||
|
||||
- `RuntimeStore` is split into durable state collaborators: `RuntimeConfigStore`, `LayerStackStore`, `ShaderPackageCatalog`, `RenderSnapshotBuilder`, presentation read models, and `HealthTelemetry`.
|
||||
- `RuntimeCoordinator` owns mutation validation/classification and publishes accepted/rejected/follow-up events.
|
||||
- `RuntimeSnapshotProvider` publishes render snapshots from `RenderSnapshotBuilder`.
|
||||
- `RuntimeLiveState` owns transient OSC overlay bookkeeping and commit-settlement policy.
|
||||
- `RenderStateComposer` exists as the first pure composition boundary for combining base layer state with live overlays.
|
||||
- `RenderFrameInput` / `RenderFrameState` now provide a named frame-facing handoff model for preparing layer state and render inputs before drawing.
|
||||
- `RenderFrameStateResolver` now owns snapshot cache selection, parameter refresh decisions, and final frame-state resolution before drawing.
|
||||
- `RenderEngine` owns GL/render resources and delegates frame-state preparation to the resolver.
|
||||
- `ControlServices` owns OSC ingress, pending OSC updates, completed OSC commit notifications, and service start/stop.
|
||||
- `RuntimeServiceLiveBridge` translates service OSC queues into render live-state updates and queues settled overlay commit requests.
|
||||
- `RuntimeEventDispatcher` now routes accepted mutations, reloads, snapshots, shader build events, backend observations, and health observations.
|
||||
|
||||
The current architecture is much better than the original `RuntimeHost` shape. Phase 4 has since moved normal runtime GL work onto the `RenderEngine` render thread, so the remaining render-facing risk is no longer shared context ownership; it is the later producer/consumer playout work needed to keep DeckLink callbacks from synchronously waiting on output production.
|
||||
|
||||
## Why Phase 3 Exists
|
||||
|
||||
The resilience review says render-thread isolation should come after state access and control coordination are no longer centered on a large mutable runtime object. Phase 2 gives us the event substrate; Phase 3 should make the data flowing into render explicit enough that Phase 4 can make the render thread the sole GL owner without dragging service coordination and state reconciliation with it.
|
||||
|
||||
The main problems Phase 3 addressed:
|
||||
|
||||
- transient OSC overlay state and persisted committed state needed a named reconciliation boundary
|
||||
- `RenderEngine` needed to move final frame-state selection and value composition out of drawing code
|
||||
- service-side queues for pending OSC updates and completed OSC commits needed a bridge outside `OpenGLComposite`
|
||||
- `RuntimeStore` still performs synchronous persistence directly from many state mutation paths
|
||||
- `RuntimeUpdateController` still exists partly as compatibility glue between synchronous coordinator results and event-driven effects
|
||||
|
||||
## Goals
|
||||
|
||||
Phase 3 should establish:
|
||||
|
||||
- an explicit live-state model separating persisted state, committed runtime state, and transient automation overlay
|
||||
- service-facing event bridges for OSC overlay updates and overlay commit completions
|
||||
- a narrower `OpenGLComposite::renderEffect()` that renders a prepared read model instead of orchestrating runtime/service state
|
||||
- a clear owner for final render-layer state resolution before it reaches GL drawing
|
||||
- a contained persistence request model that prepares for the later background writer phase
|
||||
- tests for live-state composition, overlay settlement, and service-to-runtime event behavior without GL or DeckLink
|
||||
|
||||
## Non-Goals
|
||||
|
||||
Phase 3 should not require:
|
||||
|
||||
- a dedicated render thread
|
||||
- moving all GL calls off the current callback path
|
||||
- a background persistence writer implementation
|
||||
- a final DeckLink lifecycle state machine
|
||||
- replacing every direct synchronous command API
|
||||
- a final cue/preset/timeline system
|
||||
|
||||
Those are later phases. Phase 3 is about making state and service coordination clean enough for those later phases.
|
||||
|
||||
## Current Coordination Shape
|
||||
|
||||
`OpenGLComposite::renderEffect()` is now the app-level frame entrypoint, but it is intentionally narrow:
|
||||
|
||||
1. pumps `RuntimeUpdateController::ProcessRuntimeWork()`
|
||||
2. builds a `RenderFrameInput`
|
||||
3. renders the frame through `RuntimeServiceLiveBridge`, `RenderFrameStateResolver`, and `RenderEngine`
|
||||
|
||||
The bridge now owns service queue draining, live automation settlement, committed/live state selection, and OSC commit handoff. `RenderFrameStateResolver` owns snapshot cache selection, parameter refresh decisions, and dynamic render-field refresh before handing a prepared frame state to `RenderEngine`.
|
||||
|
||||
## Target State Model
|
||||
|
||||
Phase 3 should formalize three state categories:
|
||||
|
||||
| State category | Owner | Lifetime | Render role |
|
||||
| --- | --- | --- | --- |
|
||||
| Persisted layer state | `LayerStackStore` behind `RuntimeStore` | saved durable state | base layer stack and saved parameter values |
|
||||
| Committed runtime state | `RuntimeCoordinator` / snapshot publication | accepted operator/UI/OSC commits | stable render snapshot selected for rendering |
|
||||
| Transient automation overlay | new live-state collaborator or narrowed render-side owner | high-rate OSC automation between commits | temporary per-route override blended into final values |
|
||||
|
||||
Render should eventually consume:
|
||||
|
||||
```text
|
||||
final render state = published snapshot + committed live selection + transient overlay
|
||||
```
|
||||
|
||||
The important change is not the exact formula name. The important change is that final render-state composition has one named owner and can be tested without GL.
|
||||
|
||||
## Phase 3 Collaborators
|
||||
|
||||
### `RuntimeLiveState`
|
||||
|
||||
Small runtime collaborator for transient automation state.
|
||||
|
||||
Responsibilities:
|
||||
|
||||
- keep transient OSC overlay values keyed by route
|
||||
- track overlay generation and pending commit generation
|
||||
- apply overlay commit completions
|
||||
- decide when an overlay value has settled enough to request a commit
|
||||
- build a `LiveStateOverlaySnapshot` for final render-state composition
|
||||
|
||||
Non-responsibilities:
|
||||
|
||||
- persistent state mutation
|
||||
- shader package lookup
|
||||
- GL resources
|
||||
- OSC socket ownership
|
||||
|
||||
### `RenderStateComposer`
|
||||
|
||||
Pure or mostly pure collaborator for frame value composition.
|
||||
|
||||
Responsibilities:
|
||||
|
||||
- combine published render snapshots with live overlay state
|
||||
- apply smoothing/time-based automation policy
|
||||
- return final `RuntimeRenderState` values plus any commit requests
|
||||
- stay testable without OpenGL
|
||||
|
||||
Non-responsibilities:
|
||||
|
||||
- drawing
|
||||
- service queue draining
|
||||
- disk persistence
|
||||
- OSC packet parsing
|
||||
|
||||
### `RuntimeServiceLiveBridge`
|
||||
|
||||
`RuntimeServiceLiveBridge` is the current source-local bridge between services, live state, and render-state preparation.
|
||||
|
||||
Responsibilities:
|
||||
|
||||
- translate service-side OSC ingress into typed events or live-state commands
|
||||
- publish overlay applied/settled events where useful
|
||||
- route overlay commit requests to `RuntimeCoordinator`
|
||||
- keep `OpenGLComposite` out of service queue draining
|
||||
|
||||
Non-responsibilities:
|
||||
|
||||
- final GL rendering
|
||||
- persistent store mutation outside coordinator APIs
|
||||
|
||||
## Event Bridge Targets
|
||||
|
||||
| Current flow | Phase 3 bridge target | Notes |
|
||||
| --- | --- | --- |
|
||||
| pending OSC updates drained by `OpenGLComposite` | `OscValueReceived` -> live-state overlay update handler | Phase 2 already has the event type; Phase 3 decides whether transient overlay updates enter the app dispatcher or a source-local bridge. |
|
||||
| render asks for overlay commit requests | `OscOverlaySettled` or direct coordinator command plus event publication | Commit request creation should leave `renderEffect()` and live near the live-state owner. |
|
||||
| completed OSC commits drained by `OpenGLComposite` | `RuntimeMutationAccepted` / completion event -> live-state commit completion | Completed commit routing should be event-driven or owned by live-state service bridge. |
|
||||
| `RenderFrameStateResolver::Resolve(...)` | `RenderStateComposer::BuildFrameState(...)` | Keep final state composition testable without GL. |
|
||||
| direct persistence writes from store mutations | `RuntimePersistenceRequested` as the durable write trigger | Background writer lands later; Phase 3 should make request boundaries clear. |
|
||||
| runtime-state broadcast side effects | `RuntimeStateBroadcastRequested` plus optional completed/failed observations | Keep broadcast delivery in services and presentation ownership in runtime presentation. |
|
||||
|
||||
## Runtime Store Scope In Phase 3
|
||||
|
||||
`RuntimeStore` is already much smaller than the original host, but Phase 3 should keep narrowing it toward durable state and read-model publishing.
|
||||
|
||||
Target responsibilities:
|
||||
|
||||
- initialize runtime config and persistent state
|
||||
- expose durable layer/package/config read models
|
||||
- own saved layer stack and preset serialization until the background writer phase
|
||||
- publish or support immutable render/presentation snapshots
|
||||
|
||||
Avoid adding:
|
||||
|
||||
- transient OSC overlay state
|
||||
- frame-local render composition decisions
|
||||
- service queue coordination
|
||||
- background worker policy
|
||||
|
||||
## Runtime Coordinator Scope In Phase 3
|
||||
|
||||
`RuntimeCoordinator` should remain the command/mutation policy owner.
|
||||
|
||||
Keep:
|
||||
|
||||
- validation/classification
|
||||
- accepted/rejected mutation publication
|
||||
- reload/build/persistence follow-up events
|
||||
- synchronous command results for UI/API callers that need immediate success or error
|
||||
|
||||
Narrow:
|
||||
|
||||
- any behavior that looks like render-frame state composition
|
||||
- any direct service queue interpretation
|
||||
- any persistence timing policy beyond publishing `RuntimePersistenceRequested`
|
||||
|
||||
## Render Engine Scope In Phase 3
|
||||
|
||||
`RenderEngine` should move closer to being a GL/render-local owner.
|
||||
|
||||
Keep:
|
||||
|
||||
- GL resources
|
||||
- shader programs
|
||||
- render passes
|
||||
- preview/output rendering
|
||||
- temporal history and feedback resources
|
||||
|
||||
Move or narrow:
|
||||
|
||||
- transient OSC overlay bookkeeping
|
||||
- final layer-state composition from snapshot plus overlay
|
||||
- creation of commit requests from smoothed overlay values
|
||||
|
||||
Some transient render-only state may remain in `RenderEngine` if it truly belongs to GL or temporal resources. But value composition should be separable from drawing.
|
||||
|
||||
## OpenGLComposite Scope In Phase 3
|
||||
|
||||
`OpenGLComposite` should remain the current composition root, but not the runtime-service coordinator.
|
||||
|
||||
Target:
|
||||
|
||||
- wire collaborators
|
||||
- own app-level lifecycle
|
||||
- initialize GL/backend/runtime services
|
||||
- call narrow render/update entrypoints
|
||||
|
||||
Avoid:
|
||||
|
||||
- draining OSC queues directly
|
||||
- converting service DTOs into render DTOs
|
||||
- deciding final layer-state composition
|
||||
- coordinating commit completion settlement
|
||||
|
||||
## Persistence Position
|
||||
|
||||
Phase 3 should not implement the background writer, but it should prepare for it.
|
||||
|
||||
Target behavior by Phase 3 exit:
|
||||
|
||||
- state mutations publish `RuntimePersistenceRequested`
|
||||
- persistence can be observed and tested as an event side effect
|
||||
- synchronous `SavePersistentState()` remains allowed as an implementation detail inside `RuntimeStore`
|
||||
- callers outside the store/coordinator should not infer disk writes from mutation categories
|
||||
|
||||
This keeps Phase 6 smaller: the background snapshot writer can subscribe to persistence requests and consume a stored-state snapshot rather than rediscovering mutation policy.
|
||||
|
||||
## Migration Plan
|
||||
|
||||
### Step 1. Name The Live State Boundary
|
||||
|
||||
Introduce `RuntimeLiveState`, `RenderStateComposer`, or an equivalent pair of classes.
|
||||
|
||||
Start by moving pure data operations out of frame rendering without changing behavior.
|
||||
|
||||
Status: complete for Phase 3. `runtime/live/RuntimeLiveState` and `runtime/live/RenderStateComposer` exist, are included in the build, and have a focused `RuntimeLiveStateTests` target.
|
||||
|
||||
### Step 2. Move OSC Overlay Bookkeeping Behind The Boundary
|
||||
|
||||
Move these responsibilities out of the current frame orchestration:
|
||||
|
||||
- overlay updates by route
|
||||
- commit completion tracking
|
||||
- generation matching
|
||||
- settle/commit request creation
|
||||
|
||||
The first implementation can still be called synchronously from the current render path. The important part is that the behavior has a named owner and tests.
|
||||
|
||||
Status: complete for Phase 3. `RenderEngine` still exposes compatibility methods used by the service bridge, but it delegates overlay updates, commit completions, smoothing, generation matching, and commit-request creation to `RuntimeLiveState`/`RenderStateComposer`.
|
||||
|
||||
### Step 3. Bridge Service Queues To Events Or Live-State Commands
|
||||
|
||||
Replace `OpenGLComposite::renderEffect()` queue draining with a bridge that publishes or applies:
|
||||
|
||||
- `OscValueReceived`
|
||||
- `OscOverlayApplied`
|
||||
- `OscOverlaySettled`
|
||||
- overlay commit completion observations
|
||||
|
||||
This is where the remaining Phase 2 open question about transient OSC overlay event scope should be resolved for the current architecture.
|
||||
|
||||
Status: complete for Phase 3. `RuntimeServiceLiveBridge` now drains pending OSC updates and completed OSC commits, applies them to render live state, and queues settled commit requests. It remains a source-local bridge by design until later live-state layering decides whether transient automation should enter the app-level dispatcher.
|
||||
|
||||
### Step 4. Narrow `OpenGLComposite::renderEffect()`
|
||||
|
||||
Target shape:
|
||||
|
||||
```cpp
|
||||
void OpenGLComposite::renderEffect()
|
||||
{
|
||||
mRuntimeUpdateController->ProcessRuntimeWork();
|
||||
const RenderFrameInput frameInput = BuildRenderFrameInput();
|
||||
RenderFrame(frameInput);
|
||||
}
|
||||
```
|
||||
|
||||
The exact names can change. The goal is that render effect no longer manually drains services, settles overlay commits, and resolves layer values.
|
||||
|
||||
Status: complete for Phase 3. `OpenGLComposite::renderEffect()` now processes runtime work, builds `RenderFrameInput`, and calls a narrow frame-render helper. Service draining, state resolution, and commit handoff sit behind `RuntimeServiceLiveBridge::PrepareLiveRenderFrameState(...)`, `RenderFrameStateResolver`, and `RenderFrameState`.
|
||||
|
||||
### Step 5. Add Persistence Boundary Tests
|
||||
|
||||
Add behavior tests for:
|
||||
|
||||
- accepted persisted mutations publish `RuntimePersistenceRequested`
|
||||
- transient OSC commits do not force immediate persistence
|
||||
- preset load/save persistence requests remain explicit
|
||||
- rejected mutations do not publish persistence work
|
||||
|
||||
Status: complete for Phase 3. `RuntimeSubsystemTests` and `RuntimeEventTypeTests` cover accepted mutation persistence requests, rejected mutations, and transient OSC overlay behavior that does not request persistence.
|
||||
|
||||
### Step 6. Update Docs And Phase 4 Readiness
|
||||
|
||||
Before calling Phase 3 complete, update:
|
||||
|
||||
- subsystem docs for new live-state/composer collaborators
|
||||
- architecture review checklist
|
||||
- Phase 4 assumptions about render thread input state
|
||||
|
||||
Status: complete. The Phase 4 design note started from the `RenderFrameInput` / `RenderFrameState` contract and has now completed the shared-GL ownership migration.
|
||||
|
||||
## Testing Strategy
|
||||
|
||||
Phase 3 tests should avoid GL, DeckLink, and sockets.
|
||||
|
||||
Recommended tests:
|
||||
|
||||
- final layer-state composition applies snapshot values when no overlay exists
|
||||
- transient overlay overrides the matching parameter by route
|
||||
- smoothing moves toward target values over time
|
||||
- overlay settle creates one commit request per route/generation
|
||||
- completed commits clear pending overlay commit state
|
||||
- stale commit completions are ignored by generation
|
||||
- accepted mutations publish persistence requests where expected
|
||||
- rejected mutations do not publish persistence or render follow-ups
|
||||
- `OpenGLComposite` no longer needs to drain service result queues for runtime effects
|
||||
|
||||
Existing useful homes:
|
||||
|
||||
- `RuntimeSubsystemTests` for pure state/composer behavior
|
||||
- `RuntimeEventTypeTests` for event bridge behavior
|
||||
- `RuntimeLiveStateTests` for the new live-state/composer boundary
|
||||
|
||||
## Parallel Work Lanes
|
||||
|
||||
The current groundwork is intended to let these lanes proceed in parallel with low overlap:
|
||||
|
||||
| Lane | Primary files | Goal |
|
||||
| --- | --- | --- |
|
||||
| A. Live-state behavior | `runtime/live/RuntimeLiveState.*`, `tests/RuntimeLiveStateTests.cpp` | Implemented for Phase 3: stale completion, smoothing, trigger behavior, and overlay settle policy are covered by focused tests. |
|
||||
| B. Render-state composition | `runtime/live/RenderStateComposer.*`, `gl/RenderFrameStateResolver.*`, `gl/RenderEngine.*` | Implemented for Phase 3: value composition and frame-state selection sit outside GL drawing while GL calls remain in `RenderEngine`. |
|
||||
| C. Service bridge | `control/RuntimeServices.*`, `control/RuntimeServiceLiveBridge.*`, `control/ControlServices.*` | Implemented for Phase 3: `OpenGLComposite::renderEffect()` no longer drains OSC update/completion queues directly. |
|
||||
| D. App-frame orchestration | `gl/OpenGLComposite.*`, `gl/RuntimeUpdateController.*` | Implemented for Phase 3: render-effect glue is a narrow runtime-work, frame-input, render-frame sequence. |
|
||||
| E. Persistence boundary | `runtime/coordination/RuntimeCoordinator.*`, `runtime/store/*`, event tests | Implemented for Phase 3: persistence request publication is explicit and ready for a later background writer. |
|
||||
|
||||
## Phase 3 Exit Criteria
|
||||
|
||||
Phase 3 can be considered complete once the project can say:
|
||||
|
||||
- [x] final render-state composition has named owners outside `OpenGLComposite` (`RenderStateComposer` covers live value composition; `RenderFrameStateResolver` covers snapshot/cache selection and frame-state resolution)
|
||||
- [x] transient OSC overlay state has a named owner and tests
|
||||
- [x] overlay commit requests and completions no longer require `OpenGLComposite` to drain service queues directly
|
||||
- [x] `RenderEngine` is closer to GL/render resource ownership and less responsible for value composition
|
||||
- [x] `RuntimeStore` remains durable-state focused and does not gain live overlay responsibilities
|
||||
- [x] persistence requests are explicit event outcomes for persisted mutations
|
||||
- [x] Phase 4 can define a render-thread input contract around immutable or near-immutable frame state
|
||||
|
||||
## Open Questions
|
||||
|
||||
- Should transient OSC overlay values enter the app-level event dispatcher, or should they use a dedicated source-local latest-value bridge until live-state layering is finalized?
|
||||
- Should the new live-state owner live under `runtime/`, `gl/`, or a new `renderstate/` boundary?
|
||||
- Should smoothing policy be owned by live state, render-state composition, or render settings?
|
||||
- Should overlay commit completion be represented as a new typed event, or derived from existing accepted mutation events with route/generation metadata?
|
||||
- How much of persistence should remain synchronous until Phase 6?
|
||||
|
||||
## Short Version
|
||||
|
||||
Phase 3 should make the app's live state boring and explicit.
|
||||
|
||||
- persisted state stays in the store
|
||||
- accepted command policy stays in the coordinator
|
||||
- transient automation gets a named owner
|
||||
- final render-state composition becomes testable without GL
|
||||
- `OpenGLComposite` stops manually reconciling service queues and layer values
|
||||
|
||||
Once that is true, Phase 4 can make the render thread the sole GL owner without also having to invent a clean state model at the same time.
|
||||
408
docs/PHASE_4_RENDER_THREAD_OWNERSHIP_DESIGN.md
Normal file
408
docs/PHASE_4_RENDER_THREAD_OWNERSHIP_DESIGN.md
Normal file
@@ -0,0 +1,408 @@
|
||||
# Phase 4 Design: Render Thread Ownership
|
||||
|
||||
This document expands Phase 4 of [ARCHITECTURE_RESILIENCE_REVIEW.md](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/docs/ARCHITECTURE_RESILIENCE_REVIEW.md) into a concrete design target.
|
||||
|
||||
Phase 1 named the subsystems. Phase 2 added the typed event substrate. Phase 3 made render-facing live state explicit through `RuntimeLiveState`, `RenderStateComposer`, `RenderFrameInput`, `RenderFrameState`, `RenderFrameStateResolver`, and `RuntimeServiceLiveBridge`. Phase 4 can now focus on the core timing-risk boundary: making one render thread the only owner of OpenGL work.
|
||||
|
||||
## Status
|
||||
|
||||
- Phase 4 design package: implemented.
|
||||
- Phase 4 implementation: complete for GL ownership. `RenderEngine` starts a dedicated render thread, owns the GL context during normal runtime work, and exposes queue/request entrypoints for input upload, output render, preview presentation, screenshot capture, shader rebuild application, and render-local resets.
|
||||
- Current alignment: normal runtime GL work is routed through the render thread after startup. Startup initialization still runs before the render thread starts while the app explicitly owns the context, and shutdown now stops DeckLink/backend work before destroying render-thread GL resources and deleting the context.
|
||||
|
||||
Current GL ownership footholds:
|
||||
|
||||
- `RenderEngine` owns GL resources, a dedicated render thread, synchronous request/response for output frames, a small render command mailbox, named render-thread helper methods, and wrong-thread diagnostics for those helpers.
|
||||
- `RenderFrameInput` / `RenderFrameState` provide the frame-state contract that a render thread can consume.
|
||||
- `RenderFrameStateResolver` prepares the render-facing layer state before drawing.
|
||||
- `OpenGLVideoIOBridge` calls `RenderEngine::QueueInputFrame(...)` from the input path and `RenderEngine::RequestOutputFrame(...)` from the output path.
|
||||
- `OpenGLComposite::paintGL(...)`, screenshot capture, input upload, and output rendering enter render work through explicit `RenderEngine` requests. After `OpenGLComposite::Start()` starts the render thread, those requests do not bind the GL context on the caller thread.
|
||||
|
||||
## Why Phase 4 Exists
|
||||
|
||||
The resilience review identifies shared GL ownership as the main remaining timing and failure-isolation risk. Today the shared context lock protects correctness, but it does not isolate timing:
|
||||
|
||||
- input callbacks can attempt texture upload
|
||||
- output callbacks can trigger frame rendering and readback
|
||||
- preview paint can enter the same GL context
|
||||
- screenshot capture can enter the same GL context
|
||||
- the DeckLink completion path is still too close to render work
|
||||
|
||||
That means brief input, preview, readback, or callback stalls can still collide on the most timing-sensitive path.
|
||||
|
||||
Phase 4 should turn GL from a shared resource guarded by a lock into a resource owned by one thread with explicit queues and handoff points.
|
||||
|
||||
## Goals
|
||||
|
||||
Phase 4 should establish:
|
||||
|
||||
- one render thread as the sole long-lived owner of the GL context
|
||||
- non-render threads enqueue work instead of binding the GL context
|
||||
- input upload requests are accepted and executed by the render thread
|
||||
- output frame rendering is requested or scheduled through render-owned work
|
||||
- preview and screenshot requests become render-thread commands or consumers
|
||||
- `RenderFrameInput` / `RenderFrameState` become the stable data contract for frame production
|
||||
- GL context entrypoints are reduced to render-thread-only code paths
|
||||
- tests for queue semantics and request coalescing without requiring DeckLink hardware, plus explicit lifecycle ordering in code
|
||||
|
||||
## Non-Goals
|
||||
|
||||
Phase 4 should not require:
|
||||
|
||||
- the final producer/consumer playout queue for DeckLink
|
||||
- the final DeckLink lifecycle state machine
|
||||
- replacing the async readback policy
|
||||
- implementing background persistence
|
||||
- completing Phase 5's deeper live-state layering
|
||||
- replacing every UI or backend API at once
|
||||
|
||||
Those are later phases or follow-on work. Phase 4 is about making GL ownership deterministic first.
|
||||
|
||||
## Current GL Entry Points
|
||||
|
||||
The current code paths that matter most are:
|
||||
|
||||
| Entry point | Current behavior | Phase 4 direction |
|
||||
| --- | --- | --- |
|
||||
| `RenderEngine::QueueInputFrame(...)` | copies the latest input frame into the render mailbox and returns without waiting for GL | render thread uploads latest input without callback-owned GL |
|
||||
| `RenderEngine::RequestOutputFrame(...)` | synchronous output request; after render-thread startup it queues output render work and waits for render-thread completion with timeout/failure reporting | render thread executes output frame production |
|
||||
| `RenderEngine::TryPresentPreview(...)` | best-effort request; callers queue preview presentation and return | render thread consumes latest completed frame for preview |
|
||||
| `RenderEngine::RequestScreenshotCapture(...)` | queues screenshot capture and async disk write completion | screenshot capture is a render-thread command |
|
||||
| `OpenGLVideoIOBridge::UploadInputFrame(...)` | copies the latest input frame into the render mailbox and returns without waiting for GL | render thread uploads the latest queued input frame |
|
||||
| `OpenGLVideoIOBridge::RenderScheduledFrame(...)` | requests render-thread output production and reports success/failure to the backend | consume render-produced output without callback-owned GL |
|
||||
|
||||
## Target Ownership Model
|
||||
|
||||
### Render Thread
|
||||
|
||||
The render thread should own:
|
||||
|
||||
- `wglMakeCurrent(...)` for the rendering context
|
||||
- all GL resource creation/destruction
|
||||
- input texture upload
|
||||
- pass execution
|
||||
- output pack conversion
|
||||
- async readback buffers and fences
|
||||
- preview presentation or preview frame publication
|
||||
- screenshot readback
|
||||
- temporal history and feedback resources
|
||||
|
||||
### Other Threads
|
||||
|
||||
Other threads may:
|
||||
|
||||
- enqueue input frames or replace the latest input frame
|
||||
- publish control/runtime/backend events
|
||||
- request shader build application
|
||||
- request render-local resets
|
||||
- request screenshots
|
||||
- consume ready output frames or receive completion notifications
|
||||
|
||||
Other threads should not:
|
||||
|
||||
- call GL directly
|
||||
- bind or unbind the render context
|
||||
- wait on GL fences directly
|
||||
- mutate render-local resource state
|
||||
|
||||
## Proposed Collaborators
|
||||
|
||||
### `RenderThread`
|
||||
|
||||
Owns the OS thread, wakeup primitive, lifecycle, and render-loop execution.
|
||||
|
||||
Responsibilities:
|
||||
|
||||
- start and stop the render thread
|
||||
- bind the GL context for the thread lifetime or render-loop lifetime
|
||||
- drain render commands
|
||||
- execute frame production work
|
||||
- publish lifecycle and failure observations
|
||||
|
||||
Non-responsibilities:
|
||||
|
||||
- runtime mutation policy
|
||||
- DeckLink scheduling policy
|
||||
- durable persistence
|
||||
|
||||
### `RenderCommandQueue`
|
||||
|
||||
Small bounded queue or command mailbox for render-thread work.
|
||||
|
||||
Current implementation:
|
||||
|
||||
- `RenderCommandQueue` exists as a pure C++ mailbox helper.
|
||||
- Preview present and screenshot capture requests use latest-value coalescing.
|
||||
- Input upload requests use latest-value coalescing with owned frame bytes copied at enqueue time.
|
||||
- Output frame requests use FIFO semantics so scheduled output demand is not collapsed.
|
||||
- Render-local reset requests coalesce to the strongest pending reset scope.
|
||||
- Output frame requests use synchronous request/response through the render thread as the remaining transitional playout bridge.
|
||||
|
||||
Possible commands:
|
||||
|
||||
- `UploadInputFrame`
|
||||
- `RenderOutputFrame`
|
||||
- `PrepareFrameState`
|
||||
- `ApplyShaderBuild`
|
||||
- `ResetTemporalHistory`
|
||||
- `ResetShaderFeedback`
|
||||
- `PresentPreview`
|
||||
- `CaptureScreenshot`
|
||||
- `Stop`
|
||||
|
||||
High-rate commands should be coalesced where appropriate. Input frames should likely be latest-value rather than unbounded FIFO.
|
||||
|
||||
### `RenderFrameCoordinator`
|
||||
|
||||
Optional helper that combines Phase 3's frame contract with render-thread execution.
|
||||
|
||||
Responsibilities:
|
||||
|
||||
- build or receive `RenderFrameInput`
|
||||
- call `RuntimeServiceLiveBridge` and `RenderFrameStateResolver`
|
||||
- hand `RenderFrameState` to `RenderEngine`
|
||||
|
||||
This can begin as a thin helper. The important part is that it keeps frame-state preparation explicit when `renderEffect()` stops being called directly from the callback path.
|
||||
|
||||
### `RenderOutputMailbox`
|
||||
|
||||
Optional transitional bridge for output frames.
|
||||
|
||||
Responsibilities:
|
||||
|
||||
- hold the latest completed output frame or a small bounded set
|
||||
- let backend code consume output without owning GL
|
||||
- report underrun/stale-frame reuse observations
|
||||
|
||||
This may be a Phase 4 late step or a Phase 7 playout-policy step. Phase 4 should at least avoid designing the render thread in a way that blocks it.
|
||||
|
||||
## Threading Contract
|
||||
|
||||
Phase 4 should make thread ownership visible in APIs.
|
||||
|
||||
Candidate naming:
|
||||
|
||||
- `RenderEngine::StartRenderThread(...)`
|
||||
- `RenderEngine::StopRenderThread()`
|
||||
- `RenderEngine::EnqueueInputFrame(...)`
|
||||
- `RenderEngine::RequestOutputFrame(...)`
|
||||
- `RenderEngine::RequestPreviewPresent(...)`
|
||||
- `RenderEngine::RequestScreenshot(...)`
|
||||
|
||||
Render-thread-only methods should be private or clearly named:
|
||||
|
||||
- `RenderEngine::UploadInputFrameOnRenderThread(...)`
|
||||
- `RenderEngine::RenderOutputFrameOnRenderThread(...)`
|
||||
- `RenderEngine::CaptureOutputFrameRgbaTopDownOnRenderThread(...)`
|
||||
|
||||
The public runtime entrypoints now use queue/request language. `RequestOutputFrame(...)` remains synchronous so the existing DeckLink callback path can keep producing an output frame while Phase 7's producer/consumer playout queue is still future work.
|
||||
|
||||
## Frame Production Shape
|
||||
|
||||
A target render-thread frame should look like:
|
||||
|
||||
1. wake for input, output demand, preview demand, shader build, reset, screenshot, or stop
|
||||
2. drain bounded render commands
|
||||
3. coalesce to the latest input frame and latest control/live state
|
||||
4. build `RenderFrameInput`
|
||||
5. prepare `RenderFrameState`
|
||||
6. upload accepted input frame
|
||||
7. render layer stack
|
||||
8. pack output if needed
|
||||
9. stage readback or output buffer
|
||||
10. publish preview/screenshot/output completion as needed
|
||||
11. record timing and queue metrics
|
||||
|
||||
The exact cadence can remain demand-driven initially. The architectural win is that the demand wakes the render thread rather than borrowing GL from the caller.
|
||||
|
||||
## Migration Plan
|
||||
|
||||
### Step 1. Name Render-Thread-Only Methods
|
||||
|
||||
Split existing direct GL methods into public request methods and private render-thread methods without changing behavior much.
|
||||
|
||||
Initial target:
|
||||
|
||||
- [x] keep current synchronous behavior where callers need a result
|
||||
- [x] move GL bodies into clearly render-thread-owned helpers for upload, output render, preview presentation, and screenshot readback
|
||||
- [x] make future queue migration mechanical
|
||||
|
||||
### Step 2. Add Render Command Queue
|
||||
|
||||
Introduce a small queue/mailbox for render commands.
|
||||
|
||||
Start with low-risk commands:
|
||||
|
||||
- [x] preview present request
|
||||
- [x] screenshot request
|
||||
- [x] render-local reset requests
|
||||
- [x] input upload request
|
||||
- [x] output render request
|
||||
|
||||
The queue and wakeup behavior still need the dedicated render thread before the callbacks stop borrowing the GL context.
|
||||
|
||||
### Step 3. Start A Dedicated Render Thread
|
||||
|
||||
Create the render thread and make it own context binding.
|
||||
|
||||
- [x] create a dedicated render thread owned by `RenderEngine`
|
||||
- [x] bind the existing GL context on the render thread for normal runtime work
|
||||
- [x] stop the render thread before GL context destruction
|
||||
- [x] keep transitional synchronous request/response for output frames
|
||||
- [x] remove normal runtime dependence on the shared GL `CRITICAL_SECTION`
|
||||
- [x] add timeout/failure behavior for render-thread requests
|
||||
|
||||
Transitional behavior still allows synchronous request/response for output frames. Render-thread requests now fail fast if they cannot begin within the request timeout, and log over-budget tasks that have already started before waiting for safe completion. The important change is that the caller waits for render-thread completion rather than taking the GL context itself.
|
||||
|
||||
### Step 4. Move Input Upload To The Render Thread
|
||||
|
||||
Change `OpenGLVideoIOBridge::UploadInputFrame(...)` so it enqueues or replaces the latest input frame.
|
||||
|
||||
Policy targets:
|
||||
|
||||
- [x] bounded memory
|
||||
- [x] latest-frame wins under load
|
||||
- [x] input upload skip count is observable through render command coalescing metrics
|
||||
- [x] input callback never waits for GL
|
||||
|
||||
Current implementation: `OpenGLVideoIOBridge::UploadInputFrame(...)` calls `RenderEngine::QueueInputFrame(...)`, which copies the input bytes into the latest-value render mailbox and schedules one bounded render-thread wakeup to upload the newest pending frame.
|
||||
|
||||
### Step 5. Move Output Rendering To The Render Thread
|
||||
|
||||
Change `OpenGLVideoIOBridge::RenderScheduledFrame(...)` so it requests render-thread output production or consumes a completed render-thread output.
|
||||
|
||||
Transitional option:
|
||||
|
||||
- [x] synchronous request/response through the render thread
|
||||
|
||||
Better follow-up:
|
||||
|
||||
- render ahead into a bounded output queue and let backend callbacks consume ready frames
|
||||
|
||||
Current implementation: `OpenGLVideoIOBridge::RenderScheduledFrame(...)` calls `RenderEngine::RequestOutputFrame(...)` and returns whether the render-thread request produced an output frame. `VideoBackend` skips scheduling that frame when render production fails or times out.
|
||||
|
||||
### Step 6. Decouple Preview And Screenshot Requests
|
||||
|
||||
Preview should become best-effort:
|
||||
|
||||
- [x] request preview presentation from the render thread
|
||||
- [x] skip/coalesce when render is busy or output deadline pressure is high
|
||||
- [x] record preview skips through render command coalescing metrics
|
||||
|
||||
Screenshot should become:
|
||||
|
||||
- [x] queued render-thread capture request
|
||||
- [x] async disk write remains outside render thread
|
||||
|
||||
Current implementation: `OpenGLComposite::RequestScreenshot(...)` builds the output path, queues `RenderEngine::RequestScreenshotCapture(...)`, and the render thread captures pixels before handing them to the existing async PNG writer. Preview presentation is a latest-value best-effort render command that is queued behind output render work, even when requested from the render pipeline.
|
||||
|
||||
### Step 7. Remove Shared GL Lock From Normal Paths
|
||||
|
||||
Once all GL entrypoints are render-thread-owned:
|
||||
|
||||
- [x] remove normal dependence on `pMutex` for render correctness
|
||||
- [x] keep diagnostics that detect wrong-thread render-thread helper calls
|
||||
- [x] leave only lifecycle context binding where needed
|
||||
|
||||
Current implementation: `OpenGLComposite` no longer owns or passes a shared `CRITICAL_SECTION`, and `RenderEngine` no longer has caller-thread GL fallback paths for preview, input upload, output render, or screenshot capture. Runtime callers must go through the render thread; pre-start direct GL fallback is limited to startup initialization while the app explicitly owns the context.
|
||||
|
||||
### Shutdown Order
|
||||
|
||||
Current shutdown order is explicit in code:
|
||||
|
||||
1. `OpenGLComposite::Stop()` stops runtime services so control/OSC work stops entering the runtime.
|
||||
2. `VideoBackend::Stop()` stops DeckLink streams/playout so input and output callbacks stop requesting render work.
|
||||
3. `RenderEngine::StopRenderThread()` destroys GL resources on the render thread, signals the render thread to stop, joins it, and unbinds the context on render-thread exit.
|
||||
4. `WM_DESTROY` deletes `OpenGLComposite`, unbinds the window context, and deletes the GL context.
|
||||
|
||||
This order is build-tested, and `RenderCommandQueue` behavior is covered by non-GL unit tests. It still benefits from a real-window/DeckLink shutdown smoke test, but the code path is explicit enough for Phase 4's design exit.
|
||||
|
||||
## Testing Strategy
|
||||
|
||||
Phase 4 tests should avoid hardware where possible.
|
||||
|
||||
Recommended tests:
|
||||
|
||||
- render command queue preserves FIFO for non-coalesced commands
|
||||
- latest-input mailbox drops older frames under load
|
||||
- shutdown path stops backend callbacks before stopping and joining the render thread
|
||||
- screenshot request receives one completion or failure
|
||||
- output render request reports failure if render thread is stopped
|
||||
- render reset commands coalesce where expected
|
||||
- wrong-thread render-only diagnostics are present on private render-thread helpers
|
||||
|
||||
Existing useful homes:
|
||||
|
||||
- `RuntimeEventTypeTests` for new render/backend observations
|
||||
- `RuntimeSubsystemTests` for pure request/coalescing helpers
|
||||
- a future `RenderThreadTests` target if render-thread lifecycle is extracted behind a non-GL test seam
|
||||
|
||||
Manual verification will still be needed for:
|
||||
|
||||
- real DeckLink input/output
|
||||
- preview interaction
|
||||
- screenshot capture
|
||||
- shader reload while rendering
|
||||
- real window/context shutdown
|
||||
|
||||
## Telemetry Added During Phase 4
|
||||
|
||||
Phase 4 should add minimal metrics while moving ownership:
|
||||
|
||||
- render command queue depth
|
||||
- input frames accepted, replaced, and dropped
|
||||
- render-thread wake reason counts
|
||||
- render-thread frame duration
|
||||
- output request latency
|
||||
- preview request skipped count
|
||||
- screenshot request success/failure count
|
||||
- wrong-thread GL call diagnostics if practical
|
||||
|
||||
Full operational reporting remains Phase 8, but these metrics make the threading migration debuggable.
|
||||
|
||||
## Risks
|
||||
|
||||
### Deadlock Risk
|
||||
|
||||
Synchronous request/response shims can deadlock if the caller is already on the render thread or holds a lock the render thread needs. Phase 4 should keep request waits narrow and add render-thread detection early.
|
||||
|
||||
### Latency Risk
|
||||
|
||||
Moving work through queues can hide latency. Queue depth and output request latency should be measured from the first migration step.
|
||||
|
||||
### Lifetime Risk
|
||||
|
||||
Moving context ownership changes startup and shutdown order. The render thread must stop before GL resources or window/context handles are destroyed.
|
||||
|
||||
### Callback Pressure Risk
|
||||
|
||||
If DeckLink callbacks wait too long for render-thread work, Phase 4 may improve GL ownership but still leave callback timing fragile. A synchronous bridge is acceptable as a transition, but the design should keep the path open for producer/consumer playout.
|
||||
|
||||
### Preview Coupling Risk
|
||||
|
||||
Preview can remain a hidden budget consumer if it stays in the output frame path. Phase 4 should keep preview explicitly best-effort, even if physical decoupling continues later.
|
||||
|
||||
## Phase 4 Exit Criteria
|
||||
|
||||
Phase 4 can be considered complete once the project can say:
|
||||
|
||||
- [x] one render thread owns the GL context during normal operation
|
||||
- [x] input callbacks do not bind GL or wait on GL upload
|
||||
- [x] output callbacks do not bind GL directly
|
||||
- [x] preview and screenshot requests enter render through explicit render-thread requests
|
||||
- [x] `RenderFrameInput` / `RenderFrameState` remain the frame-state contract
|
||||
- [x] normal frame production no longer depends on a shared GL `CRITICAL_SECTION`
|
||||
- [x] render-thread queue/mailbox behavior has non-GL tests
|
||||
- [x] shutdown order is explicit and tested or manually verified
|
||||
|
||||
## Open Questions
|
||||
|
||||
- What exact producer/consumer output queue shape should replace the current synchronous output request in Phase 7?
|
||||
- Should preview present on the render thread, or should render publish a preview image/texture to a separate presenter?
|
||||
- Should wrong-thread GL access eventually escalate from debug diagnostics to structured telemetry or assertions?
|
||||
|
||||
## Short Version
|
||||
|
||||
Phase 4 should make GL ownership boring and deterministic.
|
||||
|
||||
One render thread owns the context. Other threads submit work or consume results. Input upload, frame rendering, readback, preview, and screenshot capture all move behind render-thread entrypoints. Output production remains a synchronous request/response bridge for now, but the app no longer relies on callback and UI paths borrowing the GL context under one shared lock.
|
||||
416
docs/PHASE_5_LIVE_STATE_LAYERING_DESIGN.md
Normal file
416
docs/PHASE_5_LIVE_STATE_LAYERING_DESIGN.md
Normal file
@@ -0,0 +1,416 @@
|
||||
# Phase 5 Design: Live State Layering And Composition
|
||||
|
||||
This document expands Phase 5 of [ARCHITECTURE_RESILIENCE_REVIEW.md](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/docs/ARCHITECTURE_RESILIENCE_REVIEW.md) into a concrete design target.
|
||||
|
||||
Phase 1 named the subsystems. Phase 2 added the typed event substrate. Phase 3 made render-facing live state explicit through `RuntimeLiveState`, `RenderStateComposer`, `RenderFrameInput`, `RenderFrameState`, `RenderFrameStateResolver`, and `RuntimeServiceLiveBridge`. Phase 4 made one render thread the owner of normal runtime GL work. Phase 5 should now make the live parameter model itself explicit: persisted truth, operator/session truth, and transient automation should be separate layers with one predictable composition rule.
|
||||
|
||||
## Status
|
||||
|
||||
- Phase 5 design package: proposed.
|
||||
- Phase 5 implementation: Step 5 complete.
|
||||
- Current alignment: Phase 3 introduced the first pure composition boundary and transient OSC overlay owner. Phase 5 now has a small `RuntimeStateLayerModel` inventory that names the current state categories, `RenderStateComposer` consumes a `LayeredRenderStateInput` whose fields make base persisted, committed live, and transient automation inputs explicit, `RuntimeLiveState` owns transient-overlay invalidation against current layer/parameter compatibility, settled OSC commits have an explicit session-only persistence policy, and snapshot publication consumes a named `CommittedLiveStateReadModel`. Committed runtime values are still physically backed by `RuntimeStore`/`LayerStackStore` during this conservative migration step.
|
||||
|
||||
Current live-state footholds:
|
||||
|
||||
- `RuntimeStore` owns persisted layer stack, parameter values, presets, config, and render snapshot read models.
|
||||
- `RuntimeCoordinator` owns mutation validation, classification, accepted/rejected event publication, snapshot/reload follow-ups, and the policy switch between committed states and live snapshots.
|
||||
- `RuntimeSnapshotProvider` publishes render-facing snapshots from committed runtime state.
|
||||
- `RuntimeLiveState` owns transient OSC overlay bookkeeping, smoothing, generation tracking, and commit-settlement policy.
|
||||
- `RenderStateComposer` consumes `LayeredRenderStateInput`, chooses committed-live layer states over base-persisted layer states when both are supplied, applies transient automation on top, and returns final per-frame layer states plus settled commit requests.
|
||||
- `RuntimeServiceLiveBridge` drains OSC ingress/completion queues and applies them to render live state during frame preparation.
|
||||
- `RuntimeStateLayerModel` names the Phase 5 state categories and classifies current fields as base persisted, committed live, transient automation, render-local, or health/config state.
|
||||
- `RuntimeCoordinator` can request layer-scoped transient OSC invalidation, while `RuntimeLiveState` prunes overlays that no longer map to the current render-facing layer/parameter definitions.
|
||||
- `RuntimeCoordinator::CommitOscParameterByControlKey(...)` commits settled OSC values into session state without requesting persistence by default.
|
||||
- `CommittedLiveStateReadModel` names the current committed/session read boundary that feeds render snapshot publication while remaining physically backed by `RuntimeStore`.
|
||||
|
||||
## Why Phase 5 Exists
|
||||
|
||||
The resilience review identifies live OSC overlay and persisted state as separate concepts that still do not have a fully formal model. The app now has better boundaries, but several policies are still implicit:
|
||||
|
||||
- whether a value is durable, committed for the current session, or transient automation
|
||||
- whether an OSC value should merely influence the current frame or eventually commit
|
||||
- what reload, preset load, layer removal, shader change, and reset should do to transient values
|
||||
- which layer wins when UI/operator changes race with OSC automation
|
||||
- which state changes should publish snapshots, request persistence, or only affect render frames
|
||||
|
||||
Without a formal layering model, these rules can leak across `RuntimeStore`, `RuntimeCoordinator`, `RuntimeLiveState`, `RenderStateComposer`, and service bridges. Phase 5 should make those rules boring and testable.
|
||||
|
||||
## Goals
|
||||
|
||||
Phase 5 should establish:
|
||||
|
||||
- explicit state layers for persisted, committed/session, and transient automation values
|
||||
- one named composition contract for final render values
|
||||
- clear ownership for layer-specific mutation policy
|
||||
- explicit reset/reload/preset behavior for transient and committed state
|
||||
- a clean path for OSC automation to remain high-rate without becoming durable state by accident
|
||||
- tests for layer precedence, lifecycle, invalidation, and commit policy without GL or DeckLink
|
||||
- documentation that distinguishes render-local temporal/feedback state from parameter/live-state overlays
|
||||
|
||||
## Non-Goals
|
||||
|
||||
Phase 5 should not require:
|
||||
|
||||
- a background persistence writer implementation
|
||||
- a DeckLink producer/consumer playout queue
|
||||
- a full cue/timeline/preset performance system
|
||||
- a new UI state-management framework
|
||||
- replacing every synchronous coordinator API
|
||||
- moving temporal history or shader feedback into the runtime state model
|
||||
|
||||
Those are later phases or separate feature work. Phase 5 is about parameter and live-value layering.
|
||||
|
||||
## Target State Model
|
||||
|
||||
Phase 5 should formalize three layers:
|
||||
|
||||
| Layer | Owner | Lifetime | Persistence | Render role |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| Base persisted state | `RuntimeStore` / `LayerStackStore` | survives restart | written to disk | default layer stack, shader selections, saved parameter values |
|
||||
| Committed live state | `RuntimeCoordinator` or a new live-session collaborator | current running session | may request persistence depending on mutation type | operator/UI/current truth until changed again |
|
||||
| Transient automation overlay | `RuntimeLiveState` or a new automation overlay collaborator | high-rate/short-lived | not persisted directly | temporary OSC/automation target applied over committed truth |
|
||||
|
||||
The target composition rule is:
|
||||
|
||||
```text
|
||||
final render state = base persisted state + committed live state + transient automation overlay
|
||||
```
|
||||
|
||||
The actual implementation may continue using render snapshots as the base transport. The important part is that each layer has named ownership, documented lifetime, and tested precedence.
|
||||
|
||||
## Current Composition Shape
|
||||
|
||||
Today, final frame state is prepared through this path:
|
||||
|
||||
1. `OpenGLComposite::renderEffect()` processes runtime work.
|
||||
2. `OpenGLComposite` builds `RenderFrameInput`.
|
||||
3. `RuntimeServiceLiveBridge` drains OSC updates and completed commits.
|
||||
4. `RenderEngine` updates `RuntimeLiveState`.
|
||||
5. `RenderFrameStateResolver` chooses committed states or live snapshot states.
|
||||
6. `RenderStateComposer` applies transient overlay values.
|
||||
7. `RenderEngine::RenderPreparedFrame(...)` consumes `RenderFrameState`.
|
||||
|
||||
That is a good Phase 3/4 foundation. Phase 5 should make the hidden assumptions in steps 5 and 6 explicit enough that reset/reload/preset and future UI automation behavior are not scattered across those collaborators.
|
||||
|
||||
## Proposed Collaborators
|
||||
|
||||
### `RuntimeStateLayerModel`
|
||||
|
||||
Optional pure model that names the layers and composition metadata.
|
||||
|
||||
Responsibilities:
|
||||
|
||||
- represent base, committed, and transient layer state inputs
|
||||
- define precedence and invalidation categories
|
||||
- expose a pure composition function or input object
|
||||
- keep GL, services, persistence, and device callbacks out of the model
|
||||
|
||||
Non-responsibilities:
|
||||
|
||||
- disk IO
|
||||
- OSC socket handling
|
||||
- render-thread scheduling
|
||||
- shader compilation
|
||||
|
||||
This may be a small set of structs rather than a large class. The value is in naming the contract.
|
||||
|
||||
### `CommittedLiveState`
|
||||
|
||||
Optional runtime/session collaborator if committed session state needs to move out of `RuntimeStore`.
|
||||
|
||||
Responsibilities:
|
||||
|
||||
- hold operator/UI committed values that are true for the current session
|
||||
- distinguish persistence-required commits from session-only commits
|
||||
- expose a read model for snapshot publication
|
||||
- provide reset/load behavior separate from durable storage
|
||||
|
||||
Non-responsibilities:
|
||||
|
||||
- transient OSC smoothing
|
||||
- disk writes
|
||||
- GL resources
|
||||
|
||||
Phase 5 can defer this physical split if the policy is documented and covered by tests. The key is that committed-live state becomes a distinct concept even if it still lives inside existing storage temporarily.
|
||||
|
||||
### `AutomationOverlayState`
|
||||
|
||||
Possible evolution of `RuntimeLiveState`.
|
||||
|
||||
Responsibilities:
|
||||
|
||||
- hold transient automation values keyed by route/layer/parameter identity
|
||||
- track generation, commit-in-flight, and completion
|
||||
- apply smoothing and settle policy
|
||||
- decide whether an overlay is render-only, commit-requesting, stale, or invalidated
|
||||
|
||||
Non-responsibilities:
|
||||
|
||||
- owning committed truth
|
||||
- persistent state mutation
|
||||
- snapshot publication
|
||||
|
||||
This can start by renaming or narrowing current `RuntimeLiveState` responsibilities rather than replacing it outright.
|
||||
|
||||
### `LayeredStateComposer`
|
||||
|
||||
Possible evolution of `RenderStateComposer`.
|
||||
|
||||
Responsibilities:
|
||||
|
||||
- apply the target precedence rule
|
||||
- produce final `RuntimeRenderState` values for a frame
|
||||
- return commit requests or overlay observations when policy says a transient value settled
|
||||
- keep value composition testable without OpenGL
|
||||
|
||||
Non-responsibilities:
|
||||
|
||||
- frame rendering
|
||||
- service queue draining
|
||||
- storage mutation
|
||||
|
||||
## Layering Rules
|
||||
|
||||
### Precedence
|
||||
|
||||
Default precedence should be:
|
||||
|
||||
1. base persisted/snapshot value
|
||||
2. committed live/session value
|
||||
3. transient automation overlay
|
||||
|
||||
The topmost valid layer wins for discrete values. Numeric/vector values may be smoothed by overlay policy before they win.
|
||||
|
||||
### Identity
|
||||
|
||||
Layering should use stable render-facing identity:
|
||||
|
||||
- layer id for persisted structural identity
|
||||
- layer key/control key for OSC-facing identity
|
||||
- parameter id for shader-defined identity
|
||||
- parameter control key for external-control identity
|
||||
|
||||
Phase 5 should document which identity is authoritative when layer id and control key disagree or when a shader changes.
|
||||
|
||||
### Invalidations
|
||||
|
||||
The following should have explicit behavior:
|
||||
|
||||
- layer removed: clear committed and transient state for that layer
|
||||
- layer shader changed: clear or remap parameter overlays according to compatible control keys
|
||||
- preset loaded: replace base/committed state and clear incompatible transient overlays
|
||||
- shader reload with same controls: preserve compatible transient overlays where safe
|
||||
- manual reset parameters: clear committed overrides and transient overlays for that layer
|
||||
- no input/source changes: should not affect parameter layers
|
||||
|
||||
### Commit Policy
|
||||
|
||||
Transient automation may:
|
||||
|
||||
- remain render-only
|
||||
- settle and request a committed mutation
|
||||
- commit without persistence
|
||||
- commit with persistence only when the control path explicitly requests it
|
||||
|
||||
The policy should be explicit per ingress path or parameter category. Phase 5 does not need a full UI for it, but the default behavior should be documented and tested.
|
||||
|
||||
## Event And Snapshot Contract
|
||||
|
||||
Phase 5 should clarify which changes publish which effects:
|
||||
|
||||
| Change | Snapshot publication | Persistence request | Render reset | Runtime event |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| persisted layer stack mutation | yes | yes | maybe | accepted mutation + persistence requested |
|
||||
| operator live parameter change | yes | maybe | no, unless structural | accepted mutation |
|
||||
| transient OSC overlay update | no committed snapshot by default | no | no | optional overlay observation |
|
||||
| overlay settled commit | yes if accepted | usually no for OSC | no | accepted mutation or overlay-settled observation |
|
||||
| preset load | yes | maybe | temporal/feedback policy dependent | accepted mutation + reload/reset observations |
|
||||
| shader change/reload | yes after build | maybe | temporal/feedback policy dependent | shader build/reload events |
|
||||
|
||||
This table should evolve with implementation, but Phase 5 should prevent transient overlay updates from masquerading as durable committed state.
|
||||
|
||||
## Migration Plan
|
||||
|
||||
### Step 1. Inventory Current State Layers
|
||||
|
||||
Document and/or encode where each current state category lives:
|
||||
|
||||
- persisted layer stack and parameter values
|
||||
- committed current-session parameter values
|
||||
- runtime compile/reload flags
|
||||
- transient OSC overlays
|
||||
- render-local temporal history and feedback state
|
||||
|
||||
Initial target:
|
||||
|
||||
- [x] identify which fields are durable, committed-live, transient automation, render-local, or health/config
|
||||
- [x] update subsystem docs where the current ownership is misleading
|
||||
- [x] add small tests for classification if a pure helper exists
|
||||
|
||||
### Step 2. Name The Layered Composition Input
|
||||
|
||||
Introduce a named composition input model around the previous `RenderStateCompositionInput`.
|
||||
|
||||
Initial target:
|
||||
|
||||
- [x] make base/committed/transient inputs visible in type names or field names
|
||||
- [x] keep `RenderStateComposer` behavior unchanged at first
|
||||
- [x] add tests that assert precedence with no GL
|
||||
|
||||
Possible outcomes:
|
||||
|
||||
- [x] add a new `LayeredRenderStateInput`
|
||||
- [ ] add a thin adapter if a later migration needs compatibility with the previous input shape
|
||||
|
||||
### Step 3. Make Reset And Reload Policy Explicit
|
||||
|
||||
Move reset/reload transient-state decisions into one policy point.
|
||||
|
||||
Initial target:
|
||||
|
||||
- [x] layer removal clears matching transient overlays
|
||||
- [x] shader change clears incompatible overlays
|
||||
- [x] preset load clears incompatible overlays
|
||||
- [x] shader reload can preserve compatible overlays when requested
|
||||
- [x] temporal/feedback resets stay render-local and separate from parameter overlays
|
||||
|
||||
This is where Phase 5 should prevent "clear everything" and "preserve everything" from being scattered through unrelated code.
|
||||
|
||||
Current implementation:
|
||||
|
||||
- `RuntimeCoordinatorResult` carries a named `RuntimeCoordinatorTransientOscInvalidation` request rather than a raw clear-all flag.
|
||||
- `RuntimeUpdateController` applies layer-scoped invalidation to both render-owned overlay state and queued OSC service state.
|
||||
- `RuntimeLiveState::PruneIncompatibleOverlays(...)` is the central compatibility policy for current render-facing layer/parameter definitions.
|
||||
- `RuntimeLiveState::ApplyToLayerStates(...)` prunes incompatible overlays before applying transient values, so shader changes, preset loads, and layer removals stop carrying stale overlays once the current frame state no longer maps them.
|
||||
|
||||
### Step 4. Clarify OSC Commit Semantics
|
||||
|
||||
Make the transient-to-committed path explicit.
|
||||
|
||||
Initial target:
|
||||
|
||||
- [x] document and test whether settled OSC commits persist
|
||||
- [x] ensure stale generation completions are ignored
|
||||
- [x] ensure one settled route does not clear unrelated overlay state
|
||||
- [x] publish or preserve useful events for accepted overlay commits
|
||||
|
||||
Current Phase 3 behavior is a good base; Phase 5 should make the policy easier to reason about from the code.
|
||||
|
||||
Current policy:
|
||||
|
||||
- settled OSC commits are `RuntimeCoordinatorOscCommitPersistence::SessionOnly` by default
|
||||
- accepted settled OSC commits update the committed session value through `RuntimeStore::SetStoredParameterValue(..., persistState = false, ...)`
|
||||
- accepted settled OSC commits publish runtime mutation/state-change observations, but no `RuntimePersistenceRequested` event
|
||||
- accepted service-side commit completions publish `OscOverlaySettled`
|
||||
- stale generation completions are ignored by `RuntimeLiveState::ApplyOscCommitCompletions(...)`
|
||||
- unrelated routes remain untouched when a different route settles or completes
|
||||
|
||||
### Step 5. Separate Committed-Live Concept From Durable Storage
|
||||
|
||||
Decide whether to physically split committed-live state now or introduce a read/model boundary first.
|
||||
|
||||
Conservative option:
|
||||
|
||||
- [x] leave storage physically in `RuntimeStore`
|
||||
- [x] add a named committed-live read model
|
||||
- [x] keep persistence decisions in `RuntimeCoordinator`
|
||||
|
||||
Stronger option:
|
||||
|
||||
- introduce `CommittedLiveState`
|
||||
- make `RuntimeSnapshotProvider` consume committed live state through a read model
|
||||
- leave durable writes in `RuntimeStore`
|
||||
|
||||
Phase 5 does not need a flag-day split. It needs the concept to stop being implicit.
|
||||
|
||||
Current implementation:
|
||||
|
||||
- `CommittedLiveStateReadModel` carries the current committed/session layer stack and shader package metadata used by snapshot publication.
|
||||
- `RenderSnapshotReadModel` contains `committedLiveState` rather than exposing layer-stack fields directly.
|
||||
- `RenderSnapshotBuilder` builds render snapshots and parameter refreshes from committed-live read APIs.
|
||||
- `RuntimeStore` still provides the physical backing during this phase, but session-only committed changes can be observed through the committed-live read model without requiring durable persistence.
|
||||
|
||||
### Step 6. Update Docs And Exit Criteria
|
||||
|
||||
Before calling Phase 5 complete, update:
|
||||
|
||||
- architecture review checklist
|
||||
- `RuntimeCoordinator`, `RuntimeStore`, `RuntimeSnapshotProvider`, `RenderEngine`, and `ControlServices` subsystem docs
|
||||
- Phase 6 assumptions about persistence inputs
|
||||
- Phase 7 assumptions about what render/backend state is not part of live parameter layering
|
||||
|
||||
## Testing Strategy
|
||||
|
||||
Phase 5 tests should avoid GL, DeckLink, sockets, and filesystem writes where possible.
|
||||
|
||||
Recommended tests:
|
||||
|
||||
- base value is used when no committed or transient value exists
|
||||
- committed value overrides base value
|
||||
- transient overlay overrides committed value
|
||||
- numeric smoothing applies only to transient overlay values
|
||||
- trigger/bool/discrete overlay behavior is explicit
|
||||
- layer removal clears matching transient state
|
||||
- shader change preserves only compatible overlays if policy allows
|
||||
- preset load clears or replaces committed/transient state according to policy
|
||||
- settled OSC overlay creates the expected commit request
|
||||
- settled OSC commit does not request persistence unless policy says so
|
||||
- stale commit completion does not clear a newer overlay
|
||||
- render-local temporal/feedback resets do not mutate parameter layers
|
||||
|
||||
Existing useful homes:
|
||||
|
||||
- `RuntimeLiveStateTests` for overlay generation, smoothing, settle, and invalidation behavior
|
||||
- `RuntimeSubsystemTests` for coordinator mutation, persistence request, and reset/reload policy
|
||||
- `RuntimeEventTypeTests` for any new observations or accepted mutation events
|
||||
- a possible new `RuntimeStateLayeringTests` target if the composition model gets a pure helper
|
||||
|
||||
## Risks
|
||||
|
||||
### Over-Abstraction Risk
|
||||
|
||||
It would be easy to introduce too many state containers. Phase 5 should add names where they clarify behavior, not create an elaborate framework.
|
||||
|
||||
### Persistence Confusion Risk
|
||||
|
||||
Committed live state and persisted state are related but not identical. If Phase 5 blurs them, Phase 6's background persistence writer will inherit ambiguous inputs.
|
||||
|
||||
### Automation Surprise Risk
|
||||
|
||||
OSC automation can be high-rate and transient, but users may expect settled values to become "real." The commit policy needs to be explicit enough that UI, OSC, presets, and reloads behave predictably.
|
||||
|
||||
### Identity/Compatibility Risk
|
||||
|
||||
Shader changes and preset loads can invalidate layer/parameter identities. Phase 5 should prefer conservative clearing over accidental application of an old automation value to the wrong control.
|
||||
|
||||
### Render Coupling Risk
|
||||
|
||||
Render-local resources such as temporal history, feedback buffers, readback caches, and playout queues are not parameter layers. Keeping them out of this model avoids turning Phase 5 into a render-resource refactor.
|
||||
|
||||
## Phase 5 Exit Criteria
|
||||
|
||||
Phase 5 can be considered complete once the project can say:
|
||||
|
||||
- [x] persisted, committed-live, and transient automation layers are named in code or clear read models
|
||||
- [x] final render-value precedence is explicit and covered by tests
|
||||
- [x] `RenderStateComposer` or its replacement consumes a layered input contract
|
||||
- [x] reset/reload/preset behavior for transient overlays is centralized or clearly delegated
|
||||
- [x] OSC overlay settle/commit behavior is explicit, including persistence policy
|
||||
- [x] `RuntimeStore` remains durable-state focused and does not absorb transient automation policy
|
||||
- [ ] render-local temporal/feedback state remains separate from live parameter layering
|
||||
- [ ] subsystem docs and the architecture review reflect the final ownership model
|
||||
|
||||
## Open Questions
|
||||
|
||||
- Should committed live state remain physically in `RuntimeStore` for now, or move to a `CommittedLiveState` collaborator?
|
||||
- Should transient OSC overlay updates become app-level typed events, or stay source-local through `RuntimeServiceLiveBridge`?
|
||||
- Should overlay commit persistence be global, ingress-specific, or parameter-definition-driven?
|
||||
- What compatibility rule should apply when shader reload preserves a control key but changes parameter shape?
|
||||
- Should preset load clear all transient automation, or only automation that no longer maps to the loaded stack?
|
||||
- Should UI slider drags use the committed-live layer directly, or a short-lived transient layer that commits on release?
|
||||
|
||||
## Short Version
|
||||
|
||||
Phase 5 should make live values boring and explicit.
|
||||
|
||||
Persisted state is durable truth. Committed live state is current-session/operator truth. Transient automation is high-rate overlay truth. Render consumes the composed result, and each layer has clear ownership, lifetime, persistence behavior, and reset/reload rules.
|
||||
301
docs/PHASE_6_BACKGROUND_PERSISTENCE_DESIGN.md
Normal file
301
docs/PHASE_6_BACKGROUND_PERSISTENCE_DESIGN.md
Normal file
@@ -0,0 +1,301 @@
|
||||
# Phase 6 Design: Background Persistence
|
||||
|
||||
This document expands Phase 6 of [ARCHITECTURE_RESILIENCE_REVIEW.md](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/docs/ARCHITECTURE_RESILIENCE_REVIEW.md) into a concrete design target.
|
||||
|
||||
Phases 1-5 separate durable state, coordination policy, render-facing snapshots, render-thread ownership, and live-state layering. Phase 6 should make disk persistence a background snapshot-writing concern instead of a synchronous side effect of mutations.
|
||||
|
||||
## Status
|
||||
|
||||
- Phase 6 design package: proposed.
|
||||
- Phase 6 implementation: not started.
|
||||
- Current alignment: `RuntimeStore` owns durable state and serialization, while `RuntimeCoordinator` already publishes explicit persistence-request outcomes for persisted mutations. The remaining issue is that actual disk writes are still synchronous store work rather than queued, debounced, atomic background writes.
|
||||
|
||||
Current persistence footholds:
|
||||
|
||||
- `RuntimeStore` owns persistent runtime-state serialization and stack preset serialization.
|
||||
- `LayerStackStore` owns durable layer and parameter state.
|
||||
- `RuntimeCoordinatorResult::persistenceRequested` exists as an explicit mutation outcome.
|
||||
- `RuntimeEventType::RuntimePersistenceRequested` exists as the event-level persistence request.
|
||||
- Phase 5 is expected to clarify which live-state mutations are durable, committed-live, or transient.
|
||||
|
||||
## Why Phase 6 Exists
|
||||
|
||||
Synchronous persistence is a poor fit for live software. A mutation that changes state should not also have to block on filesystem timing, antivirus scans, slow disks, or transient IO failures. The app needs persistence to be reliable and observable, but not timing-sensitive.
|
||||
|
||||
The resilience review calls this out because `SavePersistentState()` style behavior can create unnecessary stalls and makes recovery harder to reason about.
|
||||
|
||||
Phase 6 should turn persistence into:
|
||||
|
||||
- request
|
||||
- snapshot
|
||||
- background write
|
||||
- completion/failure observation
|
||||
|
||||
## Goals
|
||||
|
||||
Phase 6 should establish:
|
||||
|
||||
- a queued persistence request path
|
||||
- debounced/coalesced durable-state snapshot writes
|
||||
- atomic file replacement for runtime-state saves where practical
|
||||
- structured completion/failure reporting
|
||||
- clear separation between state mutation and disk flush
|
||||
- deterministic shutdown flushing policy
|
||||
- tests for coalescing, snapshot selection, write failure, and shutdown behavior without rendering or DeckLink
|
||||
|
||||
## Non-Goals
|
||||
|
||||
Phase 6 should not require:
|
||||
|
||||
- changing live-state layering rules
|
||||
- changing DeckLink/backend lifecycle
|
||||
- replacing stack preset semantics wholesale
|
||||
- adding cloud sync or external storage
|
||||
- building an unlimited historical state archive
|
||||
- making every write async immediately if a narrow compatibility path still needs a synchronous result
|
||||
|
||||
## Target Model
|
||||
|
||||
Phase 6 should make persistence a small pipeline:
|
||||
|
||||
```text
|
||||
RuntimeCoordinator accepts mutation
|
||||
-> publishes/returns persistence request
|
||||
-> PersistenceWriter captures a durable snapshot
|
||||
-> background worker debounces/coalesces writes
|
||||
-> atomic write commits file
|
||||
-> HealthTelemetry/runtime event records success or failure
|
||||
```
|
||||
|
||||
The key rule is:
|
||||
|
||||
- `RuntimeStore` owns durable state and serialization
|
||||
- `PersistenceWriter` owns when and how snapshots are written
|
||||
- `RuntimeCoordinator` owns whether a mutation requests persistence
|
||||
|
||||
## Proposed Collaborators
|
||||
|
||||
### `PersistenceWriter`
|
||||
|
||||
Owns the worker thread, queue, debounce timer, and write execution.
|
||||
|
||||
Responsibilities:
|
||||
|
||||
- accept persistence requests
|
||||
- coalesce repeated runtime-state writes
|
||||
- request/build a durable snapshot from `RuntimeStore`
|
||||
- write to a temporary file and atomically replace the target
|
||||
- report success/failure observations
|
||||
- flush on shutdown according to policy
|
||||
|
||||
Non-responsibilities:
|
||||
|
||||
- deciding mutation validity
|
||||
- owning durable in-memory state
|
||||
- composing render snapshots
|
||||
- blocking render/backend timing paths
|
||||
|
||||
### `PersistenceSnapshot`
|
||||
|
||||
Immutable write input captured from durable state.
|
||||
|
||||
Responsibilities:
|
||||
|
||||
- contain serialized runtime-state text or structured data ready to serialize
|
||||
- identify target path and snapshot generation
|
||||
- preserve enough metadata for completion/failure diagnostics
|
||||
|
||||
Non-responsibilities:
|
||||
|
||||
- mutation policy
|
||||
- file IO
|
||||
|
||||
### `PersistenceRequest`
|
||||
|
||||
Small request object or event payload.
|
||||
|
||||
Expected fields:
|
||||
|
||||
- reason/action name
|
||||
- target kind: runtime state, preset, config if later needed
|
||||
- optional debounce key
|
||||
- force/flush flag for explicit save operations
|
||||
- generation or sequence
|
||||
|
||||
## Write Policy
|
||||
|
||||
### Runtime State
|
||||
|
||||
Default policy:
|
||||
|
||||
- coalesce repeated requests
|
||||
- debounce short bursts
|
||||
- write newest snapshot
|
||||
- report failures without blocking render/control paths
|
||||
|
||||
### Stack Presets
|
||||
|
||||
Preset save is more operator-explicit than routine runtime-state persistence.
|
||||
|
||||
Initial policy options:
|
||||
|
||||
- keep preset save synchronous while runtime-state persistence becomes async
|
||||
- or route preset writes through the same worker with a completion result for the caller
|
||||
|
||||
Conservative Phase 6 default:
|
||||
|
||||
- background runtime-state persistence first
|
||||
- leave preset save/load synchronous unless the implementation has a clean completion path
|
||||
|
||||
### Shutdown
|
||||
|
||||
Shutdown should explicitly decide:
|
||||
|
||||
- flush latest pending snapshot before exit
|
||||
- skip flush if no pending durable change exists
|
||||
- report/write failure if flush fails
|
||||
- avoid indefinite hang on shutdown
|
||||
|
||||
## Atomicity And Failure Handling
|
||||
|
||||
Runtime-state writes should prefer:
|
||||
|
||||
1. serialize snapshot content in memory
|
||||
2. write to `target.tmp`
|
||||
3. flush/close file
|
||||
4. replace target atomically where platform support allows
|
||||
5. retain or report backup/failure context if replacement fails
|
||||
|
||||
Failures should not silently disappear. They should publish:
|
||||
|
||||
- persistence target
|
||||
- reason/action
|
||||
- error message
|
||||
- whether a newer request is pending
|
||||
- whether the app is still running with unsaved changes
|
||||
|
||||
## Migration Plan
|
||||
|
||||
### Step 1. Name Persistence Requests
|
||||
|
||||
Make request types and event payloads explicit enough that callers stop thinking in terms of direct disk writes.
|
||||
|
||||
Initial target:
|
||||
|
||||
- keep existing coordinator persistence decisions
|
||||
- introduce a `PersistenceRequest`/`PersistenceSnapshot` shape
|
||||
- document which requests are debounceable
|
||||
|
||||
### Step 2. Extract Snapshot Writing From `RuntimeStore`
|
||||
|
||||
Move file-write mechanics behind a helper while keeping serialization ownership in `RuntimeStore`.
|
||||
|
||||
Initial target:
|
||||
|
||||
- `RuntimeStore` can build serialized runtime-state snapshots
|
||||
- `PersistenceWriter` writes the snapshot
|
||||
- existing synchronous save path can call through the writer/helper during transition
|
||||
|
||||
### Step 3. Add Debounced Background Worker
|
||||
|
||||
Introduce a worker thread or queued task owner.
|
||||
|
||||
Initial target:
|
||||
|
||||
- repeated runtime-state requests coalesce
|
||||
- worker writes only latest pending snapshot
|
||||
- tests cover coalescing without filesystem where possible
|
||||
|
||||
### Step 4. Add Atomic Write And Failure Reporting
|
||||
|
||||
Make disk writes safer and observable.
|
||||
|
||||
Initial target:
|
||||
|
||||
- temp-file then replace
|
||||
- failure returned/published with structured reason
|
||||
- `HealthTelemetry` receives persistence warning state
|
||||
|
||||
### Step 5. Wire Coordinator/Event Requests To Writer
|
||||
|
||||
Route `RuntimePersistenceRequested` or coordinator persistence outcomes into the writer.
|
||||
|
||||
Initial target:
|
||||
|
||||
- accepted durable mutations request persistence
|
||||
- transient-only mutations do not
|
||||
- runtime reload/preset policies remain explicit
|
||||
|
||||
### Step 6. Define Shutdown Flush
|
||||
|
||||
Make app shutdown persistence behavior deterministic.
|
||||
|
||||
Initial target:
|
||||
|
||||
- stop accepting new requests
|
||||
- flush latest pending snapshot with bounded wait
|
||||
- report failure if flush fails
|
||||
|
||||
## Testing Strategy
|
||||
|
||||
Recommended tests:
|
||||
|
||||
- repeated persistence requests coalesce into one write
|
||||
- newest snapshot wins after multiple mutations
|
||||
- transient-only mutation does not request persistence
|
||||
- write failure records an error and keeps unsaved state visible
|
||||
- shutdown flush writes pending snapshot
|
||||
- shutdown with no pending request does not write
|
||||
- preset save path remains explicit
|
||||
- temp-file replacement success/failure is handled
|
||||
|
||||
Useful homes:
|
||||
|
||||
- `RuntimeSubsystemTests` for coordinator persistence outcomes
|
||||
- a new `PersistenceWriterTests` target for worker/coalescing/write policy
|
||||
- filesystem tests using a temporary directory for atomic write behavior
|
||||
|
||||
## Risks
|
||||
|
||||
### Data Loss Risk
|
||||
|
||||
Debouncing introduces a window where in-memory state is newer than disk. Shutdown flush and unsaved-state telemetry are the guardrails.
|
||||
|
||||
### Complexity Risk
|
||||
|
||||
A persistence worker can become a hidden second store if it owns mutable truth. It should own snapshots and write policy only.
|
||||
|
||||
### Blocking Shutdown Risk
|
||||
|
||||
Flushing forever on shutdown is not acceptable. Use bounded waits and visible failure reporting.
|
||||
|
||||
### Preset Semantics Risk
|
||||
|
||||
Operator-triggered preset save often feels like it should complete before reporting success. Keep preset behavior explicit rather than silently changing it.
|
||||
|
||||
## Phase 6 Exit Criteria
|
||||
|
||||
Phase 6 can be considered complete once the project can say:
|
||||
|
||||
- [ ] durable mutations enqueue persistence instead of directly writing from mutation paths
|
||||
- [ ] runtime-state writes are debounced/coalesced
|
||||
- [ ] writes use temp-file/replace or equivalent atomic policy
|
||||
- [ ] persistence failures are reported through structured health/events
|
||||
- [ ] transient/live-only mutations do not request persistence
|
||||
- [ ] shutdown flush behavior is explicit and tested
|
||||
- [ ] `RuntimeStore` remains durable-state/serialization owner, not worker policy owner
|
||||
- [ ] persistence behavior has focused non-render tests
|
||||
|
||||
## Open Questions
|
||||
|
||||
- Should preset save remain synchronous, or move behind a completion-based async request?
|
||||
- What debounce interval is appropriate for routine runtime-state writes?
|
||||
- Should failed persistence retry automatically, or wait for the next mutation/request?
|
||||
- Should the app expose "unsaved changes" in the UI/health snapshot?
|
||||
- Should runtime config writes share this worker, or stay separate?
|
||||
|
||||
## Short Version
|
||||
|
||||
Phase 6 should make persistence boring, safe, and off the hot path.
|
||||
|
||||
Mutations update in-memory durable state. Persistence requests are queued and coalesced. A background writer saves atomic snapshots and reports failures. Render, backend callbacks, and control ingress should not pay filesystem costs.
|
||||
333
docs/PHASE_7_BACKEND_LIFECYCLE_PLAYOUT_DESIGN.md
Normal file
333
docs/PHASE_7_BACKEND_LIFECYCLE_PLAYOUT_DESIGN.md
Normal file
@@ -0,0 +1,333 @@
|
||||
# Phase 7 Design: Backend Lifecycle And Playout
|
||||
|
||||
This document expands Phase 7 of [ARCHITECTURE_RESILIENCE_REVIEW.md](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/docs/ARCHITECTURE_RESILIENCE_REVIEW.md) into a concrete design target.
|
||||
|
||||
Phase 4 made the render thread the sole owner of normal runtime GL work, but output timing is still callback-coupled: DeckLink completion callbacks synchronously request render-thread output production before scheduling the next hardware frame. Phase 7 should make backend lifecycle, buffer policy, playout headroom, and recovery explicit.
|
||||
|
||||
## Status
|
||||
|
||||
- Phase 7 design package: proposed.
|
||||
- Phase 7 implementation: not started.
|
||||
- Current alignment: `VideoBackend`, `VideoIODevice`, `DeckLinkSession`, and `VideoPlayoutScheduler` exist. Phase 4 removed callback-thread GL ownership, but the DeckLink completion path still waits for render-thread output production.
|
||||
|
||||
Current backend footholds:
|
||||
|
||||
- `VideoBackend` wraps device discovery/configuration, start/stop, input callback handling, output completion handling, and telemetry publication.
|
||||
- `DeckLinkSession` owns DeckLink device handles, frame pool creation, preroll, keyer configuration, and scheduled playback.
|
||||
- `VideoPlayoutScheduler` owns basic schedule time generation and simple late/drop skip-ahead behavior.
|
||||
- `OpenGLVideoIOBridge` is the current adapter between `VideoBackend` and `RenderEngine`.
|
||||
- `HealthTelemetry` receives some signal, render, and pacing stats.
|
||||
|
||||
## Why Phase 7 Exists
|
||||
|
||||
The current output path works only while render/readback stays comfortably inside budget. A late render can make the callback late, which reduces device-side headroom, which makes the next callback more fragile.
|
||||
|
||||
The resilience review calls this the main remaining live-resilience risk after Phase 4:
|
||||
|
||||
- output playout is still effectively render-on-demand from the DeckLink completion callback
|
||||
- buffer pool size and preroll depth are not sourced from one policy
|
||||
- late/dropped recovery is a fixed skip rule
|
||||
- backend lifecycle is imperative rather than represented as explicit states
|
||||
|
||||
Phase 7 should separate hardware timing from render production.
|
||||
|
||||
## Goals
|
||||
|
||||
Phase 7 should establish:
|
||||
|
||||
- explicit backend lifecycle states and allowed transitions
|
||||
- one playout policy for frame pool size, preroll, headroom, and underrun behavior
|
||||
- a bounded producer/consumer output queue between render and DeckLink scheduling
|
||||
- lightweight DeckLink callbacks that dequeue/schedule/account rather than render
|
||||
- measured recovery from late/dropped frames
|
||||
- structured backend health reporting
|
||||
- tests for scheduler, queue, lifecycle, and underrun policy without DeckLink hardware
|
||||
|
||||
## Non-Goals
|
||||
|
||||
Phase 7 should not require:
|
||||
|
||||
- a new renderer
|
||||
- changing shader/state composition
|
||||
- replacing DeckLink support with multiple backends
|
||||
- full telemetry UI redesign
|
||||
- removing every synchronous API immediately
|
||||
- perfect adaptive latency policy in the first pass
|
||||
|
||||
## Target Timing Model
|
||||
|
||||
The target model is producer/consumer playout:
|
||||
|
||||
```text
|
||||
RenderEngine/render scheduler produces completed output frames
|
||||
-> bounded ready-frame queue
|
||||
-> VideoBackend consumes ready frames
|
||||
-> DeckLink callback schedules already-prepared frames
|
||||
```
|
||||
|
||||
The callback should not wait for rendering. It should:
|
||||
|
||||
- record completion result
|
||||
- recycle/release completed buffers
|
||||
- dequeue a ready frame or apply underrun policy
|
||||
- schedule the next frame
|
||||
- publish backend timing/health observations
|
||||
|
||||
## Target Lifecycle Model
|
||||
|
||||
Suggested backend 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`
|
||||
|
||||
The exact enum can change, but the lifecycle should become observable and testable.
|
||||
|
||||
## Proposed Collaborators
|
||||
|
||||
### `VideoBackendStateMachine`
|
||||
|
||||
Pure or mostly pure lifecycle transition helper.
|
||||
|
||||
Responsibilities:
|
||||
|
||||
- validate state transitions
|
||||
- produce transition observations
|
||||
- track failure reasons
|
||||
- keep start/stop/recovery behavior auditable
|
||||
|
||||
Non-responsibilities:
|
||||
|
||||
- DeckLink API calls
|
||||
- rendering
|
||||
- persistence
|
||||
|
||||
### `PlayoutPolicy`
|
||||
|
||||
Policy object for queue and timing behavior.
|
||||
|
||||
Expected fields:
|
||||
|
||||
- target preroll frames
|
||||
- maximum ready frames
|
||||
- minimum spare device buffers
|
||||
- underrun behavior
|
||||
- maximum catch-up frames
|
||||
- adaptive headroom enabled/disabled
|
||||
|
||||
### `RenderOutputQueue`
|
||||
|
||||
Bounded queue or ring for completed output frames.
|
||||
|
||||
Responsibilities:
|
||||
|
||||
- accept completed render outputs
|
||||
- expose ready frames for scheduling
|
||||
- track depth, drops, stale reuse, and underruns
|
||||
- keep ownership/lifetime clear between render and backend
|
||||
|
||||
### `OutputFramePool`
|
||||
|
||||
Backend-owned device buffer pool.
|
||||
|
||||
Responsibilities:
|
||||
|
||||
- own DeckLink mutable frames
|
||||
- expose available buffers for render/readback or scheduling
|
||||
- recycle completed frames
|
||||
- report spare-buffer depth
|
||||
|
||||
### `PlayoutController`
|
||||
|
||||
Coordinates policy, ready frames, device schedule times, and completion accounting.
|
||||
|
||||
Responsibilities:
|
||||
|
||||
- preroll frames
|
||||
- schedule next frame
|
||||
- handle late/drop/completed/flushed results
|
||||
- apply underrun policy
|
||||
- publish timing state
|
||||
|
||||
## Output Queue Policy
|
||||
|
||||
The initial output queue should be small and bounded.
|
||||
|
||||
Candidate defaults:
|
||||
|
||||
- target ready frames: 2-3
|
||||
- max ready frames: 3-5
|
||||
- underrun: reuse last completed frame if available, otherwise black
|
||||
- late/drop: increase degraded counters and optionally increase headroom within limits
|
||||
|
||||
The exact numbers should be measured, but the policy should live in one place instead of being split across constants.
|
||||
|
||||
## Underrun Policy
|
||||
|
||||
When no fresh rendered frame is available, options are:
|
||||
|
||||
1. reuse newest completed frame
|
||||
2. reuse last scheduled frame
|
||||
3. schedule black/degraded frame
|
||||
4. skip/catch up schedule time
|
||||
|
||||
Phase 7 should pick one default and make it visible in telemetry. Reusing the newest completed frame is often the best first policy for live visual continuity, but key/fill behavior may require careful testing.
|
||||
|
||||
## Migration Plan
|
||||
|
||||
### Step 1. Name Lifecycle States
|
||||
|
||||
Introduce backend state enum and transition reporting without changing scheduling behavior much.
|
||||
|
||||
Initial target:
|
||||
|
||||
- state changes are explicit
|
||||
- invalid transitions are detectable
|
||||
- tests cover allowed transitions
|
||||
|
||||
### Step 2. Create Playout Policy Object
|
||||
|
||||
Unify fixed constants and scheduler assumptions.
|
||||
|
||||
Initial target:
|
||||
|
||||
- frame pool size derives from policy
|
||||
- preroll count derives from policy
|
||||
- late/drop recovery reads policy
|
||||
|
||||
### Step 3. Add Ready Output Queue
|
||||
|
||||
Introduce a bounded queue for completed output frames.
|
||||
|
||||
Initial target:
|
||||
|
||||
- pure queue tests
|
||||
- explicit depth/underrun metrics
|
||||
- no DeckLink dependency in queue tests
|
||||
|
||||
### Step 4. Move Callback Toward Dequeue/Schedule
|
||||
|
||||
Stop producing frames directly in the completion callback path.
|
||||
|
||||
Transitional target:
|
||||
|
||||
- callback wakes/schedules a backend worker
|
||||
- worker consumes ready frames
|
||||
|
||||
Final target:
|
||||
|
||||
- callback only records, recycles, dequeues, schedules
|
||||
|
||||
### Step 5. Make Render Produce Ahead
|
||||
|
||||
Teach render/output code to keep the ready queue filled to target headroom.
|
||||
|
||||
Initial target:
|
||||
|
||||
- render thread produces on demand until queue has target depth
|
||||
- callback does not synchronously wait for fresh render
|
||||
- stale/black fallback is explicit on underrun
|
||||
|
||||
### Step 6. Replace Fixed Late/Drop Recovery
|
||||
|
||||
Replace fixed `+2` schedule-index recovery with measured lag/headroom accounting.
|
||||
|
||||
Initial target:
|
||||
|
||||
- track scheduled index, completed index, queue depth, late streak, drop streak
|
||||
- recovery decisions use measured lag
|
||||
|
||||
### Step 7. Route Backend Health Structurally
|
||||
|
||||
Publish backend lifecycle, queue depth, underrun, late/drop, and degraded-state observations through `HealthTelemetry`.
|
||||
|
||||
## Testing Strategy
|
||||
|
||||
Recommended tests:
|
||||
|
||||
- allowed lifecycle transitions pass
|
||||
- invalid lifecycle transitions fail
|
||||
- playout policy derives frame pool/preroll sizes consistently
|
||||
- output queue preserves ordering
|
||||
- bounded output queue rejects/drops according to policy
|
||||
- underrun reuses last frame or black according to policy
|
||||
- late/drop accounting updates degraded state
|
||||
- scheduler catch-up uses measured lag, not fixed skip
|
||||
- stop drains/recycles device-frame ownership in pure fakes
|
||||
|
||||
Useful homes:
|
||||
|
||||
- `VideoPlayoutSchedulerTests` for scheduler evolution
|
||||
- `VideoIODeviceFakeTests` for fake backend lifecycle
|
||||
- a new `VideoBackendStateMachineTests`
|
||||
- a new `RenderOutputQueueTests`
|
||||
|
||||
## Risks
|
||||
|
||||
### Latency Risk
|
||||
|
||||
More headroom means more latency. Phase 7 should make latency a visible policy choice.
|
||||
|
||||
### Buffer Lifetime Risk
|
||||
|
||||
Render and backend will share ownership boundaries around output buffers. Frame ownership must be explicit to avoid reuse while hardware still owns a frame.
|
||||
|
||||
### Underrun Policy Risk
|
||||
|
||||
Reusing stale frames can be visually acceptable, but wrong key/fill behavior may be worse than black. Test with real output.
|
||||
|
||||
### Callback Thread Risk
|
||||
|
||||
Even after decoupling render, callback work must stay small and bounded.
|
||||
|
||||
### Scope Risk
|
||||
|
||||
Backend lifecycle and playout queue are related, but either can grow large. Implement in small, testable slices.
|
||||
|
||||
## Phase 7 Exit Criteria
|
||||
|
||||
Phase 7 can be considered complete once the project can say:
|
||||
|
||||
- [ ] backend lifecycle states and transitions are explicit
|
||||
- [ ] playout policy owns preroll, pool size, headroom, and underrun behavior
|
||||
- [ ] output callbacks no longer synchronously wait for render production
|
||||
- [ ] render produces completed output frames into a bounded queue
|
||||
- [ ] underrun behavior is explicit and observable
|
||||
- [ ] late/drop recovery is measured rather than fixed skip-only
|
||||
- [ ] backend health reports lifecycle, queue, underrun, late, and dropped state
|
||||
- [ ] queue/lifecycle/scheduler behavior has non-DeckLink tests
|
||||
|
||||
## Open Questions
|
||||
|
||||
- What should the default ready-frame depth be at 30fps and 60fps?
|
||||
- Should underrun reuse last completed, last scheduled, or black?
|
||||
- Should output queue depth be user-configurable?
|
||||
- Should render cadence be driven by backend demand, a timer, or queue-fill pressure?
|
||||
- How should external keying influence stale-frame/black fallback?
|
||||
- Should input and output lifecycle states be separate endpoints under one backend shell?
|
||||
|
||||
## Short Version
|
||||
|
||||
Phase 7 should stop making DeckLink callbacks wait for render.
|
||||
|
||||
Render produces ahead into a bounded queue. The backend consumes ready frames according to explicit lifecycle and playout policy. Queue depth, underruns, late frames, dropped frames, and degraded states become measured and visible.
|
||||
367
docs/PHASE_8_HEALTH_TELEMETRY_DESIGN.md
Normal file
367
docs/PHASE_8_HEALTH_TELEMETRY_DESIGN.md
Normal file
@@ -0,0 +1,367 @@
|
||||
# Phase 8 Design: Health, Telemetry, And Operational Reporting
|
||||
|
||||
This document expands Phase 8 of [ARCHITECTURE_RESILIENCE_REVIEW.md](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/docs/ARCHITECTURE_RESILIENCE_REVIEW.md) into a concrete design target.
|
||||
|
||||
Earlier phases clarify subsystem ownership, state layering, render-thread ownership, persistence, and backend lifecycle. Phase 8 should make operational visibility match that architecture: structured health state, timing, counters, warnings, and logs should flow through one telemetry subsystem instead of scattered debug strings and ad hoc status fields.
|
||||
|
||||
## Status
|
||||
|
||||
- Phase 8 design package: proposed.
|
||||
- Phase 8 implementation: not started.
|
||||
- Current alignment: `HealthTelemetry` exists and already receives some render, signal, video IO, and pacing observations. Runtime events also carry some timing and backend observations. The remaining work is to make health/telemetry structured, comprehensive, bounded, and operator-facing.
|
||||
|
||||
Current telemetry footholds:
|
||||
|
||||
- `HealthTelemetry` owns basic signal, performance, frame pacing, and video IO status reporting.
|
||||
- `RuntimeEventDispatcher` publishes typed observations such as timing samples and backend state changes.
|
||||
- `RuntimeStatePresenter` includes some health/performance fields in runtime-state output.
|
||||
- Render and backend paths already collect some timing and late/drop counts.
|
||||
|
||||
## Why Phase 8 Exists
|
||||
|
||||
The app can detect many problems, but operational visibility is still fragmented:
|
||||
|
||||
- some failures show modal dialogs
|
||||
- some warnings go only to `OutputDebugStringA`
|
||||
- some timing lives in health telemetry
|
||||
- some observations are runtime events
|
||||
- UI-facing state combines operational state with runtime state
|
||||
- repeated warnings are not uniformly deduplicated, classified, or summarized
|
||||
|
||||
Live software needs to answer:
|
||||
|
||||
- what is healthy right now?
|
||||
- what is degraded but still running?
|
||||
- what recently failed?
|
||||
- which subsystem is under timing pressure?
|
||||
- what should an operator see versus what should an engineer debug?
|
||||
|
||||
## Goals
|
||||
|
||||
Phase 8 should establish:
|
||||
|
||||
- structured log entries with subsystem, severity, category, timestamp, and message
|
||||
- subsystem-scoped health states
|
||||
- bounded recent warning/error history
|
||||
- timing samples, counters, and gauges for render/control/backend/persistence
|
||||
- stable health snapshots for UI/diagnostics
|
||||
- direct debug-output paths wrapped by structured telemetry
|
||||
- low-overhead reporting from render and callback paths
|
||||
- tests for severity, deduplication, counters, snapshots, and bounded retention
|
||||
|
||||
## Non-Goals
|
||||
|
||||
Phase 8 should not require:
|
||||
|
||||
- a cloud telemetry service
|
||||
- external metrics database
|
||||
- a full UI redesign
|
||||
- automatic recovery policy owned by telemetry
|
||||
- unbounded logs or time-series storage
|
||||
- replacing every `MessageBoxA` on day one
|
||||
|
||||
Telemetry observes and reports. It does not become the control plane.
|
||||
|
||||
## Target Model
|
||||
|
||||
Suggested core model:
|
||||
|
||||
- `TelemetrySubsystem`
|
||||
- `TelemetrySeverity`
|
||||
- `TelemetryLogEntry`
|
||||
- `TelemetryWarningRecord`
|
||||
- `TelemetryCounter`
|
||||
- `TelemetryGauge`
|
||||
- `TelemetryTimingSample`
|
||||
- `SubsystemHealthState`
|
||||
- `HealthSnapshot`
|
||||
|
||||
Important distinction:
|
||||
|
||||
- raw observations are append/update operations
|
||||
- health snapshots are derived read models
|
||||
|
||||
## Health Domains
|
||||
|
||||
At minimum:
|
||||
|
||||
- `ApplicationShell`
|
||||
- `RuntimeStore`
|
||||
- `RuntimeCoordinator`
|
||||
- `RuntimeSnapshotProvider`
|
||||
- `ControlServices`
|
||||
- `RenderEngine`
|
||||
- `VideoBackend`
|
||||
- `Persistence`
|
||||
|
||||
Suggested states:
|
||||
|
||||
- `Healthy`
|
||||
- `Warning`
|
||||
- `Degraded`
|
||||
- `Error`
|
||||
- `Unavailable`
|
||||
|
||||
The overall app health should be derived from subsystem states.
|
||||
|
||||
## Proposed Interfaces
|
||||
|
||||
### Write Interface
|
||||
|
||||
Target operations:
|
||||
|
||||
- `AppendLog(...)`
|
||||
- `RaiseWarning(...)`
|
||||
- `ClearWarning(...)`
|
||||
- `RecordCounterDelta(...)`
|
||||
- `RecordGauge(...)`
|
||||
- `RecordTimingSample(...)`
|
||||
- `ReportSubsystemState(...)`
|
||||
|
||||
Hot-path producers should be able to record observations cheaply and return.
|
||||
|
||||
### Read Interface
|
||||
|
||||
Target operations:
|
||||
|
||||
- `BuildHealthSnapshot()`
|
||||
- `GetSubsystemHealth(...)`
|
||||
- `GetActiveWarnings()`
|
||||
- `GetRecentLogs(...)`
|
||||
- `GetTimingSummary(...)`
|
||||
|
||||
UI/control services should consume snapshots, not scrape subsystem internals.
|
||||
|
||||
## Producer Expectations
|
||||
|
||||
### `RenderEngine`
|
||||
|
||||
Expected observations:
|
||||
|
||||
- render frame duration
|
||||
- input upload duration/count/drop/coalescing
|
||||
- output request latency
|
||||
- readback duration
|
||||
- synchronous readback fallback count
|
||||
- preview present cost/skips
|
||||
- wrong-thread diagnostics
|
||||
|
||||
### `VideoBackend`
|
||||
|
||||
Expected observations:
|
||||
|
||||
- lifecycle state
|
||||
- playout queue depth
|
||||
- output underruns
|
||||
- late/dropped/flushed/completed counts
|
||||
- input signal state
|
||||
- output model/mode status
|
||||
- spare buffer depth
|
||||
|
||||
### `ControlServices`
|
||||
|
||||
Expected observations:
|
||||
|
||||
- OSC decode errors
|
||||
- control request failures
|
||||
- websocket broadcast failures
|
||||
- ingress queue depth
|
||||
- file-watch/reload events
|
||||
- service start/stop state
|
||||
|
||||
### `RuntimeCoordinator`
|
||||
|
||||
Expected observations:
|
||||
|
||||
- rejected mutation count and reasons
|
||||
- reload requests
|
||||
- preset failures
|
||||
- transient-state invalidations
|
||||
- persistence request publication
|
||||
|
||||
### `RuntimeSnapshotProvider`
|
||||
|
||||
Expected observations:
|
||||
|
||||
- snapshot publish duration
|
||||
- snapshot version churn
|
||||
- stale snapshot/fallback behavior
|
||||
- publish failures
|
||||
|
||||
### `PersistenceWriter`
|
||||
|
||||
Expected observations:
|
||||
|
||||
- pending write count
|
||||
- coalesced write count
|
||||
- write duration
|
||||
- write failure
|
||||
- unsaved durable changes
|
||||
- shutdown flush result
|
||||
|
||||
## Logging Policy
|
||||
|
||||
Direct string logging can remain as an output sink, but not as the source of truth.
|
||||
|
||||
Target flow:
|
||||
|
||||
```text
|
||||
subsystem reports structured warning/log
|
||||
-> HealthTelemetry stores bounded structured entry
|
||||
-> optional debug sink prints text
|
||||
-> UI/diagnostics reads health snapshot
|
||||
```
|
||||
|
||||
Repeated warnings should be deduplicated by key while preserving counts and last-seen timestamps.
|
||||
|
||||
## Snapshot Contract
|
||||
|
||||
`HealthSnapshot` should answer:
|
||||
|
||||
- overall health
|
||||
- subsystem health states
|
||||
- active warnings
|
||||
- recent important logs
|
||||
- key counters
|
||||
- key timing summaries
|
||||
- degraded-state reasons
|
||||
|
||||
The snapshot should avoid copying durable runtime truth. Runtime state and health state can be published together by `ControlServices`, but they should remain separate read models.
|
||||
|
||||
## Migration Plan
|
||||
|
||||
### Step 1. Expand Health Model Types
|
||||
|
||||
Add structured subsystem/severity/category types and snapshot models.
|
||||
|
||||
Initial target:
|
||||
|
||||
- keep existing health fields
|
||||
- add structured warning/log/counter/gauge containers
|
||||
- add tests for bounded retention and deduplication
|
||||
|
||||
### Step 2. Wrap Direct Warning Paths
|
||||
|
||||
Route common direct logs through telemetry first.
|
||||
|
||||
Initial candidates:
|
||||
|
||||
- backend fallback warnings
|
||||
- screenshot write failures
|
||||
- OSC decode/dispatch failures
|
||||
- render-thread request failures
|
||||
|
||||
### Step 3. Add Subsystem Health States
|
||||
|
||||
Let subsystems report state transitions.
|
||||
|
||||
Initial target:
|
||||
|
||||
- `RenderEngine`: healthy/degraded on render-thread request failures
|
||||
- `VideoBackend`: configured/running/degraded/no-input/dropping
|
||||
- `ControlServices`: running/degraded/stopped
|
||||
- `Persistence`: clean/pending/error
|
||||
|
||||
### Step 4. Split Timing Into Named Metrics
|
||||
|
||||
Move from broad timing fields to named samples/gauges.
|
||||
|
||||
Initial target:
|
||||
|
||||
- render duration
|
||||
- readback duration/fallback count
|
||||
- output request latency
|
||||
- playout completion interval
|
||||
- event queue depth
|
||||
- persistence write duration
|
||||
|
||||
### Step 5. Publish Health Snapshot
|
||||
|
||||
Expose `HealthTelemetry` snapshot through control/runtime presentation.
|
||||
|
||||
Initial target:
|
||||
|
||||
- UI can distinguish runtime state from operational health
|
||||
- active warnings are visible
|
||||
- recent degraded reasons are visible
|
||||
|
||||
### Step 6. Add Operational Tests
|
||||
|
||||
Cover:
|
||||
|
||||
- warning raise/clear
|
||||
- repeated warning coalescing
|
||||
- counter/gauge updates
|
||||
- health derivation
|
||||
- bounded log retention
|
||||
- snapshot stability
|
||||
|
||||
## Testing Strategy
|
||||
|
||||
Recommended tests:
|
||||
|
||||
- warning raised appears in active warnings
|
||||
- warning clear removes active warning but preserves history
|
||||
- repeated warning increments count and updates last-seen time
|
||||
- bounded log keeps newest entries
|
||||
- subsystem `Error` makes overall health `Error`
|
||||
- subsystem `Degraded` makes overall health degraded if no error exists
|
||||
- timing sample updates summary
|
||||
- counter delta accumulates
|
||||
- health snapshot is read-only/stable
|
||||
|
||||
Useful homes:
|
||||
|
||||
- `HealthTelemetryTests`
|
||||
- `RuntimeEventTypeTests` for observation event payloads
|
||||
- future integration tests for control-service health publication
|
||||
|
||||
## Risks
|
||||
|
||||
### Telemetry Becomes Behavior
|
||||
|
||||
Telemetry must not become the hidden way subsystems command each other. It reports. Subsystems own mitigation.
|
||||
|
||||
### Too Much Hot-Path Cost
|
||||
|
||||
Render and callback paths need cheap writes. Use bounded structures and avoid expensive formatting on hot paths.
|
||||
|
||||
### String-Only Logging
|
||||
|
||||
Centralizing strings is not enough. Severity, subsystem, category, and structured fields should be first-class.
|
||||
|
||||
### Snapshot Bloat
|
||||
|
||||
Health snapshots should summarize operational state, not duplicate full runtime/project state.
|
||||
|
||||
### Alert Noise
|
||||
|
||||
Without deduplication and severity discipline, operator-facing health can become noisy and ignored.
|
||||
|
||||
## Phase 8 Exit Criteria
|
||||
|
||||
Phase 8 can be considered complete once the project can say:
|
||||
|
||||
- [ ] major subsystems publish structured health/telemetry observations
|
||||
- [ ] active warnings and recent logs are structured and bounded
|
||||
- [ ] subsystem health states roll up to an overall health state
|
||||
- [ ] render/backend/control/persistence timing metrics are named and visible
|
||||
- [ ] direct debug-string warning paths are wrapped or retired for major cases
|
||||
- [ ] UI/control diagnostics can consume a stable health snapshot
|
||||
- [ ] telemetry write paths are cheap enough for render/callback use
|
||||
- [ ] telemetry behavior has focused tests
|
||||
|
||||
## Open Questions
|
||||
|
||||
- Should debug output remain enabled by default as a telemetry sink?
|
||||
- How many recent logs/warnings should be retained in memory?
|
||||
- Should timing summaries store raw samples, rolling windows, or both?
|
||||
- Should warning thresholds be declared centrally or owned by each subsystem?
|
||||
- Should health snapshots be published with runtime state or on a separate endpoint/channel?
|
||||
- Should logs eventually be written to disk, and if so, through Phase 6 persistence infrastructure or a separate log sink?
|
||||
|
||||
## Short Version
|
||||
|
||||
Phase 8 should make the app diagnosable.
|
||||
|
||||
Subsystems report structured observations. `HealthTelemetry` records bounded logs, warnings, counters, gauges, timing, and subsystem states. UI and diagnostics consume stable health snapshots. Debug strings become a sink, not the source of truth.
|
||||
589
docs/subsystems/ControlServices.md
Normal file
589
docs/subsystems/ControlServices.md
Normal 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:
|
||||
|
||||
- legacy direct runtime polling dependency
|
||||
- deferred OSC commit behavior that has since moved behind coordinator-facing outcomes
|
||||
- any remaining direct state-broadcast decisions tied to runtime internals
|
||||
|
||||
### Current `ControlServer`
|
||||
|
||||
Should become primarily:
|
||||
|
||||
- HTTP ingress adapter
|
||||
- WebSocket session host
|
||||
- static asset/doc host if that remains embedded
|
||||
|
||||
The callback table in current code:
|
||||
|
||||
- [ControlServer.h](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/control/ControlServer.h:18)
|
||||
|
||||
is a useful migration aid, but long-term it should evolve from callback-per-action toward typed action emission.
|
||||
|
||||
### Current `OscServer`
|
||||
|
||||
Should remain transport-focused.
|
||||
|
||||
Its clean long-term responsibilities are:
|
||||
|
||||
- UDP socket lifecycle
|
||||
- OSC frame decode
|
||||
- syntactic route extraction
|
||||
- emitting decoded automation payloads into the `ControlServices` shell
|
||||
|
||||
It should not own any runtime state semantics beyond ingress decoding.
|
||||
|
||||
## Migration Plan
|
||||
|
||||
The safest migration is incremental.
|
||||
|
||||
### Step 1. Name The Boundary Explicitly
|
||||
|
||||
Create and use the `ControlServices` name in docs and future interfaces before moving all logic.
|
||||
|
||||
This document is part of that step.
|
||||
|
||||
### Step 2. Convert Callback Thinking Into Action Thinking
|
||||
|
||||
Without changing all runtime code at once, introduce typed action/event shapes for the major ingress paths.
|
||||
|
||||
The goal is for transports to emit actions, even if temporary adapters still call into existing code.
|
||||
|
||||
### Step 3. Extract Service Hosting From `OpenGLComposite`
|
||||
|
||||
`OpenGLComposite` currently owns `RuntimeServices` startup and consumption:
|
||||
|
||||
- [OpenGLComposite.cpp](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/gl/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 Runtime Mutation Dependency
|
||||
|
||||
Previous polling and deferred OSC commit work directly against runtime storage:
|
||||
|
||||
- [RuntimeServices.cpp](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/control/RuntimeServices.cpp:194)
|
||||
|
||||
That has been routed through coordinator-facing actions; later phases should replace the remaining polling shape with event-driven flows.
|
||||
|
||||
### Step 5. Split Out Observation Delivery
|
||||
|
||||
WebSocket outbound delivery can stay in `ControlServices`, but serialization ownership should move toward the owning subsystems so the service layer stops assembling authoritative state itself.
|
||||
|
||||
## Risks
|
||||
|
||||
### Risk 1. Recreating `RuntimeHost` Coupling Under A New Name
|
||||
|
||||
If `ControlServices` is allowed to keep direct knowledge of runtime mutation internals, it will become a renamed version of the same coupling problem.
|
||||
|
||||
Mitigation:
|
||||
|
||||
- keep the boundary strict
|
||||
- route mutations through coordinator interfaces
|
||||
- treat any direct runtime mutation calls as migration-only compatibility
|
||||
|
||||
### Risk 2. Service Queues Becoming Hidden State Authority
|
||||
|
||||
Latest-value OSC maps and reload debounce flags are appropriate here. Full committed runtime state is not.
|
||||
|
||||
Mitigation:
|
||||
|
||||
- define ingress-local versus authoritative state explicitly
|
||||
- bound queues
|
||||
- publish queue metrics into telemetry
|
||||
|
||||
### Risk 3. WebSocket Broadcast Path Reintroducing Heavy Synchronous Work
|
||||
|
||||
If `ControlServices` becomes the place where whole runtime state is rebuilt or serialized on every input, it will recreate timing stalls.
|
||||
|
||||
Mitigation:
|
||||
|
||||
- broadcast snapshots produced elsewhere
|
||||
- collapse redundant outbound requests
|
||||
- track serialization/broadcast timing in telemetry
|
||||
|
||||
### Risk 4. Polling Surviving Too Long As Architecture
|
||||
|
||||
Some polling may remain during migration, but it should not become the permanent contract.
|
||||
|
||||
Mitigation:
|
||||
|
||||
- isolate polling behind ingress interfaces
|
||||
- make replacement with event-driven flows a planned Phase 2/3 outcome
|
||||
|
||||
## Open Questions
|
||||
|
||||
- Should the embedded static UI/docs hosting stay inside `ControlServices`, or move to a thinner app-shell concern while control APIs remain in `ControlServices`?
|
||||
- Should outbound state for WebSocket clients be one combined payload or separate runtime and health channels?
|
||||
- How much route/key resolution should happen in `ControlServices` versus `RuntimeCoordinator`?
|
||||
- Should any deferred automation-settle delivery remain in `ControlServices`, or should all commit/settle policy move entirely into coordinator/render ownership once the live-state model is formalized?
|
||||
- When file watching is modernized, should reload classification live entirely in `ControlServices`, or should it emit a lower-level `FilesChanged` event and let `RuntimeCoordinator` decide reload semantics?
|
||||
- Will future non-OSC automation sources reuse the same `AutomationTargetReceived` path, or need source-specific typed actions for policy reasons?
|
||||
|
||||
## Short Version
|
||||
|
||||
`ControlServices` should become the app's clean ingress boundary:
|
||||
|
||||
- transport handling stays here
|
||||
- input normalization stays here
|
||||
- ingress-local buffering stays here
|
||||
- mutation policy does not
|
||||
- authoritative runtime state does not
|
||||
- render-local transient state does not
|
||||
|
||||
If later phases keep that line sharp, the app gains a control layer that is fast, testable, and timing-aware without becoming another shared state authority.
|
||||
644
docs/subsystems/HealthTelemetry.md
Normal file
644
docs/subsystems/HealthTelemetry.md
Normal 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.
|
||||
|
||||
Before the Phase 1 runtime split, those responsibilities were fragmented across `RuntimeHost` status setters, ad hoc `OutputDebugStringA` calls, callback-local warnings, and UI-facing runtime-state payloads. The result was that the app could often detect problems, but did not yet have one clear place that answered:
|
||||
|
||||
- what is healthy right now
|
||||
- what is degraded right now
|
||||
- what has recently gone wrong
|
||||
- which subsystem is under pressure
|
||||
- how timing behavior is trending over time
|
||||
|
||||
`HealthTelemetry` is the target boundary that should answer those questions.
|
||||
|
||||
## Why This Subsystem Exists
|
||||
|
||||
The codebase already contains meaningful health and timing signals, but some are still spread through unrelated ownership domains:
|
||||
|
||||
- previous `RuntimeHost` status fields stored signal and timing status:
|
||||
- `RuntimeHost.h`
|
||||
- `RuntimeHost.cpp`
|
||||
- `RuntimeHost.cpp`
|
||||
- `RuntimeHost.cpp`
|
||||
- render and bridge code historically reported timing by writing back into `RuntimeHost`:
|
||||
- [OpenGLRenderPipeline.cpp](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/gl/pipeline/OpenGLRenderPipeline.cpp:50)
|
||||
- [OpenGLVideoIOBridge.cpp](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/gl/pipeline/OpenGLVideoIOBridge.cpp:49)
|
||||
- backend warning paths still log directly:
|
||||
- [DeckLinkFrameTransfer.cpp](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/videoio/decklink/DeckLinkFrameTransfer.cpp:84)
|
||||
- [DeckLinkSession.cpp](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/videoio/decklink/DeckLinkSession.cpp:305)
|
||||
- control ingress failures still log directly:
|
||||
- [OscServer.cpp](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/control/OscServer.cpp:142)
|
||||
- [RuntimeServices.cpp](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/control/RuntimeServices.cpp:100)
|
||||
|
||||
This creates several recurring problems:
|
||||
|
||||
- health information shares storage and lock scope with runtime state
|
||||
- warnings are not consistently classified by subsystem or severity
|
||||
- timing data is hard to compare across render, control, and backend paths
|
||||
- UI connection state and operational state are too closely coupled
|
||||
- logging is mostly text-first instead of structured-first
|
||||
- recovery behavior is hard to audit because the app does not retain a coherent health snapshot
|
||||
|
||||
`HealthTelemetry` exists so timing and health concerns have one subsystem whose only job is observation and reporting, instead of drifting back into runtime storage, callback-local logging, or UI payload assembly.
|
||||
|
||||
## Design Goals
|
||||
|
||||
`HealthTelemetry` should optimize for:
|
||||
|
||||
- one authoritative home for operational visibility
|
||||
- structured health state per subsystem
|
||||
- timing and counter recording that does not require a UI to be connected
|
||||
- low-friction reporting from render, backend, coordinator, and services
|
||||
- explicit degraded-mode reporting instead of only raw text logs
|
||||
- support for live operator summaries and deeper engineering diagnostics
|
||||
- minimal risk of telemetry writes becoming a render or callback bottleneck
|
||||
|
||||
## Responsibilities
|
||||
|
||||
`HealthTelemetry` owns structured operational visibility.
|
||||
|
||||
Primary responsibilities:
|
||||
|
||||
- accept timing samples from major subsystems
|
||||
- accept counter deltas and point-in-time gauges
|
||||
- accept warning, error, and degraded-state transitions
|
||||
- collect subsystem-scoped health state
|
||||
- collect operator-visible summary state
|
||||
- collect structured log entries
|
||||
- build stable health snapshots for UI, diagnostics, and later persistence/export if desired
|
||||
- retain recent history needed for short-term troubleshooting
|
||||
- classify observations by subsystem, severity, and category
|
||||
|
||||
Secondary responsibilities that still fit here:
|
||||
|
||||
- smoothing or rolling-window summaries for timing metrics
|
||||
- mapping raw subsystem observations into operator-facing health summaries
|
||||
- deduplicating repeated warnings
|
||||
- tracking warning open/clear lifecycles
|
||||
- providing bounded in-memory history for recent logs and warning transitions
|
||||
|
||||
## Explicit Non-Responsibilities
|
||||
|
||||
`HealthTelemetry` should not become a behavior owner.
|
||||
|
||||
It does not own:
|
||||
|
||||
- layer stack truth
|
||||
- persistence policy
|
||||
- render scheduling
|
||||
- DeckLink scheduling
|
||||
- OSC buffering or routing
|
||||
- reload coordination
|
||||
- shader compilation
|
||||
- recovery actions themselves
|
||||
|
||||
It also should not decide:
|
||||
|
||||
- whether render should skip a frame
|
||||
- whether VideoBackend should increase queue depth
|
||||
- whether RuntimeCoordinator should reject a mutation
|
||||
- whether ControlServices should drop or coalesce ingress traffic
|
||||
|
||||
Those decisions belong to the subsystem being observed. `HealthTelemetry` may describe that a subsystem is degraded, but it must not quietly become the mechanism that tells the app how to react.
|
||||
|
||||
## Ownership Boundaries
|
||||
|
||||
`HealthTelemetry` owns the following state categories.
|
||||
|
||||
### Structured Log State
|
||||
|
||||
Examples:
|
||||
|
||||
- subsystem name
|
||||
- severity
|
||||
- category
|
||||
- timestamp
|
||||
- message
|
||||
- optional structured fields such as layer id, preset name, queue depth, or shader id
|
||||
|
||||
This replaces the idea that `OutputDebugStringA` text is itself the main diagnostic product.
|
||||
|
||||
### Warning And Error State
|
||||
|
||||
Examples:
|
||||
|
||||
- active warning set
|
||||
- warning occurrence counts
|
||||
- first-seen and last-seen timestamps
|
||||
- clear timestamps
|
||||
- subsystem-scoped degraded flags
|
||||
|
||||
This is the durable in-memory operational state that should answer "what is currently wrong?" even if no UI was connected when the warning was raised.
|
||||
|
||||
### Timing State
|
||||
|
||||
Examples:
|
||||
|
||||
- render duration
|
||||
- frame budget
|
||||
- playout completion interval
|
||||
- smoothed completion interval
|
||||
- queue depth
|
||||
- input upload skip count
|
||||
- async readback fallback count
|
||||
- control ingress lag or queue depth
|
||||
- snapshot publication cost
|
||||
|
||||
This state should be organized as time-series-like rolling telemetry, not as a grab bag of unrelated `double` fields mixed into the runtime store.
|
||||
|
||||
### Health Snapshot State
|
||||
|
||||
Examples:
|
||||
|
||||
- current subsystem health summaries
|
||||
- current operator-facing overall health summary
|
||||
- most recent warning list
|
||||
- recent counters and timing summaries
|
||||
- "degraded but still running" status
|
||||
|
||||
This is the material that `ControlServices` or a diagnostics endpoint may later publish.
|
||||
|
||||
## State Model
|
||||
|
||||
The subsystem should model health and telemetry in a way that supports both machine-friendly and operator-friendly views.
|
||||
|
||||
Suggested conceptual model:
|
||||
|
||||
- `TelemetryLogEntry`
|
||||
- `TelemetryWarningRecord`
|
||||
- `TelemetryCounterState`
|
||||
- `TelemetryGaugeState`
|
||||
- `TelemetryTimingSeries`
|
||||
- `SubsystemHealthState`
|
||||
- `HealthSnapshot`
|
||||
|
||||
Important distinction:
|
||||
|
||||
- raw observations are append/update operations
|
||||
- health snapshots are derived read models
|
||||
|
||||
That distinction matters because the system should be able to retain richer recent telemetry internally than what is necessarily sent to the UI on every refresh.
|
||||
|
||||
## Subsystem Health Domains
|
||||
|
||||
`HealthTelemetry` should track health by subsystem rather than as one flat status blob.
|
||||
|
||||
At minimum, Phase 1 should assume domains for:
|
||||
|
||||
- `RuntimeStore`
|
||||
- `RuntimeCoordinator`
|
||||
- `RuntimeSnapshotProvider`
|
||||
- `ControlServices`
|
||||
- `RenderEngine`
|
||||
- `VideoBackend`
|
||||
|
||||
Optional cross-cutting domain:
|
||||
|
||||
- `ApplicationShell`
|
||||
|
||||
Each domain should be able to express states such as:
|
||||
|
||||
- `Healthy`
|
||||
- `Warning`
|
||||
- `Degraded`
|
||||
- `Error`
|
||||
- `Unavailable`
|
||||
|
||||
The exact enum can change, but the design should preserve the idea that each subsystem reports into its own health lane first, and only then is an overall status derived.
|
||||
|
||||
## Logging Boundaries
|
||||
|
||||
Logging belongs here, but logging should be structured-first.
|
||||
|
||||
Expected inputs:
|
||||
|
||||
- subsystem-scoped debug information
|
||||
- warning and error messages
|
||||
- recovery events
|
||||
- notable state transitions
|
||||
- significant operator actions that matter for diagnostics
|
||||
|
||||
Expected design rules:
|
||||
|
||||
- textual messages are still useful, but they should be wrapped in a structured log entry
|
||||
- repeated transient failures should be rate-limited or deduplicated at the telemetry layer where possible
|
||||
- log storage should be bounded in memory
|
||||
- UI publication should read from health/log snapshots, not scrape stdout/debug output
|
||||
|
||||
Examples of current direct log paths that should eventually move behind `HealthTelemetry`:
|
||||
|
||||
- OSC decode/dispatch failures
|
||||
- screenshot write failures
|
||||
- DeckLink fallback warnings
|
||||
- late/dropped frame warnings
|
||||
|
||||
## Metrics And Timing Boundaries
|
||||
|
||||
Timing and metrics should also move here, but their ownership line matters.
|
||||
|
||||
`HealthTelemetry` should own:
|
||||
|
||||
- metric collection interfaces
|
||||
- rolling summaries
|
||||
- recent history buffers
|
||||
- warning thresholds if the app later chooses to define them declaratively
|
||||
- operator-facing derived summaries
|
||||
|
||||
The producing subsystem should still own:
|
||||
|
||||
- the meaning of the measurement
|
||||
- when it is sampled
|
||||
- whether it triggers local mitigation
|
||||
|
||||
Examples:
|
||||
|
||||
- `RenderEngine` owns when render duration is sampled
|
||||
- `VideoBackend` owns when queue depth or playout lateness is sampled
|
||||
- `ControlServices` owns when ingress backlog is sampled
|
||||
- `RuntimeSnapshotProvider` owns when snapshot publish/build timing is sampled
|
||||
|
||||
`HealthTelemetry` should not invent those timings by inference. It records them when producers report them.
|
||||
|
||||
## Proposed Interfaces
|
||||
|
||||
These are target-shape interfaces, not final signatures.
|
||||
|
||||
### Write/Record Interface
|
||||
|
||||
Core write-side operations could look like:
|
||||
|
||||
```cpp
|
||||
enum class TelemetrySeverity;
|
||||
enum class TelemetrySubsystem;
|
||||
|
||||
struct TelemetryLogEntry;
|
||||
struct TelemetryWarning;
|
||||
struct TelemetryTimingSample;
|
||||
struct TelemetryCounterDelta;
|
||||
struct TelemetryGaugeUpdate;
|
||||
|
||||
class IHealthTelemetry
|
||||
{
|
||||
public:
|
||||
virtual void AppendLogEntry(const TelemetryLogEntry& entry) = 0;
|
||||
virtual void RaiseWarning(const TelemetryWarning& warning) = 0;
|
||||
virtual void ClearWarning(std::string_view warningKey) = 0;
|
||||
virtual void RecordTimingSample(const TelemetryTimingSample& sample) = 0;
|
||||
virtual void RecordCounterDelta(const TelemetryCounterDelta& delta) = 0;
|
||||
virtual void RecordGauge(const TelemetryGaugeUpdate& gauge) = 0;
|
||||
virtual void ReportSubsystemState(TelemetrySubsystem subsystem,
|
||||
SubsystemHealthState state) = 0;
|
||||
};
|
||||
```
|
||||
|
||||
The key is that every subsystem should be able to publish observations without also needing to know how UI payloads, rolling summaries, or log retention are implemented.
|
||||
|
||||
### Read Interface
|
||||
|
||||
Expected read-side operations:
|
||||
|
||||
- `BuildHealthSnapshot()`
|
||||
- `GetSubsystemHealth(...)`
|
||||
- `GetRecentLogs(...)`
|
||||
- `GetActiveWarnings()`
|
||||
- `GetRecentTimingSummary(...)`
|
||||
|
||||
Design notes:
|
||||
|
||||
- the read interface should return stable snapshots or read models
|
||||
- UI/websocket publication should consume those snapshots through `ControlServices`
|
||||
- read-side access should not require direct knowledge of internal ring buffers or lock layout
|
||||
|
||||
## Producer Expectations By Subsystem
|
||||
|
||||
The parent Phase 1 design already allows multiple subsystems to publish into telemetry. This section makes that concrete.
|
||||
|
||||
### From `RuntimeCoordinator`
|
||||
|
||||
Expected observations:
|
||||
|
||||
- mutation rejected
|
||||
- reload requested
|
||||
- preset apply failed
|
||||
- transient state cleared due to compatibility rules
|
||||
- policy-driven degraded notices such as repeated invalid external control input
|
||||
|
||||
### From `RuntimeSnapshotProvider`
|
||||
|
||||
Expected observations:
|
||||
|
||||
- snapshot publication duration
|
||||
- snapshot build failure
|
||||
- snapshot version churn metrics
|
||||
- repeated publish retries or stale-snapshot conditions
|
||||
|
||||
### From `ControlServices`
|
||||
|
||||
Expected observations:
|
||||
|
||||
- OSC decode failures
|
||||
- websocket broadcast failures
|
||||
- REST/control transport errors
|
||||
- ingress queue depth
|
||||
- coalescing/drop counts
|
||||
- file-watch reload request activity
|
||||
|
||||
### From `RenderEngine`
|
||||
|
||||
Expected observations:
|
||||
|
||||
- frame render duration
|
||||
- upload duration
|
||||
- readback duration
|
||||
- fallback to synchronous readback
|
||||
- preview present timing
|
||||
- render-local state resets caused by reload or incompatibility
|
||||
|
||||
### From `VideoBackend`
|
||||
|
||||
Expected observations:
|
||||
|
||||
- current playout queue depth
|
||||
- input signal state
|
||||
- late frames
|
||||
- dropped frames
|
||||
- backend mode changes
|
||||
- fallback from 10-bit to 8-bit input
|
||||
- output-only black-frame mode
|
||||
|
||||
## Current Code Mapping
|
||||
|
||||
The current codebase already contains several telemetry responsibilities that should migrate here.
|
||||
|
||||
### Previous `RuntimeHost` Status Setters
|
||||
|
||||
These were the clearest initial migration candidates:
|
||||
|
||||
- `SetSignalStatus(...)`
|
||||
- `TrySetSignalStatus(...)`
|
||||
- `SetPerformanceStats(...)`
|
||||
- `TrySetPerformanceStats(...)`
|
||||
- `SetFramePacingStats(...)`
|
||||
- `TrySetFramePacingStats(...)`
|
||||
|
||||
See:
|
||||
|
||||
- `RuntimeHost.h`
|
||||
- `RuntimeHost.cpp`
|
||||
- `RuntimeHost.cpp`
|
||||
- `RuntimeHost.cpp`
|
||||
|
||||
In the target architecture, this kind of state should not sit on the same object that owns persistent layer truth.
|
||||
|
||||
### Render Timing Production
|
||||
|
||||
Current render timing is produced in:
|
||||
|
||||
- [OpenGLRenderPipeline.cpp](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/gl/pipeline/OpenGLRenderPipeline.cpp:50)
|
||||
|
||||
That timing sample should conceptually become:
|
||||
|
||||
- `RenderEngine -> HealthTelemetry::RecordTimingSample(...)`
|
||||
|
||||
not the old pattern:
|
||||
|
||||
- `RenderEngine -> RuntimeHost::TrySetPerformanceStats(...)`
|
||||
|
||||
### Playout And Signal Status Production
|
||||
|
||||
Current signal and frame pacing updates are produced in:
|
||||
|
||||
- [OpenGLVideoIOBridge.cpp](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/gl/pipeline/OpenGLVideoIOBridge.cpp:49)
|
||||
- [OpenGLVideoIOBridge.cpp](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/gl/pipeline/OpenGLVideoIOBridge.cpp:61)
|
||||
|
||||
These should eventually become structured `VideoBackend` observations instead of bridge-to-host status writes.
|
||||
|
||||
### Direct Warning And Log Paths
|
||||
|
||||
Current examples:
|
||||
|
||||
- late/dropped frame warnings:
|
||||
- [DeckLinkFrameTransfer.cpp](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/videoio/decklink/DeckLinkFrameTransfer.cpp:84)
|
||||
- backend fallback warnings:
|
||||
- [DeckLinkSession.cpp](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/videoio/decklink/DeckLinkSession.cpp:305)
|
||||
- [DeckLinkSession.cpp](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/videoio/decklink/DeckLinkSession.cpp:320)
|
||||
- OSC errors:
|
||||
- [OscServer.cpp](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/control/OscServer.cpp:142)
|
||||
- [RuntimeServices.cpp](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/control/RuntimeServices.cpp:100)
|
||||
|
||||
All of these are clear migration candidates for `AppendLogEntry(...)`, `RaiseWarning(...)`, or counter/timing updates.
|
||||
|
||||
## Health Snapshot Contract
|
||||
|
||||
`HealthTelemetry` should expose one coherent health snapshot that other publication layers can consume.
|
||||
|
||||
That snapshot should be able to answer, at minimum:
|
||||
|
||||
- what the overall app health is
|
||||
- whether input signal is present
|
||||
- whether playout is healthy, degraded, or underrunning
|
||||
- whether render timing is within budget
|
||||
- what active warnings exist
|
||||
- what recent notable events occurred
|
||||
- what the current subsystem-specific states are
|
||||
|
||||
The important boundary is:
|
||||
|
||||
- `HealthTelemetry` builds the health snapshot
|
||||
- `ControlServices` may publish it
|
||||
- UI consumes it
|
||||
|
||||
That avoids rebuilding health summaries ad hoc in UI-facing runtime state serializers.
|
||||
|
||||
## Concurrency Expectations
|
||||
|
||||
This subsystem will likely receive updates from multiple threads:
|
||||
|
||||
- control ingress threads
|
||||
- render thread
|
||||
- backend callback threads
|
||||
- coordinator/service threads
|
||||
|
||||
So the design should assume:
|
||||
|
||||
- low-contention write paths
|
||||
- bounded memory
|
||||
- no long-held global mutex that callbacks and render both depend on
|
||||
|
||||
Phase 1 does not require lock-free implementation, but it does require the architecture to avoid recreating the old problem where health writes share the same lock as durable state and render-facing concerns.
|
||||
|
||||
Practical expectations:
|
||||
|
||||
- per-domain aggregation or lightweight internal locking is acceptable
|
||||
- read snapshots should be cheap and stable
|
||||
- callback paths should record telemetry cheaply and return
|
||||
|
||||
## Migration Plan From Current Code
|
||||
|
||||
The safest migration path is to peel telemetry responsibilities away from the existing classes incrementally.
|
||||
|
||||
### Step 1: Introduce The `HealthTelemetry` Interface
|
||||
|
||||
Create a small interface and health model types first.
|
||||
|
||||
Initial responsibilities:
|
||||
|
||||
- append structured logs
|
||||
- record timing samples
|
||||
- record counter deltas
|
||||
- raise and clear warnings
|
||||
- build a read-only health snapshot
|
||||
|
||||
The first implementation can still be backed by simple in-memory structures.
|
||||
|
||||
### Step 2: Keep New Observations Off Runtime Storage
|
||||
|
||||
Route new health-style work into `HealthTelemetry` instead of adding more status fields to runtime storage.
|
||||
|
||||
This prevents the old status surface from growing during migration.
|
||||
|
||||
### Step 3: Replace Legacy Status Setters With Telemetry Producers
|
||||
|
||||
Refactor:
|
||||
|
||||
- render timing writes
|
||||
- signal status writes
|
||||
- playout pacing writes
|
||||
|
||||
so they publish structured observations instead of mutating store-adjacent fields.
|
||||
|
||||
### Step 4: Replace Direct `OutputDebugStringA` Warning Paths
|
||||
|
||||
Wrap common warning/error cases in telemetry producers.
|
||||
|
||||
This includes:
|
||||
|
||||
- OSC decode/dispatch failures
|
||||
- DeckLink late/dropped frame notifications
|
||||
- backend fallback notices
|
||||
- screenshot write failures
|
||||
|
||||
Direct debug output can remain as a sink of telemetry if desired, but not as the primary source of truth.
|
||||
|
||||
### Step 5: Publish Health Snapshot Through UI/Diagnostics Paths
|
||||
|
||||
Once the snapshot format exists, let `ControlServices` publish health summaries and recent warnings explicitly rather than depending on the runtime-state payload alone.
|
||||
|
||||
## Risks
|
||||
|
||||
### 1. Telemetry becomes a hidden behavior controller
|
||||
|
||||
If warning states start being used as the indirect way subsystems tell each other what to do, the subsystem boundary will fail.
|
||||
|
||||
Guardrail:
|
||||
|
||||
- telemetry observes and reports
|
||||
- it does not coordinate or command
|
||||
|
||||
### 2. Logging stays string-only
|
||||
|
||||
If the subsystem only centralizes text logging without structure, later diagnostics will still be difficult.
|
||||
|
||||
Guardrail:
|
||||
|
||||
- severity, subsystem, category, and optional fields should be first-class
|
||||
|
||||
### 3. Timing writes become too expensive
|
||||
|
||||
If every sample requires heavy locking or snapshot rebuilds, render and callback timing could regress.
|
||||
|
||||
Guardrail:
|
||||
|
||||
- cheap recording path
|
||||
- derived summaries built separately from hot-path writes
|
||||
|
||||
### 4. Health snapshot duplicates runtime truth
|
||||
|
||||
If health snapshots start storing copies of durable runtime state, the subsystem boundary will blur again.
|
||||
|
||||
Guardrail:
|
||||
|
||||
- health snapshots summarize operational state
|
||||
- they do not become a second runtime store
|
||||
|
||||
### 5. Warning severity semantics drift by subsystem
|
||||
|
||||
If each subsystem invents its own meaning for warning/degraded/error, operator visibility becomes noisy and inconsistent.
|
||||
|
||||
Guardrail:
|
||||
|
||||
- define shared severity and health-state vocabulary early
|
||||
|
||||
## Open Questions
|
||||
|
||||
### 1. Should debug-output sinks remain enabled by default?
|
||||
|
||||
Current recommendation:
|
||||
|
||||
- yes, as a sink fed by structured telemetry entries, not as the source of truth
|
||||
|
||||
### 2. How much timing history should be retained in memory?
|
||||
|
||||
Current recommendation:
|
||||
|
||||
- enough for short-term live troubleshooting and UI summaries
|
||||
- not an unbounded time-series archive
|
||||
|
||||
### 3. Should operator-facing health and engineering diagnostics use the same snapshot?
|
||||
|
||||
Current recommendation:
|
||||
|
||||
- share one core telemetry model
|
||||
- allow separate derived views for concise operator summaries versus deeper engineering detail
|
||||
|
||||
### 4. Where should threshold policy live if the app later formalizes warnings like "render over budget"?
|
||||
|
||||
Current recommendation:
|
||||
|
||||
- telemetry may evaluate declared thresholds
|
||||
- subsystem owners still own mitigation behavior
|
||||
|
||||
### 5. Should input signal presence remain part of runtime state or move fully into telemetry?
|
||||
|
||||
Current recommendation:
|
||||
|
||||
- treat it as operational health state under `VideoBackend` reporting into telemetry
|
||||
- avoid keeping it as a core durable runtime-store concern
|
||||
|
||||
## Success Criteria For This Subsystem
|
||||
|
||||
`HealthTelemetry` can be considered well-defined once the codebase can say, without ambiguity:
|
||||
|
||||
- all major subsystems have one place to publish timing, warnings, and counters
|
||||
- health and timing state no longer share ownership with durable runtime state
|
||||
- the UI can consume a stable health snapshot without scraping unrelated runtime fields
|
||||
- direct debug-string warning paths are being retired or wrapped behind structured telemetry
|
||||
- degraded-but-running conditions are visible as first-class state
|
||||
|
||||
## Short Version
|
||||
|
||||
`HealthTelemetry` is the subsystem that should answer:
|
||||
|
||||
- what is healthy right now
|
||||
- what is degraded right now
|
||||
- what recent warnings and errors occurred
|
||||
- how render, control, and playout timing are behaving
|
||||
|
||||
It should:
|
||||
|
||||
- collect structured logs
|
||||
- collect warnings and counters
|
||||
- collect timing samples and gauges
|
||||
- build stable health snapshots for publication
|
||||
|
||||
It should not:
|
||||
|
||||
- own core runtime truth
|
||||
- decide app behavior
|
||||
- coordinate recovery actions
|
||||
- become a replacement for the render or backend policy layers
|
||||
|
||||
If this boundary holds, later phases can keep moving toward a much more diagnosable live system without putting timing and warning state back into runtime storage.
|
||||
70
docs/subsystems/README.md
Normal file
70
docs/subsystems/README.md
Normal file
@@ -0,0 +1,70 @@
|
||||
# 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.
|
||||
|
||||
Status note:
|
||||
|
||||
- The Phase 1 design package is complete.
|
||||
- The runtime implementation foothold is complete: the named runtime subsystems exist in code, `RuntimeHost` is retired from the compiled runtime path, and subsystem tests cover the new seams.
|
||||
- The whole app is not fully extracted yet, so these notes still describe the architecture later phases should continue toward.
|
||||
|
||||
## 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-state facade over layer-stack, config, package-catalog, presentation, and persistence boundaries.
|
||||
- [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 publication boundary backed by explicit render snapshot building/versioning.
|
||||
- [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.
|
||||
|
||||
As of the current codebase, those design questions are settled well enough for later work to build against them. Remaining implementation work should be tracked under later phases, especially eventing, render-thread ownership, persistence, backend lifecycle, live-state layering, and telemetry.
|
||||
487
docs/subsystems/RenderEngine.md
Normal file
487
docs/subsystems/RenderEngine.md
Normal file
@@ -0,0 +1,487 @@
|
||||
# RenderEngine Subsystem Design
|
||||
|
||||
This document expands the `RenderEngine` portion of [PHASE_1_SUBSYSTEM_BOUNDARIES_DESIGN.md](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/docs/PHASE_1_SUBSYSTEM_BOUNDARIES_DESIGN.md). It defines the target ownership, boundaries, and migration shape for the rendering subsystem so later phases can move GL work out of today's mixed orchestration paths without inventing new boundaries on the fly.
|
||||
|
||||
The intent here is not to force a one-step rewrite. It is to make the target render boundary explicit enough that later work on events, live-state layering, sole-owner GL threading, and backend decoupling all land in the same place.
|
||||
|
||||
## Purpose
|
||||
|
||||
`RenderEngine` is the live frame-production subsystem.
|
||||
|
||||
It owns:
|
||||
|
||||
- GL context ownership in the target architecture
|
||||
- render loop cadence and render task execution
|
||||
- shader program and render-pass execution once build outputs are available
|
||||
- capture texture upload scheduling once frames are accepted for render
|
||||
- temporal history resources
|
||||
- shader feedback resources
|
||||
- render-local transient overlays
|
||||
- preview-ready frame production
|
||||
- playout-ready frame production
|
||||
- render-local reset and rebuild behavior
|
||||
|
||||
It does not own:
|
||||
|
||||
- persisted runtime state
|
||||
- high-level mutation policy
|
||||
- OSC/UI ingress
|
||||
- device discovery or callback policy
|
||||
- playout queue policy
|
||||
- operator-visible health policy beyond publishing observations
|
||||
|
||||
In the Phase 1 terminology, `RenderEngine` consumes snapshots plus render-local transient state and produces completed visual frames plus timing signals.
|
||||
|
||||
## Why This Subsystem Needs A Sharp Boundary
|
||||
|
||||
The current rendering path is split across several classes:
|
||||
|
||||
- [OpenGLComposite.cpp](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/gl/OpenGLComposite.cpp:86) constructs the renderer, render pipeline, shader programs, runtime services, and video bridge in one owner.
|
||||
- [OpenGLRenderPipeline.cpp](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/gl/pipeline/OpenGLRenderPipeline.cpp:31) performs pass execution, pack/readback, preview paint, and performance stat publication.
|
||||
- [OpenGLVideoIOBridge.cpp](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/gl/pipeline/OpenGLVideoIOBridge.cpp:58) accepts capture frames and still performs render work from the playout completion callback path.
|
||||
- `RenderFrameStateResolver` and `RenderStateComposer` now keep frame-state selection and live value composition outside GL drawing, while `RenderEngine` still owns the current GL resource and draw path.
|
||||
|
||||
That split is workable today, but it creates architectural pressure:
|
||||
|
||||
- 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 now has clearer Phase 3 boundaries, but GL ownership is still shared through callback and UI entrypoints.
|
||||
- it is difficult to test render behavior separately from app bootstrap and hardware integration.
|
||||
|
||||
`RenderEngine` exists to absorb that responsibility into one subsystem with one direction of ownership. Phase 4 has completed the GL ownership part of this target: normal runtime GL work now enters through the `RenderEngine` render thread.
|
||||
|
||||
## Responsibilities
|
||||
|
||||
### 1. Sole GL Ownership
|
||||
|
||||
In the target design, `RenderEngine` should be the only subsystem that performs long-lived GL work.
|
||||
|
||||
That includes:
|
||||
|
||||
- context binding and release policy
|
||||
- framebuffer and texture lifetime
|
||||
- shader program binding and draw execution
|
||||
- upload/readback buffer lifetime
|
||||
- preview blit or present paths
|
||||
- render-local resource reset on rebuild or video-format changes
|
||||
|
||||
This is the most important boundary. Other subsystems may request work or provide data, but they should not directly perform GL commands.
|
||||
|
||||
### 2. Snapshot Consumption
|
||||
|
||||
`RenderEngine` should consume immutable or near-immutable render snapshots from `RuntimeSnapshotProvider`.
|
||||
|
||||
It is responsible for:
|
||||
|
||||
- detecting snapshot version changes
|
||||
- rebuilding or re-binding render-local resources when the snapshot changes
|
||||
- resolving render-pass execution from snapshot contents
|
||||
- separating structural snapshot changes from transient overlay changes
|
||||
|
||||
It should not inspect mutable runtime store objects directly.
|
||||
|
||||
### 3. Frame Production
|
||||
|
||||
`RenderEngine` should produce completed frames for two consumers:
|
||||
|
||||
- preview presentation
|
||||
- `VideoBackend` playout consumption
|
||||
|
||||
Those outputs may share most of their render work, but they are not equal-priority outputs. The subsystem rule from Phase 1 should be preserved:
|
||||
|
||||
- playout is the primary timing-sensitive output
|
||||
- preview is subordinate and best-effort
|
||||
|
||||
### 4. Render-Local Transient State
|
||||
|
||||
`RenderEngine` owns transient visual state that affects output but is not persisted truth.
|
||||
|
||||
Examples:
|
||||
|
||||
- temporal history textures
|
||||
- feedback ping-pong buffers
|
||||
- render-local OSC/live overlay state
|
||||
- queued input frames accepted for upload
|
||||
- cached readback frames
|
||||
- preview-only presentation state
|
||||
- in-flight rebuild generations
|
||||
|
||||
This state should remain render-local even when it influences visible output.
|
||||
|
||||
Phase 5's `RuntimeStateLayerModel` explicitly keeps temporal history, feedback state, accepted input frames, staged output frames, preview staging, and screenshot/readback staging in the render-local category. These are deliberately outside the persisted/committed/transient-automation parameter composition rule.
|
||||
|
||||
`RuntimeLiveState` now owns transient automation invalidation for render-facing compatibility. It can clear overlays for a target layer/control key and prunes overlays that no longer resolve to the current layer and parameter definitions before applying them to a frame. This keeps shader reload, preset load, and layer removal behavior local to the live-state/composition boundary instead of scattering it through GL drawing code.
|
||||
|
||||
Render snapshots now flow through a named `CommittedLiveStateReadModel`, so render-facing committed state is distinct from durable storage even while both are physically backed by the same store during migration.
|
||||
|
||||
### 5. Shader Build Application
|
||||
|
||||
Compilation itself may eventually move into a separate build service, but once shader build outputs exist, `RenderEngine` owns:
|
||||
|
||||
- program creation/link usage
|
||||
- pass graph application
|
||||
- sampler/texture binding layout application
|
||||
- resource reallocation required by shader shape changes
|
||||
- safe invalidation of old render-local feedback/history resources
|
||||
|
||||
### 6. Render Timing Publication
|
||||
|
||||
`RenderEngine` should publish observations to `HealthTelemetry` such as:
|
||||
|
||||
- frame render duration
|
||||
- upload duration
|
||||
- pass execution duration
|
||||
- pack/readback duration
|
||||
- preview present timing
|
||||
- rebuild stalls
|
||||
- dropped/skipped input uploads
|
||||
- output frame production latency
|
||||
|
||||
It should publish them, not own the health policy built from them.
|
||||
|
||||
## Non-Responsibilities
|
||||
|
||||
The target boundary should remain explicit about what does not belong here.
|
||||
|
||||
`RenderEngine` should not:
|
||||
|
||||
- decide whether a parameter mutation is persisted
|
||||
- normalize OSC/UI actions
|
||||
- choose device modes
|
||||
- own DeckLink callback behavior
|
||||
- own playout headroom policy
|
||||
- perform stack preset serialization
|
||||
- broadcast UI state
|
||||
- treat telemetry as a control plane
|
||||
|
||||
Those rules matter because the current codebase often solves timing issues by letting the render path reach sideways into nearby systems.
|
||||
|
||||
## GL Ownership Model
|
||||
|
||||
## Current Rule
|
||||
|
||||
One subsystem owns GL. `RenderEngine` now starts a dedicated render thread, binds the existing GL context on that thread for normal runtime work, and routes input upload, output render, preview presentation, screenshot capture, shader application, and render-local reset work through render-thread requests.
|
||||
|
||||
The render thread should:
|
||||
|
||||
- create or adopt the GL context
|
||||
- execute all frame production work
|
||||
- perform accepted texture uploads
|
||||
- execute all pass graphs
|
||||
- manage async readback and output packing
|
||||
- manage feedback/history resets and reallocations
|
||||
|
||||
Other threads should interact with the subsystem through queues, snapshots, and completion signals, not by borrowing the GL context.
|
||||
|
||||
## Remaining Timing State
|
||||
|
||||
GL ownership is no longer shared across callback-driven and UI entrypoints:
|
||||
|
||||
- input upload is requested through [OpenGLVideoIOBridge::UploadInputFrame()](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/gl/pipeline/OpenGLVideoIOBridge.cpp:11)
|
||||
- playout-triggered render is requested through [OpenGLVideoIOBridge::RenderScheduledFrame()](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/gl/pipeline/OpenGLVideoIOBridge.cpp:18)
|
||||
- render-pass execution occurs in [OpenGLRenderPipeline::RenderFrame()](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/gl/pipeline/OpenGLRenderPipeline.cpp:31)
|
||||
- preview and screenshot paths enter `RenderEngine` queue/request methods
|
||||
|
||||
The remaining timing issue is not shared GL ownership; it is the transitional synchronous output request/response path. The DeckLink completion callback still waits while the render thread produces an output frame, fills the DeckLink buffer, and then schedules the next frame.
|
||||
|
||||
## Migration Direction
|
||||
|
||||
The next target path should be:
|
||||
|
||||
1. input callback enqueues frame payloads or references
|
||||
2. render thread accepts the latest usable input frame
|
||||
3. render thread performs uploads on its own cadence
|
||||
4. render thread produces completed output frames ahead of backend demand
|
||||
5. backend callbacks only dequeue and schedule pre-rendered frames
|
||||
|
||||
Phase 4 completed the part that removes callback-thread GL ownership. Phase 7 should complete the producer/consumer playout part.
|
||||
|
||||
## Render Loop Boundaries
|
||||
|
||||
`RenderEngine` should own a render loop with explicit phases. A good target shape is:
|
||||
|
||||
1. drain render-side commands and accepted service events
|
||||
2. swap to the latest published snapshot if needed
|
||||
3. apply render-local transient overlays
|
||||
4. accept or coalesce latest input frame for upload
|
||||
5. perform required uploads
|
||||
6. execute pass graph
|
||||
7. update temporal and feedback resources
|
||||
8. pack and stage output frame(s)
|
||||
9. publish preview-ready image if due
|
||||
10. publish playout-ready frame(s) to `VideoBackend`
|
||||
11. emit timing and health samples
|
||||
|
||||
The important property is that preview, playout preparation, feedback maintenance, and upload execution all happen under one render-owned cadence rather than as ad hoc side effects of unrelated callbacks.
|
||||
|
||||
## Snapshot And Overlay Interaction
|
||||
|
||||
`RenderEngine` should treat snapshots and overlays as different layers of state.
|
||||
|
||||
### Snapshot Inputs
|
||||
|
||||
Snapshots should provide:
|
||||
|
||||
- layer stack structure
|
||||
- shader/package selections
|
||||
- validated committed parameter values
|
||||
- pass graph definitions
|
||||
- resource requirements derived from runtime state
|
||||
|
||||
### Render-Local Overlay Inputs
|
||||
|
||||
Overlays should provide:
|
||||
|
||||
- active automation targets
|
||||
- smoothed transient parameter overrides
|
||||
- temporary visual state that should not persist back into the store
|
||||
- queued reset/rebuild invalidations for render-local resources
|
||||
|
||||
### Resolution Rule
|
||||
|
||||
The render-side resolution order should be:
|
||||
|
||||
1. snapshot committed state forms the baseline
|
||||
2. render-local transient overlays are applied on top
|
||||
3. feedback/history resources influence shading as render-local inputs
|
||||
4. completed frame is produced without mutating the underlying snapshot
|
||||
|
||||
This is especially important after the OSC work already moved toward render-local overlays. Phase 1 should keep that direction: render consumes committed truth plus transient live overlays, but render does not become the owner of persisted truth.
|
||||
|
||||
## Preview And Playout Relationship
|
||||
|
||||
Preview should be a subordinate consumer of render results, not a peer that can disturb playout timing.
|
||||
|
||||
### Target Rule
|
||||
|
||||
- playout deadlines come first
|
||||
- preview is best-effort
|
||||
- preview cadence may be reduced independently
|
||||
- preview failure must not stall output frame production
|
||||
|
||||
### Current State
|
||||
|
||||
Today preview still hangs off the render pipeline path through `mPaint()` in [OpenGLRenderPipeline::RenderFrame()](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/gl/pipeline/OpenGLRenderPipeline.cpp:54). That keeps preview close enough to the playout path that it is still part of the same timing surface.
|
||||
|
||||
### Target Shape
|
||||
|
||||
`RenderEngine` should internally distinguish:
|
||||
|
||||
- playout-ready frame production
|
||||
- preview presentation or preview-copy publication
|
||||
|
||||
Possible later implementations:
|
||||
|
||||
- playout frame and preview frame share one composite render, but preview present is decoupled and rate-limited
|
||||
- render publishes a preview texture handle or CPU-side preview image to a preview presenter
|
||||
- preview updates are skipped under load without affecting playout queue fill
|
||||
|
||||
The exact implementation can change later, but the subsystem contract should already assume preview is subordinate.
|
||||
|
||||
## Interaction With `RuntimeSnapshotProvider`
|
||||
|
||||
`RenderEngine` should depend on `RuntimeSnapshotProvider`, not on `RuntimeStore`.
|
||||
|
||||
Expected interactions:
|
||||
|
||||
- query latest snapshot version
|
||||
- consume latest stable snapshot
|
||||
- detect structural versus parameter-only changes
|
||||
- request no mutation back into the snapshot provider during render
|
||||
|
||||
Expected non-interactions:
|
||||
|
||||
- no direct persistence reads/writes
|
||||
- no raw store mutation
|
||||
- no direct service ingress handling
|
||||
|
||||
This is one of the main Phase 1 guardrails, because the current code often achieves convenience by letting render reach back into runtime-owned mutable objects.
|
||||
|
||||
## Interaction With `VideoBackend`
|
||||
|
||||
The target dependency direction stays:
|
||||
|
||||
- `VideoBackend -> RenderEngine`
|
||||
|
||||
That means:
|
||||
|
||||
- backend requests or consumes ready frames
|
||||
- backend reports output timing/completion events
|
||||
- render does not own output device policy
|
||||
|
||||
`RenderEngine` should expose frame-production and queue-facing interfaces, while `VideoBackend` owns:
|
||||
|
||||
- device callback handling
|
||||
- output scheduling policy
|
||||
- buffer pool policy
|
||||
- backend state transitions
|
||||
|
||||
In later phases, this should evolve toward a producer/consumer queue where:
|
||||
|
||||
- render produces completed frames ahead of demand
|
||||
- backend consumes already-produced frames
|
||||
- callbacks drive dequeue/schedule/accounting only
|
||||
|
||||
## Current Code Mapping
|
||||
|
||||
The following current responsibilities should converge into `RenderEngine`.
|
||||
|
||||
### From `OpenGLComposite`
|
||||
|
||||
- render-local overlay management
|
||||
- render-facing rebuild application
|
||||
- screenshot-related render execution hooks
|
||||
- render bootstrap ownership currently mixed with app bootstrap
|
||||
|
||||
### From `OpenGLRenderPipeline`
|
||||
|
||||
- frame render orchestration
|
||||
- output pack conversion
|
||||
- async readback state
|
||||
- output frame caching
|
||||
- preview-ready signal publication
|
||||
|
||||
### From `OpenGLVideoIOBridge`
|
||||
|
||||
- GL texture upload execution should move under render ownership
|
||||
- playout callback render work should move out of the callback path
|
||||
|
||||
### Remains Outside `RenderEngine`
|
||||
|
||||
- device callback registration
|
||||
- playout scheduling policy
|
||||
- signal/device status lifecycle
|
||||
- runtime mutation policy
|
||||
|
||||
## Suggested Internal Components
|
||||
|
||||
This document does not require final class names, but `RenderEngine` will likely be easier to evolve if it is not one monolithic replacement for `OpenGLComposite`.
|
||||
|
||||
Reasonable internal pieces could include:
|
||||
|
||||
- `RenderLoopController`
|
||||
- `RenderSnapshotConsumer`
|
||||
- `RenderOverlayState`
|
||||
- `RenderInputQueue`
|
||||
- `RenderPassExecutor`
|
||||
- `RenderHistoryManager`
|
||||
- `RenderOutputStager`
|
||||
- `PreviewPresenter`
|
||||
|
||||
Those are internal implementation helpers. They should not become new cross-cutting subsystem boundaries by themselves.
|
||||
|
||||
## Public Interface Shape
|
||||
|
||||
Aligned with the Phase 1 design, `RenderEngine` should eventually expose operations in this family:
|
||||
|
||||
- `StartRenderLoop(...)`
|
||||
- `StopRenderLoop()`
|
||||
- `ConsumeSnapshot(...)`
|
||||
- `EnqueueInputFrame(...)`
|
||||
- `ApplyOverlayUpdate(...)`
|
||||
- `RequestRenderLocalReset(...)`
|
||||
- `HandleRebuildOutputs(...)`
|
||||
- `TryProduceOutputFrame(...)`
|
||||
- `GetPreviewFrame(...)`
|
||||
- `ReportRenderState()`
|
||||
|
||||
Interface goals:
|
||||
|
||||
- calls are explicit about whether they mutate render-local state or request frame production
|
||||
- no caller needs direct GL access
|
||||
- preview and playout are exposed as outputs, not as reasons for callers to enter the render path
|
||||
|
||||
## Migration Plan From Current Code
|
||||
|
||||
### Step 1. Name The Boundary
|
||||
|
||||
Treat `OpenGLRenderPipeline` plus the render portions of `OpenGLComposite` and `OpenGLVideoIOBridge` as conceptually belonging to `RenderEngine`, even before physical extraction is complete.
|
||||
|
||||
### Step 2. Stop New Render Work From Escaping
|
||||
|
||||
As new features are added, keep:
|
||||
|
||||
- feedback buffers
|
||||
- temporal history
|
||||
- render-local overlays
|
||||
- preview state
|
||||
|
||||
inside render-owned code paths instead of putting them back into runtime storage or service layers.
|
||||
|
||||
### Step 3. Isolate Snapshot Consumption
|
||||
|
||||
Introduce snapshot-facing APIs so render no longer depends on broad runtime-state access for frame production.
|
||||
|
||||
Current status: Phase 3 introduced `RenderFrameInput`, `RenderFrameState`, and `RenderFrameStateResolver`, so frame-state selection is named and no longer lives inside GL drawing. Phase 4 built on that contract and moved normal runtime GL ownership onto the render thread.
|
||||
|
||||
### Step 4. Move Uploads Onto Render Ownership
|
||||
|
||||
Input callbacks should enqueue or hand off frame data; render executes the upload.
|
||||
|
||||
### Step 5. Break Callback-Driven Rendering
|
||||
|
||||
Move from "render in playout completion callback" to "render ahead and let backend consume ready frames."
|
||||
|
||||
### Step 6. Decouple Preview Cadence
|
||||
|
||||
Make preview a best-effort presentation path with its own skip/rate-limit policy.
|
||||
|
||||
### Step 7. Narrow `OpenGLComposite`
|
||||
|
||||
After the above, `OpenGLComposite` should collapse toward a composition root and legacy adapter rather than remaining the owner of render behavior.
|
||||
|
||||
## Risks
|
||||
|
||||
### Latency Risk
|
||||
|
||||
Moving to queue-based frame production can accidentally increase latency if headroom is allowed to grow without policy. `RenderEngine` should therefore expose queue-friendly production, but `VideoBackend` must still own explicit latency/headroom policy.
|
||||
|
||||
### Resource Churn Risk
|
||||
|
||||
Snapshot changes, shader rebuilds, and video-format changes can cause expensive reallocation of:
|
||||
|
||||
- feedback surfaces
|
||||
- history buffers
|
||||
- output pack resources
|
||||
- readback buffers
|
||||
|
||||
The subsystem needs clear structural-change boundaries so parameter-only changes do not trigger broad resource churn.
|
||||
|
||||
### Preview Coupling Risk
|
||||
|
||||
If preview remains too close to the render/playout path, it can continue to steal budget from output production even after the rest of the subsystem is cleaned up.
|
||||
|
||||
### Readback Deadline Risk
|
||||
|
||||
The current async readback path still falls back to synchronous reads when the deadline is missed. That behavior may remain necessary, but `RenderEngine` should treat it as a degraded-path metric, not as an invisible normal case.
|
||||
|
||||
### Overlay Complexity Risk
|
||||
|
||||
Render-local overlays are powerful, but they can become a hidden second state model if not kept clearly subordinate to committed snapshot state.
|
||||
|
||||
## Open Questions
|
||||
|
||||
- Should preview become a separate presenter helper inside `RenderEngine`, or remain a subordinate callback/output sink?
|
||||
- Where should screenshot capture live long-term: inside `RenderEngine`, or in a small render consumer layered on top of it?
|
||||
- Should shader compilation outputs be delivered to render as whole-framegraph rebuild packages, or incrementally by layer/pass?
|
||||
- How should input frame ownership work under load: newest-only, bounded queue, or policy selected by backend mode?
|
||||
- Should render expose one playout-ready frame at a time, or a bounded ring the backend drains directly?
|
||||
- What exact distinction should the snapshot provider publish between structural changes and parameter-only changes so render rebuilds stay cheap?
|
||||
|
||||
## Phase 1 Exit Criteria For `RenderEngine`
|
||||
|
||||
For Phase 1, this subsystem design is sufficiently defined once the project agrees that:
|
||||
|
||||
- render is the sole long-term owner of GL work
|
||||
- render consumes snapshots, not mutable runtime store objects
|
||||
- preview is subordinate to playout
|
||||
- feedback/history/overlays are render-local transient state
|
||||
- backend callbacks should converge toward dequeue/schedule behavior rather than direct rendering
|
||||
- current render responsibilities in `OpenGLComposite`, `OpenGLRenderPipeline`, and `OpenGLVideoIOBridge` are expected to migrate under this subsystem
|
||||
|
||||
## Short Version
|
||||
|
||||
`RenderEngine` should become the subsystem that owns live GPU execution and nothing else.
|
||||
|
||||
It consumes committed snapshots plus render-local overlays, owns the full GL lifecycle, produces preview and playout-ready frames, and publishes timing observations. It should not own persistence, control ingress, or hardware scheduling policy. If later phases hold to that line, timing work and render-state work can get cleaner without reintroducing the same cross-thread coupling in a different form.
|
||||
563
docs/subsystems/RuntimeCoordinator.md
Normal file
563
docs/subsystems/RuntimeCoordinator.md
Normal file
@@ -0,0 +1,563 @@
|
||||
# RuntimeCoordinator Design Note
|
||||
|
||||
This document defines the target design for the `RuntimeCoordinator` subsystem introduced in [PHASE_1_SUBSYSTEM_BOUNDARIES_DESIGN.md](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/docs/PHASE_1_SUBSYSTEM_BOUNDARIES_DESIGN.md).
|
||||
|
||||
`RuntimeCoordinator` is the mutation and policy layer for the app. Its job is to accept already-normalized actions from ingress systems, decide whether those actions are valid, classify how they should affect durable and live state, and trigger downstream publication or persistence work without taking ownership of rendering, device callbacks, or disk serialization details.
|
||||
|
||||
## Why This Subsystem Exists
|
||||
|
||||
Before the Phase 1 runtime split, the app's mutation path was split across several places:
|
||||
|
||||
- `RuntimeHost` performed validation, mutation, persistence, render-state invalidation, and some status updates:
|
||||
- `RuntimeHost.h`
|
||||
- `RuntimeHost.cpp`
|
||||
- `OpenGLComposite` currently acts like an orchestration shell and a mutation coordinator at the same time:
|
||||
- [OpenGLCompositeRuntimeControls.cpp](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/gl/OpenGLCompositeRuntimeControls.cpp:1)
|
||||
- `RuntimeServices` still owns some deferred control flow around OSC commit and polling:
|
||||
- [RuntimeServices.h](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/control/RuntimeServices.h:46)
|
||||
|
||||
That overlap makes several kinds of regressions more likely:
|
||||
|
||||
- persistence policy leaks into control handlers
|
||||
- render invalidation rules are spread across UI and non-UI paths
|
||||
- transient automation behavior is hard to reason about
|
||||
- reload behavior is partly a render concern and partly a runtime concern
|
||||
- future event-model work has no single policy owner to target
|
||||
|
||||
`RuntimeCoordinator` exists to centralize those decisions without becoming a new monolith.
|
||||
|
||||
## Core Responsibilities
|
||||
|
||||
`RuntimeCoordinator` should own the following responsibilities.
|
||||
|
||||
### 1. Mutation intake after normalization
|
||||
|
||||
`RuntimeCoordinator` accepts typed, already-parsed actions from `ControlServices` or composition-root adapters. Examples:
|
||||
|
||||
- add/remove/move layer
|
||||
- change shader on a layer
|
||||
- change a parameter value
|
||||
- reset a layer
|
||||
- save or load a stack preset
|
||||
- request a shader/package reload
|
||||
- apply a transient automation target
|
||||
- commit or clear transient overlay state
|
||||
|
||||
The coordinator should not parse JSON, decode OSC payloads, or inspect HTTP payload syntax. That belongs to ingress systems.
|
||||
|
||||
### 2. Validation and policy decisions
|
||||
|
||||
The coordinator validates whether a requested mutation is allowed and decides how it should behave.
|
||||
|
||||
Examples:
|
||||
|
||||
- whether a layer id exists
|
||||
- whether a shader id is valid
|
||||
- whether a parameter exists on the targeted shader
|
||||
- whether a value is within the definition's allowed range or enum set
|
||||
- whether a trigger should update committed state, transient state, or both
|
||||
- whether a structural change should preserve compatible transient state such as feedback buffers
|
||||
|
||||
This is the policy surface that used to be spread between `RuntimeHost` methods such as:
|
||||
|
||||
- `AddLayer(...)`
|
||||
- `SetLayerShader(...)`
|
||||
- `UpdateLayerParameter(...)`
|
||||
- `UpdateLayerParameterByControlKey(...)`
|
||||
- `ApplyOscTargetByControlKey(...)`
|
||||
- `ResetLayerParameters(...)`
|
||||
|
||||
See `RuntimeHost.h`.
|
||||
|
||||
### 3. State classification
|
||||
|
||||
The coordinator decides which state category a mutation affects:
|
||||
|
||||
- persisted state
|
||||
- committed live state
|
||||
- transient live overlay state
|
||||
- health/timing state only
|
||||
|
||||
The design rule is that classification belongs here, not in the ingress layer and not in render code.
|
||||
|
||||
Phase 5 has started codifying the shared vocabulary for this classification in `RuntimeStateLayerModel`. The current model records committed session parameter values, layer bypass state, and runtime compile/reload flags as committed-live/session coordination state, even though some of those values are still physically backed by `RuntimeStore` during migration.
|
||||
|
||||
### 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 while committed live state is physically stored 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
|
||||
|
||||
Phase 5 sets the default settled OSC policy to session-only. `CommitOscParameterByControlKey(...)` updates committed session state through the store with persistence disabled, publishes ordinary mutation/state-change observations, and does not request a persistence write unless a future explicit policy opts into durable OSC commits.
|
||||
|
||||
The committed-live concept now has a named read model, `CommittedLiveStateReadModel`. The coordinator remains the owner of whether a mutation should be durable or session-only, while `RuntimeStore` temporarily backs the read model until a physical `CommittedLiveState` collaborator is worth extracting.
|
||||
|
||||
### Health and timing state
|
||||
|
||||
The coordinator may emit events like:
|
||||
|
||||
- mutation rejected
|
||||
- reload requested
|
||||
- preset load succeeded/failed
|
||||
- transient state cleared because structure changed
|
||||
|
||||
But those are observations into `HealthTelemetry`, not coordinator-owned data.
|
||||
|
||||
## Proposed Interfaces
|
||||
|
||||
These are target-shape interfaces, not final signatures.
|
||||
|
||||
### Input-facing API
|
||||
|
||||
Core mutation entrypoints could look like:
|
||||
|
||||
```cpp
|
||||
struct RuntimeMutationRequest;
|
||||
struct RuntimeMutationResult;
|
||||
struct ReloadRequest;
|
||||
struct OverlayCommitRequest;
|
||||
|
||||
class RuntimeCoordinator
|
||||
{
|
||||
public:
|
||||
RuntimeMutationResult ApplyMutation(const RuntimeMutationRequest& request);
|
||||
RuntimeMutationResult ApplyAutomationTarget(const RuntimeMutationRequest& request);
|
||||
RuntimeMutationResult ResetLayer(const std::string& layerId);
|
||||
RuntimeMutationResult RequestReload(const ReloadRequest& request);
|
||||
RuntimeMutationResult CommitOverlayState(const OverlayCommitRequest& request);
|
||||
RuntimeMutationResult ClearTransientStateForScope(const RuntimeResetScope& scope);
|
||||
};
|
||||
```
|
||||
|
||||
The important point is not the exact names. It is that ingress systems send typed requests into one policy owner.
|
||||
|
||||
### Downstream collaborators
|
||||
|
||||
The coordinator likely needs collaborators conceptually equivalent to:
|
||||
|
||||
- `IRuntimeStore`
|
||||
- `IRuntimeSnapshotProvider`
|
||||
- `IHealthTelemetry`
|
||||
- compatibility adapters only where older call shapes still need to be supported during migration
|
||||
|
||||
### Mutation result shape
|
||||
|
||||
A useful result structure should carry more than success/failure. It should support policy-driven downstream behavior without re-deriving the decision elsewhere.
|
||||
|
||||
Suggested fields:
|
||||
|
||||
- `accepted`
|
||||
- `errorMessage`
|
||||
- `stateChanged`
|
||||
- `persistedStateChanged`
|
||||
- `committedLiveStateChanged`
|
||||
- `transientStateChanged`
|
||||
- `snapshotPublicationRequired`
|
||||
- `persistenceRequested`
|
||||
- `renderResetScope`
|
||||
- `telemetryNotes`
|
||||
|
||||
This prevents callers from guessing whether they need to reload, publish, or persist.
|
||||
|
||||
## Current Code Mapping
|
||||
|
||||
The current app does not have a separate coordinator class, but several existing code paths are clearly doing coordinator work.
|
||||
|
||||
### `OpenGLCompositeRuntimeControls.cpp`
|
||||
|
||||
Methods like:
|
||||
|
||||
- `AddLayer(...)`
|
||||
- `RemoveLayer(...)`
|
||||
- `MoveLayer(...)`
|
||||
- `SetLayerBypass(...)`
|
||||
- `SetLayerShader(...)`
|
||||
- `UpdateLayerParameterJson(...)`
|
||||
- `ResetLayerParameters(...)`
|
||||
- `SaveStackPreset(...)`
|
||||
- `LoadStackPreset(...)`
|
||||
|
||||
currently do this pattern:
|
||||
|
||||
1. call a host/store mutation directly
|
||||
2. decide whether to call `ReloadShader(...)`
|
||||
3. call `broadcastRuntimeState()`
|
||||
|
||||
See [OpenGLCompositeRuntimeControls.cpp](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/gl/OpenGLCompositeRuntimeControls.cpp:1).
|
||||
|
||||
That "call host, then decide reload/broadcast policy" logic is a direct candidate for migration into `RuntimeCoordinator`.
|
||||
|
||||
### Previous `RuntimeHost`
|
||||
|
||||
`RuntimeHost` previously combined:
|
||||
|
||||
- mutation validation
|
||||
- state mutation
|
||||
- value normalization
|
||||
- persistence writes
|
||||
- render-state dirty marking
|
||||
|
||||
Examples from the old `RuntimeHost.cpp`:
|
||||
|
||||
- `AddLayer(...)`
|
||||
- `SetLayerShader(...)`
|
||||
- `UpdateLayerParameter(...)`
|
||||
- `UpdateLayerParameterByControlKey(...)`
|
||||
- `ApplyOscTargetByControlKey(...)`
|
||||
- `ResetLayerParameters(...)`
|
||||
- `LoadStackPreset(...)`
|
||||
|
||||
The target design is not to move all implementation in one step. It is to peel policy and orchestration decisions away first.
|
||||
|
||||
### `RuntimeServices`
|
||||
|
||||
Current OSC-specific flow in `RuntimeServices` includes:
|
||||
|
||||
- queueing updates
|
||||
- applying pending updates
|
||||
- queueing commits
|
||||
- consuming completed commits
|
||||
- clearing OSC state
|
||||
|
||||
See [RuntimeServices.h](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/control/RuntimeServices.h:46).
|
||||
|
||||
The coordinator should eventually own the rules for when these updates are transient, when they commit, and what reset/reload does to them, while `ControlServices` keeps only the ingress mechanics.
|
||||
|
||||
## Recommended Internal Model
|
||||
|
||||
The coordinator should remain small enough to reason about. A good target is to split its internal logic into policy-focused helpers rather than letting one class become another `RuntimeHost`.
|
||||
|
||||
Possible internal helper concepts:
|
||||
|
||||
- `LayerMutationPolicy`
|
||||
- `ParameterMutationPolicy`
|
||||
- `PresetMutationPolicy`
|
||||
- `ReloadPolicy`
|
||||
- `OverlayPolicy`
|
||||
|
||||
That can still be presented as one subsystem to the rest of the app, while keeping the implementation testable.
|
||||
|
||||
## Snapshot Publication Contract
|
||||
|
||||
The coordinator should never force callers to know whether a snapshot must be rebuilt. That policy should be owned here.
|
||||
|
||||
Examples:
|
||||
|
||||
- parameter changes require snapshot publication
|
||||
- layer reorder requires snapshot publication
|
||||
- shader swap requires snapshot publication and render-local rebuild work
|
||||
- stack preset load requires snapshot publication and likely broader transient-state invalidation
|
||||
- pure health/status changes do not require snapshot publication
|
||||
|
||||
This contract matters because current call sites often use coarse actions like `ReloadShader()` after structural edits. The coordinator should return a more precise outcome than "reload or not."
|
||||
|
||||
## Reload and Reset Policy
|
||||
|
||||
Reload and reset behavior has been a recurring source of edge cases in the current app, especially with shader feedback, temporal history, and OSC overlay state.
|
||||
|
||||
The coordinator should define explicit reset scopes such as:
|
||||
|
||||
- parameter-values-only reset
|
||||
- committed-live-state reset for a layer
|
||||
- transient-overlay reset for a layer
|
||||
- render-local-history reset for a layer
|
||||
- whole-stack structural reset
|
||||
- reload-induced compatibility reset
|
||||
|
||||
That allows later phases to stop encoding reset behavior implicitly in UI handlers or render rebuild code.
|
||||
|
||||
Phase 5 has made this more concrete for OSC overlays: coordinator results now carry a named transient OSC invalidation request, with layer-scoped invalidation used for layer removal and manual parameter reset. The render/live-state owner still decides compatibility details, but callers no longer infer transient reset behavior from a generic boolean.
|
||||
|
||||
## Migration Plan From Current Code
|
||||
|
||||
The coordinator should be introduced incrementally.
|
||||
|
||||
### Step 1. Define request and result types
|
||||
|
||||
Introduce typed mutation request/result objects without changing most internals yet.
|
||||
|
||||
### Step 2. Wrap direct runtime mutations behind coordinator entrypoints
|
||||
|
||||
The first implementation could still delegate heavily into existing runtime mutation paths, but the call sites should stop deciding policy on their own.
|
||||
|
||||
For example, instead of:
|
||||
|
||||
1. `OpenGLComposite::AddLayer()`
|
||||
2. direct layer-add mutation
|
||||
3. `ReloadShader(true)`
|
||||
4. `broadcastRuntimeState()`
|
||||
|
||||
the flow becomes:
|
||||
|
||||
1. `OpenGLComposite` or `ControlServices` creates a typed request
|
||||
2. `RuntimeCoordinator::ApplyMutation(...)`
|
||||
3. coordinator returns a result describing snapshot, reset, and persistence needs
|
||||
4. composition root dispatches those downstream effects
|
||||
|
||||
### Step 3. Move validation and classification out of direct mutation helpers
|
||||
|
||||
Once coordinator entrypoints are stable, pull up:
|
||||
|
||||
- mutation classification
|
||||
- reset/reload policy
|
||||
- transient-versus-durable decisions
|
||||
|
||||
while leaving raw store operations in place.
|
||||
|
||||
### Step 4. Split storage and snapshot collaborators
|
||||
|
||||
Only after the coordinator is clearly owning policy should storage and snapshot responsibilities be split into real target subsystems.
|
||||
|
||||
## Key Risks
|
||||
|
||||
### Risk 1. Coordinator becomes a new god object
|
||||
|
||||
If the coordinator starts owning persistence details, status counters, or render reset mechanics directly, it will just recreate the current problem under a new name.
|
||||
|
||||
Mitigation:
|
||||
|
||||
- keep collaborators explicit
|
||||
- keep request/result types narrow
|
||||
- avoid direct dependencies on render or backend internals
|
||||
|
||||
### Risk 2. Call sites bypass coordinator during migration
|
||||
|
||||
If new code bypasses `RuntimeCoordinator` for convenience, the architecture will fork into two policy systems.
|
||||
|
||||
Mitigation:
|
||||
|
||||
- treat the coordinator as the required entrypoint for new non-render mutations
|
||||
- add compatibility adapters rather than parallel mutation paths
|
||||
|
||||
### Risk 3. Too much policy stays implicit in return conventions
|
||||
|
||||
If callers still infer policy from "which method was called," the coordinator will not actually clarify the system.
|
||||
|
||||
Mitigation:
|
||||
|
||||
- return explicit mutation outcomes
|
||||
- define reset and publication scopes as named concepts
|
||||
|
||||
### Risk 4. Transient-state ownership remains fuzzy
|
||||
|
||||
OSC overlay behavior, feedback invalidation, and reload compatibility can easily blur subsystem boundaries again.
|
||||
|
||||
Mitigation:
|
||||
|
||||
- coordinator owns classification rules
|
||||
- subsystem owners retain storage ownership
|
||||
- reset scopes are explicit
|
||||
|
||||
## Open Questions
|
||||
|
||||
- Should 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
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user