Compare commits
123 Commits
v0.0.5
...
f589b1e1fe
| Author | SHA1 | Date | |
|---|---|---|---|
| f589b1e1fe | |||
| 7e17315e74 | |||
| bfaa3f5e0e | |||
| 1d4eb7a34c | |||
| f461a05c65 | |||
|
|
3ffb562ff7 | ||
|
|
c2d548499c | ||
|
|
6a0340d1b4 | ||
|
|
5c1fc2a6cf | ||
|
|
d411453f80 | ||
|
|
4a049a557a | ||
|
|
13586c611a | ||
|
|
3a83d9617f | ||
|
|
5c66cfdc64 | ||
|
|
d72272b5a8 | ||
|
|
c25ae7b25b | ||
|
|
a39be6fb20 | ||
|
|
0a1fe440d9 | ||
|
|
3e45bba54b | ||
|
|
fd4b70ec9c | ||
|
|
ce28904891 | ||
|
|
2c5e925b97 | ||
|
|
957c0be05a | ||
|
|
0a8b335048 | ||
|
|
6e32941675 | ||
|
|
5fb4607d8c | ||
|
|
f43b6f6519 | ||
|
|
dfd49fd0e3 | ||
|
|
1429b2e660 | ||
|
|
02b221f481 | ||
|
|
6a33bd02ab | ||
|
|
da7e1a93f6 | ||
|
|
334693f28c | ||
|
|
c5fd8e72b4 | ||
|
|
95b4a54326 | ||
|
|
d07ea1f63a | ||
|
|
1ddcf5d621 | ||
|
|
38d729b346 | ||
|
|
4b62627479 | ||
|
|
430cf0733d | ||
|
|
b44504500a | ||
|
|
bc690e2a87 | ||
|
|
9938a6cc26 | ||
|
|
79f7ac6c86 | ||
|
|
44b198b14d | ||
|
|
511b67c9bc | ||
|
|
c0d7e84495 | ||
|
|
4ea829af85 | ||
|
|
e0ca548ef5 | ||
|
|
2531d871e8 | ||
|
|
709d3d3fa4 | ||
|
|
ea31d0ca13 | ||
|
|
f1f4e3421b | ||
|
|
ac729dc2b9 | ||
|
|
bf23cd880a | ||
|
|
9e3412712c | ||
|
|
a434a88108 | ||
|
|
c5cead6003 | ||
|
|
f8adbbe0fe | ||
|
|
0a7954e879 | ||
|
|
f288455709 | ||
|
|
50d5880835 | ||
|
|
52eaf16a8c | ||
|
|
6b0638336a | ||
|
|
0da6ad6802 | ||
|
|
dd3cd6b66c | ||
|
|
1d08dec5fe | ||
|
|
0d57920bc1 | ||
|
|
1629dbc77a | ||
|
|
205c90e52e | ||
|
|
ab38bfad24 | ||
|
|
68503256dc | ||
|
|
a91cc91a21 | ||
|
|
a530325fa1 | ||
|
|
d332dceb5b | ||
|
|
79855d788c | ||
|
|
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 |
@@ -7,6 +7,9 @@ on:
|
||||
- master
|
||||
- develop
|
||||
pull_request:
|
||||
schedule:
|
||||
# Nightly build at 14:00 UTC, roughly midnight in Australia/Sydney.
|
||||
- cron: "0 14 * * *"
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
@@ -57,11 +60,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
|
||||
@@ -82,6 +85,7 @@ jobs:
|
||||
package-windows:
|
||||
name: Windows Release Package
|
||||
runs-on: windows-2022
|
||||
if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch'
|
||||
needs:
|
||||
- native-windows
|
||||
- ui-ubuntu
|
||||
@@ -136,7 +140,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
|
||||
|
||||
15
.vscode/launch.json
vendored
15
.vscode/launch.json
vendored
@@ -2,16 +2,21 @@
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "Debug LoopThroughWithOpenGLCompositing",
|
||||
"name": "Debug RenderCadenceCompositor",
|
||||
"type": "cppvsdbg",
|
||||
"request": "launch",
|
||||
"program": "${workspaceFolder}\\build\\vs2022-x64-debug\\Debug\\LoopThroughWithOpenGLCompositing.exe",
|
||||
"program": "${workspaceFolder}\\build\\vs2022-x64-debug\\Debug\\RenderCadenceCompositor.exe",
|
||||
"args": [],
|
||||
"stopAtEntry": false,
|
||||
"cwd": "${workspaceFolder}\\build\\vs2022-x64-debug\\Debug",
|
||||
"cwd": "${workspaceFolder}",
|
||||
"environment": [],
|
||||
"console": "internalConsole",
|
||||
"preLaunchTask": "Build LoopThroughWithOpenGLCompositing Debug x64"
|
||||
"console": "externalTerminal",
|
||||
"symbolSearchPath": "${workspaceFolder}\\build\\vs2022-x64-debug\\Debug",
|
||||
"requireExactSource": true,
|
||||
"logging": {
|
||||
"moduleLoad": true
|
||||
},
|
||||
"preLaunchTask": "Build RenderCadenceCompositor Debug x64"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
46
.vscode/tasks.json
vendored
46
.vscode/tasks.json
vendored
@@ -2,42 +2,72 @@
|
||||
"version": "2.0.0",
|
||||
"tasks": [
|
||||
{
|
||||
"label": "Build LoopThroughWithOpenGLCompositing Debug x64",
|
||||
"label": "Configure Debug x64",
|
||||
"type": "process",
|
||||
"command": "C:\\Program Files\\Microsoft Visual Studio\\2022\\Community\\Common7\\IDE\\CommonExtensions\\Microsoft\\CMake\\CMake\\bin\\cmake.exe",
|
||||
"command": "cmake",
|
||||
"args": [
|
||||
"--preset",
|
||||
"vs2022-x64-debug"
|
||||
],
|
||||
"problemMatcher": "$msCompile"
|
||||
},
|
||||
{
|
||||
"label": "Build RenderCadenceCompositor Debug x64",
|
||||
"type": "process",
|
||||
"command": "cmake",
|
||||
"args": [
|
||||
"--build",
|
||||
"${workspaceFolder}\\build\\vs2022-x64-debug",
|
||||
"--config",
|
||||
"Debug",
|
||||
"--target",
|
||||
"LoopThroughWithOpenGLCompositing"
|
||||
"RenderCadenceCompositor",
|
||||
"--parallel"
|
||||
],
|
||||
"group": {
|
||||
"kind": "build",
|
||||
"isDefault": true
|
||||
},
|
||||
"dependsOn": "Configure Debug x64",
|
||||
"problemMatcher": "$msCompile"
|
||||
},
|
||||
{
|
||||
"label": "Build LoopThroughWithOpenGLCompositing Release x64",
|
||||
"label": "Build RenderCadenceCompositor Release x64",
|
||||
"type": "process",
|
||||
"command": "C:\\Program Files\\Microsoft Visual Studio\\2022\\Community\\Common7\\IDE\\CommonExtensions\\Microsoft\\CMake\\CMake\\bin\\cmake.exe",
|
||||
"command": "cmake",
|
||||
"args": [
|
||||
"--build",
|
||||
"${workspaceFolder}\\build\\vs2022-x64-release",
|
||||
"--config",
|
||||
"Release",
|
||||
"--target",
|
||||
"LoopThroughWithOpenGLCompositing"
|
||||
"RenderCadenceCompositor",
|
||||
"--parallel"
|
||||
],
|
||||
"group": "build",
|
||||
"problemMatcher": "$msCompile"
|
||||
},
|
||||
{
|
||||
"label": "Clean LoopThroughWithOpenGLCompositing Debug x64",
|
||||
"label": "Run Native Tests Debug x64",
|
||||
"type": "process",
|
||||
"command": "C:\\Program Files\\Microsoft Visual Studio\\2022\\Community\\Common7\\IDE\\CommonExtensions\\Microsoft\\CMake\\CMake\\bin\\cmake.exe",
|
||||
"command": "cmake",
|
||||
"args": [
|
||||
"--build",
|
||||
"${workspaceFolder}\\build\\vs2022-x64-debug",
|
||||
"--config",
|
||||
"Debug",
|
||||
"--target",
|
||||
"RUN_TESTS",
|
||||
"--parallel"
|
||||
],
|
||||
"group": "test",
|
||||
"dependsOn": "Configure Debug x64",
|
||||
"problemMatcher": "$msCompile"
|
||||
},
|
||||
{
|
||||
"label": "Clean Debug x64",
|
||||
"type": "process",
|
||||
"command": "cmake",
|
||||
"args": [
|
||||
"--build",
|
||||
"${workspaceFolder}\\build\\vs2022-x64-debug",
|
||||
|
||||
450
CMakeLists.txt
450
CMakeLists.txt
@@ -1,353 +1,149 @@
|
||||
cmake_minimum_required(VERSION 3.24)
|
||||
|
||||
project(video_shader LANGUAGES C CXX RC)
|
||||
project(video_shader LANGUAGES C CXX)
|
||||
|
||||
include(CTest)
|
||||
|
||||
set(CMAKE_CXX_STANDARD 17)
|
||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||
set(CMAKE_CXX_EXTENSIONS OFF)
|
||||
|
||||
set(APP_DIR "${CMAKE_CURRENT_SOURCE_DIR}/apps/LoopThroughWithOpenGLCompositing")
|
||||
set(SRC_DIR "${CMAKE_CURRENT_SOURCE_DIR}/src")
|
||||
set(TEST_DIR "${CMAKE_CURRENT_SOURCE_DIR}/tests")
|
||||
set(SLANG_ROOT "${CMAKE_CURRENT_SOURCE_DIR}/3rdParty/slang-2026.8-windows-x86_64" CACHE PATH "Path to a Slang binary release containing bin/slangc.exe")
|
||||
|
||||
if(NOT EXISTS "${APP_DIR}/LoopThroughWithOpenGLCompositing.cpp")
|
||||
message(FATAL_ERROR "Imported app sources were not found under ${APP_DIR}")
|
||||
set(VIDEO_SHADER_INCLUDE_DIRS
|
||||
"${SRC_DIR}"
|
||||
"${SRC_DIR}/app"
|
||||
"${SRC_DIR}/control"
|
||||
"${SRC_DIR}/control/http"
|
||||
"${SRC_DIR}/frames"
|
||||
"${SRC_DIR}/json"
|
||||
"${SRC_DIR}/logging"
|
||||
"${SRC_DIR}/platform"
|
||||
"${SRC_DIR}/preview"
|
||||
"${SRC_DIR}/render"
|
||||
"${SRC_DIR}/render/readback"
|
||||
"${SRC_DIR}/render/runtime"
|
||||
"${SRC_DIR}/runtime"
|
||||
"${SRC_DIR}/shader"
|
||||
"${SRC_DIR}/telemetry"
|
||||
"${SRC_DIR}/video"
|
||||
)
|
||||
|
||||
function(video_shader_target_defaults target)
|
||||
target_include_directories(${target} PRIVATE ${VIDEO_SHADER_INCLUDE_DIRS})
|
||||
target_compile_definitions(${target} PRIVATE _UNICODE UNICODE)
|
||||
if(MSVC)
|
||||
target_compile_options(${target} PRIVATE /W3)
|
||||
endif()
|
||||
endfunction()
|
||||
|
||||
function(video_shader_files_exist out_var)
|
||||
set(missing_files)
|
||||
foreach(file IN LISTS ARGN)
|
||||
if(NOT EXISTS "${file}")
|
||||
list(APPEND missing_files "${file}")
|
||||
endif()
|
||||
endforeach()
|
||||
set(${out_var} "${missing_files}" PARENT_SCOPE)
|
||||
endfunction()
|
||||
|
||||
function(add_video_shader_test name)
|
||||
add_executable(${name} ${ARGN})
|
||||
video_shader_target_defaults(${name})
|
||||
add_test(NAME ${name} COMMAND ${name})
|
||||
endfunction()
|
||||
|
||||
set(SLANG_RUNTIME_FILES
|
||||
"${SLANG_ROOT}/bin/slangc.exe"
|
||||
"${SLANG_ROOT}/bin/slang-compiler.dll"
|
||||
"${SLANG_ROOT}/bin/slang-glslang.dll"
|
||||
)
|
||||
set(SLANG_LICENSE_FILE "${SLANG_ROOT}/LICENSE")
|
||||
|
||||
foreach(SLANG_RUNTIME_FILE IN LISTS SLANG_RUNTIME_FILES)
|
||||
if(NOT EXISTS "${SLANG_RUNTIME_FILE}")
|
||||
message(FATAL_ERROR "Required Slang runtime file not found: ${SLANG_RUNTIME_FILE}")
|
||||
set(RENDER_CADENCE_APP_REQUIRED_FILES
|
||||
"${SRC_DIR}/RenderCadenceCompositor.cpp"
|
||||
"${SRC_DIR}/video/DeckLinkAPI_i.c"
|
||||
"${SRC_DIR}/video/DeckLinkAPI_h.h"
|
||||
"${SRC_DIR}/video/DeckLinkDisplayMode.cpp"
|
||||
"${SRC_DIR}/video/DeckLinkDisplayMode.h"
|
||||
"${SRC_DIR}/video/DeckLinkSession.cpp"
|
||||
"${SRC_DIR}/video/DeckLinkSession.h"
|
||||
"${SRC_DIR}/video/DeckLinkVideoIOFormat.cpp"
|
||||
"${SRC_DIR}/video/DeckLinkVideoIOFormat.h"
|
||||
"${SRC_DIR}/video/VideoIOFormat.cpp"
|
||||
"${SRC_DIR}/video/VideoIOFormat.h"
|
||||
"${SRC_DIR}/video/VideoIOTypes.h"
|
||||
"${SRC_DIR}/render/GLExtensions.cpp"
|
||||
"${SRC_DIR}/render/GLExtensions.h"
|
||||
"${SRC_DIR}/render/Std140Buffer.h"
|
||||
"${SRC_DIR}/runtime/RuntimeJson.cpp"
|
||||
"${SRC_DIR}/runtime/RuntimeJson.h"
|
||||
"${SRC_DIR}/runtime/RuntimeParameterUtils.cpp"
|
||||
"${SRC_DIR}/runtime/RuntimeParameterUtils.h"
|
||||
"${SRC_DIR}/shader/ShaderCompiler.cpp"
|
||||
"${SRC_DIR}/shader/ShaderCompiler.h"
|
||||
"${SRC_DIR}/shader/ShaderPackageRegistry.cpp"
|
||||
"${SRC_DIR}/shader/ShaderPackageRegistry.h"
|
||||
"${SRC_DIR}/shader/ShaderTypes.h"
|
||||
)
|
||||
|
||||
video_shader_files_exist(RENDER_CADENCE_APP_MISSING_FILES ${RENDER_CADENCE_APP_REQUIRED_FILES})
|
||||
if(RENDER_CADENCE_APP_MISSING_FILES)
|
||||
message(STATUS "RenderCadenceCompositor target skipped; source reorganization has not provided these legacy shared files:")
|
||||
foreach(missing_file IN LISTS RENDER_CADENCE_APP_MISSING_FILES)
|
||||
message(STATUS " ${missing_file}")
|
||||
endforeach()
|
||||
else()
|
||||
file(GLOB_RECURSE RENDER_CADENCE_APP_SOURCES CONFIGURE_DEPENDS
|
||||
"${SRC_DIR}/*.c"
|
||||
"${SRC_DIR}/*.cpp"
|
||||
"${SRC_DIR}/*.h"
|
||||
)
|
||||
list(REMOVE_ITEM RENDER_CADENCE_APP_SOURCES
|
||||
"${SRC_DIR}/video/VideoBackend.cpp"
|
||||
"${SRC_DIR}/video/VideoBackend.h"
|
||||
)
|
||||
|
||||
add_executable(RenderCadenceCompositor ${RENDER_CADENCE_APP_SOURCES})
|
||||
video_shader_target_defaults(RenderCadenceCompositor)
|
||||
target_link_libraries(RenderCadenceCompositor PRIVATE
|
||||
opengl32
|
||||
Ole32
|
||||
Ws2_32
|
||||
)
|
||||
source_group(TREE "${SRC_DIR}" FILES ${RENDER_CADENCE_APP_SOURCES})
|
||||
endif()
|
||||
|
||||
if(BUILD_TESTING)
|
||||
add_subdirectory(tests)
|
||||
endif()
|
||||
|
||||
if(TARGET RenderCadenceCompositor)
|
||||
install(TARGETS RenderCadenceCompositor
|
||||
RUNTIME DESTINATION "."
|
||||
)
|
||||
endif()
|
||||
|
||||
foreach(slang_runtime_file IN LISTS SLANG_RUNTIME_FILES)
|
||||
if(EXISTS "${slang_runtime_file}")
|
||||
install(FILES "${slang_runtime_file}"
|
||||
DESTINATION "3rdParty/slang/bin"
|
||||
)
|
||||
else()
|
||||
message(STATUS "Slang runtime file not found and will not be installed: ${slang_runtime_file}")
|
||||
endif()
|
||||
endforeach()
|
||||
|
||||
set(SLANG_LICENSE_FILE "${SLANG_ROOT}/LICENSE")
|
||||
if(NOT EXISTS "${SLANG_LICENSE_FILE}")
|
||||
message(FATAL_ERROR "Slang license file not found: ${SLANG_LICENSE_FILE}")
|
||||
endif()
|
||||
|
||||
set(APP_SOURCES
|
||||
"${APP_DIR}/videoio/decklink/DeckLinkAPI_i.c"
|
||||
"${APP_DIR}/control/ControlServer.cpp"
|
||||
"${APP_DIR}/control/ControlServer.h"
|
||||
"${APP_DIR}/control/OscServer.cpp"
|
||||
"${APP_DIR}/control/OscServer.h"
|
||||
"${APP_DIR}/control/RuntimeControlBridge.cpp"
|
||||
"${APP_DIR}/control/RuntimeControlBridge.h"
|
||||
"${APP_DIR}/control/RuntimeServices.cpp"
|
||||
"${APP_DIR}/control/RuntimeServices.h"
|
||||
"${APP_DIR}/videoio/decklink/DeckLinkAPI_h.h"
|
||||
"${APP_DIR}/videoio/decklink/DeckLinkDisplayMode.cpp"
|
||||
"${APP_DIR}/videoio/decklink/DeckLinkDisplayMode.h"
|
||||
"${APP_DIR}/videoio/decklink/DeckLinkFrameTransfer.cpp"
|
||||
"${APP_DIR}/videoio/decklink/DeckLinkFrameTransfer.h"
|
||||
"${APP_DIR}/videoio/decklink/DeckLinkSession.cpp"
|
||||
"${APP_DIR}/videoio/decklink/DeckLinkSession.h"
|
||||
"${APP_DIR}/videoio/decklink/DeckLinkVideoIOFormat.cpp"
|
||||
"${APP_DIR}/videoio/decklink/DeckLinkVideoIOFormat.h"
|
||||
"${APP_DIR}/gl/renderer/GLExtensions.cpp"
|
||||
"${APP_DIR}/gl/renderer/GLExtensions.h"
|
||||
"${APP_DIR}/gl/shader/GlobalParamsBuffer.cpp"
|
||||
"${APP_DIR}/gl/shader/GlobalParamsBuffer.h"
|
||||
"${APP_DIR}/gl/renderer/GlRenderConstants.h"
|
||||
"${APP_DIR}/gl/renderer/GlScopedObjects.h"
|
||||
"${APP_DIR}/gl/shader/GlShaderSources.cpp"
|
||||
"${APP_DIR}/gl/shader/GlShaderSources.h"
|
||||
"${APP_DIR}/gl/OpenGLComposite.cpp"
|
||||
"${APP_DIR}/gl/OpenGLComposite.h"
|
||||
"${APP_DIR}/gl/OpenGLCompositeRuntimeControls.cpp"
|
||||
"${APP_DIR}/gl/pipeline/OpenGLRenderPass.cpp"
|
||||
"${APP_DIR}/gl/pipeline/OpenGLRenderPass.h"
|
||||
"${APP_DIR}/gl/pipeline/OpenGLRenderPipeline.cpp"
|
||||
"${APP_DIR}/gl/pipeline/OpenGLRenderPipeline.h"
|
||||
"${APP_DIR}/gl/pipeline/RenderPassDescriptor.h"
|
||||
"${APP_DIR}/gl/pipeline/ShaderFeedbackBuffers.cpp"
|
||||
"${APP_DIR}/gl/pipeline/ShaderFeedbackBuffers.h"
|
||||
"${APP_DIR}/gl/renderer/OpenGLRenderer.cpp"
|
||||
"${APP_DIR}/gl/renderer/OpenGLRenderer.h"
|
||||
"${APP_DIR}/gl/renderer/RenderTargetPool.cpp"
|
||||
"${APP_DIR}/gl/renderer/RenderTargetPool.h"
|
||||
"${APP_DIR}/gl/pipeline/OpenGLVideoIOBridge.cpp"
|
||||
"${APP_DIR}/gl/pipeline/OpenGLVideoIOBridge.h"
|
||||
"${APP_DIR}/gl/shader/OpenGLShaderPrograms.cpp"
|
||||
"${APP_DIR}/gl/shader/OpenGLShaderPrograms.h"
|
||||
"${APP_DIR}/gl/pipeline/PngScreenshotWriter.cpp"
|
||||
"${APP_DIR}/gl/pipeline/PngScreenshotWriter.h"
|
||||
"${APP_DIR}/gl/shader/ShaderProgramCompiler.cpp"
|
||||
"${APP_DIR}/gl/shader/ShaderProgramCompiler.h"
|
||||
"${APP_DIR}/gl/shader/ShaderBuildQueue.cpp"
|
||||
"${APP_DIR}/gl/shader/ShaderBuildQueue.h"
|
||||
"${APP_DIR}/gl/shader/ShaderTextureBindings.cpp"
|
||||
"${APP_DIR}/gl/shader/ShaderTextureBindings.h"
|
||||
"${APP_DIR}/gl/shader/Std140Buffer.h"
|
||||
"${APP_DIR}/gl/shader/TextRasterizer.cpp"
|
||||
"${APP_DIR}/gl/shader/TextRasterizer.h"
|
||||
"${APP_DIR}/gl/shader/TextureAssetLoader.cpp"
|
||||
"${APP_DIR}/gl/shader/TextureAssetLoader.h"
|
||||
"${APP_DIR}/gl/pipeline/TemporalHistoryBuffers.cpp"
|
||||
"${APP_DIR}/gl/pipeline/TemporalHistoryBuffers.h"
|
||||
"${APP_DIR}/LoopThroughWithOpenGLCompositing.cpp"
|
||||
"${APP_DIR}/LoopThroughWithOpenGLCompositing.h"
|
||||
"${APP_DIR}/LoopThroughWithOpenGLCompositing.rc"
|
||||
"${APP_DIR}/platform/NativeHandles.h"
|
||||
"${APP_DIR}/platform/NativeSockets.h"
|
||||
"${APP_DIR}/resource.h"
|
||||
"${APP_DIR}/runtime/RuntimeHost.cpp"
|
||||
"${APP_DIR}/runtime/RuntimeHost.h"
|
||||
"${APP_DIR}/runtime/RuntimeClock.cpp"
|
||||
"${APP_DIR}/runtime/RuntimeClock.h"
|
||||
"${APP_DIR}/runtime/RuntimeJson.cpp"
|
||||
"${APP_DIR}/runtime/RuntimeJson.h"
|
||||
"${APP_DIR}/runtime/RuntimeParameterUtils.cpp"
|
||||
"${APP_DIR}/runtime/RuntimeParameterUtils.h"
|
||||
"${APP_DIR}/shader/ShaderCompiler.cpp"
|
||||
"${APP_DIR}/shader/ShaderCompiler.h"
|
||||
"${APP_DIR}/shader/ShaderPackageRegistry.cpp"
|
||||
"${APP_DIR}/shader/ShaderPackageRegistry.h"
|
||||
"${APP_DIR}/shader/ShaderTypes.h"
|
||||
"${APP_DIR}/stdafx.cpp"
|
||||
"${APP_DIR}/stdafx.h"
|
||||
"${APP_DIR}/targetver.h"
|
||||
"${APP_DIR}/videoio/VideoIOFormat.cpp"
|
||||
"${APP_DIR}/videoio/VideoIOFormat.h"
|
||||
"${APP_DIR}/videoio/VideoIOTypes.h"
|
||||
"${APP_DIR}/videoio/VideoPlayoutScheduler.cpp"
|
||||
"${APP_DIR}/videoio/VideoPlayoutScheduler.h"
|
||||
)
|
||||
|
||||
add_executable(LoopThroughWithOpenGLCompositing WIN32 ${APP_SOURCES})
|
||||
|
||||
target_include_directories(LoopThroughWithOpenGLCompositing PRIVATE
|
||||
"${APP_DIR}"
|
||||
"${APP_DIR}/control"
|
||||
"${APP_DIR}/gl"
|
||||
"${APP_DIR}/gl/pipeline"
|
||||
"${APP_DIR}/gl/renderer"
|
||||
"${APP_DIR}/gl/shader"
|
||||
"${APP_DIR}/platform"
|
||||
"${APP_DIR}/runtime"
|
||||
"${APP_DIR}/shader"
|
||||
"${APP_DIR}/videoio"
|
||||
"${APP_DIR}/videoio/decklink"
|
||||
)
|
||||
|
||||
target_link_libraries(LoopThroughWithOpenGLCompositing PRIVATE
|
||||
opengl32
|
||||
glu32
|
||||
Ws2_32
|
||||
Crypt32
|
||||
Advapi32
|
||||
Gdiplus
|
||||
Ole32
|
||||
Windowscodecs
|
||||
)
|
||||
|
||||
target_compile_definitions(LoopThroughWithOpenGLCompositing PRIVATE
|
||||
_UNICODE
|
||||
UNICODE
|
||||
)
|
||||
|
||||
if(MSVC)
|
||||
target_compile_options(LoopThroughWithOpenGLCompositing PRIVATE /W3)
|
||||
endif()
|
||||
|
||||
add_executable(RuntimeJsonTests
|
||||
"${APP_DIR}/runtime/RuntimeJson.cpp"
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/tests/RuntimeJsonTests.cpp"
|
||||
)
|
||||
|
||||
target_include_directories(RuntimeJsonTests PRIVATE
|
||||
"${APP_DIR}"
|
||||
"${APP_DIR}/runtime"
|
||||
)
|
||||
|
||||
if(MSVC)
|
||||
target_compile_options(RuntimeJsonTests PRIVATE /W3)
|
||||
endif()
|
||||
|
||||
enable_testing()
|
||||
add_test(NAME RuntimeJsonTests COMMAND RuntimeJsonTests)
|
||||
|
||||
add_executable(RuntimeClockTests
|
||||
"${APP_DIR}/runtime/RuntimeClock.cpp"
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/tests/RuntimeClockTests.cpp"
|
||||
)
|
||||
|
||||
target_include_directories(RuntimeClockTests PRIVATE
|
||||
"${APP_DIR}"
|
||||
"${APP_DIR}/runtime"
|
||||
)
|
||||
|
||||
if(MSVC)
|
||||
target_compile_options(RuntimeClockTests PRIVATE /W3)
|
||||
endif()
|
||||
|
||||
add_test(NAME RuntimeClockTests COMMAND RuntimeClockTests)
|
||||
|
||||
add_executable(RuntimeParameterUtilsTests
|
||||
"${APP_DIR}/runtime/RuntimeJson.cpp"
|
||||
"${APP_DIR}/runtime/RuntimeParameterUtils.cpp"
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/tests/RuntimeParameterUtilsTests.cpp"
|
||||
)
|
||||
|
||||
target_include_directories(RuntimeParameterUtilsTests PRIVATE
|
||||
"${APP_DIR}"
|
||||
"${APP_DIR}/runtime"
|
||||
"${APP_DIR}/shader"
|
||||
)
|
||||
|
||||
if(MSVC)
|
||||
target_compile_options(RuntimeParameterUtilsTests PRIVATE /W3)
|
||||
endif()
|
||||
|
||||
add_test(NAME RuntimeParameterUtilsTests COMMAND RuntimeParameterUtilsTests)
|
||||
|
||||
add_executable(Std140BufferTests
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/tests/Std140BufferTests.cpp"
|
||||
)
|
||||
|
||||
target_include_directories(Std140BufferTests PRIVATE
|
||||
"${APP_DIR}"
|
||||
"${APP_DIR}/gl"
|
||||
"${APP_DIR}/gl/shader"
|
||||
)
|
||||
|
||||
if(MSVC)
|
||||
target_compile_options(Std140BufferTests PRIVATE /W3)
|
||||
endif()
|
||||
|
||||
add_test(NAME Std140BufferTests COMMAND Std140BufferTests)
|
||||
|
||||
add_executable(ShaderPackageRegistryTests
|
||||
"${APP_DIR}/runtime/RuntimeJson.cpp"
|
||||
"${APP_DIR}/shader/ShaderPackageRegistry.cpp"
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/tests/ShaderPackageRegistryTests.cpp"
|
||||
)
|
||||
|
||||
target_include_directories(ShaderPackageRegistryTests PRIVATE
|
||||
"${APP_DIR}"
|
||||
"${APP_DIR}/runtime"
|
||||
"${APP_DIR}/shader"
|
||||
)
|
||||
|
||||
if(MSVC)
|
||||
target_compile_options(ShaderPackageRegistryTests PRIVATE /W3)
|
||||
endif()
|
||||
|
||||
add_test(NAME ShaderPackageRegistryTests COMMAND ShaderPackageRegistryTests)
|
||||
|
||||
add_executable(ShaderSlangValidationTests
|
||||
"${APP_DIR}/runtime/RuntimeJson.cpp"
|
||||
"${APP_DIR}/shader/ShaderCompiler.cpp"
|
||||
"${APP_DIR}/shader/ShaderPackageRegistry.cpp"
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/tests/ShaderSlangValidationTests.cpp"
|
||||
)
|
||||
|
||||
target_include_directories(ShaderSlangValidationTests PRIVATE
|
||||
"${APP_DIR}"
|
||||
"${APP_DIR}/platform"
|
||||
"${APP_DIR}/runtime"
|
||||
"${APP_DIR}/shader"
|
||||
)
|
||||
|
||||
if(MSVC)
|
||||
target_compile_options(ShaderSlangValidationTests PRIVATE /W3)
|
||||
endif()
|
||||
|
||||
add_test(NAME ShaderSlangValidationTests COMMAND ShaderSlangValidationTests)
|
||||
set_tests_properties(ShaderSlangValidationTests PROPERTIES
|
||||
ENVIRONMENT "SLANG_ROOT=${SLANG_ROOT}"
|
||||
)
|
||||
|
||||
add_executable(OscServerTests
|
||||
"${APP_DIR}/control/OscServer.cpp"
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/tests/OscServerTests.cpp"
|
||||
)
|
||||
|
||||
target_include_directories(OscServerTests PRIVATE
|
||||
"${APP_DIR}"
|
||||
"${APP_DIR}/control"
|
||||
"${APP_DIR}/platform"
|
||||
)
|
||||
|
||||
target_link_libraries(OscServerTests PRIVATE
|
||||
Ws2_32
|
||||
)
|
||||
|
||||
if(MSVC)
|
||||
target_compile_options(OscServerTests PRIVATE /W3)
|
||||
endif()
|
||||
|
||||
add_test(NAME OscServerTests COMMAND OscServerTests)
|
||||
|
||||
add_executable(VideoIOFormatTests
|
||||
"${APP_DIR}/videoio/decklink/DeckLinkVideoIOFormat.cpp"
|
||||
"${APP_DIR}/videoio/VideoIOFormat.cpp"
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/tests/VideoIOFormatTests.cpp"
|
||||
)
|
||||
|
||||
target_include_directories(VideoIOFormatTests PRIVATE
|
||||
"${APP_DIR}"
|
||||
"${APP_DIR}/videoio"
|
||||
"${APP_DIR}/videoio/decklink"
|
||||
)
|
||||
|
||||
if(MSVC)
|
||||
target_compile_options(VideoIOFormatTests PRIVATE /W3)
|
||||
endif()
|
||||
|
||||
add_test(NAME VideoIOFormatTests COMMAND VideoIOFormatTests)
|
||||
|
||||
add_executable(VideoPlayoutSchedulerTests
|
||||
"${APP_DIR}/videoio/VideoPlayoutScheduler.cpp"
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/tests/VideoPlayoutSchedulerTests.cpp"
|
||||
)
|
||||
|
||||
target_include_directories(VideoPlayoutSchedulerTests PRIVATE
|
||||
"${APP_DIR}"
|
||||
"${APP_DIR}/videoio"
|
||||
"${APP_DIR}/videoio/decklink"
|
||||
)
|
||||
|
||||
if(MSVC)
|
||||
target_compile_options(VideoPlayoutSchedulerTests PRIVATE /W3)
|
||||
endif()
|
||||
|
||||
add_test(NAME VideoPlayoutSchedulerTests COMMAND VideoPlayoutSchedulerTests)
|
||||
|
||||
add_executable(VideoIODeviceFakeTests
|
||||
"${APP_DIR}/videoio/VideoIOFormat.cpp"
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/tests/VideoIODeviceFakeTests.cpp"
|
||||
)
|
||||
|
||||
target_include_directories(VideoIODeviceFakeTests PRIVATE
|
||||
"${APP_DIR}"
|
||||
"${APP_DIR}/videoio"
|
||||
"${APP_DIR}/videoio/decklink"
|
||||
)
|
||||
|
||||
if(MSVC)
|
||||
target_compile_options(VideoIODeviceFakeTests PRIVATE /W3)
|
||||
endif()
|
||||
|
||||
add_test(NAME VideoIODeviceFakeTests COMMAND VideoIODeviceFakeTests)
|
||||
|
||||
install(TARGETS LoopThroughWithOpenGLCompositing
|
||||
RUNTIME DESTINATION "."
|
||||
)
|
||||
|
||||
install(FILES ${SLANG_RUNTIME_FILES}
|
||||
DESTINATION "3rdParty/slang/bin"
|
||||
)
|
||||
|
||||
if(EXISTS "${SLANG_LICENSE_FILE}")
|
||||
install(FILES "${SLANG_LICENSE_FILE}"
|
||||
DESTINATION "third_party_notices"
|
||||
RENAME "SLANG_LICENSE.txt"
|
||||
)
|
||||
else()
|
||||
message(STATUS "Slang license file not found and will not be installed: ${SLANG_LICENSE_FILE}")
|
||||
endif()
|
||||
|
||||
install(FILES "${CMAKE_CURRENT_SOURCE_DIR}/shaders/SHADER_CONTRACT.md"
|
||||
DESTINATION "."
|
||||
@@ -378,5 +174,3 @@ install(DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/docs/"
|
||||
DESTINATION "docs"
|
||||
OPTIONAL
|
||||
)
|
||||
|
||||
source_group(TREE "${APP_DIR}" FILES ${APP_SOURCES})
|
||||
|
||||
@@ -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,589 +0,0 @@
|
||||
/* -LICENSE-START-
|
||||
** Copyright (c) 2012 Blackmagic Design
|
||||
**
|
||||
** Permission is hereby granted, free of charge, to any person or organization
|
||||
** obtaining a copy of the software and accompanying documentation (the
|
||||
** "Software") to use, reproduce, display, distribute, sub-license, execute,
|
||||
** and transmit the Software, and to prepare derivative works of the Software,
|
||||
** and to permit third-parties to whom the Software is furnished to do so, in
|
||||
** accordance with:
|
||||
**
|
||||
** (1) if the Software is obtained from Blackmagic Design, the End User License
|
||||
** Agreement for the Software Development Kit ("EULA") available at
|
||||
** https://www.blackmagicdesign.com/EULA/DeckLinkSDK; or
|
||||
**
|
||||
** (2) if the Software is obtained from any third party, such licensing terms
|
||||
** as notified by that third party,
|
||||
**
|
||||
** and all subject to the following:
|
||||
**
|
||||
** (3) the copyright notices in the Software and this entire statement,
|
||||
** including the above license grant, this restriction and the following
|
||||
** disclaimer, must be included in all copies of the Software, in whole or in
|
||||
** part, and all derivative works of the Software, unless such copies or
|
||||
** derivative works are solely in the form of machine-executable object code
|
||||
** generated by a source language processor.
|
||||
**
|
||||
** (4) THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
||||
** OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
** FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT
|
||||
** SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE
|
||||
** FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE,
|
||||
** ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
** DEALINGS IN THE SOFTWARE.
|
||||
**
|
||||
** A copy of the Software is available free of charge at
|
||||
** https://www.blackmagicdesign.com/desktopvideo_sdk under the EULA.
|
||||
**
|
||||
** -LICENSE-END-
|
||||
*/
|
||||
//
|
||||
// LoopThroughWithOpenGLCompositing.cpp
|
||||
// LoopThroughWithOpenGLCompositing
|
||||
//
|
||||
|
||||
#include "stdafx.h"
|
||||
#include "resource.h"
|
||||
#include "OpenGLComposite.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <shellapi.h>
|
||||
#include <string>
|
||||
|
||||
#ifndef WGL_CONTEXT_MAJOR_VERSION_ARB
|
||||
#define WGL_CONTEXT_MAJOR_VERSION_ARB 0x2091
|
||||
#endif
|
||||
#ifndef WGL_CONTEXT_MINOR_VERSION_ARB
|
||||
#define WGL_CONTEXT_MINOR_VERSION_ARB 0x2092
|
||||
#endif
|
||||
#ifndef WGL_CONTEXT_PROFILE_MASK_ARB
|
||||
#define WGL_CONTEXT_PROFILE_MASK_ARB 0x9126
|
||||
#endif
|
||||
#ifndef WGL_CONTEXT_CORE_PROFILE_BIT_ARB
|
||||
#define WGL_CONTEXT_CORE_PROFILE_BIT_ARB 0x00000001
|
||||
#endif
|
||||
|
||||
#define MAX_LOADSTRING 100
|
||||
|
||||
// Declaration for Window procedure
|
||||
LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam);
|
||||
typedef HGLRC (WINAPI* PFNWGLCREATECONTEXTATTRIBSARBPROC)(HDC hdc, HGLRC hShareContext, const int* attribList);
|
||||
|
||||
namespace
|
||||
{
|
||||
const int kStatusPanelWidth = 680;
|
||||
const int kStatusPanelHeight = 92;
|
||||
const int kStatusPadding = 8;
|
||||
const int kStatusLabelWidth = 58;
|
||||
const int kStatusButtonWidth = 86;
|
||||
const int kStatusRowHeight = 24;
|
||||
const int kStatusGap = 6;
|
||||
const UINT kCreateStatusStripMessage = WM_APP + 1;
|
||||
|
||||
enum StatusControlId
|
||||
{
|
||||
kControlUrlEditId = 2001,
|
||||
kDocsUrlEditId = 2002,
|
||||
kOscAddressEditId = 2003,
|
||||
kOpenControlButtonId = 2004,
|
||||
kOpenDocsButtonId = 2005
|
||||
};
|
||||
|
||||
struct StatusStripControls
|
||||
{
|
||||
HWND panel = NULL;
|
||||
HWND controlLabel = NULL;
|
||||
HWND controlUrl = NULL;
|
||||
HWND openControl = NULL;
|
||||
HWND docsLabel = NULL;
|
||||
HWND docsUrl = NULL;
|
||||
HWND openDocs = NULL;
|
||||
HWND oscLabel = NULL;
|
||||
HWND oscAddress = NULL;
|
||||
};
|
||||
|
||||
bool StatusStripCreated(const StatusStripControls& controls)
|
||||
{
|
||||
return controls.panel != NULL;
|
||||
}
|
||||
|
||||
HWND CreateStatusChild(HWND parent, const char* className, const char* text, DWORD style, DWORD exStyle, int controlId)
|
||||
{
|
||||
return CreateWindowExA(
|
||||
exStyle,
|
||||
className,
|
||||
text,
|
||||
WS_CHILD | WS_VISIBLE | WS_CLIPSIBLINGS | style,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
parent,
|
||||
reinterpret_cast<HMENU>(static_cast<INT_PTR>(controlId)),
|
||||
reinterpret_cast<HINSTANCE>(GetWindowLongPtr(parent, GWLP_HINSTANCE)),
|
||||
NULL);
|
||||
}
|
||||
|
||||
void CreateStatusStrip(HWND hWnd, StatusStripControls& controls)
|
||||
{
|
||||
controls.panel = CreateStatusChild(hWnd, "STATIC", "", SS_LEFT, WS_EX_CLIENTEDGE, 0);
|
||||
controls.controlLabel = CreateStatusChild(hWnd, "STATIC", "Control", SS_LEFT, 0, 0);
|
||||
controls.controlUrl = CreateStatusChild(hWnd, "EDIT", "", ES_AUTOHSCROLL | ES_READONLY | WS_TABSTOP, WS_EX_CLIENTEDGE, kControlUrlEditId);
|
||||
controls.openControl = CreateStatusChild(hWnd, "BUTTON", "Open", BS_PUSHBUTTON | WS_TABSTOP, 0, kOpenControlButtonId);
|
||||
controls.docsLabel = CreateStatusChild(hWnd, "STATIC", "Docs", SS_LEFT, 0, 0);
|
||||
controls.docsUrl = CreateStatusChild(hWnd, "EDIT", "", ES_AUTOHSCROLL | ES_READONLY | WS_TABSTOP, WS_EX_CLIENTEDGE, kDocsUrlEditId);
|
||||
controls.openDocs = CreateStatusChild(hWnd, "BUTTON", "Open", BS_PUSHBUTTON | WS_TABSTOP, 0, kOpenDocsButtonId);
|
||||
controls.oscLabel = CreateStatusChild(hWnd, "STATIC", "OSC", SS_LEFT, 0, 0);
|
||||
controls.oscAddress = CreateStatusChild(hWnd, "EDIT", "", ES_AUTOHSCROLL | ES_READONLY | WS_TABSTOP, WS_EX_CLIENTEDGE, kOscAddressEditId);
|
||||
|
||||
HFONT guiFont = reinterpret_cast<HFONT>(GetStockObject(DEFAULT_GUI_FONT));
|
||||
HWND children[] = {
|
||||
controls.controlLabel,
|
||||
controls.controlUrl,
|
||||
controls.openControl,
|
||||
controls.docsLabel,
|
||||
controls.docsUrl,
|
||||
controls.openDocs,
|
||||
controls.oscLabel,
|
||||
controls.oscAddress
|
||||
};
|
||||
for (HWND child : children)
|
||||
{
|
||||
if (child)
|
||||
SendMessage(child, WM_SETFONT, reinterpret_cast<WPARAM>(guiFont), TRUE);
|
||||
}
|
||||
|
||||
SetWindowTextA(controls.controlUrl, "Starting control server...");
|
||||
SetWindowTextA(controls.docsUrl, "Starting API docs...");
|
||||
SetWindowTextA(controls.oscAddress, "Starting OSC listener...");
|
||||
}
|
||||
|
||||
void RaiseStatusControls(const StatusStripControls& controls)
|
||||
{
|
||||
if (!StatusStripCreated(controls))
|
||||
return;
|
||||
|
||||
SetWindowPos(controls.panel, HWND_BOTTOM, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE);
|
||||
|
||||
HWND interactiveControls[] = {
|
||||
controls.controlLabel,
|
||||
controls.controlUrl,
|
||||
controls.openControl,
|
||||
controls.docsLabel,
|
||||
controls.docsUrl,
|
||||
controls.openDocs,
|
||||
controls.oscLabel,
|
||||
controls.oscAddress
|
||||
};
|
||||
for (HWND control : interactiveControls)
|
||||
{
|
||||
if (control)
|
||||
SetWindowPos(control, HWND_TOP, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE);
|
||||
}
|
||||
}
|
||||
|
||||
void LayoutStatusStrip(HWND hWnd, const StatusStripControls& controls)
|
||||
{
|
||||
RECT clientRect = {};
|
||||
if (!GetClientRect(hWnd, &clientRect) || !controls.panel)
|
||||
return;
|
||||
|
||||
const int clientWidth = static_cast<int>(clientRect.right - clientRect.left);
|
||||
const int clientHeight = static_cast<int>(clientRect.bottom - clientRect.top);
|
||||
const int panelWidth = std::max(280, std::min(kStatusPanelWidth, clientWidth - (kStatusPadding * 2)));
|
||||
const int panelHeight = kStatusPanelHeight;
|
||||
const int panelLeft = kStatusPadding;
|
||||
const int panelTop = std::max(kStatusPadding, clientHeight - panelHeight - kStatusPadding);
|
||||
MoveWindow(controls.panel, panelLeft, panelTop, panelWidth, panelHeight, TRUE);
|
||||
|
||||
const int rowX = panelLeft + kStatusPadding;
|
||||
const int editX = rowX + kStatusLabelWidth + kStatusGap;
|
||||
const int buttonX = panelLeft + panelWidth - kStatusPadding - kStatusButtonWidth;
|
||||
const int editWidth = std::max(80, buttonX - editX - kStatusGap);
|
||||
const int oscWidth = std::max(80, panelLeft + panelWidth - editX - kStatusPadding);
|
||||
const int row1 = panelTop + kStatusPadding;
|
||||
const int row2 = row1 + kStatusRowHeight + kStatusGap;
|
||||
const int row3 = row2 + kStatusRowHeight + kStatusGap;
|
||||
|
||||
MoveWindow(controls.controlLabel, rowX, row1 + 3, kStatusLabelWidth, kStatusRowHeight, TRUE);
|
||||
MoveWindow(controls.controlUrl, editX, row1, editWidth, kStatusRowHeight, TRUE);
|
||||
MoveWindow(controls.openControl, buttonX, row1, kStatusButtonWidth, kStatusRowHeight, TRUE);
|
||||
MoveWindow(controls.docsLabel, rowX, row2 + 3, kStatusLabelWidth, kStatusRowHeight, TRUE);
|
||||
MoveWindow(controls.docsUrl, editX, row2, editWidth, kStatusRowHeight, TRUE);
|
||||
MoveWindow(controls.openDocs, buttonX, row2, kStatusButtonWidth, kStatusRowHeight, TRUE);
|
||||
MoveWindow(controls.oscLabel, rowX, row3 + 3, kStatusLabelWidth, kStatusRowHeight, TRUE);
|
||||
MoveWindow(controls.oscAddress, editX, row3, oscWidth, kStatusRowHeight, TRUE);
|
||||
RaiseStatusControls(controls);
|
||||
}
|
||||
|
||||
void UpdateStatusStrip(const StatusStripControls& controls, const OpenGLComposite& composite)
|
||||
{
|
||||
if (!StatusStripCreated(controls))
|
||||
return;
|
||||
|
||||
SetWindowTextA(controls.controlUrl, composite.GetControlUrl().c_str());
|
||||
SetWindowTextA(controls.docsUrl, composite.GetDocsUrl().c_str());
|
||||
SetWindowTextA(controls.oscAddress, composite.GetOscAddress().c_str());
|
||||
}
|
||||
|
||||
void OpenUrl(const char* url)
|
||||
{
|
||||
ShellExecuteA(NULL, "open", url, NULL, NULL, SW_SHOWNORMAL);
|
||||
}
|
||||
}
|
||||
|
||||
void ShowUnhandledExceptionMessage(const char* prefix)
|
||||
{
|
||||
try
|
||||
{
|
||||
throw;
|
||||
}
|
||||
catch (const std::exception& exception)
|
||||
{
|
||||
std::string message = std::string(prefix) + "\n\n" + exception.what();
|
||||
MessageBoxA(NULL, message.c_str(), "Unhandled exception", MB_OK | MB_ICONERROR);
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
MessageBoxA(NULL, prefix, "Unhandled exception", MB_OK | MB_ICONERROR);
|
||||
}
|
||||
}
|
||||
|
||||
// Select the pixel format for a given device context
|
||||
void SetDCPixelFormat(HDC hDC)
|
||||
{
|
||||
int nPixelFormat;
|
||||
|
||||
static PIXELFORMATDESCRIPTOR pfd = {
|
||||
sizeof(PIXELFORMATDESCRIPTOR), // Size of this structure
|
||||
1, // Version of this structure
|
||||
PFD_DRAW_TO_WINDOW | // Draw to Window (not to bitmap)
|
||||
PFD_SUPPORT_OPENGL | // Support OpenGL calls in window
|
||||
PFD_DOUBLEBUFFER, // Double buffered mode
|
||||
PFD_TYPE_RGBA, // RGBA Color mode
|
||||
32, // Want 32 bit color
|
||||
0,0,0,0,0,0, // Not used to select mode
|
||||
0,0, // Not used to select mode
|
||||
0,0,0,0,0, // Not used to select mode
|
||||
16, // Size of depth buffer
|
||||
0, // Not used
|
||||
0, // Not used
|
||||
0, // Not used
|
||||
0, // Not used
|
||||
0,0,0 }; // Not used
|
||||
|
||||
// Choose a pixel format that best matches that described in pfd
|
||||
nPixelFormat = ChoosePixelFormat(hDC, &pfd);
|
||||
|
||||
// Set the pixel format for the device context
|
||||
SetPixelFormat(hDC, nPixelFormat, &pfd);
|
||||
}
|
||||
|
||||
HGLRC CreateModernOpenGLContext(HDC hDC)
|
||||
{
|
||||
PFNWGLCREATECONTEXTATTRIBSARBPROC wglCreateContextAttribsARB =
|
||||
reinterpret_cast<PFNWGLCREATECONTEXTATTRIBSARBPROC>(wglGetProcAddress("wglCreateContextAttribsARB"));
|
||||
if (!wglCreateContextAttribsARB)
|
||||
return NULL;
|
||||
|
||||
const int versionCandidates[][2] =
|
||||
{
|
||||
{ 4, 5 },
|
||||
{ 4, 3 },
|
||||
{ 3, 3 }
|
||||
};
|
||||
|
||||
for (const auto& version : versionCandidates)
|
||||
{
|
||||
const int attribs[] =
|
||||
{
|
||||
WGL_CONTEXT_MAJOR_VERSION_ARB, version[0],
|
||||
WGL_CONTEXT_MINOR_VERSION_ARB, version[1],
|
||||
WGL_CONTEXT_PROFILE_MASK_ARB, WGL_CONTEXT_CORE_PROFILE_BIT_ARB,
|
||||
0
|
||||
};
|
||||
|
||||
HGLRC modernContext = wglCreateContextAttribsARB(hDC, 0, attribs);
|
||||
if (modernContext != NULL)
|
||||
return modernContext;
|
||||
}
|
||||
|
||||
return NULL;
|
||||
}
|
||||
|
||||
int APIENTRY WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow)
|
||||
{
|
||||
MSG msg; // Windows message structure
|
||||
WNDCLASS wc; // Windows class structure
|
||||
HWND hWnd; // Storeage for window handle
|
||||
TCHAR szTitle[MAX_LOADSTRING]; // The title bar text
|
||||
TCHAR szWindowClass[MAX_LOADSTRING]; // the main window class name
|
||||
|
||||
// Initialize global strings
|
||||
LoadString(hInstance, IDS_APP_TITLE, szTitle, MAX_LOADSTRING);
|
||||
LoadString(hInstance, IDC_OPENGLOUTPUT, szWindowClass, MAX_LOADSTRING);
|
||||
|
||||
// Register Window style
|
||||
wc.style = CS_HREDRAW | CS_VREDRAW | CS_OWNDC;
|
||||
wc.lpfnWndProc = (WNDPROC) WndProc;
|
||||
wc.cbClsExtra = 0;
|
||||
wc.cbWndExtra = 0;
|
||||
wc.hInstance = hInstance;
|
||||
wc.hIcon = NULL;
|
||||
wc.hCursor = LoadCursor(NULL, IDC_ARROW);
|
||||
|
||||
// No need for background brush for OpenGL window
|
||||
wc.hbrBackground = NULL;
|
||||
wc.lpszMenuName = NULL;
|
||||
wc.lpszClassName = szWindowClass;
|
||||
|
||||
// Register the window class
|
||||
if (RegisterClass(&wc) == 0)
|
||||
return FALSE;
|
||||
|
||||
// Create the main application window
|
||||
hWnd = CreateWindow(szWindowClass, szTitle, WS_OVERLAPPEDWINDOW | WS_CLIPCHILDREN | WS_CLIPSIBLINGS,
|
||||
CW_USEDEFAULT, 0, 250, 250, NULL, NULL, hInstance, NULL);
|
||||
|
||||
// If window was not created, quit
|
||||
if (hWnd == NULL)
|
||||
return FALSE;
|
||||
|
||||
// Display the window
|
||||
ShowWindow(hWnd,SW_SHOW);
|
||||
UpdateWindow(hWnd);
|
||||
|
||||
// Process application messages until the application closes
|
||||
while (GetMessage(&msg, NULL, 0, 0))
|
||||
{
|
||||
TranslateMessage(&msg);
|
||||
DispatchMessage(&msg);
|
||||
}
|
||||
|
||||
return (int)msg.wParam;
|
||||
}
|
||||
|
||||
// Window procedure, handles all messages for this program
|
||||
LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
|
||||
{
|
||||
static HGLRC hRC = NULL; // Permenant Rendering context
|
||||
static HDC hDC = NULL; // Private GDI Device context
|
||||
static OpenGLComposite* pOpenGLComposite = NULL;
|
||||
static bool sInteractiveResize = false;
|
||||
static StatusStripControls sStatusStrip;
|
||||
|
||||
switch (message)
|
||||
{
|
||||
// Window creation, setup for OpenGL context
|
||||
case WM_CREATE:
|
||||
{
|
||||
try
|
||||
{
|
||||
// Store the device context
|
||||
hDC = GetDC(hWnd);
|
||||
|
||||
// Select the pixel format
|
||||
SetDCPixelFormat(hDC);
|
||||
|
||||
// Create the rendering context and make it current
|
||||
hRC = wglCreateContext(hDC);
|
||||
wglMakeCurrent(hDC, hRC);
|
||||
|
||||
HGLRC modernRC = CreateModernOpenGLContext(hDC);
|
||||
if (modernRC == NULL)
|
||||
{
|
||||
MessageBox(NULL, _T("This application requires an OpenGL 3.3+ core profile context."), _T("OpenGL initialization Error."), MB_OK);
|
||||
PostMessage(hWnd, WM_CLOSE, 0, 0);
|
||||
break;
|
||||
}
|
||||
|
||||
wglMakeCurrent(NULL, NULL);
|
||||
wglDeleteContext(hRC);
|
||||
hRC = modernRC;
|
||||
wglMakeCurrent(hDC, hRC);
|
||||
|
||||
// Initialize COM
|
||||
HRESULT result;
|
||||
result = CoInitialize(NULL);
|
||||
if (FAILED(result))
|
||||
{
|
||||
MessageBox(NULL, _T("Initialization of COM failed."), _T("Application initialization Error."),MB_OK);
|
||||
PostMessage(hWnd, WM_CLOSE, 0, 0);
|
||||
break;
|
||||
}
|
||||
|
||||
// Setup OpenGL and DeckLink capture and playout object
|
||||
pOpenGLComposite = new OpenGLComposite(hWnd, hDC, hRC);
|
||||
|
||||
if (pOpenGLComposite->InitDeckLink())
|
||||
{
|
||||
wglMakeCurrent( NULL, NULL );
|
||||
if (pOpenGLComposite->Start())
|
||||
{
|
||||
PostMessage(hWnd, kCreateStatusStripMessage, 0, 0);
|
||||
break; // success
|
||||
}
|
||||
MessageBoxA(NULL, "The OpenGL/DeckLink runtime initialized, but playout failed to start. See the previous DeckLink start message for the failing call.", "Startup failed", MB_OK | MB_ICONERROR);
|
||||
}
|
||||
else
|
||||
{
|
||||
MessageBoxA(NULL, "The OpenGL/DeckLink runtime failed to initialize. See the previous initialization message for the failing call.", "Startup failed", MB_OK | MB_ICONERROR);
|
||||
}
|
||||
|
||||
// Failed to initialize - cleanup
|
||||
delete pOpenGLComposite;
|
||||
pOpenGLComposite = NULL;
|
||||
PostMessage(hWnd, WM_CLOSE, 0, 0);
|
||||
break;
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
ShowUnhandledExceptionMessage("Startup failed while creating the OpenGL/DeckLink runtime.");
|
||||
PostMessage(hWnd, WM_CLOSE, 0, 0);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
case kCreateStatusStripMessage:
|
||||
if (pOpenGLComposite)
|
||||
{
|
||||
if (!StatusStripCreated(sStatusStrip))
|
||||
CreateStatusStrip(hWnd, sStatusStrip);
|
||||
|
||||
UpdateStatusStrip(sStatusStrip, *pOpenGLComposite);
|
||||
LayoutStatusStrip(hWnd, sStatusStrip);
|
||||
RECT clientRect = {};
|
||||
if (GetClientRect(hWnd, &clientRect))
|
||||
{
|
||||
pOpenGLComposite->resizeGL(
|
||||
static_cast<WORD>(clientRect.right - clientRect.left),
|
||||
static_cast<WORD>(clientRect.bottom - clientRect.top));
|
||||
}
|
||||
InvalidateRect(hWnd, NULL, FALSE);
|
||||
}
|
||||
break;
|
||||
|
||||
case WM_DESTROY:
|
||||
try
|
||||
{
|
||||
if (pOpenGLComposite)
|
||||
{
|
||||
pOpenGLComposite->Stop();
|
||||
delete pOpenGLComposite;
|
||||
}
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
ShowUnhandledExceptionMessage("Shutdown failed while tearing down the OpenGL/DeckLink runtime.");
|
||||
}
|
||||
|
||||
// Deselect the current rendering context and delete it
|
||||
wglMakeCurrent(hDC, NULL);
|
||||
wglDeleteContext(hRC);
|
||||
|
||||
// Tell the application to terminate after the window is gone
|
||||
PostQuitMessage(0);
|
||||
break;
|
||||
|
||||
case WM_ENTERSIZEMOVE:
|
||||
sInteractiveResize = true;
|
||||
break;
|
||||
|
||||
case WM_EXITSIZEMOVE:
|
||||
sInteractiveResize = false;
|
||||
if (pOpenGLComposite)
|
||||
{
|
||||
RECT clientRect = {};
|
||||
if (GetClientRect(hWnd, &clientRect))
|
||||
{
|
||||
pOpenGLComposite->resizeGL(
|
||||
static_cast<WORD>(clientRect.right - clientRect.left),
|
||||
static_cast<WORD>(clientRect.bottom - clientRect.top));
|
||||
}
|
||||
}
|
||||
InvalidateRect(hWnd, NULL, FALSE);
|
||||
break;
|
||||
|
||||
case WM_SIZE:
|
||||
try
|
||||
{
|
||||
if (StatusStripCreated(sStatusStrip))
|
||||
LayoutStatusStrip(hWnd, sStatusStrip);
|
||||
if (pOpenGLComposite)
|
||||
pOpenGLComposite->resizeGL(LOWORD(lParam), HIWORD(lParam));
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
ShowUnhandledExceptionMessage("Resize failed inside the OpenGL runtime.");
|
||||
}
|
||||
break;
|
||||
|
||||
case WM_ERASEBKGND:
|
||||
return 1;
|
||||
|
||||
case WM_PAINT:
|
||||
try
|
||||
{
|
||||
PAINTSTRUCT paint = {};
|
||||
BeginPaint(hWnd, &paint);
|
||||
EndPaint(hWnd, &paint);
|
||||
|
||||
if (!sInteractiveResize && pOpenGLComposite)
|
||||
{
|
||||
wglMakeCurrent(hDC, hRC);
|
||||
pOpenGLComposite->paintGL(true);
|
||||
wglMakeCurrent( NULL, NULL );
|
||||
RaiseStatusControls(sStatusStrip);
|
||||
}
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
wglMakeCurrent( NULL, NULL );
|
||||
ShowUnhandledExceptionMessage("Paint failed inside the OpenGL runtime.");
|
||||
}
|
||||
break;
|
||||
|
||||
case WM_KEYDOWN:
|
||||
try
|
||||
{
|
||||
if (pOpenGLComposite && (wParam == 'R' || wParam == 'r'))
|
||||
{
|
||||
pOpenGLComposite->ReloadShader();
|
||||
InvalidateRect(hWnd, NULL, FALSE);
|
||||
}
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
ShowUnhandledExceptionMessage("Shader reload failed inside the OpenGL runtime.");
|
||||
}
|
||||
break;
|
||||
|
||||
case WM_COMMAND:
|
||||
switch (LOWORD(wParam))
|
||||
{
|
||||
case kOpenControlButtonId:
|
||||
if (pOpenGLComposite)
|
||||
{
|
||||
std::string url = pOpenGLComposite->GetControlUrl();
|
||||
OpenUrl(url.c_str());
|
||||
}
|
||||
break;
|
||||
case kOpenDocsButtonId:
|
||||
if (pOpenGLComposite)
|
||||
{
|
||||
std::string url = pOpenGLComposite->GetDocsUrl();
|
||||
OpenUrl(url.c_str());
|
||||
}
|
||||
break;
|
||||
default:
|
||||
return DefWindowProc(hWnd, message, wParam, lParam);
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
return (DefWindowProc(hWnd, message, wParam, lParam));
|
||||
}
|
||||
|
||||
return (0L);
|
||||
}
|
||||
|
||||
@@ -1,47 +0,0 @@
|
||||
/* -LICENSE-START-
|
||||
** Copyright (c) 2012 Blackmagic Design
|
||||
**
|
||||
** Permission is hereby granted, free of charge, to any person or organization
|
||||
** obtaining a copy of the software and accompanying documentation (the
|
||||
** "Software") to use, reproduce, display, distribute, sub-license, execute,
|
||||
** and transmit the Software, and to prepare derivative works of the Software,
|
||||
** and to permit third-parties to whom the Software is furnished to do so, in
|
||||
** accordance with:
|
||||
**
|
||||
** (1) if the Software is obtained from Blackmagic Design, the End User License
|
||||
** Agreement for the Software Development Kit ("EULA") available at
|
||||
** https://www.blackmagicdesign.com/EULA/DeckLinkSDK; or
|
||||
**
|
||||
** (2) if the Software is obtained from any third party, such licensing terms
|
||||
** as notified by that third party,
|
||||
**
|
||||
** and all subject to the following:
|
||||
**
|
||||
** (3) the copyright notices in the Software and this entire statement,
|
||||
** including the above license grant, this restriction and the following
|
||||
** disclaimer, must be included in all copies of the Software, in whole or in
|
||||
** part, and all derivative works of the Software, unless such copies or
|
||||
** derivative works are solely in the form of machine-executable object code
|
||||
** generated by a source language processor.
|
||||
**
|
||||
** (4) THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
||||
** OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
** FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT
|
||||
** SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE
|
||||
** FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE,
|
||||
** ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
** DEALINGS IN THE SOFTWARE.
|
||||
**
|
||||
** A copy of the Software is available free of charge at
|
||||
** https://www.blackmagicdesign.com/desktopvideo_sdk under the EULA.
|
||||
**
|
||||
** -LICENSE-END-
|
||||
*/
|
||||
//
|
||||
// LoopThroughWithOpenGLCompositing.h
|
||||
// LoopThroughWithOpenGLCompositing
|
||||
//
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "resource.h"
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 23 KiB |
@@ -1,95 +0,0 @@
|
||||
// Microsoft Visual C++ generated resource script.
|
||||
//
|
||||
#include "resource.h"
|
||||
|
||||
#define APSTUDIO_READONLY_SYMBOLS
|
||||
/////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// Generated from the TEXTINCLUDE 2 resource.
|
||||
//
|
||||
#ifndef APSTUDIO_INVOKED
|
||||
#include "targetver.h"
|
||||
#endif
|
||||
#define APSTUDIO_HIDDEN_SYMBOLS
|
||||
#include "windows.h"
|
||||
#undef APSTUDIO_HIDDEN_SYMBOLS
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////
|
||||
#undef APSTUDIO_READONLY_SYMBOLS
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////
|
||||
// English (U.S.) resources
|
||||
|
||||
#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU)
|
||||
#ifdef _WIN32
|
||||
LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US
|
||||
#pragma code_page(1252)
|
||||
#endif //_WIN32
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// Icon
|
||||
//
|
||||
|
||||
// Icon with lowest ID value placed first to ensure application icon
|
||||
// remains consistent on all systems.
|
||||
IDI_OPENGLOUTPUT ICON "LoopThroughWithOpenGLCompositing.ico"
|
||||
IDI_SMALL ICON "small.ico"
|
||||
|
||||
#ifdef APSTUDIO_INVOKED
|
||||
/////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// TEXTINCLUDE
|
||||
//
|
||||
|
||||
1 TEXTINCLUDE
|
||||
BEGIN
|
||||
"resource.h\0"
|
||||
END
|
||||
|
||||
2 TEXTINCLUDE
|
||||
BEGIN
|
||||
"#ifndef APSTUDIO_INVOKED\r\n"
|
||||
"#include ""targetver.h""\r\n"
|
||||
"#endif\r\n"
|
||||
"#define APSTUDIO_HIDDEN_SYMBOLS\r\n"
|
||||
"#include ""windows.h""\r\n"
|
||||
"#undef APSTUDIO_HIDDEN_SYMBOLS\r\n"
|
||||
"\0"
|
||||
END
|
||||
|
||||
3 TEXTINCLUDE
|
||||
BEGIN
|
||||
"\r\n"
|
||||
"\0"
|
||||
END
|
||||
|
||||
#endif // APSTUDIO_INVOKED
|
||||
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// String Table
|
||||
//
|
||||
|
||||
STRINGTABLE
|
||||
BEGIN
|
||||
IDS_APP_TITLE "Video Shader Toys"
|
||||
IDC_OPENGLOUTPUT "OPENGLOUTPUT"
|
||||
END
|
||||
|
||||
#endif // English (U.S.) resources
|
||||
/////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
|
||||
|
||||
#ifndef APSTUDIO_INVOKED
|
||||
/////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// Generated from the TEXTINCLUDE 3 resource.
|
||||
//
|
||||
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////
|
||||
#endif // not APSTUDIO_INVOKED
|
||||
|
||||
@@ -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>
|
||||
@@ -1,648 +0,0 @@
|
||||
#include "stdafx.h"
|
||||
#include "ControlServer.h"
|
||||
|
||||
#include "RuntimeJson.h"
|
||||
|
||||
#include <Wincrypt.h>
|
||||
#include <ws2tcpip.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <fstream>
|
||||
#include <sstream>
|
||||
|
||||
#pragma comment(lib, "Ws2_32.lib")
|
||||
#pragma comment(lib, "Crypt32.lib")
|
||||
#pragma comment(lib, "Advapi32.lib")
|
||||
|
||||
namespace
|
||||
{
|
||||
constexpr DWORD kStateBroadcastIntervalMs = 250;
|
||||
constexpr DWORD kStateBroadcastThrottleMs = 50;
|
||||
|
||||
bool InitializeWinsock(std::string& error)
|
||||
{
|
||||
WSADATA wsaData = {};
|
||||
int result = WSAStartup(MAKEWORD(2, 2), &wsaData);
|
||||
if (result != 0)
|
||||
{
|
||||
error = "WSAStartup failed.";
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
std::string ToLower(std::string text)
|
||||
{
|
||||
std::transform(text.begin(), text.end(), text.begin(),
|
||||
[](unsigned char ch) { return static_cast<char>(std::tolower(ch)); });
|
||||
return text;
|
||||
}
|
||||
|
||||
bool IsSafeUiPath(const std::filesystem::path& relativePath)
|
||||
{
|
||||
for (const std::filesystem::path& part : relativePath)
|
||||
{
|
||||
if (part == "..")
|
||||
return false;
|
||||
}
|
||||
return !relativePath.empty();
|
||||
}
|
||||
|
||||
std::string GuessContentType(const std::filesystem::path& assetPath)
|
||||
{
|
||||
const std::string extension = ToLower(assetPath.extension().string());
|
||||
if (extension == ".js" || extension == ".mjs")
|
||||
return "text/javascript";
|
||||
if (extension == ".css")
|
||||
return "text/css";
|
||||
if (extension == ".json")
|
||||
return "application/json";
|
||||
if (extension == ".yaml" || extension == ".yml")
|
||||
return "application/yaml";
|
||||
if (extension == ".svg")
|
||||
return "image/svg+xml";
|
||||
if (extension == ".png")
|
||||
return "image/png";
|
||||
if (extension == ".jpg" || extension == ".jpeg")
|
||||
return "image/jpeg";
|
||||
if (extension == ".ico")
|
||||
return "image/x-icon";
|
||||
if (extension == ".map")
|
||||
return "application/json";
|
||||
if (extension == ".md")
|
||||
return "text/markdown";
|
||||
return "text/html";
|
||||
}
|
||||
}
|
||||
|
||||
ControlServer::ControlServer()
|
||||
: mPort(0), mRunning(false), mBroadcastPending(false)
|
||||
{
|
||||
}
|
||||
|
||||
ControlServer::~ControlServer()
|
||||
{
|
||||
Stop();
|
||||
}
|
||||
|
||||
bool ControlServer::Start(const std::filesystem::path& uiRoot, const std::filesystem::path& docsRoot, unsigned short preferredPort, const Callbacks& callbacks, std::string& error)
|
||||
{
|
||||
mUiRoot = uiRoot;
|
||||
mDocsRoot = docsRoot;
|
||||
mCallbacks = callbacks;
|
||||
|
||||
if (!InitializeWinsock(error))
|
||||
return false;
|
||||
|
||||
mListenSocket.reset(socket(AF_INET, SOCK_STREAM, IPPROTO_TCP));
|
||||
if (!mListenSocket.valid())
|
||||
{
|
||||
error = "Could not create listening socket.";
|
||||
return false;
|
||||
}
|
||||
|
||||
u_long nonBlocking = 1;
|
||||
ioctlsocket(mListenSocket.get(), FIONBIO, &nonBlocking);
|
||||
|
||||
sockaddr_in address = {};
|
||||
address.sin_family = AF_INET;
|
||||
address.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
|
||||
|
||||
bool bound = false;
|
||||
for (unsigned short offset = 0; offset < 20; ++offset)
|
||||
{
|
||||
address.sin_port = htons(static_cast<u_short>(preferredPort + offset));
|
||||
if (bind(mListenSocket.get(), reinterpret_cast<sockaddr*>(&address), sizeof(address)) == 0)
|
||||
{
|
||||
mPort = preferredPort + offset;
|
||||
bound = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!bound)
|
||||
{
|
||||
error = "Could not bind the local control server to any port in the preferred range.";
|
||||
mListenSocket.reset();
|
||||
return false;
|
||||
}
|
||||
|
||||
if (listen(mListenSocket.get(), SOMAXCONN) != 0)
|
||||
{
|
||||
error = "Could not start listening on the local control server socket.";
|
||||
mListenSocket.reset();
|
||||
return false;
|
||||
}
|
||||
|
||||
mRunning = true;
|
||||
mThread = std::thread(&ControlServer::ServerLoop, this);
|
||||
return true;
|
||||
}
|
||||
|
||||
void ControlServer::Stop()
|
||||
{
|
||||
const bool wasActive = mRunning || mListenSocket.valid() || mThread.joinable();
|
||||
mRunning = false;
|
||||
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mMutex);
|
||||
for (ClientConnection& client : mClients)
|
||||
client.socket.reset();
|
||||
mClients.clear();
|
||||
}
|
||||
|
||||
mListenSocket.reset();
|
||||
|
||||
if (mThread.joinable())
|
||||
mThread.join();
|
||||
|
||||
if (wasActive)
|
||||
WSACleanup();
|
||||
}
|
||||
|
||||
void ControlServer::BroadcastState()
|
||||
{
|
||||
mBroadcastPending = false;
|
||||
std::lock_guard<std::mutex> lock(mMutex);
|
||||
BroadcastStateLocked();
|
||||
}
|
||||
|
||||
void ControlServer::RequestBroadcastState()
|
||||
{
|
||||
mBroadcastPending = true;
|
||||
}
|
||||
|
||||
void ControlServer::ServerLoop()
|
||||
{
|
||||
DWORD lastStateBroadcastMs = GetTickCount();
|
||||
while (mRunning)
|
||||
{
|
||||
TryAcceptClient();
|
||||
|
||||
const DWORD nowMs = GetTickCount();
|
||||
if (mBroadcastPending && nowMs - lastStateBroadcastMs >= kStateBroadcastThrottleMs)
|
||||
{
|
||||
BroadcastState();
|
||||
lastStateBroadcastMs = nowMs;
|
||||
}
|
||||
else if (nowMs - lastStateBroadcastMs >= kStateBroadcastIntervalMs)
|
||||
{
|
||||
BroadcastState();
|
||||
lastStateBroadcastMs = nowMs;
|
||||
}
|
||||
|
||||
Sleep(25);
|
||||
}
|
||||
}
|
||||
|
||||
bool ControlServer::HandleHttpClient(UniqueSocket clientSocket)
|
||||
{
|
||||
std::string request;
|
||||
char buffer[8192];
|
||||
int received = recv(clientSocket.get(), buffer, sizeof(buffer), 0);
|
||||
if (received <= 0)
|
||||
return false;
|
||||
|
||||
request.assign(buffer, buffer + received);
|
||||
return HandleHttpRequest(std::move(clientSocket), request);
|
||||
}
|
||||
|
||||
bool ControlServer::TryAcceptClient()
|
||||
{
|
||||
sockaddr_in clientAddress = {};
|
||||
int addressSize = sizeof(clientAddress);
|
||||
UniqueSocket clientSocket(accept(mListenSocket.get(), reinterpret_cast<sockaddr*>(&clientAddress), &addressSize));
|
||||
if (!clientSocket.valid())
|
||||
return false;
|
||||
|
||||
return HandleHttpClient(std::move(clientSocket));
|
||||
}
|
||||
|
||||
bool ControlServer::SendHttpResponse(SOCKET clientSocket, const std::string& status, const std::string& contentType, const std::string& body)
|
||||
{
|
||||
std::ostringstream response;
|
||||
response << "HTTP/1.1 " << status << "\r\n";
|
||||
response << "Content-Type: " << contentType << "\r\n";
|
||||
response << "Content-Length: " << body.size() << "\r\n";
|
||||
response << "Connection: close\r\n\r\n";
|
||||
response << body;
|
||||
|
||||
const std::string payload = response.str();
|
||||
return send(clientSocket, payload.c_str(), static_cast<int>(payload.size()), 0) == static_cast<int>(payload.size());
|
||||
}
|
||||
|
||||
bool ControlServer::SendHttpResponse(SOCKET clientSocket, const HttpResponse& response)
|
||||
{
|
||||
return SendHttpResponse(clientSocket, response.status, response.contentType, response.body);
|
||||
}
|
||||
|
||||
bool ControlServer::HandleHttpRequest(UniqueSocket clientSocket, const std::string& request)
|
||||
{
|
||||
HttpRequest httpRequest;
|
||||
if (!ParseHttpRequest(request, httpRequest))
|
||||
{
|
||||
SendHttpResponse(clientSocket.get(), "400 Bad Request", "text/plain", "Bad Request");
|
||||
return true;
|
||||
}
|
||||
|
||||
if (ToLower(GetHeaderValue(httpRequest, "Upgrade")) == "websocket")
|
||||
return HandleWebSocketUpgrade(std::move(clientSocket), httpRequest);
|
||||
|
||||
const HttpResponse response = RouteHttpRequest(httpRequest);
|
||||
SendHttpResponse(clientSocket.get(), response);
|
||||
if (response.broadcastState)
|
||||
BroadcastState();
|
||||
return true;
|
||||
}
|
||||
|
||||
ControlServer::HttpResponse ControlServer::RouteHttpRequest(const HttpRequest& request)
|
||||
{
|
||||
if (request.method == "GET")
|
||||
return ServeGetRequest(request);
|
||||
|
||||
if (request.method == "POST")
|
||||
return HandleApiPost(request);
|
||||
|
||||
return { "404 Not Found", "text/plain", "Not Found" };
|
||||
}
|
||||
|
||||
ControlServer::HttpResponse ControlServer::ServeGetRequest(const HttpRequest& request) const
|
||||
{
|
||||
if (request.path == "/" || request.path == "/index.html")
|
||||
return ServeUiAsset("index.html");
|
||||
|
||||
if (request.path == "/api/state")
|
||||
return { "200 OK", "application/json", mCallbacks.getStateJson ? mCallbacks.getStateJson() : "{}" };
|
||||
|
||||
if (request.path == "/openapi.yaml" || request.path == "/docs/openapi.yaml")
|
||||
return ServeOpenApiSpec();
|
||||
|
||||
if (request.path == "/docs" || request.path == "/docs/")
|
||||
return ServeSwaggerDocs();
|
||||
|
||||
const std::string docsPrefix = "/docs/";
|
||||
if (request.path.rfind(docsPrefix, 0) == 0)
|
||||
return ServeDocsAsset(request.path.substr(docsPrefix.size()));
|
||||
|
||||
if (request.path.size() > 1)
|
||||
{
|
||||
const HttpResponse assetResponse = ServeUiAsset(request.path.substr(1));
|
||||
if (!assetResponse.body.empty())
|
||||
return assetResponse;
|
||||
}
|
||||
|
||||
return { "404 Not Found", "text/plain", "Not Found" };
|
||||
}
|
||||
|
||||
ControlServer::HttpResponse ControlServer::ServeUiAsset(const std::string& relativePath) const
|
||||
{
|
||||
std::string contentType;
|
||||
const std::string body = LoadUiAsset(relativePath, contentType);
|
||||
return body.empty()
|
||||
? HttpResponse{ "404 Not Found", "text/plain", "Not Found" }
|
||||
: HttpResponse{ "200 OK", contentType, body };
|
||||
}
|
||||
|
||||
ControlServer::HttpResponse ControlServer::ServeDocsAsset(const std::string& relativePath) const
|
||||
{
|
||||
const std::filesystem::path sanitizedPath = std::filesystem::path(relativePath).lexically_normal();
|
||||
if (!IsSafeUiPath(sanitizedPath))
|
||||
return { "404 Not Found", "text/plain", "Not Found" };
|
||||
|
||||
const std::filesystem::path docsPath = mDocsRoot / sanitizedPath;
|
||||
const std::string body = LoadTextFile(docsPath);
|
||||
return body.empty()
|
||||
? HttpResponse{ "404 Not Found", "text/plain", "Not Found" }
|
||||
: HttpResponse{ "200 OK", GuessContentType(docsPath), body };
|
||||
}
|
||||
|
||||
ControlServer::HttpResponse ControlServer::ServeOpenApiSpec() const
|
||||
{
|
||||
const std::filesystem::path specPath = mDocsRoot / "openapi.yaml";
|
||||
const std::string body = LoadTextFile(specPath);
|
||||
return body.empty()
|
||||
? HttpResponse{ "404 Not Found", "text/plain", "OpenAPI spec not found" }
|
||||
: HttpResponse{ "200 OK", GuessContentType(specPath), body };
|
||||
}
|
||||
|
||||
ControlServer::HttpResponse ControlServer::ServeSwaggerDocs() const
|
||||
{
|
||||
std::ostringstream html;
|
||||
html << "<!doctype html>\n"
|
||||
<< "<html lang=\"en\">\n"
|
||||
<< "<head>\n"
|
||||
<< " <meta charset=\"utf-8\">\n"
|
||||
<< " <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n"
|
||||
<< " <title>Video Shader Toys API Docs</title>\n"
|
||||
<< " <link rel=\"stylesheet\" href=\"https://unpkg.com/swagger-ui-dist@5/swagger-ui.css\">\n"
|
||||
<< "</head>\n"
|
||||
<< "<body>\n"
|
||||
<< " <div id=\"swagger-ui\"></div>\n"
|
||||
<< " <script src=\"https://unpkg.com/swagger-ui-dist@5/swagger-ui-bundle.js\"></script>\n"
|
||||
<< " <script>SwaggerUIBundle({url:'/docs/openapi.yaml',dom_id:'#swagger-ui'});</script>\n"
|
||||
<< "</body>\n"
|
||||
<< "</html>\n";
|
||||
return { "200 OK", "text/html", html.str() };
|
||||
}
|
||||
|
||||
ControlServer::HttpResponse ControlServer::HandleApiPost(const HttpRequest& request)
|
||||
{
|
||||
JsonValue root;
|
||||
std::string parseError;
|
||||
if (!ParseJson(request.body, root, parseError))
|
||||
return { "400 Bad Request", "application/json", BuildJsonResponse(false, parseError) };
|
||||
|
||||
std::string actionError;
|
||||
const bool success = InvokePostRoute(request.path, root, actionError);
|
||||
return {
|
||||
success ? "200 OK" : "400 Bad Request",
|
||||
"application/json",
|
||||
BuildJsonResponse(success, actionError),
|
||||
success
|
||||
};
|
||||
}
|
||||
|
||||
bool ControlServer::InvokePostRoute(const std::string& path, const JsonValue& root, std::string& actionError)
|
||||
{
|
||||
using PostHandler = std::function<bool(const JsonValue&, std::string&)>;
|
||||
const std::map<std::string, PostHandler> postRoutes =
|
||||
{
|
||||
{ "/api/layers/add", [this](const JsonValue& json, std::string& error)
|
||||
{
|
||||
const JsonValue* shaderId = json.find("shaderId");
|
||||
return shaderId && mCallbacks.addLayer && mCallbacks.addLayer(shaderId->asString(), error);
|
||||
}
|
||||
},
|
||||
{ "/api/layers/remove", [this](const JsonValue& json, std::string& error)
|
||||
{
|
||||
const JsonValue* layerId = json.find("layerId");
|
||||
return layerId && mCallbacks.removeLayer && mCallbacks.removeLayer(layerId->asString(), error);
|
||||
}
|
||||
},
|
||||
{ "/api/layers/move", [this](const JsonValue& json, std::string& error)
|
||||
{
|
||||
const JsonValue* layerId = json.find("layerId");
|
||||
const JsonValue* direction = json.find("direction");
|
||||
return layerId && direction && mCallbacks.moveLayer &&
|
||||
mCallbacks.moveLayer(layerId->asString(), static_cast<int>(direction->asNumber()), error);
|
||||
}
|
||||
},
|
||||
{ "/api/layers/reorder", [this](const JsonValue& json, std::string& error)
|
||||
{
|
||||
const JsonValue* layerId = json.find("layerId");
|
||||
const JsonValue* targetIndex = json.find("targetIndex");
|
||||
return layerId && targetIndex && mCallbacks.moveLayerToIndex &&
|
||||
mCallbacks.moveLayerToIndex(layerId->asString(), static_cast<std::size_t>(targetIndex->asNumber()), error);
|
||||
}
|
||||
},
|
||||
{ "/api/layers/set-bypass", [this](const JsonValue& json, std::string& error)
|
||||
{
|
||||
const JsonValue* layerId = json.find("layerId");
|
||||
const JsonValue* bypass = json.find("bypass");
|
||||
return layerId && bypass && mCallbacks.setLayerBypass &&
|
||||
mCallbacks.setLayerBypass(layerId->asString(), bypass->asBoolean(), error);
|
||||
}
|
||||
},
|
||||
{ "/api/layers/set-shader", [this](const JsonValue& json, std::string& error)
|
||||
{
|
||||
const JsonValue* layerId = json.find("layerId");
|
||||
const JsonValue* shaderId = json.find("shaderId");
|
||||
return layerId && shaderId && mCallbacks.setLayerShader &&
|
||||
mCallbacks.setLayerShader(layerId->asString(), shaderId->asString(), error);
|
||||
}
|
||||
},
|
||||
{ "/api/layers/update-parameter", [this](const JsonValue& json, std::string& error)
|
||||
{
|
||||
const JsonValue* layerId = json.find("layerId");
|
||||
const JsonValue* parameterId = json.find("parameterId");
|
||||
const JsonValue* value = json.find("value");
|
||||
return layerId && parameterId && value && mCallbacks.updateLayerParameter &&
|
||||
mCallbacks.updateLayerParameter(layerId->asString(), parameterId->asString(), SerializeJson(*value, false), error);
|
||||
}
|
||||
},
|
||||
{ "/api/layers/reset-parameters", [this](const JsonValue& json, std::string& error)
|
||||
{
|
||||
const JsonValue* layerId = json.find("layerId");
|
||||
return layerId && mCallbacks.resetLayerParameters &&
|
||||
mCallbacks.resetLayerParameters(layerId->asString(), error);
|
||||
}
|
||||
},
|
||||
{ "/api/stack-presets/save", [this](const JsonValue& json, std::string& error)
|
||||
{
|
||||
const JsonValue* presetName = json.find("presetName");
|
||||
return presetName && mCallbacks.saveStackPreset &&
|
||||
mCallbacks.saveStackPreset(presetName->asString(), error);
|
||||
}
|
||||
},
|
||||
{ "/api/stack-presets/load", [this](const JsonValue& json, std::string& error)
|
||||
{
|
||||
const JsonValue* presetName = json.find("presetName");
|
||||
return presetName && mCallbacks.loadStackPreset &&
|
||||
mCallbacks.loadStackPreset(presetName->asString(), error);
|
||||
}
|
||||
},
|
||||
{ "/api/reload", [this](const JsonValue&, std::string& error)
|
||||
{
|
||||
return mCallbacks.reloadShader && mCallbacks.reloadShader(error);
|
||||
}
|
||||
},
|
||||
{ "/api/screenshot", [this](const JsonValue&, std::string& error)
|
||||
{
|
||||
return mCallbacks.requestScreenshot && mCallbacks.requestScreenshot(error);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const auto route = postRoutes.find(path);
|
||||
return route != postRoutes.end() && route->second(root, actionError);
|
||||
}
|
||||
|
||||
bool ControlServer::HandleWebSocketUpgrade(UniqueSocket clientSocket, const HttpRequest& request)
|
||||
{
|
||||
const std::string clientKey = GetHeaderValue(request, "Sec-WebSocket-Key");
|
||||
if (clientKey.empty())
|
||||
{
|
||||
SendHttpResponse(clientSocket.get(), "400 Bad Request", "text/plain", "Missing Sec-WebSocket-Key");
|
||||
return true;
|
||||
}
|
||||
|
||||
std::ostringstream response;
|
||||
response << "HTTP/1.1 101 Switching Protocols\r\n";
|
||||
response << "Upgrade: websocket\r\n";
|
||||
response << "Connection: Upgrade\r\n";
|
||||
response << "Sec-WebSocket-Accept: " << ComputeWebSocketAcceptKey(clientKey) << "\r\n\r\n";
|
||||
|
||||
const std::string payload = response.str();
|
||||
send(clientSocket.get(), payload.c_str(), static_cast<int>(payload.size()), 0);
|
||||
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mMutex);
|
||||
ClientConnection client;
|
||||
client.socket.reset(clientSocket.release());
|
||||
client.websocket = true;
|
||||
mClients.push_back(std::move(client));
|
||||
mBroadcastPending = false;
|
||||
BroadcastStateLocked();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ControlServer::SendWebSocketText(SOCKET clientSocket, const std::string& payload)
|
||||
{
|
||||
std::string frame;
|
||||
frame.push_back(static_cast<char>(0x81));
|
||||
if (payload.size() <= 125)
|
||||
{
|
||||
frame.push_back(static_cast<char>(payload.size()));
|
||||
}
|
||||
else if (payload.size() <= 65535)
|
||||
{
|
||||
frame.push_back(126);
|
||||
frame.push_back(static_cast<char>((payload.size() >> 8) & 0xFF));
|
||||
frame.push_back(static_cast<char>(payload.size() & 0xFF));
|
||||
}
|
||||
else
|
||||
{
|
||||
frame.push_back(127);
|
||||
for (int shift = 56; shift >= 0; shift -= 8)
|
||||
frame.push_back(static_cast<char>((payload.size() >> shift) & 0xFF));
|
||||
}
|
||||
frame.append(payload);
|
||||
|
||||
return send(clientSocket, frame.data(), static_cast<int>(frame.size()), 0) == static_cast<int>(frame.size());
|
||||
}
|
||||
|
||||
void ControlServer::BroadcastStateLocked()
|
||||
{
|
||||
if (mClients.empty())
|
||||
return;
|
||||
|
||||
const std::string stateMessage = mCallbacks.getStateJson ? mCallbacks.getStateJson() : "{}";
|
||||
for (auto it = mClients.begin(); it != mClients.end();)
|
||||
{
|
||||
if (!SendWebSocketText(it->socket.get(), stateMessage))
|
||||
{
|
||||
it = mClients.erase(it);
|
||||
}
|
||||
else
|
||||
{
|
||||
++it;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
std::string ControlServer::LoadUiAsset(const std::string& relativePath, std::string& contentType) const
|
||||
{
|
||||
const std::filesystem::path sanitizedPath = std::filesystem::path(relativePath).lexically_normal();
|
||||
if (!IsSafeUiPath(sanitizedPath))
|
||||
return std::string();
|
||||
|
||||
const std::filesystem::path assetPath = mUiRoot / sanitizedPath;
|
||||
contentType = GuessContentType(assetPath);
|
||||
return LoadTextFile(assetPath);
|
||||
}
|
||||
|
||||
std::string ControlServer::LoadTextFile(const std::filesystem::path& path) const
|
||||
{
|
||||
std::ifstream input(path, std::ios::binary);
|
||||
if (!input)
|
||||
return std::string();
|
||||
|
||||
std::ostringstream buffer;
|
||||
buffer << input.rdbuf();
|
||||
return buffer.str();
|
||||
}
|
||||
|
||||
std::string ControlServer::BuildJsonResponse(bool success, const std::string& error) const
|
||||
{
|
||||
JsonValue response = JsonValue::MakeObject();
|
||||
response.set("ok", JsonValue(success));
|
||||
if (!error.empty())
|
||||
response.set("error", JsonValue(error));
|
||||
return SerializeJson(response, false);
|
||||
}
|
||||
|
||||
std::string ControlServer::Base64Encode(const unsigned char* data, DWORD dataLength)
|
||||
{
|
||||
DWORD outputLength = 0;
|
||||
CryptBinaryToStringA(data, dataLength, CRYPT_STRING_BASE64 | CRYPT_STRING_NOCRLF, NULL, &outputLength);
|
||||
std::string encoded(outputLength, '\0');
|
||||
CryptBinaryToStringA(data, dataLength, CRYPT_STRING_BASE64 | CRYPT_STRING_NOCRLF, &encoded[0], &outputLength);
|
||||
if (!encoded.empty() && encoded.back() == '\0')
|
||||
encoded.pop_back();
|
||||
return encoded;
|
||||
}
|
||||
|
||||
std::string ControlServer::ComputeWebSocketAcceptKey(const std::string& clientKey)
|
||||
{
|
||||
const std::string combined = clientKey + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
|
||||
HCRYPTPROV provider = 0;
|
||||
HCRYPTHASH hash = 0;
|
||||
BYTE digest[20] = {};
|
||||
DWORD digestLength = sizeof(digest);
|
||||
|
||||
CryptAcquireContext(&provider, NULL, NULL, PROV_RSA_FULL, CRYPT_VERIFYCONTEXT);
|
||||
CryptCreateHash(provider, CALG_SHA1, 0, 0, &hash);
|
||||
CryptHashData(hash, reinterpret_cast<const BYTE*>(combined.data()), static_cast<DWORD>(combined.size()), 0);
|
||||
CryptGetHashParam(hash, HP_HASHVAL, digest, &digestLength, 0);
|
||||
|
||||
if (hash)
|
||||
CryptDestroyHash(hash);
|
||||
if (provider)
|
||||
CryptReleaseContext(provider, 0);
|
||||
|
||||
return Base64Encode(digest, digestLength);
|
||||
}
|
||||
|
||||
std::string ControlServer::GetHeaderValue(const HttpRequest& request, const std::string& headerName)
|
||||
{
|
||||
const auto header = request.headers.find(ToLower(headerName));
|
||||
return header == request.headers.end() ? std::string() : header->second;
|
||||
}
|
||||
|
||||
bool ControlServer::ParseHttpRequest(const std::string& rawRequest, HttpRequest& request)
|
||||
{
|
||||
const std::size_t requestLineEnd = rawRequest.find("\r\n");
|
||||
if (requestLineEnd == std::string::npos)
|
||||
return false;
|
||||
|
||||
const std::string requestLine = rawRequest.substr(0, requestLineEnd);
|
||||
const std::size_t methodEnd = requestLine.find(' ');
|
||||
if (methodEnd == std::string::npos)
|
||||
return false;
|
||||
|
||||
const std::size_t pathEnd = requestLine.find(' ', methodEnd + 1);
|
||||
if (pathEnd == std::string::npos)
|
||||
return false;
|
||||
|
||||
request.method = requestLine.substr(0, methodEnd);
|
||||
request.path = requestLine.substr(methodEnd + 1, pathEnd - methodEnd - 1);
|
||||
request.headers.clear();
|
||||
|
||||
const std::size_t headersStart = requestLineEnd + 2;
|
||||
const std::size_t bodySeparator = rawRequest.find("\r\n\r\n", headersStart);
|
||||
const std::size_t headersEnd = bodySeparator == std::string::npos ? rawRequest.size() : bodySeparator;
|
||||
|
||||
for (std::size_t lineStart = headersStart; lineStart < headersEnd;)
|
||||
{
|
||||
const std::size_t lineEnd = rawRequest.find("\r\n", lineStart);
|
||||
const std::size_t currentLineEnd = lineEnd == std::string::npos ? headersEnd : std::min(lineEnd, headersEnd);
|
||||
const std::string line = rawRequest.substr(lineStart, currentLineEnd - lineStart);
|
||||
const std::size_t separator = line.find(':');
|
||||
if (separator != std::string::npos)
|
||||
{
|
||||
const std::string key = ToLower(line.substr(0, separator));
|
||||
std::string value = line.substr(separator + 1);
|
||||
const std::size_t first = value.find_first_not_of(" \t");
|
||||
const std::size_t last = value.find_last_not_of(" \t");
|
||||
request.headers[key] = first == std::string::npos ? std::string() : value.substr(first, last - first + 1);
|
||||
}
|
||||
|
||||
if (lineEnd == std::string::npos || lineEnd >= headersEnd)
|
||||
break;
|
||||
lineStart = lineEnd + 2;
|
||||
}
|
||||
|
||||
request.body = bodySeparator == std::string::npos ? std::string() : rawRequest.substr(bodySeparator + 4);
|
||||
return !request.method.empty() && !request.path.empty();
|
||||
}
|
||||
@@ -1,107 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "NativeSockets.h"
|
||||
|
||||
#include <winsock2.h>
|
||||
|
||||
#include <atomic>
|
||||
#include <filesystem>
|
||||
#include <functional>
|
||||
#include <map>
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
#include <vector>
|
||||
|
||||
class JsonValue;
|
||||
|
||||
class ControlServer
|
||||
{
|
||||
public:
|
||||
struct Callbacks
|
||||
{
|
||||
std::function<std::string()> getStateJson;
|
||||
std::function<bool(const std::string&, std::string&)> addLayer;
|
||||
std::function<bool(const std::string&, std::string&)> removeLayer;
|
||||
std::function<bool(const std::string&, int, std::string&)> moveLayer;
|
||||
std::function<bool(const std::string&, std::size_t, std::string&)> moveLayerToIndex;
|
||||
std::function<bool(const std::string&, bool, std::string&)> setLayerBypass;
|
||||
std::function<bool(const std::string&, const std::string&, std::string&)> setLayerShader;
|
||||
std::function<bool(const std::string&, const std::string&, const std::string&, std::string&)> updateLayerParameter;
|
||||
std::function<bool(const std::string&, std::string&)> resetLayerParameters;
|
||||
std::function<bool(const std::string&, std::string&)> saveStackPreset;
|
||||
std::function<bool(const std::string&, std::string&)> loadStackPreset;
|
||||
std::function<bool(std::string&)> reloadShader;
|
||||
std::function<bool(std::string&)> requestScreenshot;
|
||||
};
|
||||
|
||||
ControlServer();
|
||||
~ControlServer();
|
||||
|
||||
bool Start(const std::filesystem::path& uiRoot, const std::filesystem::path& docsRoot, unsigned short preferredPort, const Callbacks& callbacks, std::string& error);
|
||||
void Stop();
|
||||
void BroadcastState();
|
||||
void RequestBroadcastState();
|
||||
|
||||
unsigned short GetPort() const { return mPort; }
|
||||
|
||||
private:
|
||||
struct ClientConnection
|
||||
{
|
||||
UniqueSocket socket;
|
||||
bool websocket = false;
|
||||
};
|
||||
|
||||
struct HttpRequest
|
||||
{
|
||||
std::string method;
|
||||
std::string path;
|
||||
std::map<std::string, std::string> headers;
|
||||
std::string body;
|
||||
};
|
||||
|
||||
struct HttpResponse
|
||||
{
|
||||
std::string status;
|
||||
std::string contentType;
|
||||
std::string body;
|
||||
bool broadcastState = false;
|
||||
};
|
||||
|
||||
void ServerLoop();
|
||||
bool HandleHttpClient(UniqueSocket clientSocket);
|
||||
bool TryAcceptClient();
|
||||
bool SendHttpResponse(SOCKET clientSocket, const HttpResponse& response);
|
||||
bool SendHttpResponse(SOCKET clientSocket, const std::string& status, const std::string& contentType, const std::string& body);
|
||||
bool HandleHttpRequest(UniqueSocket clientSocket, const std::string& request);
|
||||
bool HandleWebSocketUpgrade(UniqueSocket clientSocket, const HttpRequest& request);
|
||||
HttpResponse RouteHttpRequest(const HttpRequest& request);
|
||||
HttpResponse ServeGetRequest(const HttpRequest& request) const;
|
||||
HttpResponse ServeUiAsset(const std::string& relativePath) const;
|
||||
HttpResponse ServeDocsAsset(const std::string& relativePath) const;
|
||||
HttpResponse ServeOpenApiSpec() const;
|
||||
HttpResponse ServeSwaggerDocs() const;
|
||||
HttpResponse HandleApiPost(const HttpRequest& request);
|
||||
bool InvokePostRoute(const std::string& path, const JsonValue& root, std::string& actionError);
|
||||
bool SendWebSocketText(SOCKET clientSocket, const std::string& payload);
|
||||
void BroadcastStateLocked();
|
||||
std::string LoadUiAsset(const std::string& relativePath, std::string& contentType) const;
|
||||
std::string LoadTextFile(const std::filesystem::path& path) const;
|
||||
std::string BuildJsonResponse(bool success, const std::string& error = std::string()) const;
|
||||
static std::string Base64Encode(const unsigned char* data, DWORD dataLength);
|
||||
static std::string ComputeWebSocketAcceptKey(const std::string& clientKey);
|
||||
static std::string GetHeaderValue(const HttpRequest& request, const std::string& headerName);
|
||||
static bool ParseHttpRequest(const std::string& rawRequest, HttpRequest& request);
|
||||
|
||||
private:
|
||||
std::filesystem::path mUiRoot;
|
||||
std::filesystem::path mDocsRoot;
|
||||
Callbacks mCallbacks;
|
||||
UniqueSocket mListenSocket;
|
||||
unsigned short mPort;
|
||||
std::thread mThread;
|
||||
std::atomic<bool> mRunning;
|
||||
std::atomic<bool> mBroadcastPending;
|
||||
mutable std::mutex mMutex;
|
||||
std::vector<ClientConnection> mClients;
|
||||
};
|
||||
@@ -1,336 +0,0 @@
|
||||
#include "stdafx.h"
|
||||
#include "OscServer.h"
|
||||
|
||||
#include <ws2tcpip.h>
|
||||
|
||||
#include <array>
|
||||
#include <cstring>
|
||||
#include <iomanip>
|
||||
#include <sstream>
|
||||
#include <vector>
|
||||
|
||||
#pragma comment(lib, "Ws2_32.lib")
|
||||
|
||||
namespace
|
||||
{
|
||||
bool InitializeWinsock(std::string& error)
|
||||
{
|
||||
WSADATA wsaData = {};
|
||||
const int result = WSAStartup(MAKEWORD(2, 2), &wsaData);
|
||||
if (result != 0)
|
||||
{
|
||||
error = "WSAStartup failed.";
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
std::vector<std::string> SplitAddress(const std::string& address)
|
||||
{
|
||||
std::vector<std::string> parts;
|
||||
std::size_t start = !address.empty() && address[0] == '/' ? 1 : 0;
|
||||
|
||||
while (start <= address.size())
|
||||
{
|
||||
const std::size_t slash = address.find('/', start);
|
||||
const std::size_t end = slash == std::string::npos ? address.size() : slash;
|
||||
if (end > start)
|
||||
parts.push_back(address.substr(start, end - start));
|
||||
if (slash == std::string::npos)
|
||||
break;
|
||||
start = slash + 1;
|
||||
}
|
||||
return parts;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
OscServer::OscServer()
|
||||
: mPort(0), mRunning(false)
|
||||
{
|
||||
}
|
||||
|
||||
OscServer::~OscServer()
|
||||
{
|
||||
Stop();
|
||||
}
|
||||
|
||||
bool OscServer::Start(const std::string& bindAddress, unsigned short port, const Callbacks& callbacks, std::string& error)
|
||||
{
|
||||
if (port == 0)
|
||||
return true;
|
||||
|
||||
mCallbacks = callbacks;
|
||||
mPort = port;
|
||||
|
||||
if (!InitializeWinsock(error))
|
||||
return false;
|
||||
|
||||
mSocket.reset(socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP));
|
||||
if (!mSocket.valid())
|
||||
{
|
||||
error = "Could not create OSC UDP socket.";
|
||||
return false;
|
||||
}
|
||||
|
||||
DWORD timeoutMilliseconds = 100;
|
||||
setsockopt(mSocket.get(), SOL_SOCKET, SO_RCVTIMEO, reinterpret_cast<const char*>(&timeoutMilliseconds), sizeof(timeoutMilliseconds));
|
||||
|
||||
sockaddr_in address = {};
|
||||
address.sin_family = AF_INET;
|
||||
if (!TryParseBindAddress(bindAddress, address.sin_addr, error))
|
||||
{
|
||||
mSocket.reset();
|
||||
return false;
|
||||
}
|
||||
address.sin_port = htons(static_cast<u_short>(port));
|
||||
if (bind(mSocket.get(), reinterpret_cast<sockaddr*>(&address), sizeof(address)) != 0)
|
||||
{
|
||||
error = "Could not bind OSC listener to " + bindAddress + ":" + std::to_string(port) + ".";
|
||||
mSocket.reset();
|
||||
return false;
|
||||
}
|
||||
|
||||
mRunning = true;
|
||||
mThread = std::thread(&OscServer::ServerLoop, this);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool OscServer::TryParseBindAddress(const std::string& bindAddress, in_addr& address, std::string& error)
|
||||
{
|
||||
if (bindAddress.empty())
|
||||
{
|
||||
error = "OSC bind address must not be empty.";
|
||||
return false;
|
||||
}
|
||||
|
||||
address = {};
|
||||
if (InetPtonA(AF_INET, bindAddress.c_str(), &address) != 1)
|
||||
{
|
||||
error = "Invalid OSC bind address '" + bindAddress + "'. Use an IPv4 address such as 127.0.0.1 or 0.0.0.0.";
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void OscServer::Stop()
|
||||
{
|
||||
mRunning = false;
|
||||
mSocket.reset();
|
||||
if (mThread.joinable())
|
||||
mThread.join();
|
||||
}
|
||||
|
||||
void OscServer::ServerLoop()
|
||||
{
|
||||
std::array<char, 4096> buffer = {};
|
||||
while (mRunning)
|
||||
{
|
||||
sockaddr_in sender = {};
|
||||
int senderLength = sizeof(sender);
|
||||
const int byteCount = recvfrom(mSocket.get(), buffer.data(), static_cast<int>(buffer.size()), 0,
|
||||
reinterpret_cast<sockaddr*>(&sender), &senderLength);
|
||||
if (byteCount <= 0)
|
||||
continue;
|
||||
|
||||
OscMessage message;
|
||||
std::string error;
|
||||
if (DecodeMessage(buffer.data(), byteCount, message, error))
|
||||
{
|
||||
if (!DispatchMessage(message, error) && !error.empty())
|
||||
OutputDebugStringA(("OSC dispatch failed: " + error + "\n").c_str());
|
||||
}
|
||||
else if (!error.empty())
|
||||
{
|
||||
OutputDebugStringA(("OSC decode failed: " + error + "\n").c_str());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool OscServer::DecodeMessage(const char* data, int byteCount, OscMessage& message, std::string& error) const
|
||||
{
|
||||
int offset = 0;
|
||||
if (!ReadPaddedString(data, byteCount, offset, message.address) || message.address.empty() || message.address[0] != '/')
|
||||
{
|
||||
error = "Invalid OSC address.";
|
||||
return false;
|
||||
}
|
||||
|
||||
std::string typeTags;
|
||||
if (!ReadPaddedString(data, byteCount, offset, typeTags) || typeTags.empty() || typeTags[0] != ',')
|
||||
{
|
||||
error = "Invalid OSC type tag string.";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (typeTags.size() < 2)
|
||||
{
|
||||
error = "OSC message has no parameter value.";
|
||||
return false;
|
||||
}
|
||||
|
||||
std::vector<std::string> values;
|
||||
for (std::size_t index = 1; index < typeTags.size(); ++index)
|
||||
{
|
||||
std::string valueJson;
|
||||
if (!DecodeArgument(data, byteCount, offset, typeTags[index], valueJson))
|
||||
{
|
||||
error = "Unsupported or malformed OSC value type.";
|
||||
return false;
|
||||
}
|
||||
values.push_back(valueJson);
|
||||
}
|
||||
|
||||
if (values.size() == 1)
|
||||
{
|
||||
message.valueJson = values.front();
|
||||
return true;
|
||||
}
|
||||
|
||||
std::ostringstream arrayJson;
|
||||
arrayJson << "[";
|
||||
for (std::size_t index = 0; index < values.size(); ++index)
|
||||
{
|
||||
if (index > 0)
|
||||
arrayJson << ",";
|
||||
arrayJson << values[index];
|
||||
}
|
||||
arrayJson << "]";
|
||||
message.valueJson = arrayJson.str();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool OscServer::DispatchMessage(const OscMessage& message, std::string& error) const
|
||||
{
|
||||
const std::vector<std::string> parts = SplitAddress(message.address);
|
||||
if (parts.size() != 3 || parts[0] != "VideoShaderToys")
|
||||
{
|
||||
error = "Unsupported OSC address: " + message.address;
|
||||
return false;
|
||||
}
|
||||
|
||||
return mCallbacks.updateParameter &&
|
||||
mCallbacks.updateParameter(parts[1], parts[2], message.valueJson, error);
|
||||
}
|
||||
|
||||
bool OscServer::DecodeArgument(const char* data, int byteCount, int& offset, char valueType, std::string& valueJson)
|
||||
{
|
||||
if (valueType == 'f')
|
||||
{
|
||||
double value = 0.0;
|
||||
if (!ReadFloat32(data, byteCount, offset, value))
|
||||
return false;
|
||||
std::ostringstream stream;
|
||||
stream << std::setprecision(9) << value;
|
||||
valueJson = stream.str();
|
||||
return true;
|
||||
}
|
||||
|
||||
if (valueType == 'd')
|
||||
{
|
||||
double value = 0.0;
|
||||
if (!ReadFloat64(data, byteCount, offset, value))
|
||||
return false;
|
||||
std::ostringstream stream;
|
||||
stream << std::setprecision(17) << value;
|
||||
valueJson = stream.str();
|
||||
return true;
|
||||
}
|
||||
|
||||
if (valueType == 'i')
|
||||
{
|
||||
int value = 0;
|
||||
if (!ReadInt32(data, byteCount, offset, value))
|
||||
return false;
|
||||
valueJson = std::to_string(value);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (valueType == 's')
|
||||
{
|
||||
std::string value;
|
||||
if (!ReadPaddedString(data, byteCount, offset, value))
|
||||
return false;
|
||||
valueJson = BuildJsonString(value);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (valueType == 'T' || valueType == 'F')
|
||||
{
|
||||
valueJson = valueType == 'T' ? "true" : "false";
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
bool OscServer::ReadPaddedString(const char* data, int byteCount, int& offset, std::string& value)
|
||||
{
|
||||
if (offset < 0 || offset >= byteCount)
|
||||
return false;
|
||||
|
||||
const int start = offset;
|
||||
while (offset < byteCount && data[offset] != '\0')
|
||||
++offset;
|
||||
if (offset >= byteCount)
|
||||
return false;
|
||||
|
||||
value.assign(data + start, data + offset);
|
||||
++offset;
|
||||
while (offset % 4 != 0)
|
||||
++offset;
|
||||
return offset <= byteCount;
|
||||
}
|
||||
|
||||
bool OscServer::ReadInt32(const char* data, int byteCount, int& offset, int& value)
|
||||
{
|
||||
if (offset + 4 > byteCount)
|
||||
return false;
|
||||
const unsigned char* bytes = reinterpret_cast<const unsigned char*>(data + offset);
|
||||
value = static_cast<int>((bytes[0] << 24) | (bytes[1] << 16) | (bytes[2] << 8) | bytes[3]);
|
||||
offset += 4;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool OscServer::ReadFloat32(const char* data, int byteCount, int& offset, double& value)
|
||||
{
|
||||
int bits = 0;
|
||||
if (!ReadInt32(data, byteCount, offset, bits))
|
||||
return false;
|
||||
|
||||
float floatValue = 0.0f;
|
||||
const unsigned int unsignedBits = static_cast<unsigned int>(bits);
|
||||
std::memcpy(&floatValue, &unsignedBits, sizeof(floatValue));
|
||||
value = static_cast<double>(floatValue);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool OscServer::ReadFloat64(const char* data, int byteCount, int& offset, double& value)
|
||||
{
|
||||
if (offset + 8 > byteCount)
|
||||
return false;
|
||||
|
||||
const unsigned char* bytes = reinterpret_cast<const unsigned char*>(data + offset);
|
||||
uint64_t bits = 0;
|
||||
for (int index = 0; index < 8; ++index)
|
||||
bits = (bits << 8) | static_cast<uint64_t>(bytes[index]);
|
||||
|
||||
std::memcpy(&value, &bits, sizeof(value));
|
||||
offset += 8;
|
||||
return true;
|
||||
}
|
||||
|
||||
std::string OscServer::BuildJsonString(const std::string& value)
|
||||
{
|
||||
std::ostringstream stream;
|
||||
stream << '"';
|
||||
for (char ch : value)
|
||||
{
|
||||
if (ch == '"' || ch == '\\')
|
||||
stream << '\\';
|
||||
stream << ch;
|
||||
}
|
||||
stream << '"';
|
||||
return stream.str();
|
||||
}
|
||||
@@ -1,53 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "NativeSockets.h"
|
||||
|
||||
#include <winsock2.h>
|
||||
|
||||
#include <atomic>
|
||||
#include <functional>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
|
||||
class OscServer
|
||||
{
|
||||
public:
|
||||
struct Callbacks
|
||||
{
|
||||
std::function<bool(const std::string&, const std::string&, const std::string&, std::string&)> updateParameter;
|
||||
};
|
||||
|
||||
OscServer();
|
||||
~OscServer();
|
||||
|
||||
bool Start(const std::string& bindAddress, unsigned short port, const Callbacks& callbacks, std::string& error);
|
||||
void Stop();
|
||||
|
||||
unsigned short GetPort() const { return mPort; }
|
||||
|
||||
private:
|
||||
friend struct OscServerTestAccess;
|
||||
|
||||
struct OscMessage
|
||||
{
|
||||
std::string address;
|
||||
std::string valueJson;
|
||||
};
|
||||
|
||||
void ServerLoop();
|
||||
bool DecodeMessage(const char* data, int byteCount, OscMessage& message, std::string& error) const;
|
||||
bool DispatchMessage(const OscMessage& message, std::string& error) const;
|
||||
static bool TryParseBindAddress(const std::string& bindAddress, in_addr& address, std::string& error);
|
||||
static bool DecodeArgument(const char* data, int byteCount, int& offset, char valueType, std::string& valueJson);
|
||||
static bool ReadPaddedString(const char* data, int byteCount, int& offset, std::string& value);
|
||||
static bool ReadInt32(const char* data, int byteCount, int& offset, int& value);
|
||||
static bool ReadFloat32(const char* data, int byteCount, int& offset, double& value);
|
||||
static bool ReadFloat64(const char* data, int byteCount, int& offset, double& value);
|
||||
static std::string BuildJsonString(const std::string& value);
|
||||
|
||||
Callbacks mCallbacks;
|
||||
UniqueSocket mSocket;
|
||||
unsigned short mPort;
|
||||
std::thread mThread;
|
||||
std::atomic<bool> mRunning;
|
||||
};
|
||||
@@ -1,53 +0,0 @@
|
||||
#include "RuntimeControlBridge.h"
|
||||
|
||||
#include "ControlServer.h"
|
||||
#include "OpenGLComposite.h"
|
||||
#include "OscServer.h"
|
||||
#include "RuntimeHost.h"
|
||||
#include "RuntimeServices.h"
|
||||
|
||||
bool StartRuntimeControlServices(
|
||||
OpenGLComposite& composite,
|
||||
RuntimeHost& runtimeHost,
|
||||
RuntimeServices& runtimeServices,
|
||||
ControlServer& controlServer,
|
||||
OscServer& oscServer,
|
||||
std::string& error)
|
||||
{
|
||||
ControlServer::Callbacks callbacks;
|
||||
callbacks.getStateJson = [&composite]() { return composite.GetRuntimeStateJson(); };
|
||||
callbacks.addLayer = [&composite](const std::string& shaderId, std::string& actionError) { return composite.AddLayer(shaderId, actionError); };
|
||||
callbacks.removeLayer = [&composite](const std::string& layerId, std::string& actionError) { return composite.RemoveLayer(layerId, actionError); };
|
||||
callbacks.moveLayer = [&composite](const std::string& layerId, int direction, std::string& actionError) { return composite.MoveLayer(layerId, direction, actionError); };
|
||||
callbacks.moveLayerToIndex = [&composite](const std::string& layerId, std::size_t targetIndex, std::string& actionError) { return composite.MoveLayerToIndex(layerId, targetIndex, actionError); };
|
||||
callbacks.setLayerBypass = [&composite](const std::string& layerId, bool bypassed, std::string& actionError) { return composite.SetLayerBypass(layerId, bypassed, actionError); };
|
||||
callbacks.setLayerShader = [&composite](const std::string& layerId, const std::string& shaderId, std::string& actionError) { return composite.SetLayerShader(layerId, shaderId, actionError); };
|
||||
callbacks.updateLayerParameter = [&composite](const std::string& layerId, const std::string& parameterId, const std::string& valueJson, std::string& actionError) {
|
||||
return composite.UpdateLayerParameterJson(layerId, parameterId, valueJson, actionError);
|
||||
};
|
||||
callbacks.resetLayerParameters = [&composite](const std::string& layerId, std::string& actionError) { return composite.ResetLayerParameters(layerId, actionError); };
|
||||
callbacks.saveStackPreset = [&composite](const std::string& presetName, std::string& actionError) { return composite.SaveStackPreset(presetName, actionError); };
|
||||
callbacks.loadStackPreset = [&composite](const std::string& presetName, std::string& actionError) { return composite.LoadStackPreset(presetName, actionError); };
|
||||
callbacks.requestScreenshot = [&composite](std::string& actionError) { return composite.RequestScreenshot(actionError); };
|
||||
callbacks.reloadShader = [&composite](std::string& actionError) {
|
||||
if (!composite.ReloadShader())
|
||||
{
|
||||
actionError = "Shader reload failed. See native app status for details.";
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
if (!controlServer.Start(runtimeHost.GetUiRoot(), runtimeHost.GetDocsRoot(), runtimeHost.GetServerPort(), callbacks, error))
|
||||
return false;
|
||||
runtimeHost.SetServerPort(controlServer.GetPort());
|
||||
|
||||
OscServer::Callbacks oscCallbacks;
|
||||
oscCallbacks.updateParameter = [&runtimeServices](const std::string& layerKey, const std::string& parameterKey, const std::string& valueJson, std::string& actionError) {
|
||||
return runtimeServices.QueueOscUpdate(layerKey, parameterKey, valueJson, actionError);
|
||||
};
|
||||
if (runtimeHost.GetOscPort() > 0 && !oscServer.Start(runtimeHost.GetOscBindAddress(), runtimeHost.GetOscPort(), oscCallbacks, error))
|
||||
return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
|
||||
class ControlServer;
|
||||
class OpenGLComposite;
|
||||
class OscServer;
|
||||
class RuntimeHost;
|
||||
class RuntimeServices;
|
||||
|
||||
bool StartRuntimeControlServices(
|
||||
OpenGLComposite& composite,
|
||||
RuntimeHost& runtimeHost,
|
||||
RuntimeServices& runtimeServices,
|
||||
ControlServer& controlServer,
|
||||
OscServer& oscServer,
|
||||
std::string& error);
|
||||
@@ -1,247 +0,0 @@
|
||||
#include "RuntimeServices.h"
|
||||
|
||||
#include "ControlServer.h"
|
||||
#include "OscServer.h"
|
||||
#include "RuntimeControlBridge.h"
|
||||
#include "RuntimeHost.h"
|
||||
#include <windows.h>
|
||||
|
||||
RuntimeServices::RuntimeServices() :
|
||||
mControlServer(std::make_unique<ControlServer>()),
|
||||
mOscServer(std::make_unique<OscServer>()),
|
||||
mPollRunning(false),
|
||||
mRegistryChanged(false),
|
||||
mReloadRequested(false),
|
||||
mPollFailed(false)
|
||||
{
|
||||
}
|
||||
|
||||
RuntimeServices::~RuntimeServices()
|
||||
{
|
||||
Stop();
|
||||
}
|
||||
|
||||
bool RuntimeServices::Start(OpenGLComposite& composite, RuntimeHost& runtimeHost, std::string& error)
|
||||
{
|
||||
Stop();
|
||||
|
||||
if (!StartRuntimeControlServices(composite, runtimeHost, *this, *mControlServer, *mOscServer, error))
|
||||
{
|
||||
Stop();
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void RuntimeServices::BeginPolling(RuntimeHost& runtimeHost)
|
||||
{
|
||||
StartPolling(runtimeHost);
|
||||
}
|
||||
|
||||
void RuntimeServices::Stop()
|
||||
{
|
||||
StopPolling();
|
||||
|
||||
if (mOscServer)
|
||||
mOscServer->Stop();
|
||||
|
||||
if (mControlServer)
|
||||
mControlServer->Stop();
|
||||
}
|
||||
|
||||
void RuntimeServices::BroadcastState()
|
||||
{
|
||||
if (mControlServer)
|
||||
mControlServer->BroadcastState();
|
||||
}
|
||||
|
||||
void RuntimeServices::RequestBroadcastState()
|
||||
{
|
||||
if (mControlServer)
|
||||
mControlServer->RequestBroadcastState();
|
||||
}
|
||||
|
||||
bool RuntimeServices::QueueOscUpdate(const std::string& layerKey, const std::string& parameterKey, const std::string& valueJson, std::string& error)
|
||||
{
|
||||
(void)error;
|
||||
|
||||
PendingOscUpdate update;
|
||||
update.layerKey = layerKey;
|
||||
update.parameterKey = parameterKey;
|
||||
update.valueJson = valueJson;
|
||||
|
||||
const std::string routeKey = layerKey + "\n" + parameterKey;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mPendingOscMutex);
|
||||
mPendingOscUpdates[routeKey] = std::move(update);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool RuntimeServices::ApplyPendingOscUpdates(std::vector<AppliedOscUpdate>& appliedUpdates, std::string& error)
|
||||
{
|
||||
appliedUpdates.clear();
|
||||
|
||||
std::map<std::string, PendingOscUpdate> pending;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mPendingOscMutex);
|
||||
if (mPendingOscUpdates.empty())
|
||||
return true;
|
||||
pending.swap(mPendingOscUpdates);
|
||||
}
|
||||
|
||||
for (const auto& entry : pending)
|
||||
{
|
||||
JsonValue targetValue;
|
||||
std::string parseError;
|
||||
if (!ParseJson(entry.second.valueJson, targetValue, parseError))
|
||||
{
|
||||
OutputDebugStringA(("OSC queued value parse failed: " + parseError + "\n").c_str());
|
||||
continue;
|
||||
}
|
||||
|
||||
AppliedOscUpdate appliedUpdate;
|
||||
appliedUpdate.routeKey = entry.first;
|
||||
appliedUpdate.layerKey = entry.second.layerKey;
|
||||
appliedUpdate.parameterKey = entry.second.parameterKey;
|
||||
appliedUpdate.targetValue = targetValue;
|
||||
appliedUpdates.push_back(std::move(appliedUpdate));
|
||||
}
|
||||
|
||||
(void)error;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool RuntimeServices::QueueOscCommit(const std::string& routeKey, const std::string& layerKey, const std::string& parameterKey, const JsonValue& value, uint64_t generation, std::string& error)
|
||||
{
|
||||
(void)error;
|
||||
|
||||
PendingOscCommit commit;
|
||||
commit.routeKey = routeKey;
|
||||
commit.layerKey = layerKey;
|
||||
commit.parameterKey = parameterKey;
|
||||
commit.value = value;
|
||||
commit.generation = generation;
|
||||
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mPendingOscCommitMutex);
|
||||
mPendingOscCommits[routeKey] = std::move(commit);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void RuntimeServices::ClearOscState()
|
||||
{
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mPendingOscMutex);
|
||||
mPendingOscUpdates.clear();
|
||||
}
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mPendingOscCommitMutex);
|
||||
mPendingOscCommits.clear();
|
||||
}
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mCompletedOscCommitMutex);
|
||||
mCompletedOscCommits.clear();
|
||||
}
|
||||
}
|
||||
|
||||
void RuntimeServices::ConsumeCompletedOscCommits(std::vector<CompletedOscCommit>& completedCommits)
|
||||
{
|
||||
completedCommits.clear();
|
||||
|
||||
std::lock_guard<std::mutex> lock(mCompletedOscCommitMutex);
|
||||
if (mCompletedOscCommits.empty())
|
||||
return;
|
||||
|
||||
completedCommits.swap(mCompletedOscCommits);
|
||||
}
|
||||
|
||||
RuntimePollEvents RuntimeServices::ConsumePollEvents()
|
||||
{
|
||||
RuntimePollEvents events;
|
||||
events.registryChanged = mRegistryChanged.exchange(false);
|
||||
events.reloadRequested = mReloadRequested.exchange(false);
|
||||
events.failed = mPollFailed.exchange(false);
|
||||
|
||||
if (events.failed)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mPollErrorMutex);
|
||||
events.error = mPollError;
|
||||
}
|
||||
|
||||
return events;
|
||||
}
|
||||
|
||||
void RuntimeServices::StartPolling(RuntimeHost& runtimeHost)
|
||||
{
|
||||
if (mPollRunning.exchange(true))
|
||||
return;
|
||||
|
||||
mPollThread = std::thread([this, &runtimeHost]() { PollLoop(runtimeHost); });
|
||||
}
|
||||
|
||||
void RuntimeServices::StopPolling()
|
||||
{
|
||||
if (!mPollRunning.exchange(false))
|
||||
return;
|
||||
|
||||
if (mPollThread.joinable())
|
||||
mPollThread.join();
|
||||
}
|
||||
|
||||
void RuntimeServices::PollLoop(RuntimeHost& runtimeHost)
|
||||
{
|
||||
while (mPollRunning)
|
||||
{
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -1,94 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "RuntimeJson.h"
|
||||
#include "ShaderTypes.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 RuntimeServices
|
||||
{
|
||||
public:
|
||||
struct AppliedOscUpdate
|
||||
{
|
||||
std::string routeKey;
|
||||
std::string layerKey;
|
||||
std::string parameterKey;
|
||||
JsonValue targetValue;
|
||||
};
|
||||
|
||||
struct CompletedOscCommit
|
||||
{
|
||||
std::string routeKey;
|
||||
uint64_t generation = 0;
|
||||
};
|
||||
|
||||
RuntimeServices();
|
||||
~RuntimeServices();
|
||||
|
||||
bool Start(OpenGLComposite& composite, RuntimeHost& runtimeHost, std::string& error);
|
||||
void BeginPolling(RuntimeHost& runtimeHost);
|
||||
void Stop();
|
||||
void BroadcastState();
|
||||
void RequestBroadcastState();
|
||||
bool QueueOscUpdate(const std::string& layerKey, const std::string& parameterKey, const std::string& valueJson, std::string& error);
|
||||
bool ApplyPendingOscUpdates(std::vector<AppliedOscUpdate>& appliedUpdates, std::string& error);
|
||||
bool QueueOscCommit(const std::string& routeKey, const std::string& layerKey, const std::string& parameterKey, const JsonValue& value, uint64_t generation, std::string& error);
|
||||
void ClearOscState();
|
||||
void ConsumeCompletedOscCommits(std::vector<CompletedOscCommit>& completedCommits);
|
||||
RuntimePollEvents ConsumePollEvents();
|
||||
|
||||
private:
|
||||
struct PendingOscUpdate
|
||||
{
|
||||
std::string layerKey;
|
||||
std::string parameterKey;
|
||||
std::string valueJson;
|
||||
};
|
||||
|
||||
struct PendingOscCommit
|
||||
{
|
||||
std::string routeKey;
|
||||
std::string layerKey;
|
||||
std::string parameterKey;
|
||||
JsonValue value;
|
||||
uint64_t generation = 0;
|
||||
};
|
||||
|
||||
void StartPolling(RuntimeHost& runtimeHost);
|
||||
void StopPolling();
|
||||
void PollLoop(RuntimeHost& runtimeHost);
|
||||
|
||||
std::unique_ptr<ControlServer> mControlServer;
|
||||
std::unique_ptr<OscServer> mOscServer;
|
||||
std::thread mPollThread;
|
||||
std::atomic<bool> mPollRunning;
|
||||
std::atomic<bool> mRegistryChanged;
|
||||
std::atomic<bool> mReloadRequested;
|
||||
std::atomic<bool> mPollFailed;
|
||||
std::mutex mPollErrorMutex;
|
||||
std::string mPollError;
|
||||
std::mutex mPendingOscMutex;
|
||||
std::map<std::string, PendingOscUpdate> mPendingOscUpdates;
|
||||
std::mutex mPendingOscCommitMutex;
|
||||
std::map<std::string, PendingOscCommit> mPendingOscCommits;
|
||||
std::mutex mCompletedOscCommitMutex;
|
||||
std::vector<CompletedOscCommit> mCompletedOscCommits;
|
||||
};
|
||||
@@ -1,800 +0,0 @@
|
||||
#include "DeckLinkDisplayMode.h"
|
||||
#include "DeckLinkSession.h"
|
||||
#include "OpenGLComposite.h"
|
||||
#include "GLExtensions.h"
|
||||
#include "GlRenderConstants.h"
|
||||
#include "OpenGLRenderPass.h"
|
||||
#include "OpenGLRenderPipeline.h"
|
||||
#include "OpenGLShaderPrograms.h"
|
||||
#include "OpenGLVideoIOBridge.h"
|
||||
#include "PngScreenshotWriter.h"
|
||||
#include "RuntimeParameterUtils.h"
|
||||
#include "RuntimeServices.h"
|
||||
#include "ShaderBuildQueue.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
#include <chrono>
|
||||
#include <cmath>
|
||||
#include <ctime>
|
||||
#include <filesystem>
|
||||
#include <iomanip>
|
||||
#include <memory>
|
||||
#include <set>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace
|
||||
{
|
||||
constexpr auto kOscOverlayCommitDelay = std::chrono::milliseconds(150);
|
||||
constexpr double kOscSmoothingReferenceFps = 60.0;
|
||||
constexpr double kOscSmoothingMaxStepSeconds = 0.25;
|
||||
|
||||
std::string SimplifyOscControlKey(const std::string& text)
|
||||
{
|
||||
std::string simplified;
|
||||
for (unsigned char ch : text)
|
||||
{
|
||||
if (std::isalnum(ch))
|
||||
simplified.push_back(static_cast<char>(std::tolower(ch)));
|
||||
}
|
||||
return simplified;
|
||||
}
|
||||
|
||||
bool MatchesOscControlKey(const std::string& candidate, const std::string& key)
|
||||
{
|
||||
return candidate == key || SimplifyOscControlKey(candidate) == SimplifyOscControlKey(key);
|
||||
}
|
||||
|
||||
double ClampOscAlpha(double value)
|
||||
{
|
||||
return (std::max)(0.0, (std::min)(1.0, value));
|
||||
}
|
||||
|
||||
double ComputeTimeBasedOscAlpha(double smoothing, double deltaSeconds)
|
||||
{
|
||||
const double clampedSmoothing = ClampOscAlpha(smoothing);
|
||||
if (clampedSmoothing <= 0.0)
|
||||
return 0.0;
|
||||
if (clampedSmoothing >= 1.0)
|
||||
return 1.0;
|
||||
|
||||
const double clampedDeltaSeconds = (std::max)(0.0, (std::min)(kOscSmoothingMaxStepSeconds, deltaSeconds));
|
||||
if (clampedDeltaSeconds <= 0.0)
|
||||
return 0.0;
|
||||
|
||||
const double frameScale = clampedDeltaSeconds * kOscSmoothingReferenceFps;
|
||||
return ClampOscAlpha(1.0 - std::pow(1.0 - clampedSmoothing, frameScale));
|
||||
}
|
||||
|
||||
JsonValue BuildOscCommitValue(const ShaderParameterDefinition& definition, const ShaderParameterValue& value)
|
||||
{
|
||||
switch (definition.type)
|
||||
{
|
||||
case ShaderParameterType::Boolean:
|
||||
return JsonValue(value.booleanValue);
|
||||
case ShaderParameterType::Enum:
|
||||
return JsonValue(value.enumValue);
|
||||
case ShaderParameterType::Text:
|
||||
return JsonValue(value.textValue);
|
||||
case ShaderParameterType::Trigger:
|
||||
case ShaderParameterType::Float:
|
||||
return JsonValue(value.numberValues.empty() ? 0.0 : value.numberValues.front());
|
||||
case ShaderParameterType::Vec2:
|
||||
case ShaderParameterType::Color:
|
||||
{
|
||||
JsonValue array = JsonValue::MakeArray();
|
||||
for (double number : value.numberValues)
|
||||
array.pushBack(JsonValue(number));
|
||||
return array;
|
||||
}
|
||||
}
|
||||
|
||||
return JsonValue();
|
||||
}
|
||||
}
|
||||
|
||||
OpenGLComposite::OpenGLComposite(HWND hWnd, HDC hDC, HGLRC hRC) :
|
||||
hGLWnd(hWnd), hGLDC(hDC), hGLRC(hRC),
|
||||
mVideoIO(std::make_unique<DeckLinkSession>()),
|
||||
mRenderer(std::make_unique<OpenGLRenderer>()),
|
||||
mUseCommittedLayerStates(false),
|
||||
mScreenshotRequested(false)
|
||||
{
|
||||
InitializeCriticalSection(&pMutex);
|
||||
mRuntimeHost = std::make_unique<RuntimeHost>();
|
||||
mRenderPipeline = std::make_unique<OpenGLRenderPipeline>(
|
||||
*mRenderer,
|
||||
*mRuntimeHost,
|
||||
[this]() { renderEffect(); },
|
||||
[this]() { ProcessScreenshotRequest(); },
|
||||
[this]() { paintGL(false); });
|
||||
mVideoIOBridge = std::make_unique<OpenGLVideoIOBridge>(
|
||||
*mVideoIO,
|
||||
*mRenderer,
|
||||
*mRenderPipeline,
|
||||
*mRuntimeHost,
|
||||
pMutex,
|
||||
hGLDC,
|
||||
hGLRC);
|
||||
mRenderPass = std::make_unique<OpenGLRenderPass>(*mRenderer);
|
||||
mShaderPrograms = std::make_unique<OpenGLShaderPrograms>(*mRenderer, *mRuntimeHost);
|
||||
mShaderBuildQueue = std::make_unique<ShaderBuildQueue>(*mRuntimeHost);
|
||||
mRuntimeServices = std::make_unique<RuntimeServices>();
|
||||
}
|
||||
|
||||
OpenGLComposite::~OpenGLComposite()
|
||||
{
|
||||
if (mRuntimeServices)
|
||||
mRuntimeServices->Stop();
|
||||
if (mShaderBuildQueue)
|
||||
mShaderBuildQueue->Stop();
|
||||
mVideoIO->ReleaseResources();
|
||||
mRenderer->DestroyResources();
|
||||
|
||||
DeleteCriticalSection(&pMutex);
|
||||
}
|
||||
|
||||
bool OpenGLComposite::InitDeckLink()
|
||||
{
|
||||
return InitVideoIO();
|
||||
}
|
||||
|
||||
bool OpenGLComposite::InitVideoIO()
|
||||
{
|
||||
VideoFormatSelection videoModes;
|
||||
std::string initFailureReason;
|
||||
|
||||
if (mRuntimeHost && mRuntimeHost->GetRepoRoot().empty())
|
||||
{
|
||||
std::string runtimeError;
|
||||
if (!mRuntimeHost->Initialize(runtimeError))
|
||||
{
|
||||
MessageBoxA(NULL, runtimeError.c_str(), "Runtime host failed to initialize", MB_OK);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (mRuntimeHost)
|
||||
{
|
||||
if (!ResolveConfiguredVideoFormats(
|
||||
mRuntimeHost->GetInputVideoFormat(),
|
||||
mRuntimeHost->GetInputFrameRate(),
|
||||
mRuntimeHost->GetOutputVideoFormat(),
|
||||
mRuntimeHost->GetOutputFrameRate(),
|
||||
videoModes,
|
||||
initFailureReason))
|
||||
{
|
||||
MessageBoxA(NULL, initFailureReason.c_str(), "DeckLink mode configuration error", MB_OK);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (!mVideoIO->DiscoverDevicesAndModes(videoModes, initFailureReason))
|
||||
{
|
||||
const char* title = initFailureReason == "Please install the Blackmagic DeckLink drivers to use the features of this application."
|
||||
? "This application requires the DeckLink drivers installed."
|
||||
: "DeckLink initialization failed";
|
||||
MessageBoxA(NULL, initFailureReason.c_str(), title, MB_OK | MB_ICONERROR);
|
||||
return false;
|
||||
}
|
||||
const bool outputAlphaRequired = mRuntimeHost && mRuntimeHost->ExternalKeyingEnabled();
|
||||
if (!mVideoIO->SelectPreferredFormats(videoModes, outputAlphaRequired, initFailureReason))
|
||||
goto error;
|
||||
|
||||
if (! CheckOpenGLExtensions())
|
||||
{
|
||||
initFailureReason = "OpenGL extension checks failed.";
|
||||
goto error;
|
||||
}
|
||||
|
||||
if (! InitOpenGLState())
|
||||
{
|
||||
initFailureReason = "OpenGL state initialization failed.";
|
||||
goto error;
|
||||
}
|
||||
|
||||
PublishVideoIOStatus(mVideoIO->OutputModelName().empty()
|
||||
? "DeckLink output device selected."
|
||||
: ("Selected output device: " + mVideoIO->OutputModelName()));
|
||||
|
||||
// Resize window to match output video frame, but scale large formats down by half for viewing.
|
||||
if (mVideoIO->OutputFrameWidth() < 1920)
|
||||
resizeWindow(mVideoIO->OutputFrameWidth(), mVideoIO->OutputFrameHeight());
|
||||
else
|
||||
resizeWindow(mVideoIO->OutputFrameWidth() / 2, mVideoIO->OutputFrameHeight() / 2);
|
||||
|
||||
if (!mVideoIO->ConfigureInput([this](const VideoIOFrame& frame) { mVideoIOBridge->VideoFrameArrived(frame); }, videoModes.input, initFailureReason))
|
||||
{
|
||||
goto error;
|
||||
}
|
||||
if (!mVideoIO->HasInputDevice() && mRuntimeHost)
|
||||
{
|
||||
mRuntimeHost->SetSignalStatus(false, mVideoIO->InputFrameWidth(), mVideoIO->InputFrameHeight(), mVideoIO->InputDisplayModeName());
|
||||
}
|
||||
|
||||
if (!mVideoIO->ConfigureOutput([this](const VideoIOCompletion& completion) { mVideoIOBridge->PlayoutFrameCompleted(completion); }, videoModes.output, mRuntimeHost && mRuntimeHost->ExternalKeyingEnabled(), initFailureReason))
|
||||
{
|
||||
goto error;
|
||||
}
|
||||
|
||||
PublishVideoIOStatus(mVideoIO->StatusMessage());
|
||||
|
||||
return true;
|
||||
|
||||
error:
|
||||
if (!initFailureReason.empty())
|
||||
MessageBoxA(NULL, initFailureReason.c_str(), "DeckLink initialization failed", MB_OK | MB_ICONERROR);
|
||||
mVideoIO->ReleaseResources();
|
||||
return false;
|
||||
}
|
||||
|
||||
void OpenGLComposite::paintGL(bool force)
|
||||
{
|
||||
if (!force)
|
||||
{
|
||||
if (IsIconic(hGLWnd))
|
||||
return;
|
||||
|
||||
const unsigned previewFps = mRuntimeHost ? mRuntimeHost->GetPreviewFps() : 30u;
|
||||
if (previewFps == 0)
|
||||
return;
|
||||
|
||||
const auto now = std::chrono::steady_clock::now();
|
||||
const auto minimumInterval = std::chrono::microseconds(1000000 / (previewFps == 0 ? 1u : previewFps));
|
||||
if (mLastPreviewPresentTime != std::chrono::steady_clock::time_point() &&
|
||||
now - mLastPreviewPresentTime < minimumInterval)
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (!TryEnterCriticalSection(&pMutex))
|
||||
{
|
||||
ValidateRect(hGLWnd, NULL);
|
||||
return;
|
||||
}
|
||||
|
||||
mRenderer->PresentToWindow(hGLDC, mVideoIO->OutputFrameWidth(), mVideoIO->OutputFrameHeight());
|
||||
mLastPreviewPresentTime = std::chrono::steady_clock::now();
|
||||
ValidateRect(hGLWnd, NULL);
|
||||
LeaveCriticalSection(&pMutex);
|
||||
}
|
||||
|
||||
void OpenGLComposite::resizeGL(WORD width, WORD height)
|
||||
{
|
||||
// We don't set the project or model matrices here since the window data is copied directly from
|
||||
// an off-screen FBO in paintGL(). Just save the width and height for use in paintGL().
|
||||
mRenderer->ResizeView(width, height);
|
||||
}
|
||||
|
||||
void OpenGLComposite::resizeWindow(int width, int height)
|
||||
{
|
||||
RECT r;
|
||||
if (GetWindowRect(hGLWnd, &r))
|
||||
{
|
||||
SetWindowPos(hGLWnd, HWND_TOP, r.left, r.top, r.left + width, r.top + height, 0);
|
||||
}
|
||||
}
|
||||
|
||||
void OpenGLComposite::PublishVideoIOStatus(const std::string& statusMessage)
|
||||
{
|
||||
if (!mRuntimeHost)
|
||||
return;
|
||||
|
||||
if (!statusMessage.empty())
|
||||
mVideoIO->SetStatusMessage(statusMessage);
|
||||
|
||||
mRuntimeHost->SetVideoIOStatus(
|
||||
"decklink",
|
||||
mVideoIO->OutputModelName(),
|
||||
mVideoIO->SupportsInternalKeying(),
|
||||
mVideoIO->SupportsExternalKeying(),
|
||||
mVideoIO->KeyerInterfaceAvailable(),
|
||||
mRuntimeHost->ExternalKeyingEnabled(),
|
||||
mVideoIO->ExternalKeyingActive(),
|
||||
mVideoIO->StatusMessage());
|
||||
}
|
||||
|
||||
bool OpenGLComposite::InitOpenGLState()
|
||||
{
|
||||
if (! ResolveGLExtensions())
|
||||
return false;
|
||||
|
||||
std::string runtimeError;
|
||||
if (mRuntimeHost->GetRepoRoot().empty() && !mRuntimeHost->Initialize(runtimeError))
|
||||
{
|
||||
MessageBoxA(NULL, runtimeError.c_str(), "Runtime host failed to initialize", MB_OK);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!mRuntimeServices->Start(*this, *mRuntimeHost, runtimeError))
|
||||
{
|
||||
MessageBoxA(NULL, runtimeError.c_str(), "Runtime control services failed to start", MB_OK);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Prepare the runtime shader program generated from the active shader package.
|
||||
char compilerErrorMessage[1024];
|
||||
if (!mShaderPrograms->CompileDecodeShader(sizeof(compilerErrorMessage), compilerErrorMessage))
|
||||
{
|
||||
MessageBoxA(NULL, compilerErrorMessage, "OpenGL decode shader failed to load or compile", MB_OK);
|
||||
return false;
|
||||
}
|
||||
if (!mShaderPrograms->CompileOutputPackShader(sizeof(compilerErrorMessage), compilerErrorMessage))
|
||||
{
|
||||
MessageBoxA(NULL, compilerErrorMessage, "OpenGL output pack shader failed to load or compile", MB_OK);
|
||||
return false;
|
||||
}
|
||||
|
||||
std::string rendererError;
|
||||
if (!mRenderer->InitializeResources(
|
||||
mVideoIO->InputFrameWidth(),
|
||||
mVideoIO->InputFrameHeight(),
|
||||
mVideoIO->CaptureTextureWidth(),
|
||||
mVideoIO->OutputFrameWidth(),
|
||||
mVideoIO->OutputFrameHeight(),
|
||||
mVideoIO->OutputPackTextureWidth(),
|
||||
rendererError))
|
||||
{
|
||||
MessageBoxA(NULL, rendererError.c_str(), "OpenGL initialization error.", MB_OK);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!mShaderPrograms->CompileLayerPrograms(mVideoIO->InputFrameWidth(), mVideoIO->InputFrameHeight(), sizeof(compilerErrorMessage), compilerErrorMessage))
|
||||
{
|
||||
MessageBoxA(NULL, compilerErrorMessage, "OpenGL shader failed to load or compile", MB_OK);
|
||||
return false;
|
||||
}
|
||||
mCachedLayerRenderStates = mShaderPrograms->CommittedLayerStates();
|
||||
mUseCommittedLayerStates = false;
|
||||
|
||||
mShaderPrograms->ResetTemporalHistoryState();
|
||||
mShaderPrograms->ResetShaderFeedbackState();
|
||||
|
||||
broadcastRuntimeState();
|
||||
mRuntimeServices->BeginPolling(*mRuntimeHost);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool OpenGLComposite::Start()
|
||||
{
|
||||
return mVideoIO->Start();
|
||||
}
|
||||
|
||||
bool OpenGLComposite::Stop()
|
||||
{
|
||||
if (mRuntimeServices)
|
||||
mRuntimeServices->Stop();
|
||||
|
||||
const bool wasExternalKeyingActive = mVideoIO->ExternalKeyingActive();
|
||||
mVideoIO->Stop();
|
||||
if (wasExternalKeyingActive)
|
||||
PublishVideoIOStatus("External keying has been disabled.");
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool OpenGLComposite::ReloadShader(bool preserveFeedbackState)
|
||||
{
|
||||
mPreserveFeedbackOnNextShaderBuild = preserveFeedbackState;
|
||||
if (mRuntimeHost)
|
||||
{
|
||||
mRuntimeHost->SetCompileStatus(true, "Shader rebuild queued.");
|
||||
mRuntimeHost->ClearReloadRequest();
|
||||
}
|
||||
RequestShaderBuild();
|
||||
broadcastRuntimeState();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool OpenGLComposite::RequestScreenshot(std::string& error)
|
||||
{
|
||||
(void)error;
|
||||
mScreenshotRequested.store(true);
|
||||
return true;
|
||||
}
|
||||
|
||||
void OpenGLComposite::renderEffect()
|
||||
{
|
||||
ProcessRuntimePollResults();
|
||||
std::vector<RuntimeServices::AppliedOscUpdate> appliedOscUpdates;
|
||||
std::vector<RuntimeServices::CompletedOscCommit> completedOscCommits;
|
||||
if (mRuntimeHost && mRuntimeServices)
|
||||
{
|
||||
std::string oscError;
|
||||
if (!mRuntimeServices->ApplyPendingOscUpdates(appliedOscUpdates, oscError) && !oscError.empty())
|
||||
OutputDebugStringA(("OSC apply failed: " + oscError + "\n").c_str());
|
||||
mRuntimeServices->ConsumeCompletedOscCommits(completedOscCommits);
|
||||
}
|
||||
|
||||
for (const RuntimeServices::CompletedOscCommit& completedCommit : completedOscCommits)
|
||||
{
|
||||
auto overlayIt = mOscOverlayStates.find(completedCommit.routeKey);
|
||||
if (overlayIt == mOscOverlayStates.end())
|
||||
continue;
|
||||
|
||||
OscOverlayState& overlay = overlayIt->second;
|
||||
if (overlay.commitQueued &&
|
||||
overlay.pendingCommitGeneration == completedCommit.generation &&
|
||||
overlay.generation == completedCommit.generation)
|
||||
{
|
||||
mOscOverlayStates.erase(overlayIt);
|
||||
}
|
||||
}
|
||||
|
||||
std::set<std::string> pendingOscRouteKeys;
|
||||
const auto oscNow = std::chrono::steady_clock::now();
|
||||
for (const RuntimeServices::AppliedOscUpdate& update : appliedOscUpdates)
|
||||
{
|
||||
const std::string routeKey = update.routeKey;
|
||||
auto overlayIt = mOscOverlayStates.find(routeKey);
|
||||
if (overlayIt == mOscOverlayStates.end())
|
||||
{
|
||||
OscOverlayState overlay;
|
||||
overlay.layerKey = update.layerKey;
|
||||
overlay.parameterKey = update.parameterKey;
|
||||
overlay.targetValue = update.targetValue;
|
||||
overlay.lastUpdatedTime = oscNow;
|
||||
overlay.lastAppliedTime = oscNow;
|
||||
overlay.generation = 1;
|
||||
mOscOverlayStates[routeKey] = std::move(overlay);
|
||||
}
|
||||
else
|
||||
{
|
||||
overlayIt->second.targetValue = update.targetValue;
|
||||
overlayIt->second.lastUpdatedTime = oscNow;
|
||||
overlayIt->second.generation += 1;
|
||||
overlayIt->second.commitQueued = false;
|
||||
}
|
||||
pendingOscRouteKeys.insert(routeKey);
|
||||
}
|
||||
|
||||
const auto applyOscOverlays = [&](std::vector<RuntimeRenderState>& states, bool allowCommit)
|
||||
{
|
||||
if (states.empty() || mOscOverlayStates.empty() || !mRuntimeHost)
|
||||
return;
|
||||
|
||||
const double smoothing = ClampOscAlpha(mRuntimeHost->GetOscSmoothing());
|
||||
std::vector<std::string> overlayKeysToRemove;
|
||||
for (auto& item : mOscOverlayStates)
|
||||
{
|
||||
OscOverlayState& overlay = item.second;
|
||||
auto stateIt = std::find_if(states.begin(), states.end(),
|
||||
[&overlay](const RuntimeRenderState& state)
|
||||
{
|
||||
return MatchesOscControlKey(state.layerId, overlay.layerKey) ||
|
||||
MatchesOscControlKey(state.shaderId, overlay.layerKey) ||
|
||||
MatchesOscControlKey(state.shaderName, overlay.layerKey);
|
||||
});
|
||||
if (stateIt == states.end())
|
||||
continue;
|
||||
|
||||
auto definitionIt = std::find_if(stateIt->parameterDefinitions.begin(), stateIt->parameterDefinitions.end(),
|
||||
[&overlay](const ShaderParameterDefinition& definition)
|
||||
{
|
||||
return MatchesOscControlKey(definition.id, overlay.parameterKey) ||
|
||||
MatchesOscControlKey(definition.label, overlay.parameterKey);
|
||||
});
|
||||
if (definitionIt == stateIt->parameterDefinitions.end())
|
||||
continue;
|
||||
|
||||
if (definitionIt->type == ShaderParameterType::Trigger)
|
||||
{
|
||||
if (pendingOscRouteKeys.find(item.first) == pendingOscRouteKeys.end())
|
||||
continue;
|
||||
|
||||
ShaderParameterValue& value = stateIt->parameterValues[definitionIt->id];
|
||||
const double previousCount = value.numberValues.empty() ? 0.0 : value.numberValues[0];
|
||||
const double triggerTime = stateIt->timeSeconds;
|
||||
value.numberValues = { previousCount + 1.0, triggerTime };
|
||||
overlayKeysToRemove.push_back(item.first);
|
||||
continue;
|
||||
}
|
||||
|
||||
ShaderParameterValue targetValue;
|
||||
std::string normalizeError;
|
||||
if (!NormalizeAndValidateParameterValue(*definitionIt, overlay.targetValue, targetValue, normalizeError))
|
||||
continue;
|
||||
|
||||
const bool smoothable =
|
||||
smoothing > 0.0 &&
|
||||
(definitionIt->type == ShaderParameterType::Float ||
|
||||
definitionIt->type == ShaderParameterType::Vec2 ||
|
||||
definitionIt->type == ShaderParameterType::Color);
|
||||
if (!smoothable)
|
||||
{
|
||||
overlay.currentValue = targetValue;
|
||||
overlay.hasCurrentValue = true;
|
||||
stateIt->parameterValues[definitionIt->id] = overlay.currentValue;
|
||||
if (allowCommit &&
|
||||
!overlay.commitQueued &&
|
||||
oscNow - overlay.lastUpdatedTime >= kOscOverlayCommitDelay &&
|
||||
mRuntimeServices)
|
||||
{
|
||||
std::string commitError;
|
||||
if (mRuntimeServices->QueueOscCommit(item.first, overlay.layerKey, overlay.parameterKey, overlay.targetValue, overlay.generation, commitError))
|
||||
{
|
||||
overlay.pendingCommitGeneration = overlay.generation;
|
||||
overlay.commitQueued = true;
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!overlay.hasCurrentValue)
|
||||
{
|
||||
overlay.currentValue = DefaultValueForDefinition(*definitionIt);
|
||||
auto currentIt = stateIt->parameterValues.find(definitionIt->id);
|
||||
if (currentIt != stateIt->parameterValues.end())
|
||||
overlay.currentValue = currentIt->second;
|
||||
overlay.hasCurrentValue = true;
|
||||
}
|
||||
|
||||
if (overlay.currentValue.numberValues.size() != targetValue.numberValues.size())
|
||||
overlay.currentValue.numberValues = targetValue.numberValues;
|
||||
|
||||
double smoothingAlpha = smoothing;
|
||||
if (overlay.lastAppliedTime != std::chrono::steady_clock::time_point())
|
||||
{
|
||||
const double deltaSeconds =
|
||||
std::chrono::duration_cast<std::chrono::duration<double>>(oscNow - overlay.lastAppliedTime).count();
|
||||
smoothingAlpha = ComputeTimeBasedOscAlpha(smoothing, deltaSeconds);
|
||||
}
|
||||
overlay.lastAppliedTime = oscNow;
|
||||
|
||||
ShaderParameterValue nextValue = targetValue;
|
||||
bool converged = true;
|
||||
for (std::size_t index = 0; index < targetValue.numberValues.size(); ++index)
|
||||
{
|
||||
const double currentNumber = overlay.currentValue.numberValues[index];
|
||||
const double targetNumber = targetValue.numberValues[index];
|
||||
const double delta = targetNumber - currentNumber;
|
||||
double nextNumber = currentNumber + delta * smoothingAlpha;
|
||||
if (std::fabs(delta) <= 0.0005)
|
||||
nextNumber = targetNumber;
|
||||
else
|
||||
converged = false;
|
||||
nextValue.numberValues[index] = nextNumber;
|
||||
}
|
||||
|
||||
if (converged)
|
||||
nextValue.numberValues = targetValue.numberValues;
|
||||
|
||||
overlay.currentValue = nextValue;
|
||||
overlay.hasCurrentValue = true;
|
||||
stateIt->parameterValues[definitionIt->id] = overlay.currentValue;
|
||||
if (allowCommit &&
|
||||
converged &&
|
||||
!overlay.commitQueued &&
|
||||
oscNow - overlay.lastUpdatedTime >= kOscOverlayCommitDelay &&
|
||||
mRuntimeServices)
|
||||
{
|
||||
std::string commitError;
|
||||
JsonValue committedValue = BuildOscCommitValue(*definitionIt, overlay.currentValue);
|
||||
if (mRuntimeServices->QueueOscCommit(item.first, overlay.layerKey, overlay.parameterKey, committedValue, overlay.generation, commitError))
|
||||
{
|
||||
overlay.pendingCommitGeneration = overlay.generation;
|
||||
overlay.commitQueued = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const std::string& overlayKey : overlayKeysToRemove)
|
||||
mOscOverlayStates.erase(overlayKey);
|
||||
};
|
||||
|
||||
const bool hasInputSource = mVideoIO->HasInputSource();
|
||||
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);
|
||||
});
|
||||
}
|
||||
|
||||
void OpenGLComposite::ProcessScreenshotRequest()
|
||||
{
|
||||
if (!mScreenshotRequested.exchange(false))
|
||||
return;
|
||||
|
||||
const unsigned width = mVideoIO ? mVideoIO->OutputFrameWidth() : 0;
|
||||
const unsigned height = mVideoIO ? mVideoIO->OutputFrameHeight() : 0;
|
||||
if (width == 0 || height == 0)
|
||||
return;
|
||||
|
||||
std::vector<unsigned char> bottomUpPixels(static_cast<std::size_t>(width) * height * 4);
|
||||
std::vector<unsigned char> topDownPixels(bottomUpPixels.size());
|
||||
|
||||
glBindFramebuffer(GL_READ_FRAMEBUFFER, mRenderer->OutputFramebuffer());
|
||||
glReadBuffer(GL_COLOR_ATTACHMENT0);
|
||||
glPixelStorei(GL_PACK_ALIGNMENT, 1);
|
||||
glPixelStorei(GL_PACK_ROW_LENGTH, 0);
|
||||
glReadPixels(0, 0, width, height, GL_RGBA, GL_UNSIGNED_BYTE, bottomUpPixels.data());
|
||||
glPixelStorei(GL_PACK_ALIGNMENT, 4);
|
||||
|
||||
const std::size_t rowBytes = static_cast<std::size_t>(width) * 4;
|
||||
for (unsigned y = 0; y < height; ++y)
|
||||
{
|
||||
const unsigned sourceY = height - 1 - y;
|
||||
std::copy(
|
||||
bottomUpPixels.begin() + static_cast<std::ptrdiff_t>(sourceY * rowBytes),
|
||||
bottomUpPixels.begin() + static_cast<std::ptrdiff_t>((sourceY + 1) * rowBytes),
|
||||
topDownPixels.begin() + static_cast<std::ptrdiff_t>(y * rowBytes));
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
const std::filesystem::path outputPath = BuildScreenshotPath();
|
||||
std::filesystem::create_directories(outputPath.parent_path());
|
||||
WritePngFileAsync(outputPath, width, height, std::move(topDownPixels));
|
||||
}
|
||||
catch (const std::exception& exception)
|
||||
{
|
||||
OutputDebugStringA((std::string("Screenshot request failed: ") + exception.what() + "\n").c_str());
|
||||
}
|
||||
}
|
||||
|
||||
std::filesystem::path OpenGLComposite::BuildScreenshotPath() const
|
||||
{
|
||||
const std::filesystem::path root = mRuntimeHost && !mRuntimeHost->GetRuntimeRoot().empty()
|
||||
? mRuntimeHost->GetRuntimeRoot()
|
||||
: std::filesystem::current_path();
|
||||
|
||||
const auto now = std::chrono::system_clock::now();
|
||||
const auto milliseconds = std::chrono::duration_cast<std::chrono::milliseconds>(now.time_since_epoch()) % 1000;
|
||||
const std::time_t nowTime = std::chrono::system_clock::to_time_t(now);
|
||||
std::tm localTime = {};
|
||||
localtime_s(&localTime, &nowTime);
|
||||
|
||||
std::ostringstream filename;
|
||||
filename << "video-shader-toys-"
|
||||
<< std::put_time(&localTime, "%Y%m%d-%H%M%S")
|
||||
<< "-" << std::setw(3) << std::setfill('0') << milliseconds.count()
|
||||
<< ".png";
|
||||
|
||||
return root / "screenshots" / filename.str();
|
||||
}
|
||||
|
||||
bool OpenGLComposite::ProcessRuntimePollResults()
|
||||
{
|
||||
if (!mRuntimeHost || !mRuntimeServices)
|
||||
return true;
|
||||
|
||||
const RuntimePollEvents events = mRuntimeServices->ConsumePollEvents();
|
||||
if (events.failed)
|
||||
{
|
||||
mRuntimeHost->SetCompileStatus(false, events.error);
|
||||
broadcastRuntimeState();
|
||||
return false;
|
||||
}
|
||||
|
||||
if (events.registryChanged)
|
||||
broadcastRuntimeState();
|
||||
|
||||
if (!events.reloadRequested)
|
||||
{
|
||||
PreparedShaderBuild readyBuild;
|
||||
if (!mShaderBuildQueue || !mShaderBuildQueue->TryConsumeReadyBuild(readyBuild))
|
||||
return true;
|
||||
|
||||
char compilerErrorMessage[1024] = {};
|
||||
if (!mShaderPrograms->CommitPreparedLayerPrograms(readyBuild, mVideoIO->InputFrameWidth(), mVideoIO->InputFrameHeight(), sizeof(compilerErrorMessage), compilerErrorMessage))
|
||||
{
|
||||
mRuntimeHost->SetCompileStatus(false, compilerErrorMessage);
|
||||
mUseCommittedLayerStates = true;
|
||||
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;
|
||||
}
|
||||
|
||||
////////////////////////////////////////////
|
||||
|
||||
|
||||
@@ -1,125 +0,0 @@
|
||||
#ifndef __OPENGL_COMPOSITE_H__
|
||||
#define __OPENGL_COMPOSITE_H__
|
||||
|
||||
#include <windows.h>
|
||||
#include <process.h>
|
||||
#include <tchar.h>
|
||||
#include <gl/gl.h>
|
||||
#include <gl/glu.h>
|
||||
|
||||
#include <objbase.h>
|
||||
#include <atlbase.h>
|
||||
#include <comutil.h>
|
||||
|
||||
#include "GLExtensions.h"
|
||||
#include "OpenGLRenderer.h"
|
||||
#include "RuntimeHost.h"
|
||||
|
||||
#include <functional>
|
||||
#include <atomic>
|
||||
#include <filesystem>
|
||||
#include <map>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <deque>
|
||||
#include <chrono>
|
||||
|
||||
class VideoIODevice;
|
||||
class OpenGLVideoIOBridge;
|
||||
class OpenGLRenderPass;
|
||||
class OpenGLRenderPipeline;
|
||||
class OpenGLShaderPrograms;
|
||||
class RuntimeServices;
|
||||
class ShaderBuildQueue;
|
||||
|
||||
|
||||
class OpenGLComposite
|
||||
{
|
||||
public:
|
||||
OpenGLComposite(HWND hWnd, HDC hDC, HGLRC hRC);
|
||||
~OpenGLComposite();
|
||||
|
||||
bool InitDeckLink();
|
||||
bool InitVideoIO();
|
||||
bool Start();
|
||||
bool Stop();
|
||||
bool ReloadShader(bool preserveFeedbackState = false);
|
||||
std::string GetRuntimeStateJson() const;
|
||||
bool AddLayer(const std::string& shaderId, std::string& error);
|
||||
bool RemoveLayer(const std::string& layerId, std::string& error);
|
||||
bool MoveLayer(const std::string& layerId, int direction, std::string& error);
|
||||
bool MoveLayerToIndex(const std::string& layerId, std::size_t targetIndex, std::string& error);
|
||||
bool SetLayerBypass(const std::string& layerId, bool bypassed, std::string& error);
|
||||
bool SetLayerShader(const std::string& layerId, const std::string& shaderId, std::string& error);
|
||||
bool UpdateLayerParameterJson(const std::string& layerId, const std::string& parameterId, const std::string& valueJson, std::string& error);
|
||||
bool UpdateLayerParameterByControlKeyJson(const std::string& layerKey, const std::string& parameterKey, const std::string& valueJson, std::string& error);
|
||||
bool ResetLayerParameters(const std::string& layerId, std::string& error);
|
||||
bool SaveStackPreset(const std::string& presetName, std::string& error);
|
||||
bool LoadStackPreset(const std::string& presetName, std::string& error);
|
||||
bool RequestScreenshot(std::string& error);
|
||||
unsigned short GetControlServerPort() const;
|
||||
unsigned short GetOscPort() const;
|
||||
std::string GetOscBindAddress() const;
|
||||
std::string GetControlUrl() const;
|
||||
std::string GetDocsUrl() const;
|
||||
std::string GetOscAddress() const;
|
||||
|
||||
void resizeGL(WORD width, WORD height);
|
||||
void paintGL(bool force = false);
|
||||
|
||||
private:
|
||||
void resizeWindow(int width, int height);
|
||||
bool CheckOpenGLExtensions();
|
||||
void PublishVideoIOStatus(const std::string& statusMessage);
|
||||
using LayerProgram = OpenGLRenderer::LayerProgram;
|
||||
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<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;
|
||||
|
||||
bool InitOpenGLState();
|
||||
void renderEffect();
|
||||
bool ProcessRuntimePollResults();
|
||||
void RequestShaderBuild();
|
||||
void ProcessScreenshotRequest();
|
||||
std::filesystem::path BuildScreenshotPath() const;
|
||||
void broadcastRuntimeState();
|
||||
void resetTemporalHistoryState();
|
||||
};
|
||||
|
||||
#endif // __OPENGL_COMPOSITE_H__
|
||||
@@ -1,156 +0,0 @@
|
||||
#include "OpenGLComposite.h"
|
||||
#include "RuntimeServices.h"
|
||||
|
||||
std::string OpenGLComposite::GetRuntimeStateJson() const
|
||||
{
|
||||
return mRuntimeHost ? mRuntimeHost->BuildStateJson() : "{}";
|
||||
}
|
||||
|
||||
unsigned short OpenGLComposite::GetControlServerPort() const
|
||||
{
|
||||
return mRuntimeHost ? mRuntimeHost->GetServerPort() : 0;
|
||||
}
|
||||
|
||||
unsigned short OpenGLComposite::GetOscPort() const
|
||||
{
|
||||
return mRuntimeHost ? mRuntimeHost->GetOscPort() : 0;
|
||||
}
|
||||
|
||||
std::string OpenGLComposite::GetOscBindAddress() const
|
||||
{
|
||||
return mRuntimeHost ? mRuntimeHost->GetOscBindAddress() : "127.0.0.1";
|
||||
}
|
||||
|
||||
std::string OpenGLComposite::GetControlUrl() const
|
||||
{
|
||||
return "http://127.0.0.1:" + std::to_string(GetControlServerPort()) + "/";
|
||||
}
|
||||
|
||||
std::string OpenGLComposite::GetDocsUrl() const
|
||||
{
|
||||
return "http://127.0.0.1:" + std::to_string(GetControlServerPort()) + "/docs";
|
||||
}
|
||||
|
||||
std::string OpenGLComposite::GetOscAddress() const
|
||||
{
|
||||
return "udp://" + GetOscBindAddress() + ":" + std::to_string(GetOscPort()) + " /VideoShaderToys/{Layer}/{Parameter}";
|
||||
}
|
||||
|
||||
bool OpenGLComposite::AddLayer(const std::string& shaderId, std::string& error)
|
||||
{
|
||||
if (!mRuntimeHost->AddLayer(shaderId, error))
|
||||
return false;
|
||||
|
||||
ReloadShader(true);
|
||||
broadcastRuntimeState();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool OpenGLComposite::RemoveLayer(const std::string& layerId, std::string& error)
|
||||
{
|
||||
if (!mRuntimeHost->RemoveLayer(layerId, error))
|
||||
return false;
|
||||
|
||||
ReloadShader(true);
|
||||
broadcastRuntimeState();
|
||||
return true;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
bool OpenGLComposite::SetLayerBypass(const std::string& layerId, bool bypassed, std::string& error)
|
||||
{
|
||||
if (!mRuntimeHost->SetLayerBypass(layerId, bypassed, error))
|
||||
return false;
|
||||
|
||||
ReloadShader();
|
||||
broadcastRuntimeState();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool OpenGLComposite::SetLayerShader(const std::string& layerId, const std::string& shaderId, std::string& error)
|
||||
{
|
||||
if (!mRuntimeHost->SetLayerShader(layerId, shaderId, error))
|
||||
return false;
|
||||
|
||||
ReloadShader();
|
||||
broadcastRuntimeState();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool OpenGLComposite::UpdateLayerParameterJson(const std::string& layerId, const std::string& parameterId, const std::string& valueJson, std::string& error)
|
||||
{
|
||||
JsonValue parsedValue;
|
||||
if (!ParseJson(valueJson, parsedValue, error))
|
||||
return false;
|
||||
|
||||
if (!mRuntimeHost->UpdateLayerParameter(layerId, parameterId, parsedValue, error))
|
||||
return false;
|
||||
|
||||
broadcastRuntimeState();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool OpenGLComposite::UpdateLayerParameterByControlKeyJson(const std::string& layerKey, const std::string& parameterKey, const std::string& valueJson, std::string& error)
|
||||
{
|
||||
JsonValue parsedValue;
|
||||
if (!ParseJson(valueJson, parsedValue, error))
|
||||
return false;
|
||||
|
||||
if (!mRuntimeHost->UpdateLayerParameterByControlKey(layerKey, parameterKey, parsedValue, error))
|
||||
return false;
|
||||
|
||||
broadcastRuntimeState();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool OpenGLComposite::ResetLayerParameters(const std::string& layerId, std::string& error)
|
||||
{
|
||||
if (!mRuntimeHost->ResetLayerParameters(layerId, error))
|
||||
return false;
|
||||
|
||||
mOscOverlayStates.clear();
|
||||
if (mRuntimeServices)
|
||||
mRuntimeServices->ClearOscState();
|
||||
resetTemporalHistoryState();
|
||||
|
||||
broadcastRuntimeState();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool OpenGLComposite::SaveStackPreset(const std::string& presetName, std::string& error)
|
||||
{
|
||||
if (!mRuntimeHost->SaveStackPreset(presetName, error))
|
||||
return false;
|
||||
|
||||
broadcastRuntimeState();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool OpenGLComposite::LoadStackPreset(const std::string& presetName, std::string& error)
|
||||
{
|
||||
if (!mRuntimeHost->LoadStackPreset(presetName, error))
|
||||
return false;
|
||||
|
||||
ReloadShader();
|
||||
broadcastRuntimeState();
|
||||
return true;
|
||||
}
|
||||
@@ -1,288 +0,0 @@
|
||||
#include "OpenGLRenderPass.h"
|
||||
|
||||
#include "GlRenderConstants.h"
|
||||
|
||||
#include <map>
|
||||
|
||||
OpenGLRenderPass::OpenGLRenderPass(OpenGLRenderer& renderer) :
|
||||
mRenderer(renderer)
|
||||
{
|
||||
}
|
||||
|
||||
void OpenGLRenderPass::Render(
|
||||
bool hasInputSource,
|
||||
const std::vector<RuntimeRenderState>& layerStates,
|
||||
unsigned inputFrameWidth,
|
||||
unsigned inputFrameHeight,
|
||||
unsigned captureTextureWidth,
|
||||
VideoIOPixelFormat inputPixelFormat,
|
||||
unsigned historyCap,
|
||||
const TextBindingUpdater& updateTextBinding,
|
||||
const GlobalParamsUpdater& updateGlobalParams)
|
||||
{
|
||||
glDisable(GL_SCISSOR_TEST);
|
||||
glDisable(GL_BLEND);
|
||||
glDisable(GL_DEPTH_TEST);
|
||||
if (hasInputSource)
|
||||
{
|
||||
RenderDecodePass(inputFrameWidth, inputFrameHeight, captureTextureWidth, inputPixelFormat);
|
||||
}
|
||||
else
|
||||
{
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, mRenderer.DecodeFramebuffer());
|
||||
glViewport(0, 0, inputFrameWidth, inputFrameHeight);
|
||||
glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
|
||||
glClear(GL_COLOR_BUFFER_BIT);
|
||||
}
|
||||
|
||||
std::vector<LayerProgram>& layerPrograms = mRenderer.LayerPrograms();
|
||||
if (layerStates.empty() || layerPrograms.empty())
|
||||
{
|
||||
glBindFramebuffer(GL_READ_FRAMEBUFFER, mRenderer.DecodeFramebuffer());
|
||||
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, mRenderer.CompositeFramebuffer());
|
||||
glBlitFramebuffer(0, 0, inputFrameWidth, inputFrameHeight, 0, 0, inputFrameWidth, inputFrameHeight, GL_COLOR_BUFFER_BIT, GL_LINEAR);
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, mRenderer.CompositeFramebuffer());
|
||||
}
|
||||
else
|
||||
{
|
||||
const std::vector<RenderPassDescriptor>& passes = BuildLayerPassDescriptors(layerStates, layerPrograms);
|
||||
for (const RenderPassDescriptor& pass : passes)
|
||||
{
|
||||
RenderLayerPass(
|
||||
pass,
|
||||
inputFrameWidth,
|
||||
inputFrameHeight,
|
||||
historyCap,
|
||||
updateTextBinding,
|
||||
updateGlobalParams);
|
||||
}
|
||||
}
|
||||
|
||||
mRenderer.TemporalHistory().PushSourceFramebuffer(mRenderer.DecodeFramebuffer(), inputFrameWidth, inputFrameHeight);
|
||||
mRenderer.FeedbackBuffers().FinalizeFrame();
|
||||
}
|
||||
|
||||
void OpenGLRenderPass::RenderDecodePass(unsigned inputFrameWidth, unsigned inputFrameHeight, unsigned captureTextureWidth, VideoIOPixelFormat inputPixelFormat)
|
||||
{
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, mRenderer.DecodeFramebuffer());
|
||||
glViewport(0, 0, inputFrameWidth, inputFrameHeight);
|
||||
glClear(GL_COLOR_BUFFER_BIT);
|
||||
glActiveTexture(GL_TEXTURE0 + kPackedVideoTextureUnit);
|
||||
glBindTexture(GL_TEXTURE_2D, mRenderer.CaptureTexture());
|
||||
glBindVertexArray(mRenderer.FullscreenVertexArray());
|
||||
glUseProgram(mRenderer.DecodeProgram());
|
||||
|
||||
const GLint packedResolutionLocation = mRenderer.DecodePackedResolutionLocation();
|
||||
const GLint decodedResolutionLocation = mRenderer.DecodeDecodedResolutionLocation();
|
||||
const GLint inputPixelFormatLocation = mRenderer.DecodeInputPixelFormatLocation();
|
||||
if (packedResolutionLocation >= 0)
|
||||
glUniform2f(packedResolutionLocation, static_cast<float>(captureTextureWidth), static_cast<float>(inputFrameHeight));
|
||||
if (decodedResolutionLocation >= 0)
|
||||
glUniform2f(decodedResolutionLocation, static_cast<float>(inputFrameWidth), static_cast<float>(inputFrameHeight));
|
||||
if (inputPixelFormatLocation >= 0)
|
||||
glUniform1i(inputPixelFormatLocation, inputPixelFormat == VideoIOPixelFormat::V210 ? 1 : 0);
|
||||
|
||||
glDrawArrays(GL_TRIANGLES, 0, 3);
|
||||
|
||||
glUseProgram(0);
|
||||
glBindVertexArray(0);
|
||||
glBindTexture(GL_TEXTURE_2D, 0);
|
||||
glActiveTexture(GL_TEXTURE0);
|
||||
}
|
||||
|
||||
std::vector<RenderPassDescriptor> OpenGLRenderPass::BuildLayerPassDescriptors(
|
||||
const std::vector<RuntimeRenderState>& layerStates,
|
||||
std::vector<LayerProgram>& layerPrograms) const
|
||||
{
|
||||
// Flatten the layer stack into concrete GL passes. A layer may now contain
|
||||
// several shader passes, but the outer stack still sees one visible output
|
||||
// per layer.
|
||||
std::vector<RenderPassDescriptor>& passes = mPassScratch;
|
||||
passes.clear();
|
||||
const std::size_t passCount = layerStates.size() < layerPrograms.size() ? layerStates.size() : layerPrograms.size();
|
||||
std::size_t descriptorCount = 0;
|
||||
for (std::size_t index = 0; index < passCount; ++index)
|
||||
descriptorCount += layerPrograms[index].passes.size();
|
||||
passes.reserve(descriptorCount);
|
||||
|
||||
GLuint sourceTexture = mRenderer.DecodedTexture();
|
||||
GLuint sourceFramebuffer = mRenderer.DecodeFramebuffer();
|
||||
for (std::size_t index = 0; index < passCount; ++index)
|
||||
{
|
||||
const RuntimeRenderState& state = layerStates[index];
|
||||
LayerProgram& layerProgram = layerPrograms[index];
|
||||
if (layerProgram.passes.empty())
|
||||
continue;
|
||||
|
||||
// Preserve the original two-target layer ping-pong. Intermediate passes
|
||||
// inside this layer are routed through pooled temporary targets instead.
|
||||
const std::size_t remaining = layerStates.size() - index;
|
||||
const bool writeToMain = (remaining % 2) == 1;
|
||||
const GLuint layerOutputTexture = writeToMain ? mRenderer.CompositeTexture() : mRenderer.LayerTempTexture();
|
||||
const GLuint layerOutputFramebuffer = writeToMain ? mRenderer.CompositeFramebuffer() : mRenderer.LayerTempFramebuffer();
|
||||
const RenderPassOutputTarget layerOutputTarget = writeToMain ? RenderPassOutputTarget::Composite : RenderPassOutputTarget::LayerTemp;
|
||||
|
||||
const GLuint layerInputTexture = sourceTexture;
|
||||
const GLuint layerInputFramebuffer = sourceFramebuffer;
|
||||
GLuint previousPassTexture = layerInputTexture;
|
||||
GLuint previousPassFramebuffer = layerInputFramebuffer;
|
||||
std::map<std::string, std::pair<GLuint, GLuint>> namedOutputs;
|
||||
std::size_t temporaryTargetIndex = 0;
|
||||
|
||||
for (std::size_t passIndex = 0; passIndex < layerProgram.passes.size(); ++passIndex)
|
||||
{
|
||||
PassProgram& passProgram = layerProgram.passes[passIndex];
|
||||
const bool lastPassForLayer = passIndex + 1 == layerProgram.passes.size();
|
||||
const std::string outputName = passProgram.outputName.empty() ? passProgram.passId : passProgram.outputName;
|
||||
const bool writesLayerOutput = outputName == "layerOutput" || lastPassForLayer;
|
||||
|
||||
GLuint passSourceTexture = previousPassTexture;
|
||||
GLuint passSourceFramebuffer = previousPassFramebuffer;
|
||||
if (!passProgram.inputNames.empty())
|
||||
{
|
||||
// v1 multipass uses the first declared input as gVideoInput.
|
||||
// Later inputs are parsed for forward compatibility.
|
||||
const std::string& inputName = passProgram.inputNames.front();
|
||||
if (inputName == "layerInput")
|
||||
{
|
||||
passSourceTexture = layerInputTexture;
|
||||
passSourceFramebuffer = layerInputFramebuffer;
|
||||
}
|
||||
else if (inputName == "previousPass")
|
||||
{
|
||||
passSourceTexture = previousPassTexture;
|
||||
passSourceFramebuffer = previousPassFramebuffer;
|
||||
}
|
||||
else
|
||||
{
|
||||
auto namedOutputIt = namedOutputs.find(inputName);
|
||||
if (namedOutputIt != namedOutputs.end())
|
||||
{
|
||||
passSourceTexture = namedOutputIt->second.first;
|
||||
passSourceFramebuffer = namedOutputIt->second.second;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
GLuint passDestinationTexture = layerOutputTexture;
|
||||
GLuint passDestinationFramebuffer = layerOutputFramebuffer;
|
||||
RenderPassOutputTarget outputTarget = layerOutputTarget;
|
||||
if (!writesLayerOutput)
|
||||
{
|
||||
// Temporary targets are reserved when the shader stack is
|
||||
// committed, avoiding texture allocation during playback.
|
||||
if (temporaryTargetIndex < mRenderer.TemporaryRenderTargetCount())
|
||||
{
|
||||
const RenderTarget& temporaryTarget = mRenderer.TemporaryRenderTarget(temporaryTargetIndex);
|
||||
++temporaryTargetIndex;
|
||||
passDestinationTexture = temporaryTarget.texture;
|
||||
passDestinationFramebuffer = temporaryTarget.framebuffer;
|
||||
outputTarget = RenderPassOutputTarget::Temporary;
|
||||
}
|
||||
}
|
||||
|
||||
RenderPassDescriptor pass;
|
||||
pass.kind = RenderPassKind::LayerEffect;
|
||||
pass.outputTarget = outputTarget;
|
||||
pass.passIndex = passes.size();
|
||||
pass.passId = passProgram.passId;
|
||||
pass.layerId = state.layerId;
|
||||
pass.shaderId = state.shaderId;
|
||||
pass.layerInputTexture = layerInputTexture;
|
||||
pass.sourceTexture = passSourceTexture;
|
||||
pass.sourceFramebuffer = passIndex == 0 ? layerInputFramebuffer : passSourceFramebuffer;
|
||||
pass.destinationTexture = passDestinationTexture;
|
||||
pass.destinationFramebuffer = passDestinationFramebuffer;
|
||||
pass.layerProgram = &layerProgram;
|
||||
pass.passProgram = &passProgram;
|
||||
pass.layerState = &state;
|
||||
pass.capturePreLayerHistory = passIndex == 0 && state.temporalHistorySource == TemporalHistorySource::PreLayerInput;
|
||||
pass.captureFeedbackWrite = state.feedback.enabled && passProgram.passId == state.feedback.writePassId;
|
||||
passes.push_back(pass);
|
||||
|
||||
// A later pass can reference either the explicit output name or the
|
||||
// pass id, which keeps small manifests pleasant to write.
|
||||
namedOutputs[outputName] = std::make_pair(passDestinationTexture, passDestinationFramebuffer);
|
||||
namedOutputs[passProgram.passId] = std::make_pair(passDestinationTexture, passDestinationFramebuffer);
|
||||
previousPassTexture = passDestinationTexture;
|
||||
previousPassFramebuffer = passDestinationFramebuffer;
|
||||
}
|
||||
|
||||
sourceTexture = layerOutputTexture;
|
||||
sourceFramebuffer = layerOutputFramebuffer;
|
||||
}
|
||||
|
||||
return passes;
|
||||
}
|
||||
|
||||
void OpenGLRenderPass::RenderLayerPass(
|
||||
const RenderPassDescriptor& pass,
|
||||
unsigned inputFrameWidth,
|
||||
unsigned inputFrameHeight,
|
||||
unsigned historyCap,
|
||||
const TextBindingUpdater& updateTextBinding,
|
||||
const GlobalParamsUpdater& updateGlobalParams)
|
||||
{
|
||||
if (pass.passProgram == nullptr || pass.layerState == nullptr)
|
||||
return;
|
||||
|
||||
RenderShaderProgram(
|
||||
pass.layerInputTexture,
|
||||
pass.sourceTexture,
|
||||
pass.destinationFramebuffer,
|
||||
*pass.passProgram,
|
||||
*pass.layerState,
|
||||
inputFrameWidth,
|
||||
inputFrameHeight,
|
||||
historyCap,
|
||||
updateTextBinding,
|
||||
updateGlobalParams);
|
||||
|
||||
if (pass.capturePreLayerHistory)
|
||||
mRenderer.TemporalHistory().PushPreLayerFramebuffer(pass.layerId, pass.sourceFramebuffer, inputFrameWidth, inputFrameHeight);
|
||||
if (pass.captureFeedbackWrite)
|
||||
mRenderer.FeedbackBuffers().CaptureFeedbackFramebuffer(pass.layerId, pass.destinationFramebuffer, inputFrameWidth, inputFrameHeight);
|
||||
}
|
||||
|
||||
void OpenGLRenderPass::RenderShaderProgram(
|
||||
GLuint layerInputTexture,
|
||||
GLuint sourceTexture,
|
||||
GLuint destinationFrameBuffer,
|
||||
PassProgram& passProgram,
|
||||
const RuntimeRenderState& state,
|
||||
unsigned inputFrameWidth,
|
||||
unsigned inputFrameHeight,
|
||||
unsigned historyCap,
|
||||
const TextBindingUpdater& updateTextBinding,
|
||||
const GlobalParamsUpdater& updateGlobalParams)
|
||||
{
|
||||
for (LayerProgram::TextBinding& textBinding : passProgram.textBindings)
|
||||
{
|
||||
std::string textError;
|
||||
if (!updateTextBinding(state, textBinding, textError))
|
||||
OutputDebugStringA((textError + "\n").c_str());
|
||||
}
|
||||
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, destinationFrameBuffer);
|
||||
glViewport(0, 0, inputFrameWidth, inputFrameHeight);
|
||||
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
|
||||
const std::vector<GLuint> sourceHistoryTextures = mRenderer.TemporalHistory().ResolveSourceHistoryTextures(sourceTexture, state.isTemporal ? historyCap : 0);
|
||||
const std::vector<GLuint> temporalHistoryTextures = mRenderer.TemporalHistory().ResolveTemporalHistoryTextures(state, sourceTexture, state.isTemporal ? historyCap : 0);
|
||||
const GLuint feedbackTexture = mRenderer.FeedbackBuffers().ResolveReadTexture(state);
|
||||
const ShaderTextureBindings::RuntimeTextureBindingPlan texturePlan =
|
||||
mTextureBindings.BuildLayerRuntimeBindingPlan(passProgram, sourceTexture, layerInputTexture, state, feedbackTexture, sourceHistoryTextures, temporalHistoryTextures);
|
||||
mTextureBindings.BindRuntimeTexturePlan(texturePlan);
|
||||
glBindVertexArray(mRenderer.FullscreenVertexArray());
|
||||
glUseProgram(passProgram.program);
|
||||
// The UBO is shared by every pass in a layer; texture routing is what
|
||||
// changes from pass to pass.
|
||||
updateGlobalParams(
|
||||
state,
|
||||
mRenderer.TemporalHistory().SourceAvailableCount(),
|
||||
mRenderer.TemporalHistory().AvailableCountForLayer(state.layerId),
|
||||
mRenderer.FeedbackBuffers().FeedbackAvailable(state));
|
||||
glDrawArrays(GL_TRIANGLES, 0, 3);
|
||||
glUseProgram(0);
|
||||
glBindVertexArray(0);
|
||||
mTextureBindings.UnbindRuntimeTexturePlan(texturePlan);
|
||||
}
|
||||
@@ -1,61 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "OpenGLRenderer.h"
|
||||
#include "RenderPassDescriptor.h"
|
||||
#include "ShaderTextureBindings.h"
|
||||
#include "ShaderTypes.h"
|
||||
#include "VideoIOFormat.h"
|
||||
|
||||
#include <functional>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
class OpenGLRenderPass
|
||||
{
|
||||
public:
|
||||
using LayerProgram = OpenGLRenderer::LayerProgram;
|
||||
using PassProgram = OpenGLRenderer::LayerProgram::PassProgram;
|
||||
using TextBindingUpdater = std::function<bool(const RuntimeRenderState&, LayerProgram::TextBinding&, std::string&)>;
|
||||
using GlobalParamsUpdater = std::function<bool(const RuntimeRenderState&, unsigned, unsigned, bool)>;
|
||||
|
||||
explicit OpenGLRenderPass(OpenGLRenderer& renderer);
|
||||
|
||||
void Render(
|
||||
bool hasInputSource,
|
||||
const std::vector<RuntimeRenderState>& layerStates,
|
||||
unsigned inputFrameWidth,
|
||||
unsigned inputFrameHeight,
|
||||
unsigned captureTextureWidth,
|
||||
VideoIOPixelFormat inputPixelFormat,
|
||||
unsigned historyCap,
|
||||
const TextBindingUpdater& updateTextBinding,
|
||||
const GlobalParamsUpdater& updateGlobalParams);
|
||||
|
||||
private:
|
||||
void RenderDecodePass(unsigned inputFrameWidth, unsigned inputFrameHeight, unsigned captureTextureWidth, VideoIOPixelFormat inputPixelFormat);
|
||||
std::vector<RenderPassDescriptor> BuildLayerPassDescriptors(
|
||||
const std::vector<RuntimeRenderState>& layerStates,
|
||||
std::vector<LayerProgram>& layerPrograms) const;
|
||||
void RenderLayerPass(
|
||||
const RenderPassDescriptor& pass,
|
||||
unsigned inputFrameWidth,
|
||||
unsigned inputFrameHeight,
|
||||
unsigned historyCap,
|
||||
const TextBindingUpdater& updateTextBinding,
|
||||
const GlobalParamsUpdater& updateGlobalParams);
|
||||
void RenderShaderProgram(
|
||||
GLuint layerInputTexture,
|
||||
GLuint sourceTexture,
|
||||
GLuint destinationFrameBuffer,
|
||||
PassProgram& passProgram,
|
||||
const RuntimeRenderState& state,
|
||||
unsigned inputFrameWidth,
|
||||
unsigned inputFrameHeight,
|
||||
unsigned historyCap,
|
||||
const TextBindingUpdater& updateTextBinding,
|
||||
const GlobalParamsUpdater& updateGlobalParams);
|
||||
|
||||
OpenGLRenderer& mRenderer;
|
||||
ShaderTextureBindings mTextureBindings;
|
||||
mutable std::vector<RenderPassDescriptor> mPassScratch;
|
||||
};
|
||||
@@ -1,279 +0,0 @@
|
||||
#include "OpenGLRenderPipeline.h"
|
||||
|
||||
#include "OpenGLRenderer.h"
|
||||
#include "RuntimeHost.h"
|
||||
#include "VideoIOFormat.h"
|
||||
|
||||
#include <cstring>
|
||||
|
||||
#include <chrono>
|
||||
#include <gl/gl.h>
|
||||
|
||||
OpenGLRenderPipeline::OpenGLRenderPipeline(
|
||||
OpenGLRenderer& renderer,
|
||||
RuntimeHost& runtimeHost,
|
||||
RenderEffectCallback renderEffect,
|
||||
OutputReadyCallback outputReady,
|
||||
PaintCallback paint) :
|
||||
mRenderer(renderer),
|
||||
mRuntimeHost(runtimeHost),
|
||||
mRenderEffect(renderEffect),
|
||||
mOutputReady(outputReady),
|
||||
mPaint(paint)
|
||||
{
|
||||
}
|
||||
|
||||
OpenGLRenderPipeline::~OpenGLRenderPipeline()
|
||||
{
|
||||
ResetAsyncReadbackState();
|
||||
}
|
||||
|
||||
bool OpenGLRenderPipeline::RenderFrame(const RenderPipelineFrameContext& context, VideoIOOutputFrame& outputFrame)
|
||||
{
|
||||
const VideoIOState& state = context.videoState;
|
||||
|
||||
const auto renderStartTime = std::chrono::steady_clock::now();
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, mRenderer.CompositeFramebuffer());
|
||||
mRenderEffect();
|
||||
glBindFramebuffer(GL_READ_FRAMEBUFFER, mRenderer.CompositeFramebuffer());
|
||||
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, mRenderer.OutputFramebuffer());
|
||||
glBlitFramebuffer(0, 0, state.inputFrameSize.width, state.inputFrameSize.height, 0, 0, state.outputFrameSize.width, state.outputFrameSize.height, GL_COLOR_BUFFER_BIT, GL_LINEAR);
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, mRenderer.OutputFramebuffer());
|
||||
if (mOutputReady)
|
||||
mOutputReady();
|
||||
if (state.outputPixelFormat == VideoIOPixelFormat::V210 || state.outputPixelFormat == VideoIOPixelFormat::Yuva10)
|
||||
PackOutputFor10Bit(state);
|
||||
glFlush();
|
||||
|
||||
const auto renderEndTime = std::chrono::steady_clock::now();
|
||||
const double renderMilliseconds = std::chrono::duration_cast<std::chrono::duration<double, std::milli>>(renderEndTime - renderStartTime).count();
|
||||
mRuntimeHost.TrySetPerformanceStats(state.frameBudgetMilliseconds, renderMilliseconds);
|
||||
mRuntimeHost.TryAdvanceFrame();
|
||||
|
||||
ReadOutputFrame(state, outputFrame);
|
||||
if (mPaint)
|
||||
mPaint();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void OpenGLRenderPipeline::PackOutputFor10Bit(const VideoIOState& state)
|
||||
{
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, mRenderer.OutputPackFramebuffer());
|
||||
glViewport(0, 0, state.outputPackTextureWidth, state.outputFrameSize.height);
|
||||
glDisable(GL_SCISSOR_TEST);
|
||||
glDisable(GL_BLEND);
|
||||
glDisable(GL_DEPTH_TEST);
|
||||
glActiveTexture(GL_TEXTURE0);
|
||||
glBindTexture(GL_TEXTURE_2D, mRenderer.OutputTexture());
|
||||
glBindVertexArray(mRenderer.FullscreenVertexArray());
|
||||
glUseProgram(mRenderer.OutputPackProgram());
|
||||
|
||||
const GLint outputResolutionLocation = mRenderer.OutputPackResolutionLocation();
|
||||
const GLint activeWordsLocation = mRenderer.OutputPackActiveWordsLocation();
|
||||
const GLint packFormatLocation = mRenderer.OutputPackFormatLocation();
|
||||
if (outputResolutionLocation >= 0)
|
||||
glUniform2f(outputResolutionLocation, static_cast<float>(state.outputFrameSize.width), static_cast<float>(state.outputFrameSize.height));
|
||||
if (activeWordsLocation >= 0)
|
||||
glUniform1f(activeWordsLocation, static_cast<float>(ActiveV210WordsForWidth(state.outputFrameSize.width)));
|
||||
if (packFormatLocation >= 0)
|
||||
glUniform1i(packFormatLocation, state.outputPixelFormat == VideoIOPixelFormat::Yuva10 ? 2 : 1);
|
||||
|
||||
glDrawArrays(GL_TRIANGLES, 0, 3);
|
||||
glUseProgram(0);
|
||||
glBindVertexArray(0);
|
||||
glBindTexture(GL_TEXTURE_2D, 0);
|
||||
}
|
||||
|
||||
bool OpenGLRenderPipeline::EnsureAsyncReadbackBuffers(std::size_t requiredBytes)
|
||||
{
|
||||
if (requiredBytes == 0)
|
||||
return false;
|
||||
|
||||
if (mAsyncReadbackBytes == requiredBytes && mAsyncReadbackSlots[0].pixelPackBuffer != 0)
|
||||
return true;
|
||||
|
||||
ResetAsyncReadbackState();
|
||||
mAsyncReadbackBytes = requiredBytes;
|
||||
for (AsyncReadbackSlot& slot : mAsyncReadbackSlots)
|
||||
{
|
||||
glGenBuffers(1, &slot.pixelPackBuffer);
|
||||
glBindBuffer(GL_PIXEL_PACK_BUFFER, slot.pixelPackBuffer);
|
||||
glBufferData(GL_PIXEL_PACK_BUFFER, static_cast<GLsizeiptr>(requiredBytes), nullptr, GL_STREAM_READ);
|
||||
slot.sizeBytes = requiredBytes;
|
||||
slot.inFlight = false;
|
||||
}
|
||||
glBindBuffer(GL_PIXEL_PACK_BUFFER, 0);
|
||||
mAsyncReadbackWriteIndex = 0;
|
||||
mAsyncReadbackReadIndex = 0;
|
||||
return true;
|
||||
}
|
||||
|
||||
void OpenGLRenderPipeline::ResetAsyncReadbackState()
|
||||
{
|
||||
FlushAsyncReadbackPipeline();
|
||||
for (AsyncReadbackSlot& slot : mAsyncReadbackSlots)
|
||||
slot.sizeBytes = 0;
|
||||
|
||||
if (mAsyncReadbackSlots[0].pixelPackBuffer != 0)
|
||||
{
|
||||
for (AsyncReadbackSlot& slot : mAsyncReadbackSlots)
|
||||
{
|
||||
if (slot.pixelPackBuffer != 0)
|
||||
{
|
||||
glDeleteBuffers(1, &slot.pixelPackBuffer);
|
||||
slot.pixelPackBuffer = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mAsyncReadbackWriteIndex = 0;
|
||||
mAsyncReadbackReadIndex = 0;
|
||||
mAsyncReadbackBytes = 0;
|
||||
}
|
||||
|
||||
void OpenGLRenderPipeline::FlushAsyncReadbackPipeline()
|
||||
{
|
||||
for (AsyncReadbackSlot& slot : mAsyncReadbackSlots)
|
||||
{
|
||||
if (slot.fence != nullptr)
|
||||
{
|
||||
glDeleteSync(slot.fence);
|
||||
slot.fence = nullptr;
|
||||
}
|
||||
slot.inFlight = false;
|
||||
}
|
||||
|
||||
mAsyncReadbackWriteIndex = 0;
|
||||
mAsyncReadbackReadIndex = 0;
|
||||
}
|
||||
|
||||
void OpenGLRenderPipeline::QueueAsyncReadback(const VideoIOState& state)
|
||||
{
|
||||
const bool usePackedOutput = state.outputPixelFormat == VideoIOPixelFormat::V210 || state.outputPixelFormat == VideoIOPixelFormat::Yuva10;
|
||||
const std::size_t requiredBytes = static_cast<std::size_t>(state.outputFrameRowBytes) * state.outputFrameSize.height;
|
||||
const GLenum format = usePackedOutput ? GL_RGBA : GL_BGRA;
|
||||
const GLenum type = usePackedOutput ? GL_UNSIGNED_BYTE : GL_UNSIGNED_INT_8_8_8_8_REV;
|
||||
const GLuint framebuffer = usePackedOutput ? mRenderer.OutputPackFramebuffer() : mRenderer.OutputFramebuffer();
|
||||
const GLsizei readWidth = static_cast<GLsizei>(usePackedOutput ? state.outputPackTextureWidth : state.outputFrameSize.width);
|
||||
const GLsizei readHeight = static_cast<GLsizei>(state.outputFrameSize.height);
|
||||
|
||||
if (requiredBytes == 0)
|
||||
return;
|
||||
|
||||
if (mAsyncReadbackBytes != requiredBytes
|
||||
|| mAsyncReadbackFormat != format
|
||||
|| mAsyncReadbackType != type
|
||||
|| mAsyncReadbackFramebuffer != framebuffer)
|
||||
{
|
||||
mAsyncReadbackFormat = format;
|
||||
mAsyncReadbackType = type;
|
||||
mAsyncReadbackFramebuffer = framebuffer;
|
||||
if (!EnsureAsyncReadbackBuffers(requiredBytes))
|
||||
return;
|
||||
}
|
||||
|
||||
AsyncReadbackSlot& slot = mAsyncReadbackSlots[mAsyncReadbackWriteIndex];
|
||||
if (slot.fence != nullptr)
|
||||
{
|
||||
glDeleteSync(slot.fence);
|
||||
slot.fence = nullptr;
|
||||
}
|
||||
|
||||
glPixelStorei(GL_PACK_ALIGNMENT, 4);
|
||||
glPixelStorei(GL_PACK_ROW_LENGTH, 0);
|
||||
glBindFramebuffer(GL_READ_FRAMEBUFFER, framebuffer);
|
||||
glBindBuffer(GL_PIXEL_PACK_BUFFER, slot.pixelPackBuffer);
|
||||
glBufferData(GL_PIXEL_PACK_BUFFER, static_cast<GLsizeiptr>(requiredBytes), nullptr, GL_STREAM_READ);
|
||||
glReadPixels(0, 0, readWidth, readHeight, format, type, nullptr);
|
||||
slot.fence = glFenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE, 0);
|
||||
slot.inFlight = slot.fence != nullptr;
|
||||
glBindBuffer(GL_PIXEL_PACK_BUFFER, 0);
|
||||
|
||||
mAsyncReadbackWriteIndex = (mAsyncReadbackWriteIndex + 1) % mAsyncReadbackSlots.size();
|
||||
}
|
||||
|
||||
bool OpenGLRenderPipeline::TryConsumeAsyncReadback(VideoIOOutputFrame& outputFrame, GLuint64 timeoutNanoseconds)
|
||||
{
|
||||
if (mAsyncReadbackBytes == 0 || outputFrame.bytes == nullptr)
|
||||
return false;
|
||||
|
||||
AsyncReadbackSlot& slot = mAsyncReadbackSlots[mAsyncReadbackReadIndex];
|
||||
if (!slot.inFlight || slot.fence == nullptr || slot.pixelPackBuffer == 0)
|
||||
return false;
|
||||
|
||||
const GLenum waitFlags = timeoutNanoseconds > 0 ? GL_SYNC_FLUSH_COMMANDS_BIT : 0;
|
||||
const GLenum waitResult = glClientWaitSync(slot.fence, waitFlags, timeoutNanoseconds);
|
||||
if (waitResult != GL_ALREADY_SIGNALED && waitResult != GL_CONDITION_SATISFIED)
|
||||
return false;
|
||||
|
||||
glDeleteSync(slot.fence);
|
||||
slot.fence = nullptr;
|
||||
|
||||
glBindBuffer(GL_PIXEL_PACK_BUFFER, slot.pixelPackBuffer);
|
||||
void* mappedBytes = glMapBuffer(GL_PIXEL_PACK_BUFFER, GL_READ_ONLY);
|
||||
if (mappedBytes == nullptr)
|
||||
{
|
||||
glBindBuffer(GL_PIXEL_PACK_BUFFER, 0);
|
||||
slot.inFlight = false;
|
||||
mAsyncReadbackReadIndex = (mAsyncReadbackReadIndex + 1) % mAsyncReadbackSlots.size();
|
||||
return false;
|
||||
}
|
||||
|
||||
std::memcpy(outputFrame.bytes, mappedBytes, slot.sizeBytes);
|
||||
glUnmapBuffer(GL_PIXEL_PACK_BUFFER);
|
||||
glBindBuffer(GL_PIXEL_PACK_BUFFER, 0);
|
||||
|
||||
slot.inFlight = false;
|
||||
mAsyncReadbackReadIndex = (mAsyncReadbackReadIndex + 1) % mAsyncReadbackSlots.size();
|
||||
CacheOutputFrame(outputFrame);
|
||||
return true;
|
||||
}
|
||||
|
||||
void OpenGLRenderPipeline::CacheOutputFrame(const VideoIOOutputFrame& outputFrame)
|
||||
{
|
||||
if (outputFrame.bytes == nullptr || outputFrame.height == 0 || outputFrame.rowBytes <= 0)
|
||||
return;
|
||||
|
||||
const std::size_t byteCount = static_cast<std::size_t>(outputFrame.rowBytes) * outputFrame.height;
|
||||
mCachedOutputFrame.resize(byteCount);
|
||||
std::memcpy(mCachedOutputFrame.data(), outputFrame.bytes, byteCount);
|
||||
}
|
||||
|
||||
void OpenGLRenderPipeline::ReadOutputFrameSynchronously(const VideoIOState& state, void* destinationBytes)
|
||||
{
|
||||
const bool usePackedOutput = state.outputPixelFormat == VideoIOPixelFormat::V210 || state.outputPixelFormat == VideoIOPixelFormat::Yuva10;
|
||||
|
||||
glPixelStorei(GL_PACK_ALIGNMENT, 4);
|
||||
glPixelStorei(GL_PACK_ROW_LENGTH, 0);
|
||||
if (usePackedOutput)
|
||||
{
|
||||
glBindFramebuffer(GL_READ_FRAMEBUFFER, mRenderer.OutputPackFramebuffer());
|
||||
glReadPixels(0, 0, state.outputPackTextureWidth, state.outputFrameSize.height, GL_RGBA, GL_UNSIGNED_BYTE, destinationBytes);
|
||||
}
|
||||
else
|
||||
{
|
||||
glBindFramebuffer(GL_READ_FRAMEBUFFER, mRenderer.OutputFramebuffer());
|
||||
glReadPixels(0, 0, state.outputFrameSize.width, state.outputFrameSize.height, GL_BGRA, GL_UNSIGNED_INT_8_8_8_8_REV, destinationBytes);
|
||||
}
|
||||
}
|
||||
|
||||
void OpenGLRenderPipeline::ReadOutputFrame(const VideoIOState& state, VideoIOOutputFrame& outputFrame)
|
||||
{
|
||||
if (TryConsumeAsyncReadback(outputFrame, 500000))
|
||||
{
|
||||
QueueAsyncReadback(state);
|
||||
return;
|
||||
}
|
||||
|
||||
// If async readback misses the playout deadline, prefer a fresh synchronous
|
||||
// frame over reusing stale cached output, then restart the async pipeline.
|
||||
if (outputFrame.bytes != nullptr)
|
||||
{
|
||||
ReadOutputFrameSynchronously(state, outputFrame.bytes);
|
||||
CacheOutputFrame(outputFrame);
|
||||
}
|
||||
|
||||
FlushAsyncReadbackPipeline();
|
||||
QueueAsyncReadback(state);
|
||||
}
|
||||
@@ -1,68 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "GLExtensions.h"
|
||||
#include "VideoIOTypes.h"
|
||||
|
||||
#include <array>
|
||||
#include <functional>
|
||||
#include <vector>
|
||||
|
||||
class OpenGLRenderer;
|
||||
class RuntimeHost;
|
||||
|
||||
struct RenderPipelineFrameContext
|
||||
{
|
||||
VideoIOState videoState;
|
||||
VideoIOCompletion completion;
|
||||
};
|
||||
|
||||
class OpenGLRenderPipeline
|
||||
{
|
||||
public:
|
||||
using RenderEffectCallback = std::function<void()>;
|
||||
using OutputReadyCallback = std::function<void()>;
|
||||
using PaintCallback = std::function<void()>;
|
||||
|
||||
OpenGLRenderPipeline(
|
||||
OpenGLRenderer& renderer,
|
||||
RuntimeHost& runtimeHost,
|
||||
RenderEffectCallback renderEffect,
|
||||
OutputReadyCallback outputReady,
|
||||
PaintCallback paint);
|
||||
~OpenGLRenderPipeline();
|
||||
|
||||
bool RenderFrame(const RenderPipelineFrameContext& context, VideoIOOutputFrame& outputFrame);
|
||||
|
||||
private:
|
||||
struct AsyncReadbackSlot
|
||||
{
|
||||
GLuint pixelPackBuffer = 0;
|
||||
GLsync fence = nullptr;
|
||||
std::size_t sizeBytes = 0;
|
||||
bool inFlight = false;
|
||||
};
|
||||
|
||||
bool EnsureAsyncReadbackBuffers(std::size_t requiredBytes);
|
||||
void ResetAsyncReadbackState();
|
||||
void FlushAsyncReadbackPipeline();
|
||||
void QueueAsyncReadback(const VideoIOState& state);
|
||||
bool TryConsumeAsyncReadback(VideoIOOutputFrame& outputFrame, GLuint64 timeoutNanoseconds);
|
||||
void CacheOutputFrame(const VideoIOOutputFrame& outputFrame);
|
||||
void ReadOutputFrameSynchronously(const VideoIOState& state, void* destinationBytes);
|
||||
void PackOutputFor10Bit(const VideoIOState& state);
|
||||
void ReadOutputFrame(const VideoIOState& state, VideoIOOutputFrame& outputFrame);
|
||||
|
||||
OpenGLRenderer& mRenderer;
|
||||
RuntimeHost& mRuntimeHost;
|
||||
RenderEffectCallback mRenderEffect;
|
||||
OutputReadyCallback mOutputReady;
|
||||
PaintCallback mPaint;
|
||||
std::array<AsyncReadbackSlot, 3> mAsyncReadbackSlots;
|
||||
std::size_t mAsyncReadbackWriteIndex = 0;
|
||||
std::size_t mAsyncReadbackReadIndex = 0;
|
||||
std::size_t mAsyncReadbackBytes = 0;
|
||||
GLenum mAsyncReadbackFormat = GL_BGRA;
|
||||
GLenum mAsyncReadbackType = GL_UNSIGNED_INT_8_8_8_8_REV;
|
||||
GLuint mAsyncReadbackFramebuffer = 0;
|
||||
std::vector<unsigned char> mCachedOutputFrame;
|
||||
};
|
||||
@@ -1,124 +0,0 @@
|
||||
#include "OpenGLVideoIOBridge.h"
|
||||
|
||||
#include "OpenGLRenderer.h"
|
||||
#include "RuntimeHost.h"
|
||||
|
||||
#include <chrono>
|
||||
#include <gl/gl.h>
|
||||
|
||||
OpenGLVideoIOBridge::OpenGLVideoIOBridge(
|
||||
VideoIODevice& videoIO,
|
||||
OpenGLRenderer& renderer,
|
||||
OpenGLRenderPipeline& renderPipeline,
|
||||
RuntimeHost& runtimeHost,
|
||||
CRITICAL_SECTION& mutex,
|
||||
HDC hdc,
|
||||
HGLRC hglrc) :
|
||||
mVideoIO(videoIO),
|
||||
mRenderer(renderer),
|
||||
mRenderPipeline(renderPipeline),
|
||||
mRuntimeHost(runtimeHost),
|
||||
mMutex(mutex),
|
||||
mHdc(hdc),
|
||||
mHglrc(hglrc)
|
||||
{
|
||||
}
|
||||
|
||||
void OpenGLVideoIOBridge::RecordFramePacing(VideoIOCompletionResult completionResult)
|
||||
{
|
||||
const auto now = std::chrono::steady_clock::now();
|
||||
if (mLastPlayoutCompletionTime != std::chrono::steady_clock::time_point())
|
||||
{
|
||||
mCompletionIntervalMilliseconds = std::chrono::duration_cast<std::chrono::duration<double, std::milli>>(now - mLastPlayoutCompletionTime).count();
|
||||
if (mSmoothedCompletionIntervalMilliseconds <= 0.0)
|
||||
mSmoothedCompletionIntervalMilliseconds = mCompletionIntervalMilliseconds;
|
||||
else
|
||||
mSmoothedCompletionIntervalMilliseconds = mSmoothedCompletionIntervalMilliseconds * 0.9 + mCompletionIntervalMilliseconds * 0.1;
|
||||
if (mCompletionIntervalMilliseconds > mMaxCompletionIntervalMilliseconds)
|
||||
mMaxCompletionIntervalMilliseconds = mCompletionIntervalMilliseconds;
|
||||
}
|
||||
mLastPlayoutCompletionTime = now;
|
||||
|
||||
if (completionResult == VideoIOCompletionResult::DisplayedLate)
|
||||
++mLateFrameCount;
|
||||
else if (completionResult == VideoIOCompletionResult::Dropped)
|
||||
++mDroppedFrameCount;
|
||||
else if (completionResult == VideoIOCompletionResult::Flushed)
|
||||
++mFlushedFrameCount;
|
||||
|
||||
mRuntimeHost.TrySetFramePacingStats(
|
||||
mCompletionIntervalMilliseconds,
|
||||
mSmoothedCompletionIntervalMilliseconds,
|
||||
mMaxCompletionIntervalMilliseconds,
|
||||
mLateFrameCount,
|
||||
mDroppedFrameCount,
|
||||
mFlushedFrameCount);
|
||||
}
|
||||
|
||||
void OpenGLVideoIOBridge::VideoFrameArrived(const VideoIOFrame& inputFrame)
|
||||
{
|
||||
const VideoIOState& state = mVideoIO.State();
|
||||
mRuntimeHost.TrySetSignalStatus(!inputFrame.hasNoInputSource, state.inputFrameSize.width, state.inputFrameSize.height, state.inputDisplayModeName);
|
||||
|
||||
if (inputFrame.hasNoInputSource || inputFrame.bytes == nullptr)
|
||||
return; // don't transfer texture when there's no input
|
||||
|
||||
const long textureSize = inputFrame.rowBytes * static_cast<long>(inputFrame.height);
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
||||
void OpenGLVideoIOBridge::PlayoutFrameCompleted(const VideoIOCompletion& completion)
|
||||
{
|
||||
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);
|
||||
}
|
||||
@@ -1,44 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "OpenGLRenderPipeline.h"
|
||||
|
||||
#include <windows.h>
|
||||
|
||||
#include <chrono>
|
||||
#include <cstdint>
|
||||
|
||||
class RuntimeHost;
|
||||
|
||||
class OpenGLVideoIOBridge
|
||||
{
|
||||
public:
|
||||
OpenGLVideoIOBridge(
|
||||
VideoIODevice& videoIO,
|
||||
OpenGLRenderer& renderer,
|
||||
OpenGLRenderPipeline& renderPipeline,
|
||||
RuntimeHost& runtimeHost,
|
||||
CRITICAL_SECTION& mutex,
|
||||
HDC hdc,
|
||||
HGLRC hglrc);
|
||||
|
||||
void VideoFrameArrived(const VideoIOFrame& inputFrame);
|
||||
void PlayoutFrameCompleted(const VideoIOCompletion& completion);
|
||||
|
||||
private:
|
||||
void RecordFramePacing(VideoIOCompletionResult completionResult);
|
||||
|
||||
VideoIODevice& mVideoIO;
|
||||
OpenGLRenderer& mRenderer;
|
||||
OpenGLRenderPipeline& mRenderPipeline;
|
||||
RuntimeHost& mRuntimeHost;
|
||||
CRITICAL_SECTION& mMutex;
|
||||
HDC mHdc;
|
||||
HGLRC mHglrc;
|
||||
std::chrono::steady_clock::time_point mLastPlayoutCompletionTime;
|
||||
double mCompletionIntervalMilliseconds = 0.0;
|
||||
double mSmoothedCompletionIntervalMilliseconds = 0.0;
|
||||
double mMaxCompletionIntervalMilliseconds = 0.0;
|
||||
uint64_t mLateFrameCount = 0;
|
||||
uint64_t mDroppedFrameCount = 0;
|
||||
uint64_t mFlushedFrameCount = 0;
|
||||
};
|
||||
@@ -1,137 +0,0 @@
|
||||
#include "PngScreenshotWriter.h"
|
||||
|
||||
#include <windows.h>
|
||||
#include <wincodec.h>
|
||||
#include <atlbase.h>
|
||||
|
||||
#include <sstream>
|
||||
#include <thread>
|
||||
|
||||
namespace
|
||||
{
|
||||
std::string HResultToString(HRESULT hr)
|
||||
{
|
||||
std::ostringstream stream;
|
||||
stream << "HRESULT 0x" << std::hex << static_cast<unsigned long>(hr);
|
||||
return stream.str();
|
||||
}
|
||||
|
||||
bool WritePngFile(
|
||||
const std::filesystem::path& outputPath,
|
||||
unsigned width,
|
||||
unsigned height,
|
||||
const std::vector<unsigned char>& bgraPixels,
|
||||
std::string& error)
|
||||
{
|
||||
if (width == 0 || height == 0 || bgraPixels.size() < static_cast<std::size_t>(width) * height * 4)
|
||||
{
|
||||
error = "Invalid screenshot dimensions or pixel buffer.";
|
||||
return false;
|
||||
}
|
||||
|
||||
HRESULT initializeResult = CoInitializeEx(nullptr, COINIT_MULTITHREADED);
|
||||
const bool shouldUninitialize = SUCCEEDED(initializeResult);
|
||||
if (FAILED(initializeResult) && initializeResult != RPC_E_CHANGED_MODE)
|
||||
{
|
||||
error = "CoInitializeEx failed: " + HResultToString(initializeResult);
|
||||
return false;
|
||||
}
|
||||
|
||||
CComPtr<IWICImagingFactory> factory;
|
||||
HRESULT result = CoCreateInstance(
|
||||
CLSID_WICImagingFactory,
|
||||
nullptr,
|
||||
CLSCTX_INPROC_SERVER,
|
||||
IID_PPV_ARGS(&factory));
|
||||
if (FAILED(result))
|
||||
{
|
||||
error = "Could not create WIC imaging factory: " + HResultToString(result);
|
||||
if (shouldUninitialize)
|
||||
CoUninitialize();
|
||||
return false;
|
||||
}
|
||||
|
||||
CComPtr<IWICStream> stream;
|
||||
result = factory->CreateStream(&stream);
|
||||
if (SUCCEEDED(result))
|
||||
result = stream->InitializeFromFilename(outputPath.wstring().c_str(), GENERIC_WRITE);
|
||||
if (FAILED(result))
|
||||
{
|
||||
error = "Could not open screenshot output file: " + HResultToString(result);
|
||||
if (shouldUninitialize)
|
||||
CoUninitialize();
|
||||
return false;
|
||||
}
|
||||
|
||||
CComPtr<IWICBitmapEncoder> encoder;
|
||||
result = factory->CreateEncoder(GUID_ContainerFormatPng, nullptr, &encoder);
|
||||
if (SUCCEEDED(result))
|
||||
result = encoder->Initialize(stream, WICBitmapEncoderNoCache);
|
||||
if (FAILED(result))
|
||||
{
|
||||
error = "Could not initialize PNG encoder: " + HResultToString(result);
|
||||
if (shouldUninitialize)
|
||||
CoUninitialize();
|
||||
return false;
|
||||
}
|
||||
|
||||
CComPtr<IWICBitmapFrameEncode> frame;
|
||||
CComPtr<IPropertyBag2> propertyBag;
|
||||
result = encoder->CreateNewFrame(&frame, &propertyBag);
|
||||
if (SUCCEEDED(result))
|
||||
result = frame->Initialize(propertyBag);
|
||||
if (SUCCEEDED(result))
|
||||
result = frame->SetSize(width, height);
|
||||
|
||||
WICPixelFormatGUID pixelFormat = GUID_WICPixelFormat32bppBGRA;
|
||||
if (SUCCEEDED(result))
|
||||
result = frame->SetPixelFormat(&pixelFormat);
|
||||
if (SUCCEEDED(result) && pixelFormat != GUID_WICPixelFormat32bppBGRA)
|
||||
{
|
||||
error = "PNG encoder did not accept BGRA pixel format.";
|
||||
result = E_FAIL;
|
||||
}
|
||||
|
||||
const UINT stride = width * 4;
|
||||
const UINT imageSize = stride * height;
|
||||
if (SUCCEEDED(result))
|
||||
result = frame->WritePixels(height, stride, imageSize, const_cast<BYTE*>(bgraPixels.data()));
|
||||
if (SUCCEEDED(result))
|
||||
result = frame->Commit();
|
||||
if (SUCCEEDED(result))
|
||||
result = encoder->Commit();
|
||||
|
||||
if (shouldUninitialize)
|
||||
CoUninitialize();
|
||||
|
||||
if (FAILED(result))
|
||||
{
|
||||
error = "Could not write screenshot PNG: " + HResultToString(result);
|
||||
std::error_code ignored;
|
||||
std::filesystem::remove(outputPath, ignored);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
void WritePngFileAsync(
|
||||
const std::filesystem::path& outputPath,
|
||||
unsigned width,
|
||||
unsigned height,
|
||||
std::vector<unsigned char> rgbaPixels)
|
||||
{
|
||||
std::thread(
|
||||
[outputPath, width, height, pixels = std::move(rgbaPixels)]() mutable
|
||||
{
|
||||
for (std::size_t index = 0; index + 3 < pixels.size(); index += 4)
|
||||
std::swap(pixels[index], pixels[index + 2]);
|
||||
|
||||
std::string error;
|
||||
if (!WritePngFile(outputPath, width, height, pixels, error))
|
||||
OutputDebugStringA(("Screenshot write failed: " + error + "\n").c_str());
|
||||
else
|
||||
OutputDebugStringA(("Screenshot written: " + outputPath.string() + "\n").c_str());
|
||||
}).detach();
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <filesystem>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
void WritePngFileAsync(
|
||||
const std::filesystem::path& outputPath,
|
||||
unsigned width,
|
||||
unsigned height,
|
||||
std::vector<unsigned char> rgbaPixels);
|
||||
|
||||
@@ -1,41 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "OpenGLRenderer.h"
|
||||
#include "ShaderTypes.h"
|
||||
|
||||
#include <gl/gl.h>
|
||||
|
||||
#include <cstddef>
|
||||
#include <string>
|
||||
|
||||
enum class RenderPassKind
|
||||
{
|
||||
LayerEffect
|
||||
};
|
||||
|
||||
enum class RenderPassOutputTarget
|
||||
{
|
||||
Temporary,
|
||||
LayerTemp,
|
||||
Composite
|
||||
};
|
||||
|
||||
struct RenderPassDescriptor
|
||||
{
|
||||
RenderPassKind kind = RenderPassKind::LayerEffect;
|
||||
RenderPassOutputTarget outputTarget = RenderPassOutputTarget::Composite;
|
||||
std::size_t passIndex = 0;
|
||||
std::string passId;
|
||||
std::string layerId;
|
||||
std::string shaderId;
|
||||
GLuint layerInputTexture = 0;
|
||||
GLuint sourceTexture = 0;
|
||||
GLuint sourceFramebuffer = 0;
|
||||
GLuint destinationTexture = 0;
|
||||
GLuint destinationFramebuffer = 0;
|
||||
OpenGLRenderer::LayerProgram* layerProgram = nullptr;
|
||||
OpenGLRenderer::LayerProgram::PassProgram* passProgram = nullptr;
|
||||
const RuntimeRenderState* layerState = nullptr;
|
||||
bool capturePreLayerHistory = false;
|
||||
bool captureFeedbackWrite = false;
|
||||
};
|
||||
@@ -1,202 +0,0 @@
|
||||
#include "ShaderFeedbackBuffers.h"
|
||||
|
||||
#include <set>
|
||||
|
||||
namespace
|
||||
{
|
||||
void ConfigureFeedbackTexture(unsigned frameWidth, unsigned frameHeight)
|
||||
{
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
|
||||
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA16F, frameWidth, frameHeight, 0, GL_RGBA, GL_FLOAT, NULL);
|
||||
}
|
||||
}
|
||||
|
||||
bool ShaderFeedbackBuffers::EnsureResources(const std::vector<RuntimeRenderState>& layerStates, unsigned frameWidth, unsigned frameHeight, std::string& error)
|
||||
{
|
||||
if (!EnsureZeroTexture())
|
||||
{
|
||||
error = "Failed to initialize shader feedback fallback texture.";
|
||||
return false;
|
||||
}
|
||||
|
||||
std::set<std::string> requiredLayerIds;
|
||||
for (const RuntimeRenderState& state : layerStates)
|
||||
{
|
||||
if (!state.feedback.enabled)
|
||||
continue;
|
||||
|
||||
requiredLayerIds.insert(state.layerId);
|
||||
auto surfaceIt = mSurfacesByLayerId.find(state.layerId);
|
||||
if (surfaceIt == mSurfacesByLayerId.end() ||
|
||||
surfaceIt->second.width != frameWidth ||
|
||||
surfaceIt->second.height != frameHeight)
|
||||
{
|
||||
Surface replacement;
|
||||
if (!CreateSurface(replacement, frameWidth, frameHeight, error))
|
||||
return false;
|
||||
mSurfacesByLayerId[state.layerId] = std::move(replacement);
|
||||
}
|
||||
}
|
||||
|
||||
for (auto it = mSurfacesByLayerId.begin(); it != mSurfacesByLayerId.end();)
|
||||
{
|
||||
if (requiredLayerIds.find(it->first) == requiredLayerIds.end())
|
||||
{
|
||||
DestroySurface(it->second);
|
||||
it = mSurfacesByLayerId.erase(it);
|
||||
}
|
||||
else
|
||||
{
|
||||
++it;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void ShaderFeedbackBuffers::DestroyResources()
|
||||
{
|
||||
for (auto& entry : mSurfacesByLayerId)
|
||||
DestroySurface(entry.second);
|
||||
mSurfacesByLayerId.clear();
|
||||
|
||||
if (mZeroTexture != 0)
|
||||
{
|
||||
glDeleteTextures(1, &mZeroTexture);
|
||||
mZeroTexture = 0;
|
||||
}
|
||||
}
|
||||
|
||||
void ShaderFeedbackBuffers::ResetState()
|
||||
{
|
||||
for (auto& entry : mSurfacesByLayerId)
|
||||
ClearSurfaceState(entry.second);
|
||||
}
|
||||
|
||||
GLuint ShaderFeedbackBuffers::ResolveReadTexture(const RuntimeRenderState& state) const
|
||||
{
|
||||
if (!state.feedback.enabled)
|
||||
return mZeroTexture;
|
||||
|
||||
auto surfaceIt = mSurfacesByLayerId.find(state.layerId);
|
||||
if (surfaceIt == mSurfacesByLayerId.end() || !surfaceIt->second.hasData)
|
||||
return mZeroTexture;
|
||||
|
||||
return surfaceIt->second.slots[surfaceIt->second.readIndex].texture != 0
|
||||
? surfaceIt->second.slots[surfaceIt->second.readIndex].texture
|
||||
: mZeroTexture;
|
||||
}
|
||||
|
||||
bool ShaderFeedbackBuffers::FeedbackAvailable(const RuntimeRenderState& state) const
|
||||
{
|
||||
if (!state.feedback.enabled)
|
||||
return false;
|
||||
|
||||
auto surfaceIt = mSurfacesByLayerId.find(state.layerId);
|
||||
return surfaceIt != mSurfacesByLayerId.end() && surfaceIt->second.hasData;
|
||||
}
|
||||
|
||||
void ShaderFeedbackBuffers::CaptureFeedbackFramebuffer(const std::string& layerId, GLuint sourceFramebuffer, unsigned frameWidth, unsigned frameHeight)
|
||||
{
|
||||
auto surfaceIt = mSurfacesByLayerId.find(layerId);
|
||||
if (surfaceIt == mSurfacesByLayerId.end())
|
||||
return;
|
||||
|
||||
Surface& surface = surfaceIt->second;
|
||||
const unsigned writeIndex = 1u - surface.readIndex;
|
||||
glBindFramebuffer(GL_READ_FRAMEBUFFER, sourceFramebuffer);
|
||||
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, surface.slots[writeIndex].framebuffer);
|
||||
glBlitFramebuffer(0, 0, frameWidth, frameHeight, 0, 0, frameWidth, frameHeight, GL_COLOR_BUFFER_BIT, GL_LINEAR);
|
||||
surface.pendingWrite = true;
|
||||
}
|
||||
|
||||
void ShaderFeedbackBuffers::FinalizeFrame()
|
||||
{
|
||||
for (auto& entry : mSurfacesByLayerId)
|
||||
{
|
||||
Surface& surface = entry.second;
|
||||
if (!surface.pendingWrite)
|
||||
continue;
|
||||
|
||||
surface.readIndex = 1u - surface.readIndex;
|
||||
surface.hasData = true;
|
||||
surface.pendingWrite = false;
|
||||
}
|
||||
}
|
||||
|
||||
bool ShaderFeedbackBuffers::EnsureZeroTexture()
|
||||
{
|
||||
if (mZeroTexture != 0)
|
||||
return true;
|
||||
|
||||
glGenTextures(1, &mZeroTexture);
|
||||
glBindTexture(GL_TEXTURE_2D, mZeroTexture);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
|
||||
const float zeroPixel[4] = { 0.0f, 0.0f, 0.0f, 0.0f };
|
||||
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA16F, 1, 1, 0, GL_RGBA, GL_FLOAT, zeroPixel);
|
||||
glBindTexture(GL_TEXTURE_2D, 0);
|
||||
return mZeroTexture != 0;
|
||||
}
|
||||
|
||||
bool ShaderFeedbackBuffers::CreateSurface(Surface& surface, unsigned frameWidth, unsigned frameHeight, std::string& error)
|
||||
{
|
||||
DestroySurface(surface);
|
||||
|
||||
surface.width = frameWidth;
|
||||
surface.height = frameHeight;
|
||||
for (Slot& slot : surface.slots)
|
||||
{
|
||||
glGenTextures(1, &slot.texture);
|
||||
glBindTexture(GL_TEXTURE_2D, slot.texture);
|
||||
ConfigureFeedbackTexture(frameWidth, frameHeight);
|
||||
|
||||
glGenFramebuffers(1, &slot.framebuffer);
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, slot.framebuffer);
|
||||
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, slot.texture, 0);
|
||||
if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE)
|
||||
{
|
||||
error = "Failed to initialize a shader feedback framebuffer.";
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||
glBindTexture(GL_TEXTURE_2D, 0);
|
||||
DestroySurface(surface);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||
glBindTexture(GL_TEXTURE_2D, 0);
|
||||
ClearSurfaceState(surface);
|
||||
return true;
|
||||
}
|
||||
|
||||
void ShaderFeedbackBuffers::DestroySurface(Surface& surface)
|
||||
{
|
||||
for (Slot& slot : surface.slots)
|
||||
{
|
||||
if (slot.framebuffer != 0)
|
||||
glDeleteFramebuffers(1, &slot.framebuffer);
|
||||
if (slot.texture != 0)
|
||||
glDeleteTextures(1, &slot.texture);
|
||||
slot.framebuffer = 0;
|
||||
slot.texture = 0;
|
||||
}
|
||||
|
||||
surface.width = 0;
|
||||
surface.height = 0;
|
||||
surface.readIndex = 0;
|
||||
surface.hasData = false;
|
||||
surface.pendingWrite = false;
|
||||
}
|
||||
|
||||
void ShaderFeedbackBuffers::ClearSurfaceState(Surface& surface)
|
||||
{
|
||||
surface.readIndex = 0;
|
||||
surface.hasData = false;
|
||||
surface.pendingWrite = false;
|
||||
}
|
||||
@@ -1,46 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "GLExtensions.h"
|
||||
#include "ShaderTypes.h"
|
||||
|
||||
#include <map>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
class ShaderFeedbackBuffers
|
||||
{
|
||||
public:
|
||||
struct Slot
|
||||
{
|
||||
GLuint texture = 0;
|
||||
GLuint framebuffer = 0;
|
||||
};
|
||||
|
||||
struct Surface
|
||||
{
|
||||
Slot slots[2];
|
||||
unsigned width = 0;
|
||||
unsigned height = 0;
|
||||
unsigned readIndex = 0;
|
||||
bool hasData = false;
|
||||
bool pendingWrite = false;
|
||||
};
|
||||
|
||||
bool EnsureResources(const std::vector<RuntimeRenderState>& layerStates, unsigned frameWidth, unsigned frameHeight, std::string& error);
|
||||
void DestroyResources();
|
||||
void ResetState();
|
||||
GLuint ResolveReadTexture(const RuntimeRenderState& state) const;
|
||||
bool FeedbackAvailable(const RuntimeRenderState& state) const;
|
||||
void CaptureFeedbackFramebuffer(const std::string& layerId, GLuint sourceFramebuffer, unsigned frameWidth, unsigned frameHeight);
|
||||
void FinalizeFrame();
|
||||
|
||||
private:
|
||||
bool EnsureZeroTexture();
|
||||
bool CreateSurface(Surface& surface, unsigned frameWidth, unsigned frameHeight, std::string& error);
|
||||
void DestroySurface(Surface& surface);
|
||||
void ClearSurfaceState(Surface& surface);
|
||||
|
||||
private:
|
||||
std::map<std::string, Surface> mSurfacesByLayerId;
|
||||
GLuint mZeroTexture = 0;
|
||||
};
|
||||
@@ -1,261 +0,0 @@
|
||||
#include "TemporalHistoryBuffers.h"
|
||||
|
||||
#include "GlRenderConstants.h"
|
||||
#include "ShaderTypes.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <sstream>
|
||||
#include <set>
|
||||
|
||||
bool TemporalHistoryBuffers::ValidateTextureUnitBudget(const std::vector<RuntimeRenderState>& layerStates, unsigned historyCap, std::string& error) const
|
||||
{
|
||||
unsigned requiredUnits = kSourceHistoryTextureUnitBase;
|
||||
for (const RuntimeRenderState& state : layerStates)
|
||||
{
|
||||
unsigned textTextureCount = 0;
|
||||
for (const ShaderParameterDefinition& definition : state.parameterDefinitions)
|
||||
{
|
||||
if (definition.type == ShaderParameterType::Text)
|
||||
++textTextureCount;
|
||||
}
|
||||
const unsigned totalShaderTextures = static_cast<unsigned>(state.textureAssets.size()) + textTextureCount;
|
||||
const unsigned feedbackTextureCount = state.feedback.enabled ? 1u : 0u;
|
||||
const unsigned layerRequiredUnits = kSourceHistoryTextureUnitBase + (state.isTemporal ? historyCap + historyCap : 0u) + feedbackTextureCount + totalShaderTextures;
|
||||
if (layerRequiredUnits > requiredUnits)
|
||||
requiredUnits = layerRequiredUnits;
|
||||
}
|
||||
|
||||
GLint maxTextureUnits = 0;
|
||||
glGetIntegerv(GL_MAX_TEXTURE_IMAGE_UNITS, &maxTextureUnits);
|
||||
const unsigned availableUnits = maxTextureUnits > 0 ? static_cast<unsigned>(maxTextureUnits) : 0u;
|
||||
if (requiredUnits > availableUnits)
|
||||
{
|
||||
std::ostringstream message;
|
||||
message << "The current history and shader texture asset configuration requires " << requiredUnits
|
||||
<< " fragment texture units, but only " << maxTextureUnits << " are available.";
|
||||
error = message.str();
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool TemporalHistoryBuffers::EnsureResources(const std::vector<RuntimeRenderState>& layerStates, unsigned historyCap, unsigned frameWidth, unsigned frameHeight, std::string& error)
|
||||
{
|
||||
const bool sourceHistoryNeeded = std::any_of(layerStates.begin(), layerStates.end(),
|
||||
[](const RuntimeRenderState& state) { return state.isTemporal && state.effectiveTemporalHistoryLength > 0; });
|
||||
const unsigned sourceHistoryLength = sourceHistoryNeeded ? historyCap : 0;
|
||||
|
||||
if (sourceHistoryRing.effectiveLength != sourceHistoryLength)
|
||||
{
|
||||
if (!CreateRing(sourceHistoryRing, sourceHistoryLength, TemporalHistorySource::Source, frameWidth, frameHeight, error))
|
||||
return false;
|
||||
mNeedsReset = true;
|
||||
}
|
||||
|
||||
std::set<std::string> requiredPreLayerIds;
|
||||
for (const RuntimeRenderState& state : layerStates)
|
||||
{
|
||||
if (!state.isTemporal || state.temporalHistorySource != TemporalHistorySource::PreLayerInput)
|
||||
continue;
|
||||
requiredPreLayerIds.insert(state.layerId);
|
||||
auto historyIt = preLayerHistoryByLayerId.find(state.layerId);
|
||||
if (historyIt == preLayerHistoryByLayerId.end() || historyIt->second.effectiveLength != state.effectiveTemporalHistoryLength)
|
||||
{
|
||||
Ring replacement;
|
||||
if (!CreateRing(replacement, state.effectiveTemporalHistoryLength, TemporalHistorySource::PreLayerInput, frameWidth, frameHeight, error))
|
||||
return false;
|
||||
preLayerHistoryByLayerId[state.layerId] = std::move(replacement);
|
||||
mNeedsReset = true;
|
||||
}
|
||||
}
|
||||
|
||||
for (auto it = preLayerHistoryByLayerId.begin(); it != preLayerHistoryByLayerId.end();)
|
||||
{
|
||||
if (requiredPreLayerIds.find(it->first) == requiredPreLayerIds.end())
|
||||
{
|
||||
DestroyRing(it->second);
|
||||
it = preLayerHistoryByLayerId.erase(it);
|
||||
mNeedsReset = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
++it;
|
||||
}
|
||||
}
|
||||
|
||||
if (mNeedsReset)
|
||||
ResetState();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool TemporalHistoryBuffers::CreateRing(Ring& ring, unsigned effectiveLength, TemporalHistorySource historySource, unsigned frameWidth, unsigned frameHeight, std::string& error)
|
||||
{
|
||||
DestroyRing(ring);
|
||||
ring.effectiveLength = effectiveLength;
|
||||
ring.historySource = historySource;
|
||||
if (effectiveLength == 0)
|
||||
return true;
|
||||
|
||||
ring.slots.resize(effectiveLength);
|
||||
for (Slot& slot : ring.slots)
|
||||
{
|
||||
glGenTextures(1, &slot.texture);
|
||||
glBindTexture(GL_TEXTURE_2D, slot.texture);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
|
||||
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA16F, frameWidth, frameHeight, 0, GL_RGBA, GL_FLOAT, NULL);
|
||||
|
||||
glGenFramebuffers(1, &slot.framebuffer);
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, slot.framebuffer);
|
||||
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, slot.texture, 0);
|
||||
const GLenum framebufferStatus = glCheckFramebufferStatus(GL_FRAMEBUFFER);
|
||||
if (framebufferStatus != GL_FRAMEBUFFER_COMPLETE)
|
||||
{
|
||||
error = "Failed to initialize a temporal history framebuffer.";
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||
glBindTexture(GL_TEXTURE_2D, 0);
|
||||
DestroyRing(ring);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||
glBindTexture(GL_TEXTURE_2D, 0);
|
||||
return true;
|
||||
}
|
||||
|
||||
void TemporalHistoryBuffers::DestroyRing(Ring& ring)
|
||||
{
|
||||
for (Slot& slot : ring.slots)
|
||||
{
|
||||
if (slot.framebuffer != 0)
|
||||
glDeleteFramebuffers(1, &slot.framebuffer);
|
||||
if (slot.texture != 0)
|
||||
glDeleteTextures(1, &slot.texture);
|
||||
slot.framebuffer = 0;
|
||||
slot.texture = 0;
|
||||
}
|
||||
ring.slots.clear();
|
||||
ring.nextWriteIndex = 0;
|
||||
ring.filledCount = 0;
|
||||
ring.effectiveLength = 0;
|
||||
ring.historySource = TemporalHistorySource::None;
|
||||
}
|
||||
|
||||
void TemporalHistoryBuffers::DestroyResources()
|
||||
{
|
||||
DestroyRing(sourceHistoryRing);
|
||||
for (auto& historyEntry : preLayerHistoryByLayerId)
|
||||
DestroyRing(historyEntry.second);
|
||||
preLayerHistoryByLayerId.clear();
|
||||
mNeedsReset = true;
|
||||
}
|
||||
|
||||
void TemporalHistoryBuffers::ResetState()
|
||||
{
|
||||
sourceHistoryRing.nextWriteIndex = 0;
|
||||
sourceHistoryRing.filledCount = 0;
|
||||
for (auto& historyEntry : preLayerHistoryByLayerId)
|
||||
{
|
||||
historyEntry.second.nextWriteIndex = 0;
|
||||
historyEntry.second.filledCount = 0;
|
||||
}
|
||||
mNeedsReset = false;
|
||||
}
|
||||
|
||||
void TemporalHistoryBuffers::PushFramebuffer(GLuint sourceFramebuffer, Ring& ring, unsigned frameWidth, unsigned frameHeight)
|
||||
{
|
||||
if (ring.effectiveLength == 0 || ring.slots.empty())
|
||||
return;
|
||||
|
||||
Slot& targetSlot = ring.slots[ring.nextWriteIndex];
|
||||
glBindFramebuffer(GL_READ_FRAMEBUFFER, sourceFramebuffer);
|
||||
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, targetSlot.framebuffer);
|
||||
glBlitFramebuffer(0, 0, frameWidth, frameHeight, 0, 0, frameWidth, frameHeight, GL_COLOR_BUFFER_BIT, GL_LINEAR);
|
||||
ring.nextWriteIndex = (ring.nextWriteIndex + 1) % ring.slots.size();
|
||||
ring.filledCount = std::min<std::size_t>(ring.filledCount + 1, ring.slots.size());
|
||||
}
|
||||
|
||||
void TemporalHistoryBuffers::PushSourceFramebuffer(GLuint sourceFramebuffer, unsigned frameWidth, unsigned frameHeight)
|
||||
{
|
||||
PushFramebuffer(sourceFramebuffer, sourceHistoryRing, frameWidth, frameHeight);
|
||||
}
|
||||
|
||||
void TemporalHistoryBuffers::PushPreLayerFramebuffer(const std::string& layerId, GLuint sourceFramebuffer, unsigned frameWidth, unsigned frameHeight)
|
||||
{
|
||||
auto historyIt = preLayerHistoryByLayerId.find(layerId);
|
||||
if (historyIt != preLayerHistoryByLayerId.end())
|
||||
PushFramebuffer(sourceFramebuffer, historyIt->second, frameWidth, frameHeight);
|
||||
}
|
||||
|
||||
void TemporalHistoryBuffers::BindSamplers(const RuntimeRenderState& state, GLuint currentSourceTexture, unsigned historyCap)
|
||||
{
|
||||
for (unsigned index = 0; index < historyCap; ++index)
|
||||
{
|
||||
glActiveTexture(GL_TEXTURE0 + kSourceHistoryTextureUnitBase + index);
|
||||
glBindTexture(GL_TEXTURE_2D, ResolveTexture(sourceHistoryRing, currentSourceTexture, index));
|
||||
}
|
||||
|
||||
const GLuint temporalBase = kSourceHistoryTextureUnitBase + historyCap;
|
||||
const Ring* temporalRing = nullptr;
|
||||
auto it = preLayerHistoryByLayerId.find(state.layerId);
|
||||
if (it != preLayerHistoryByLayerId.end())
|
||||
temporalRing = &it->second;
|
||||
|
||||
for (unsigned index = 0; index < historyCap; ++index)
|
||||
{
|
||||
glActiveTexture(GL_TEXTURE0 + temporalBase + index);
|
||||
glBindTexture(GL_TEXTURE_2D, temporalRing ? ResolveTexture(*temporalRing, currentSourceTexture, index) : currentSourceTexture);
|
||||
}
|
||||
glActiveTexture(GL_TEXTURE0);
|
||||
}
|
||||
|
||||
std::vector<GLuint> TemporalHistoryBuffers::ResolveSourceHistoryTextures(GLuint fallbackTexture, unsigned historyCap) const
|
||||
{
|
||||
std::vector<GLuint> textures;
|
||||
textures.reserve(historyCap);
|
||||
for (unsigned index = 0; index < historyCap; ++index)
|
||||
textures.push_back(ResolveTexture(sourceHistoryRing, fallbackTexture, index));
|
||||
return textures;
|
||||
}
|
||||
|
||||
std::vector<GLuint> TemporalHistoryBuffers::ResolveTemporalHistoryTextures(const RuntimeRenderState& state, GLuint fallbackTexture, unsigned historyCap) const
|
||||
{
|
||||
const Ring* temporalRing = nullptr;
|
||||
auto it = preLayerHistoryByLayerId.find(state.layerId);
|
||||
if (it != preLayerHistoryByLayerId.end())
|
||||
temporalRing = &it->second;
|
||||
|
||||
std::vector<GLuint> textures;
|
||||
textures.reserve(historyCap);
|
||||
for (unsigned index = 0; index < historyCap; ++index)
|
||||
textures.push_back(temporalRing ? ResolveTexture(*temporalRing, fallbackTexture, index) : fallbackTexture);
|
||||
return textures;
|
||||
}
|
||||
|
||||
GLuint TemporalHistoryBuffers::ResolveTexture(const Ring& ring, GLuint fallbackTexture, std::size_t framesAgo) const
|
||||
{
|
||||
if (ring.filledCount == 0 || ring.slots.empty())
|
||||
return fallbackTexture;
|
||||
|
||||
const std::size_t clampedOffset = std::min<std::size_t>(framesAgo, ring.filledCount - 1);
|
||||
const std::size_t newestIndex = (ring.nextWriteIndex + ring.slots.size() - 1) % ring.slots.size();
|
||||
const std::size_t slotIndex = (newestIndex + ring.slots.size() - clampedOffset) % ring.slots.size();
|
||||
return ring.slots[slotIndex].texture != 0 ? ring.slots[slotIndex].texture : fallbackTexture;
|
||||
}
|
||||
|
||||
unsigned TemporalHistoryBuffers::SourceAvailableCount() const
|
||||
{
|
||||
return static_cast<unsigned>(sourceHistoryRing.filledCount);
|
||||
}
|
||||
|
||||
unsigned TemporalHistoryBuffers::AvailableCountForLayer(const std::string& layerId) const
|
||||
{
|
||||
auto it = preLayerHistoryByLayerId.find(layerId);
|
||||
if (it == preLayerHistoryByLayerId.end())
|
||||
return 0;
|
||||
return static_cast<unsigned>(it->second.filledCount);
|
||||
}
|
||||
@@ -1,53 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "GLExtensions.h"
|
||||
#include "ShaderTypes.h"
|
||||
|
||||
#include <windows.h>
|
||||
|
||||
#include <gl/gl.h>
|
||||
#include <map>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
struct RuntimeRenderState;
|
||||
|
||||
class TemporalHistoryBuffers
|
||||
{
|
||||
public:
|
||||
struct Slot
|
||||
{
|
||||
GLuint texture = 0;
|
||||
GLuint framebuffer = 0;
|
||||
};
|
||||
|
||||
struct Ring
|
||||
{
|
||||
std::vector<Slot> slots;
|
||||
std::size_t nextWriteIndex = 0;
|
||||
std::size_t filledCount = 0;
|
||||
unsigned effectiveLength = 0;
|
||||
TemporalHistorySource historySource = TemporalHistorySource::None;
|
||||
};
|
||||
|
||||
bool ValidateTextureUnitBudget(const std::vector<RuntimeRenderState>& layerStates, unsigned historyCap, std::string& error) const;
|
||||
bool EnsureResources(const std::vector<RuntimeRenderState>& layerStates, unsigned historyCap, unsigned frameWidth, unsigned frameHeight, std::string& error);
|
||||
bool CreateRing(Ring& ring, unsigned effectiveLength, TemporalHistorySource historySource, unsigned frameWidth, unsigned frameHeight, std::string& error);
|
||||
void DestroyRing(Ring& ring);
|
||||
void DestroyResources();
|
||||
void ResetState();
|
||||
void PushFramebuffer(GLuint sourceFramebuffer, Ring& ring, unsigned frameWidth, unsigned frameHeight);
|
||||
void PushSourceFramebuffer(GLuint sourceFramebuffer, unsigned frameWidth, unsigned frameHeight);
|
||||
void PushPreLayerFramebuffer(const std::string& layerId, GLuint sourceFramebuffer, unsigned frameWidth, unsigned frameHeight);
|
||||
void BindSamplers(const RuntimeRenderState& state, GLuint currentSourceTexture, unsigned historyCap);
|
||||
std::vector<GLuint> ResolveSourceHistoryTextures(GLuint fallbackTexture, unsigned historyCap) const;
|
||||
std::vector<GLuint> ResolveTemporalHistoryTextures(const RuntimeRenderState& state, GLuint fallbackTexture, unsigned historyCap) const;
|
||||
GLuint ResolveTexture(const Ring& ring, GLuint fallbackTexture, std::size_t framesAgo) const;
|
||||
unsigned SourceAvailableCount() const;
|
||||
unsigned AvailableCountForLayer(const std::string& layerId) const;
|
||||
|
||||
private:
|
||||
Ring sourceHistoryRing;
|
||||
std::map<std::string, Ring> preLayerHistoryByLayerId;
|
||||
bool mNeedsReset = true;
|
||||
};
|
||||
@@ -1,10 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <gl/gl.h>
|
||||
|
||||
constexpr GLuint kLayerInputTextureUnit = 0;
|
||||
constexpr GLuint kDecodedVideoTextureUnit = 1;
|
||||
constexpr GLuint kSourceHistoryTextureUnitBase = 2;
|
||||
constexpr GLuint kPackedVideoTextureUnit = 2;
|
||||
constexpr GLuint kGlobalParamsBindingPoint = 0;
|
||||
constexpr unsigned kPrerollFrameCount = 12;
|
||||
@@ -1,57 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <gl/gl.h>
|
||||
|
||||
class ScopedGlShader
|
||||
{
|
||||
public:
|
||||
explicit ScopedGlShader(GLuint shader = 0) : mShader(shader) {}
|
||||
~ScopedGlShader() { reset(); }
|
||||
|
||||
ScopedGlShader(const ScopedGlShader&) = delete;
|
||||
ScopedGlShader& operator=(const ScopedGlShader&) = delete;
|
||||
|
||||
GLuint get() const { return mShader; }
|
||||
GLuint release()
|
||||
{
|
||||
GLuint shader = mShader;
|
||||
mShader = 0;
|
||||
return shader;
|
||||
}
|
||||
void reset(GLuint shader = 0)
|
||||
{
|
||||
if (mShader != 0)
|
||||
glDeleteShader(mShader);
|
||||
mShader = shader;
|
||||
}
|
||||
|
||||
private:
|
||||
GLuint mShader;
|
||||
};
|
||||
|
||||
class ScopedGlProgram
|
||||
{
|
||||
public:
|
||||
explicit ScopedGlProgram(GLuint program = 0) : mProgram(program) {}
|
||||
~ScopedGlProgram() { reset(); }
|
||||
|
||||
ScopedGlProgram(const ScopedGlProgram&) = delete;
|
||||
ScopedGlProgram& operator=(const ScopedGlProgram&) = delete;
|
||||
|
||||
GLuint get() const { return mProgram; }
|
||||
GLuint release()
|
||||
{
|
||||
GLuint program = mProgram;
|
||||
mProgram = 0;
|
||||
return program;
|
||||
}
|
||||
void reset(GLuint program = 0)
|
||||
{
|
||||
if (mProgram != 0)
|
||||
glDeleteProgram(mProgram);
|
||||
mProgram = program;
|
||||
}
|
||||
|
||||
private:
|
||||
GLuint mProgram;
|
||||
};
|
||||
@@ -1,268 +0,0 @@
|
||||
#include "OpenGLRenderer.h"
|
||||
|
||||
#include "GlRenderConstants.h"
|
||||
|
||||
namespace
|
||||
{
|
||||
void ConfigureByteFrameTexture(unsigned width, unsigned height)
|
||||
{
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
|
||||
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, NULL);
|
||||
}
|
||||
}
|
||||
|
||||
bool OpenGLRenderer::InitializeResources(unsigned inputFrameWidth, unsigned inputFrameHeight, unsigned captureTextureWidth, unsigned outputFrameWidth, unsigned outputFrameHeight, unsigned outputPackTextureWidth, std::string& error)
|
||||
{
|
||||
glClearColor(0.0f, 0.0f, 0.0f, 0.5f);
|
||||
glDisable(GL_DEPTH_TEST);
|
||||
|
||||
glGenBuffers(1, &mTextureUploadBuffer);
|
||||
|
||||
glGenTextures(1, &mCaptureTexture);
|
||||
glBindTexture(GL_TEXTURE_2D, mCaptureTexture);
|
||||
ConfigureByteFrameTexture(captureTextureWidth, inputFrameHeight);
|
||||
glBindTexture(GL_TEXTURE_2D, 0);
|
||||
|
||||
glGenRenderbuffers(1, &mIdColorBuf);
|
||||
glGenRenderbuffers(1, &mIdDepthBuf);
|
||||
glGenVertexArrays(1, &mFullscreenVAO);
|
||||
glGenBuffers(1, &mGlobalParamsUBO);
|
||||
|
||||
if (!mRenderTargets.Create(RenderTargetId::Decoded, inputFrameWidth, inputFrameHeight, GL_RGBA16F, GL_RGBA, GL_FLOAT, "decode", error))
|
||||
return false;
|
||||
if (!mRenderTargets.Create(RenderTargetId::LayerTemp, inputFrameWidth, inputFrameHeight, GL_RGBA16F, GL_RGBA, GL_FLOAT, "layer", error))
|
||||
return false;
|
||||
if (!mRenderTargets.Create(RenderTargetId::Composite, inputFrameWidth, inputFrameHeight, GL_RGBA16F, GL_RGBA, GL_FLOAT, "composite", error))
|
||||
return false;
|
||||
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, CompositeFramebuffer());
|
||||
glBindRenderbuffer(GL_RENDERBUFFER, mIdDepthBuf);
|
||||
glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH_COMPONENT24, inputFrameWidth, inputFrameHeight);
|
||||
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT_EXT, GL_RENDERBUFFER, mIdDepthBuf);
|
||||
if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE)
|
||||
{
|
||||
error = "Cannot initialize framebuffer.";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!mRenderTargets.Create(RenderTargetId::Output, outputFrameWidth, outputFrameHeight, GL_RGBA16F, GL_RGBA, GL_FLOAT, "output", error))
|
||||
return false;
|
||||
if (!mRenderTargets.Create(RenderTargetId::OutputPack, outputPackTextureWidth, outputFrameHeight, GL_RGBA8, GL_RGBA, GL_UNSIGNED_BYTE, "output pack", error))
|
||||
return false;
|
||||
|
||||
glBindTexture(GL_TEXTURE_2D, 0);
|
||||
glBindRenderbuffer(GL_RENDERBUFFER, 0);
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||
glBindVertexArray(mFullscreenVAO);
|
||||
glBindVertexArray(0);
|
||||
glBindBuffer(GL_UNIFORM_BUFFER, mGlobalParamsUBO);
|
||||
glBufferData(GL_UNIFORM_BUFFER, 1024, NULL, GL_DYNAMIC_DRAW);
|
||||
glBindBufferBase(GL_UNIFORM_BUFFER, kGlobalParamsBindingPoint, mGlobalParamsUBO);
|
||||
glBindBuffer(GL_UNIFORM_BUFFER, 0);
|
||||
|
||||
mResourcesInitialized = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
void OpenGLRenderer::SetDecodeShaderProgram(GLuint program, GLuint vertexShader, GLuint fragmentShader)
|
||||
{
|
||||
mDecodeProgram = program;
|
||||
mDecodeVertexShader = vertexShader;
|
||||
mDecodeFragmentShader = fragmentShader;
|
||||
mDecodePackedResolutionLocation = program != 0 ? glGetUniformLocation(program, "uPackedVideoResolution") : -1;
|
||||
mDecodeDecodedResolutionLocation = program != 0 ? glGetUniformLocation(program, "uDecodedVideoResolution") : -1;
|
||||
mDecodeInputPixelFormatLocation = program != 0 ? glGetUniformLocation(program, "uInputPixelFormat") : -1;
|
||||
}
|
||||
|
||||
void OpenGLRenderer::SetOutputPackShaderProgram(GLuint program, GLuint vertexShader, GLuint fragmentShader)
|
||||
{
|
||||
mOutputPackProgram = program;
|
||||
mOutputPackVertexShader = vertexShader;
|
||||
mOutputPackFragmentShader = fragmentShader;
|
||||
mOutputPackResolutionLocation = program != 0 ? glGetUniformLocation(program, "uOutputVideoResolution") : -1;
|
||||
mOutputPackActiveWordsLocation = program != 0 ? glGetUniformLocation(program, "uActiveV210Words") : -1;
|
||||
mOutputPackFormatLocation = program != 0 ? glGetUniformLocation(program, "uOutputPackFormat") : -1;
|
||||
}
|
||||
|
||||
bool OpenGLRenderer::ReserveTemporaryRenderTargets(std::size_t count, unsigned width, unsigned height, std::string& error)
|
||||
{
|
||||
return mRenderTargets.ReserveTemporaryTargets(count, width, height, GL_RGBA16F, GL_RGBA, GL_FLOAT, error);
|
||||
}
|
||||
|
||||
void OpenGLRenderer::ResizeView(int width, int height)
|
||||
{
|
||||
mViewWidth = width;
|
||||
mViewHeight = height;
|
||||
}
|
||||
|
||||
void OpenGLRenderer::PresentToWindow(HDC hdc, unsigned outputFrameWidth, unsigned outputFrameHeight)
|
||||
{
|
||||
int destWidth = mViewWidth;
|
||||
int destHeight = mViewHeight;
|
||||
int destX = 0;
|
||||
int destY = 0;
|
||||
|
||||
if (outputFrameWidth > 0 && outputFrameHeight > 0 && mViewWidth > 0 && mViewHeight > 0)
|
||||
{
|
||||
const double frameAspect = static_cast<double>(outputFrameWidth) / static_cast<double>(outputFrameHeight);
|
||||
const double viewAspect = static_cast<double>(mViewWidth) / static_cast<double>(mViewHeight);
|
||||
|
||||
if (viewAspect > frameAspect)
|
||||
{
|
||||
destHeight = mViewHeight;
|
||||
destWidth = static_cast<int>(destHeight * frameAspect + 0.5);
|
||||
destX = (mViewWidth - destWidth) / 2;
|
||||
}
|
||||
else
|
||||
{
|
||||
destWidth = mViewWidth;
|
||||
destHeight = static_cast<int>(destWidth / frameAspect + 0.5);
|
||||
destY = (mViewHeight - destHeight) / 2;
|
||||
}
|
||||
}
|
||||
|
||||
glBindFramebuffer(GL_READ_FRAMEBUFFER, OutputFramebuffer());
|
||||
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0);
|
||||
glDisable(GL_SCISSOR_TEST);
|
||||
glViewport(0, 0, mViewWidth, mViewHeight);
|
||||
glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
|
||||
glClear(GL_COLOR_BUFFER_BIT);
|
||||
glBlitFramebuffer(0, 0, outputFrameWidth, outputFrameHeight, destX, destY, destX + destWidth, destY + destHeight, GL_COLOR_BUFFER_BIT, GL_LINEAR);
|
||||
|
||||
SwapBuffers(hdc);
|
||||
}
|
||||
|
||||
void OpenGLRenderer::DestroyResources()
|
||||
{
|
||||
if (mFullscreenVAO != 0)
|
||||
glDeleteVertexArrays(1, &mFullscreenVAO);
|
||||
if (mGlobalParamsUBO != 0)
|
||||
glDeleteBuffers(1, &mGlobalParamsUBO);
|
||||
if (mIdColorBuf != 0)
|
||||
glDeleteRenderbuffers(1, &mIdColorBuf);
|
||||
if (mIdDepthBuf != 0)
|
||||
glDeleteRenderbuffers(1, &mIdDepthBuf);
|
||||
if (mCaptureTexture != 0)
|
||||
glDeleteTextures(1, &mCaptureTexture);
|
||||
if (mTextureUploadBuffer != 0)
|
||||
glDeleteBuffers(1, &mTextureUploadBuffer);
|
||||
mRenderTargets.Destroy();
|
||||
|
||||
mFullscreenVAO = 0;
|
||||
mGlobalParamsUBO = 0;
|
||||
mIdColorBuf = 0;
|
||||
mIdDepthBuf = 0;
|
||||
mCaptureTexture = 0;
|
||||
mTextureUploadBuffer = 0;
|
||||
mGlobalParamsUBOSize = 0;
|
||||
mResourcesInitialized = false;
|
||||
|
||||
mTemporalHistory.DestroyResources();
|
||||
mFeedbackBuffers.DestroyResources();
|
||||
DestroyLayerPrograms();
|
||||
DestroyDecodeShaderProgram();
|
||||
DestroyOutputPackShaderProgram();
|
||||
}
|
||||
|
||||
void OpenGLRenderer::DestroySingleLayerProgram(LayerProgram& layerProgram)
|
||||
{
|
||||
for (LayerProgram::PassProgram& passProgram : layerProgram.passes)
|
||||
{
|
||||
for (LayerProgram::TextureBinding& binding : passProgram.textureBindings)
|
||||
{
|
||||
if (binding.texture != 0)
|
||||
{
|
||||
glDeleteTextures(1, &binding.texture);
|
||||
binding.texture = 0;
|
||||
}
|
||||
}
|
||||
passProgram.textureBindings.clear();
|
||||
|
||||
for (LayerProgram::TextBinding& binding : passProgram.textBindings)
|
||||
{
|
||||
if (binding.texture != 0)
|
||||
{
|
||||
glDeleteTextures(1, &binding.texture);
|
||||
binding.texture = 0;
|
||||
}
|
||||
}
|
||||
passProgram.textBindings.clear();
|
||||
|
||||
if (passProgram.program != 0)
|
||||
{
|
||||
glDeleteProgram(passProgram.program);
|
||||
passProgram.program = 0;
|
||||
}
|
||||
|
||||
if (passProgram.fragmentShader != 0)
|
||||
{
|
||||
glDeleteShader(passProgram.fragmentShader);
|
||||
passProgram.fragmentShader = 0;
|
||||
}
|
||||
|
||||
if (passProgram.vertexShader != 0)
|
||||
{
|
||||
glDeleteShader(passProgram.vertexShader);
|
||||
passProgram.vertexShader = 0;
|
||||
}
|
||||
}
|
||||
layerProgram.passes.clear();
|
||||
}
|
||||
|
||||
void OpenGLRenderer::DestroyLayerPrograms()
|
||||
{
|
||||
for (LayerProgram& layerProgram : mLayerPrograms)
|
||||
DestroySingleLayerProgram(layerProgram);
|
||||
mLayerPrograms.clear();
|
||||
}
|
||||
|
||||
void OpenGLRenderer::DestroyDecodeShaderProgram()
|
||||
{
|
||||
if (mDecodeProgram != 0)
|
||||
{
|
||||
glDeleteProgram(mDecodeProgram);
|
||||
mDecodeProgram = 0;
|
||||
}
|
||||
mDecodePackedResolutionLocation = -1;
|
||||
mDecodeDecodedResolutionLocation = -1;
|
||||
mDecodeInputPixelFormatLocation = -1;
|
||||
|
||||
if (mDecodeFragmentShader != 0)
|
||||
{
|
||||
glDeleteShader(mDecodeFragmentShader);
|
||||
mDecodeFragmentShader = 0;
|
||||
}
|
||||
|
||||
if (mDecodeVertexShader != 0)
|
||||
{
|
||||
glDeleteShader(mDecodeVertexShader);
|
||||
mDecodeVertexShader = 0;
|
||||
}
|
||||
}
|
||||
|
||||
void OpenGLRenderer::DestroyOutputPackShaderProgram()
|
||||
{
|
||||
if (mOutputPackProgram != 0)
|
||||
{
|
||||
glDeleteProgram(mOutputPackProgram);
|
||||
mOutputPackProgram = 0;
|
||||
}
|
||||
mOutputPackResolutionLocation = -1;
|
||||
mOutputPackActiveWordsLocation = -1;
|
||||
mOutputPackFormatLocation = -1;
|
||||
|
||||
if (mOutputPackFragmentShader != 0)
|
||||
{
|
||||
glDeleteShader(mOutputPackFragmentShader);
|
||||
mOutputPackFragmentShader = 0;
|
||||
}
|
||||
|
||||
if (mOutputPackVertexShader != 0)
|
||||
{
|
||||
glDeleteShader(mOutputPackVertexShader);
|
||||
mOutputPackVertexShader = 0;
|
||||
}
|
||||
}
|
||||
@@ -1,131 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "GLExtensions.h"
|
||||
#include "RenderTargetPool.h"
|
||||
#include "ShaderFeedbackBuffers.h"
|
||||
#include "ShaderTypes.h"
|
||||
#include "TemporalHistoryBuffers.h"
|
||||
|
||||
#include <windows.h>
|
||||
|
||||
#include <filesystem>
|
||||
#include <gl/gl.h>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
class OpenGLRenderer
|
||||
{
|
||||
public:
|
||||
struct LayerProgram
|
||||
{
|
||||
struct TextureBinding
|
||||
{
|
||||
std::string samplerName;
|
||||
std::filesystem::path sourcePath;
|
||||
GLuint texture = 0;
|
||||
};
|
||||
|
||||
struct TextBinding
|
||||
{
|
||||
std::string parameterId;
|
||||
std::string samplerName;
|
||||
std::string fontId;
|
||||
GLuint texture = 0;
|
||||
std::string renderedText;
|
||||
unsigned renderedWidth = 0;
|
||||
unsigned renderedHeight = 0;
|
||||
};
|
||||
|
||||
std::string layerId;
|
||||
std::string shaderId;
|
||||
|
||||
struct PassProgram
|
||||
{
|
||||
std::string passId;
|
||||
std::vector<std::string> inputNames;
|
||||
std::string outputName;
|
||||
GLuint shaderTextureBase = 0;
|
||||
GLuint program = 0;
|
||||
GLuint vertexShader = 0;
|
||||
GLuint fragmentShader = 0;
|
||||
std::vector<TextureBinding> textureBindings;
|
||||
std::vector<TextBinding> textBindings;
|
||||
};
|
||||
|
||||
std::vector<PassProgram> passes;
|
||||
};
|
||||
|
||||
GLuint CaptureTexture() const { return mCaptureTexture; }
|
||||
GLuint DecodedTexture() const { return mRenderTargets.Texture(RenderTargetId::Decoded); }
|
||||
GLuint LayerTempTexture() const { return mRenderTargets.Texture(RenderTargetId::LayerTemp); }
|
||||
GLuint CompositeTexture() const { return mRenderTargets.Texture(RenderTargetId::Composite); }
|
||||
GLuint OutputTexture() const { return mRenderTargets.Texture(RenderTargetId::Output); }
|
||||
GLuint OutputPackTexture() const { return mRenderTargets.Texture(RenderTargetId::OutputPack); }
|
||||
GLuint TextureUploadBuffer() const { return mTextureUploadBuffer; }
|
||||
GLuint DecodeFramebuffer() const { return mRenderTargets.Framebuffer(RenderTargetId::Decoded); }
|
||||
GLuint LayerTempFramebuffer() const { return mRenderTargets.Framebuffer(RenderTargetId::LayerTemp); }
|
||||
GLuint CompositeFramebuffer() const { return mRenderTargets.Framebuffer(RenderTargetId::Composite); }
|
||||
GLuint OutputFramebuffer() const { return mRenderTargets.Framebuffer(RenderTargetId::Output); }
|
||||
GLuint OutputPackFramebuffer() const { return mRenderTargets.Framebuffer(RenderTargetId::OutputPack); }
|
||||
GLuint FullscreenVertexArray() const { return mFullscreenVAO; }
|
||||
GLuint GlobalParamsUBO() const { return mGlobalParamsUBO; }
|
||||
GLuint DecodeProgram() const { return mDecodeProgram; }
|
||||
GLuint OutputPackProgram() const { return mOutputPackProgram; }
|
||||
GLint DecodePackedResolutionLocation() const { return mDecodePackedResolutionLocation; }
|
||||
GLint DecodeDecodedResolutionLocation() const { return mDecodeDecodedResolutionLocation; }
|
||||
GLint DecodeInputPixelFormatLocation() const { return mDecodeInputPixelFormatLocation; }
|
||||
GLint OutputPackResolutionLocation() const { return mOutputPackResolutionLocation; }
|
||||
GLint OutputPackActiveWordsLocation() const { return mOutputPackActiveWordsLocation; }
|
||||
GLint OutputPackFormatLocation() const { return mOutputPackFormatLocation; }
|
||||
GLsizeiptr GlobalParamsUBOSize() const { return mGlobalParamsUBOSize; }
|
||||
void SetGlobalParamsUBOSize(GLsizeiptr size) { mGlobalParamsUBOSize = size; }
|
||||
bool ResourcesInitialized() const { return mResourcesInitialized; }
|
||||
void ReplaceLayerPrograms(std::vector<LayerProgram>& newPrograms) { mLayerPrograms.swap(newPrograms); }
|
||||
std::vector<LayerProgram>& LayerPrograms() { return mLayerPrograms; }
|
||||
const std::vector<LayerProgram>& LayerPrograms() const { return mLayerPrograms; }
|
||||
bool ReserveTemporaryRenderTargets(std::size_t count, unsigned width, unsigned height, std::string& error);
|
||||
const RenderTarget& TemporaryRenderTarget(std::size_t index) const { return mRenderTargets.TemporaryTarget(index); }
|
||||
std::size_t TemporaryRenderTargetCount() const { return mRenderTargets.TemporaryTargetCount(); }
|
||||
TemporalHistoryBuffers& TemporalHistory() { return mTemporalHistory; }
|
||||
const TemporalHistoryBuffers& TemporalHistory() const { return mTemporalHistory; }
|
||||
ShaderFeedbackBuffers& FeedbackBuffers() { return mFeedbackBuffers; }
|
||||
const ShaderFeedbackBuffers& FeedbackBuffers() const { return mFeedbackBuffers; }
|
||||
void SetDecodeShaderProgram(GLuint program, GLuint vertexShader, GLuint fragmentShader);
|
||||
void SetOutputPackShaderProgram(GLuint program, GLuint vertexShader, GLuint fragmentShader);
|
||||
bool InitializeResources(unsigned inputFrameWidth, unsigned inputFrameHeight, unsigned captureTextureWidth, unsigned outputFrameWidth, unsigned outputFrameHeight, unsigned outputPackTextureWidth, std::string& error);
|
||||
void ResizeView(int width, int height);
|
||||
void PresentToWindow(HDC hdc, unsigned outputFrameWidth, unsigned outputFrameHeight);
|
||||
void DestroyResources();
|
||||
void DestroySingleLayerProgram(LayerProgram& layerProgram);
|
||||
void DestroyLayerPrograms();
|
||||
void DestroyDecodeShaderProgram();
|
||||
void DestroyOutputPackShaderProgram();
|
||||
|
||||
private:
|
||||
GLuint mCaptureTexture = 0;
|
||||
GLuint mTextureUploadBuffer = 0;
|
||||
GLuint mIdColorBuf = 0;
|
||||
GLuint mIdDepthBuf = 0;
|
||||
GLuint mFullscreenVAO = 0;
|
||||
GLuint mGlobalParamsUBO = 0;
|
||||
GLuint mDecodeProgram = 0;
|
||||
GLuint mDecodeVertexShader = 0;
|
||||
GLuint mDecodeFragmentShader = 0;
|
||||
GLint mDecodePackedResolutionLocation = -1;
|
||||
GLint mDecodeDecodedResolutionLocation = -1;
|
||||
GLint mDecodeInputPixelFormatLocation = -1;
|
||||
GLuint mOutputPackProgram = 0;
|
||||
GLuint mOutputPackVertexShader = 0;
|
||||
GLuint mOutputPackFragmentShader = 0;
|
||||
GLint mOutputPackResolutionLocation = -1;
|
||||
GLint mOutputPackActiveWordsLocation = -1;
|
||||
GLint mOutputPackFormatLocation = -1;
|
||||
GLsizeiptr mGlobalParamsUBOSize = 0;
|
||||
bool mResourcesInitialized = false;
|
||||
int mViewWidth = 0;
|
||||
int mViewHeight = 0;
|
||||
std::vector<LayerProgram> mLayerPrograms;
|
||||
RenderTargetPool mRenderTargets;
|
||||
TemporalHistoryBuffers mTemporalHistory;
|
||||
ShaderFeedbackBuffers mFeedbackBuffers;
|
||||
};
|
||||
@@ -1,136 +0,0 @@
|
||||
#include "RenderTargetPool.h"
|
||||
|
||||
#include <cstddef>
|
||||
|
||||
namespace
|
||||
{
|
||||
void ConfigureRenderTargetTexture(
|
||||
unsigned width,
|
||||
unsigned height,
|
||||
GLenum internalFormat,
|
||||
GLenum pixelFormat,
|
||||
GLenum pixelType)
|
||||
{
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
|
||||
glTexImage2D(GL_TEXTURE_2D, 0, internalFormat, width, height, 0, pixelFormat, pixelType, NULL);
|
||||
}
|
||||
}
|
||||
|
||||
bool RenderTargetPool::Create(
|
||||
RenderTargetId id,
|
||||
unsigned width,
|
||||
unsigned height,
|
||||
GLenum internalFormat,
|
||||
GLenum pixelFormat,
|
||||
GLenum pixelType,
|
||||
const char* errorPrefix,
|
||||
std::string& error)
|
||||
{
|
||||
RenderTarget& target = mTargets[TargetIndex(id)];
|
||||
if (target.texture != 0 || target.framebuffer != 0)
|
||||
{
|
||||
error = std::string(errorPrefix) + " render target was already initialized.";
|
||||
return false;
|
||||
}
|
||||
|
||||
glGenTextures(1, &target.texture);
|
||||
glBindTexture(GL_TEXTURE_2D, target.texture);
|
||||
ConfigureRenderTargetTexture(width, height, internalFormat, pixelFormat, pixelType);
|
||||
glBindTexture(GL_TEXTURE_2D, 0);
|
||||
|
||||
glGenFramebuffers(1, &target.framebuffer);
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, target.framebuffer);
|
||||
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, target.texture, 0);
|
||||
if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE)
|
||||
{
|
||||
error = std::string("Cannot initialize ") + errorPrefix + " framebuffer.";
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||
return false;
|
||||
}
|
||||
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||
target.width = width;
|
||||
target.height = height;
|
||||
target.internalFormat = internalFormat;
|
||||
target.pixelFormat = pixelFormat;
|
||||
target.pixelType = pixelType;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool RenderTargetPool::ReserveTemporaryTargets(
|
||||
std::size_t count,
|
||||
unsigned width,
|
||||
unsigned height,
|
||||
GLenum internalFormat,
|
||||
GLenum pixelFormat,
|
||||
GLenum pixelType,
|
||||
std::string& error)
|
||||
{
|
||||
if (mTemporaryTargets.size() == count)
|
||||
return true;
|
||||
|
||||
DestroyTemporaryTargets();
|
||||
|
||||
mTemporaryTargets.resize(count);
|
||||
for (std::size_t index = 0; index < mTemporaryTargets.size(); ++index)
|
||||
{
|
||||
RenderTarget& target = mTemporaryTargets[index];
|
||||
glGenTextures(1, &target.texture);
|
||||
glBindTexture(GL_TEXTURE_2D, target.texture);
|
||||
ConfigureRenderTargetTexture(width, height, internalFormat, pixelFormat, pixelType);
|
||||
glBindTexture(GL_TEXTURE_2D, 0);
|
||||
|
||||
glGenFramebuffers(1, &target.framebuffer);
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, target.framebuffer);
|
||||
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, target.texture, 0);
|
||||
if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE)
|
||||
{
|
||||
error = "Cannot initialize temporary render target.";
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||
return false;
|
||||
}
|
||||
|
||||
target.width = width;
|
||||
target.height = height;
|
||||
target.internalFormat = internalFormat;
|
||||
target.pixelFormat = pixelFormat;
|
||||
target.pixelType = pixelType;
|
||||
}
|
||||
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||
return true;
|
||||
}
|
||||
|
||||
void RenderTargetPool::DestroyTemporaryTargets()
|
||||
{
|
||||
for (RenderTarget& target : mTemporaryTargets)
|
||||
{
|
||||
if (target.framebuffer != 0)
|
||||
glDeleteFramebuffers(1, &target.framebuffer);
|
||||
if (target.texture != 0)
|
||||
glDeleteTextures(1, &target.texture);
|
||||
}
|
||||
mTemporaryTargets.clear();
|
||||
}
|
||||
|
||||
void RenderTargetPool::Destroy()
|
||||
{
|
||||
for (RenderTarget& target : mTargets)
|
||||
{
|
||||
if (target.framebuffer != 0)
|
||||
glDeleteFramebuffers(1, &target.framebuffer);
|
||||
if (target.texture != 0)
|
||||
glDeleteTextures(1, &target.texture);
|
||||
target = RenderTarget();
|
||||
}
|
||||
|
||||
DestroyTemporaryTargets();
|
||||
}
|
||||
|
||||
const RenderTarget& RenderTargetPool::Target(RenderTargetId id) const
|
||||
{
|
||||
return mTargets[TargetIndex(id)];
|
||||
}
|
||||
@@ -1,64 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "GLExtensions.h"
|
||||
|
||||
#include <array>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
enum class RenderTargetId
|
||||
{
|
||||
Decoded,
|
||||
LayerTemp,
|
||||
Composite,
|
||||
Output,
|
||||
OutputPack,
|
||||
Count
|
||||
};
|
||||
|
||||
struct RenderTarget
|
||||
{
|
||||
GLuint texture = 0;
|
||||
GLuint framebuffer = 0;
|
||||
unsigned width = 0;
|
||||
unsigned height = 0;
|
||||
GLenum internalFormat = GL_RGBA8;
|
||||
GLenum pixelFormat = GL_RGBA;
|
||||
GLenum pixelType = GL_UNSIGNED_BYTE;
|
||||
};
|
||||
|
||||
class RenderTargetPool
|
||||
{
|
||||
public:
|
||||
bool Create(
|
||||
RenderTargetId id,
|
||||
unsigned width,
|
||||
unsigned height,
|
||||
GLenum internalFormat,
|
||||
GLenum pixelFormat,
|
||||
GLenum pixelType,
|
||||
const char* errorPrefix,
|
||||
std::string& error);
|
||||
bool ReserveTemporaryTargets(
|
||||
std::size_t count,
|
||||
unsigned width,
|
||||
unsigned height,
|
||||
GLenum internalFormat,
|
||||
GLenum pixelFormat,
|
||||
GLenum pixelType,
|
||||
std::string& error);
|
||||
void DestroyTemporaryTargets();
|
||||
void Destroy();
|
||||
|
||||
GLuint Texture(RenderTargetId id) const { return Target(id).texture; }
|
||||
GLuint Framebuffer(RenderTargetId id) const { return Target(id).framebuffer; }
|
||||
const RenderTarget& Target(RenderTargetId id) const;
|
||||
const RenderTarget& TemporaryTarget(std::size_t index) const { return mTemporaryTargets[index]; }
|
||||
std::size_t TemporaryTargetCount() const { return mTemporaryTargets.size(); }
|
||||
|
||||
private:
|
||||
static std::size_t TargetIndex(RenderTargetId id) { return static_cast<std::size_t>(id); }
|
||||
|
||||
std::array<RenderTarget, static_cast<std::size_t>(RenderTargetId::Count)> mTargets;
|
||||
std::vector<RenderTarget> mTemporaryTargets;
|
||||
};
|
||||
@@ -1,172 +0,0 @@
|
||||
#include "GlShaderSources.h"
|
||||
|
||||
const char* kFullscreenTriangleVertexShaderSource =
|
||||
"#version 430 core\n"
|
||||
"out vec2 vTexCoord;\n"
|
||||
"void main()\n"
|
||||
"{\n"
|
||||
" vec2 positions[3] = vec2[3](vec2(-1.0, -1.0), vec2(3.0, -1.0), vec2(-1.0, 3.0));\n"
|
||||
" vec2 texCoords[3] = vec2[3](vec2(0.0, 0.0), vec2(2.0, 0.0), vec2(0.0, 2.0));\n"
|
||||
" gl_Position = vec4(positions[gl_VertexID], 0.0, 1.0);\n"
|
||||
" vTexCoord = texCoords[gl_VertexID];\n"
|
||||
"}\n";
|
||||
|
||||
const char* kDecodeFragmentShaderSource =
|
||||
"#version 430 core\n"
|
||||
"layout(binding = 2) uniform sampler2D uPackedVideoInput;\n"
|
||||
"uniform vec2 uPackedVideoResolution;\n"
|
||||
"uniform vec2 uDecodedVideoResolution;\n"
|
||||
"uniform int uInputPixelFormat;\n"
|
||||
"in vec2 vTexCoord;\n"
|
||||
"layout(location = 0) out vec4 fragColor;\n"
|
||||
"vec4 rec709YCbCr2rgba(float Y, float Cb, float Cr, float a)\n"
|
||||
"{\n"
|
||||
" Y = (Y * 256.0 - 16.0) / 219.0;\n"
|
||||
" Cb = (Cb * 256.0 - 16.0) / 224.0 - 0.5;\n"
|
||||
" Cr = (Cr * 256.0 - 16.0) / 224.0 - 0.5;\n"
|
||||
" return vec4(Y + 1.5748 * Cr, Y - 0.1873 * Cb - 0.4681 * Cr, Y + 1.8556 * Cb, a);\n"
|
||||
"}\n"
|
||||
"vec4 rec709YCbCr10_2rgba(float Y, float Cb, float Cr, float a)\n"
|
||||
"{\n"
|
||||
" Y = (Y - 64.0) / 876.0;\n"
|
||||
" Cb = (Cb - 64.0) / 896.0 - 0.5;\n"
|
||||
" Cr = (Cr - 64.0) / 896.0 - 0.5;\n"
|
||||
" return vec4(Y + 1.5748 * Cr, Y - 0.1873 * Cb - 0.4681 * Cr, Y + 1.8556 * Cb, a);\n"
|
||||
"}\n"
|
||||
"uint loadV210Word(ivec2 coord)\n"
|
||||
"{\n"
|
||||
" vec4 b = round(texelFetch(uPackedVideoInput, coord, 0) * 255.0);\n"
|
||||
" return uint(b.r) | (uint(b.g) << 8) | (uint(b.b) << 16) | (uint(b.a) << 24);\n"
|
||||
"}\n"
|
||||
"float v210Component(uint word, int index)\n"
|
||||
"{\n"
|
||||
" return float((word >> uint(index * 10)) & 1023u);\n"
|
||||
"}\n"
|
||||
"vec4 decodeUyvy8(ivec2 outputCoord, ivec2 packedSize)\n"
|
||||
"{\n"
|
||||
" ivec2 packedCoord = ivec2(clamp(outputCoord.x / 2, 0, packedSize.x - 1), clamp(outputCoord.y, 0, packedSize.y - 1));\n"
|
||||
" vec4 macroPixel = texelFetch(uPackedVideoInput, packedCoord, 0);\n"
|
||||
" float ySample = (outputCoord.x & 1) != 0 ? macroPixel.a : macroPixel.g;\n"
|
||||
" return rec709YCbCr2rgba(ySample, macroPixel.b, macroPixel.r, 1.0);\n"
|
||||
"}\n"
|
||||
"vec4 decodeV210(ivec2 outputCoord, ivec2 packedSize)\n"
|
||||
"{\n"
|
||||
" int group = outputCoord.x / 6;\n"
|
||||
" int pixel = outputCoord.x - group * 6;\n"
|
||||
" int wordBase = group * 4;\n"
|
||||
" ivec2 rowBase = ivec2(wordBase, clamp(outputCoord.y, 0, packedSize.y - 1));\n"
|
||||
" uint w0 = loadV210Word(ivec2(min(rowBase.x + 0, packedSize.x - 1), rowBase.y));\n"
|
||||
" uint w1 = loadV210Word(ivec2(min(rowBase.x + 1, packedSize.x - 1), rowBase.y));\n"
|
||||
" uint w2 = loadV210Word(ivec2(min(rowBase.x + 2, packedSize.x - 1), rowBase.y));\n"
|
||||
" uint w3 = loadV210Word(ivec2(min(rowBase.x + 3, packedSize.x - 1), rowBase.y));\n"
|
||||
" float y0 = v210Component(w0, 1);\n"
|
||||
" float y1 = v210Component(w1, 0);\n"
|
||||
" float y2 = v210Component(w1, 2);\n"
|
||||
" float y3 = v210Component(w2, 1);\n"
|
||||
" float y4 = v210Component(w3, 0);\n"
|
||||
" float y5 = v210Component(w3, 2);\n"
|
||||
" float cb0 = v210Component(w0, 0);\n"
|
||||
" float cr0 = v210Component(w0, 2);\n"
|
||||
" float cb2 = v210Component(w1, 1);\n"
|
||||
" float cr2 = v210Component(w2, 0);\n"
|
||||
" float cb4 = v210Component(w2, 2);\n"
|
||||
" float cr4 = v210Component(w3, 1);\n"
|
||||
" float ySample = pixel == 0 ? y0 : pixel == 1 ? y1 : pixel == 2 ? y2 : pixel == 3 ? y3 : pixel == 4 ? y4 : y5;\n"
|
||||
" float cbSample = pixel < 2 ? cb0 : pixel < 4 ? cb2 : cb4;\n"
|
||||
" float crSample = pixel < 2 ? cr0 : pixel < 4 ? cr2 : cr4;\n"
|
||||
" return rec709YCbCr10_2rgba(ySample, cbSample, crSample, 1.0);\n"
|
||||
"}\n"
|
||||
"void main()\n"
|
||||
"{\n"
|
||||
" vec2 correctedUv = vec2(vTexCoord.x, 1.0 - vTexCoord.y);\n"
|
||||
" ivec2 decodedSize = ivec2(max(uDecodedVideoResolution, vec2(1.0, 1.0)));\n"
|
||||
" ivec2 outputCoord = clamp(ivec2(correctedUv * vec2(decodedSize)), ivec2(0, 0), decodedSize - ivec2(1, 1));\n"
|
||||
" ivec2 packedSize = ivec2(max(uPackedVideoResolution, vec2(1.0, 1.0)));\n"
|
||||
" fragColor = uInputPixelFormat == 1 ? decodeV210(outputCoord, packedSize) : decodeUyvy8(outputCoord, packedSize);\n"
|
||||
"}\n";
|
||||
|
||||
const char* kOutputPackFragmentShaderSource =
|
||||
"#version 430 core\n"
|
||||
"layout(binding = 0) uniform sampler2D uOutputRgb;\n"
|
||||
"uniform vec2 uOutputVideoResolution;\n"
|
||||
"uniform float uActiveV210Words;\n"
|
||||
"uniform int uOutputPackFormat;\n"
|
||||
"in vec2 vTexCoord;\n"
|
||||
"layout(location = 0) out vec4 fragColor;\n"
|
||||
"vec4 rgbaAt(int x, int y)\n"
|
||||
"{\n"
|
||||
" ivec2 size = ivec2(max(uOutputVideoResolution, vec2(1.0, 1.0)));\n"
|
||||
" return clamp(texelFetch(uOutputRgb, ivec2(clamp(x, 0, size.x - 1), clamp(y, 0, size.y - 1)), 0), vec4(0.0), vec4(1.0));\n"
|
||||
"}\n"
|
||||
"vec3 rgbAt(int x, int y)\n"
|
||||
"{\n"
|
||||
" return rgbaAt(x, y).rgb;\n"
|
||||
"}\n"
|
||||
"vec3 rgbToLegalYcbcr10(vec3 rgb)\n"
|
||||
"{\n"
|
||||
" float y = dot(rgb, vec3(0.2126, 0.7152, 0.0722));\n"
|
||||
" float cb = (rgb.b - y) / 1.8556 + 0.5;\n"
|
||||
" float cr = (rgb.r - y) / 1.5748 + 0.5;\n"
|
||||
" return vec3(clamp(round(64.0 + y * 876.0), 64.0, 940.0), clamp(round(64.0 + cb * 896.0), 64.0, 960.0), clamp(round(64.0 + cr * 896.0), 64.0, 960.0));\n"
|
||||
"}\n"
|
||||
"uint makeWord(float a, float b, float c)\n"
|
||||
"{\n"
|
||||
" return (uint(a) & 1023u) | ((uint(b) & 1023u) << 10) | ((uint(c) & 1023u) << 20);\n"
|
||||
"}\n"
|
||||
"vec4 wordToBytes(uint word)\n"
|
||||
"{\n"
|
||||
" return vec4(float(word & 255u), float((word >> 8) & 255u), float((word >> 16) & 255u), float((word >> 24) & 255u)) / 255.0;\n"
|
||||
"}\n"
|
||||
"vec4 bigEndianWordToBytes(uint word)\n"
|
||||
"{\n"
|
||||
" return vec4(float((word >> 24) & 255u), float((word >> 16) & 255u), float((word >> 8) & 255u), float(word & 255u)) / 255.0;\n"
|
||||
"}\n"
|
||||
"vec4 packAy10Word(ivec2 outCoord)\n"
|
||||
"{\n"
|
||||
" ivec2 size = ivec2(max(uOutputVideoResolution, vec2(1.0, 1.0)));\n"
|
||||
" if (outCoord.x >= size.x)\n"
|
||||
" return vec4(0.0);\n"
|
||||
" int pixelBase = (outCoord.x / 2) * 2;\n"
|
||||
" int y = outCoord.y;\n"
|
||||
" vec4 rgba0 = rgbaAt(pixelBase + 0, y);\n"
|
||||
" vec4 rgba1 = rgbaAt(pixelBase + 1, y);\n"
|
||||
" vec3 c0 = rgbToLegalYcbcr10(rgba0.rgb);\n"
|
||||
" vec3 c1 = rgbToLegalYcbcr10(rgba1.rgb);\n"
|
||||
" float chroma = (outCoord.x & 1) == 0 ? round((c0.y + c1.y) * 0.5) : round((c0.z + c1.z) * 0.5);\n"
|
||||
" float alpha = round(clamp(((outCoord.x & 1) == 0 ? rgba0.a : rgba1.a), 0.0, 1.0) * 1023.0);\n"
|
||||
" float luma = (outCoord.x & 1) == 0 ? c0.x : c1.x;\n"
|
||||
" uint word = ((uint(luma) & 1023u) << 22) | ((uint(chroma) & 1023u) << 12) | ((uint(alpha) & 1023u) << 2);\n"
|
||||
" return bigEndianWordToBytes(word);\n"
|
||||
"}\n"
|
||||
"void main()\n"
|
||||
"{\n"
|
||||
" ivec2 outCoord = ivec2(gl_FragCoord.xy);\n"
|
||||
" if (uOutputPackFormat == 2)\n"
|
||||
" {\n"
|
||||
" fragColor = packAy10Word(outCoord);\n"
|
||||
" return;\n"
|
||||
" }\n"
|
||||
" if (float(outCoord.x) >= uActiveV210Words)\n"
|
||||
" {\n"
|
||||
" fragColor = vec4(0.0);\n"
|
||||
" return;\n"
|
||||
" }\n"
|
||||
" int group = outCoord.x / 4;\n"
|
||||
" int wordIndex = outCoord.x - group * 4;\n"
|
||||
" int pixelBase = group * 6;\n"
|
||||
" int y = outCoord.y;\n"
|
||||
" vec3 c0 = rgbToLegalYcbcr10(rgbAt(pixelBase + 0, y));\n"
|
||||
" vec3 c1 = rgbToLegalYcbcr10(rgbAt(pixelBase + 1, y));\n"
|
||||
" vec3 c2 = rgbToLegalYcbcr10(rgbAt(pixelBase + 2, y));\n"
|
||||
" vec3 c3 = rgbToLegalYcbcr10(rgbAt(pixelBase + 3, y));\n"
|
||||
" vec3 c4 = rgbToLegalYcbcr10(rgbAt(pixelBase + 4, y));\n"
|
||||
" vec3 c5 = rgbToLegalYcbcr10(rgbAt(pixelBase + 5, y));\n"
|
||||
" float cb0 = round((c0.y + c1.y) * 0.5);\n"
|
||||
" float cr0 = round((c0.z + c1.z) * 0.5);\n"
|
||||
" float cb2 = round((c2.y + c3.y) * 0.5);\n"
|
||||
" float cr2 = round((c2.z + c3.z) * 0.5);\n"
|
||||
" float cb4 = round((c4.y + c5.y) * 0.5);\n"
|
||||
" float cr4 = round((c4.z + c5.z) * 0.5);\n"
|
||||
" uint word = wordIndex == 0 ? makeWord(cb0, c0.x, cr0) : wordIndex == 1 ? makeWord(c1.x, cb2, c2.x) : wordIndex == 2 ? makeWord(cr2, c3.x, cb4) : makeWord(c4.x, cr4, c5.x);\n"
|
||||
" fragColor = wordToBytes(word);\n"
|
||||
"}\n";
|
||||
@@ -1,5 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
extern const char* kFullscreenTriangleVertexShaderSource;
|
||||
extern const char* kDecodeFragmentShaderSource;
|
||||
extern const char* kOutputPackFragmentShaderSource;
|
||||
@@ -1,104 +0,0 @@
|
||||
#include "GlobalParamsBuffer.h"
|
||||
|
||||
#include "GlRenderConstants.h"
|
||||
#include "Std140Buffer.h"
|
||||
|
||||
#include <vector>
|
||||
|
||||
GlobalParamsBuffer::GlobalParamsBuffer(OpenGLRenderer& renderer) :
|
||||
mRenderer(renderer)
|
||||
{
|
||||
}
|
||||
|
||||
bool GlobalParamsBuffer::Update(const RuntimeRenderState& state, unsigned availableSourceHistoryLength, unsigned availableTemporalHistoryLength, bool feedbackAvailable)
|
||||
{
|
||||
std::vector<unsigned char>& buffer = mScratchBuffer;
|
||||
buffer.clear();
|
||||
buffer.reserve(512);
|
||||
|
||||
AppendStd140Float(buffer, static_cast<float>(state.timeSeconds));
|
||||
AppendStd140Vec2(buffer, static_cast<float>(state.inputWidth), static_cast<float>(state.inputHeight));
|
||||
AppendStd140Vec2(buffer, static_cast<float>(state.outputWidth), static_cast<float>(state.outputHeight));
|
||||
AppendStd140Float(buffer, static_cast<float>(state.utcTimeSeconds));
|
||||
AppendStd140Float(buffer, static_cast<float>(state.utcOffsetSeconds));
|
||||
AppendStd140Float(buffer, static_cast<float>(state.startupRandom));
|
||||
AppendStd140Float(buffer, static_cast<float>(state.frameCount));
|
||||
AppendStd140Float(buffer, static_cast<float>(state.mixAmount));
|
||||
AppendStd140Float(buffer, static_cast<float>(state.bypass));
|
||||
const unsigned effectiveSourceHistoryLength = availableSourceHistoryLength < state.effectiveTemporalHistoryLength
|
||||
? availableSourceHistoryLength
|
||||
: state.effectiveTemporalHistoryLength;
|
||||
const unsigned effectiveTemporalHistoryLength = (state.temporalHistorySource == TemporalHistorySource::PreLayerInput)
|
||||
? (availableTemporalHistoryLength < state.effectiveTemporalHistoryLength ? availableTemporalHistoryLength : state.effectiveTemporalHistoryLength)
|
||||
: 0u;
|
||||
AppendStd140Int(buffer, static_cast<int>(effectiveSourceHistoryLength));
|
||||
AppendStd140Int(buffer, static_cast<int>(effectiveTemporalHistoryLength));
|
||||
AppendStd140Int(buffer, feedbackAvailable ? 1 : 0);
|
||||
|
||||
for (const ShaderParameterDefinition& definition : state.parameterDefinitions)
|
||||
{
|
||||
auto valueIt = state.parameterValues.find(definition.id);
|
||||
const ShaderParameterValue value = valueIt != state.parameterValues.end()
|
||||
? valueIt->second
|
||||
: ShaderParameterValue();
|
||||
|
||||
switch (definition.type)
|
||||
{
|
||||
case ShaderParameterType::Float:
|
||||
AppendStd140Float(buffer, value.numberValues.empty() ? 0.0f : static_cast<float>(value.numberValues[0]));
|
||||
break;
|
||||
case ShaderParameterType::Vec2:
|
||||
AppendStd140Vec2(buffer,
|
||||
value.numberValues.size() > 0 ? static_cast<float>(value.numberValues[0]) : 0.0f,
|
||||
value.numberValues.size() > 1 ? static_cast<float>(value.numberValues[1]) : 0.0f);
|
||||
break;
|
||||
case ShaderParameterType::Color:
|
||||
AppendStd140Vec4(buffer,
|
||||
value.numberValues.size() > 0 ? static_cast<float>(value.numberValues[0]) : 1.0f,
|
||||
value.numberValues.size() > 1 ? static_cast<float>(value.numberValues[1]) : 1.0f,
|
||||
value.numberValues.size() > 2 ? static_cast<float>(value.numberValues[2]) : 1.0f,
|
||||
value.numberValues.size() > 3 ? static_cast<float>(value.numberValues[3]) : 1.0f);
|
||||
break;
|
||||
case ShaderParameterType::Boolean:
|
||||
AppendStd140Int(buffer, value.booleanValue ? 1 : 0);
|
||||
break;
|
||||
case ShaderParameterType::Enum:
|
||||
{
|
||||
int selectedIndex = 0;
|
||||
for (std::size_t optionIndex = 0; optionIndex < definition.enumOptions.size(); ++optionIndex)
|
||||
{
|
||||
if (definition.enumOptions[optionIndex].value == value.enumValue)
|
||||
{
|
||||
selectedIndex = static_cast<int>(optionIndex);
|
||||
break;
|
||||
}
|
||||
}
|
||||
AppendStd140Int(buffer, selectedIndex);
|
||||
break;
|
||||
}
|
||||
case ShaderParameterType::Text:
|
||||
break;
|
||||
case ShaderParameterType::Trigger:
|
||||
AppendStd140Int(buffer, value.numberValues.empty() ? 0 : static_cast<int>(value.numberValues[0]));
|
||||
AppendStd140Float(buffer, value.numberValues.size() > 1 ? static_cast<float>(value.numberValues[1]) : -1000000.0f);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
buffer.resize(AlignStd140(buffer.size(), 16), 0);
|
||||
|
||||
glBindBuffer(GL_UNIFORM_BUFFER, mRenderer.GlobalParamsUBO());
|
||||
if (mRenderer.GlobalParamsUBOSize() != static_cast<GLsizeiptr>(buffer.size()))
|
||||
{
|
||||
glBufferData(GL_UNIFORM_BUFFER, static_cast<GLsizeiptr>(buffer.size()), buffer.data(), GL_DYNAMIC_DRAW);
|
||||
mRenderer.SetGlobalParamsUBOSize(static_cast<GLsizeiptr>(buffer.size()));
|
||||
}
|
||||
else
|
||||
{
|
||||
glBufferSubData(GL_UNIFORM_BUFFER, 0, static_cast<GLsizeiptr>(buffer.size()), buffer.data());
|
||||
}
|
||||
glBindBufferBase(GL_UNIFORM_BUFFER, kGlobalParamsBindingPoint, mRenderer.GlobalParamsUBO());
|
||||
glBindBuffer(GL_UNIFORM_BUFFER, 0);
|
||||
|
||||
return true;
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "OpenGLRenderer.h"
|
||||
#include "ShaderTypes.h"
|
||||
|
||||
#include <vector>
|
||||
|
||||
class GlobalParamsBuffer
|
||||
{
|
||||
public:
|
||||
explicit GlobalParamsBuffer(OpenGLRenderer& renderer);
|
||||
|
||||
bool Update(const RuntimeRenderState& state, unsigned availableSourceHistoryLength, unsigned availableTemporalHistoryLength, bool feedbackAvailable);
|
||||
|
||||
private:
|
||||
OpenGLRenderer& mRenderer;
|
||||
std::vector<unsigned char> mScratchBuffer;
|
||||
};
|
||||
@@ -1,204 +0,0 @@
|
||||
#include "OpenGLShaderPrograms.h"
|
||||
|
||||
#include <cstring>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace
|
||||
{
|
||||
void CopyErrorMessage(const std::string& message, int errorMessageSize, char* errorMessage)
|
||||
{
|
||||
if (!errorMessage || errorMessageSize <= 0)
|
||||
return;
|
||||
|
||||
strncpy_s(errorMessage, errorMessageSize, message.c_str(), _TRUNCATE);
|
||||
}
|
||||
|
||||
std::size_t RequiredTemporaryRenderTargets(const std::vector<OpenGLRenderer::LayerProgram>& layerPrograms)
|
||||
{
|
||||
// Only one layer renders at a time, so the pool needs to cover the widest
|
||||
// layer, not the sum of every intermediate pass in the stack.
|
||||
std::size_t requiredTargets = 0;
|
||||
for (const OpenGLRenderer::LayerProgram& layerProgram : layerPrograms)
|
||||
{
|
||||
const std::size_t internalPasses = layerProgram.passes.size() > 0 ? layerProgram.passes.size() - 1 : 0;
|
||||
if (internalPasses > requiredTargets)
|
||||
requiredTargets = internalPasses;
|
||||
}
|
||||
return requiredTargets;
|
||||
}
|
||||
}
|
||||
|
||||
OpenGLShaderPrograms::OpenGLShaderPrograms(OpenGLRenderer& renderer, RuntimeHost& runtimeHost) :
|
||||
mRenderer(renderer),
|
||||
mRuntimeHost(runtimeHost),
|
||||
mGlobalParamsBuffer(renderer),
|
||||
mCompiler(renderer, runtimeHost, mTextureBindings)
|
||||
{
|
||||
}
|
||||
|
||||
bool OpenGLShaderPrograms::CompileLayerPrograms(unsigned inputFrameWidth, unsigned inputFrameHeight, int errorMessageSize, char* errorMessage)
|
||||
{
|
||||
const std::vector<RuntimeRenderState> layerStates = mRuntimeHost.GetLayerRenderStates(inputFrameWidth, inputFrameHeight);
|
||||
std::string temporalError;
|
||||
const unsigned historyCap = mRuntimeHost.GetMaxTemporalHistoryFrames();
|
||||
if (!mRenderer.TemporalHistory().ValidateTextureUnitBudget(layerStates, historyCap, temporalError))
|
||||
{
|
||||
CopyErrorMessage(temporalError, errorMessageSize, errorMessage);
|
||||
return false;
|
||||
}
|
||||
if (!mRenderer.TemporalHistory().EnsureResources(layerStates, historyCap, inputFrameWidth, inputFrameHeight, temporalError))
|
||||
{
|
||||
CopyErrorMessage(temporalError, errorMessageSize, errorMessage);
|
||||
return false;
|
||||
}
|
||||
if (mRenderer.ResourcesInitialized() &&
|
||||
!mRenderer.FeedbackBuffers().EnsureResources(layerStates, inputFrameWidth, inputFrameHeight, temporalError))
|
||||
{
|
||||
CopyErrorMessage(temporalError, errorMessageSize, errorMessage);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Initial startup still compiles synchronously; auto-reload uses the build
|
||||
// queue so Slang/file work stays off the playback path.
|
||||
std::vector<LayerProgram> newPrograms;
|
||||
newPrograms.reserve(layerStates.size());
|
||||
|
||||
for (const RuntimeRenderState& state : layerStates)
|
||||
{
|
||||
LayerProgram layerProgram;
|
||||
if (!mCompiler.CompileLayerProgram(state, layerProgram, errorMessageSize, errorMessage))
|
||||
{
|
||||
for (LayerProgram& program : newPrograms)
|
||||
DestroySingleLayerProgram(program);
|
||||
return false;
|
||||
}
|
||||
newPrograms.push_back(layerProgram);
|
||||
}
|
||||
|
||||
std::string targetError;
|
||||
if (!mRenderer.ReserveTemporaryRenderTargets(RequiredTemporaryRenderTargets(newPrograms), inputFrameWidth, inputFrameHeight, targetError))
|
||||
{
|
||||
for (LayerProgram& program : newPrograms)
|
||||
DestroySingleLayerProgram(program);
|
||||
CopyErrorMessage(targetError, errorMessageSize, errorMessage);
|
||||
return false;
|
||||
}
|
||||
|
||||
DestroyLayerPrograms();
|
||||
mRenderer.ReplaceLayerPrograms(newPrograms);
|
||||
mCommittedLayerStates = layerStates;
|
||||
|
||||
mRuntimeHost.SetCompileStatus(true, "Shader layers compiled successfully.");
|
||||
mRuntimeHost.ClearReloadRequest();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool OpenGLShaderPrograms::CommitPreparedLayerPrograms(const PreparedShaderBuild& preparedBuild, unsigned inputFrameWidth, unsigned inputFrameHeight, int errorMessageSize, char* errorMessage)
|
||||
{
|
||||
if (!preparedBuild.succeeded)
|
||||
{
|
||||
CopyErrorMessage(preparedBuild.message, errorMessageSize, errorMessage);
|
||||
return false;
|
||||
}
|
||||
|
||||
std::string temporalError;
|
||||
const unsigned historyCap = mRuntimeHost.GetMaxTemporalHistoryFrames();
|
||||
if (!mRenderer.TemporalHistory().ValidateTextureUnitBudget(preparedBuild.layerStates, historyCap, temporalError))
|
||||
{
|
||||
CopyErrorMessage(temporalError, errorMessageSize, errorMessage);
|
||||
return false;
|
||||
}
|
||||
if (!mRenderer.TemporalHistory().EnsureResources(preparedBuild.layerStates, historyCap, inputFrameWidth, inputFrameHeight, temporalError))
|
||||
{
|
||||
CopyErrorMessage(temporalError, errorMessageSize, errorMessage);
|
||||
return false;
|
||||
}
|
||||
if (mRenderer.ResourcesInitialized() &&
|
||||
!mRenderer.FeedbackBuffers().EnsureResources(preparedBuild.layerStates, inputFrameWidth, inputFrameHeight, temporalError))
|
||||
{
|
||||
CopyErrorMessage(temporalError, errorMessageSize, errorMessage);
|
||||
return false;
|
||||
}
|
||||
|
||||
// The prepared build already contains GLSL text for each pass. This commit
|
||||
// step performs the short GL work on the render thread.
|
||||
std::vector<LayerProgram> newPrograms;
|
||||
newPrograms.reserve(preparedBuild.layers.size());
|
||||
|
||||
for (const PreparedLayerShader& preparedLayer : preparedBuild.layers)
|
||||
{
|
||||
LayerProgram layerProgram;
|
||||
if (!mCompiler.CompilePreparedLayerProgram(preparedLayer.state, preparedLayer.passes, layerProgram, errorMessageSize, errorMessage))
|
||||
{
|
||||
for (LayerProgram& program : newPrograms)
|
||||
DestroySingleLayerProgram(program);
|
||||
return false;
|
||||
}
|
||||
newPrograms.push_back(layerProgram);
|
||||
}
|
||||
|
||||
std::string targetError;
|
||||
if (!mRenderer.ReserveTemporaryRenderTargets(RequiredTemporaryRenderTargets(newPrograms), inputFrameWidth, inputFrameHeight, targetError))
|
||||
{
|
||||
for (LayerProgram& program : newPrograms)
|
||||
DestroySingleLayerProgram(program);
|
||||
CopyErrorMessage(targetError, errorMessageSize, errorMessage);
|
||||
return false;
|
||||
}
|
||||
|
||||
DestroyLayerPrograms();
|
||||
mRenderer.ReplaceLayerPrograms(newPrograms);
|
||||
mCommittedLayerStates = preparedBuild.layerStates;
|
||||
|
||||
mRuntimeHost.SetCompileStatus(true, "Shader layers compiled successfully.");
|
||||
mRuntimeHost.ClearReloadRequest();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool OpenGLShaderPrograms::CompileDecodeShader(int errorMessageSize, char* errorMessage)
|
||||
{
|
||||
return mCompiler.CompileDecodeShader(errorMessageSize, errorMessage);
|
||||
}
|
||||
|
||||
bool OpenGLShaderPrograms::CompileOutputPackShader(int errorMessageSize, char* errorMessage)
|
||||
{
|
||||
return mCompiler.CompileOutputPackShader(errorMessageSize, errorMessage);
|
||||
}
|
||||
|
||||
void OpenGLShaderPrograms::DestroySingleLayerProgram(LayerProgram& layerProgram)
|
||||
{
|
||||
mRenderer.DestroySingleLayerProgram(layerProgram);
|
||||
}
|
||||
|
||||
void OpenGLShaderPrograms::DestroyLayerPrograms()
|
||||
{
|
||||
mRenderer.DestroyLayerPrograms();
|
||||
}
|
||||
|
||||
void OpenGLShaderPrograms::DestroyDecodeShaderProgram()
|
||||
{
|
||||
mRenderer.DestroyDecodeShaderProgram();
|
||||
}
|
||||
|
||||
void OpenGLShaderPrograms::ResetTemporalHistoryState()
|
||||
{
|
||||
mRenderer.TemporalHistory().ResetState();
|
||||
}
|
||||
|
||||
void OpenGLShaderPrograms::ResetShaderFeedbackState()
|
||||
{
|
||||
mRenderer.FeedbackBuffers().ResetState();
|
||||
}
|
||||
|
||||
bool OpenGLShaderPrograms::UpdateTextBindingTexture(const RuntimeRenderState& state, LayerProgram::TextBinding& textBinding, std::string& error)
|
||||
{
|
||||
return mTextureBindings.UpdateTextBindingTexture(state, textBinding, error);
|
||||
}
|
||||
|
||||
bool OpenGLShaderPrograms::UpdateGlobalParamsBuffer(const RuntimeRenderState& state, unsigned availableSourceHistoryLength, unsigned availableTemporalHistoryLength, bool feedbackAvailable)
|
||||
{
|
||||
return mGlobalParamsBuffer.Update(state, availableSourceHistoryLength, availableTemporalHistoryLength, feedbackAvailable);
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "GlobalParamsBuffer.h"
|
||||
#include "OpenGLRenderer.h"
|
||||
#include "RuntimeHost.h"
|
||||
#include "ShaderBuildQueue.h"
|
||||
#include "ShaderTypes.h"
|
||||
#include "ShaderProgramCompiler.h"
|
||||
#include "ShaderTextureBindings.h"
|
||||
|
||||
#include <string>
|
||||
|
||||
class OpenGLShaderPrograms
|
||||
{
|
||||
public:
|
||||
using LayerProgram = OpenGLRenderer::LayerProgram;
|
||||
|
||||
OpenGLShaderPrograms(OpenGLRenderer& renderer, RuntimeHost& runtimeHost);
|
||||
|
||||
bool CompileLayerPrograms(unsigned inputFrameWidth, unsigned inputFrameHeight, int errorMessageSize, char* errorMessage);
|
||||
bool CommitPreparedLayerPrograms(const PreparedShaderBuild& preparedBuild, unsigned inputFrameWidth, unsigned inputFrameHeight, int errorMessageSize, char* errorMessage);
|
||||
bool CompileDecodeShader(int errorMessageSize, char* errorMessage);
|
||||
bool CompileOutputPackShader(int errorMessageSize, char* errorMessage);
|
||||
void DestroyLayerPrograms();
|
||||
void DestroySingleLayerProgram(LayerProgram& layerProgram);
|
||||
void DestroyDecodeShaderProgram();
|
||||
void ResetTemporalHistoryState();
|
||||
void ResetShaderFeedbackState();
|
||||
const std::vector<RuntimeRenderState>& CommittedLayerStates() const { return mCommittedLayerStates; }
|
||||
bool UpdateTextBindingTexture(const RuntimeRenderState& state, LayerProgram::TextBinding& textBinding, std::string& error);
|
||||
bool UpdateGlobalParamsBuffer(const RuntimeRenderState& state, unsigned availableSourceHistoryLength, unsigned availableTemporalHistoryLength, bool feedbackAvailable);
|
||||
|
||||
private:
|
||||
OpenGLRenderer& mRenderer;
|
||||
RuntimeHost& mRuntimeHost;
|
||||
ShaderTextureBindings mTextureBindings;
|
||||
GlobalParamsBuffer mGlobalParamsBuffer;
|
||||
ShaderProgramCompiler mCompiler;
|
||||
std::vector<RuntimeRenderState> mCommittedLayerStates;
|
||||
};
|
||||
@@ -1,134 +0,0 @@
|
||||
#include "ShaderBuildQueue.h"
|
||||
|
||||
#include "RuntimeHost.h"
|
||||
|
||||
#include <chrono>
|
||||
#include <utility>
|
||||
|
||||
namespace
|
||||
{
|
||||
constexpr auto kShaderBuildDebounce = std::chrono::milliseconds(400);
|
||||
}
|
||||
|
||||
ShaderBuildQueue::ShaderBuildQueue(RuntimeHost& runtimeHost) :
|
||||
mRuntimeHost(runtimeHost),
|
||||
mWorkerThread([this]() { WorkerLoop(); })
|
||||
{
|
||||
}
|
||||
|
||||
ShaderBuildQueue::~ShaderBuildQueue()
|
||||
{
|
||||
Stop();
|
||||
}
|
||||
|
||||
void ShaderBuildQueue::RequestBuild(unsigned outputWidth, unsigned outputHeight)
|
||||
{
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mMutex);
|
||||
mHasRequest = true;
|
||||
++mRequestedGeneration;
|
||||
mRequestedOutputWidth = outputWidth;
|
||||
mRequestedOutputHeight = outputHeight;
|
||||
mHasReadyBuild = false;
|
||||
}
|
||||
mCondition.notify_one();
|
||||
}
|
||||
|
||||
bool ShaderBuildQueue::TryConsumeReadyBuild(PreparedShaderBuild& build)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mMutex);
|
||||
if (!mHasReadyBuild)
|
||||
return false;
|
||||
|
||||
build = std::move(mReadyBuild);
|
||||
mReadyBuild = PreparedShaderBuild();
|
||||
mHasReadyBuild = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
void ShaderBuildQueue::Stop()
|
||||
{
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mMutex);
|
||||
if (mStopping)
|
||||
return;
|
||||
mStopping = true;
|
||||
}
|
||||
mCondition.notify_one();
|
||||
if (mWorkerThread.joinable())
|
||||
mWorkerThread.join();
|
||||
}
|
||||
|
||||
void ShaderBuildQueue::WorkerLoop()
|
||||
{
|
||||
for (;;)
|
||||
{
|
||||
uint64_t generation = 0;
|
||||
unsigned outputWidth = 0;
|
||||
unsigned outputHeight = 0;
|
||||
{
|
||||
std::unique_lock<std::mutex> lock(mMutex);
|
||||
mCondition.wait(lock, [this]() { return mStopping || mHasRequest; });
|
||||
if (mStopping)
|
||||
return;
|
||||
|
||||
generation = mRequestedGeneration;
|
||||
outputWidth = mRequestedOutputWidth;
|
||||
outputHeight = mRequestedOutputHeight;
|
||||
mHasRequest = false;
|
||||
}
|
||||
|
||||
for (;;)
|
||||
{
|
||||
std::unique_lock<std::mutex> lock(mMutex);
|
||||
if (mCondition.wait_for(lock, kShaderBuildDebounce, [this, generation]() {
|
||||
return mStopping || (mHasRequest && mRequestedGeneration != generation);
|
||||
}))
|
||||
{
|
||||
if (mStopping)
|
||||
return;
|
||||
|
||||
generation = mRequestedGeneration;
|
||||
outputWidth = mRequestedOutputWidth;
|
||||
outputHeight = mRequestedOutputHeight;
|
||||
mHasRequest = false;
|
||||
continue;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
PreparedShaderBuild build = Build(generation, outputWidth, outputHeight);
|
||||
|
||||
std::lock_guard<std::mutex> lock(mMutex);
|
||||
if (mStopping)
|
||||
return;
|
||||
if (generation != mRequestedGeneration)
|
||||
continue;
|
||||
mReadyBuild = std::move(build);
|
||||
mHasReadyBuild = true;
|
||||
}
|
||||
}
|
||||
|
||||
PreparedShaderBuild ShaderBuildQueue::Build(uint64_t generation, unsigned outputWidth, unsigned outputHeight)
|
||||
{
|
||||
PreparedShaderBuild build;
|
||||
build.generation = generation;
|
||||
build.layerStates = mRuntimeHost.GetLayerRenderStates(outputWidth, outputHeight);
|
||||
build.layers.reserve(build.layerStates.size());
|
||||
|
||||
for (const RuntimeRenderState& state : build.layerStates)
|
||||
{
|
||||
PreparedLayerShader layer;
|
||||
layer.state = state;
|
||||
if (!mRuntimeHost.BuildLayerPassFragmentShaderSources(state.layerId, layer.passes, build.message))
|
||||
{
|
||||
build.succeeded = false;
|
||||
return build;
|
||||
}
|
||||
build.layers.push_back(std::move(layer));
|
||||
}
|
||||
|
||||
build.succeeded = true;
|
||||
build.message = "Shader layers prepared successfully.";
|
||||
return build;
|
||||
}
|
||||
@@ -1,57 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "ShaderTypes.h"
|
||||
|
||||
#include <condition_variable>
|
||||
#include <cstdint>
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
#include <vector>
|
||||
|
||||
class RuntimeHost;
|
||||
|
||||
struct PreparedLayerShader
|
||||
{
|
||||
RuntimeRenderState state;
|
||||
std::vector<ShaderPassBuildSource> passes;
|
||||
};
|
||||
|
||||
struct PreparedShaderBuild
|
||||
{
|
||||
uint64_t generation = 0;
|
||||
bool succeeded = false;
|
||||
std::string message;
|
||||
std::vector<RuntimeRenderState> layerStates;
|
||||
std::vector<PreparedLayerShader> layers;
|
||||
};
|
||||
|
||||
class ShaderBuildQueue
|
||||
{
|
||||
public:
|
||||
explicit ShaderBuildQueue(RuntimeHost& runtimeHost);
|
||||
~ShaderBuildQueue();
|
||||
|
||||
ShaderBuildQueue(const ShaderBuildQueue&) = delete;
|
||||
ShaderBuildQueue& operator=(const ShaderBuildQueue&) = delete;
|
||||
|
||||
void RequestBuild(unsigned outputWidth, unsigned outputHeight);
|
||||
bool TryConsumeReadyBuild(PreparedShaderBuild& build);
|
||||
void Stop();
|
||||
|
||||
private:
|
||||
void WorkerLoop();
|
||||
PreparedShaderBuild Build(uint64_t generation, unsigned outputWidth, unsigned outputHeight);
|
||||
|
||||
RuntimeHost& mRuntimeHost;
|
||||
std::thread mWorkerThread;
|
||||
std::mutex mMutex;
|
||||
std::condition_variable mCondition;
|
||||
bool mStopping = false;
|
||||
bool mHasRequest = false;
|
||||
uint64_t mRequestedGeneration = 0;
|
||||
unsigned mRequestedOutputWidth = 0;
|
||||
unsigned mRequestedOutputHeight = 0;
|
||||
bool mHasReadyBuild = false;
|
||||
PreparedShaderBuild mReadyBuild;
|
||||
};
|
||||
@@ -1,233 +0,0 @@
|
||||
#include "ShaderProgramCompiler.h"
|
||||
|
||||
#include "GlRenderConstants.h"
|
||||
#include "GlScopedObjects.h"
|
||||
#include "GlShaderSources.h"
|
||||
|
||||
#include <cstring>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
namespace
|
||||
{
|
||||
void CopyErrorMessage(const std::string& message, int errorMessageSize, char* errorMessage)
|
||||
{
|
||||
if (!errorMessage || errorMessageSize <= 0)
|
||||
return;
|
||||
|
||||
strncpy_s(errorMessage, errorMessageSize, message.c_str(), _TRUNCATE);
|
||||
}
|
||||
}
|
||||
|
||||
ShaderProgramCompiler::ShaderProgramCompiler(OpenGLRenderer& renderer, RuntimeHost& runtimeHost, ShaderTextureBindings& textureBindings) :
|
||||
mRenderer(renderer),
|
||||
mRuntimeHost(runtimeHost),
|
||||
mTextureBindings(textureBindings)
|
||||
{
|
||||
}
|
||||
|
||||
bool ShaderProgramCompiler::CompileLayerProgram(const RuntimeRenderState& state, LayerProgram& layerProgram, int errorMessageSize, char* errorMessage)
|
||||
{
|
||||
std::vector<ShaderPassBuildSource> passSources;
|
||||
std::string loadError;
|
||||
|
||||
if (!mRuntimeHost.BuildLayerPassFragmentShaderSources(state.layerId, passSources, loadError))
|
||||
{
|
||||
CopyErrorMessage(loadError, errorMessageSize, errorMessage);
|
||||
return false;
|
||||
}
|
||||
|
||||
return CompilePreparedLayerProgram(state, passSources, layerProgram, errorMessageSize, errorMessage);
|
||||
}
|
||||
|
||||
bool ShaderProgramCompiler::CompilePreparedLayerProgram(const RuntimeRenderState& state, const std::vector<ShaderPassBuildSource>& passSources, LayerProgram& layerProgram, int errorMessageSize, char* errorMessage)
|
||||
{
|
||||
GLsizei errorBufferSize = 0;
|
||||
std::string loadError;
|
||||
const char* vertexSource = kFullscreenTriangleVertexShaderSource;
|
||||
|
||||
layerProgram.layerId = state.layerId;
|
||||
layerProgram.shaderId = state.shaderId;
|
||||
layerProgram.passes.clear();
|
||||
|
||||
for (const auto& passSource : passSources)
|
||||
{
|
||||
GLint compileResult = GL_FALSE;
|
||||
GLint linkResult = GL_FALSE;
|
||||
const char* fragmentSource = passSource.fragmentShaderSource.c_str();
|
||||
|
||||
ScopedGlShader newVertexShader(glCreateShader(GL_VERTEX_SHADER));
|
||||
glShaderSource(newVertexShader.get(), 1, (const GLchar**)&vertexSource, NULL);
|
||||
glCompileShader(newVertexShader.get());
|
||||
glGetShaderiv(newVertexShader.get(), GL_COMPILE_STATUS, &compileResult);
|
||||
if (compileResult == GL_FALSE)
|
||||
{
|
||||
glGetShaderInfoLog(newVertexShader.get(), errorMessageSize, &errorBufferSize, errorMessage);
|
||||
mRenderer.DestroySingleLayerProgram(layerProgram);
|
||||
return false;
|
||||
}
|
||||
|
||||
ScopedGlShader newFragmentShader(glCreateShader(GL_FRAGMENT_SHADER));
|
||||
glShaderSource(newFragmentShader.get(), 1, (const GLchar**)&fragmentSource, NULL);
|
||||
glCompileShader(newFragmentShader.get());
|
||||
glGetShaderiv(newFragmentShader.get(), GL_COMPILE_STATUS, &compileResult);
|
||||
if (compileResult == GL_FALSE)
|
||||
{
|
||||
glGetShaderInfoLog(newFragmentShader.get(), errorMessageSize, &errorBufferSize, errorMessage);
|
||||
mRenderer.DestroySingleLayerProgram(layerProgram);
|
||||
return false;
|
||||
}
|
||||
|
||||
ScopedGlProgram newProgram(glCreateProgram());
|
||||
glAttachShader(newProgram.get(), newVertexShader.get());
|
||||
glAttachShader(newProgram.get(), newFragmentShader.get());
|
||||
glLinkProgram(newProgram.get());
|
||||
glGetProgramiv(newProgram.get(), GL_LINK_STATUS, &linkResult);
|
||||
if (linkResult == GL_FALSE)
|
||||
{
|
||||
glGetProgramInfoLog(newProgram.get(), errorMessageSize, &errorBufferSize, errorMessage);
|
||||
mRenderer.DestroySingleLayerProgram(layerProgram);
|
||||
return false;
|
||||
}
|
||||
|
||||
std::vector<LayerProgram::TextureBinding> textureBindings;
|
||||
for (const ShaderTextureAsset& textureAsset : state.textureAssets)
|
||||
{
|
||||
LayerProgram::TextureBinding textureBinding;
|
||||
textureBinding.samplerName = textureAsset.id;
|
||||
textureBinding.sourcePath = textureAsset.path;
|
||||
if (!mTextureBindings.LoadTextureAsset(textureAsset, textureBinding.texture, loadError))
|
||||
{
|
||||
for (LayerProgram::TextureBinding& loadedTexture : textureBindings)
|
||||
{
|
||||
if (loadedTexture.texture != 0)
|
||||
glDeleteTextures(1, &loadedTexture.texture);
|
||||
}
|
||||
CopyErrorMessage(loadError, errorMessageSize, errorMessage);
|
||||
mRenderer.DestroySingleLayerProgram(layerProgram);
|
||||
return false;
|
||||
}
|
||||
textureBindings.push_back(textureBinding);
|
||||
}
|
||||
|
||||
std::vector<LayerProgram::TextBinding> textBindings;
|
||||
mTextureBindings.CreateTextBindings(state, textBindings);
|
||||
|
||||
PassProgram passProgram;
|
||||
passProgram.passId = passSource.passId;
|
||||
passProgram.inputNames = passSource.inputNames;
|
||||
passProgram.outputName = passSource.outputName;
|
||||
passProgram.shaderTextureBase = mTextureBindings.ResolveShaderTextureBase(state, mRuntimeHost.GetMaxTemporalHistoryFrames());
|
||||
passProgram.textureBindings.swap(textureBindings);
|
||||
passProgram.textBindings.swap(textBindings);
|
||||
|
||||
const GLuint globalParamsIndex = glGetUniformBlockIndex(newProgram.get(), "GlobalParams");
|
||||
if (globalParamsIndex != GL_INVALID_INDEX)
|
||||
glUniformBlockBinding(newProgram.get(), globalParamsIndex, kGlobalParamsBindingPoint);
|
||||
|
||||
const unsigned historyCap = mRuntimeHost.GetMaxTemporalHistoryFrames();
|
||||
glUseProgram(newProgram.get());
|
||||
mTextureBindings.AssignLayerSamplerUniforms(newProgram.get(), state, passProgram, historyCap);
|
||||
glUseProgram(0);
|
||||
|
||||
passProgram.program = newProgram.release();
|
||||
passProgram.vertexShader = newVertexShader.release();
|
||||
passProgram.fragmentShader = newFragmentShader.release();
|
||||
layerProgram.passes.push_back(std::move(passProgram));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ShaderProgramCompiler::CompileDecodeShader(int errorMessageSize, char* errorMessage)
|
||||
{
|
||||
GLsizei errorBufferSize = 0;
|
||||
GLint compileResult = GL_FALSE;
|
||||
GLint linkResult = GL_FALSE;
|
||||
const char* vertexSource = kFullscreenTriangleVertexShaderSource;
|
||||
const char* fragmentSource = kDecodeFragmentShaderSource;
|
||||
|
||||
ScopedGlShader newVertexShader(glCreateShader(GL_VERTEX_SHADER));
|
||||
glShaderSource(newVertexShader.get(), 1, (const GLchar**)&vertexSource, NULL);
|
||||
glCompileShader(newVertexShader.get());
|
||||
glGetShaderiv(newVertexShader.get(), GL_COMPILE_STATUS, &compileResult);
|
||||
if (compileResult == GL_FALSE)
|
||||
{
|
||||
glGetShaderInfoLog(newVertexShader.get(), errorMessageSize, &errorBufferSize, errorMessage);
|
||||
return false;
|
||||
}
|
||||
|
||||
ScopedGlShader newFragmentShader(glCreateShader(GL_FRAGMENT_SHADER));
|
||||
glShaderSource(newFragmentShader.get(), 1, (const GLchar**)&fragmentSource, NULL);
|
||||
glCompileShader(newFragmentShader.get());
|
||||
glGetShaderiv(newFragmentShader.get(), GL_COMPILE_STATUS, &compileResult);
|
||||
if (compileResult == GL_FALSE)
|
||||
{
|
||||
glGetShaderInfoLog(newFragmentShader.get(), errorMessageSize, &errorBufferSize, errorMessage);
|
||||
return false;
|
||||
}
|
||||
|
||||
ScopedGlProgram newProgram(glCreateProgram());
|
||||
glAttachShader(newProgram.get(), newVertexShader.get());
|
||||
glAttachShader(newProgram.get(), newFragmentShader.get());
|
||||
glLinkProgram(newProgram.get());
|
||||
glGetProgramiv(newProgram.get(), GL_LINK_STATUS, &linkResult);
|
||||
if (linkResult == GL_FALSE)
|
||||
{
|
||||
glGetProgramInfoLog(newProgram.get(), errorMessageSize, &errorBufferSize, errorMessage);
|
||||
return false;
|
||||
}
|
||||
|
||||
mRenderer.DestroyDecodeShaderProgram();
|
||||
mRenderer.SetDecodeShaderProgram(newProgram.release(), newVertexShader.release(), newFragmentShader.release());
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ShaderProgramCompiler::CompileOutputPackShader(int errorMessageSize, char* errorMessage)
|
||||
{
|
||||
GLsizei errorBufferSize = 0;
|
||||
GLint compileResult = GL_FALSE;
|
||||
GLint linkResult = GL_FALSE;
|
||||
const char* vertexSource = kFullscreenTriangleVertexShaderSource;
|
||||
const char* fragmentSource = kOutputPackFragmentShaderSource;
|
||||
|
||||
ScopedGlShader newVertexShader(glCreateShader(GL_VERTEX_SHADER));
|
||||
glShaderSource(newVertexShader.get(), 1, (const GLchar**)&vertexSource, NULL);
|
||||
glCompileShader(newVertexShader.get());
|
||||
glGetShaderiv(newVertexShader.get(), GL_COMPILE_STATUS, &compileResult);
|
||||
if (compileResult == GL_FALSE)
|
||||
{
|
||||
glGetShaderInfoLog(newVertexShader.get(), errorMessageSize, &errorBufferSize, errorMessage);
|
||||
return false;
|
||||
}
|
||||
|
||||
ScopedGlShader newFragmentShader(glCreateShader(GL_FRAGMENT_SHADER));
|
||||
glShaderSource(newFragmentShader.get(), 1, (const GLchar**)&fragmentSource, NULL);
|
||||
glCompileShader(newFragmentShader.get());
|
||||
glGetShaderiv(newFragmentShader.get(), GL_COMPILE_STATUS, &compileResult);
|
||||
if (compileResult == GL_FALSE)
|
||||
{
|
||||
glGetShaderInfoLog(newFragmentShader.get(), errorMessageSize, &errorBufferSize, errorMessage);
|
||||
return false;
|
||||
}
|
||||
|
||||
ScopedGlProgram newProgram(glCreateProgram());
|
||||
glAttachShader(newProgram.get(), newVertexShader.get());
|
||||
glAttachShader(newProgram.get(), newFragmentShader.get());
|
||||
glLinkProgram(newProgram.get());
|
||||
glGetProgramiv(newProgram.get(), GL_LINK_STATUS, &linkResult);
|
||||
if (linkResult == GL_FALSE)
|
||||
{
|
||||
glGetProgramInfoLog(newProgram.get(), errorMessageSize, &errorBufferSize, errorMessage);
|
||||
return false;
|
||||
}
|
||||
|
||||
glUseProgram(newProgram.get());
|
||||
const GLint outputSamplerLocation = glGetUniformLocation(newProgram.get(), "uOutputRgb");
|
||||
if (outputSamplerLocation >= 0)
|
||||
glUniform1i(outputSamplerLocation, 0);
|
||||
glUseProgram(0);
|
||||
|
||||
mRenderer.DestroyOutputPackShaderProgram();
|
||||
mRenderer.SetOutputPackShaderProgram(newProgram.release(), newVertexShader.release(), newFragmentShader.release());
|
||||
return true;
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "OpenGLRenderer.h"
|
||||
#include "RuntimeHost.h"
|
||||
#include "ShaderTextureBindings.h"
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
class ShaderProgramCompiler
|
||||
{
|
||||
public:
|
||||
using LayerProgram = OpenGLRenderer::LayerProgram;
|
||||
using PassProgram = OpenGLRenderer::LayerProgram::PassProgram;
|
||||
|
||||
ShaderProgramCompiler(OpenGLRenderer& renderer, RuntimeHost& runtimeHost, ShaderTextureBindings& textureBindings);
|
||||
|
||||
bool CompileLayerProgram(const RuntimeRenderState& state, LayerProgram& layerProgram, int errorMessageSize, char* errorMessage);
|
||||
bool CompilePreparedLayerProgram(const RuntimeRenderState& state, const std::vector<ShaderPassBuildSource>& passSources, LayerProgram& layerProgram, int errorMessageSize, char* errorMessage);
|
||||
bool CompileDecodeShader(int errorMessageSize, char* errorMessage);
|
||||
bool CompileOutputPackShader(int errorMessageSize, char* errorMessage);
|
||||
|
||||
private:
|
||||
OpenGLRenderer& mRenderer;
|
||||
RuntimeHost& mRuntimeHost;
|
||||
ShaderTextureBindings& mTextureBindings;
|
||||
};
|
||||
@@ -1,256 +0,0 @@
|
||||
#include "ShaderTextureBindings.h"
|
||||
|
||||
#include "GlRenderConstants.h"
|
||||
#include "TextRasterizer.h"
|
||||
#include "TextureAssetLoader.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <filesystem>
|
||||
|
||||
namespace
|
||||
{
|
||||
std::string TextValueForBinding(const RuntimeRenderState& state, const std::string& parameterId)
|
||||
{
|
||||
auto valueIt = state.parameterValues.find(parameterId);
|
||||
return valueIt == state.parameterValues.end() ? std::string() : valueIt->second.textValue;
|
||||
}
|
||||
|
||||
const ShaderFontAsset* FindFontAssetForParameter(const RuntimeRenderState& state, const ShaderParameterDefinition& definition)
|
||||
{
|
||||
if (!definition.fontId.empty())
|
||||
{
|
||||
for (const ShaderFontAsset& fontAsset : state.fontAssets)
|
||||
{
|
||||
if (fontAsset.id == definition.fontId)
|
||||
return &fontAsset;
|
||||
}
|
||||
}
|
||||
return state.fontAssets.empty() ? nullptr : &state.fontAssets.front();
|
||||
}
|
||||
}
|
||||
|
||||
bool ShaderTextureBindings::LoadTextureAsset(const ShaderTextureAsset& textureAsset, GLuint& textureId, std::string& error)
|
||||
{
|
||||
return ::LoadTextureAsset(textureAsset, textureId, error);
|
||||
}
|
||||
|
||||
void ShaderTextureBindings::CreateTextBindings(const RuntimeRenderState& state, std::vector<LayerProgram::TextBinding>& textBindings)
|
||||
{
|
||||
for (const ShaderParameterDefinition& definition : state.parameterDefinitions)
|
||||
{
|
||||
if (definition.type != ShaderParameterType::Text)
|
||||
continue;
|
||||
LayerProgram::TextBinding textBinding;
|
||||
textBinding.parameterId = definition.id;
|
||||
textBinding.samplerName = definition.id + "Texture";
|
||||
textBinding.fontId = definition.fontId;
|
||||
glGenTextures(1, &textBinding.texture);
|
||||
glBindTexture(GL_TEXTURE_2D, textBinding.texture);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
|
||||
std::vector<unsigned char> empty(static_cast<std::size_t>(kTextTextureWidth) * kTextTextureHeight * 4, 0);
|
||||
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, kTextTextureWidth, kTextTextureHeight, 0, GL_BGRA, GL_UNSIGNED_INT_8_8_8_8_REV, empty.data());
|
||||
glBindTexture(GL_TEXTURE_2D, 0);
|
||||
textBindings.push_back(textBinding);
|
||||
}
|
||||
}
|
||||
|
||||
bool ShaderTextureBindings::UpdateTextBindingTexture(const RuntimeRenderState& state, LayerProgram::TextBinding& textBinding, std::string& error)
|
||||
{
|
||||
const std::string text = TextValueForBinding(state, textBinding.parameterId);
|
||||
if (text == textBinding.renderedText && textBinding.renderedWidth == kTextTextureWidth && textBinding.renderedHeight == kTextTextureHeight)
|
||||
return true;
|
||||
|
||||
auto definitionIt = std::find_if(state.parameterDefinitions.begin(), state.parameterDefinitions.end(),
|
||||
[&textBinding](const ShaderParameterDefinition& definition) { return definition.id == textBinding.parameterId; });
|
||||
if (definitionIt == state.parameterDefinitions.end())
|
||||
return true;
|
||||
|
||||
const ShaderFontAsset* fontAsset = FindFontAssetForParameter(state, *definitionIt);
|
||||
std::filesystem::path fontPath;
|
||||
if (fontAsset)
|
||||
fontPath = fontAsset->path;
|
||||
|
||||
std::vector<unsigned char> sdf;
|
||||
if (!RasterizeTextSdf(text, fontPath, sdf, error))
|
||||
return false;
|
||||
|
||||
GLint previousActiveTexture = 0;
|
||||
GLint previousUnpackBuffer = 0;
|
||||
glGetIntegerv(GL_ACTIVE_TEXTURE, &previousActiveTexture);
|
||||
glGetIntegerv(GL_PIXEL_UNPACK_BUFFER_BINDING, &previousUnpackBuffer);
|
||||
glActiveTexture(GL_TEXTURE0);
|
||||
glBindBuffer(GL_PIXEL_UNPACK_BUFFER, 0);
|
||||
glBindTexture(GL_TEXTURE_2D, textBinding.texture);
|
||||
glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, kTextTextureWidth, kTextTextureHeight, GL_BGRA, GL_UNSIGNED_INT_8_8_8_8_REV, sdf.data());
|
||||
glBindTexture(GL_TEXTURE_2D, 0);
|
||||
glBindBuffer(GL_PIXEL_UNPACK_BUFFER, static_cast<GLuint>(previousUnpackBuffer));
|
||||
glActiveTexture(static_cast<GLenum>(previousActiveTexture));
|
||||
|
||||
textBinding.renderedText = text;
|
||||
textBinding.renderedWidth = kTextTextureWidth;
|
||||
textBinding.renderedHeight = kTextTextureHeight;
|
||||
return true;
|
||||
}
|
||||
|
||||
GLint ShaderTextureBindings::FindSamplerUniformLocation(GLuint program, const std::string& samplerName) const
|
||||
{
|
||||
GLint location = glGetUniformLocation(program, samplerName.c_str());
|
||||
if (location >= 0)
|
||||
return location;
|
||||
return glGetUniformLocation(program, (samplerName + "_0").c_str());
|
||||
}
|
||||
|
||||
GLuint ShaderTextureBindings::ResolveFeedbackTextureUnit(const RuntimeRenderState& state, unsigned historyCap) const
|
||||
{
|
||||
return state.isTemporal ? kSourceHistoryTextureUnitBase + historyCap + historyCap : kSourceHistoryTextureUnitBase;
|
||||
}
|
||||
|
||||
GLuint ShaderTextureBindings::ResolveShaderTextureBase(const RuntimeRenderState& state, unsigned historyCap) const
|
||||
{
|
||||
return ResolveFeedbackTextureUnit(state, historyCap) + (state.feedback.enabled ? 1u : 0u);
|
||||
}
|
||||
|
||||
void ShaderTextureBindings::AssignLayerSamplerUniforms(GLuint program, const RuntimeRenderState& state, const PassProgram& passProgram, unsigned historyCap) const
|
||||
{
|
||||
const GLuint shaderTextureBase = ResolveShaderTextureBase(state, historyCap);
|
||||
|
||||
const GLint layerInputLocation = FindSamplerUniformLocation(program, "gLayerInput");
|
||||
if (layerInputLocation >= 0)
|
||||
glUniform1i(layerInputLocation, static_cast<GLint>(kLayerInputTextureUnit));
|
||||
|
||||
const GLint videoInputLocation = FindSamplerUniformLocation(program, "gVideoInput");
|
||||
if (videoInputLocation >= 0)
|
||||
glUniform1i(videoInputLocation, static_cast<GLint>(kDecodedVideoTextureUnit));
|
||||
|
||||
for (unsigned index = 0; index < historyCap; ++index)
|
||||
{
|
||||
const std::string sourceSamplerName = "gSourceHistory" + std::to_string(index);
|
||||
const GLint sourceSamplerLocation = glGetUniformLocation(program, sourceSamplerName.c_str());
|
||||
if (sourceSamplerLocation >= 0)
|
||||
glUniform1i(sourceSamplerLocation, static_cast<GLint>(kSourceHistoryTextureUnitBase + index));
|
||||
|
||||
const std::string temporalSamplerName = "gTemporalHistory" + std::to_string(index);
|
||||
const GLint temporalSamplerLocation = glGetUniformLocation(program, temporalSamplerName.c_str());
|
||||
if (temporalSamplerLocation >= 0)
|
||||
glUniform1i(temporalSamplerLocation, static_cast<GLint>(kSourceHistoryTextureUnitBase + historyCap + index));
|
||||
}
|
||||
|
||||
if (state.feedback.enabled)
|
||||
{
|
||||
const GLint feedbackSamplerLocation = FindSamplerUniformLocation(program, "gFeedbackState");
|
||||
if (feedbackSamplerLocation >= 0)
|
||||
glUniform1i(feedbackSamplerLocation, static_cast<GLint>(ResolveFeedbackTextureUnit(state, historyCap)));
|
||||
}
|
||||
|
||||
for (std::size_t index = 0; index < passProgram.textureBindings.size(); ++index)
|
||||
{
|
||||
const GLint textureSamplerLocation = FindSamplerUniformLocation(program, passProgram.textureBindings[index].samplerName);
|
||||
if (textureSamplerLocation >= 0)
|
||||
glUniform1i(textureSamplerLocation, static_cast<GLint>(shaderTextureBase + static_cast<GLuint>(index)));
|
||||
}
|
||||
|
||||
const GLuint textTextureBase = shaderTextureBase + static_cast<GLuint>(passProgram.textureBindings.size());
|
||||
for (std::size_t index = 0; index < passProgram.textBindings.size(); ++index)
|
||||
{
|
||||
const GLint textSamplerLocation = FindSamplerUniformLocation(program, passProgram.textBindings[index].samplerName);
|
||||
if (textSamplerLocation >= 0)
|
||||
glUniform1i(textSamplerLocation, static_cast<GLint>(textTextureBase + static_cast<GLuint>(index)));
|
||||
}
|
||||
}
|
||||
|
||||
ShaderTextureBindings::RuntimeTextureBindingPlan ShaderTextureBindings::BuildLayerRuntimeBindingPlan(
|
||||
const PassProgram& passProgram,
|
||||
GLuint layerInputTexture,
|
||||
GLuint originalLayerInputTexture,
|
||||
const RuntimeRenderState& state,
|
||||
GLuint feedbackTexture,
|
||||
const std::vector<GLuint>& sourceHistoryTextures,
|
||||
const std::vector<GLuint>& temporalHistoryTextures) const
|
||||
{
|
||||
RuntimeTextureBindingPlan plan;
|
||||
plan.bindings.push_back({ "originalLayerInput", "gLayerInput", originalLayerInputTexture, kLayerInputTextureUnit });
|
||||
plan.bindings.push_back({ "layerInput", "gVideoInput", layerInputTexture, kDecodedVideoTextureUnit });
|
||||
|
||||
for (std::size_t index = 0; index < sourceHistoryTextures.size(); ++index)
|
||||
{
|
||||
plan.bindings.push_back({
|
||||
"sourceHistory",
|
||||
"gSourceHistory" + std::to_string(index),
|
||||
sourceHistoryTextures[index],
|
||||
kSourceHistoryTextureUnitBase + static_cast<GLuint>(index)
|
||||
});
|
||||
}
|
||||
|
||||
const GLuint temporalBase = kSourceHistoryTextureUnitBase + static_cast<GLuint>(sourceHistoryTextures.size());
|
||||
for (std::size_t index = 0; index < temporalHistoryTextures.size(); ++index)
|
||||
{
|
||||
plan.bindings.push_back({
|
||||
"temporalHistory",
|
||||
"gTemporalHistory" + std::to_string(index),
|
||||
temporalHistoryTextures[index],
|
||||
temporalBase + static_cast<GLuint>(index)
|
||||
});
|
||||
}
|
||||
|
||||
const GLuint feedbackTextureUnit = ResolveFeedbackTextureUnit(state, static_cast<unsigned>(sourceHistoryTextures.size()));
|
||||
if (state.feedback.enabled)
|
||||
{
|
||||
plan.bindings.push_back({
|
||||
"feedbackState",
|
||||
"gFeedbackState",
|
||||
feedbackTexture,
|
||||
feedbackTextureUnit
|
||||
});
|
||||
}
|
||||
|
||||
const GLuint shaderTextureBase = passProgram.shaderTextureBase != 0
|
||||
? passProgram.shaderTextureBase
|
||||
: feedbackTextureUnit + (state.feedback.enabled ? 1u : 0u);
|
||||
for (std::size_t index = 0; index < passProgram.textureBindings.size(); ++index)
|
||||
{
|
||||
const LayerProgram::TextureBinding& textureBinding = passProgram.textureBindings[index];
|
||||
plan.bindings.push_back({
|
||||
"shaderTexture",
|
||||
textureBinding.samplerName,
|
||||
textureBinding.texture,
|
||||
shaderTextureBase + static_cast<GLuint>(index)
|
||||
});
|
||||
}
|
||||
|
||||
const GLuint textTextureBase = shaderTextureBase + static_cast<GLuint>(passProgram.textureBindings.size());
|
||||
for (std::size_t index = 0; index < passProgram.textBindings.size(); ++index)
|
||||
{
|
||||
const LayerProgram::TextBinding& textBinding = passProgram.textBindings[index];
|
||||
plan.bindings.push_back({
|
||||
"textTexture",
|
||||
textBinding.samplerName,
|
||||
textBinding.texture,
|
||||
textTextureBase + static_cast<GLuint>(index)
|
||||
});
|
||||
}
|
||||
|
||||
return plan;
|
||||
}
|
||||
|
||||
void ShaderTextureBindings::BindRuntimeTexturePlan(const RuntimeTextureBindingPlan& plan) const
|
||||
{
|
||||
for (const RuntimeTextureBinding& binding : plan.bindings)
|
||||
{
|
||||
glActiveTexture(GL_TEXTURE0 + binding.textureUnit);
|
||||
glBindTexture(GL_TEXTURE_2D, binding.texture);
|
||||
}
|
||||
glActiveTexture(GL_TEXTURE0);
|
||||
}
|
||||
|
||||
void ShaderTextureBindings::UnbindRuntimeTexturePlan(const RuntimeTextureBindingPlan& plan) const
|
||||
{
|
||||
for (const RuntimeTextureBinding& binding : plan.bindings)
|
||||
{
|
||||
glActiveTexture(GL_TEXTURE0 + binding.textureUnit);
|
||||
glBindTexture(GL_TEXTURE_2D, 0);
|
||||
}
|
||||
glActiveTexture(GL_TEXTURE0);
|
||||
}
|
||||
@@ -1,45 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "OpenGLRenderer.h"
|
||||
#include "ShaderTypes.h"
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
class ShaderTextureBindings
|
||||
{
|
||||
public:
|
||||
using LayerProgram = OpenGLRenderer::LayerProgram;
|
||||
using PassProgram = OpenGLRenderer::LayerProgram::PassProgram;
|
||||
|
||||
struct RuntimeTextureBinding
|
||||
{
|
||||
std::string semanticName;
|
||||
std::string samplerName;
|
||||
GLuint texture = 0;
|
||||
GLuint textureUnit = 0;
|
||||
};
|
||||
|
||||
struct RuntimeTextureBindingPlan
|
||||
{
|
||||
std::vector<RuntimeTextureBinding> bindings;
|
||||
};
|
||||
|
||||
bool LoadTextureAsset(const ShaderTextureAsset& textureAsset, GLuint& textureId, std::string& error);
|
||||
void CreateTextBindings(const RuntimeRenderState& state, std::vector<LayerProgram::TextBinding>& textBindings);
|
||||
bool UpdateTextBindingTexture(const RuntimeRenderState& state, LayerProgram::TextBinding& textBinding, std::string& error);
|
||||
GLint FindSamplerUniformLocation(GLuint program, const std::string& samplerName) const;
|
||||
GLuint ResolveFeedbackTextureUnit(const RuntimeRenderState& state, unsigned historyCap) const;
|
||||
GLuint ResolveShaderTextureBase(const RuntimeRenderState& state, unsigned historyCap) const;
|
||||
void AssignLayerSamplerUniforms(GLuint program, const RuntimeRenderState& state, const PassProgram& passProgram, unsigned historyCap) const;
|
||||
RuntimeTextureBindingPlan BuildLayerRuntimeBindingPlan(
|
||||
const PassProgram& passProgram,
|
||||
GLuint layerInputTexture,
|
||||
GLuint originalLayerInputTexture,
|
||||
const RuntimeRenderState& state,
|
||||
GLuint feedbackTexture,
|
||||
const std::vector<GLuint>& sourceHistoryTextures,
|
||||
const std::vector<GLuint>& temporalHistoryTextures) const;
|
||||
void BindRuntimeTexturePlan(const RuntimeTextureBindingPlan& plan) const;
|
||||
void UnbindRuntimeTexturePlan(const RuntimeTextureBindingPlan& plan) const;
|
||||
};
|
||||
@@ -1,243 +0,0 @@
|
||||
#include "TextRasterizer.h"
|
||||
|
||||
#include <windows.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <cstring>
|
||||
#include <gdiplus.h>
|
||||
#include <memory>
|
||||
|
||||
namespace
|
||||
{
|
||||
constexpr int kTextSdfSpread = 20;
|
||||
constexpr float kTextFontPixelSize = 144.0f;
|
||||
constexpr float kTextLayoutPadding = 48.0f;
|
||||
constexpr float kSdfInfinity = 1.0e20f;
|
||||
|
||||
class GdiplusSession
|
||||
{
|
||||
public:
|
||||
GdiplusSession()
|
||||
{
|
||||
Gdiplus::GdiplusStartupInput startupInput;
|
||||
mStarted = Gdiplus::GdiplusStartup(&mToken, &startupInput, NULL) == Gdiplus::Ok;
|
||||
}
|
||||
|
||||
~GdiplusSession()
|
||||
{
|
||||
if (mStarted)
|
||||
Gdiplus::GdiplusShutdown(mToken);
|
||||
}
|
||||
|
||||
GdiplusSession(const GdiplusSession&) = delete;
|
||||
GdiplusSession& operator=(const GdiplusSession&) = delete;
|
||||
|
||||
bool started() const { return mStarted; }
|
||||
|
||||
private:
|
||||
ULONG_PTR mToken = 0;
|
||||
bool mStarted = false;
|
||||
};
|
||||
|
||||
std::wstring Utf8ToWide(const std::string& text)
|
||||
{
|
||||
if (text.empty())
|
||||
return std::wstring();
|
||||
const int required = MultiByteToWideChar(CP_UTF8, 0, text.c_str(), -1, NULL, 0);
|
||||
if (required <= 1)
|
||||
return std::wstring();
|
||||
std::wstring wide(static_cast<std::size_t>(required - 1), L'\0');
|
||||
MultiByteToWideChar(CP_UTF8, 0, text.c_str(), -1, wide.data(), required);
|
||||
return wide;
|
||||
}
|
||||
|
||||
void DistanceTransform1D(const std::vector<float>& input, std::vector<float>& output, unsigned count)
|
||||
{
|
||||
std::vector<unsigned> locations(count, 0);
|
||||
std::vector<float> boundaries(static_cast<std::size_t>(count) + 1, 0.0f);
|
||||
|
||||
unsigned segment = 0;
|
||||
locations[0] = 0;
|
||||
boundaries[0] = -kSdfInfinity;
|
||||
boundaries[1] = kSdfInfinity;
|
||||
|
||||
for (unsigned q = 1; q < count; ++q)
|
||||
{
|
||||
float intersection = 0.0f;
|
||||
for (;;)
|
||||
{
|
||||
const unsigned location = locations[segment];
|
||||
intersection =
|
||||
((input[q] + static_cast<float>(q * q)) - (input[location] + static_cast<float>(location * location))) /
|
||||
(2.0f * static_cast<float>(q) - 2.0f * static_cast<float>(location));
|
||||
if (intersection > boundaries[segment] || segment == 0)
|
||||
break;
|
||||
--segment;
|
||||
}
|
||||
|
||||
++segment;
|
||||
locations[segment] = q;
|
||||
boundaries[segment] = intersection;
|
||||
boundaries[segment + 1] = kSdfInfinity;
|
||||
}
|
||||
|
||||
segment = 0;
|
||||
for (unsigned q = 0; q < count; ++q)
|
||||
{
|
||||
while (boundaries[segment + 1] < static_cast<float>(q))
|
||||
++segment;
|
||||
const unsigned location = locations[segment];
|
||||
const float delta = static_cast<float>(q) - static_cast<float>(location);
|
||||
output[q] = delta * delta + input[location];
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<float> DistanceTransform2D(const std::vector<unsigned char>& targetMask, unsigned width, unsigned height)
|
||||
{
|
||||
std::vector<float> rowInput(width, 0.0f);
|
||||
std::vector<float> rowOutput(width, 0.0f);
|
||||
std::vector<float> columnInput(height, 0.0f);
|
||||
std::vector<float> columnOutput(height, 0.0f);
|
||||
std::vector<float> rowDistance(static_cast<std::size_t>(width) * height, 0.0f);
|
||||
std::vector<float> distance(static_cast<std::size_t>(width) * height, 0.0f);
|
||||
|
||||
for (unsigned y = 0; y < height; ++y)
|
||||
{
|
||||
for (unsigned x = 0; x < width; ++x)
|
||||
rowInput[x] = targetMask[static_cast<std::size_t>(y) * width + x] ? 0.0f : kSdfInfinity;
|
||||
DistanceTransform1D(rowInput, rowOutput, width);
|
||||
for (unsigned x = 0; x < width; ++x)
|
||||
rowDistance[static_cast<std::size_t>(y) * width + x] = rowOutput[x];
|
||||
}
|
||||
|
||||
for (unsigned x = 0; x < width; ++x)
|
||||
{
|
||||
for (unsigned y = 0; y < height; ++y)
|
||||
columnInput[y] = rowDistance[static_cast<std::size_t>(y) * width + x];
|
||||
DistanceTransform1D(columnInput, columnOutput, height);
|
||||
for (unsigned y = 0; y < height; ++y)
|
||||
distance[static_cast<std::size_t>(y) * width + x] = columnOutput[y];
|
||||
}
|
||||
|
||||
return distance;
|
||||
}
|
||||
|
||||
std::vector<unsigned char> BuildTextSdfTexture(const std::vector<unsigned char>& alpha, unsigned width, unsigned height)
|
||||
{
|
||||
std::vector<unsigned char> insideMask(static_cast<std::size_t>(width) * height, 0);
|
||||
std::vector<unsigned char> outsideMask(static_cast<std::size_t>(width) * height, 0);
|
||||
for (std::size_t index = 0; index < alpha.size(); ++index)
|
||||
{
|
||||
const bool inside = alpha[index] > 127;
|
||||
insideMask[index] = inside ? 1 : 0;
|
||||
outsideMask[index] = inside ? 0 : 1;
|
||||
}
|
||||
|
||||
const std::vector<float> distanceToInside = DistanceTransform2D(insideMask, width, height);
|
||||
const std::vector<float> distanceToOutside = DistanceTransform2D(outsideMask, width, height);
|
||||
std::vector<unsigned char> sdf(static_cast<std::size_t>(width) * height * 4, 0);
|
||||
|
||||
for (unsigned y = 0; y < height; ++y)
|
||||
{
|
||||
const unsigned flippedY = height - 1 - y;
|
||||
for (unsigned x = 0; x < width; ++x)
|
||||
{
|
||||
const std::size_t source = static_cast<std::size_t>(y) * width + x;
|
||||
const float signedDistance = std::sqrt(distanceToOutside[source]) - std::sqrt(distanceToInside[source]);
|
||||
const float normalized = std::clamp(
|
||||
0.5f + signedDistance / static_cast<float>(kTextSdfSpread * 2),
|
||||
0.0f,
|
||||
1.0f);
|
||||
const unsigned char value = static_cast<unsigned char>(normalized * 255.0f + 0.5f);
|
||||
const std::size_t out = (static_cast<std::size_t>(flippedY) * width + x) * 4;
|
||||
sdf[out + 0] = value;
|
||||
sdf[out + 1] = value;
|
||||
sdf[out + 2] = value;
|
||||
sdf[out + 3] = value;
|
||||
}
|
||||
}
|
||||
|
||||
return sdf;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
bool RasterizeTextSdf(const std::string& text, const std::filesystem::path& fontPath, std::vector<unsigned char>& sdf, std::string& error)
|
||||
{
|
||||
GdiplusSession gdiplus;
|
||||
if (!gdiplus.started())
|
||||
{
|
||||
error = "Could not start GDI+ for text rendering.";
|
||||
return false;
|
||||
}
|
||||
|
||||
Gdiplus::PrivateFontCollection fontCollection;
|
||||
Gdiplus::FontFamily fallbackFamily(L"Arial");
|
||||
Gdiplus::FontFamily* fontFamily = &fallbackFamily;
|
||||
std::unique_ptr<Gdiplus::FontFamily[]> families;
|
||||
const std::wstring wideFontPath = fontPath.empty() ? std::wstring() : fontPath.wstring();
|
||||
if (!wideFontPath.empty())
|
||||
{
|
||||
if (fontCollection.AddFontFile(wideFontPath.c_str()) != Gdiplus::Ok)
|
||||
{
|
||||
error = "Could not load packaged font file for text rendering: " + fontPath.string();
|
||||
return false;
|
||||
}
|
||||
|
||||
const INT familyCount = fontCollection.GetFamilyCount();
|
||||
if (familyCount <= 0)
|
||||
{
|
||||
error = "Packaged font did not contain a usable font family: " + fontPath.string();
|
||||
return false;
|
||||
}
|
||||
|
||||
families.reset(new Gdiplus::FontFamily[familyCount]);
|
||||
INT found = 0;
|
||||
if (fontCollection.GetFamilies(familyCount, families.get(), &found) != Gdiplus::Ok || found <= 0)
|
||||
{
|
||||
error = "Could not read the packaged font family: " + fontPath.string();
|
||||
return false;
|
||||
}
|
||||
fontFamily = &families[0];
|
||||
}
|
||||
|
||||
Gdiplus::Bitmap bitmap(kTextTextureWidth, kTextTextureHeight, PixelFormat32bppARGB);
|
||||
Gdiplus::Graphics graphics(&bitmap);
|
||||
graphics.SetCompositingMode(Gdiplus::CompositingModeSourceCopy);
|
||||
graphics.Clear(Gdiplus::Color(255, 0, 0, 0));
|
||||
graphics.SetCompositingMode(Gdiplus::CompositingModeSourceOver);
|
||||
graphics.SetTextRenderingHint(Gdiplus::TextRenderingHintAntiAlias);
|
||||
graphics.SetSmoothingMode(Gdiplus::SmoothingModeHighQuality);
|
||||
Gdiplus::Font font(fontFamily, kTextFontPixelSize, Gdiplus::FontStyleRegular, Gdiplus::UnitPixel);
|
||||
Gdiplus::SolidBrush brush(Gdiplus::Color(255, 255, 255, 255));
|
||||
Gdiplus::StringFormat format;
|
||||
format.SetAlignment(Gdiplus::StringAlignmentNear);
|
||||
format.SetLineAlignment(Gdiplus::StringAlignmentCenter);
|
||||
format.SetFormatFlags(Gdiplus::StringFormatFlagsNoWrap | Gdiplus::StringFormatFlagsMeasureTrailingSpaces);
|
||||
const Gdiplus::RectF layout(
|
||||
kTextLayoutPadding,
|
||||
0.0f,
|
||||
static_cast<Gdiplus::REAL>(kTextTextureWidth) - (kTextLayoutPadding * 2.0f),
|
||||
static_cast<Gdiplus::REAL>(kTextTextureHeight));
|
||||
const std::wstring wideText = Utf8ToWide(text);
|
||||
graphics.DrawString(wideText.c_str(), -1, &font, layout, &format, &brush);
|
||||
|
||||
std::vector<unsigned char> alpha(static_cast<std::size_t>(kTextTextureWidth) * kTextTextureHeight, 0);
|
||||
for (unsigned y = 0; y < kTextTextureHeight; ++y)
|
||||
{
|
||||
for (unsigned x = 0; x < kTextTextureWidth; ++x)
|
||||
{
|
||||
Gdiplus::Color pixel;
|
||||
bitmap.GetPixel(x, y, &pixel);
|
||||
BYTE luminance = pixel.GetRed();
|
||||
if (pixel.GetGreen() > luminance)
|
||||
luminance = pixel.GetGreen();
|
||||
if (pixel.GetBlue() > luminance)
|
||||
luminance = pixel.GetBlue();
|
||||
alpha[static_cast<std::size_t>(y) * kTextTextureWidth + x] = static_cast<unsigned char>(luminance);
|
||||
}
|
||||
}
|
||||
sdf = BuildTextSdfTexture(alpha, kTextTextureWidth, kTextTextureHeight);
|
||||
return true;
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <filesystem>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
constexpr unsigned kTextTextureWidth = 4096;
|
||||
constexpr unsigned kTextTextureHeight = 512;
|
||||
|
||||
bool RasterizeTextSdf(const std::string& text, const std::filesystem::path& fontPath, std::vector<unsigned char>& sdf, std::string& error);
|
||||
@@ -1,222 +0,0 @@
|
||||
#include "TextureAssetLoader.h"
|
||||
|
||||
#include <windows.h>
|
||||
#include <wincodec.h>
|
||||
|
||||
#include <atlbase.h>
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
#include <cstring>
|
||||
#include <fstream>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#ifndef GL_RGBA32F
|
||||
#define GL_RGBA32F 0x8814
|
||||
#endif
|
||||
|
||||
namespace
|
||||
{
|
||||
std::string LowercaseExtension(const std::filesystem::path& path)
|
||||
{
|
||||
std::string extension = path.extension().string();
|
||||
std::transform(extension.begin(), extension.end(), extension.begin(),
|
||||
[](unsigned char value) { return static_cast<char>(std::tolower(value)); });
|
||||
return extension;
|
||||
}
|
||||
|
||||
bool LoadCubeTextureAsset(const ShaderTextureAsset& textureAsset, GLuint& textureId, std::string& error)
|
||||
{
|
||||
std::ifstream file(textureAsset.path);
|
||||
if (!file)
|
||||
{
|
||||
error = "Could not open shader LUT asset: " + textureAsset.path.string();
|
||||
return false;
|
||||
}
|
||||
|
||||
unsigned lutSize = 0;
|
||||
std::vector<float> values;
|
||||
std::string line;
|
||||
while (std::getline(file, line))
|
||||
{
|
||||
const std::size_t commentStart = line.find('#');
|
||||
if (commentStart != std::string::npos)
|
||||
line.resize(commentStart);
|
||||
|
||||
std::istringstream stream(line);
|
||||
std::string firstToken;
|
||||
if (!(stream >> firstToken))
|
||||
continue;
|
||||
|
||||
if (firstToken == "TITLE" || firstToken == "DOMAIN_MIN" || firstToken == "DOMAIN_MAX")
|
||||
continue;
|
||||
if (firstToken == "LUT_3D_SIZE")
|
||||
{
|
||||
stream >> lutSize;
|
||||
continue;
|
||||
}
|
||||
if (firstToken == "LUT_1D_SIZE")
|
||||
{
|
||||
error = "Only 3D .cube LUT assets are supported: " + textureAsset.path.string();
|
||||
return false;
|
||||
}
|
||||
|
||||
float red = 0.0f;
|
||||
float green = 0.0f;
|
||||
float blue = 0.0f;
|
||||
try
|
||||
{
|
||||
red = std::stof(firstToken);
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
error = "Unsupported .cube directive in shader LUT asset: " + firstToken;
|
||||
return false;
|
||||
}
|
||||
if (!(stream >> green >> blue))
|
||||
{
|
||||
error = "Malformed RGB entry in shader LUT asset: " + textureAsset.path.string();
|
||||
return false;
|
||||
}
|
||||
values.push_back(red);
|
||||
values.push_back(green);
|
||||
values.push_back(blue);
|
||||
values.push_back(1.0f);
|
||||
}
|
||||
|
||||
if (lutSize == 0)
|
||||
{
|
||||
error = "Shader LUT asset is missing LUT_3D_SIZE: " + textureAsset.path.string();
|
||||
return false;
|
||||
}
|
||||
|
||||
const std::size_t expectedFloats = static_cast<std::size_t>(lutSize) * lutSize * lutSize * 4;
|
||||
if (values.size() != expectedFloats)
|
||||
{
|
||||
error = "Shader LUT asset entry count does not match LUT_3D_SIZE: " + textureAsset.path.string();
|
||||
return false;
|
||||
}
|
||||
|
||||
const GLsizei atlasWidth = static_cast<GLsizei>(lutSize * lutSize);
|
||||
const GLsizei atlasHeight = static_cast<GLsizei>(lutSize);
|
||||
glGenTextures(1, &textureId);
|
||||
glBindTexture(GL_TEXTURE_2D, textureId);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
|
||||
glPixelStorei(GL_UNPACK_ALIGNMENT, 4);
|
||||
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA32F, atlasWidth, atlasHeight, 0, GL_RGBA, GL_FLOAT, values.data());
|
||||
glBindTexture(GL_TEXTURE_2D, 0);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
bool LoadTextureAsset(const ShaderTextureAsset& textureAsset, GLuint& textureId, std::string& error)
|
||||
{
|
||||
textureId = 0;
|
||||
if (LowercaseExtension(textureAsset.path) == ".cube")
|
||||
return LoadCubeTextureAsset(textureAsset, textureId, error);
|
||||
|
||||
HRESULT comInitResult = CoInitializeEx(NULL, COINIT_MULTITHREADED);
|
||||
const bool shouldUninitializeCom = (comInitResult == S_OK || comInitResult == S_FALSE);
|
||||
if (FAILED(comInitResult) && comInitResult != RPC_E_CHANGED_MODE)
|
||||
{
|
||||
error = "Could not initialize COM to load shader texture assets.";
|
||||
return false;
|
||||
}
|
||||
|
||||
CComPtr<IWICImagingFactory> imagingFactory;
|
||||
HRESULT result = CoCreateInstance(CLSID_WICImagingFactory, NULL, CLSCTX_INPROC_SERVER, IID_PPV_ARGS(&imagingFactory));
|
||||
if (FAILED(result) || !imagingFactory)
|
||||
{
|
||||
if (shouldUninitializeCom)
|
||||
CoUninitialize();
|
||||
error = "Could not create a WIC imaging factory to load shader texture assets.";
|
||||
return false;
|
||||
}
|
||||
|
||||
CComPtr<IWICBitmapDecoder> bitmapDecoder;
|
||||
result = imagingFactory->CreateDecoderFromFilename(textureAsset.path.wstring().c_str(), NULL, GENERIC_READ, WICDecodeMetadataCacheOnLoad, &bitmapDecoder);
|
||||
if (FAILED(result) || !bitmapDecoder)
|
||||
{
|
||||
if (shouldUninitializeCom)
|
||||
CoUninitialize();
|
||||
error = "Could not open shader texture asset: " + textureAsset.path.string();
|
||||
return false;
|
||||
}
|
||||
|
||||
CComPtr<IWICBitmapFrameDecode> bitmapFrame;
|
||||
result = bitmapDecoder->GetFrame(0, &bitmapFrame);
|
||||
if (FAILED(result) || !bitmapFrame)
|
||||
{
|
||||
if (shouldUninitializeCom)
|
||||
CoUninitialize();
|
||||
error = "Could not decode the first frame of shader texture asset: " + textureAsset.path.string();
|
||||
return false;
|
||||
}
|
||||
|
||||
CComPtr<IWICFormatConverter> formatConverter;
|
||||
result = imagingFactory->CreateFormatConverter(&formatConverter);
|
||||
if (FAILED(result) || !formatConverter)
|
||||
{
|
||||
if (shouldUninitializeCom)
|
||||
CoUninitialize();
|
||||
error = "Could not create a WIC format converter for shader texture asset: " + textureAsset.path.string();
|
||||
return false;
|
||||
}
|
||||
|
||||
result = formatConverter->Initialize(bitmapFrame, GUID_WICPixelFormat32bppBGRA, WICBitmapDitherTypeNone, NULL, 0.0, WICBitmapPaletteTypeCustom);
|
||||
if (FAILED(result))
|
||||
{
|
||||
if (shouldUninitializeCom)
|
||||
CoUninitialize();
|
||||
error = "Could not convert shader texture asset to BGRA: " + textureAsset.path.string();
|
||||
return false;
|
||||
}
|
||||
|
||||
UINT width = 0;
|
||||
UINT height = 0;
|
||||
result = formatConverter->GetSize(&width, &height);
|
||||
if (FAILED(result) || width == 0 || height == 0)
|
||||
{
|
||||
if (shouldUninitializeCom)
|
||||
CoUninitialize();
|
||||
error = "Shader texture asset has an invalid size: " + textureAsset.path.string();
|
||||
return false;
|
||||
}
|
||||
|
||||
const UINT stride = width * 4;
|
||||
std::vector<unsigned char> pixels(static_cast<std::size_t>(stride) * static_cast<std::size_t>(height));
|
||||
result = formatConverter->CopyPixels(NULL, stride, static_cast<UINT>(pixels.size()), pixels.data());
|
||||
if (FAILED(result))
|
||||
{
|
||||
if (shouldUninitializeCom)
|
||||
CoUninitialize();
|
||||
error = "Could not read shader texture pixels: " + textureAsset.path.string();
|
||||
return false;
|
||||
}
|
||||
|
||||
std::vector<unsigned char> flippedPixels(pixels.size());
|
||||
for (UINT row = 0; row < height; ++row)
|
||||
{
|
||||
const std::size_t srcOffset = static_cast<std::size_t>(row) * stride;
|
||||
const std::size_t dstOffset = static_cast<std::size_t>(height - 1 - row) * stride;
|
||||
std::memcpy(flippedPixels.data() + dstOffset, pixels.data() + srcOffset, stride);
|
||||
}
|
||||
|
||||
glGenTextures(1, &textureId);
|
||||
glBindTexture(GL_TEXTURE_2D, textureId);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
|
||||
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, static_cast<GLsizei>(width), static_cast<GLsizei>(height), 0, GL_BGRA, GL_UNSIGNED_BYTE, flippedPixels.data());
|
||||
glBindTexture(GL_TEXTURE_2D, 0);
|
||||
|
||||
if (shouldUninitializeCom)
|
||||
CoUninitialize();
|
||||
|
||||
return true;
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "GLExtensions.h"
|
||||
#include "ShaderTypes.h"
|
||||
|
||||
#include <windows.h>
|
||||
#include <gl/gl.h>
|
||||
|
||||
#include <string>
|
||||
|
||||
bool LoadTextureAsset(const ShaderTextureAsset& textureAsset, GLuint& textureId, std::string& error);
|
||||
@@ -1,24 +0,0 @@
|
||||
//{{NO_DEPENDENCIES}}
|
||||
// Microsoft Visual C++ generated include file.
|
||||
// Used by LoopThroughWithOpenGLCompositing.rc
|
||||
//
|
||||
#define IDC_MYICON 2
|
||||
#define IDD_OPENGLOUTPUT_DIALOG 102
|
||||
#define IDS_APP_TITLE 103
|
||||
#define IDI_OPENGLOUTPUT 107
|
||||
#define IDI_SMALL 108
|
||||
#define IDC_OPENGLOUTPUT 109
|
||||
#define IDR_MAINFRAME 128
|
||||
#define IDC_STATIC -1
|
||||
|
||||
// Next default values for new objects
|
||||
//
|
||||
#ifdef APSTUDIO_INVOKED
|
||||
#ifndef APSTUDIO_READONLY_SYMBOLS
|
||||
#define _APS_NO_MFC 1
|
||||
#define _APS_NEXT_RESOURCE_VALUE 129
|
||||
#define _APS_NEXT_COMMAND_VALUE 32771
|
||||
#define _APS_NEXT_CONTROL_VALUE 1000
|
||||
#define _APS_NEXT_SYMED_VALUE 110
|
||||
#endif
|
||||
#endif
|
||||
@@ -1,45 +0,0 @@
|
||||
#include "RuntimeClock.h"
|
||||
|
||||
#include <chrono>
|
||||
|
||||
namespace
|
||||
{
|
||||
bool ToUtcTime(std::time_t time, std::tm& utcTime)
|
||||
{
|
||||
return gmtime_s(&utcTime, &time) == 0;
|
||||
}
|
||||
|
||||
bool ToLocalTime(std::time_t time, std::tm& localTime)
|
||||
{
|
||||
return localtime_s(&localTime, &time) == 0;
|
||||
}
|
||||
}
|
||||
|
||||
RuntimeClockSnapshot GetRuntimeClockSnapshot()
|
||||
{
|
||||
return MakeRuntimeClockSnapshot(std::chrono::system_clock::to_time_t(std::chrono::system_clock::now()));
|
||||
}
|
||||
|
||||
RuntimeClockSnapshot MakeRuntimeClockSnapshot(std::time_t now)
|
||||
{
|
||||
RuntimeClockSnapshot snapshot;
|
||||
|
||||
std::tm utcTime = {};
|
||||
if (!ToUtcTime(now, utcTime))
|
||||
return snapshot;
|
||||
|
||||
snapshot.utcTimeSeconds =
|
||||
static_cast<double>(utcTime.tm_hour * 3600 + utcTime.tm_min * 60 + utcTime.tm_sec);
|
||||
|
||||
std::tm localTime = {};
|
||||
if (!ToLocalTime(now, localTime))
|
||||
return snapshot;
|
||||
|
||||
utcTime.tm_isdst = localTime.tm_isdst;
|
||||
const std::time_t localAsTime = std::mktime(&localTime);
|
||||
const std::time_t utcAsLocalTime = std::mktime(&utcTime);
|
||||
if (localAsTime != static_cast<std::time_t>(-1) && utcAsLocalTime != static_cast<std::time_t>(-1))
|
||||
snapshot.utcOffsetSeconds = std::difftime(localAsTime, utcAsLocalTime);
|
||||
|
||||
return snapshot;
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <ctime>
|
||||
|
||||
struct RuntimeClockSnapshot
|
||||
{
|
||||
double utcTimeSeconds = 0.0;
|
||||
double utcOffsetSeconds = 0.0;
|
||||
};
|
||||
|
||||
RuntimeClockSnapshot GetRuntimeClockSnapshot();
|
||||
RuntimeClockSnapshot MakeRuntimeClockSnapshot(std::time_t now);
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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;
|
||||
};
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 23 KiB |
@@ -1,46 +0,0 @@
|
||||
/* -LICENSE-START-
|
||||
** Copyright (c) 2012 Blackmagic Design
|
||||
**
|
||||
** Permission is hereby granted, free of charge, to any person or organization
|
||||
** obtaining a copy of the software and accompanying documentation (the
|
||||
** "Software") to use, reproduce, display, distribute, sub-license, execute,
|
||||
** and transmit the Software, and to prepare derivative works of the Software,
|
||||
** and to permit third-parties to whom the Software is furnished to do so, in
|
||||
** accordance with:
|
||||
**
|
||||
** (1) if the Software is obtained from Blackmagic Design, the End User License
|
||||
** Agreement for the Software Development Kit ("EULA") available at
|
||||
** https://www.blackmagicdesign.com/EULA/DeckLinkSDK; or
|
||||
**
|
||||
** (2) if the Software is obtained from any third party, such licensing terms
|
||||
** as notified by that third party,
|
||||
**
|
||||
** and all subject to the following:
|
||||
**
|
||||
** (3) the copyright notices in the Software and this entire statement,
|
||||
** including the above license grant, this restriction and the following
|
||||
** disclaimer, must be included in all copies of the Software, in whole or in
|
||||
** part, and all derivative works of the Software, unless such copies or
|
||||
** derivative works are solely in the form of machine-executable object code
|
||||
** generated by a source language processor.
|
||||
**
|
||||
** (4) THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
||||
** OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
** FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT
|
||||
** SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE
|
||||
** FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE,
|
||||
** ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
** DEALINGS IN THE SOFTWARE.
|
||||
**
|
||||
** A copy of the Software is available free of charge at
|
||||
** https://www.blackmagicdesign.com/desktopvideo_sdk under the EULA.
|
||||
**
|
||||
** -LICENSE-END-
|
||||
*/
|
||||
//
|
||||
// stdafx.cpp : source file that includes just the standard includes
|
||||
// LoopThroughWithOpenGLCompositing.pch will be the pre-compiled header
|
||||
// stdafx.obj will contain the pre-compiled type information
|
||||
|
||||
#include "stdafx.h"
|
||||
|
||||
@@ -1,65 +0,0 @@
|
||||
/* -LICENSE-START-
|
||||
** Copyright (c) 2012 Blackmagic Design
|
||||
**
|
||||
** Permission is hereby granted, free of charge, to any person or organization
|
||||
** obtaining a copy of the software and accompanying documentation (the
|
||||
** "Software") to use, reproduce, display, distribute, sub-license, execute,
|
||||
** and transmit the Software, and to prepare derivative works of the Software,
|
||||
** and to permit third-parties to whom the Software is furnished to do so, in
|
||||
** accordance with:
|
||||
**
|
||||
** (1) if the Software is obtained from Blackmagic Design, the End User License
|
||||
** Agreement for the Software Development Kit ("EULA") available at
|
||||
** https://www.blackmagicdesign.com/EULA/DeckLinkSDK; or
|
||||
**
|
||||
** (2) if the Software is obtained from any third party, such licensing terms
|
||||
** as notified by that third party,
|
||||
**
|
||||
** and all subject to the following:
|
||||
**
|
||||
** (3) the copyright notices in the Software and this entire statement,
|
||||
** including the above license grant, this restriction and the following
|
||||
** disclaimer, must be included in all copies of the Software, in whole or in
|
||||
** part, and all derivative works of the Software, unless such copies or
|
||||
** derivative works are solely in the form of machine-executable object code
|
||||
** generated by a source language processor.
|
||||
**
|
||||
** (4) THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
||||
** OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
** FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT
|
||||
** SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE
|
||||
** FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE,
|
||||
** ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
** DEALINGS IN THE SOFTWARE.
|
||||
**
|
||||
** A copy of the Software is available free of charge at
|
||||
** https://www.blackmagicdesign.com/desktopvideo_sdk under the EULA.
|
||||
**
|
||||
** -LICENSE-END-
|
||||
*/
|
||||
//
|
||||
// stdafx.h : include file for standard system include files,
|
||||
// or project specific include files that are used frequently, but
|
||||
// are changed infrequently
|
||||
//
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "targetver.h"
|
||||
|
||||
#ifndef NOMINMAX
|
||||
#define NOMINMAX
|
||||
#endif
|
||||
|
||||
#define WIN32_LEAN_AND_MEAN // Exclude rarely-used stuff from Windows headers
|
||||
// Windows Header Files:
|
||||
#include <winsock2.h>
|
||||
#include <ws2tcpip.h>
|
||||
#include <windows.h>
|
||||
|
||||
// C RunTime Header Files
|
||||
#include <stdlib.h>
|
||||
#include <malloc.h>
|
||||
#include <memory.h>
|
||||
#include <tchar.h>
|
||||
|
||||
@@ -1,64 +0,0 @@
|
||||
/* -LICENSE-START-
|
||||
** Copyright (c) 2012 Blackmagic Design
|
||||
**
|
||||
** Permission is hereby granted, free of charge, to any person or organization
|
||||
** obtaining a copy of the software and accompanying documentation (the
|
||||
** "Software") to use, reproduce, display, distribute, sub-license, execute,
|
||||
** and transmit the Software, and to prepare derivative works of the Software,
|
||||
** and to permit third-parties to whom the Software is furnished to do so, in
|
||||
** accordance with:
|
||||
**
|
||||
** (1) if the Software is obtained from Blackmagic Design, the End User License
|
||||
** Agreement for the Software Development Kit ("EULA") available at
|
||||
** https://www.blackmagicdesign.com/EULA/DeckLinkSDK; or
|
||||
**
|
||||
** (2) if the Software is obtained from any third party, such licensing terms
|
||||
** as notified by that third party,
|
||||
**
|
||||
** and all subject to the following:
|
||||
**
|
||||
** (3) the copyright notices in the Software and this entire statement,
|
||||
** including the above license grant, this restriction and the following
|
||||
** disclaimer, must be included in all copies of the Software, in whole or in
|
||||
** part, and all derivative works of the Software, unless such copies or
|
||||
** derivative works are solely in the form of machine-executable object code
|
||||
** generated by a source language processor.
|
||||
**
|
||||
** (4) THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
||||
** OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
** FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT
|
||||
** SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE
|
||||
** FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE,
|
||||
** ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
** DEALINGS IN THE SOFTWARE.
|
||||
**
|
||||
** A copy of the Software is available free of charge at
|
||||
** https://www.blackmagicdesign.com/desktopvideo_sdk under the EULA.
|
||||
**
|
||||
** -LICENSE-END-
|
||||
*/
|
||||
//
|
||||
#pragma once
|
||||
|
||||
// The following macros define the minimum required platform. The minimum required platform
|
||||
// is the earliest version of Windows, Internet Explorer etc. that has the necessary features to run
|
||||
// your application. The macros work by enabling all features available on platform versions up to and
|
||||
// including the version specified.
|
||||
|
||||
// Modify the following defines if you have to target a platform prior to the ones specified below.
|
||||
// Refer to MSDN for the latest info on corresponding values for different platforms.
|
||||
#ifndef WINVER // Specifies that the minimum required platform is Windows Vista.
|
||||
#define WINVER 0x0600 // Change this to the appropriate value to target other versions of Windows.
|
||||
#endif
|
||||
|
||||
#ifndef _WIN32_WINNT // Specifies that the minimum required platform is Windows Vista.
|
||||
#define _WIN32_WINNT 0x0600 // Change this to the appropriate value to target other versions of Windows.
|
||||
#endif
|
||||
|
||||
#ifndef _WIN32_WINDOWS // Specifies that the minimum required platform is Windows 98.
|
||||
#define _WIN32_WINDOWS 0x0410 // Change this to the appropriate value to target Windows Me or later.
|
||||
#endif
|
||||
|
||||
#ifndef _WIN32_IE // Specifies that the minimum required platform is Internet Explorer 7.0.
|
||||
#define _WIN32_IE 0x0700 // Change this to the appropriate value to target other versions of IE.
|
||||
#endif
|
||||
@@ -1,37 +0,0 @@
|
||||
#include "VideoPlayoutScheduler.h"
|
||||
|
||||
void VideoPlayoutScheduler::Configure(int64_t frameDuration, int64_t timeScale)
|
||||
{
|
||||
mFrameDuration = frameDuration;
|
||||
mTimeScale = timeScale;
|
||||
Reset();
|
||||
}
|
||||
|
||||
void VideoPlayoutScheduler::Reset()
|
||||
{
|
||||
mScheduledFrameIndex = 0;
|
||||
}
|
||||
|
||||
VideoIOScheduleTime VideoPlayoutScheduler::NextScheduleTime()
|
||||
{
|
||||
VideoIOScheduleTime time;
|
||||
time.streamTime = static_cast<int64_t>(mScheduledFrameIndex) * mFrameDuration;
|
||||
time.duration = mFrameDuration;
|
||||
time.timeScale = mTimeScale;
|
||||
time.frameIndex = mScheduledFrameIndex;
|
||||
++mScheduledFrameIndex;
|
||||
return time;
|
||||
}
|
||||
|
||||
void VideoPlayoutScheduler::AccountForCompletionResult(VideoIOCompletionResult result)
|
||||
{
|
||||
if (result == VideoIOCompletionResult::DisplayedLate || result == VideoIOCompletionResult::Dropped)
|
||||
mScheduledFrameIndex += 2;
|
||||
}
|
||||
|
||||
double VideoPlayoutScheduler::FrameBudgetMilliseconds() const
|
||||
{
|
||||
return mTimeScale != 0
|
||||
? (static_cast<double>(mFrameDuration) * 1000.0) / static_cast<double>(mTimeScale)
|
||||
: 0.0;
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "VideoIOTypes.h"
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
class VideoPlayoutScheduler
|
||||
{
|
||||
public:
|
||||
void Configure(int64_t frameDuration, int64_t timeScale);
|
||||
void Reset();
|
||||
VideoIOScheduleTime NextScheduleTime();
|
||||
void AccountForCompletionResult(VideoIOCompletionResult result);
|
||||
double FrameBudgetMilliseconds() const;
|
||||
uint64_t ScheduledFrameIndex() const { return mScheduledFrameIndex; }
|
||||
int64_t TimeScale() const { return mTimeScale; }
|
||||
|
||||
private:
|
||||
int64_t mFrameDuration = 0;
|
||||
int64_t mTimeScale = 0;
|
||||
uint64_t mScheduledFrameIndex = 0;
|
||||
};
|
||||
@@ -10,6 +10,7 @@
|
||||
"outputFrameRate": "59.94",
|
||||
"autoReload": true,
|
||||
"maxTemporalHistoryFrames": 12,
|
||||
"previewFps": 30,
|
||||
"previewEnabled": true,
|
||||
"previewFps": 59.94,
|
||||
"enableExternalKeying": true
|
||||
}
|
||||
|
||||
@@ -4,15 +4,26 @@ 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
|
||||
- [ ] 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
|
||||
- [x] Define subsystem boundaries and target architecture
|
||||
- [x] Introduce an internal event model
|
||||
- [x] Split `RuntimeHost`
|
||||
- [x] Finish live-state and service-facing coordination
|
||||
- [x] Make the render thread the sole GL owner
|
||||
- [x] Refactor live state layering into an explicit composition model
|
||||
- [x] Move persistence onto a background snapshot writer
|
||||
- [x] Make DeckLink/backend lifecycle explicit with a state machine
|
||||
- [ ] Make playout timing proactive and deadline-aware
|
||||
- [ ] Add structured health, telemetry, and operational reporting
|
||||
|
||||
Checklist note:
|
||||
|
||||
- The checked Phase 1 item means the subsystem vocabulary, dependency direction, state categories, design package, and runtime implementation foothold are in place.
|
||||
- The checked Phase 2 item means the internal event model substrate is complete enough for later phases: the typed event vocabulary, app-owned dispatcher, coalesced event pump, reload bridge events, production bridges, and pure event tests are in place. Remaining items in [PHASE_2_INTERNAL_EVENT_MODEL_DESIGN.md](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/docs/PHASE_2_INTERNAL_EVENT_MODEL_DESIGN.md) are narrow follow-ups, mainly completion/failure observations and later replacement of the runtime-store poll fallback with real file-watch events.
|
||||
- The checked Phase 3 item means the render-facing state path now has named live-state, composition, frame-state, resolver, and service-bridge boundaries. `OpenGLComposite::renderEffect()` is reduced to runtime work, frame input construction, and frame rendering.
|
||||
- The checked Phase 4 item means normal runtime GL work is now owned by a dedicated `RenderEngine` render thread. Input upload, output render, preview, screenshot capture, render-local resets, and shader application enter through render-thread queue/request paths instead of caller-thread context borrowing. The remaining output timing risk is callback-coupled synchronous output production, which is intentionally tracked for the later DeckLink/backend lifecycle and playout-queue work.
|
||||
- The checked Phase 5 item means persisted, committed/session, transient automation, and render-local state are explicitly named. `CommittedLiveState` physically owns current session layer state, `RuntimeLiveState` owns transient OSC overlays, `RenderStateComposer` consumes a layered input contract, and reset/reload/preset overlay invalidation is centralized and covered by non-GL tests.
|
||||
- It does not mean the whole app is fully extracted. Backend lifecycle/playout queue policy and richer telemetry continue through later phases.
|
||||
|
||||
## Timing Review
|
||||
|
||||
The recent OSC work removed several control-path stalls, but the app still has a few deeper timing characteristics that matter for live resilience:
|
||||
@@ -20,16 +31,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 +53,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 +61,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/composite/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
|
||||
|
||||
@@ -103,7 +114,7 @@ Failures are often surfaced via `MessageBoxA`, while background services mainly
|
||||
|
||||
Relevant code:
|
||||
|
||||
- [OpenGLComposite.cpp](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/gl/OpenGLComposite.cpp:314)
|
||||
- [OpenGLComposite.cpp](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/gl/composite/OpenGLComposite.cpp:314)
|
||||
- [DeckLinkSession.cpp](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/videoio/decklink/DeckLinkSession.cpp:478)
|
||||
- [RuntimeServices.cpp](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/control/RuntimeServices.cpp:205)
|
||||
|
||||
@@ -116,30 +127,31 @@ Recommended direction:
|
||||
- prefer degraded runtime states over modal failure handling where possible
|
||||
- add a rolling log file for operational troubleshooting
|
||||
|
||||
### 5. Live OSC overlay and persisted state are still separate concepts without a formal model
|
||||
### 5. Live OSC overlay and persisted state now have an explicit layering model
|
||||
|
||||
The current design works better now, but it still relies on hand-managed reconciliation between:
|
||||
Phase 5 formalized the previous hand-managed reconciliation between:
|
||||
|
||||
- persisted parameter state in `RuntimeHost`
|
||||
- transient OSC overlay state in `OpenGLComposite`
|
||||
- base persisted state owned by `RuntimeStore` serialization/preset IO
|
||||
- committed session state owned by `CommittedLiveState`
|
||||
- transient OSC overlay state owned by `RuntimeLiveState`
|
||||
- render-local temporal, feedback, preview, screenshot, and playout state owned by `RenderEngine`
|
||||
|
||||
Relevant code:
|
||||
|
||||
- [OpenGLComposite.h](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/gl/OpenGLComposite.h:66)
|
||||
- [CommittedLiveState.h](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/runtime/live/CommittedLiveState.h:1)
|
||||
- [RuntimeLiveState.h](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/runtime/live/RuntimeLiveState.h:1)
|
||||
- [RenderStateComposer.h](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/runtime/live/RenderStateComposer.h:1)
|
||||
- [RuntimeStateLayerModel.h](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/runtime/live/RuntimeStateLayerModel.h:1)
|
||||
|
||||
Recommended direction:
|
||||
Current direction:
|
||||
|
||||
Formalize three layers of state:
|
||||
|
||||
- base persisted state
|
||||
- operator/UI committed state
|
||||
- transient live automation overlay
|
||||
|
||||
Then render can always resolve:
|
||||
- render resolves values with a named composition rule:
|
||||
|
||||
- `final = base + committed + transient`
|
||||
|
||||
That avoids special-case sync behavior becoming scattered across the code.
|
||||
- settled OSC commits are session-only by default and do not request persistence unless policy explicitly opts in
|
||||
- reset, reload, preset load, and shader compatibility changes prune or clear transient overlays at the live-state boundary
|
||||
- render-local temporal and feedback resources remain outside the parameter layering model
|
||||
|
||||
### 6. DeckLink lifecycle could be modeled more explicitly
|
||||
|
||||
@@ -171,7 +183,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 +207,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
|
||||
|
||||
@@ -249,17 +261,22 @@ Recommended direction:
|
||||
|
||||
### 7. Persistence should be more asynchronous and debounced
|
||||
|
||||
`SavePersistentState()` is still called directly from many update paths.
|
||||
Status: addressed by Phase 6.
|
||||
|
||||
Relevant code:
|
||||
|
||||
- [RuntimeHost.cpp](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/runtime/RuntimeHost.cpp:1841)
|
||||
- `RuntimeCoordinator.cpp`
|
||||
- `RuntimeUpdateController.cpp`
|
||||
- `RuntimeStore.cpp`
|
||||
- `PersistenceWriter.cpp`
|
||||
|
||||
Recent OSC work already reduced this problem for live automation, but the broader architecture would still benefit from:
|
||||
Runtime-state persistence now flows from accepted coordinator mutations to typed persistence events, then into a debounced background writer. The store still owns serialization and preset IO, while the writer owns temp-file replacement, coalescing, result reporting, and shutdown flushing.
|
||||
|
||||
- a debounced persistence queue
|
||||
- atomic write-behind snapshots
|
||||
- clear separation between state mutation and disk flush
|
||||
The remaining architecture concern is broader persistence policy, not direct mutation-path disk writes:
|
||||
|
||||
- whether preset saves should stay synchronous
|
||||
- whether runtime config writes should share the persistence writer
|
||||
- whether failed writes should retry automatically or wait for the next request
|
||||
|
||||
This improves both resilience and timing safety.
|
||||
|
||||
@@ -278,7 +295,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 +305,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.
|
||||
|
||||
@@ -305,7 +322,7 @@ The desktop preview is rate-limited, but still presented from inside the render
|
||||
Relevant code:
|
||||
|
||||
- [OpenGLRenderPipeline.cpp](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/gl/pipeline/OpenGLRenderPipeline.cpp:54)
|
||||
- [OpenGLComposite.cpp](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/gl/OpenGLComposite.cpp:235)
|
||||
- [OpenGLComposite.cpp](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/gl/composite/OpenGLComposite.cpp:235)
|
||||
|
||||
This means preview presentation can still consume time on the same path that is trying to meet output deadlines.
|
||||
|
||||
@@ -332,19 +349,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 +373,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 +403,7 @@ Target split:
|
||||
- `VideoBackend`
|
||||
- DeckLink input/output lifecycle
|
||||
- pacing and scheduling
|
||||
- `Health/Telemetry`
|
||||
- `HealthTelemetry`
|
||||
- logging
|
||||
- counters
|
||||
- timing traces
|
||||
@@ -393,11 +420,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 +469,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 +507,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 +532,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,11 +546,18 @@ Expected benefits:
|
||||
|
||||
Once rendering and snapshots are isolated, formalize how final parameter values are derived.
|
||||
|
||||
Recommended layers:
|
||||
Dedicated design note:
|
||||
|
||||
- base persisted state
|
||||
- operator-committed live state
|
||||
- transient automation overlay
|
||||
- [PHASE_5_LIVE_STATE_LAYERING_DESIGN.md](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/docs/PHASE_5_LIVE_STATE_LAYERING_DESIGN.md)
|
||||
|
||||
Status:
|
||||
|
||||
- complete for the current architecture
|
||||
- `RuntimeStateLayerModel` names the state categories
|
||||
- `CommittedLiveState` physically owns committed/session layer state
|
||||
- `RenderStateComposer` consumes `LayeredRenderStateInput`
|
||||
- `RuntimeLiveState` owns transient overlay smoothing, generation, commit settlement, and compatibility pruning
|
||||
- settled OSC commits update session state without requesting persistence by default
|
||||
|
||||
Render should derive final values from a clear composition rule such as:
|
||||
|
||||
@@ -517,19 +576,24 @@ Expected benefits:
|
||||
|
||||
### Phase 6. Move persistence onto a background snapshot writer
|
||||
|
||||
After the state model is explicit, persistence should become a background concern rather than a synchronous side effect of mutations.
|
||||
Status: complete. Runtime-state persistence is now a background concern rather than a synchronous side effect of mutations.
|
||||
|
||||
Target behavior:
|
||||
Dedicated design note:
|
||||
|
||||
- [PHASE_6_BACKGROUND_PERSISTENCE_DESIGN.md](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/docs/PHASE_6_BACKGROUND_PERSISTENCE_DESIGN.md)
|
||||
|
||||
Implemented behavior:
|
||||
|
||||
- mutations update authoritative in-memory stored state
|
||||
- persistence requests are queued
|
||||
- disk writes are debounced and coalesced
|
||||
- writes are atomic and versioned where practical
|
||||
- writes use temp-file replacement where practical
|
||||
- shutdown flush behavior is explicit and tested
|
||||
|
||||
Why this phase comes after state splitting:
|
||||
|
||||
- otherwise persistence logic will need to be rewritten twice
|
||||
- it should operate on the new `RuntimeStore` model, not on the current mixed-responsibility object
|
||||
- it should operate on the new `RuntimeStore` model, not on a mixed-responsibility runtime object
|
||||
|
||||
Expected benefits:
|
||||
|
||||
@@ -541,6 +605,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
|
||||
@@ -565,14 +633,32 @@ Expected benefits:
|
||||
- easier handling of missing input, dropped frames, or reconfiguration
|
||||
- a clearer place to own playout headroom policy, output queue sizing, and late-frame recovery behavior
|
||||
|
||||
### Phase 7.5. Make playout timing proactive and deadline-aware
|
||||
|
||||
Phase 7 made backend lifecycle, ready-frame queueing, measured recovery, and backend playout health visible. The remaining timing-specific work is to make output production proactive instead of demand-filled by completion pressure.
|
||||
|
||||
Dedicated design note:
|
||||
|
||||
- [PHASE_7_5_PROACTIVE_PLAYOUT_TIMING_DESIGN.md](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/docs/PHASE_7_5_PROACTIVE_PLAYOUT_TIMING_DESIGN.md)
|
||||
|
||||
Expected benefits:
|
||||
|
||||
- output frames are produced ahead based on queue pressure or cadence
|
||||
- DeckLink completion handling normally consumes already-ready frames
|
||||
- preview and synchronous readback fallback become explicitly subordinate to playout deadlines
|
||||
- queue depth, readback misses, preview skips, and render timing explain why headroom drains
|
||||
|
||||
### Phase 8. Add structured health, telemetry, and operational reporting
|
||||
|
||||
This phase should happen after the main ownership changes so the telemetry can reflect the final architecture instead of a transient one.
|
||||
|
||||
Dedicated design note:
|
||||
|
||||
- [PHASE_8_HEALTH_TELEMETRY_DESIGN.md](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/docs/PHASE_8_HEALTH_TELEMETRY_DESIGN.md)
|
||||
|
||||
Recommended coverage:
|
||||
|
||||
- render queue depth
|
||||
- GL lock wait time, if any shared lock remains
|
||||
- input callback latency
|
||||
- input upload skip count
|
||||
- output scheduling lag
|
||||
@@ -605,34 +691,36 @@ 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.
|
||||
7. Refactor DeckLink/backend lifecycle into an explicit state machine.
|
||||
8. Add structured telemetry, health reporting, and operational diagnostics.
|
||||
8. Make playout timing proactive and deadline-aware.
|
||||
9. Add structured telemetry, health reporting, and operational diagnostics.
|
||||
|
||||
## Why This Order Makes Sense
|
||||
|
||||
This order tries to avoid doing foundational work twice.
|
||||
|
||||
- The event model comes before major subsystem extraction so coordination patterns stabilize early.
|
||||
- `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.
|
||||
- Persistence moved after the state model split so it could target the durable snapshot model rather than an older mixed-responsibility runtime object.
|
||||
- Telemetry is intentionally late so it instruments the architecture that survives the refactor.
|
||||
|
||||
## Short Version
|
||||
|
||||
The app is in a much better place than it was before the OSC timing work, 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
|
||||
3. split state ownership
|
||||
4. isolate rendering
|
||||
5. formalize layered live state
|
||||
6. background persistence
|
||||
6. complete background persistence
|
||||
7. explicit backend lifecycle
|
||||
8. health and telemetry
|
||||
8. proactive playout timing
|
||||
9. health and telemetry
|
||||
|
||||
That sequence gives each later phase a cleaner foundation than the current app has today.
|
||||
|
||||
540
docs/CURRENT_SYSTEM_ARCHITECTURE.md
Normal file
540
docs/CURRENT_SYSTEM_ARCHITECTURE.md
Normal file
@@ -0,0 +1,540 @@
|
||||
# Current System Architecture
|
||||
|
||||
This document describes how the application currently works.
|
||||
|
||||
It replaces the phase-by-phase design trail as the best entry point for understanding the repo. The older phase documents remain useful history, but they mix implementation notes, experiments, and target designs. This document is organized by current runtime behavior and subsystem ownership instead.
|
||||
|
||||
The active plan for tightening render-thread ownership is:
|
||||
|
||||
- [Render Thread Ownership Plan](RENDER_THREAD_OWNERSHIP_PLAN.md)
|
||||
|
||||
The plan for building a fresh modular app around the proven probe architecture is:
|
||||
|
||||
- [RenderCadenceCompositor README](../apps/RenderCadenceCompositor/README.md)
|
||||
- [Render Cadence Golden Rules](RENDER_CADENCE_GOLDEN_RULES.md)
|
||||
|
||||
`NEW_RENDER_CADENCE_APP_PLAN.md` remains as historical planning context, but the README and golden rules are the current contract for the new cadence-first app.
|
||||
|
||||
## Application Shape
|
||||
|
||||
The app is a live OpenGL compositor with DeckLink input/output, runtime control services, persistent layer-stack state, live state overlays, health telemetry, and a small internal event model.
|
||||
|
||||
At runtime the major subsystems are:
|
||||
|
||||
- `OpenGLComposite`
|
||||
- `RuntimeStore`
|
||||
- `RuntimeCoordinator`
|
||||
- `RuntimeSnapshotProvider`
|
||||
- `RuntimeServices`
|
||||
- `RuntimeUpdateController`
|
||||
- `RenderEngine`
|
||||
- `VideoBackend`
|
||||
- `DeckLinkSession`
|
||||
- `HealthTelemetry`
|
||||
- `RuntimeEventDispatcher`
|
||||
- `PersistenceWriter`
|
||||
|
||||
The key architectural rule is:
|
||||
|
||||
- runtime/control subsystems decide what state should exist
|
||||
- render subsystems decide how to draw that state
|
||||
- video subsystems decide how frames move to and from hardware
|
||||
- telemetry observes behavior without becoming a control plane
|
||||
|
||||
## Process Startup
|
||||
|
||||
The Win32 app creates the window, chooses a pixel format, creates an OpenGL context, initializes COM, and constructs `OpenGLComposite`.
|
||||
|
||||
`OpenGLComposite` owns the high-level assembly of the runtime:
|
||||
|
||||
- runtime store
|
||||
- runtime coordinator
|
||||
- runtime services
|
||||
- runtime update controller
|
||||
- render engine
|
||||
- video backend
|
||||
|
||||
Startup proceeds broadly as:
|
||||
|
||||
1. COM and OpenGL are initialized by the Win32 app.
|
||||
2. `OpenGLComposite::InitDeckLink()` discovers/configures DeckLink and runtime state.
|
||||
3. Runtime services are started.
|
||||
4. Shader programs and GL resources are initialized.
|
||||
5. The render thread is started.
|
||||
6. The video backend starts output preroll and playback.
|
||||
|
||||
The normal VS Code debug launch currently sets:
|
||||
|
||||
```text
|
||||
VST_DISABLE_INPUT_CAPTURE=1
|
||||
```
|
||||
|
||||
That disables DeckLink input capture for output-timing isolation while keeping the output path active.
|
||||
|
||||
## Runtime State
|
||||
|
||||
### `RuntimeStore`
|
||||
|
||||
`RuntimeStore` owns durable runtime data and file-backed state.
|
||||
|
||||
It owns:
|
||||
|
||||
- runtime host configuration
|
||||
- stored layer stack data
|
||||
- persisted parameter values
|
||||
- stack presets
|
||||
- shader package catalog metadata
|
||||
- runtime state presentation data
|
||||
- persistence requests
|
||||
|
||||
It does not own render-thread resources, DeckLink timing, control ingress, or mutation policy.
|
||||
|
||||
### `CommittedLiveState`
|
||||
|
||||
`CommittedLiveState` owns current session/operator layer state that is live but not necessarily persisted as the durable base state.
|
||||
|
||||
It gives the renderer and snapshot builder a named read model for current committed layer state.
|
||||
|
||||
### `RuntimeCoordinator`
|
||||
|
||||
`RuntimeCoordinator` is the mutation policy boundary.
|
||||
|
||||
It validates and applies runtime mutations, classifies whether changes are persisted/committed/transient, emits persistence requests, and produces render reset/reload decisions.
|
||||
|
||||
It keeps mutation decisions out of:
|
||||
|
||||
- the render engine
|
||||
- control services
|
||||
- video backend
|
||||
- telemetry
|
||||
|
||||
### `RuntimeSnapshotProvider`
|
||||
|
||||
`RuntimeSnapshotProvider` publishes render-facing snapshots.
|
||||
|
||||
It owns the currently published render snapshot and gives the render path a stable read boundary. Rendering does not read mutable store objects directly.
|
||||
|
||||
## Live State And Layering
|
||||
|
||||
The current render state is built from named layers of state:
|
||||
|
||||
- persisted layer/package/default state from the runtime store
|
||||
- committed live/session state
|
||||
- transient live overlays from OSC/control input
|
||||
- render-local state owned by the renderer
|
||||
|
||||
`RuntimeStateLayerModel` names these categories. `RenderStateComposer` and `RuntimeLiveState` combine live values into render-facing state.
|
||||
|
||||
`RenderFrameInput` and `RenderFrameState` are the frame contract:
|
||||
|
||||
- `RenderFrameInput` describes what kind of frame is being built
|
||||
- `RenderFrameState` describes the resolved state used to draw that frame
|
||||
|
||||
The renderer should not ask global state systems which snapshot or layer state to use midway through drawing.
|
||||
|
||||
## Control And Events
|
||||
|
||||
### `RuntimeServices`
|
||||
|
||||
`RuntimeServices` owns runtime-facing services such as OSC/control integration and service lifecycle.
|
||||
|
||||
It connects control ingress to the coordinator and live-state bridge.
|
||||
|
||||
### `ControlServices`
|
||||
|
||||
`ControlServices` handles OSC/control ingress, buffering, and polling/wake behavior.
|
||||
|
||||
It does not own runtime mutation policy. It normalizes ingress and asks the coordinator/runtime services to apply changes.
|
||||
|
||||
### `RuntimeEventDispatcher`
|
||||
|
||||
The app uses typed runtime events for internal coordination and observation.
|
||||
|
||||
Events are used for:
|
||||
|
||||
- runtime state broadcast requests
|
||||
- shader build lifecycle
|
||||
- backend state changes
|
||||
- input/output frame observations
|
||||
- timing samples
|
||||
- health and queue observations
|
||||
|
||||
Events say what happened. Commands/request methods still exist where a caller needs an immediate success/failure answer.
|
||||
|
||||
## Persistence
|
||||
|
||||
Persistence is handled by `PersistenceWriter`.
|
||||
|
||||
Runtime mutations can enqueue persistence requests without blocking the render/output path. Shutdown performs a bounded persistence flush.
|
||||
|
||||
The store owns durable state; the writer owns background write execution.
|
||||
|
||||
## Render System
|
||||
|
||||
### `RenderEngine`
|
||||
|
||||
`RenderEngine` owns normal runtime OpenGL work.
|
||||
|
||||
It starts a dedicated render thread and binds the GL context on that thread. Runtime GL work enters through render-thread requests or render command queues.
|
||||
|
||||
The render thread handles:
|
||||
|
||||
- output frame rendering
|
||||
- input frame upload
|
||||
- preview present
|
||||
- screenshot capture
|
||||
- render-local resets
|
||||
- shader/rebuild application
|
||||
- temporal history and shader feedback resources
|
||||
|
||||
Startup initialization still happens before the render thread starts while the app explicitly owns the context. Normal runtime work is routed through `RenderEngine`.
|
||||
|
||||
### Current Render-Thread Limitation
|
||||
|
||||
The current render thread is a shared GL executor, not a pure output-only cadence thread.
|
||||
|
||||
This means output render can still be delayed by:
|
||||
|
||||
- input upload work
|
||||
- preview present requests
|
||||
- screenshot capture
|
||||
- render reset commands
|
||||
- shader/resource update work
|
||||
- synchronous render-thread request queue wait
|
||||
|
||||
For output-timing diagnosis, input capture can be disabled with:
|
||||
|
||||
```text
|
||||
VST_DISABLE_INPUT_CAPTURE=1
|
||||
```
|
||||
|
||||
When enabled, the backend skips DeckLink input configuration/start and `HasInputSource()` reports false.
|
||||
|
||||
### `OpenGLRenderPipeline`
|
||||
|
||||
`OpenGLRenderPipeline` draws the frame and performs output packing/readback.
|
||||
|
||||
The current output path:
|
||||
|
||||
1. binds the composite framebuffer
|
||||
2. calls the render effect callback
|
||||
3. blits/composes into the output framebuffer
|
||||
4. packs the output for the configured pixel format
|
||||
5. flushes GL
|
||||
6. reads output into the provided system-memory output frame
|
||||
7. records render/readback timing
|
||||
|
||||
For BGRA8 output, the pipeline uses a BGRA-compatible pack framebuffer and async PBO readback by default.
|
||||
|
||||
## Video Backend
|
||||
|
||||
### `VideoBackend`
|
||||
|
||||
`VideoBackend` owns app-level video device lifecycle, output production, system-memory frame slots, and backend playout health.
|
||||
|
||||
It owns:
|
||||
|
||||
- backend lifecycle state
|
||||
- output production worker
|
||||
- output completion worker
|
||||
- system-memory output frame pool
|
||||
- ready/completed output queue
|
||||
- render cadence controller
|
||||
- playout policy
|
||||
- output frame scheduling into `VideoIODevice`
|
||||
- backend timing and queue telemetry
|
||||
|
||||
It does not own GL drawing. It asks `OpenGLVideoIOBridge` / `RenderEngine` to render into system-memory output frames.
|
||||
|
||||
### Lifecycle
|
||||
|
||||
The current backend lifecycle includes:
|
||||
|
||||
- discovery
|
||||
- configuring
|
||||
- configured
|
||||
- prerolling
|
||||
- running
|
||||
- degraded
|
||||
- stopping
|
||||
- stopped
|
||||
- failed
|
||||
|
||||
Startup now separates output schedule preparation from scheduled playback:
|
||||
|
||||
1. prepare the DeckLink output schedule
|
||||
2. start output completion worker
|
||||
3. start output producer worker
|
||||
4. warm up rendered system-memory preroll frames
|
||||
5. optionally start input streams
|
||||
6. start DeckLink scheduled playback
|
||||
|
||||
### Output Production
|
||||
|
||||
The output producer is cadence-driven.
|
||||
|
||||
`RenderCadenceController` tracks the selected output frame duration and decides when the producer should render another frame.
|
||||
|
||||
The render producer attempts to render one output frame per selected output tick. It does not speed up just because DeckLink is empty.
|
||||
|
||||
If render/GPU work is late enough, the cadence controller can skip late ticks according to policy.
|
||||
|
||||
### System-Memory Frame Pool
|
||||
|
||||
`SystemOutputFramePool` owns reusable system-memory output slots.
|
||||
|
||||
Slots have four states:
|
||||
|
||||
- `Free`
|
||||
- `Rendering`
|
||||
- `Completed`
|
||||
- `Scheduled`
|
||||
|
||||
In the current legacy app, completed-but-unscheduled frames are treated as a latest-N cache. The newer `RenderCadenceCompositor` uses a bounded FIFO completed reserve instead; see its README for the cadence-first contract.
|
||||
|
||||
Scheduled frames are protected until DeckLink reports completion.
|
||||
|
||||
### Output Queue
|
||||
|
||||
`RenderOutputQueue` holds completed unscheduled output frames waiting to be scheduled.
|
||||
|
||||
In the legacy app it is bounded and latest-N:
|
||||
|
||||
- pushing beyond capacity releases/drops the oldest ready frame
|
||||
- `DropOldestFrame()` is used when the frame pool needs to recycle old completed work
|
||||
|
||||
### Scheduling
|
||||
|
||||
`VideoBackend::ScheduleReadyOutputFramesToTarget()` schedules completed system-memory frames up to the configured preroll/scheduled target.
|
||||
|
||||
DeckLink scheduling is capped by the current app-owned scheduled count. Real DeckLink buffered-frame telemetry is also recorded.
|
||||
|
||||
### Completion Handling
|
||||
|
||||
DeckLink completion callbacks do not render.
|
||||
|
||||
The callback path reports completion into `VideoBackend`, which processes completions on a backend worker. Completion processing:
|
||||
|
||||
- releases the system-memory slot by buffer pointer
|
||||
- records pacing
|
||||
- accounts for late/drop/flushed/completed result
|
||||
- records telemetry
|
||||
- wakes the output producer
|
||||
|
||||
## DeckLink Integration
|
||||
|
||||
### `DeckLinkSession`
|
||||
|
||||
`DeckLinkSession` is the DeckLink implementation of `VideoIODevice`.
|
||||
|
||||
It owns:
|
||||
|
||||
- DeckLink discovery
|
||||
- input/output mode selection
|
||||
- DeckLink input/output interfaces
|
||||
- keyer configuration
|
||||
- capture and playout delegates
|
||||
- schedule-time generation through `VideoPlayoutScheduler`
|
||||
- DeckLink frame scheduling
|
||||
- actual buffered-frame telemetry
|
||||
|
||||
For output, system-memory frames are scheduled through DeckLink `CreateVideoFrameWithBuffer()`.
|
||||
|
||||
When a system-memory frame is scheduled, `DeckLinkSession` records a map from the DeckLink frame object back to the app-owned system-memory buffer pointer. On completion, the buffer pointer is returned so `VideoBackend` can release the matching slot.
|
||||
|
||||
### Actual DeckLink Buffer Telemetry
|
||||
|
||||
`DeckLinkSession` calls `GetBufferedVideoFrameCount()` after schedule/completion where available.
|
||||
|
||||
Telemetry separates:
|
||||
|
||||
- actual DeckLink buffered frames
|
||||
- app-owned scheduled system-memory slots
|
||||
- synthetic schedule/completion counters
|
||||
- late/drop/flushed completion results
|
||||
|
||||
## Output Timing Experiments And Current Finding
|
||||
|
||||
The repo includes `DeckLinkRenderCadenceProbe`, a small standalone test app under:
|
||||
|
||||
```text
|
||||
apps/DeckLinkRenderCadenceProbe
|
||||
```
|
||||
|
||||
The probe does not use the main runtime, shader system, preview path, input upload path, or shared render engine. It uses:
|
||||
|
||||
- one OpenGL render thread with its own hidden GL context
|
||||
- simple BGRA8 motion rendering
|
||||
- async PBO readback
|
||||
- legacy latest-N system-memory frame slots; bounded FIFO completed reserve in `RenderCadenceCompositor`
|
||||
- a playout thread that feeds DeckLink
|
||||
- real rendered warmup before scheduled playback
|
||||
|
||||
The first hardware result was smooth at roughly 59.94/60 fps with:
|
||||
|
||||
- `renderFps` near 59.9
|
||||
- `scheduleFps` near 59.9
|
||||
- DeckLink actual buffered frames stable at 4
|
||||
- no late frames
|
||||
- no dropped frames
|
||||
- no PBO misses
|
||||
- no completed-frame drops
|
||||
|
||||
That proves the clean architecture can work on the test machine. Remaining main-app timing issues are therefore likely integration/ownership issues in the main app rather than a fundamental DeckLink/OpenGL/BGRA8 limitation.
|
||||
|
||||
The highest-value current suspects are:
|
||||
|
||||
- input upload sharing the output render thread
|
||||
- shared render-thread task queue contention
|
||||
- preview/screenshot work
|
||||
- runtime/render-state work on the output path
|
||||
|
||||
## Health Telemetry
|
||||
|
||||
`HealthTelemetry` owns app-visible health and timing observations.
|
||||
|
||||
It records:
|
||||
|
||||
- signal/input status
|
||||
- performance/render timing
|
||||
- event queue timing
|
||||
- backend lifecycle/playout state
|
||||
- output render queue wait
|
||||
- output render/readback timing
|
||||
- system-memory frame counts
|
||||
- actual DeckLink buffer depth
|
||||
- late/drop/flushed/completed frame counters
|
||||
- schedule-call timing/failure counts
|
||||
|
||||
Several hot-path telemetry calls use try-lock variants so observation does not become a major timing dependency.
|
||||
|
||||
Runtime state presentation exposes telemetry through the runtime JSON/open API surface.
|
||||
|
||||
## Preview And Screenshot
|
||||
|
||||
Preview is best-effort.
|
||||
|
||||
`OpenGLComposite::paintGL()` skips preview when the backend reports output pressure. Preview presentation is requested through the render thread.
|
||||
|
||||
Screenshot capture is also a render-thread request. It reads pixels from the output framebuffer and writes PNG asynchronously after capture.
|
||||
|
||||
Both preview and screenshot share GL execution with output render, so they are secondary to output timing.
|
||||
|
||||
## Output Readback Modes
|
||||
|
||||
The output readback path supports environment-selected modes:
|
||||
|
||||
```text
|
||||
VST_OUTPUT_READBACK_MODE=async_pbo
|
||||
VST_OUTPUT_READBACK_MODE=sync
|
||||
VST_OUTPUT_READBACK_MODE=cached_only
|
||||
```
|
||||
|
||||
Default behavior is `async_pbo`.
|
||||
|
||||
Experiment findings:
|
||||
|
||||
- direct synchronous readback was slower on the sampled machine
|
||||
- cached-only recovered timing but is visually invalid for live motion
|
||||
- BGRA8 pack framebuffer plus async PBO removed the earlier large readback stall
|
||||
|
||||
## Current Debug/Experiment Launches
|
||||
|
||||
VS Code launch configurations include:
|
||||
|
||||
- `Debug LoopThroughWithOpenGLCompositing`
|
||||
- `Debug LoopThroughWithOpenGLCompositing - sync readback experiment`
|
||||
- `Debug LoopThroughWithOpenGLCompositing - cached output experiment`
|
||||
- `Debug DeckLinkRenderCadenceProbe`
|
||||
|
||||
The default main-app debug launch currently disables input capture with `VST_DISABLE_INPUT_CAPTURE=1` so output timing can be tested without input upload interference.
|
||||
|
||||
## Current Ownership Summary
|
||||
|
||||
| Area | Current Owner |
|
||||
| --- | --- |
|
||||
| Durable runtime config/state | `RuntimeStore` |
|
||||
| Current committed live layer state | `CommittedLiveState` |
|
||||
| Mutation validation/policy | `RuntimeCoordinator` |
|
||||
| Render snapshot publication | `RuntimeSnapshotProvider` |
|
||||
| OSC/control ingress | `RuntimeServices` / `ControlServices` |
|
||||
| Internal event dispatch | `RuntimeEventDispatcher` |
|
||||
| Background persistence writes | `PersistenceWriter` |
|
||||
| GL context and normal GL work | `RenderEngine` render thread |
|
||||
| Render-pass execution and output readback | `OpenGLRenderPipeline` |
|
||||
| Device lifecycle and output production | `VideoBackend` |
|
||||
| DeckLink API integration | `DeckLinkSession` |
|
||||
| Operational health/timing | `HealthTelemetry` |
|
||||
|
||||
## Current Runtime Flow Summary
|
||||
|
||||
### Control Mutation
|
||||
|
||||
```text
|
||||
OSC/API/control input
|
||||
-> RuntimeServices / ControlServices
|
||||
-> RuntimeCoordinator
|
||||
-> RuntimeStore / CommittedLiveState / RuntimeLiveState
|
||||
-> RuntimeSnapshotProvider publication or live overlay update
|
||||
-> RuntimeEventDispatcher observations
|
||||
```
|
||||
|
||||
### Output Render
|
||||
|
||||
```text
|
||||
VideoBackend output producer
|
||||
-> RenderCadenceController tick
|
||||
-> SystemOutputFramePool acquire rendering slot
|
||||
-> OpenGLVideoIOBridge::RenderScheduledFrame
|
||||
-> RenderEngine::RequestOutputFrame
|
||||
-> render thread
|
||||
-> OpenGLRenderPipeline::RenderFrame
|
||||
-> system-memory output slot
|
||||
-> RenderOutputQueue completed frame
|
||||
```
|
||||
|
||||
### DeckLink Playout
|
||||
|
||||
```text
|
||||
RenderOutputQueue completed frame
|
||||
-> VideoBackend schedules to target
|
||||
-> DeckLinkSession::ScheduleOutputFrame
|
||||
-> CreateVideoFrameWithBuffer
|
||||
-> ScheduleVideoFrame
|
||||
-> DeckLink playback
|
||||
-> completion callback
|
||||
-> VideoBackend completion worker
|
||||
-> release scheduled system-memory slot
|
||||
```
|
||||
|
||||
### Input Capture
|
||||
|
||||
When input capture is enabled:
|
||||
|
||||
```text
|
||||
DeckLink input callback
|
||||
-> VideoBackend::HandleInputFrame
|
||||
-> OpenGLVideoIOBridge::UploadInputFrame
|
||||
-> RenderEngine::QueueInputFrame
|
||||
-> render thread upload
|
||||
```
|
||||
|
||||
When `VST_DISABLE_INPUT_CAPTURE=1`, this flow is skipped.
|
||||
|
||||
## Known Current Constraints
|
||||
|
||||
- The main app render thread still handles multiple kinds of GL work.
|
||||
- Output render still uses a synchronous request/response call into the render thread.
|
||||
- Input upload can contend with output render when input capture is enabled.
|
||||
- Preview and screenshot share the render thread.
|
||||
- Phase/experiment documents still exist as historical notes, but this document is the current architecture summary.
|
||||
|
||||
## Practical Rules
|
||||
|
||||
- Keep one owner for each kind of state.
|
||||
- Keep GL work on the render thread.
|
||||
- Keep DeckLink completion callbacks passive.
|
||||
- In the legacy app, treat completed unscheduled output frames as latest-N cache entries; in `RenderCadenceCompositor`, preserve completed frames as a bounded FIFO reserve.
|
||||
- Protect scheduled output frames until DeckLink completion.
|
||||
- Keep output timing more important than preview/screenshot.
|
||||
- Measure timing by domain instead of adding fallback branches blindly.
|
||||
414
docs/DECKLINK_OPENGL_LESSONS_LEARNED.md
Normal file
414
docs/DECKLINK_OPENGL_LESSONS_LEARNED.md
Normal file
@@ -0,0 +1,414 @@
|
||||
# DeckLink / OpenGL Lessons Learned
|
||||
|
||||
This document summarizes the practical lessons from the Phase 3-7.7 refactor work, especially the DeckLink playout timing experiments.
|
||||
|
||||
It is intentionally broader than the phase design docs. The goal is to preserve what we now know about the system so future architecture choices start from evidence instead of rediscovering the same constraints.
|
||||
|
||||
## High-Level Lesson
|
||||
|
||||
The application is not just a renderer with a video output attached.
|
||||
|
||||
It is a real-time playout system with several independent clocks:
|
||||
|
||||
- the selected output cadence, for example 59.94 fps
|
||||
- the GPU render/readback timeline
|
||||
- the DeckLink scheduled playback clock
|
||||
- the Windows thread scheduler
|
||||
- the input capture callback cadence
|
||||
- the preview/window message loop
|
||||
- the runtime/control update cadence
|
||||
|
||||
Stable playback depends on assigning one owner to each timing domain and keeping those domains loosely coupled.
|
||||
|
||||
## What Worked
|
||||
|
||||
### Named State Contracts Helped
|
||||
|
||||
`RenderFrameInput` and `RenderFrameState` made the render path easier to reason about.
|
||||
|
||||
Before that, frame rendering depended on scattered choices about snapshots, cache state, layer state, input source state, and runtime service state. Naming the frame contract made it possible to move logic out of `RenderEngine` and toward explicit frame construction.
|
||||
|
||||
Lesson:
|
||||
|
||||
- keep frame inputs explicit
|
||||
- keep render-frame state immutable for the duration of a frame
|
||||
- avoid making the renderer ask global systems which state it should use mid-frame
|
||||
|
||||
### Render-Thread Ownership Helped
|
||||
|
||||
Moving GL work behind a render-thread boundary reduced wrong-thread GL access risk and made ownership clearer.
|
||||
|
||||
The current render thread is still shared by output render, input upload, preview, screenshot, resize, and reset work, so it is not yet a pure output cadence thread. But the ownership direction is right.
|
||||
|
||||
Lesson:
|
||||
|
||||
- GL context ownership should be explicit
|
||||
- public methods should enqueue or request work
|
||||
- render-thread methods should own GL bodies
|
||||
- synchronous calls should be reserved for places that genuinely need a result
|
||||
|
||||
### Background Persistence Was Worth It
|
||||
|
||||
Moving persistence away from hot render/control paths reduced incidental latency risk and made state writes easier to reason about.
|
||||
|
||||
Lesson:
|
||||
|
||||
- runtime/control persistence should not sit on output render timing
|
||||
- shutdown flushing is fine, steady-state blocking is not
|
||||
|
||||
### Lifecycle State Was Worth It
|
||||
|
||||
The backend lifecycle model gave us better failure and shutdown vocabulary.
|
||||
|
||||
This became important once startup stopped being a single `Start()` call and became:
|
||||
|
||||
- prepare output schedule
|
||||
- start render cadence
|
||||
- warm up real frames
|
||||
- start input streams
|
||||
- start scheduled playback
|
||||
|
||||
Lesson:
|
||||
|
||||
- playout startup needs phases
|
||||
- degradation should be explicit
|
||||
- shutdown order should be deliberate and testable
|
||||
|
||||
## What Did Not Work
|
||||
|
||||
### Completion-Driven Rendering Was Too Fragile
|
||||
|
||||
Rendering on or near DeckLink completion can average the target frame rate, but it leaves no headroom.
|
||||
|
||||
When the callback asks for a frame just-in-time, any small delay in render, readback, scheduling, or Windows wake timing becomes visible as a buffer dip or stutter.
|
||||
|
||||
Lesson:
|
||||
|
||||
- DeckLink completion should release scheduled resources and wake scheduling
|
||||
- it should not render
|
||||
- it should not decide visual fallback policy in steady state
|
||||
|
||||
### Black Fallback Hid The Real Timing Problem
|
||||
|
||||
Scheduling black on app-ready underrun made the pipeline appear to keep moving while producing visible black flicker.
|
||||
|
||||
It also made diagnosis harder because DeckLink could have scheduled frames while the app visibly failed.
|
||||
|
||||
Lesson:
|
||||
|
||||
- black is a startup/error/degraded-state policy, not normal steady-state recovery
|
||||
- steady-state underruns should be measured as timing failures
|
||||
|
||||
### Synthetic Schedule Lead Was Misleading
|
||||
|
||||
The synthetic scheduled/completed index could report a large buffer while DeckLink still showed low actual device buffer depth.
|
||||
|
||||
Real DeckLink `GetBufferedVideoFrameCount()` telemetry was necessary to separate:
|
||||
|
||||
- app-owned scheduled slots
|
||||
- synthetic schedule lead
|
||||
- actual hardware/device buffer depth
|
||||
|
||||
Lesson:
|
||||
|
||||
- measure actual device buffer depth
|
||||
- keep synthetic counters only as diagnostics
|
||||
- do not infer device health from internal stream indexes alone
|
||||
|
||||
### Schedule Cursor Recovery Must Be Conservative
|
||||
|
||||
The DeckLink schedule cursor should normally advance as a continuous stream timeline. Continuously realigning the next scheduled stream time to the sampled playback cursor can create its own timing fault: output may look like low FPS even when render and scheduling counters average 59.94/60 fps.
|
||||
|
||||
What worked better:
|
||||
|
||||
- use the exact DeckLink frame duration for the render cadence
|
||||
- keep healthy scheduling on a continuous stream cursor
|
||||
- measure schedule lead from DeckLink playback time versus the next schedule time
|
||||
- realign only after real pressure, such as a late/drop report or dangerously low measured lead
|
||||
- re-arm proactive realignment only after lead has recovered
|
||||
|
||||
Lesson:
|
||||
|
||||
- schedule recovery is an output-edge safety valve, not a per-frame timing policy
|
||||
- if recovery increments continuously, the recovery path has become the problem
|
||||
- include schedule lead and realignment count in telemetry/logs so drift is visible before guessing
|
||||
|
||||
### More Buffer Is Not Automatically Smoother
|
||||
|
||||
Increasing DeckLink scheduled frames sometimes made the reported device buffer look healthier while visible motion still stuttered.
|
||||
|
||||
The problem was not only "how many frames are scheduled"; it was also whether the scheduled frames represented a stable render cadence.
|
||||
|
||||
Lesson:
|
||||
|
||||
- buffer depth absorbs jitter, but it cannot fix bad cadence ownership
|
||||
- a full buffer of poorly timed or repeated frames can still look wrong
|
||||
|
||||
### Speed-Up Catch-Up Was The Wrong Instinct
|
||||
|
||||
Letting the producer sprint to refill the buffer created new timing artifacts.
|
||||
|
||||
The render side should behave like a stable game/render loop: render at the selected cadence, record lateness, and only skip ticks when render/GPU work itself overruns.
|
||||
|
||||
Lesson:
|
||||
|
||||
- the render thread should not render faster because DeckLink is empty
|
||||
- buffer drain is a failure signal, not a sprint signal
|
||||
- warmup should fill buffers before playback starts
|
||||
|
||||
## GPU Readback Lessons
|
||||
|
||||
### The Original Readback Path Was The Major Collapse
|
||||
|
||||
Early Phase 7.5 telemetry showed `glReadPixels(..., nullptr)` into the PBO costing roughly 8-14 ms on representative samples. That was enough to collapse ready depth and cause long freezes.
|
||||
|
||||
Direct synchronous readback was worse on the sampled machine.
|
||||
|
||||
Cached-output mode, while visually invalid for live output, immediately recovered timing. That proved ongoing GPU-to-CPU transfer was the major cost in that version of the path.
|
||||
|
||||
Lesson:
|
||||
|
||||
- isolate readback cost from render cost
|
||||
- use intentionally invalid cached-output experiments when diagnosing throughput
|
||||
- do not assume async PBO is actually cheap on every format/driver path
|
||||
|
||||
### BGRA8 Packing Changed The Problem
|
||||
|
||||
Changing the output path so readback matched the DeckLink BGRA8 format made `asyncQueueReadPixelsMs` drop dramatically in sampled runs.
|
||||
|
||||
Long pauses disappeared and the remaining issue became short stutters/cadence gaps.
|
||||
|
||||
Lesson:
|
||||
|
||||
- output/readback format matters
|
||||
- avoid format conversions on the readback path when possible
|
||||
- BGRA8 is a good current format target for experiments
|
||||
- v210/YUV packing can be deferred until cadence is stable
|
||||
|
||||
### DeckLink SDK Fast Transfer Was Not Available On The Test GPU
|
||||
|
||||
The SDK OpenGL fast-transfer path depends on hardware/extension support that was not present on the RTX 4060 Ti test machine:
|
||||
|
||||
- NVIDIA DVP path was gated around Quadro-style support
|
||||
- `GL_AMD_pinned_memory` was not exposed
|
||||
|
||||
Lesson:
|
||||
|
||||
- SDK fast-transfer samples are useful references but not a universal fix
|
||||
- unsupported fast-transfer code should not be central to the architecture
|
||||
- the default path must work with ordinary consumer GPUs
|
||||
|
||||
## DeckLink Lessons
|
||||
|
||||
### DeckLink Wants Scheduled System-Memory Frames
|
||||
|
||||
Using `CreateVideoFrameWithBuffer()` lets DeckLink schedule frames backed by our system-memory slots.
|
||||
|
||||
That is the right ownership model for this app:
|
||||
|
||||
- render/readback writes into a slot
|
||||
- DeckLink schedules a frame that references that slot
|
||||
- the slot is protected until DeckLink completion
|
||||
|
||||
Lesson:
|
||||
|
||||
- system-memory slots are the contract between render and playout
|
||||
- scheduled slots must not be recycled early
|
||||
- completed-but-unscheduled slots should form a bounded FIFO reserve for playout
|
||||
|
||||
### Startup Needs Real Preroll
|
||||
|
||||
Starting scheduled playback before real rendered frames exist creates avoidable startup fragility.
|
||||
|
||||
The better startup shape is:
|
||||
|
||||
- prepare the DeckLink schedule
|
||||
- start render cadence
|
||||
- render warmup frames at normal cadence
|
||||
- schedule those frames as preroll
|
||||
- start DeckLink scheduled playback
|
||||
|
||||
Lesson:
|
||||
|
||||
- do not use black preroll as the normal startup path
|
||||
- do not render faster during warmup
|
||||
- if warmup cannot fill in a bounded time, fail/degrade visibly
|
||||
|
||||
## Buffering Lessons
|
||||
|
||||
### There Are Two Different Buffers
|
||||
|
||||
The app has at least two important frame stores:
|
||||
|
||||
- system-memory completed FIFO reserve frames
|
||||
- DeckLink scheduled/device buffer
|
||||
|
||||
They have different ownership rules.
|
||||
|
||||
Completed-but-unscheduled frames should be a bounded FIFO reserve for playout. If that reserve overflows, dropping the oldest completed frame is an app-side reserve policy and should be counted separately from DeckLink dropped frames.
|
||||
|
||||
Scheduled frames are not disposable because DeckLink may still read them.
|
||||
|
||||
Lesson:
|
||||
|
||||
- completed frames waiting for playout are a bounded FIFO reserve
|
||||
- scheduled frames are owned by DeckLink until completion
|
||||
- keep metrics for both
|
||||
|
||||
### Consume-Before-Render Is The Wrong Model For Completed Frames
|
||||
|
||||
If the render cadence waits for completed frames to be consumed, DeckLink timing can indirectly slow the renderer.
|
||||
|
||||
That couples the clocks again.
|
||||
|
||||
Lesson:
|
||||
|
||||
- render cadence should keep rendering at selected cadence
|
||||
- render acquire should not evict completed frames that are waiting for playout
|
||||
- if the completed reserve overflows, drop/count the oldest unscheduled completed frame
|
||||
- only scheduled/in-flight saturation should prevent rendering to a safe slot
|
||||
|
||||
## Render Thread Lessons
|
||||
|
||||
### The Current Render Thread Is Still Shared
|
||||
|
||||
The GL render thread currently handles:
|
||||
|
||||
- output rendering
|
||||
- input upload
|
||||
- preview present
|
||||
- screenshot capture
|
||||
- render reset commands
|
||||
- shader/resource operations
|
||||
|
||||
Output render can therefore be delayed by queued or inline work.
|
||||
|
||||
Lesson:
|
||||
|
||||
- "one GL thread" is not the same as "one output cadence thread"
|
||||
- output render should become the highest-priority GL operation
|
||||
- non-output GL work needs budgets, coalescing, or deferral
|
||||
|
||||
### Input Upload Is A Suspect Timing Coupling
|
||||
|
||||
Output render currently processes input upload work immediately before rendering the output frame.
|
||||
|
||||
That keeps input fresh but can steal time from the exact frame we are trying to render on cadence.
|
||||
|
||||
Lesson:
|
||||
|
||||
- measure input upload count and time immediately before output render
|
||||
- test policies such as `one_before_output` or `skip_before_output`
|
||||
- prefer latest-input semantics over draining every pending upload
|
||||
|
||||
### CPU Input Conversion Can Be Worse Than Input Copy
|
||||
|
||||
When DeckLink input only exposed UYVY8 on the test machine, an initial CPU UYVY-to-BGRA conversion in the input callback measured around a full-frame budget on sampled runs and reduced input cadence dramatically.
|
||||
|
||||
Moving the input edge to raw UYVY8 capture changed the ownership:
|
||||
|
||||
- DeckLink callback copies raw supported input bytes into `InputFrameMailbox`
|
||||
- the mailbox keeps latest-frame semantics and uses a contiguous copy when row strides match
|
||||
- the render thread uploads/decodes UYVY8 into the shader-visible `gVideoInput` texture
|
||||
- runtime shaders continue to see decoded input, not packed capture bytes
|
||||
|
||||
Lesson:
|
||||
|
||||
- keep input callbacks as capture/copy edges
|
||||
- keep GL decode/upload in the render-owned path
|
||||
- measure input copy, upload, and decode separately
|
||||
- do not hide expensive format conversion inside the DeckLink callback
|
||||
|
||||
### Preview And Screenshot Must Stay Secondary
|
||||
|
||||
Preview is useful, but DeckLink output is the real-time path.
|
||||
|
||||
Screenshot and preview share GL resources and can block or queue work on the same render thread.
|
||||
|
||||
Lesson:
|
||||
|
||||
- preview should be skipped when output is under pressure
|
||||
- screenshot capture should be treated as disruptive unless proven otherwise
|
||||
- forced preview/screenshot should be visible in telemetry
|
||||
|
||||
## Telemetry Lessons
|
||||
|
||||
The useful telemetry has been the telemetry that separates domains:
|
||||
|
||||
- output render queue wait
|
||||
- render/draw time
|
||||
- readback queue time
|
||||
- readback fence/map/copy time
|
||||
- app ready/completed queue depth
|
||||
- system-memory free/rendering/completed/scheduled counts
|
||||
- actual DeckLink buffered-frame count
|
||||
- DeckLink schedule-call time/failures
|
||||
- late/drop completion counts
|
||||
|
||||
Lesson:
|
||||
|
||||
- averages are not enough
|
||||
- timing spikes matter more than steady low values
|
||||
- count ownership states, not just queue depth
|
||||
- keep experiment logs short and evidence-based
|
||||
|
||||
## Current Architectural Direction
|
||||
|
||||
The current direction is still sound:
|
||||
|
||||
```text
|
||||
Render cadence loop
|
||||
renders at selected output cadence
|
||||
writes completed system-memory frames into a bounded FIFO reserve
|
||||
never sprints to refill DeckLink
|
||||
|
||||
Frame store
|
||||
owns free / rendering / completed / scheduled slots
|
||||
recycles unscheduled completed frames when needed
|
||||
protects scheduled frames until completion
|
||||
|
||||
DeckLink playout scheduler
|
||||
consumes completed frames
|
||||
tops up actual device buffer
|
||||
never renders
|
||||
|
||||
Completion callback
|
||||
releases scheduled slots
|
||||
records completion result
|
||||
wakes scheduler
|
||||
```
|
||||
|
||||
## Rewrite Lesson
|
||||
|
||||
A full restart is not obviously the right next move.
|
||||
|
||||
The current repo now contains:
|
||||
|
||||
- working runtime/control architecture
|
||||
- useful phase docs
|
||||
- non-GL tests around key state machines
|
||||
- real telemetry
|
||||
- a clearer understanding of DeckLink and OpenGL timing
|
||||
|
||||
The better next step is likely a contained "V2 spine" inside the current app:
|
||||
|
||||
- harden the render cadence loop
|
||||
- harden the frame store
|
||||
- separate DeckLink scheduling
|
||||
- demote preview/screenshot/input upload below output cadence
|
||||
- delete old compatibility branches as they become unnecessary
|
||||
|
||||
A full rewrite becomes attractive only if the current GL ownership model cannot be made deterministic without excessive surgery, or if the project switches rendering API.
|
||||
|
||||
## Practical Rules Going Forward
|
||||
|
||||
- One timing authority per domain.
|
||||
- Render cadence is time-driven, not completion-driven.
|
||||
- DeckLink scheduling is device-buffer-driven, not render-driven.
|
||||
- Completion callbacks release and report; they do not render.
|
||||
- System-memory completed frames are a bounded FIFO reserve.
|
||||
- Scheduled frames are protected until DeckLink completion.
|
||||
- Startup uses real rendered warmup/preroll.
|
||||
- Black fallback is degraded/error behavior, not steady-state behavior.
|
||||
- Output render has priority over preview, screenshot, and bulk input upload.
|
||||
- Measure before adding recovery branches.
|
||||
580
docs/NEW_RENDER_CADENCE_APP_PLAN.md
Normal file
580
docs/NEW_RENDER_CADENCE_APP_PLAN.md
Normal file
@@ -0,0 +1,580 @@
|
||||
# New Render Cadence App Plan
|
||||
|
||||
Status: historical implementation plan. `apps/RenderCadenceCompositor` now exists; use [apps/RenderCadenceCompositor/README.md](../apps/RenderCadenceCompositor/README.md) and [Render Cadence Golden Rules](RENDER_CADENCE_GOLDEN_RULES.md) as the current implementation contract.
|
||||
|
||||
This plan describes a new application folder that rebuilds the output path from the proven `DeckLinkRenderCadenceProbe` architecture, but as a maintainable app foundation rather than a monolithic probe file.
|
||||
|
||||
The first goal is not to port the current compositor feature set. The first goal is to reproduce the probe's smooth 59.94/60 fps DeckLink output with clean module boundaries, tests where possible, and a structure that can later accept the shader/runtime/control systems without compromising timing.
|
||||
|
||||
## Working Name
|
||||
|
||||
Suggested folder:
|
||||
|
||||
```text
|
||||
apps/RenderCadenceCompositor
|
||||
```
|
||||
|
||||
Suggested executable:
|
||||
|
||||
```text
|
||||
RenderCadenceCompositor
|
||||
```
|
||||
|
||||
The existing app remains intact:
|
||||
|
||||
```text
|
||||
apps/LoopThroughWithOpenGLCompositing
|
||||
```
|
||||
|
||||
The probe remains the control sample:
|
||||
|
||||
```text
|
||||
apps/DeckLinkRenderCadenceProbe
|
||||
```
|
||||
|
||||
## Design Principle
|
||||
|
||||
The app is built around one spine:
|
||||
|
||||
```text
|
||||
Render cadence thread
|
||||
-> owns GL context
|
||||
-> renders at selected frame cadence
|
||||
-> performs async BGRA8 readback
|
||||
-> publishes completed system-memory frames
|
||||
|
||||
System frame exchange
|
||||
-> owns Free / Rendering / Completed / Scheduled slots
|
||||
-> bounded FIFO reserve for completed unscheduled frames
|
||||
-> protects scheduled frames until DeckLink completion
|
||||
|
||||
DeckLink output thread
|
||||
-> consumes completed frames
|
||||
-> schedules to target buffer depth
|
||||
-> releases scheduled frames on completion
|
||||
-> never renders
|
||||
```
|
||||
|
||||
Everything else must fit around that spine.
|
||||
|
||||
## Non-Negotiable Rules
|
||||
|
||||
- The render thread owns its GL context from initialization to shutdown.
|
||||
- The render thread is driven by selected render cadence, not DeckLink demand.
|
||||
- DeckLink scheduling never calls render code.
|
||||
- Completion callbacks never render.
|
||||
- No synchronous render request exists in the output path.
|
||||
- Preview, screenshot, input upload, shader rebuild, and runtime control cannot run ahead of a due output frame.
|
||||
- Completed unscheduled frames are a bounded FIFO reserve; overflow drops are counted separately from DeckLink drops.
|
||||
- Scheduled frames are protected until DeckLink completion.
|
||||
- Startup warms up real rendered frames before scheduled playback starts.
|
||||
|
||||
## Borrow From The Probe
|
||||
|
||||
Keep these behaviors from `DeckLinkRenderCadenceProbe`:
|
||||
|
||||
- hidden OpenGL context owned by the render thread
|
||||
- simple render loop with `nextRenderTime`
|
||||
- BGRA8 render target
|
||||
- PBO ring readback
|
||||
- non-blocking fence polling with zero timeout
|
||||
- system-memory slots with `Free`, `Rendering`, `Completed`, `Scheduled`
|
||||
- preserve completed frames waiting for playout; drop/count the oldest completed frame only if the bounded reserve overflows
|
||||
- DeckLink playout thread only schedules completed frames
|
||||
- warmup completed frames before `StartScheduledPlayback()`
|
||||
- one-line-per-second timing telemetry
|
||||
|
||||
## Do Not Borrow Directly
|
||||
|
||||
The probe is deliberately compact. Do not carry over these probe limitations into the new app:
|
||||
|
||||
- one huge `.cpp` file
|
||||
- hard-coded output mode as permanent behavior
|
||||
- render pattern, frame store, PBO logic, DeckLink playout, COM setup, and telemetry mixed together
|
||||
- no reusable interfaces
|
||||
- no unit-testable non-GL core
|
||||
|
||||
## Proposed Folder Structure
|
||||
|
||||
```text
|
||||
apps/RenderCadenceCompositor/
|
||||
README.md
|
||||
RenderCadenceCompositor.cpp
|
||||
|
||||
app/
|
||||
RenderCadenceApp.cpp
|
||||
RenderCadenceApp.h
|
||||
AppConfig.cpp
|
||||
AppConfig.h
|
||||
AppConfigProvider.cpp
|
||||
AppConfigProvider.h
|
||||
|
||||
control/
|
||||
HttpControlServer.cpp
|
||||
HttpControlServer.h
|
||||
RuntimeStateJson.h
|
||||
|
||||
platform/
|
||||
ComInit.cpp
|
||||
ComInit.h
|
||||
HiddenGlWindow.cpp
|
||||
HiddenGlWindow.h
|
||||
Win32Console.cpp
|
||||
Win32Console.h
|
||||
|
||||
render/
|
||||
RenderThread.cpp
|
||||
RenderThread.h
|
||||
RenderCadenceClock.cpp
|
||||
RenderCadenceClock.h
|
||||
SimpleMotionRenderer.cpp
|
||||
SimpleMotionRenderer.h
|
||||
Bgra8ReadbackPipeline.cpp
|
||||
Bgra8ReadbackPipeline.h
|
||||
PboReadbackRing.cpp
|
||||
PboReadbackRing.h
|
||||
|
||||
frames/
|
||||
SystemFrameExchange.cpp
|
||||
SystemFrameExchange.h
|
||||
SystemFrameTypes.h
|
||||
|
||||
video/
|
||||
DeckLinkOutput.cpp
|
||||
DeckLinkOutput.h
|
||||
DeckLinkOutputThread.cpp
|
||||
DeckLinkOutputThread.h
|
||||
|
||||
telemetry/
|
||||
CadenceTelemetry.cpp
|
||||
CadenceTelemetry.h
|
||||
CadenceTelemetryJson.h
|
||||
TelemetryHealthMonitor.h
|
||||
|
||||
logging/
|
||||
Logger.cpp
|
||||
Logger.h
|
||||
|
||||
json/
|
||||
JsonWriter.cpp
|
||||
JsonWriter.h
|
||||
```
|
||||
|
||||
The new app can reuse selected existing source files from the current app at first:
|
||||
|
||||
- `videoio/decklink/DeckLinkSession.*`
|
||||
- `videoio/decklink/DeckLinkDisplayMode.*`
|
||||
- `videoio/decklink/DeckLinkVideoIOFormat.*`
|
||||
- `videoio/decklink/DeckLinkFrameTransfer.*`
|
||||
- `videoio/VideoIOFormat.*`
|
||||
- `videoio/VideoIOTypes.h`
|
||||
- `videoio/VideoPlayoutScheduler.*`
|
||||
- `gl/renderer/GLExtensions.*`
|
||||
|
||||
Longer term, shared code should move into common libraries, but the first version can link these files directly to avoid a big build-system refactor.
|
||||
|
||||
## Module Responsibilities
|
||||
|
||||
### `RenderCadenceApp`
|
||||
|
||||
Owns top-level startup/shutdown sequencing.
|
||||
|
||||
Responsibilities:
|
||||
|
||||
- initialize COM
|
||||
- discover/select DeckLink output
|
||||
- create frame exchange
|
||||
- start render thread
|
||||
- wait for completed-frame warmup
|
||||
- start DeckLink output thread
|
||||
- wait for scheduled buffer warmup
|
||||
- start DeckLink scheduled playback
|
||||
- start telemetry printer
|
||||
- stop in reverse order
|
||||
|
||||
It should not contain OpenGL drawing code, frame slot policy, or DeckLink scheduling loops.
|
||||
|
||||
### `AppConfig`
|
||||
|
||||
Owns runtime settings for the initial app.
|
||||
|
||||
Initial settings:
|
||||
|
||||
- output mode preference
|
||||
- output width/height validation
|
||||
- frame buffer capacity
|
||||
- PBO depth
|
||||
- warmup completed-frame count
|
||||
- target DeckLink scheduled depth
|
||||
- telemetry interval
|
||||
|
||||
Initial values should match the successful probe:
|
||||
|
||||
```text
|
||||
systemFrameSlots = 12
|
||||
pboDepth = 6
|
||||
warmupFrames = 4
|
||||
targetDeckLinkBufferedFrames = 4
|
||||
pixelFormat = BGRA8
|
||||
```
|
||||
|
||||
### `HiddenGlWindow`
|
||||
|
||||
Owns hidden Win32 window, device context, and OpenGL context creation.
|
||||
|
||||
Responsibilities:
|
||||
|
||||
- create hidden window with `CS_OWNDC`
|
||||
- choose/set pixel format
|
||||
- create `HGLRC`
|
||||
- expose `MakeCurrent()` and `ClearCurrent()`
|
||||
- destroy context/window safely
|
||||
|
||||
Only `RenderThread` should call `MakeCurrent()` after startup.
|
||||
|
||||
### `RenderThread`
|
||||
|
||||
Owns the render loop and GL context for its full lifetime.
|
||||
|
||||
Responsibilities:
|
||||
|
||||
- create/bind hidden GL context
|
||||
- resolve GL extensions
|
||||
- initialize renderer/readback pipeline
|
||||
- run cadence loop
|
||||
- render one frame when due
|
||||
- queue PBO readback
|
||||
- consume completed PBOs into `SystemFrameExchange`
|
||||
- record telemetry
|
||||
- destroy GL resources on the render thread
|
||||
|
||||
It must not:
|
||||
|
||||
- wait for DeckLink
|
||||
- schedule DeckLink frames
|
||||
- block on a system frame slot if only completed unscheduled frames can be dropped
|
||||
- accept arbitrary GL tasks ahead of output frames
|
||||
|
||||
### `RenderCadenceClock`
|
||||
|
||||
Small, testable cadence helper.
|
||||
|
||||
Responsibilities:
|
||||
|
||||
- track target frame duration
|
||||
- return whether a render is due
|
||||
- compute sleep duration
|
||||
- detect overrun/skipped ticks
|
||||
- never speed up to fill buffers
|
||||
|
||||
This should be unit tested without GL.
|
||||
|
||||
### `SimpleMotionRenderer`
|
||||
|
||||
First renderer only.
|
||||
|
||||
Responsibilities:
|
||||
|
||||
- render obvious smooth motion and color changes
|
||||
- produce BGRA8-compatible framebuffer content
|
||||
- make dropped/repeated frames visually obvious
|
||||
|
||||
This intentionally avoids shader-package/runtime complexity.
|
||||
|
||||
### `Bgra8ReadbackPipeline`
|
||||
|
||||
Owns output framebuffer and BGRA8 readback orchestration.
|
||||
|
||||
Responsibilities:
|
||||
|
||||
- configure render target dimensions
|
||||
- render into an RGBA8/BGRA-compatible texture
|
||||
- coordinate `PboReadbackRing`
|
||||
- publish completed frames into `SystemFrameExchange`
|
||||
|
||||
### `PboReadbackRing`
|
||||
|
||||
Owns PBO/fence state.
|
||||
|
||||
Responsibilities:
|
||||
|
||||
- queue readback into the next free PBO slot
|
||||
- poll completed fences with zero timeout
|
||||
- map/copy completed PBOs into provided system-memory slots
|
||||
- count PBO misses
|
||||
- clean up fences/PBOs on render thread
|
||||
|
||||
This is GL-backed, but the state model should be small and easy to reason about.
|
||||
|
||||
### `SystemFrameExchange`
|
||||
|
||||
The central handoff between render and video.
|
||||
|
||||
Responsibilities:
|
||||
|
||||
- own system-memory frame buffers
|
||||
- track slot states: `Free`, `Rendering`, `Completed`, `Scheduled`
|
||||
- provide `AcquireForRender()`
|
||||
- provide `PublishCompleted()`
|
||||
- provide `ConsumeCompletedForSchedule()`
|
||||
- provide `ReleaseScheduledByBytes()`
|
||||
- drop oldest completed unscheduled frame when render needs a slot
|
||||
- expose metrics
|
||||
|
||||
This should be unit tested heavily.
|
||||
|
||||
### `DeckLinkOutput`
|
||||
|
||||
Thin wrapper around `DeckLinkSession` for output-only use.
|
||||
|
||||
Responsibilities:
|
||||
|
||||
- discover/select output mode
|
||||
- configure output callback
|
||||
- prepare output schedule
|
||||
- schedule app-owned system-memory frames
|
||||
- start scheduled playback
|
||||
- stop/release resources
|
||||
- expose actual DeckLink buffered count
|
||||
|
||||
No input support in the first version.
|
||||
|
||||
### `DeckLinkOutputThread`
|
||||
|
||||
Owns playout scheduling loop.
|
||||
|
||||
Responsibilities:
|
||||
|
||||
- keep scheduled depth near target
|
||||
- consume completed frames from `SystemFrameExchange`
|
||||
- schedule them through `DeckLinkOutput`
|
||||
- release frame if scheduling fails
|
||||
- sleep briefly when scheduled buffer is full or no completed frame exists
|
||||
|
||||
It must not render.
|
||||
|
||||
### `CadenceTelemetry`
|
||||
|
||||
Owns counters, not policy.
|
||||
|
||||
Initial counters:
|
||||
|
||||
- rendered frames
|
||||
- completed readback frames
|
||||
- scheduled frames
|
||||
- completion count
|
||||
- completed-frame drops
|
||||
- acquire misses
|
||||
- schedule underruns
|
||||
- PBO queue misses
|
||||
- DeckLink late count
|
||||
- DeckLink dropped count
|
||||
- free/rendering/completed/scheduled slot counts
|
||||
- actual DeckLink buffered frames
|
||||
|
||||
### `TelemetryHealthMonitor`
|
||||
|
||||
Samples cadence telemetry once per interval and logs only health events.
|
||||
|
||||
Normal telemetry is available through the HTTP state endpoint. The console should not receive a healthy once-per-second cadence line.
|
||||
|
||||
Health events:
|
||||
|
||||
- warning when DeckLink late/dropped-frame counters increase
|
||||
- warning when schedule failures increase
|
||||
- error when app/DeckLink output buffering is starved
|
||||
|
||||
## Startup Sequence
|
||||
|
||||
Target first-version startup:
|
||||
|
||||
```text
|
||||
main
|
||||
-> load AppConfig through AppConfigProvider
|
||||
-> initialize COM
|
||||
-> create SystemFrameExchange
|
||||
-> start RenderThread
|
||||
-> wait for completed frame warmup
|
||||
-> optionally discover/select/configure DeckLink output
|
||||
-> if DeckLink is available:
|
||||
-> start DeckLinkOutputThread
|
||||
-> wait for scheduled depth warmup
|
||||
-> DeckLinkOutput start scheduled playback
|
||||
-> if DeckLink is unavailable:
|
||||
-> continue without video output
|
||||
-> start TelemetryHealthMonitor
|
||||
-> start HttpControlServer
|
||||
-> wait for Enter
|
||||
```
|
||||
|
||||
Shutdown:
|
||||
|
||||
```text
|
||||
stop HttpControlServer
|
||||
stop TelemetryHealthMonitor
|
||||
stop DeckLinkOutputThread
|
||||
DeckLinkOutput stop playback
|
||||
stop RenderThread
|
||||
DeckLinkOutput release resources
|
||||
release COM
|
||||
```
|
||||
|
||||
## First Milestone: Modular Probe Equivalent
|
||||
|
||||
This is the only goal for the initial implementation.
|
||||
|
||||
Feature set:
|
||||
|
||||
- console app
|
||||
- output-only DeckLink
|
||||
- no input
|
||||
- hidden GL context
|
||||
- simple motion renderer
|
||||
- BGRA8 only
|
||||
- PBO async readback
|
||||
- bounded FIFO system-memory frame exchange
|
||||
- warmup before playback
|
||||
- one-line telemetry
|
||||
|
||||
Acceptance:
|
||||
|
||||
- visible DeckLink output is smooth
|
||||
- `renderFps` near selected cadence
|
||||
- `scheduleFps` near selected cadence
|
||||
- scheduled count/decklink buffered count stable around 4
|
||||
- no continuous late/drop count
|
||||
- no continuous PBO misses
|
||||
- behavior matches or exceeds `DeckLinkRenderCadenceProbe`
|
||||
|
||||
## Second Milestone: Testable Core
|
||||
|
||||
Before porting compositor features, add tests for non-GL/non-DeckLink pieces.
|
||||
|
||||
Test targets:
|
||||
|
||||
- `SystemFrameExchangeTests`
|
||||
- `RenderCadenceClockTests`
|
||||
- `CadenceTelemetryTests`
|
||||
|
||||
Important cases:
|
||||
|
||||
- slot lifecycle transitions
|
||||
- scheduled slots are protected
|
||||
- completed unscheduled frames can be dropped
|
||||
- stale handles/generations are rejected
|
||||
- cadence does not speed up to refill buffers
|
||||
- cadence records overrun/skipped ticks
|
||||
|
||||
## Third Milestone: Replace Simple Renderer With Render Interface
|
||||
|
||||
Add an interface around frame rendering:
|
||||
|
||||
```text
|
||||
IRenderScene
|
||||
-> InitializeGl()
|
||||
-> RenderFrame(frameIndex, time)
|
||||
-> ShutdownGl()
|
||||
```
|
||||
|
||||
The first implementation remains `SimpleMotionRenderer`.
|
||||
|
||||
This creates the insertion point for shader-package rendering later without changing timing/scheduling.
|
||||
|
||||
## Fourth Milestone: Begin Porting Current App Features
|
||||
|
||||
Port only after the modular probe equivalent is stable.
|
||||
|
||||
Suggested order:
|
||||
|
||||
1. shader package compile/load
|
||||
2. render pass/layer stack drawing
|
||||
3. runtime snapshot input to renderer
|
||||
4. live state overlays
|
||||
5. control services
|
||||
6. persistence/runtime store
|
||||
7. preview from system-memory frames
|
||||
8. screenshot from system-memory frames
|
||||
9. input capture via CPU latest-frame mailbox
|
||||
|
||||
Each port must preserve the rule that the render thread cadence is primary.
|
||||
|
||||
## What Not To Port Early
|
||||
|
||||
Do not port these until the output spine is proven:
|
||||
|
||||
- DeckLink input
|
||||
- preview GL presentation
|
||||
- screenshot GL readback
|
||||
- HTTP/OSC control services
|
||||
- shader hot reload
|
||||
- persistence
|
||||
- runtime state JSON/open API
|
||||
- complex telemetry/event dispatch
|
||||
|
||||
These are useful, but they are exactly the kinds of features that can accidentally reintroduce timing coupling.
|
||||
|
||||
## Build Plan
|
||||
|
||||
Initial CMake can follow the probe pattern:
|
||||
|
||||
```cmake
|
||||
set(RENDER_CADENCE_APP_DIR "${CMAKE_CURRENT_SOURCE_DIR}/apps/RenderCadenceCompositor")
|
||||
|
||||
add_executable(RenderCadenceCompositor
|
||||
# selected shared DeckLink/video/gl support files
|
||||
# new modular app files
|
||||
)
|
||||
```
|
||||
|
||||
Later, shared source should be split into libraries:
|
||||
|
||||
```text
|
||||
video_shader_decklink
|
||||
video_shader_videoio
|
||||
video_shader_gl_support
|
||||
render_cadence_core
|
||||
```
|
||||
|
||||
Avoid doing that library split before the first modular app works.
|
||||
|
||||
## VS Code Launch
|
||||
|
||||
Add a separate launch profile:
|
||||
|
||||
```text
|
||||
Debug RenderCadenceCompositor
|
||||
```
|
||||
|
||||
Run it as a console app so telemetry remains visible.
|
||||
|
||||
## Documentation
|
||||
|
||||
Add:
|
||||
|
||||
```text
|
||||
apps/RenderCadenceCompositor/README.md
|
||||
```
|
||||
|
||||
The README should record:
|
||||
|
||||
- intended architecture
|
||||
- build/run instructions
|
||||
- expected telemetry
|
||||
- test result notes
|
||||
- differences from the old app
|
||||
- differences from the probe
|
||||
|
||||
## Success Criteria Before Porting More Features
|
||||
|
||||
Do not start feature porting until the new app can run with:
|
||||
|
||||
- stable smooth DeckLink output
|
||||
- stable target scheduled depth
|
||||
- stable actual DeckLink buffered count
|
||||
- no regular visible freezes
|
||||
- no steady PBO misses
|
||||
- no steadily increasing late/dropped completions
|
||||
- focus/minimize changes do not affect output cadence
|
||||
- clean shutdown without hangs
|
||||
|
||||
This gives us a clean foundation. Once this is true, every feature added later has to prove it does not damage the spine.
|
||||
160
docs/RENDER_CADENCE_GOLDEN_RULES.md
Normal file
160
docs/RENDER_CADENCE_GOLDEN_RULES.md
Normal file
@@ -0,0 +1,160 @@
|
||||
# Render Cadence Golden Rules
|
||||
|
||||
These are the non-negotiable rules for the new render-cadence architecture.
|
||||
|
||||
They exist because the old app drifted into a place where DeckLink timing, render work, shader build work, state coordination, readback, and recovery behavior all influenced each other. The new app should stay boring, explicit, and easy to reason about.
|
||||
|
||||
## 1. The Render Thread Owns Its GL Context
|
||||
|
||||
Only the render thread may bind and use its primary OpenGL context.
|
||||
|
||||
Allowed on the render thread:
|
||||
|
||||
- GL resource creation and destruction for resources it owns
|
||||
- GL shader/program swap from an already-prepared GL program
|
||||
- drawing the next frame
|
||||
- async readback queueing and completion polling
|
||||
- publishing completed system-memory frames
|
||||
|
||||
Not allowed on the render thread:
|
||||
|
||||
- Slang compiler invocation
|
||||
- manifest scanning/parsing
|
||||
- filesystem discovery
|
||||
- image/font/LUT decoding
|
||||
- persistence
|
||||
- network/API/OSC handling
|
||||
- DeckLink scheduling
|
||||
- blocking console logging
|
||||
- config file discovery or parsing
|
||||
|
||||
If GL preparation happens off-thread, use an explicit shared-context GL prepare thread. Do not smuggle non-render work back into the cadence loop.
|
||||
|
||||
## 2. Render Cadence Does Not Chase Buffers
|
||||
|
||||
The render thread runs at the selected render cadence.
|
||||
|
||||
It must not speed up to fill a DeckLink/system-memory buffer, and it must not slow down because a consumer is late. If the GPU is genuinely overloaded, record that as render overrun telemetry.
|
||||
|
||||
Buffers absorb timing differences. They do not control render cadence.
|
||||
|
||||
## 3. Video I/O Never Renders
|
||||
|
||||
DeckLink output consumes already-rendered system-memory frames.
|
||||
|
||||
The output/scheduling side may:
|
||||
|
||||
- schedule completed frames
|
||||
- release frames after DeckLink completion
|
||||
- report late/dropped/schedule telemetry
|
||||
- record app-side poll misses
|
||||
- conservatively realign the DeckLink schedule cursor after measured timing pressure
|
||||
|
||||
It must not:
|
||||
|
||||
- render fallback frames
|
||||
- invoke GL
|
||||
- compile shaders
|
||||
- block the render cadence waiting for DeckLink
|
||||
- continuously rewrite healthy scheduled timestamps
|
||||
|
||||
If no completed frame is available, record the miss and keep the ownership boundary intact.
|
||||
|
||||
DeckLink input is also an edge, not a renderer. It may capture/copy the latest supported CPU frame into an input mailbox and update input telemetry, but it must not call GL, schedule output, compile shaders, or drive render cadence. Format decode that requires GL belongs to the render-owned input upload path.
|
||||
|
||||
## 4. Runtime Build Work Produces Artifacts
|
||||
|
||||
Runtime shader work is split into two phases:
|
||||
|
||||
1. CPU/build phase outside the render thread
|
||||
2. shared-context GL preparation outside the render thread where practical
|
||||
3. GL program swap on the render thread
|
||||
|
||||
The CPU/build phase may parse manifests, invoke Slang, validate package shape, and prepare CPU-side data.
|
||||
|
||||
The render thread receives completed render-layer artifacts, asks the shared-context prepare worker to compile/link changed GL programs, and only swaps in prepared programs at a frame boundary. A failed artifact or failed GL preparation must not disturb the current renderer.
|
||||
|
||||
The display/render layer model is app-owned. It may track requested shaders, build state, display metadata, and render-ready artifacts, but it must not perform GL work or drive render cadence directly.
|
||||
|
||||
## 5. No Hidden Blocking In The Cadence Path
|
||||
|
||||
The render loop must not do work with unbounded or OS-dependent latency.
|
||||
|
||||
Examples to avoid:
|
||||
|
||||
- file reads
|
||||
- directory scans
|
||||
- image decoding
|
||||
- process launches
|
||||
- waits on worker threads
|
||||
- blocking locks around slow code
|
||||
- synchronous GPU readback waits
|
||||
- console I/O
|
||||
|
||||
Short mutex use for exchanging small already-prepared objects is acceptable. Holding a lock while doing heavy work is not.
|
||||
|
||||
## 6. System Memory Frames Are A Handoff, Not A Render Driver
|
||||
|
||||
The system-memory frame exchange stores completed frames as a bounded FIFO reserve and protects frames scheduled to DeckLink.
|
||||
|
||||
Render acquire must not evict completed frames that are waiting for playout, and it must never force the render thread to wait for the output side to consume a frame.
|
||||
|
||||
If the completed reserve overflows, the exchange may drop the oldest completed, unscheduled frame and record `completedDrops`. That is an app-side reserve drop, not a DeckLink dropped frame.
|
||||
|
||||
## 7. Startup Uses Warmup, Not Burst Rendering
|
||||
|
||||
DeckLink playback starts only after the render thread has produced enough real frames for preroll.
|
||||
|
||||
Warmup should happen at normal render cadence. Do not temporarily accelerate the renderer to fill buffers.
|
||||
|
||||
## 8. Telemetry Must Name Ownership Clearly
|
||||
|
||||
Counters should say which subsystem had the event.
|
||||
|
||||
Good examples:
|
||||
|
||||
- `renderFps`
|
||||
- `scheduleFps`
|
||||
- `completedPollMisses`
|
||||
- `scheduleFailures`
|
||||
- `decklinkBuffered`
|
||||
- `deckLinkScheduleLeadFrames`
|
||||
- `deckLinkScheduleRealignments`
|
||||
- `inputCaptureFps`
|
||||
- `inputSubmitMs`
|
||||
- `inputUploadMs`
|
||||
- `inputConvertMs`
|
||||
- `shaderCommitted`
|
||||
- `shaderFailures`
|
||||
|
||||
Avoid ambiguous names like `underrun` unless it is clear whether it means app-ready underrun, DeckLink buffered-frame underrun, render overrun, or schedule failure.
|
||||
|
||||
## 9. Keep Files Small And Role-Based
|
||||
|
||||
A file should have one clear reason to change.
|
||||
|
||||
Preferred boundaries:
|
||||
|
||||
- app orchestration
|
||||
- render cadence/thread ownership
|
||||
- GL rendering
|
||||
- runtime artifact build/bridge
|
||||
- app-owned display/render layer model
|
||||
- parameter packing
|
||||
- system-memory frame exchange
|
||||
- DeckLink output scheduling
|
||||
- telemetry
|
||||
- local control/API edge
|
||||
- config loading
|
||||
- JSON presentation/serialization
|
||||
- logging
|
||||
|
||||
If a file starts coordinating multiple subsystems and doing detailed work for each of them, split it before it becomes the new old app.
|
||||
|
||||
## 10. Prefer Explicit Unsupported States
|
||||
|
||||
If a feature needs storage, timing behavior, or ownership we have not designed yet, reject it clearly.
|
||||
|
||||
For example, in the current new app it is better to reject texture/LUT/text/temporal/feedback shaders than to quietly load files or allocate history state on the render thread.
|
||||
|
||||
Unsupported is healthy when it protects the architecture.
|
||||
448
docs/RENDER_THREAD_OWNERSHIP_PLAN.md
Normal file
448
docs/RENDER_THREAD_OWNERSHIP_PLAN.md
Normal file
@@ -0,0 +1,448 @@
|
||||
# Render Thread Ownership Plan
|
||||
|
||||
This plan describes how to make the main compositor behave like the successful `DeckLinkRenderCadenceProbe`: one render cadence owner, one GL context owner, no unrelated work able to interrupt output frame production.
|
||||
|
||||
The goal is not just "all GL calls happen on one thread". The current app mostly does that during runtime already. The real goal is:
|
||||
|
||||
- the output render thread owns its GL context for its whole lifetime
|
||||
- output cadence is driven by the render thread, not by DeckLink completion timing
|
||||
- non-output GL work cannot sit ahead of output frames
|
||||
- callers cannot block the render thread while waiting for synchronous answers
|
||||
- DeckLink scheduling consumes completed system-memory frames and never causes rendering
|
||||
|
||||
## Current Risk Points
|
||||
|
||||
The current main app still has several ways to interrupt output cadence.
|
||||
|
||||
### Shared GL Executor
|
||||
|
||||
`RenderEngine` owns the GL context during runtime, but it acts as a general task executor.
|
||||
|
||||
The same queue/path can run:
|
||||
|
||||
- output frame render
|
||||
- input upload
|
||||
- preview present
|
||||
- screenshot capture
|
||||
- render resets
|
||||
- shader/program commits
|
||||
- resource resize
|
||||
- state clearing
|
||||
|
||||
That means output frames are not guaranteed to be the next GL work item at the selected frame time.
|
||||
|
||||
### Synchronous Output Render Request
|
||||
|
||||
`VideoBackend` drives output production from its output producer thread, then calls:
|
||||
|
||||
```text
|
||||
VideoBackend
|
||||
-> OpenGLVideoIOBridge::RenderScheduledFrame
|
||||
-> RenderEngine::RequestOutputFrame
|
||||
-> TryInvokeOnRenderThread
|
||||
```
|
||||
|
||||
That makes output production a request/response interaction. The producer waits for the render thread, and the render thread is still shared with other work.
|
||||
|
||||
### Input Upload Shares Output Context
|
||||
|
||||
DeckLink input capture currently flows into:
|
||||
|
||||
```text
|
||||
VideoBackend::HandleInputFrame
|
||||
-> OpenGLVideoIOBridge::UploadInputFrame
|
||||
-> RenderEngine::QueueInputFrame
|
||||
-> render thread upload
|
||||
```
|
||||
|
||||
Even with coalescing, input upload can consume render-thread time and GPU bandwidth directly before output rendering.
|
||||
|
||||
### Preview And Screenshot Share Output Context
|
||||
|
||||
Preview and screenshot are lower-priority features, but today they still execute on the render thread.
|
||||
|
||||
Preview is best-effort at the caller side, but once queued it can still occupy the same context. Screenshot capture can be more expensive because it performs readback and CPU-side image preparation.
|
||||
|
||||
### Startup Context Ownership Is Transitional
|
||||
|
||||
The Win32 startup path creates and binds the GL context before `RenderEngine::StartRenderThread()`.
|
||||
|
||||
That is acceptable as a transitional state, but the final model should make context ownership explicit:
|
||||
|
||||
- bootstrap thread creates the window/context
|
||||
- bootstrap thread releases it
|
||||
- render thread binds it
|
||||
- only render thread initializes GL resources
|
||||
- only render thread destroys GL resources
|
||||
|
||||
### Render Callback Re-enters App State
|
||||
|
||||
`OpenGLRenderPipeline::RenderFrame()` calls a callback into `OpenGLComposite::renderEffect()`.
|
||||
|
||||
That callback builds `RenderFrameInput`, resolves frame state, drains runtime live state, and then calls back into `RenderEngine` to draw the prepared frame.
|
||||
|
||||
This works, but it means the output render path still reaches up into app/runtime code at frame time.
|
||||
|
||||
## Target Runtime Shape
|
||||
|
||||
The main app should match this ownership model:
|
||||
|
||||
```text
|
||||
runtime/control threads
|
||||
-> publish snapshots, live overlays, reset requests, shader-build results
|
||||
-> never call GL
|
||||
|
||||
render cadence thread
|
||||
-> sole owner of output GL context
|
||||
-> wakes at selected render cadence
|
||||
-> samples latest render input/state
|
||||
-> renders one frame
|
||||
-> queues async readback/copies completed readback into system-memory slot
|
||||
-> publishes completed frame to bounded FIFO output reserve
|
||||
|
||||
video output thread
|
||||
-> consumes completed system-memory frames
|
||||
-> schedules DeckLink frames to target buffer depth
|
||||
-> processes completion results
|
||||
-> never calls GL
|
||||
|
||||
optional input upload path
|
||||
-> writes latest input frame into CPU-side latest-frame buffer
|
||||
-> render thread imports/uploads at a controlled point in its frame
|
||||
|
||||
preview/screenshot path
|
||||
-> consumes already-rendered output/system-memory frame when possible
|
||||
-> never interrupts output render cadence
|
||||
```
|
||||
|
||||
## Non-Negotiable Rules
|
||||
|
||||
- The render thread never waits for DeckLink.
|
||||
- DeckLink callbacks never render.
|
||||
- Runtime/control threads never directly execute GL.
|
||||
- Preview and screenshot never execute ahead of output frames.
|
||||
- Input upload is never a separate urgent GL task ahead of output render.
|
||||
- Shader/resource commits are applied only at a frame boundary.
|
||||
- Telemetry on the hot path must be lock-light or try-lock only.
|
||||
- The render thread cadence does not speed up to refill buffers.
|
||||
- If output work overruns, the render thread records the overrun and resumes the selected cadence policy.
|
||||
|
||||
## Implementation Plan
|
||||
|
||||
### 1. Add Thread/Context Ownership Guards
|
||||
|
||||
Add explicit render-thread ownership checks around all GL entry points.
|
||||
|
||||
Deliverables:
|
||||
|
||||
- `RenderEngine` exposes `IsOnRenderThread()` for assertions/tests.
|
||||
- GL-facing classes get debug-only owner checks where practical.
|
||||
- wrong-thread GL access becomes a counted telemetry warning, not just `OutputDebugStringA`.
|
||||
- tests cover that public request methods do not execute GL directly.
|
||||
|
||||
Acceptance:
|
||||
|
||||
- every `RenderEngine` public method is classified as either request-only, lifecycle-only, or render-thread-only.
|
||||
- render-thread-only methods are private or guarded.
|
||||
- no normal runtime caller can accidentally invoke GL work inline.
|
||||
|
||||
### 2. Move GL Initialization Fully Onto The Render Thread
|
||||
|
||||
Start the render thread before compiling shaders and initializing GL resources.
|
||||
|
||||
Current startup does:
|
||||
|
||||
```text
|
||||
InitOpenGLState()
|
||||
-> CompileDecodeShader
|
||||
-> CompileOutputPackShader
|
||||
-> InitializeResources
|
||||
-> CompileLayerPrograms
|
||||
StartRenderThread()
|
||||
```
|
||||
|
||||
Move toward:
|
||||
|
||||
```text
|
||||
create context on Win32 thread
|
||||
release context on Win32 thread
|
||||
StartRenderThread()
|
||||
render thread binds context
|
||||
render thread initializes extensions, shaders, resources
|
||||
```
|
||||
|
||||
Deliverables:
|
||||
|
||||
- a single `RenderEngine::StartAndInitialize(RenderInitializationConfig)` path.
|
||||
- GL extension resolution happens on the render thread.
|
||||
- shader/resource initialization is a render-thread startup phase.
|
||||
- `RenderEngine` destructor only destroys resources on the render thread.
|
||||
|
||||
Acceptance:
|
||||
|
||||
- after `StartRenderThread()`, no non-render thread binds or uses the app GL context.
|
||||
- shutdown order is deterministic: stop video output, stop render cadence, destroy GL resources, release context.
|
||||
|
||||
### 3. Replace Synchronous Output Render Requests With Render-Owned Cadence
|
||||
|
||||
Move output cadence out of `VideoBackend` and into the render system.
|
||||
|
||||
Current:
|
||||
|
||||
```text
|
||||
VideoBackend output producer
|
||||
-> cadence tick
|
||||
-> acquire output slot
|
||||
-> synchronous render-thread request
|
||||
```
|
||||
|
||||
Target:
|
||||
|
||||
```text
|
||||
RenderEngine output cadence loop
|
||||
-> cadence tick
|
||||
-> acquire/free output slot through a non-blocking frame-sink interface
|
||||
-> render frame
|
||||
-> publish completed frame
|
||||
```
|
||||
|
||||
Deliverables:
|
||||
|
||||
- introduce `RenderedFrameSink` or similar interface owned by video output.
|
||||
- render thread pulls/claims a free system-memory slot without waiting.
|
||||
- if no free slot exists, render thread drops/recycles the oldest unscheduled completed frame or records backpressure without blocking.
|
||||
- remove `RenderEngine::RequestOutputFrame()` from the steady-state output path.
|
||||
|
||||
Acceptance:
|
||||
|
||||
- output rendering continues even if DeckLink completion is delayed.
|
||||
- no `std::future` wait exists in the output cadence path.
|
||||
- `VideoBackend` no longer owns the producer render loop; it owns scheduling/completion only.
|
||||
|
||||
### 4. Make The Render Thread A Frame Loop, Not A Task Queue
|
||||
|
||||
Keep a command mailbox, but process it only at safe frame-boundary points.
|
||||
|
||||
Frame loop:
|
||||
|
||||
```text
|
||||
while running:
|
||||
wait until next render timestamp
|
||||
apply bounded frame-boundary commands
|
||||
sample latest frame input/state
|
||||
upload latest input frame if enabled and budget allows
|
||||
render output frame
|
||||
queue/consume readback
|
||||
publish completed frame
|
||||
record timings
|
||||
```
|
||||
|
||||
Command classes:
|
||||
|
||||
- frame-boundary commands: reset temporal history, reset shader feedback, commit prepared shader programs
|
||||
- background/low-priority commands: preview, screenshot, diagnostic readback
|
||||
- non-GL commands: state publication, telemetry, persistence
|
||||
|
||||
Deliverables:
|
||||
|
||||
- replace FIFO render task queue with a priority/mailbox model.
|
||||
- output cadence is the loop's main clock.
|
||||
- commands have budget classes and max work per frame.
|
||||
- long commands are deferred rather than blocking the current output tick.
|
||||
|
||||
Acceptance:
|
||||
|
||||
- preview/screenshot cannot run immediately before a due output frame.
|
||||
- reset/shader work is applied between frames and measured.
|
||||
- output render starts within a small jitter window when the GPU is not overrun.
|
||||
|
||||
### 5. Move Input Capture To A CPU Latest-Frame Buffer
|
||||
|
||||
Input capture should not enqueue independent GL upload tasks.
|
||||
|
||||
Target:
|
||||
|
||||
```text
|
||||
DeckLink input callback
|
||||
-> copy/coalesce latest CPU input frame
|
||||
-> return quickly
|
||||
|
||||
render thread frame boundary
|
||||
-> if input version changed, upload latest frame
|
||||
-> render using last successfully uploaded input texture
|
||||
```
|
||||
|
||||
Deliverables:
|
||||
|
||||
- introduce `InputFrameMailbox` with latest-frame semantics.
|
||||
- remove `RenderEngine::QueueInputFrame()` from the callback path.
|
||||
- render thread owns the upload moment.
|
||||
- if upload would exceed budget, render thread can reuse the previous input texture and record an input-upload skip.
|
||||
|
||||
Acceptance:
|
||||
|
||||
- input capture enabled does not create arbitrary render-thread tasks.
|
||||
- output cadence remains stable when input frames arrive.
|
||||
- telemetry separates input-frame arrival, upload count, upload skips, and upload cost.
|
||||
|
||||
### 6. Move Preview To A Consumer Path
|
||||
|
||||
Preview should consume the latest completed output image instead of asking the output GL context to present.
|
||||
|
||||
Options:
|
||||
|
||||
- CPU preview from latest system-memory output frame.
|
||||
- a separate preview GL context fed asynchronously from completed frames.
|
||||
- a low-priority render-thread blit only when output has measurable slack.
|
||||
|
||||
Recommended first step:
|
||||
|
||||
- use latest system-memory BGRA8 output for the window preview.
|
||||
|
||||
Deliverables:
|
||||
|
||||
- preview reads from latest completed/scheduled output frame copy.
|
||||
- `TryPresentPreview()` no longer queues GL work on the output render thread.
|
||||
- preview FPS throttling remains caller-side.
|
||||
|
||||
Acceptance:
|
||||
|
||||
- forcing preview cannot delay output rendering.
|
||||
- minimizing/focusing the window does not affect output cadence.
|
||||
|
||||
### 7. Move Screenshot To Completed Frame Capture
|
||||
|
||||
Screenshot should capture from the latest completed output frame unless an explicit "exact render capture" mode is requested.
|
||||
|
||||
Deliverables:
|
||||
|
||||
- screenshot request reads the latest system-memory output frame.
|
||||
- PNG write remains async.
|
||||
- optional diagnostic exact-GL screenshot is disabled during live output or explicitly marked disruptive.
|
||||
|
||||
Acceptance:
|
||||
|
||||
- screenshot request does not call `glReadPixels` on the output render context during steady-state playout.
|
||||
|
||||
### 8. Make Shader Commits Frame-Boundary Work
|
||||
|
||||
Prepared shader builds are CPU/background work; GL program commit is still GL work.
|
||||
|
||||
Deliverables:
|
||||
|
||||
- shader build queue produces `PreparedShaderBuild`.
|
||||
- render thread sees latest pending prepared build at a frame boundary.
|
||||
- commit is applied only between frames.
|
||||
- expensive commits can temporarily enter a measured "render reconfigure" state.
|
||||
|
||||
Acceptance:
|
||||
|
||||
- shader commits do not interleave midway through output render.
|
||||
- output timing telemetry records commit duration separately from normal render duration.
|
||||
|
||||
### 9. Split Output Scheduling From Rendering Completely
|
||||
|
||||
`VideoBackend` should become a playout/scheduling owner, not a render producer.
|
||||
|
||||
Target:
|
||||
|
||||
```text
|
||||
RenderEngine
|
||||
-> produces completed frames at render cadence
|
||||
|
||||
VideoBackend
|
||||
-> schedules completed frames up to target DeckLink depth
|
||||
-> processes completions
|
||||
-> releases scheduled slots
|
||||
```
|
||||
|
||||
Deliverables:
|
||||
|
||||
- `VideoBackend` owns `SystemOutputFramePool`, or a new `SystemFrameExchange` owns it between render/video.
|
||||
- render thread publishes completed frames into the exchange.
|
||||
- video output thread schedules from the exchange.
|
||||
- no render calls exist in completion handling or scheduling paths.
|
||||
|
||||
Acceptance:
|
||||
|
||||
- DeckLink buffer depth changes cannot directly cause render-thread wakeups except through non-blocking availability signals.
|
||||
- render cadence can be tested without DeckLink by using a fake frame sink.
|
||||
- video scheduling can be tested without GL by using synthetic frames.
|
||||
|
||||
### 10. Preserve The Probe As The Reference Contract
|
||||
|
||||
The `DeckLinkRenderCadenceProbe` is now the control sample.
|
||||
|
||||
Deliverables:
|
||||
|
||||
- document which main-app components correspond to the probe components.
|
||||
- add a small regression checklist:
|
||||
- render FPS near target
|
||||
- schedule FPS near target
|
||||
- DeckLink buffered frames stable
|
||||
- no late/drop frames
|
||||
- no PBO misses or readback stalls
|
||||
- focus/minimize does not change output cadence
|
||||
|
||||
Acceptance:
|
||||
|
||||
- after each migration step, compare the main app telemetry against the probe's known-good behavior.
|
||||
|
||||
## Suggested Order Of Work
|
||||
|
||||
1. Add ownership guards and classify render methods.
|
||||
2. Move GL initialization/destruction fully onto the render thread.
|
||||
3. Introduce a render-owned cadence loop behind a feature flag.
|
||||
4. Add a frame-sink/exchange interface between render and video.
|
||||
5. Move output production from `VideoBackend` to the render cadence loop.
|
||||
6. Convert input upload to latest-frame mailbox semantics.
|
||||
7. Move preview to completed-frame consumption.
|
||||
8. Move screenshot to completed-frame capture.
|
||||
9. Convert shader commits/resets to frame-boundary mailbox commands.
|
||||
10. Remove old synchronous output render request path.
|
||||
|
||||
## Feature Flags During Migration
|
||||
|
||||
Use flags only to keep testing safe, not as long-term compatibility layers.
|
||||
|
||||
Suggested flags:
|
||||
|
||||
```text
|
||||
VST_RENDER_CADENCE_OWNER=render_thread
|
||||
VST_DISABLE_INPUT_CAPTURE=1
|
||||
VST_PREVIEW_SOURCE=system_frame
|
||||
VST_SCREENSHOT_SOURCE=system_frame
|
||||
```
|
||||
|
||||
Remove each flag once the new behavior is proven and becomes the only supported path.
|
||||
|
||||
## Telemetry Needed
|
||||
|
||||
Add or preserve counters for:
|
||||
|
||||
- render tick jitter
|
||||
- render tick overrun
|
||||
- output render duration
|
||||
- GL command mailbox depth by class
|
||||
- frame-boundary command duration
|
||||
- input upload duration and skips
|
||||
- readback queue/consume duration
|
||||
- completed system-memory frame depth
|
||||
- scheduled DeckLink frame depth
|
||||
- DeckLink actual buffered frames
|
||||
- preview frames consumed
|
||||
- screenshot requests served from system memory
|
||||
|
||||
The key metric is whether output render starts on time. Buffer depth alone is not enough; a full buffer can still contain stale or repeated frames.
|
||||
|
||||
## Completion Definition
|
||||
|
||||
This work is complete when:
|
||||
|
||||
- the output render thread owns the app GL context from initialization through shutdown
|
||||
- output rendering is driven by the render thread's selected frame cadence
|
||||
- no non-output task can run ahead of a due output frame
|
||||
- `VideoBackend` never asks the render thread to render synchronously
|
||||
- DeckLink scheduling consumes already completed system-memory frames
|
||||
- input upload, preview, screenshot, shader commits, and resets are all frame-boundary, mailbox, or consumer-side operations
|
||||
- main-app telemetry approaches the cadence probe behavior under the same output mode
|
||||
@@ -6,17 +6,20 @@ info:
|
||||
REST API exposed by the local Video Shader Toys control server.
|
||||
|
||||
The API is intended for local control tools and the bundled React UI. All mutating
|
||||
endpoints return a small action result object. Successful mutating requests also
|
||||
broadcast the latest runtime state over the `/ws` WebSocket.
|
||||
endpoints return a small action result object.
|
||||
|
||||
WebSocket state streaming is not described by OpenAPI; connect to `ws://127.0.0.1:{port}/ws`
|
||||
to receive full runtime state JSON messages whenever state changes.
|
||||
RenderCadenceCompositor serves `/api/state` for snapshots and `/ws` for local
|
||||
WebSocket state updates consumed by the bundled control UI.
|
||||
servers:
|
||||
- url: http://127.0.0.1:8080
|
||||
description: Default local control server
|
||||
tags:
|
||||
- name: State
|
||||
description: Runtime state and status.
|
||||
- name: Static
|
||||
description: Bundled control UI and static assets served by the local host.
|
||||
- name: Docs
|
||||
description: OpenAPI and Swagger UI documentation served by the local host.
|
||||
- name: Layers
|
||||
description: Layer stack control.
|
||||
- name: Stack Presets
|
||||
@@ -24,6 +27,146 @@ tags:
|
||||
- name: Runtime
|
||||
description: Runtime actions.
|
||||
paths:
|
||||
/:
|
||||
get:
|
||||
tags: [Static]
|
||||
summary: Serve the bundled control UI
|
||||
description: Returns the built React control UI `index.html` from `ui/dist`.
|
||||
operationId: getControlUiRoot
|
||||
responses:
|
||||
"200":
|
||||
description: Control UI HTML.
|
||||
content:
|
||||
text/html:
|
||||
schema:
|
||||
type: string
|
||||
"404":
|
||||
description: UI bundle was not found.
|
||||
content:
|
||||
text/plain:
|
||||
schema:
|
||||
type: string
|
||||
/index.html:
|
||||
get:
|
||||
tags: [Static]
|
||||
summary: Serve the bundled control UI index file
|
||||
description: Returns the built React control UI `index.html` from `ui/dist`.
|
||||
operationId: getControlUiIndex
|
||||
responses:
|
||||
"200":
|
||||
description: Control UI HTML.
|
||||
content:
|
||||
text/html:
|
||||
schema:
|
||||
type: string
|
||||
"404":
|
||||
description: UI bundle was not found.
|
||||
content:
|
||||
text/plain:
|
||||
schema:
|
||||
type: string
|
||||
/assets/{assetPath}:
|
||||
get:
|
||||
tags: [Static]
|
||||
summary: Serve a bundled control UI asset
|
||||
description: Serves files from `ui/dist/assets`. The server rejects unsafe relative paths and guesses the content type from the file extension.
|
||||
operationId: getControlUiAsset
|
||||
parameters:
|
||||
- name: assetPath
|
||||
in: path
|
||||
required: true
|
||||
description: Relative asset path below `ui/dist/assets`.
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
"200":
|
||||
description: Static asset.
|
||||
content:
|
||||
text/javascript:
|
||||
schema:
|
||||
type: string
|
||||
text/css:
|
||||
schema:
|
||||
type: string
|
||||
image/svg+xml:
|
||||
schema:
|
||||
type: string
|
||||
image/png:
|
||||
schema:
|
||||
type: string
|
||||
format: binary
|
||||
text/plain:
|
||||
schema:
|
||||
type: string
|
||||
"404":
|
||||
description: Asset was not found or the path was unsafe.
|
||||
content:
|
||||
text/plain:
|
||||
schema:
|
||||
type: string
|
||||
/docs:
|
||||
get:
|
||||
tags: [Docs]
|
||||
summary: Serve Swagger UI
|
||||
description: Returns a small Swagger UI page pointed at `/docs/openapi.yaml`.
|
||||
operationId: getSwaggerUi
|
||||
responses:
|
||||
"200":
|
||||
description: Swagger UI HTML.
|
||||
content:
|
||||
text/html:
|
||||
schema:
|
||||
type: string
|
||||
/docs/:
|
||||
get:
|
||||
tags: [Docs]
|
||||
summary: Serve Swagger UI
|
||||
description: Alias for `/docs`.
|
||||
operationId: getSwaggerUiWithTrailingSlash
|
||||
responses:
|
||||
"200":
|
||||
description: Swagger UI HTML.
|
||||
content:
|
||||
text/html:
|
||||
schema:
|
||||
type: string
|
||||
/docs/openapi.yaml:
|
||||
get:
|
||||
tags: [Docs]
|
||||
summary: Serve the OpenAPI document
|
||||
operationId: getOpenApiDocumentFromDocs
|
||||
responses:
|
||||
"200":
|
||||
description: OpenAPI YAML document.
|
||||
content:
|
||||
application/yaml:
|
||||
schema:
|
||||
type: string
|
||||
"404":
|
||||
description: OpenAPI document was not found.
|
||||
content:
|
||||
text/plain:
|
||||
schema:
|
||||
type: string
|
||||
/openapi.yaml:
|
||||
get:
|
||||
tags: [Docs]
|
||||
summary: Serve the OpenAPI document
|
||||
description: Alias for `/docs/openapi.yaml`.
|
||||
operationId: getOpenApiDocument
|
||||
responses:
|
||||
"200":
|
||||
description: OpenAPI YAML document.
|
||||
content:
|
||||
application/yaml:
|
||||
schema:
|
||||
type: string
|
||||
"404":
|
||||
description: OpenAPI document was not found.
|
||||
content:
|
||||
text/plain:
|
||||
schema:
|
||||
type: string
|
||||
/api/state:
|
||||
get:
|
||||
tags: [State]
|
||||
@@ -36,6 +179,24 @@ paths:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/RuntimeState"
|
||||
/ws:
|
||||
get:
|
||||
tags: [State]
|
||||
summary: Stream runtime state over WebSocket
|
||||
description: |
|
||||
Upgrades to a WebSocket connection. The server sends JSON runtime-state
|
||||
snapshots using the same shape as `GET /api/state` whenever the serialized
|
||||
state changes.
|
||||
operationId: streamRuntimeState
|
||||
responses:
|
||||
"101":
|
||||
description: WebSocket protocol upgrade accepted.
|
||||
"400":
|
||||
description: The request was not a valid WebSocket upgrade.
|
||||
content:
|
||||
text/plain:
|
||||
schema:
|
||||
type: string
|
||||
/api/layers/add:
|
||||
post:
|
||||
tags: [Layers]
|
||||
@@ -363,6 +524,10 @@ components:
|
||||
$ref: "#/components/schemas/VideoIOStatus"
|
||||
performance:
|
||||
$ref: "#/components/schemas/PerformanceStatus"
|
||||
backendPlayout:
|
||||
$ref: "#/components/schemas/BackendPlayoutStatus"
|
||||
runtimeEvents:
|
||||
$ref: "#/components/schemas/RuntimeEventStatus"
|
||||
shaders:
|
||||
type: array
|
||||
items:
|
||||
@@ -382,10 +547,16 @@ components:
|
||||
type: number
|
||||
oscPort:
|
||||
type: number
|
||||
oscBindAddress:
|
||||
type: string
|
||||
oscSmoothing:
|
||||
type: number
|
||||
autoReload:
|
||||
type: boolean
|
||||
maxTemporalHistoryFrames:
|
||||
type: number
|
||||
previewFps:
|
||||
type: number
|
||||
enableExternalKeying:
|
||||
type: boolean
|
||||
inputVideoFormat:
|
||||
@@ -462,10 +633,12 @@ components:
|
||||
type: number
|
||||
renderMs:
|
||||
type: number
|
||||
description: Alias of cadence.renderFrameMs for clients that read the top-level performance object.
|
||||
smoothedRenderMs:
|
||||
type: number
|
||||
budgetUsedPercent:
|
||||
type: number
|
||||
description: Alias of cadence.renderFrameBudgetUsedPercent for clients that read the top-level performance object.
|
||||
completionIntervalMs:
|
||||
type: number
|
||||
smoothedCompletionIntervalMs:
|
||||
@@ -478,6 +651,262 @@ components:
|
||||
type: number
|
||||
flushedFrameCount:
|
||||
type: number
|
||||
cadence:
|
||||
$ref: "#/components/schemas/CadenceTelemetry"
|
||||
CadenceTelemetry:
|
||||
type: object
|
||||
properties:
|
||||
clockOverruns:
|
||||
type: number
|
||||
description: Render cadence overruns where the render thread was late enough to skip one or more frame intervals.
|
||||
clockSkippedFrames:
|
||||
type: number
|
||||
description: Total render cadence frame intervals skipped instead of catch-up rendering.
|
||||
clockOveruns:
|
||||
type: number
|
||||
deprecated: true
|
||||
description: Deprecated misspelled alias for clockOverruns.
|
||||
clockSkipped:
|
||||
type: number
|
||||
deprecated: true
|
||||
description: Deprecated alias for clockSkippedFrames.
|
||||
renderFrameMs:
|
||||
type: number
|
||||
description: Most recent render-thread frame draw duration in milliseconds, excluding completed-readback copy and readback queue work.
|
||||
renderFrameBudgetUsedPercent:
|
||||
type: number
|
||||
description: Most recent render-thread frame draw duration as a percentage of the selected frame budget.
|
||||
renderFrameMaxMs:
|
||||
type: number
|
||||
description: Maximum observed render-thread frame draw duration in milliseconds for this process.
|
||||
readbackQueueMs:
|
||||
type: number
|
||||
description: Most recent duration spent queueing BGRA8 async PBO readback after rendering.
|
||||
completedReadbackCopyMs:
|
||||
type: number
|
||||
description: Most recent duration spent mapping and copying a completed BGRA8 readback into system-memory frame storage.
|
||||
completedDrops:
|
||||
type: number
|
||||
description: Number of completed unscheduled system-memory frames dropped so render could reuse the slot.
|
||||
acquireMisses:
|
||||
type: number
|
||||
description: Number of times render/readback could not acquire a writable system-memory frame slot.
|
||||
inputFramesReceived:
|
||||
type: number
|
||||
inputFramesDropped:
|
||||
type: number
|
||||
inputConsumeMisses:
|
||||
type: number
|
||||
description: Render ticks where no ready input frame was available to upload.
|
||||
inputUploadMisses:
|
||||
type: number
|
||||
description: Input texture upload attempts that reused the previous GL input texture.
|
||||
inputReadyFrames:
|
||||
type: number
|
||||
description: Ready input frames currently queued in the input mailbox.
|
||||
inputReadingFrames:
|
||||
type: number
|
||||
description: Input frames currently protected while render uploads them.
|
||||
inputLatestAgeMs:
|
||||
type: number
|
||||
inputUploadMs:
|
||||
type: number
|
||||
inputCaptureFps:
|
||||
type: number
|
||||
inputConvertMs:
|
||||
type: number
|
||||
inputSubmitMs:
|
||||
type: number
|
||||
inputCaptureFormat:
|
||||
type: string
|
||||
deckLinkScheduleLeadAvailable:
|
||||
type: boolean
|
||||
description: Whether DeckLink playback stream-time lead telemetry is currently available.
|
||||
deckLinkScheduleLeadFrames:
|
||||
type: number
|
||||
nullable: true
|
||||
description: Estimated number of frame intervals between the next app schedule timestamp and the DeckLink playback frame index.
|
||||
deckLinkPlaybackFrameIndex:
|
||||
type: number
|
||||
description: DeckLink playback stream time converted to frame index at the configured output cadence.
|
||||
deckLinkNextScheduleFrameIndex:
|
||||
type: number
|
||||
description: Next frame index the app scheduler will assign to a DeckLink output frame.
|
||||
deckLinkPlaybackStreamTime:
|
||||
type: number
|
||||
description: Raw DeckLink scheduled playback stream time in the output mode time scale.
|
||||
deckLinkScheduleRealignments:
|
||||
type: number
|
||||
description: Count of schedule-cursor recovery realignments triggered by DeckLink late/drop pressure.
|
||||
BackendPlayoutStatus:
|
||||
type: object
|
||||
properties:
|
||||
lifecycleState:
|
||||
type: string
|
||||
example: running
|
||||
degraded:
|
||||
type: boolean
|
||||
statusMessage:
|
||||
type: string
|
||||
lateFrameCount:
|
||||
type: number
|
||||
droppedFrameCount:
|
||||
type: number
|
||||
flushedFrameCount:
|
||||
type: number
|
||||
readyQueue:
|
||||
$ref: "#/components/schemas/BackendReadyQueueStatus"
|
||||
outputRender:
|
||||
$ref: "#/components/schemas/BackendOutputRenderStatus"
|
||||
recovery:
|
||||
$ref: "#/components/schemas/BackendPlayoutRecoveryStatus"
|
||||
BackendReadyQueueStatus:
|
||||
type: object
|
||||
properties:
|
||||
depth:
|
||||
type: number
|
||||
description: Current number of ready output frames.
|
||||
capacity:
|
||||
type: number
|
||||
description: Maximum ready output frames currently allowed.
|
||||
minDepth:
|
||||
type: number
|
||||
description: Minimum observed ready queue depth since backend worker start.
|
||||
maxDepth:
|
||||
type: number
|
||||
description: Maximum observed ready queue depth since backend worker start.
|
||||
zeroDepthCount:
|
||||
type: number
|
||||
description: Number of observed samples where the ready queue was empty.
|
||||
pushedCount:
|
||||
type: number
|
||||
poppedCount:
|
||||
type: number
|
||||
droppedCount:
|
||||
type: number
|
||||
underrunCount:
|
||||
type: number
|
||||
BackendOutputRenderStatus:
|
||||
type: object
|
||||
properties:
|
||||
renderMs:
|
||||
type: number
|
||||
description: Most recent output render duration in milliseconds.
|
||||
smoothedRenderMs:
|
||||
type: number
|
||||
description: Smoothed output render duration in milliseconds.
|
||||
maxRenderMs:
|
||||
type: number
|
||||
description: Maximum observed output render duration in milliseconds.
|
||||
acquireFrameMs:
|
||||
type: number
|
||||
description: Time spent acquiring a writable backend output frame in milliseconds.
|
||||
renderRequestMs:
|
||||
type: number
|
||||
description: Time spent executing the render-thread output frame request in milliseconds.
|
||||
endAccessMs:
|
||||
type: number
|
||||
description: Time spent ending write access to the backend output frame in milliseconds.
|
||||
queueWaitMs:
|
||||
type: number
|
||||
description: Time the output render request spent waiting for the render thread in milliseconds.
|
||||
drawMs:
|
||||
type: number
|
||||
description: Time spent drawing, blitting, packing, and flushing the output frame in milliseconds.
|
||||
fenceWaitMs:
|
||||
type: number
|
||||
description: Time spent waiting for the async readback fence in milliseconds.
|
||||
mapMs:
|
||||
type: number
|
||||
description: Time spent mapping the async readback pixel buffer in milliseconds.
|
||||
readbackCopyMs:
|
||||
type: number
|
||||
description: Time spent copying async readback bytes into the backend output frame in milliseconds.
|
||||
cachedCopyMs:
|
||||
type: number
|
||||
description: Time spent copying the cached output frame when async readback is not ready in milliseconds.
|
||||
asyncQueueMs:
|
||||
type: number
|
||||
description: Time spent queueing the next async readback in milliseconds.
|
||||
asyncQueueBufferMs:
|
||||
type: number
|
||||
description: Time spent orphaning or allocating the async readback pixel buffer in milliseconds.
|
||||
asyncQueueSetupMs:
|
||||
type: number
|
||||
description: Time spent applying readback pixel-store, framebuffer, and pixel-pack-buffer state in milliseconds.
|
||||
asyncQueueReadPixelsMs:
|
||||
type: number
|
||||
description: Time spent issuing glReadPixels for the async readback in milliseconds.
|
||||
asyncQueueFenceMs:
|
||||
type: number
|
||||
description: Time spent creating the async readback fence in milliseconds.
|
||||
syncReadMs:
|
||||
type: number
|
||||
description: Time spent in bootstrap synchronous readback in milliseconds.
|
||||
asyncReadbackMissCount:
|
||||
type: number
|
||||
description: Count of output render requests where async readback was not ready.
|
||||
cachedFallbackCount:
|
||||
type: number
|
||||
description: Count of output render requests served from the cached output frame.
|
||||
syncFallbackCount:
|
||||
type: number
|
||||
description: Count of output render requests that used bootstrap synchronous readback.
|
||||
BackendPlayoutRecoveryStatus:
|
||||
type: object
|
||||
properties:
|
||||
completionResult:
|
||||
type: string
|
||||
enum: [Completed, DisplayedLate, Dropped, Flushed, Unknown]
|
||||
completedFrameIndex:
|
||||
type: number
|
||||
scheduledFrameIndex:
|
||||
type: number
|
||||
scheduledLeadFrames:
|
||||
type: number
|
||||
measuredLagFrames:
|
||||
type: number
|
||||
catchUpFrames:
|
||||
type: number
|
||||
lateStreak:
|
||||
type: number
|
||||
dropStreak:
|
||||
type: number
|
||||
RuntimeEventStatus:
|
||||
type: object
|
||||
properties:
|
||||
queue:
|
||||
$ref: "#/components/schemas/RuntimeEventQueueStatus"
|
||||
dispatch:
|
||||
$ref: "#/components/schemas/RuntimeEventDispatchStatus"
|
||||
RuntimeEventQueueStatus:
|
||||
type: object
|
||||
properties:
|
||||
name:
|
||||
type: string
|
||||
depth:
|
||||
type: number
|
||||
capacity:
|
||||
type: number
|
||||
droppedCount:
|
||||
type: number
|
||||
oldestEventAgeMs:
|
||||
type: number
|
||||
RuntimeEventDispatchStatus:
|
||||
type: object
|
||||
properties:
|
||||
dispatchCallCount:
|
||||
type: number
|
||||
dispatchedEventCount:
|
||||
type: number
|
||||
handlerInvocationCount:
|
||||
type: number
|
||||
handlerFailureCount:
|
||||
type: number
|
||||
lastDispatchDurationMs:
|
||||
type: number
|
||||
maxDispatchDurationMs:
|
||||
type: number
|
||||
ShaderSummary:
|
||||
type: object
|
||||
properties:
|
||||
@@ -497,6 +926,8 @@ components:
|
||||
description: Error text for unavailable shader packages.
|
||||
temporal:
|
||||
$ref: "#/components/schemas/TemporalState"
|
||||
feedback:
|
||||
$ref: "#/components/schemas/FeedbackState"
|
||||
TemporalState:
|
||||
type: object
|
||||
properties:
|
||||
@@ -509,6 +940,13 @@ components:
|
||||
type: number
|
||||
effectiveHistoryLength:
|
||||
type: number
|
||||
FeedbackState:
|
||||
type: object
|
||||
properties:
|
||||
enabled:
|
||||
type: boolean
|
||||
writePass:
|
||||
type: string
|
||||
LayerState:
|
||||
type: object
|
||||
properties:
|
||||
|
||||
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`, with `RuntimeStore` and the later persistence writer carrying out durable writes when policy requests them.
|
||||
|
||||
### Render Snapshot Publication
|
||||
|
||||
`ControlServices` must not publish render-facing snapshots or poke render-local structures directly.
|
||||
|
||||
### Render-Local Overlay Ownership
|
||||
|
||||
Live OSC automation overlays belong to the live-state/render preparation boundary (`RuntimeLiveState` today). Temporal state, shader feedback, output staging, and other render-only transient state belong to `RenderEngine`.
|
||||
|
||||
`ControlServices` may ingest and coalesce automation targets, but it should not own how those targets are composed, committed, persisted, or applied inside the render domain.
|
||||
|
||||
### Hardware Timing or Playout Recovery
|
||||
|
||||
Device scheduling, queue headroom, and callback recovery belong to `VideoBackend`, not the control ingress path.
|
||||
|
||||
## Ingress Boundary Model
|
||||
|
||||
The clean boundary for `ControlServices` is:
|
||||
|
||||
- external transport in
|
||||
- typed action/event out
|
||||
|
||||
That implies three layers inside the subsystem.
|
||||
|
||||
### Transport Adapters
|
||||
|
||||
These are protocol-facing components.
|
||||
|
||||
Examples:
|
||||
|
||||
- `OscIngress`
|
||||
- `HttpControlIngress`
|
||||
- `WebSocketSessionHost`
|
||||
- `FileWatchIngress`
|
||||
|
||||
Responsibilities:
|
||||
|
||||
- socket/file watcher lifecycle
|
||||
- protocol decoding
|
||||
- request framing
|
||||
- transport-level validation
|
||||
- low-level authentication or origin checks later if added
|
||||
|
||||
### Normalization Layer
|
||||
|
||||
This layer translates decoded transport input into typed actions.
|
||||
|
||||
Responsibilities:
|
||||
|
||||
- route parsing
|
||||
- payload type normalization
|
||||
- parameter name/key resolution where that is purely syntactic
|
||||
- conversion from transport-specific errors into typed ingress errors
|
||||
|
||||
This layer should not perform deep runtime mutation policy.
|
||||
|
||||
### Service Coordination Shell
|
||||
|
||||
This shell owns:
|
||||
|
||||
- startup/shutdown ordering for ingress services
|
||||
- shared ingress-local queues
|
||||
- service-thread lifecycle
|
||||
- handing normalized actions to `RuntimeCoordinator`
|
||||
- handing outbound snapshot payloads to WebSocket clients
|
||||
|
||||
This shell is the spiritual successor to the hosting part of current `RuntimeServices`, but with a much narrower responsibility set.
|
||||
|
||||
## Service Timing Concerns
|
||||
|
||||
`ControlServices` is the correct place to isolate transport-level timing concerns that should not leak into whole-app state policy.
|
||||
|
||||
### OSC Timing
|
||||
|
||||
Current behavior already points in the right direction:
|
||||
|
||||
- OSC receive is on its own thread in `OscServer`
|
||||
- latest values are coalesced by route in `RuntimeServices`
|
||||
- updates are applied once per render tick rather than per packet
|
||||
|
||||
Relevant code:
|
||||
|
||||
- [OscServer.cpp](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/control/OscServer.cpp:95)
|
||||
- [RuntimeServices.cpp](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/control/RuntimeServices.cpp:65)
|
||||
- [RuntimeServices.cpp](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/control/RuntimeServices.cpp:82)
|
||||
|
||||
Target rule:
|
||||
|
||||
- network receive and decode stay inside `ControlServices`
|
||||
- coalescing policy stays inside `ControlServices`
|
||||
- classification of the resulting action belongs to `RuntimeCoordinator`
|
||||
- render-local application belongs to `RenderEngine`
|
||||
|
||||
This keeps high-rate ingress cheap without giving the service layer authority over render behavior or committed-state policy.
|
||||
|
||||
### HTTP / UI Timing
|
||||
|
||||
HTTP control requests are operator-facing and usually low-rate, but the UI can still generate bursts through slider drags or repeated parameter edits.
|
||||
|
||||
`ControlServices` should:
|
||||
|
||||
- normalize each request into a typed action
|
||||
- allow collapse/throttle policies for purely observational outbound state pushes
|
||||
- avoid synchronous full-state serialization on every ingress event where possible
|
||||
|
||||
It should not decide whether a request results in immediate, deferred, transient, or persisted mutation. That is a coordinator concern.
|
||||
|
||||
### WebSocket Broadcast Timing
|
||||
|
||||
Outbound state streaming is control-plane behavior, not core runtime ownership.
|
||||
|
||||
Current code already distinguishes immediate and requested broadcasts:
|
||||
|
||||
- [ControlServer.cpp](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/control/ControlServer.cpp:163)
|
||||
- [ControlServer.cpp](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/control/ControlServer.cpp:170)
|
||||
|
||||
Target rule:
|
||||
|
||||
- `ControlServices` may own broadcast scheduling and collapse policy
|
||||
- the source state payload should come from snapshot/telemetry producers, not from service-owned mutable state
|
||||
|
||||
### File-Watch Timing
|
||||
|
||||
Current file-watch and deferred OSC commit work run on a coarse poll loop:
|
||||
|
||||
- [RuntimeServices.cpp](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/control/RuntimeServices.cpp:194)
|
||||
|
||||
This is one of the cleaner migration opportunities in the whole app.
|
||||
|
||||
Target rule:
|
||||
|
||||
- file-watch detection belongs in `ControlServices`
|
||||
- coarse polling should eventually be replaced with either event-driven watching or a narrower, typed background loop
|
||||
- detected changes should be debounced and surfaced as typed reload-related actions
|
||||
|
||||
### Service Backpressure
|
||||
|
||||
`ControlServices` needs explicit backpressure rules for high-rate sources.
|
||||
|
||||
Recommended policies:
|
||||
|
||||
- coalesce latest-value automation by route
|
||||
- bound per-service queues
|
||||
- count and report dropped/coalesced events
|
||||
- prefer collapsing observation work before collapsing operator mutations
|
||||
- never let service queues become hidden durable state
|
||||
|
||||
## Interfaces
|
||||
|
||||
These are suggested target-facing interfaces, not final class signatures.
|
||||
|
||||
### Subsystem Shell
|
||||
|
||||
Possible top-level responsibilities:
|
||||
|
||||
- `Start(...)`
|
||||
- `Stop()`
|
||||
- `PublishStateSnapshot(...)`
|
||||
- `PublishHealthSnapshot(...)`
|
||||
- `DrainNormalizedActions(...)`
|
||||
|
||||
The shell should feel like a host for ingress adapters plus a normalization/buffering boundary.
|
||||
|
||||
### OSC Ingress
|
||||
|
||||
Possible responsibilities:
|
||||
|
||||
- `StartOscIngress(...)`
|
||||
- `StopOscIngress()`
|
||||
- `ConfigureOscBinding(...)`
|
||||
- `EnqueueDecodedOscMessage(...)`
|
||||
- `DrainCoalescedAutomationTargets(...)`
|
||||
|
||||
### HTTP / Web Control Ingress
|
||||
|
||||
Possible responsibilities:
|
||||
|
||||
- `StartHttpIngress(...)`
|
||||
- `StopHttpIngress()`
|
||||
- `HandleHttpRequest(...)`
|
||||
- `HandleWebSocketUpgrade(...)`
|
||||
- `QueueStateBroadcastRequest()`
|
||||
|
||||
### File-Watch Ingress
|
||||
|
||||
Possible responsibilities:
|
||||
|
||||
- `StartFileWatchIngress(...)`
|
||||
- `StopFileWatchIngress()`
|
||||
- `PollOrConsumeFileEvents(...)`
|
||||
- `DrainReloadSignals(...)`
|
||||
|
||||
### Normalized Action Types
|
||||
|
||||
These should likely become shared event/action definitions in Phase 2, but `ControlServices` should be designed around them now.
|
||||
|
||||
Examples:
|
||||
|
||||
- `LayerAddRequested`
|
||||
- `LayerRemovedRequested`
|
||||
- `LayerReorderedRequested`
|
||||
- `LayerBypassSetRequested`
|
||||
- `LayerShaderSetRequested`
|
||||
- `ParameterSetRequested`
|
||||
- `LayerResetRequested`
|
||||
- `StackPresetSaveRequested`
|
||||
- `StackPresetLoadRequested`
|
||||
- `ShaderReloadRequested`
|
||||
- `ScreenshotRequested`
|
||||
- `AutomationTargetReceived`
|
||||
- `RegistryChangeDetected`
|
||||
|
||||
## Data Ownership Inside The Subsystem
|
||||
|
||||
`ControlServices` is allowed to own ingress-local ephemeral state.
|
||||
|
||||
Examples:
|
||||
|
||||
- connected WebSocket client list
|
||||
- pending broadcast flag
|
||||
- coalesced OSC route map
|
||||
- outstanding decoded-but-undrained action queue
|
||||
- file-watch debounce state
|
||||
- transport error counters before publication to telemetry
|
||||
|
||||
It should not own:
|
||||
|
||||
- authoritative layer stack state
|
||||
- committed parameter values
|
||||
- render snapshots
|
||||
- playout queue state
|
||||
- shader feedback or render overlays
|
||||
|
||||
The rule is simple:
|
||||
|
||||
- if the state exists only to absorb or forward external input, it can live here
|
||||
- if the state defines how the app should behave over time, it belongs elsewhere
|
||||
|
||||
## Outbound Boundaries
|
||||
|
||||
`ControlServices` talks outward in only a few approved directions.
|
||||
|
||||
### To `RuntimeCoordinator`
|
||||
|
||||
Primary outbound path.
|
||||
|
||||
It sends:
|
||||
|
||||
- normalized mutation requests
|
||||
- automation targets
|
||||
- reload requests
|
||||
- stack preset requests
|
||||
|
||||
It does not send:
|
||||
|
||||
- transport-specific objects such as raw sockets or OSC packet structures
|
||||
- render-facing state objects
|
||||
|
||||
### To `HealthTelemetry`
|
||||
|
||||
Observation-only relationship.
|
||||
|
||||
It sends:
|
||||
|
||||
- counters
|
||||
- warnings
|
||||
- timing samples
|
||||
- service health transitions
|
||||
|
||||
It should not use `HealthTelemetry` as a hidden control path.
|
||||
|
||||
### From Snapshot / Telemetry Producers To Web Clients
|
||||
|
||||
`ControlServices` may deliver serialized outbound payloads to WebSocket clients, but the authoritative payload contents should be produced by the owning subsystems.
|
||||
|
||||
That means a later design may look like:
|
||||
|
||||
- `RuntimeSnapshotProvider` provides render-facing snapshot payloads or a runtime-state projection derived from those published snapshots
|
||||
- `RuntimeCoordinator` or a later runtime-read-model helper provides control-plane runtime summaries when the UI needs more than raw render state
|
||||
- `HealthTelemetry` provides health payloads
|
||||
- `ControlServices` delivers them to connected observers
|
||||
|
||||
## Current Code Mapping
|
||||
|
||||
This section maps the current implementation onto the target subsystem.
|
||||
|
||||
### Current `RuntimeServices`
|
||||
|
||||
Should split into:
|
||||
|
||||
- `ControlServices` shell
|
||||
- temporary compatibility adapter into `RuntimeCoordinator`
|
||||
- removal of any direct runtime-state mutation responsibilities over time
|
||||
|
||||
Likely keep under `ControlServices`:
|
||||
|
||||
- service startup/shutdown
|
||||
- OSC update coalescing
|
||||
- Web control hosting shell
|
||||
- file-watch ingress hosting
|
||||
|
||||
Should move out later:
|
||||
|
||||
- legacy direct runtime polling dependency
|
||||
- deferred OSC commit behavior that has since moved behind coordinator-facing outcomes
|
||||
- any remaining direct state-broadcast decisions tied to runtime internals
|
||||
|
||||
### Current `ControlServer`
|
||||
|
||||
Should become primarily:
|
||||
|
||||
- HTTP ingress adapter
|
||||
- WebSocket session host
|
||||
- static asset/doc host if that remains embedded
|
||||
|
||||
The callback table in current code:
|
||||
|
||||
- [ControlServer.h](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/control/ControlServer.h:18)
|
||||
|
||||
is a useful migration aid, but long-term it should evolve from callback-per-action toward typed action emission.
|
||||
|
||||
### Current `OscServer`
|
||||
|
||||
Should remain transport-focused.
|
||||
|
||||
Its clean long-term responsibilities are:
|
||||
|
||||
- UDP socket lifecycle
|
||||
- OSC frame decode
|
||||
- syntactic route extraction
|
||||
- emitting decoded automation payloads into the `ControlServices` shell
|
||||
|
||||
It should not own any runtime state semantics beyond ingress decoding.
|
||||
|
||||
## Migration Plan
|
||||
|
||||
The safest migration is incremental.
|
||||
|
||||
### Step 1. Name The Boundary Explicitly
|
||||
|
||||
Create and use the `ControlServices` name in docs and future interfaces before moving all logic.
|
||||
|
||||
This document is part of that step.
|
||||
|
||||
### Step 2. Convert Callback Thinking Into Action Thinking
|
||||
|
||||
Without changing all runtime code at once, introduce typed action/event shapes for the major ingress paths.
|
||||
|
||||
The goal is for transports to emit actions, even if temporary adapters still call into existing code.
|
||||
|
||||
### Step 3. Extract Service Hosting From `OpenGLComposite`
|
||||
|
||||
`OpenGLComposite` currently owns `RuntimeServices` startup and consumption:
|
||||
|
||||
- [OpenGLComposite.cpp](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/gl/composite/OpenGLComposite.cpp:312)
|
||||
- [OpenGLComposite.cpp](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/gl/composite/OpenGLComposite.cpp:723)
|
||||
|
||||
That should move toward a composition root or subsystem host arrangement where render is no longer the owner of control ingress.
|
||||
|
||||
### Step 4. Remove Direct Runtime Mutation Dependency
|
||||
|
||||
Previous polling and deferred OSC commit work directly against runtime storage:
|
||||
|
||||
- [RuntimeServices.cpp](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/control/RuntimeServices.cpp:194)
|
||||
|
||||
That has been routed through coordinator-facing actions; later phases should replace the remaining polling shape with event-driven flows.
|
||||
|
||||
### Step 5. Split Out Observation Delivery
|
||||
|
||||
WebSocket outbound delivery can stay in `ControlServices`, but serialization ownership should move toward the owning subsystems so the service layer stops assembling authoritative state itself.
|
||||
|
||||
## Risks
|
||||
|
||||
### Risk 1. Recreating `RuntimeHost` Coupling Under A New Name
|
||||
|
||||
If `ControlServices` is allowed to keep direct knowledge of runtime mutation internals, it will become a renamed version of the same coupling problem.
|
||||
|
||||
Mitigation:
|
||||
|
||||
- keep the boundary strict
|
||||
- route mutations through coordinator interfaces
|
||||
- treat any direct runtime mutation calls as migration-only compatibility
|
||||
|
||||
### Risk 2. Service Queues Becoming Hidden State Authority
|
||||
|
||||
Latest-value OSC maps and reload debounce flags are appropriate here. Full committed runtime state is not.
|
||||
|
||||
Mitigation:
|
||||
|
||||
- define ingress-local versus authoritative state explicitly
|
||||
- bound queues
|
||||
- publish queue metrics into telemetry
|
||||
|
||||
### Risk 3. WebSocket Broadcast Path Reintroducing Heavy Synchronous Work
|
||||
|
||||
If `ControlServices` becomes the place where whole runtime state is rebuilt or serialized on every input, it will recreate timing stalls.
|
||||
|
||||
Mitigation:
|
||||
|
||||
- broadcast snapshots produced elsewhere
|
||||
- collapse redundant outbound requests
|
||||
- track serialization/broadcast timing in telemetry
|
||||
|
||||
### Risk 4. Polling Surviving Too Long As Architecture
|
||||
|
||||
Some polling may remain during migration, but it should not become the permanent contract.
|
||||
|
||||
Mitigation:
|
||||
|
||||
- isolate polling behind ingress interfaces
|
||||
- make replacement with event-driven flows a planned Phase 2/3 outcome
|
||||
|
||||
## Open Questions
|
||||
|
||||
- Should the embedded static UI/docs hosting stay inside `ControlServices`, or move to a thinner app-shell concern while control APIs remain in `ControlServices`?
|
||||
- Should outbound state for WebSocket clients be one combined payload or separate runtime and health channels?
|
||||
- How much route/key resolution should happen in `ControlServices` versus `RuntimeCoordinator`?
|
||||
- Should any deferred automation-settle delivery remain in `ControlServices`, or should all commit/settle policy move entirely into coordinator/render ownership once the live-state model is formalized?
|
||||
- When file watching is modernized, should reload classification live entirely in `ControlServices`, or should it emit a lower-level `FilesChanged` event and let `RuntimeCoordinator` decide reload semantics?
|
||||
- Will future non-OSC automation sources reuse the same `AutomationTargetReceived` path, or need source-specific typed actions for policy reasons?
|
||||
|
||||
## Short Version
|
||||
|
||||
`ControlServices` should become the app's clean ingress boundary:
|
||||
|
||||
- transport handling stays here
|
||||
- input normalization stays here
|
||||
- ingress-local buffering stays here
|
||||
- mutation policy does not
|
||||
- authoritative runtime state does not
|
||||
- render-local transient state does not
|
||||
|
||||
If later phases keep that line sharp, the app gains a control layer that is fast, testable, and timing-aware without becoming another shared state authority.
|
||||
647
docs/subsystems/HealthTelemetry.md
Normal file
647
docs/subsystems/HealthTelemetry.md
Normal file
@@ -0,0 +1,647 @@
|
||||
# HealthTelemetry Subsystem Design
|
||||
|
||||
This document expands the `HealthTelemetry` subsystem introduced in [PHASE_1_SUBSYSTEM_BOUNDARIES_DESIGN.md](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/docs/PHASE_1_SUBSYSTEM_BOUNDARIES_DESIGN.md).
|
||||
|
||||
`HealthTelemetry` is the subsystem that owns operational visibility for the app. Its purpose is to gather health state, warnings, counters, logs, and timing observations from the other subsystems and publish them in a structured way without becoming a second control plane.
|
||||
|
||||
Before the Phase 1 runtime split, those responsibilities were fragmented across `RuntimeHost` status setters, ad hoc `OutputDebugStringA` calls, callback-local warnings, and UI-facing runtime-state payloads. The result was that the app could often detect problems, but did not yet have one clear place that answered:
|
||||
|
||||
- what is healthy right now
|
||||
- what is degraded right now
|
||||
- what has recently gone wrong
|
||||
- which subsystem is under pressure
|
||||
- how timing behavior is trending over time
|
||||
|
||||
`HealthTelemetry` is the target boundary that should answer those questions.
|
||||
|
||||
## Why This Subsystem Exists
|
||||
|
||||
The codebase already contains meaningful health and timing signals, but some are still spread through unrelated ownership domains:
|
||||
|
||||
- previous `RuntimeHost` status fields stored signal and timing status:
|
||||
- `RuntimeHost.h`
|
||||
- `RuntimeHost.cpp`
|
||||
- `RuntimeHost.cpp`
|
||||
- `RuntimeHost.cpp`
|
||||
- render and bridge code historically reported timing by writing back into `RuntimeHost`:
|
||||
- [OpenGLRenderPipeline.cpp](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/gl/pipeline/OpenGLRenderPipeline.cpp:50)
|
||||
- [OpenGLVideoIOBridge.cpp](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/gl/pipeline/OpenGLVideoIOBridge.cpp:49)
|
||||
- backend warning paths still log directly:
|
||||
- [DeckLinkFrameTransfer.cpp](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/videoio/decklink/DeckLinkFrameTransfer.cpp:84)
|
||||
- [DeckLinkSession.cpp](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/videoio/decklink/DeckLinkSession.cpp:305)
|
||||
- control ingress failures still log directly:
|
||||
- [OscServer.cpp](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/control/OscServer.cpp:142)
|
||||
- [RuntimeServices.cpp](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/control/RuntimeServices.cpp:100)
|
||||
|
||||
This creates several recurring problems:
|
||||
|
||||
- health information shares storage and lock scope with runtime state
|
||||
- warnings are not consistently classified by subsystem or severity
|
||||
- timing data is hard to compare across render, control, and backend paths
|
||||
- UI connection state and operational state are too closely coupled
|
||||
- logging is mostly text-first instead of structured-first
|
||||
- recovery behavior is hard to audit because the app does not retain a coherent health snapshot
|
||||
|
||||
`HealthTelemetry` exists so timing and health concerns have one subsystem whose only job is observation and reporting, instead of drifting back into runtime storage, callback-local logging, or UI payload assembly.
|
||||
|
||||
## Design Goals
|
||||
|
||||
`HealthTelemetry` should optimize for:
|
||||
|
||||
- one authoritative home for operational visibility
|
||||
- structured health state per subsystem
|
||||
- timing and counter recording that does not require a UI to be connected
|
||||
- low-friction reporting from render, backend, coordinator, and services
|
||||
- explicit degraded-mode reporting instead of only raw text logs
|
||||
- support for live operator summaries and deeper engineering diagnostics
|
||||
- minimal risk of telemetry writes becoming a render or callback bottleneck
|
||||
|
||||
## Responsibilities
|
||||
|
||||
`HealthTelemetry` owns structured operational visibility.
|
||||
|
||||
Primary responsibilities:
|
||||
|
||||
- accept timing samples from major subsystems
|
||||
- accept counter deltas and point-in-time gauges
|
||||
- accept warning, error, and degraded-state transitions
|
||||
- collect subsystem-scoped health state
|
||||
- collect operator-visible summary state
|
||||
- collect structured log entries
|
||||
- build stable health snapshots for UI, diagnostics, and later persistence/export if desired
|
||||
- retain recent history needed for short-term troubleshooting
|
||||
- classify observations by subsystem, severity, and category
|
||||
|
||||
Secondary responsibilities that still fit here:
|
||||
|
||||
- smoothing or rolling-window summaries for timing metrics
|
||||
- mapping raw subsystem observations into operator-facing health summaries
|
||||
- deduplicating repeated warnings
|
||||
- tracking warning open/clear lifecycles
|
||||
- providing bounded in-memory history for recent logs and warning transitions
|
||||
|
||||
## Explicit Non-Responsibilities
|
||||
|
||||
`HealthTelemetry` should not become a behavior owner.
|
||||
|
||||
It does not own:
|
||||
|
||||
- layer stack truth
|
||||
- persistence policy
|
||||
- render scheduling
|
||||
- DeckLink scheduling
|
||||
- OSC buffering or routing
|
||||
- reload coordination
|
||||
- shader compilation
|
||||
- recovery actions themselves
|
||||
|
||||
It also should not decide:
|
||||
|
||||
- whether render should skip a frame
|
||||
- whether VideoBackend should increase queue depth
|
||||
- whether RuntimeCoordinator should reject a mutation
|
||||
- whether ControlServices should drop or coalesce ingress traffic
|
||||
|
||||
Those decisions belong to the subsystem being observed. `HealthTelemetry` may describe that a subsystem is degraded, but it must not quietly become the mechanism that tells the app how to react.
|
||||
|
||||
## Ownership Boundaries
|
||||
|
||||
`HealthTelemetry` owns the following state categories.
|
||||
|
||||
### Structured Log State
|
||||
|
||||
Examples:
|
||||
|
||||
- subsystem name
|
||||
- severity
|
||||
- category
|
||||
- timestamp
|
||||
- message
|
||||
- optional structured fields such as layer id, preset name, queue depth, or shader id
|
||||
|
||||
This replaces the idea that `OutputDebugStringA` text is itself the main diagnostic product.
|
||||
|
||||
### Warning And Error State
|
||||
|
||||
Examples:
|
||||
|
||||
- active warning set
|
||||
- warning occurrence counts
|
||||
- first-seen and last-seen timestamps
|
||||
- clear timestamps
|
||||
- subsystem-scoped degraded flags
|
||||
|
||||
This is the durable in-memory operational state that should answer "what is currently wrong?" even if no UI was connected when the warning was raised.
|
||||
|
||||
### Timing State
|
||||
|
||||
Examples:
|
||||
|
||||
- render duration
|
||||
- frame budget
|
||||
- playout completion interval
|
||||
- smoothed completion interval
|
||||
- queue depth
|
||||
- input upload skip count
|
||||
- async readback fallback count
|
||||
- control ingress lag or queue depth
|
||||
- snapshot publication cost
|
||||
|
||||
This state should be organized as time-series-like rolling telemetry, not as a grab bag of unrelated `double` fields mixed into the runtime store.
|
||||
|
||||
### Health Snapshot State
|
||||
|
||||
Examples:
|
||||
|
||||
- current subsystem health summaries
|
||||
- current operator-facing overall health summary
|
||||
- most recent warning list
|
||||
- recent counters and timing summaries
|
||||
- "degraded but still running" status
|
||||
|
||||
This is the material that `ControlServices` or a diagnostics endpoint may later publish.
|
||||
|
||||
## State Model
|
||||
|
||||
The subsystem should model health and telemetry in a way that supports both machine-friendly and operator-friendly views.
|
||||
|
||||
Suggested conceptual model:
|
||||
|
||||
- `TelemetryLogEntry`
|
||||
- `TelemetryWarningRecord`
|
||||
- `TelemetryCounterState`
|
||||
- `TelemetryGaugeState`
|
||||
- `TelemetryTimingSeries`
|
||||
- `SubsystemHealthState`
|
||||
- `HealthSnapshot`
|
||||
|
||||
Important distinction:
|
||||
|
||||
- raw observations are append/update operations
|
||||
- health snapshots are derived read models
|
||||
|
||||
That distinction matters because the system should be able to retain richer recent telemetry internally than what is necessarily sent to the UI on every refresh.
|
||||
|
||||
## Subsystem Health Domains
|
||||
|
||||
`HealthTelemetry` should track health by subsystem rather than as one flat status blob.
|
||||
|
||||
At minimum, Phase 1 should assume domains for:
|
||||
|
||||
- `RuntimeStore`
|
||||
- `RuntimeCoordinator`
|
||||
- `RuntimeSnapshotProvider`
|
||||
- `ControlServices`
|
||||
- `RenderEngine`
|
||||
- `VideoBackend`
|
||||
|
||||
Optional cross-cutting domain:
|
||||
|
||||
- `ApplicationShell`
|
||||
|
||||
Each domain should be able to express states such as:
|
||||
|
||||
- `Healthy`
|
||||
- `Warning`
|
||||
- `Degraded`
|
||||
- `Error`
|
||||
- `Unavailable`
|
||||
|
||||
The exact enum can change, but the design should preserve the idea that each subsystem reports into its own health lane first, and only then is an overall status derived.
|
||||
|
||||
## Logging Boundaries
|
||||
|
||||
Logging belongs here, but logging should be structured-first.
|
||||
|
||||
Expected inputs:
|
||||
|
||||
- subsystem-scoped debug information
|
||||
- warning and error messages
|
||||
- recovery events
|
||||
- notable state transitions
|
||||
- significant operator actions that matter for diagnostics
|
||||
|
||||
Expected design rules:
|
||||
|
||||
- textual messages are still useful, but they should be wrapped in a structured log entry
|
||||
- repeated transient failures should be rate-limited or deduplicated at the telemetry layer where possible
|
||||
- log storage should be bounded in memory
|
||||
- UI publication should read from health/log snapshots, not scrape stdout/debug output
|
||||
|
||||
Examples of current direct log paths that should eventually move behind `HealthTelemetry`:
|
||||
|
||||
- OSC decode/dispatch failures
|
||||
- screenshot write failures
|
||||
- DeckLink fallback warnings
|
||||
- late/dropped frame warnings
|
||||
|
||||
## Metrics And Timing Boundaries
|
||||
|
||||
Timing and metrics should also move here, but their ownership line matters.
|
||||
|
||||
`HealthTelemetry` should own:
|
||||
|
||||
- metric collection interfaces
|
||||
- rolling summaries
|
||||
- recent history buffers
|
||||
- warning thresholds if the app later chooses to define them declaratively
|
||||
- operator-facing derived summaries
|
||||
|
||||
The producing subsystem should still own:
|
||||
|
||||
- the meaning of the measurement
|
||||
- when it is sampled
|
||||
- whether it triggers local mitigation
|
||||
|
||||
Examples:
|
||||
|
||||
- `RenderEngine` owns when render duration is sampled
|
||||
- `VideoBackend` owns when queue depth or playout lateness is sampled
|
||||
- `ControlServices` owns when ingress backlog is sampled
|
||||
- `RuntimeSnapshotProvider` owns when snapshot publish/build timing is sampled
|
||||
|
||||
`HealthTelemetry` should not invent those timings by inference. It records them when producers report them.
|
||||
|
||||
## Proposed Interfaces
|
||||
|
||||
These are target-shape interfaces, not final signatures.
|
||||
|
||||
### Write/Record Interface
|
||||
|
||||
Core write-side operations could look like:
|
||||
|
||||
```cpp
|
||||
enum class TelemetrySeverity;
|
||||
enum class TelemetrySubsystem;
|
||||
|
||||
struct TelemetryLogEntry;
|
||||
struct TelemetryWarning;
|
||||
struct TelemetryTimingSample;
|
||||
struct TelemetryCounterDelta;
|
||||
struct TelemetryGaugeUpdate;
|
||||
|
||||
class IHealthTelemetry
|
||||
{
|
||||
public:
|
||||
virtual void AppendLogEntry(const TelemetryLogEntry& entry) = 0;
|
||||
virtual void RaiseWarning(const TelemetryWarning& warning) = 0;
|
||||
virtual void ClearWarning(std::string_view warningKey) = 0;
|
||||
virtual void RecordTimingSample(const TelemetryTimingSample& sample) = 0;
|
||||
virtual void RecordCounterDelta(const TelemetryCounterDelta& delta) = 0;
|
||||
virtual void RecordGauge(const TelemetryGaugeUpdate& gauge) = 0;
|
||||
virtual void ReportSubsystemState(TelemetrySubsystem subsystem,
|
||||
SubsystemHealthState state) = 0;
|
||||
};
|
||||
```
|
||||
|
||||
The key is that every subsystem should be able to publish observations without also needing to know how UI payloads, rolling summaries, or log retention are implemented.
|
||||
|
||||
### Read Interface
|
||||
|
||||
Expected read-side operations:
|
||||
|
||||
- `BuildHealthSnapshot()`
|
||||
- `GetSubsystemHealth(...)`
|
||||
- `GetRecentLogs(...)`
|
||||
- `GetActiveWarnings()`
|
||||
- `GetRecentTimingSummary(...)`
|
||||
|
||||
Design notes:
|
||||
|
||||
- the read interface should return stable snapshots or read models
|
||||
- UI/websocket publication should consume those snapshots through `ControlServices`
|
||||
- read-side access should not require direct knowledge of internal ring buffers or lock layout
|
||||
|
||||
## Producer Expectations By Subsystem
|
||||
|
||||
The parent Phase 1 design already allows multiple subsystems to publish into telemetry. This section makes that concrete.
|
||||
|
||||
### From `RuntimeCoordinator`
|
||||
|
||||
Expected observations:
|
||||
|
||||
- mutation rejected
|
||||
- reload requested
|
||||
- preset apply failed
|
||||
- transient state cleared due to compatibility rules
|
||||
- policy-driven degraded notices such as repeated invalid external control input
|
||||
|
||||
### From `RuntimeSnapshotProvider`
|
||||
|
||||
Expected observations:
|
||||
|
||||
- snapshot publication duration
|
||||
- snapshot build failure
|
||||
- snapshot version churn metrics
|
||||
- repeated publish retries or stale-snapshot conditions
|
||||
|
||||
### From `ControlServices`
|
||||
|
||||
Expected observations:
|
||||
|
||||
- OSC decode failures
|
||||
- websocket broadcast failures
|
||||
- REST/control transport errors
|
||||
- ingress queue depth
|
||||
- coalescing/drop counts
|
||||
- file-watch reload request activity
|
||||
|
||||
### From `RenderEngine`
|
||||
|
||||
Expected observations:
|
||||
|
||||
- frame render duration
|
||||
- upload duration
|
||||
- readback duration
|
||||
- fallback to synchronous readback
|
||||
- preview present timing
|
||||
- render-local state resets caused by reload or incompatibility
|
||||
|
||||
### From `VideoBackend`
|
||||
|
||||
Expected observations:
|
||||
|
||||
- current playout queue depth
|
||||
- system-memory playout frame counts by state: free, ready, and scheduled
|
||||
- system-memory playout underrun, repeat, and drop counters
|
||||
- system-memory frame age at schedule and completion time
|
||||
- input signal state
|
||||
- late frames
|
||||
- dropped frames
|
||||
- backend mode changes
|
||||
- fallback from 10-bit to 8-bit input
|
||||
- output-only black-frame mode
|
||||
|
||||
## Current Code Mapping
|
||||
|
||||
The current codebase already contains several telemetry responsibilities that should migrate here.
|
||||
|
||||
### Previous `RuntimeHost` Status Setters
|
||||
|
||||
These were the clearest initial migration candidates:
|
||||
|
||||
- `SetSignalStatus(...)`
|
||||
- `TrySetSignalStatus(...)`
|
||||
- `SetPerformanceStats(...)`
|
||||
- `TrySetPerformanceStats(...)`
|
||||
- `SetFramePacingStats(...)`
|
||||
- `TrySetFramePacingStats(...)`
|
||||
|
||||
See:
|
||||
|
||||
- `RuntimeHost.h`
|
||||
- `RuntimeHost.cpp`
|
||||
- `RuntimeHost.cpp`
|
||||
- `RuntimeHost.cpp`
|
||||
|
||||
In the target architecture, this kind of state should not sit on the same object that owns persistent layer truth.
|
||||
|
||||
### Render Timing Production
|
||||
|
||||
Current render timing is produced in:
|
||||
|
||||
- [OpenGLRenderPipeline.cpp](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/gl/pipeline/OpenGLRenderPipeline.cpp:50)
|
||||
|
||||
That timing sample should conceptually become:
|
||||
|
||||
- `RenderEngine -> HealthTelemetry::RecordTimingSample(...)`
|
||||
|
||||
not the old pattern:
|
||||
|
||||
- `RenderEngine -> RuntimeHost::TrySetPerformanceStats(...)`
|
||||
|
||||
### Playout And Signal Status Production
|
||||
|
||||
Current signal and frame pacing updates are produced in:
|
||||
|
||||
- [OpenGLVideoIOBridge.cpp](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/gl/pipeline/OpenGLVideoIOBridge.cpp:49)
|
||||
- [OpenGLVideoIOBridge.cpp](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/gl/pipeline/OpenGLVideoIOBridge.cpp:61)
|
||||
|
||||
These should eventually become structured `VideoBackend` observations instead of bridge-to-host status writes.
|
||||
|
||||
### Direct Warning And Log Paths
|
||||
|
||||
Current examples:
|
||||
|
||||
- late/dropped frame warnings:
|
||||
- [DeckLinkFrameTransfer.cpp](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/videoio/decklink/DeckLinkFrameTransfer.cpp:84)
|
||||
- backend fallback warnings:
|
||||
- [DeckLinkSession.cpp](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/videoio/decklink/DeckLinkSession.cpp:305)
|
||||
- [DeckLinkSession.cpp](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/videoio/decklink/DeckLinkSession.cpp:320)
|
||||
- OSC errors:
|
||||
- [OscServer.cpp](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/control/OscServer.cpp:142)
|
||||
- [RuntimeServices.cpp](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/control/RuntimeServices.cpp:100)
|
||||
|
||||
All of these are clear migration candidates for `AppendLogEntry(...)`, `RaiseWarning(...)`, or counter/timing updates.
|
||||
|
||||
## Health Snapshot Contract
|
||||
|
||||
`HealthTelemetry` should expose one coherent health snapshot that other publication layers can consume.
|
||||
|
||||
That snapshot should be able to answer, at minimum:
|
||||
|
||||
- what the overall app health is
|
||||
- whether input signal is present
|
||||
- whether playout is healthy, degraded, or underrunning
|
||||
- whether render timing is within budget
|
||||
- what active warnings exist
|
||||
- what recent notable events occurred
|
||||
- what the current subsystem-specific states are
|
||||
|
||||
The important boundary is:
|
||||
|
||||
- `HealthTelemetry` builds the health snapshot
|
||||
- `ControlServices` may publish it
|
||||
- UI consumes it
|
||||
|
||||
That avoids rebuilding health summaries ad hoc in UI-facing runtime state serializers.
|
||||
|
||||
## Concurrency Expectations
|
||||
|
||||
This subsystem will likely receive updates from multiple threads:
|
||||
|
||||
- control ingress threads
|
||||
- render thread
|
||||
- backend callback threads
|
||||
- coordinator/service threads
|
||||
|
||||
So the design should assume:
|
||||
|
||||
- low-contention write paths
|
||||
- bounded memory
|
||||
- no long-held global mutex that callbacks and render both depend on
|
||||
|
||||
Phase 1 does not require lock-free implementation, but it does require the architecture to avoid recreating the old problem where health writes share the same lock as durable state and render-facing concerns.
|
||||
|
||||
Practical expectations:
|
||||
|
||||
- per-domain aggregation or lightweight internal locking is acceptable
|
||||
- read snapshots should be cheap and stable
|
||||
- callback paths should record telemetry cheaply and return
|
||||
|
||||
## Migration Plan From Current Code
|
||||
|
||||
The safest migration path is to peel telemetry responsibilities away from the existing classes incrementally.
|
||||
|
||||
### Step 1: Introduce The `HealthTelemetry` Interface
|
||||
|
||||
Create a small interface and health model types first.
|
||||
|
||||
Initial responsibilities:
|
||||
|
||||
- append structured logs
|
||||
- record timing samples
|
||||
- record counter deltas
|
||||
- raise and clear warnings
|
||||
- build a read-only health snapshot
|
||||
|
||||
The first implementation can still be backed by simple in-memory structures.
|
||||
|
||||
### Step 2: Keep New Observations Off Runtime Storage
|
||||
|
||||
Route new health-style work into `HealthTelemetry` instead of adding more status fields to runtime storage.
|
||||
|
||||
This prevents the old status surface from growing during migration.
|
||||
|
||||
### Step 3: Replace Legacy Status Setters With Telemetry Producers
|
||||
|
||||
Refactor:
|
||||
|
||||
- render timing writes
|
||||
- signal status writes
|
||||
- playout pacing writes
|
||||
|
||||
so they publish structured observations instead of mutating store-adjacent fields.
|
||||
|
||||
### Step 4: Replace Direct `OutputDebugStringA` Warning Paths
|
||||
|
||||
Wrap common warning/error cases in telemetry producers.
|
||||
|
||||
This includes:
|
||||
|
||||
- OSC decode/dispatch failures
|
||||
- DeckLink late/dropped frame notifications
|
||||
- backend fallback notices
|
||||
- screenshot write failures
|
||||
|
||||
Direct debug output can remain as a sink of telemetry if desired, but not as the primary source of truth.
|
||||
|
||||
### Step 5: Publish Health Snapshot Through UI/Diagnostics Paths
|
||||
|
||||
Once the snapshot format exists, let `ControlServices` publish health summaries and recent warnings explicitly rather than depending on the runtime-state payload alone.
|
||||
|
||||
## Risks
|
||||
|
||||
### 1. Telemetry becomes a hidden behavior controller
|
||||
|
||||
If warning states start being used as the indirect way subsystems tell each other what to do, the subsystem boundary will fail.
|
||||
|
||||
Guardrail:
|
||||
|
||||
- telemetry observes and reports
|
||||
- it does not coordinate or command
|
||||
|
||||
### 2. Logging stays string-only
|
||||
|
||||
If the subsystem only centralizes text logging without structure, later diagnostics will still be difficult.
|
||||
|
||||
Guardrail:
|
||||
|
||||
- severity, subsystem, category, and optional fields should be first-class
|
||||
|
||||
### 3. Timing writes become too expensive
|
||||
|
||||
If every sample requires heavy locking or snapshot rebuilds, render and callback timing could regress.
|
||||
|
||||
Guardrail:
|
||||
|
||||
- cheap recording path
|
||||
- derived summaries built separately from hot-path writes
|
||||
|
||||
### 4. Health snapshot duplicates runtime truth
|
||||
|
||||
If health snapshots start storing copies of durable runtime state, the subsystem boundary will blur again.
|
||||
|
||||
Guardrail:
|
||||
|
||||
- health snapshots summarize operational state
|
||||
- they do not become a second runtime store
|
||||
|
||||
### 5. Warning severity semantics drift by subsystem
|
||||
|
||||
If each subsystem invents its own meaning for warning/degraded/error, operator visibility becomes noisy and inconsistent.
|
||||
|
||||
Guardrail:
|
||||
|
||||
- define shared severity and health-state vocabulary early
|
||||
|
||||
## Open Questions
|
||||
|
||||
### 1. Should debug-output sinks remain enabled by default?
|
||||
|
||||
Current recommendation:
|
||||
|
||||
- yes, as a sink fed by structured telemetry entries, not as the source of truth
|
||||
|
||||
### 2. How much timing history should be retained in memory?
|
||||
|
||||
Current recommendation:
|
||||
|
||||
- enough for short-term live troubleshooting and UI summaries
|
||||
- not an unbounded time-series archive
|
||||
|
||||
### 3. Should operator-facing health and engineering diagnostics use the same snapshot?
|
||||
|
||||
Current recommendation:
|
||||
|
||||
- share one core telemetry model
|
||||
- allow separate derived views for concise operator summaries versus deeper engineering detail
|
||||
|
||||
### 4. Where should threshold policy live if the app later formalizes warnings like "render over budget"?
|
||||
|
||||
Current recommendation:
|
||||
|
||||
- telemetry may evaluate declared thresholds
|
||||
- subsystem owners still own mitigation behavior
|
||||
|
||||
### 5. Should input signal presence remain part of runtime state or move fully into telemetry?
|
||||
|
||||
Current recommendation:
|
||||
|
||||
- treat it as operational health state under `VideoBackend` reporting into telemetry
|
||||
- avoid keeping it as a core durable runtime-store concern
|
||||
|
||||
## Success Criteria For This Subsystem
|
||||
|
||||
`HealthTelemetry` can be considered well-defined once the codebase can say, without ambiguity:
|
||||
|
||||
- all major subsystems have one place to publish timing, warnings, and counters
|
||||
- health and timing state no longer share ownership with durable runtime state
|
||||
- the UI can consume a stable health snapshot without scraping unrelated runtime fields
|
||||
- direct debug-string warning paths are being retired or wrapped behind structured telemetry
|
||||
- degraded-but-running conditions are visible as first-class state
|
||||
|
||||
## Short Version
|
||||
|
||||
`HealthTelemetry` is the subsystem that should answer:
|
||||
|
||||
- what is healthy right now
|
||||
- what is degraded right now
|
||||
- what recent warnings and errors occurred
|
||||
- how render, control, and playout timing are behaving
|
||||
|
||||
It should:
|
||||
|
||||
- collect structured logs
|
||||
- collect warnings and counters
|
||||
- collect timing samples and gauges
|
||||
- build stable health snapshots for publication
|
||||
|
||||
It should not:
|
||||
|
||||
- own core runtime truth
|
||||
- decide app behavior
|
||||
- coordinate recovery actions
|
||||
- become a replacement for the render or backend policy layers
|
||||
|
||||
If this boundary holds, later phases can keep moving toward a much more diagnosable live system without putting timing and warning state back into runtime storage.
|
||||
44
docs/subsystems/README.md
Normal file
44
docs/subsystems/README.md
Normal file
@@ -0,0 +1,44 @@
|
||||
# Subsystem Notes Index
|
||||
|
||||
The current, phase-free architecture summary is:
|
||||
|
||||
- [Current System Architecture](../CURRENT_SYSTEM_ARCHITECTURE.md)
|
||||
|
||||
Start there when you want to understand how the application works now.
|
||||
|
||||
This directory contains deeper notes for individual subsystem boundaries. These notes were originally written during the phased architecture work, so some files may still mention migration steps or target-state language. Treat them as companion notes, not as the source of truth when they disagree with the current architecture summary.
|
||||
|
||||
## Recommended Reading Order
|
||||
|
||||
1. [Current System Architecture](../CURRENT_SYSTEM_ARCHITECTURE.md)
|
||||
2. [RuntimeStore](RuntimeStore.md)
|
||||
3. [RuntimeCoordinator](RuntimeCoordinator.md)
|
||||
4. [RuntimeSnapshotProvider](RuntimeSnapshotProvider.md)
|
||||
5. [ControlServices](ControlServices.md)
|
||||
6. [RenderEngine](RenderEngine.md)
|
||||
7. [VideoBackend](VideoBackend.md)
|
||||
8. [HealthTelemetry](HealthTelemetry.md)
|
||||
|
||||
That order follows the current ownership story:
|
||||
|
||||
- durable state first
|
||||
- mutation and publication next
|
||||
- control ingress after that
|
||||
- render ownership and video timing next
|
||||
- operational visibility last
|
||||
|
||||
## Subsystem Notes
|
||||
|
||||
- [RuntimeStore](RuntimeStore.md): durable runtime-state facade over layer-stack, config, package-catalog, presentation, and persistence boundaries.
|
||||
- [RuntimeCoordinator](RuntimeCoordinator.md): mutation validation, state classification, reset/reload policy, and publication/persistence requests.
|
||||
- [RuntimeSnapshotProvider](RuntimeSnapshotProvider.md): render-facing snapshot publication boundary backed by explicit render snapshot building/versioning.
|
||||
- [ControlServices](ControlServices.md): OSC, HTTP/WebSocket, and file-watch ingress plus normalization and service-local buffering.
|
||||
- [RenderEngine](RenderEngine.md): GL ownership boundary, render-local transient state, preview, and playout-ready frame production.
|
||||
- [VideoBackend](VideoBackend.md): device lifecycle, input/output pacing, buffer policy, and producer/consumer playout behavior.
|
||||
- [HealthTelemetry](HealthTelemetry.md): logs, warnings, counters, timing traces, and subsystem health snapshots.
|
||||
|
||||
## Historical Documents
|
||||
|
||||
The `docs/PHASE_*` files and experiment logs record how the architecture evolved. They are useful when you need rationale, investigation history, or rejected paths, but they are no longer arranged as the main feature split for the app.
|
||||
|
||||
For current implementation work, use [Current System Architecture](../CURRENT_SYSTEM_ARCHITECTURE.md) as the entry point and only dip into the phase documents when you need context for why a subsystem ended up this way.
|
||||
486
docs/subsystems/RenderEngine.md
Normal file
486
docs/subsystems/RenderEngine.md
Normal file
@@ -0,0 +1,486 @@
|
||||
# RenderEngine Subsystem Design
|
||||
|
||||
This document expands the `RenderEngine` portion of [PHASE_1_SUBSYSTEM_BOUNDARIES_DESIGN.md](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/docs/PHASE_1_SUBSYSTEM_BOUNDARIES_DESIGN.md). It defines the target ownership, boundaries, and migration shape for the rendering subsystem so later phases can move GL work out of today's mixed orchestration paths without inventing new boundaries on the fly.
|
||||
|
||||
The intent here is not to force a one-step rewrite. It is to make the target render boundary explicit enough that later work on events, live-state layering, sole-owner GL threading, and backend decoupling all land in the same place.
|
||||
|
||||
## Purpose
|
||||
|
||||
`RenderEngine` is the live frame-production subsystem.
|
||||
|
||||
It owns:
|
||||
|
||||
- GL context ownership in the target architecture
|
||||
- render loop cadence and render task execution
|
||||
- shader program and render-pass execution once build outputs are available
|
||||
- capture texture upload scheduling once frames are accepted for render
|
||||
- temporal history resources
|
||||
- shader feedback resources
|
||||
- render-local transient overlays
|
||||
- preview-ready frame production
|
||||
- playout-ready frame production
|
||||
- render-local reset and rebuild behavior
|
||||
|
||||
It does not own:
|
||||
|
||||
- persisted runtime state
|
||||
- high-level mutation policy
|
||||
- OSC/UI ingress
|
||||
- device discovery or callback policy
|
||||
- playout queue policy
|
||||
- operator-visible health policy beyond publishing observations
|
||||
|
||||
In the Phase 1 terminology, `RenderEngine` consumes snapshots plus render-local transient state and produces completed visual frames plus timing signals.
|
||||
|
||||
## Why This Subsystem Needs A Sharp Boundary
|
||||
|
||||
The current rendering path is split across several classes:
|
||||
|
||||
- [OpenGLComposite.cpp](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/gl/composite/OpenGLComposite.cpp:86) constructs the renderer, render pipeline, shader programs, runtime services, and video bridge in one owner.
|
||||
- [OpenGLRenderPipeline.cpp](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/gl/pipeline/OpenGLRenderPipeline.cpp:31) performs pass execution, pack/readback, preview paint, and performance stat publication.
|
||||
- [OpenGLVideoIOBridge.cpp](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/gl/pipeline/OpenGLVideoIOBridge.cpp:58) accepts capture frames and still performs render work from the playout completion callback path.
|
||||
- `RenderFrameStateResolver` and `RenderStateComposer` now keep frame-state selection and live value composition outside GL drawing, while `RenderEngine` still owns the current GL resource and draw path.
|
||||
|
||||
That split is workable today, but it creates architectural pressure:
|
||||
|
||||
- render and playout timing are still callback-coupled.
|
||||
- preview and playout are produced in the same immediate path.
|
||||
- render-local transient state now has clearer Phase 3/5 boundaries, but output production is still synchronously requested by the backend completion path.
|
||||
- it is difficult to test render behavior separately from app bootstrap and hardware integration.
|
||||
|
||||
`RenderEngine` exists to absorb that responsibility into one subsystem with one direction of ownership. Phase 4 has completed the GL ownership part of this target: normal runtime GL work now enters through the `RenderEngine` render thread.
|
||||
|
||||
## Responsibilities
|
||||
|
||||
### 1. Sole GL Ownership
|
||||
|
||||
In the target design, `RenderEngine` should be the only subsystem that performs long-lived GL work.
|
||||
|
||||
That includes:
|
||||
|
||||
- context binding and release policy
|
||||
- framebuffer and texture lifetime
|
||||
- shader program binding and draw execution
|
||||
- upload/readback buffer lifetime
|
||||
- preview blit or present paths
|
||||
- render-local resource reset on rebuild or video-format changes
|
||||
|
||||
This is the most important boundary. Other subsystems may request work or provide data, but they should not directly perform GL commands.
|
||||
|
||||
### 2. Snapshot Consumption
|
||||
|
||||
`RenderEngine` should consume immutable or near-immutable render snapshots from `RuntimeSnapshotProvider`.
|
||||
|
||||
It is responsible for:
|
||||
|
||||
- detecting snapshot version changes
|
||||
- rebuilding or re-binding render-local resources when the snapshot changes
|
||||
- resolving render-pass execution from snapshot contents
|
||||
- separating structural snapshot changes from transient overlay changes
|
||||
|
||||
It should not inspect mutable runtime store objects directly.
|
||||
|
||||
### 3. Frame Production
|
||||
|
||||
`RenderEngine` should produce completed frames for two consumers:
|
||||
|
||||
- preview presentation
|
||||
- `VideoBackend` playout consumption
|
||||
|
||||
Those outputs may share most of their render work, but they are not equal-priority outputs. The subsystem rule from Phase 1 should be preserved:
|
||||
|
||||
- playout is the primary timing-sensitive output
|
||||
- preview is subordinate and best-effort
|
||||
|
||||
### 4. Render-Local Transient State
|
||||
|
||||
`RenderEngine` owns transient visual state that affects output but is not persisted truth.
|
||||
|
||||
Examples:
|
||||
|
||||
- temporal history textures
|
||||
- feedback ping-pong buffers
|
||||
- render-local OSC/live overlay state
|
||||
- queued input frames accepted for upload
|
||||
- cached readback frames
|
||||
- preview-only presentation state
|
||||
- in-flight rebuild generations
|
||||
|
||||
This state should remain render-local even when it influences visible output.
|
||||
|
||||
Phase 5's `RuntimeStateLayerModel` explicitly keeps temporal history, feedback state, accepted input frames, staged output frames, preview staging, and screenshot/readback staging in the render-local category. These are deliberately outside the persisted/committed/transient-automation parameter composition rule.
|
||||
|
||||
`RuntimeLiveState` now owns transient automation invalidation for render-facing compatibility. It can clear overlays for a target layer/control key and prunes overlays that no longer resolve to the current layer and parameter definitions before applying them to a frame. This keeps shader reload, preset load, and layer removal behavior local to the live-state/composition boundary instead of scattering it through GL drawing code.
|
||||
|
||||
Render snapshots now flow through a named `CommittedLiveStateReadModel`, so render-facing committed state is distinct from durable storage and physically owned by `CommittedLiveState`.
|
||||
|
||||
### 5. Shader Build Application
|
||||
|
||||
Compilation itself may eventually move into a separate build service, but once shader build outputs exist, `RenderEngine` owns:
|
||||
|
||||
- program creation/link usage
|
||||
- pass graph application
|
||||
- sampler/texture binding layout application
|
||||
- resource reallocation required by shader shape changes
|
||||
- safe invalidation of old render-local feedback/history resources
|
||||
|
||||
### 6. Render Timing Publication
|
||||
|
||||
`RenderEngine` should publish observations to `HealthTelemetry` such as:
|
||||
|
||||
- frame render duration
|
||||
- upload duration
|
||||
- pass execution duration
|
||||
- pack/readback duration
|
||||
- preview present timing
|
||||
- rebuild stalls
|
||||
- dropped/skipped input uploads
|
||||
- output frame production latency
|
||||
|
||||
It should publish them, not own the health policy built from them.
|
||||
|
||||
## Non-Responsibilities
|
||||
|
||||
The target boundary should remain explicit about what does not belong here.
|
||||
|
||||
`RenderEngine` should not:
|
||||
|
||||
- decide whether a parameter mutation is persisted
|
||||
- normalize OSC/UI actions
|
||||
- choose device modes
|
||||
- own DeckLink callback behavior
|
||||
- own playout headroom policy
|
||||
- perform stack preset serialization
|
||||
- broadcast UI state
|
||||
- treat telemetry as a control plane
|
||||
|
||||
Those rules matter because the current codebase often solves timing issues by letting the render path reach sideways into nearby systems.
|
||||
|
||||
## GL Ownership Model
|
||||
|
||||
## Current Rule
|
||||
|
||||
One subsystem owns GL. `RenderEngine` now starts a dedicated render thread, binds the existing GL context on that thread for normal runtime work, and routes input upload, output render, preview presentation, screenshot capture, shader application, and render-local reset work through render-thread requests.
|
||||
|
||||
The render thread should:
|
||||
|
||||
- create or adopt the GL context
|
||||
- execute all frame production work
|
||||
- perform accepted texture uploads
|
||||
- execute all pass graphs
|
||||
- manage async readback and output packing
|
||||
- manage feedback/history resets and reallocations
|
||||
|
||||
Other threads should interact with the subsystem through queues, snapshots, and completion signals, not by borrowing the GL context.
|
||||
|
||||
## Remaining Timing State
|
||||
|
||||
GL ownership is no longer shared across callback-driven and UI entrypoints:
|
||||
|
||||
- input upload is requested through [OpenGLVideoIOBridge::UploadInputFrame()](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/gl/pipeline/OpenGLVideoIOBridge.cpp:11)
|
||||
- playout-triggered render is requested through [OpenGLVideoIOBridge::RenderScheduledFrame()](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/gl/pipeline/OpenGLVideoIOBridge.cpp:18)
|
||||
- render-pass execution occurs in [OpenGLRenderPipeline::RenderFrame()](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/gl/pipeline/OpenGLRenderPipeline.cpp:31)
|
||||
- preview and screenshot paths enter `RenderEngine` queue/request methods
|
||||
|
||||
The remaining timing issue is not shared GL ownership; it is the transitional synchronous output request/response path. The DeckLink completion callback still waits while the render thread produces an output frame, fills the DeckLink buffer, and then schedules the next frame.
|
||||
|
||||
## Migration Direction
|
||||
|
||||
The next target path should be:
|
||||
|
||||
1. input callback enqueues frame payloads or references
|
||||
2. render thread accepts the latest usable input frame
|
||||
3. render thread performs uploads on its own cadence
|
||||
4. render thread produces completed output frames ahead of backend demand
|
||||
5. backend callbacks only dequeue and schedule pre-rendered frames
|
||||
|
||||
Phase 4 completed the part that removes callback-thread GL ownership. Phase 7 should complete the producer/consumer playout part.
|
||||
|
||||
## Render Loop Boundaries
|
||||
|
||||
`RenderEngine` should own a render loop with explicit phases. A good target shape is:
|
||||
|
||||
1. drain render-side commands and accepted service events
|
||||
2. swap to the latest published snapshot if needed
|
||||
3. apply render-local transient overlays
|
||||
4. accept or coalesce latest input frame for upload
|
||||
5. perform required uploads
|
||||
6. execute pass graph
|
||||
7. update temporal and feedback resources
|
||||
8. pack and stage output frame(s)
|
||||
9. publish preview-ready image if due
|
||||
10. publish playout-ready frame(s) to `VideoBackend`
|
||||
11. emit timing and health samples
|
||||
|
||||
The important property is that preview, playout preparation, feedback maintenance, and upload execution all happen under one render-owned cadence rather than as ad hoc side effects of unrelated callbacks.
|
||||
|
||||
## Snapshot And Overlay Interaction
|
||||
|
||||
`RenderEngine` should treat snapshots and overlays as different layers of state.
|
||||
|
||||
### Snapshot Inputs
|
||||
|
||||
Snapshots should provide:
|
||||
|
||||
- layer stack structure
|
||||
- shader/package selections
|
||||
- validated committed parameter values
|
||||
- pass graph definitions
|
||||
- resource requirements derived from runtime state
|
||||
|
||||
### Render-Local Overlay Inputs
|
||||
|
||||
Overlays should provide:
|
||||
|
||||
- active automation targets
|
||||
- smoothed transient parameter overrides
|
||||
- temporary visual state that should not persist back into the store
|
||||
- queued reset/rebuild invalidations for render-local resources
|
||||
|
||||
### Resolution Rule
|
||||
|
||||
The render-side resolution order should be:
|
||||
|
||||
1. snapshot committed state forms the baseline
|
||||
2. render-local transient overlays are applied on top
|
||||
3. feedback/history resources influence shading as render-local inputs
|
||||
4. completed frame is produced without mutating the underlying snapshot
|
||||
|
||||
This is especially important after the OSC work already moved toward render-local overlays. Phase 1 should keep that direction: render consumes committed truth plus transient live overlays, but render does not become the owner of persisted truth.
|
||||
|
||||
## Preview And Playout Relationship
|
||||
|
||||
Preview should be a subordinate consumer of render results, not a peer that can disturb playout timing.
|
||||
|
||||
### Target Rule
|
||||
|
||||
- playout deadlines come first
|
||||
- preview is best-effort
|
||||
- preview cadence may be reduced independently
|
||||
- preview failure must not stall output frame production
|
||||
|
||||
### Current State
|
||||
|
||||
Today preview still hangs off the render pipeline path through `mPaint()` in [OpenGLRenderPipeline::RenderFrame()](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/gl/pipeline/OpenGLRenderPipeline.cpp:54). That keeps preview close enough to the playout path that it is still part of the same timing surface.
|
||||
|
||||
### Target Shape
|
||||
|
||||
`RenderEngine` should internally distinguish:
|
||||
|
||||
- playout-ready frame production
|
||||
- preview presentation or preview-copy publication
|
||||
|
||||
Possible later implementations:
|
||||
|
||||
- playout frame and preview frame share one composite render, but preview present is decoupled and rate-limited
|
||||
- render publishes a preview texture handle or CPU-side preview image to a preview presenter
|
||||
- preview updates are skipped under load without affecting playout queue fill
|
||||
|
||||
The exact implementation can change later, but the subsystem contract should already assume preview is subordinate.
|
||||
|
||||
## Interaction With `RuntimeSnapshotProvider`
|
||||
|
||||
`RenderEngine` should depend on `RuntimeSnapshotProvider`, not on `RuntimeStore`.
|
||||
|
||||
Expected interactions:
|
||||
|
||||
- query latest snapshot version
|
||||
- consume latest stable snapshot
|
||||
- detect structural versus parameter-only changes
|
||||
- request no mutation back into the snapshot provider during render
|
||||
|
||||
Expected non-interactions:
|
||||
|
||||
- no direct persistence reads/writes
|
||||
- no raw store mutation
|
||||
- no direct service ingress handling
|
||||
|
||||
This is one of the main Phase 1 guardrails, because the current code often achieves convenience by letting render reach back into runtime-owned mutable objects.
|
||||
|
||||
## Interaction With `VideoBackend`
|
||||
|
||||
The target dependency direction stays:
|
||||
|
||||
- `VideoBackend -> RenderEngine`
|
||||
|
||||
That means:
|
||||
|
||||
- backend requests or consumes ready frames
|
||||
- backend reports output timing/completion events
|
||||
- render does not own output device policy
|
||||
|
||||
`RenderEngine` should expose frame-production and queue-facing interfaces, while `VideoBackend` owns:
|
||||
|
||||
- device callback handling
|
||||
- output scheduling policy
|
||||
- buffer pool policy
|
||||
- backend state transitions
|
||||
|
||||
In later phases, this should evolve toward a producer/consumer queue where:
|
||||
|
||||
- render produces completed frames ahead of demand
|
||||
- backend consumes already-produced frames
|
||||
- callbacks drive dequeue/schedule/accounting only
|
||||
|
||||
## Current Code Mapping
|
||||
|
||||
The following current responsibilities should converge into `RenderEngine`.
|
||||
|
||||
### From `OpenGLComposite`
|
||||
|
||||
- render-local overlay management
|
||||
- render-facing rebuild application
|
||||
- screenshot-related render execution hooks
|
||||
- render bootstrap ownership currently mixed with app bootstrap
|
||||
|
||||
### From `OpenGLRenderPipeline`
|
||||
|
||||
- frame render orchestration
|
||||
- output pack conversion
|
||||
- async readback state
|
||||
- output frame caching
|
||||
- preview-ready signal publication
|
||||
|
||||
### From `OpenGLVideoIOBridge`
|
||||
|
||||
- GL texture upload execution should move under render ownership
|
||||
- playout callback render work should move out of the callback path
|
||||
|
||||
### Remains Outside `RenderEngine`
|
||||
|
||||
- device callback registration
|
||||
- playout scheduling policy
|
||||
- signal/device status lifecycle
|
||||
- runtime mutation policy
|
||||
|
||||
## Suggested Internal Components
|
||||
|
||||
This document does not require final class names, but `RenderEngine` will likely be easier to evolve if it is not one monolithic replacement for `OpenGLComposite`.
|
||||
|
||||
Reasonable internal pieces could include:
|
||||
|
||||
- `RenderLoopController`
|
||||
- `RenderSnapshotConsumer`
|
||||
- `RenderOverlayState`
|
||||
- `RenderInputQueue`
|
||||
- `RenderPassExecutor`
|
||||
- `RenderHistoryManager`
|
||||
- `RenderOutputStager`
|
||||
- `PreviewPresenter`
|
||||
|
||||
Those are internal implementation helpers. They should not become new cross-cutting subsystem boundaries by themselves.
|
||||
|
||||
## Public Interface Shape
|
||||
|
||||
Aligned with the Phase 1 design, `RenderEngine` should eventually expose operations in this family:
|
||||
|
||||
- `StartRenderLoop(...)`
|
||||
- `StopRenderLoop()`
|
||||
- `ConsumeSnapshot(...)`
|
||||
- `EnqueueInputFrame(...)`
|
||||
- `ApplyOverlayUpdate(...)`
|
||||
- `RequestRenderLocalReset(...)`
|
||||
- `HandleRebuildOutputs(...)`
|
||||
- `TryProduceOutputFrame(...)`
|
||||
- `GetPreviewFrame(...)`
|
||||
- `ReportRenderState()`
|
||||
|
||||
Interface goals:
|
||||
|
||||
- calls are explicit about whether they mutate render-local state or request frame production
|
||||
- no caller needs direct GL access
|
||||
- preview and playout are exposed as outputs, not as reasons for callers to enter the render path
|
||||
|
||||
## Migration Plan From Current Code
|
||||
|
||||
### Step 1. Name The Boundary
|
||||
|
||||
Treat `OpenGLRenderPipeline` plus the render portions of `OpenGLComposite` and `OpenGLVideoIOBridge` as conceptually belonging to `RenderEngine`, even before physical extraction is complete.
|
||||
|
||||
### Step 2. Stop New Render Work From Escaping
|
||||
|
||||
As new features are added, keep:
|
||||
|
||||
- feedback buffers
|
||||
- temporal history
|
||||
- render-local overlays
|
||||
- preview state
|
||||
|
||||
inside render-owned code paths instead of putting them back into runtime storage or service layers.
|
||||
|
||||
### Step 3. Isolate Snapshot Consumption
|
||||
|
||||
Introduce snapshot-facing APIs so render no longer depends on broad runtime-state access for frame production.
|
||||
|
||||
Current status: Phase 3 introduced `RenderFrameInput`, `RenderFrameState`, and `RenderFrameStateResolver`, so frame-state selection is named and no longer lives inside GL drawing. Phase 4 built on that contract and moved normal runtime GL ownership onto the render thread.
|
||||
|
||||
### Step 4. Move Uploads Onto Render Ownership
|
||||
|
||||
Input callbacks should enqueue or hand off frame data; render executes the upload.
|
||||
|
||||
### Step 5. Break Callback-Driven Rendering
|
||||
|
||||
Move from "render in playout completion callback" to "render ahead and let backend consume ready frames."
|
||||
|
||||
### Step 6. Decouple Preview Cadence
|
||||
|
||||
Make preview a best-effort presentation path with its own skip/rate-limit policy.
|
||||
|
||||
### Step 7. Narrow `OpenGLComposite`
|
||||
|
||||
After the above, `OpenGLComposite` should collapse toward a composition root and legacy adapter rather than remaining the owner of render behavior.
|
||||
|
||||
## Risks
|
||||
|
||||
### Latency Risk
|
||||
|
||||
Moving to queue-based frame production can accidentally increase latency if headroom is allowed to grow without policy. `RenderEngine` should therefore expose queue-friendly production, but `VideoBackend` must still own explicit latency/headroom policy.
|
||||
|
||||
### Resource Churn Risk
|
||||
|
||||
Snapshot changes, shader rebuilds, and video-format changes can cause expensive reallocation of:
|
||||
|
||||
- feedback surfaces
|
||||
- history buffers
|
||||
- output pack resources
|
||||
- readback buffers
|
||||
|
||||
The subsystem needs clear structural-change boundaries so parameter-only changes do not trigger broad resource churn.
|
||||
|
||||
### Preview Coupling Risk
|
||||
|
||||
If preview remains too close to the render/playout path, it can continue to steal budget from output production even after the rest of the subsystem is cleaned up.
|
||||
|
||||
### Readback Deadline Risk
|
||||
|
||||
The current async readback path still falls back to synchronous reads when the deadline is missed. That behavior may remain necessary, but `RenderEngine` should treat it as a degraded-path metric, not as an invisible normal case.
|
||||
|
||||
### Overlay Complexity Risk
|
||||
|
||||
Render-local overlays are powerful, but they can become a hidden second state model if not kept clearly subordinate to committed snapshot state.
|
||||
|
||||
## Open Questions
|
||||
|
||||
- Should preview become a separate presenter helper inside `RenderEngine`, or remain a subordinate callback/output sink?
|
||||
- Where should screenshot capture live long-term: inside `RenderEngine`, or in a small render consumer layered on top of it?
|
||||
- Should shader compilation outputs be delivered to render as whole-framegraph rebuild packages, or incrementally by layer/pass?
|
||||
- How should input frame ownership work under load: newest-only, bounded queue, or policy selected by backend mode?
|
||||
- Should render expose one playout-ready frame at a time, or a bounded ring the backend drains directly?
|
||||
- What exact distinction should the snapshot provider publish between structural changes and parameter-only changes so render rebuilds stay cheap?
|
||||
|
||||
## Phase 1 Exit Criteria For `RenderEngine`
|
||||
|
||||
For Phase 1, this subsystem design is sufficiently defined once the project agrees that:
|
||||
|
||||
- render is the sole long-term owner of GL work
|
||||
- render consumes snapshots, not mutable runtime store objects
|
||||
- preview is subordinate to playout
|
||||
- feedback/history/overlays are render-local transient state
|
||||
- backend callbacks should converge toward dequeue/schedule behavior rather than direct rendering
|
||||
- current render responsibilities in `OpenGLComposite`, `OpenGLRenderPipeline`, and `OpenGLVideoIOBridge` are expected to migrate under this subsystem
|
||||
|
||||
## Short Version
|
||||
|
||||
`RenderEngine` should become the subsystem that owns live GPU execution and nothing else.
|
||||
|
||||
It consumes committed snapshots plus render-local overlays, owns the full GL lifecycle, produces preview and playout-ready frames, and publishes timing observations. It should not own persistence, control ingress, or hardware scheduling policy. If later phases hold to that line, timing work and render-state work can get cleaner without reintroducing the same cross-thread coupling in a different form.
|
||||
564
docs/subsystems/RuntimeCoordinator.md
Normal file
564
docs/subsystems/RuntimeCoordinator.md
Normal file
@@ -0,0 +1,564 @@
|
||||
# RuntimeCoordinator Design Note
|
||||
|
||||
This document defines the target design for the `RuntimeCoordinator` subsystem introduced in [PHASE_1_SUBSYSTEM_BOUNDARIES_DESIGN.md](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/docs/PHASE_1_SUBSYSTEM_BOUNDARIES_DESIGN.md).
|
||||
|
||||
`RuntimeCoordinator` is the mutation and policy layer for the app. Its job is to accept already-normalized actions from ingress systems, decide whether those actions are valid, classify how they should affect durable and live state, and trigger downstream publication or persistence work without taking ownership of rendering, device callbacks, or disk serialization details.
|
||||
|
||||
## Why This Subsystem Exists
|
||||
|
||||
Before the Phase 1 runtime split, the app's mutation path was split across several places:
|
||||
|
||||
- `RuntimeHost` performed validation, mutation, persistence, render-state invalidation, and some status updates:
|
||||
- `RuntimeHost.h`
|
||||
- `RuntimeHost.cpp`
|
||||
- `OpenGLComposite` currently acts like an orchestration shell and a mutation coordinator at the same time:
|
||||
- [OpenGLCompositeRuntimeControls.cpp](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/gl/composite/OpenGLCompositeRuntimeControls.cpp:1)
|
||||
- `RuntimeServices` still owns some deferred control flow around OSC commit and polling:
|
||||
- [RuntimeServices.h](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/control/RuntimeServices.h:46)
|
||||
|
||||
That overlap makes several kinds of regressions more likely:
|
||||
|
||||
- persistence policy leaks into control handlers
|
||||
- render invalidation rules are spread across UI and non-UI paths
|
||||
- transient automation behavior is hard to reason about
|
||||
- reload behavior is partly a render concern and partly a runtime concern
|
||||
- future event-model work has no single policy owner to target
|
||||
|
||||
`RuntimeCoordinator` exists to centralize those decisions without becoming a new monolith.
|
||||
|
||||
## Core Responsibilities
|
||||
|
||||
`RuntimeCoordinator` should own the following responsibilities.
|
||||
|
||||
### 1. Mutation intake after normalization
|
||||
|
||||
`RuntimeCoordinator` accepts typed, already-parsed actions from `ControlServices` or composition-root adapters. Examples:
|
||||
|
||||
- add/remove/move layer
|
||||
- change shader on a layer
|
||||
- change a parameter value
|
||||
- reset a layer
|
||||
- save or load a stack preset
|
||||
- request a shader/package reload
|
||||
- apply a transient automation target
|
||||
- commit or clear transient overlay state
|
||||
|
||||
The coordinator should not parse JSON, decode OSC payloads, or inspect HTTP payload syntax. That belongs to ingress systems.
|
||||
|
||||
### 2. Validation and policy decisions
|
||||
|
||||
The coordinator validates whether a requested mutation is allowed and decides how it should behave.
|
||||
|
||||
Examples:
|
||||
|
||||
- whether a layer id exists
|
||||
- whether a shader id is valid
|
||||
- whether a parameter exists on the targeted shader
|
||||
- whether a value is within the definition's allowed range or enum set
|
||||
- whether a trigger should update committed state, transient state, or both
|
||||
- whether a structural change should preserve compatible transient state such as feedback buffers
|
||||
|
||||
This is the policy surface that used to be spread between `RuntimeHost` methods such as:
|
||||
|
||||
- `AddLayer(...)`
|
||||
- `SetLayerShader(...)`
|
||||
- `UpdateLayerParameter(...)`
|
||||
- `UpdateLayerParameterByControlKey(...)`
|
||||
- `ApplyOscTargetByControlKey(...)`
|
||||
- `ResetLayerParameters(...)`
|
||||
|
||||
See `RuntimeHost.h`.
|
||||
|
||||
### 3. State classification
|
||||
|
||||
The coordinator decides which state category a mutation affects:
|
||||
|
||||
- persisted state
|
||||
- committed live state
|
||||
- transient live overlay state
|
||||
- health/timing state only
|
||||
|
||||
The design rule is that classification belongs here, not in the ingress layer and not in render code.
|
||||
|
||||
Phase 5 codifies the shared vocabulary for this classification in `RuntimeStateLayerModel`. Current committed session parameter values and layer bypass state are committed-live/session state owned by `CommittedLiveState`; runtime compile/reload flags are coordination state rather than durable store truth.
|
||||
|
||||
### 4. Snapshot publication requests
|
||||
|
||||
When a mutation changes render-facing state, the coordinator asks `RuntimeSnapshotProvider` to publish a new snapshot or mark one dirty for publication.
|
||||
|
||||
The coordinator does not build render snapshots itself.
|
||||
|
||||
### 5. Persistence requests
|
||||
|
||||
When a mutation changes durable state, the coordinator asks `RuntimeStore` to record the new authoritative state and, when applicable, request persistence through the store's write path.
|
||||
|
||||
The coordinator does not serialize files directly.
|
||||
|
||||
### 6. Cross-subsystem consistency policy
|
||||
|
||||
The coordinator is where "what else must happen if this changes?" lives.
|
||||
|
||||
Examples:
|
||||
|
||||
- a layer add/remove/move may require:
|
||||
- store mutation
|
||||
- snapshot republish
|
||||
- compatibility-preserving render-state reset policy
|
||||
- optional UI-state notification via later event-model work
|
||||
- a stack preset load may require:
|
||||
- replacement of committed layer stack state
|
||||
- invalidation of transient overlay state that no longer maps cleanly
|
||||
- snapshot republish
|
||||
- deferred persistence request
|
||||
- an automation target may require:
|
||||
- transient overlay update only
|
||||
- no persistence write
|
||||
- optional later commit into committed live state if policy says so
|
||||
|
||||
## Explicit Non-Responsibilities
|
||||
|
||||
`RuntimeCoordinator` should explicitly not own the following.
|
||||
|
||||
### Not a persistence engine
|
||||
|
||||
It does not:
|
||||
|
||||
- read or write files
|
||||
- decide file formats
|
||||
- own preset storage layout
|
||||
- perform debounced disk flushing logic
|
||||
|
||||
Those belong in `RuntimeStore` and later persistence helpers.
|
||||
|
||||
### Not a render engine
|
||||
|
||||
It does not:
|
||||
|
||||
- own GL objects
|
||||
- perform shader compilation
|
||||
- reset temporal history textures directly
|
||||
- build render passes
|
||||
- hold frame queues
|
||||
|
||||
It may request policy outcomes that cause render-local resets, but render performs the work.
|
||||
|
||||
### Not a hardware/backend owner
|
||||
|
||||
It does not:
|
||||
|
||||
- configure DeckLink
|
||||
- react directly to device callbacks
|
||||
- schedule playout
|
||||
- own input signal callbacks
|
||||
|
||||
### Not an ingress transport layer
|
||||
|
||||
It does not:
|
||||
|
||||
- parse OSC wire messages
|
||||
- host websockets
|
||||
- own HTTP handlers
|
||||
- own polling loops
|
||||
|
||||
### Not a health reporting sink
|
||||
|
||||
It can emit mutation outcomes and warnings to `HealthTelemetry`, but it should not own counters, logs, or dashboards.
|
||||
|
||||
## Mutation Policy
|
||||
|
||||
The coordinator should use a small number of policy classes of mutation behavior rather than ad hoc per-call decisions.
|
||||
|
||||
### Durable mutation
|
||||
|
||||
Updates authoritative state that should survive beyond the current session flow.
|
||||
|
||||
Examples:
|
||||
|
||||
- add/remove/move layer
|
||||
- change selected shader on a layer
|
||||
- update a parameter via UI or API
|
||||
- load a stack preset
|
||||
- reset a layer to defaults
|
||||
|
||||
Expected coordinator behavior:
|
||||
|
||||
1. validate the request
|
||||
2. normalize the target and value if needed
|
||||
3. update committed/durable state via `RuntimeStore`
|
||||
4. request snapshot publication
|
||||
5. request persistence according to policy
|
||||
|
||||
### Live committed mutation
|
||||
|
||||
Updates committed current-session state that should be treated as true until changed again, but may not need synchronous persistence.
|
||||
|
||||
Examples:
|
||||
|
||||
- a UI action that changes a parameter repeatedly while dragging
|
||||
- a manual operator bypass toggle during live use
|
||||
|
||||
Expected coordinator behavior:
|
||||
|
||||
1. update committed live state
|
||||
2. request snapshot publication
|
||||
3. decide whether persistence should happen immediately, be debounced, or be deferred
|
||||
|
||||
### Transient overlay mutation
|
||||
|
||||
Affects output but should not masquerade as stored truth.
|
||||
|
||||
Examples:
|
||||
|
||||
- active OSC automation target
|
||||
- short-lived trigger-driven visual automation state
|
||||
|
||||
Expected coordinator behavior:
|
||||
|
||||
1. validate the route and target parameter
|
||||
2. classify the action as transient
|
||||
3. update overlay state through the appropriate owner boundary
|
||||
4. avoid persistence unless a separate commit policy is invoked
|
||||
|
||||
### Coordination-only mutation
|
||||
|
||||
A request that mainly exists to trigger a flow rather than edit value state.
|
||||
|
||||
Examples:
|
||||
|
||||
- request reload
|
||||
- request publish-now
|
||||
- request clear transient state on reset/rebuild
|
||||
|
||||
## Interaction With State Categories
|
||||
|
||||
This section restates the Phase 1 state model specifically from the coordinator's perspective.
|
||||
|
||||
### Persisted state
|
||||
|
||||
`RuntimeCoordinator` does not own persisted state, but it decides when persisted state should change.
|
||||
|
||||
Typical interaction:
|
||||
|
||||
- validate request
|
||||
- call into `RuntimeStore`
|
||||
- receive success/failure
|
||||
- request persistence if policy says this mutation should be durable
|
||||
|
||||
### Committed live state
|
||||
|
||||
This is the coordinator's primary logical domain.
|
||||
|
||||
The coordinator is the policy owner of:
|
||||
|
||||
- current layer stack composition
|
||||
- current selected shaders
|
||||
- current bypass flags
|
||||
- current operator-authored parameter values
|
||||
|
||||
`CommittedLiveState` is the physical owner for this current-session layer state. `RuntimeStore` persists or skips disk writes according to coordinator policy and remains the compatibility facade for existing mutation call shapes.
|
||||
|
||||
### Transient live overlay state
|
||||
|
||||
The coordinator defines the rules for transient state, but should not become the long-term storage owner for render-local transient data.
|
||||
|
||||
The expected split is:
|
||||
|
||||
- coordinator owns policy
|
||||
- `ControlServices` may own short ingress-side queues and coalescing buffers
|
||||
- `RenderEngine` owns render-local transient application state
|
||||
- `VideoBackend` owns playout and device transient state
|
||||
|
||||
For OSC specifically, the coordinator should eventually decide:
|
||||
|
||||
- whether an automation change is transient-only
|
||||
- whether it should later commit into committed live state
|
||||
- what reset/reload actions invalidate it
|
||||
|
||||
Phase 5 sets the default settled OSC policy to session-only. `CommitOscParameterByControlKey(...)` updates committed session state through the store with persistence disabled, publishes ordinary mutation/state-change observations, and does not request a persistence write unless a future explicit policy opts into durable OSC commits.
|
||||
|
||||
The committed-live concept now has a physical owner, `CommittedLiveState`, plus a named read model, `CommittedLiveStateReadModel`. The coordinator remains the owner of whether a mutation should be durable or session-only, while `RuntimeStore` persists or skips disk writes according to that policy.
|
||||
|
||||
### Health and timing state
|
||||
|
||||
The coordinator may emit events like:
|
||||
|
||||
- mutation rejected
|
||||
- reload requested
|
||||
- preset load succeeded/failed
|
||||
- transient state cleared because structure changed
|
||||
|
||||
But those are observations into `HealthTelemetry`, not coordinator-owned data.
|
||||
|
||||
## Proposed Interfaces
|
||||
|
||||
These are target-shape interfaces, not final signatures.
|
||||
|
||||
### Input-facing API
|
||||
|
||||
Core mutation entrypoints could look like:
|
||||
|
||||
```cpp
|
||||
struct RuntimeMutationRequest;
|
||||
struct RuntimeMutationResult;
|
||||
struct ReloadRequest;
|
||||
struct OverlayCommitRequest;
|
||||
|
||||
class RuntimeCoordinator
|
||||
{
|
||||
public:
|
||||
RuntimeMutationResult ApplyMutation(const RuntimeMutationRequest& request);
|
||||
RuntimeMutationResult ApplyAutomationTarget(const RuntimeMutationRequest& request);
|
||||
RuntimeMutationResult ResetLayer(const std::string& layerId);
|
||||
RuntimeMutationResult RequestReload(const ReloadRequest& request);
|
||||
RuntimeMutationResult CommitOverlayState(const OverlayCommitRequest& request);
|
||||
RuntimeMutationResult ClearTransientStateForScope(const RuntimeResetScope& scope);
|
||||
};
|
||||
```
|
||||
|
||||
The important point is not the exact names. It is that ingress systems send typed requests into one policy owner.
|
||||
|
||||
### Downstream collaborators
|
||||
|
||||
The coordinator likely needs collaborators conceptually equivalent to:
|
||||
|
||||
- `IRuntimeStore`
|
||||
- `IRuntimeSnapshotProvider`
|
||||
- `IHealthTelemetry`
|
||||
- compatibility adapters only where older call shapes still need to be supported during migration
|
||||
|
||||
### Mutation result shape
|
||||
|
||||
A useful result structure should carry more than success/failure. It should support policy-driven downstream behavior without re-deriving the decision elsewhere.
|
||||
|
||||
Suggested fields:
|
||||
|
||||
- `accepted`
|
||||
- `errorMessage`
|
||||
- `stateChanged`
|
||||
- `persistedStateChanged`
|
||||
- `committedLiveStateChanged`
|
||||
- `transientStateChanged`
|
||||
- `snapshotPublicationRequired`
|
||||
- `persistenceRequested`
|
||||
- `renderResetScope`
|
||||
- `telemetryNotes`
|
||||
|
||||
This prevents callers from guessing whether they need to reload, publish, or persist.
|
||||
|
||||
## Current Code Mapping
|
||||
|
||||
The current app does not have a separate coordinator class, but several existing code paths are clearly doing coordinator work.
|
||||
|
||||
### `OpenGLCompositeRuntimeControls.cpp`
|
||||
|
||||
Methods like:
|
||||
|
||||
- `AddLayer(...)`
|
||||
- `RemoveLayer(...)`
|
||||
- `MoveLayer(...)`
|
||||
- `SetLayerBypass(...)`
|
||||
- `SetLayerShader(...)`
|
||||
- `UpdateLayerParameterJson(...)`
|
||||
- `ResetLayerParameters(...)`
|
||||
- `SaveStackPreset(...)`
|
||||
- `LoadStackPreset(...)`
|
||||
|
||||
currently do this pattern:
|
||||
|
||||
1. call a host/store mutation directly
|
||||
2. decide whether to call `ReloadShader(...)`
|
||||
3. call `broadcastRuntimeState()`
|
||||
|
||||
See [OpenGLCompositeRuntimeControls.cpp](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/gl/composite/OpenGLCompositeRuntimeControls.cpp:1).
|
||||
|
||||
That "call host, then decide reload/broadcast policy" logic is a direct candidate for migration into `RuntimeCoordinator`.
|
||||
|
||||
### Previous `RuntimeHost`
|
||||
|
||||
`RuntimeHost` previously combined:
|
||||
|
||||
- mutation validation
|
||||
- state mutation
|
||||
- value normalization
|
||||
- persistence writes
|
||||
- render-state dirty marking
|
||||
|
||||
Examples from the old `RuntimeHost.cpp`:
|
||||
|
||||
- `AddLayer(...)`
|
||||
- `SetLayerShader(...)`
|
||||
- `UpdateLayerParameter(...)`
|
||||
- `UpdateLayerParameterByControlKey(...)`
|
||||
- `ApplyOscTargetByControlKey(...)`
|
||||
- `ResetLayerParameters(...)`
|
||||
- `LoadStackPreset(...)`
|
||||
|
||||
The target design is not to move all implementation in one step. It is to peel policy and orchestration decisions away first.
|
||||
|
||||
### `RuntimeServices`
|
||||
|
||||
Current OSC-specific flow in `RuntimeServices` includes:
|
||||
|
||||
- queueing updates
|
||||
- applying pending updates
|
||||
- queueing commits
|
||||
- consuming completed commits
|
||||
- clearing OSC state
|
||||
|
||||
See [RuntimeServices.h](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/control/RuntimeServices.h:46).
|
||||
|
||||
The coordinator should eventually own the rules for when these updates are transient, when they commit, and what reset/reload does to them, while `ControlServices` keeps only the ingress mechanics.
|
||||
|
||||
## Recommended Internal Model
|
||||
|
||||
The coordinator should remain small enough to reason about. A good target is to split its internal logic into policy-focused helpers rather than letting one class become another `RuntimeHost`.
|
||||
|
||||
Possible internal helper concepts:
|
||||
|
||||
- `LayerMutationPolicy`
|
||||
- `ParameterMutationPolicy`
|
||||
- `PresetMutationPolicy`
|
||||
- `ReloadPolicy`
|
||||
- `OverlayPolicy`
|
||||
|
||||
That can still be presented as one subsystem to the rest of the app, while keeping the implementation testable.
|
||||
|
||||
## Snapshot Publication Contract
|
||||
|
||||
The coordinator should never force callers to know whether a snapshot must be rebuilt. That policy should be owned here.
|
||||
|
||||
Examples:
|
||||
|
||||
- parameter changes require snapshot publication
|
||||
- layer reorder requires snapshot publication
|
||||
- shader swap requires snapshot publication and render-local rebuild work
|
||||
- stack preset load requires snapshot publication and likely broader transient-state invalidation
|
||||
- pure health/status changes do not require snapshot publication
|
||||
|
||||
This contract matters because current call sites often use coarse actions like `ReloadShader()` after structural edits. The coordinator should return a more precise outcome than "reload or not."
|
||||
|
||||
## Reload and Reset Policy
|
||||
|
||||
Reload and reset behavior has been a recurring source of edge cases in the current app, especially with shader feedback, temporal history, and OSC overlay state.
|
||||
|
||||
The coordinator should define explicit reset scopes such as:
|
||||
|
||||
- parameter-values-only reset
|
||||
- committed-live-state reset for a layer
|
||||
- transient-overlay reset for a layer
|
||||
- render-local-history reset for a layer
|
||||
- whole-stack structural reset
|
||||
- reload-induced compatibility reset
|
||||
|
||||
That allows later phases to stop encoding reset behavior implicitly in UI handlers or render rebuild code.
|
||||
|
||||
Phase 5 has made this more concrete for OSC overlays: coordinator results now carry a named transient OSC invalidation request, with layer-scoped invalidation used for layer removal and manual parameter reset. The render/live-state owner still decides compatibility details, but callers no longer infer transient reset behavior from a generic boolean.
|
||||
|
||||
## Migration Plan From Current Code
|
||||
|
||||
The coordinator should be introduced incrementally.
|
||||
|
||||
### Step 1. Define request and result types
|
||||
|
||||
Introduce typed mutation request/result objects without changing most internals yet.
|
||||
|
||||
### Step 2. Wrap direct runtime mutations behind coordinator entrypoints
|
||||
|
||||
The first implementation could still delegate heavily into existing runtime mutation paths, but the call sites should stop deciding policy on their own.
|
||||
|
||||
For example, instead of:
|
||||
|
||||
1. `OpenGLComposite::AddLayer()`
|
||||
2. direct layer-add mutation
|
||||
3. `ReloadShader(true)`
|
||||
4. `broadcastRuntimeState()`
|
||||
|
||||
the flow becomes:
|
||||
|
||||
1. `OpenGLComposite` or `ControlServices` creates a typed request
|
||||
2. `RuntimeCoordinator::ApplyMutation(...)`
|
||||
3. coordinator returns a result describing snapshot, reset, and persistence needs
|
||||
4. composition root dispatches those downstream effects
|
||||
|
||||
### Step 3. Move validation and classification out of direct mutation helpers
|
||||
|
||||
Once coordinator entrypoints are stable, pull up:
|
||||
|
||||
- mutation classification
|
||||
- reset/reload policy
|
||||
- transient-versus-durable decisions
|
||||
|
||||
while leaving raw store operations in place.
|
||||
|
||||
### Step 4. Split storage and snapshot collaborators
|
||||
|
||||
Only after the coordinator is clearly owning policy should storage and snapshot responsibilities be split into real target subsystems.
|
||||
|
||||
## Key Risks
|
||||
|
||||
### Risk 1. Coordinator becomes a new god object
|
||||
|
||||
If the coordinator starts owning persistence details, status counters, or render reset mechanics directly, it will just recreate the current problem under a new name.
|
||||
|
||||
Mitigation:
|
||||
|
||||
- keep collaborators explicit
|
||||
- keep request/result types narrow
|
||||
- avoid direct dependencies on render or backend internals
|
||||
|
||||
### Risk 2. Call sites bypass coordinator during migration
|
||||
|
||||
If new code bypasses `RuntimeCoordinator` for convenience, the architecture will fork into two policy systems.
|
||||
|
||||
Mitigation:
|
||||
|
||||
- treat the coordinator as the required entrypoint for new non-render mutations
|
||||
- add compatibility adapters rather than parallel mutation paths
|
||||
|
||||
### Risk 3. Too much policy stays implicit in return conventions
|
||||
|
||||
If callers still infer policy from "which method was called," the coordinator will not actually clarify the system.
|
||||
|
||||
Mitigation:
|
||||
|
||||
- return explicit mutation outcomes
|
||||
- define reset and publication scopes as named concepts
|
||||
|
||||
### Risk 4. Transient-state ownership remains fuzzy
|
||||
|
||||
OSC overlay behavior, feedback invalidation, and reload compatibility can easily blur subsystem boundaries again.
|
||||
|
||||
Mitigation:
|
||||
|
||||
- coordinator owns classification rules
|
||||
- subsystem owners retain storage ownership
|
||||
- reset scopes are explicit
|
||||
|
||||
## Open Questions
|
||||
|
||||
- Should preset load/save stay synchronous through early migration, or should the coordinator always treat them as policy requests whose persistence effects may complete later?
|
||||
- Should reload requests be modeled as a dedicated mutation class distinct from ordinary control mutations from the start?
|
||||
- How much normalization of parameter values should remain in store-side helpers versus moving into coordinator policy helpers?
|
||||
- Should transient overlay commit policy be global, or parameter-definition-driven for specific shader controls?
|
||||
- What is the minimal reset-scope vocabulary needed to avoid hard-coding reload behavior in `RenderEngine` later?
|
||||
|
||||
## Short Version
|
||||
|
||||
`RuntimeCoordinator` is where the app decides what a valid change means.
|
||||
|
||||
It should:
|
||||
|
||||
- accept typed mutations from ingress systems
|
||||
- validate and classify them
|
||||
- update durable and committed state through `RuntimeStore`
|
||||
- request render-facing publication through `RuntimeSnapshotProvider`
|
||||
- request persistence when policy requires it
|
||||
- define reset, reload, and transient-overlay rules
|
||||
|
||||
It should not:
|
||||
|
||||
- parse transport payloads
|
||||
- own GL work
|
||||
- own device callbacks
|
||||
- write files directly
|
||||
- become a replacement monolith for every kind of state
|
||||
476
docs/subsystems/RuntimeSnapshotProvider.md
Normal file
476
docs/subsystems/RuntimeSnapshotProvider.md
Normal file
@@ -0,0 +1,476 @@
|
||||
# RuntimeSnapshotProvider Subsystem Design
|
||||
|
||||
This document expands the `RuntimeSnapshotProvider` subsystem from [PHASE_1_SUBSYSTEM_BOUNDARIES_DESIGN.md](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/docs/PHASE_1_SUBSYSTEM_BOUNDARIES_DESIGN.md) into a concrete subsystem design.
|
||||
|
||||
The goal of `RuntimeSnapshotProvider` is to separate render-facing state publication from both runtime mutation policy and durable storage. In the target architecture, render should consume published snapshots rather than reaching into `RuntimeStore` or lock-protected live objects directly.
|
||||
|
||||
## Purpose
|
||||
|
||||
`RuntimeSnapshotProvider` is the boundary between runtime-owned state and render-consumable state.
|
||||
|
||||
It exists to solve three problems that Phase 1 pulled apart:
|
||||
|
||||
- render state was built directly out of `RuntimeHost` under a shared mutex
|
||||
- render read and refreshed partially mutable cached layer state in more than one place
|
||||
- state publication, state versioning, and dynamic frame-field refresh need explicit ownership
|
||||
|
||||
Before the Phase 1 runtime split, the closest behavior lived in:
|
||||
|
||||
- `RuntimeHost::GetLayerRenderStates(...)`
|
||||
- `RuntimeHost::TryGetLayerRenderStates(...)`
|
||||
- `RuntimeHost::TryRefreshCachedLayerStates(...)`
|
||||
- `RuntimeHost::RefreshDynamicRenderStateFields(...)`
|
||||
- `RuntimeHost::BuildLayerRenderStatesLocked(...)`
|
||||
- the render-side cache usage in [OpenGLComposite.cpp](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/gl/composite/OpenGLComposite.cpp:589)
|
||||
|
||||
`RuntimeSnapshotProvider` has absorbed that responsibility in a cleaner and more publish-oriented way.
|
||||
|
||||
## Responsibilities
|
||||
|
||||
`RuntimeSnapshotProvider` is responsible for:
|
||||
|
||||
- publishing stable, versioned snapshots that can be consumed without large shared mutable locks
|
||||
- giving `RenderEngine` a cheap read path for the latest committed snapshot
|
||||
- making snapshot invalidation and publication rules explicit
|
||||
|
||||
`RenderSnapshotBuilder` is responsible for:
|
||||
|
||||
- building render-facing snapshots from the committed-live read model and package/runtime metadata supplied by `RuntimeStore`
|
||||
- separating structural snapshot changes from dynamic frame fields
|
||||
- translating runtime layer state into render-ready layer descriptors
|
||||
- attaching immutable or near-immutable shader/package-derived data needed by render
|
||||
- maintaining render snapshot version counters and frame advancement
|
||||
|
||||
It is not responsible for:
|
||||
|
||||
- deciding whether a mutation is valid
|
||||
- classifying a change as transient versus durable
|
||||
- directly accepting OSC/UI/file-watch requests
|
||||
- disk persistence
|
||||
- GL resource allocation
|
||||
- shader compilation execution
|
||||
- render-local transient overlays such as live OSC overlay state, temporal history textures, or feedback textures
|
||||
|
||||
## Design Principles
|
||||
|
||||
### Render consumes published state, not store internals
|
||||
|
||||
The render side should never need to walk `RuntimeStore` structures directly or perform per-frame reconstruction under the store lock.
|
||||
|
||||
### Structural data and dynamic frame fields are different classes of data
|
||||
|
||||
The layer stack, shader ids, parameter definitions, texture assets, font assets, feedback declarations, and temporal requirements change relatively infrequently. Frame count, wall time, UTC time, and similar values change every frame.
|
||||
|
||||
`RuntimeSnapshotProvider` should publish structural snapshots and provide a separate mechanism for frame-local dynamic enrichment, rather than rebuilding everything for every frame.
|
||||
|
||||
### Snapshot reads should be cheap and explicit
|
||||
|
||||
The render side should be able to say:
|
||||
|
||||
- give me the latest published snapshot
|
||||
- tell me whether the structural snapshot version changed
|
||||
- apply dynamic frame fields for this frame
|
||||
|
||||
without having to infer cache validity from multiple host-owned counters and fallback lock behavior.
|
||||
|
||||
### Published shape should be stable
|
||||
|
||||
The shape of render-facing layer state should remain consistent across phases even if the underlying store or coordination model changes.
|
||||
|
||||
## Snapshot Inputs
|
||||
|
||||
`RenderSnapshotBuilder` should build from a read-oriented runtime view, not from direct mutation calls. `RuntimeSnapshotProvider` should consume the builder's output and own publication/cache behavior.
|
||||
|
||||
That view now includes:
|
||||
|
||||
- committed live layer state from `CommittedLiveStateReadModel`
|
||||
- package and manifest metadata supplied through `RuntimeStore`
|
||||
- durable runtime configuration needed to describe render-facing dimensions and defaults
|
||||
|
||||
The important Phase 1 rule is not "the provider always reads one specific object." It is:
|
||||
|
||||
- the builder consumes read-oriented committed runtime state
|
||||
- the provider consumes builder-published render snapshot data
|
||||
- the provider does not own mutation policy
|
||||
- render consumes the provider's published output instead of reaching back into whichever runtime object currently stores the truth
|
||||
|
||||
## Snapshot Model
|
||||
|
||||
The subsystem should publish a render snapshot object rather than loose vectors and ad hoc version getters.
|
||||
|
||||
Suggested top-level shape:
|
||||
|
||||
```cpp
|
||||
struct RuntimeRenderSnapshot
|
||||
{
|
||||
uint64_t snapshotVersion = 0;
|
||||
uint64_t structureVersion = 0;
|
||||
uint64_t parameterVersion = 0;
|
||||
uint64_t packageVersion = 0;
|
||||
uint64_t publicationSequence = 0;
|
||||
unsigned inputWidth = 0;
|
||||
unsigned inputHeight = 0;
|
||||
unsigned outputWidth = 0;
|
||||
unsigned outputHeight = 0;
|
||||
std::vector<RuntimeRenderLayerSnapshot> layers;
|
||||
};
|
||||
```
|
||||
|
||||
Suggested per-layer shape:
|
||||
|
||||
```cpp
|
||||
struct RuntimeRenderLayerSnapshot
|
||||
{
|
||||
std::string layerId;
|
||||
std::string shaderId;
|
||||
std::string shaderName;
|
||||
double mixAmount = 1.0;
|
||||
double bypass = 0.0;
|
||||
std::vector<ShaderParameterDefinition> parameterDefinitions;
|
||||
std::map<std::string, ShaderParameterValue> parameterValues;
|
||||
std::vector<ShaderTextureAsset> textureAssets;
|
||||
std::vector<ShaderFontAsset> fontAssets;
|
||||
bool isTemporal = false;
|
||||
TemporalHistorySource temporalHistorySource = TemporalHistorySource::None;
|
||||
unsigned requestedTemporalHistoryLength = 0;
|
||||
unsigned effectiveTemporalHistoryLength = 0;
|
||||
FeedbackSettings feedback;
|
||||
};
|
||||
```
|
||||
|
||||
This is intentionally close to today’s [RuntimeRenderState](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/shader/ShaderTypes.h:134), but split so dynamic fields are not embedded in the published structural snapshot.
|
||||
|
||||
Suggested per-frame dynamic supplement:
|
||||
|
||||
```cpp
|
||||
struct RuntimeRenderFrameContext
|
||||
{
|
||||
double timeSeconds = 0.0;
|
||||
double utcTimeSeconds = 0.0;
|
||||
double utcOffsetSeconds = 0.0;
|
||||
double startupRandom = 0.0;
|
||||
double frameCount = 0.0;
|
||||
};
|
||||
```
|
||||
|
||||
`RenderEngine` can combine `RuntimeRenderSnapshot` and `RuntimeRenderFrameContext` into its final frame-local render input without forcing snapshot republish every frame.
|
||||
|
||||
## Publication Rules
|
||||
|
||||
The provider should publish a new structural snapshot when any render-relevant structural or committed-live field changes, including:
|
||||
|
||||
- layer add/remove/reorder
|
||||
- shader id change on a layer
|
||||
- layer bypass change
|
||||
- parameter value change that is part of committed live state
|
||||
- shader package metadata refresh that changes parameter definitions, assets, temporal declarations, or feedback declarations
|
||||
- input or output dimensions that change render-facing layer interpretation
|
||||
- stack preset load that changes any render-facing state
|
||||
|
||||
The provider should not publish a new structural snapshot just because:
|
||||
|
||||
- time advanced by one frame
|
||||
- frame count increased
|
||||
- preview cadence changed
|
||||
- render-local transient overlay state changed
|
||||
- temporal history or feedback textures changed
|
||||
- device playout queue state changed
|
||||
|
||||
That distinction matters because the current model effectively mixes structural publication with frame-local refresh and lock-driven fallback logic.
|
||||
|
||||
## Versioning Model
|
||||
|
||||
The provider should own explicit version domains rather than exposing only host-wide counters.
|
||||
|
||||
Recommended version domains:
|
||||
|
||||
- `structureVersion`
|
||||
- changes when the layer graph or shader/package-derived structure changes
|
||||
- `parameterVersion`
|
||||
- changes when committed parameter or bypass values change
|
||||
- `packageVersion`
|
||||
- changes when shader manifests or package-derived metadata relevant to render changes
|
||||
- `snapshotVersion`
|
||||
- a composed version for consumers that only need a single fast invalidation key
|
||||
- `publicationSequence`
|
||||
- monotonic sequence number for diagnostics and telemetry
|
||||
|
||||
Recommended rules:
|
||||
|
||||
- `snapshotVersion` changes whenever any render-visible aspect of the structural snapshot changes
|
||||
- `structureVersion` should not change for pure parameter edits
|
||||
- `parameterVersion` should not change for time-only updates
|
||||
- dynamic frame context should not require any version change
|
||||
|
||||
This makes later cache policy much cleaner:
|
||||
|
||||
- shader rebuild decisions can key off structure/package changes
|
||||
- parameter buffer refresh can key off parameter changes
|
||||
- frame-local updates can ignore snapshot publication entirely
|
||||
|
||||
## Snapshot Read Rules
|
||||
|
||||
The target read contract for `RenderEngine` should be:
|
||||
|
||||
1. acquire the latest published snapshot atomically or under a very small provider-owned read lock
|
||||
2. compare relevant versions with the render-side cached state
|
||||
3. if unchanged, reuse render-local compiled/cached resources
|
||||
4. if changed, rebuild only the portions implied by the changed version domains
|
||||
5. attach the current `RuntimeRenderFrameContext` for the frame being rendered
|
||||
|
||||
Important rule:
|
||||
|
||||
- `RenderEngine` should never partially mutate the provider's published snapshot in place.
|
||||
|
||||
The old `TryRefreshCachedLayerStates(...)` host path is gone. The remaining dynamic refresh is explicit: `RuntimeSnapshotProvider::RefreshDynamicRenderStateFields(...)` updates frame-local fields on render-owned copies, while published snapshot structure and committed parameter data stay behind the provider boundary.
|
||||
|
||||
## Render-Facing Data Shape Rules
|
||||
|
||||
The published snapshot should contain exactly the data render needs to interpret a layer, but not render-local execution artifacts.
|
||||
|
||||
Include:
|
||||
|
||||
- layer identity
|
||||
- shader identity and display name
|
||||
- parameter definitions
|
||||
- committed parameter values
|
||||
- bypass and mix flags needed for layer evaluation
|
||||
- texture and font asset declarations
|
||||
- temporal settings
|
||||
- feedback settings
|
||||
- input/output dimensions when they affect shader configuration or resource interpretation
|
||||
|
||||
Do not include:
|
||||
|
||||
- GL object ids
|
||||
- framebuffer handles
|
||||
- compiled shader programs
|
||||
- live texture bindings resolved to hardware units
|
||||
- temporal history texture state
|
||||
- feedback buffer contents
|
||||
- queued OSC overlays
|
||||
- queued input frames
|
||||
- preview frame caches
|
||||
- DeckLink buffer handles
|
||||
|
||||
This line is important because current `RuntimeRenderState` is close to render-ready data, but the subsystem contract should stop before actual device or GL execution artifacts.
|
||||
|
||||
## Proposed Public Interface
|
||||
|
||||
Suggested interface shape:
|
||||
|
||||
```cpp
|
||||
class IRuntimeSnapshotProvider
|
||||
{
|
||||
public:
|
||||
virtual ~IRuntimeSnapshotProvider() = default;
|
||||
|
||||
virtual RuntimeRenderSnapshot BuildSnapshot(
|
||||
const RuntimeStoreView& storeView,
|
||||
const SnapshotBuildOptions& options) const = 0;
|
||||
|
||||
virtual void PublishSnapshot(RuntimeRenderSnapshot snapshot) = 0;
|
||||
virtual std::shared_ptr<const RuntimeRenderSnapshot> GetLatestSnapshot() const = 0;
|
||||
virtual uint64_t GetSnapshotVersion() const = 0;
|
||||
virtual RuntimeRenderFrameContext BuildFrameContext() const = 0;
|
||||
};
|
||||
```
|
||||
|
||||
Likely supporting methods:
|
||||
|
||||
- `BuildLayerSnapshot(...)`
|
||||
- `BuildFrameContext(...)`
|
||||
- `ComputeSnapshotVersion(...)`
|
||||
- `DidStructureChange(...)`
|
||||
- `DidParametersChange(...)`
|
||||
- `PublishIfChanged(...)`
|
||||
|
||||
Notes:
|
||||
|
||||
- `GetLatestSnapshot()` should ideally return a shared immutable snapshot pointer or equivalent stable handle
|
||||
- `BuildFrameContext()` may remain provider-owned or later move behind a clock/timing helper if that subsystem becomes more explicit
|
||||
- publication should be initiated by `RuntimeCoordinator`, not by render
|
||||
|
||||
## Relationship to Other Subsystems
|
||||
|
||||
### `RuntimeStore`
|
||||
|
||||
`RenderSnapshotBuilder` depends on store-owned durable metadata and the committed-live read model exposed through store-facing read APIs. `RuntimeSnapshotProvider` depends on the builder rather than reaching into store internals directly.
|
||||
|
||||
Committed session layer state now lives in `CommittedLiveState`; `RuntimeStore` remains the facade that combines that read model with package metadata and persistence-owned data for snapshot publication.
|
||||
|
||||
Neither the builder nor provider should mutate the store directly.
|
||||
|
||||
### `RuntimeCoordinator`
|
||||
|
||||
`RuntimeCoordinator` decides when a mutation requires snapshot republish.
|
||||
|
||||
The provider should not reclassify policy. It should only:
|
||||
|
||||
- build
|
||||
- compare
|
||||
- publish
|
||||
|
||||
based on the change request it is asked to materialize.
|
||||
|
||||
### `RenderEngine`
|
||||
|
||||
`RenderEngine` is the main consumer.
|
||||
|
||||
It should:
|
||||
|
||||
- read the latest published snapshot
|
||||
- treat that snapshot as immutable
|
||||
- derive render-local artifacts from it
|
||||
- keep frame-local overlays and history outside the provider
|
||||
|
||||
### `HealthTelemetry`
|
||||
|
||||
The provider should emit:
|
||||
|
||||
- snapshot publication counts
|
||||
- snapshot build duration
|
||||
- version bump reason categories
|
||||
- publication suppression counts when no effective change occurred
|
||||
- warning states if snapshot build repeatedly fails
|
||||
|
||||
This is especially important while migrating away from the current lock/fallback model.
|
||||
|
||||
## Current Code Mapping
|
||||
|
||||
The current runtime path is:
|
||||
|
||||
1. get latest published snapshot from provider
|
||||
2. compare snapshot versions produced by `RenderSnapshotBuilder`
|
||||
3. rebuild through `RenderSnapshotBuilder` only if needed
|
||||
4. apply render-local overlay state
|
||||
5. attach frame context
|
||||
|
||||
That replaced the old mixed lock/cache/fallback flow that lived around [OpenGLComposite.cpp](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/gl/composite/OpenGLComposite.cpp:589).
|
||||
|
||||
`RenderSnapshotBuilder` now owns:
|
||||
|
||||
- layer render-state construction
|
||||
- render-facing translation of committed live state plus package metadata
|
||||
- explicit version composition for render-visible state
|
||||
- dynamic frame-field refresh for render-owned copies
|
||||
|
||||
`RuntimeSnapshotProvider` now owns:
|
||||
|
||||
- published snapshot cache ownership
|
||||
- version matching for already-published snapshots
|
||||
- publication events and snapshot publish observations
|
||||
|
||||
## Migration Plan
|
||||
|
||||
### Step 1: Introduce provider types without changing behavior
|
||||
|
||||
- define `RuntimeRenderSnapshot`, `RuntimeRenderLayerSnapshot`, and `RuntimeRenderFrameContext`
|
||||
- initially implement provider methods as thin wrappers over existing behavior
|
||||
- completed: replace the temporary `RuntimeHost` backing source with `RenderSnapshotBuilder`
|
||||
|
||||
### Step 2: Route render reads through the provider
|
||||
|
||||
- replace direct host/store layer-state reads with provider snapshot reads
|
||||
- preserve current version behavior first, even if internally bridged to existing counters
|
||||
|
||||
### Step 3: Separate structural publication from frame context
|
||||
|
||||
- stop rebuilding structural layer state just to refresh time and frame values
|
||||
- let render request frame context separately each frame
|
||||
|
||||
### Step 4: Remove mutable snapshot refresh paths
|
||||
|
||||
- completed: retire the old `TryRefreshCachedLayerStates(...)` host path
|
||||
- publish new snapshots for committed parameter changes instead of mutating published snapshot structure in place
|
||||
|
||||
### Step 5: Move publication triggering fully behind `RuntimeCoordinator`
|
||||
|
||||
- no render-driven snapshot rebuilding
|
||||
- coordinator requests publication after successful committed mutations and reloads
|
||||
|
||||
## Risks
|
||||
|
||||
### Risk: snapshot copies become expensive
|
||||
|
||||
Publishing whole snapshots on every parameter commit could be expensive if the layer stack grows.
|
||||
|
||||
Mitigation:
|
||||
|
||||
- use immutable shared snapshots with replace-on-publish semantics
|
||||
- consider per-layer structural sharing later if real profiles justify it
|
||||
- avoid republishing for frame-local time-only changes
|
||||
|
||||
### Risk: unclear boundary between committed state and transient overlay state
|
||||
|
||||
If overlays are accidentally folded into the published snapshot, the provider will recreate the coupling that the subsystem split is supposed to remove.
|
||||
|
||||
Mitigation:
|
||||
|
||||
- keep overlays render-local or coordinator-owned transient state
|
||||
- document that snapshots represent committed render-facing truth, not in-flight automation state
|
||||
|
||||
### Risk: version domains are under-specified
|
||||
|
||||
If version rules are not crisp, render may still over-rebuild or miss needed updates.
|
||||
|
||||
Mitigation:
|
||||
|
||||
- make version bump reasons explicit
|
||||
- log version-domain changes during migration
|
||||
- add tests around parameter-only, structure-only, and package-only changes
|
||||
|
||||
### Risk: snapshot publication is treated as a background convenience rather than a core contract
|
||||
|
||||
If code keeps reaching around the provider into the store, the architecture will remain half-split.
|
||||
|
||||
Mitigation:
|
||||
|
||||
- treat provider publication as the only supported render-facing state publication path
|
||||
- convert direct host/store render-state methods into adapters, then remove them
|
||||
|
||||
## Testing Strategy
|
||||
|
||||
The provider should be testable without GL or hardware.
|
||||
|
||||
Recommended tests:
|
||||
|
||||
- snapshot build from a sample layer stack
|
||||
- parameter-only mutation increments `parameterVersion` but not `structureVersion`
|
||||
- layer reorder increments `structureVersion`
|
||||
- shader manifest change increments `packageVersion`
|
||||
- frame context changes over time without forcing `snapshotVersion` changes
|
||||
- repeated publish with no effective change suppresses unnecessary version bumps
|
||||
- feedback and temporal declarations are preserved correctly in published layer snapshots
|
||||
|
||||
## Open Questions
|
||||
|
||||
- Should output dimensions live inside the top-level snapshot only, or also be copied into each layer snapshot for compatibility with current code paths?
|
||||
- Should package-derived compile-ready pass source metadata eventually be published by this provider, or remain a separate build artifact pipeline?
|
||||
- Is `BuildFrameContext()` part of the provider long-term, or should timing/clock publication become its own helper owned adjacent to `HealthTelemetry`?
|
||||
- Do parameter-only changes always require full snapshot republish, or should later phases add more granular per-layer publication handles?
|
||||
- Should the provider own input signal dimensions directly, or should those come from a backend-published runtime environment view supplied during build?
|
||||
|
||||
## Completion Criteria For This Subsystem
|
||||
|
||||
`RuntimeSnapshotProvider` can be considered architecturally in place once:
|
||||
|
||||
- render no longer reads `RuntimeStore` or legacy host render state directly
|
||||
- render consumes published snapshot handles rather than rebuilding layer vectors from host state
|
||||
- dynamic frame fields are supplied separately from structural snapshot publication
|
||||
- snapshot version domains are explicit and observable
|
||||
- transient overlays remain outside the published snapshot contract
|
||||
|
||||
## Short Version
|
||||
|
||||
`RuntimeSnapshotProvider` should become the single place that turns committed runtime state into render-consumable published snapshots.
|
||||
|
||||
Its contract is:
|
||||
|
||||
- build from store-owned state
|
||||
- publish immutable or near-immutable render snapshots; the current implementation keeps the last matching versioned snapshot in `RuntimeSnapshotProvider`
|
||||
- version them explicitly
|
||||
- keep frame-local timing separate
|
||||
- give render a cheap, lock-light read path
|
||||
|
||||
If that boundary is held, later phases can isolate render timing and decouple playout without inventing a second render-state authority.
|
||||
590
docs/subsystems/RuntimeStore.md
Normal file
590
docs/subsystems/RuntimeStore.md
Normal file
@@ -0,0 +1,590 @@
|
||||
# RuntimeStore Subsystem Design
|
||||
|
||||
This document expands the `RuntimeStore` portion of [PHASE_1_SUBSYSTEM_BOUNDARIES_DESIGN.md](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/docs/PHASE_1_SUBSYSTEM_BOUNDARIES_DESIGN.md) into a subsystem-specific design note.
|
||||
|
||||
The purpose of `RuntimeStore` is to give the Phase 1 target architecture one clear home for durable runtime data. Before the Phase 1 runtime split, that responsibility was spread through `RuntimeHost`, where persistence, mutation entrypoints, render-state building, shader metadata access, and status reporting all shared the same object and lock domain. `RuntimeStore` is the design boundary that separates "what the app knows and saves" from "how the app decides to mutate it" and "how rendering consumes it."
|
||||
|
||||
## Role In The Phase 1 Architecture
|
||||
|
||||
Within the Phase 1 subsystem model, `RuntimeStore` is the durable data authority.
|
||||
|
||||
It exists to answer questions like:
|
||||
|
||||
- what runtime configuration is currently loaded
|
||||
- what the saved layer stack structure is
|
||||
- what the saved parameter values are
|
||||
- what stack presets exist and what they contain
|
||||
- what package and manifest metadata is available for validation and snapshot building
|
||||
|
||||
It should not answer questions like:
|
||||
|
||||
- should this control mutation be allowed
|
||||
- should this OSC value be treated as transient or persisted
|
||||
- how should the render thread consume state
|
||||
- when should output frames be scheduled
|
||||
- what warnings should be shown to the operator
|
||||
|
||||
That policy belongs elsewhere:
|
||||
|
||||
- mutation policy: `RuntimeCoordinator`
|
||||
- render-facing publication: `RuntimeSnapshotProvider`
|
||||
- hardware timing: `VideoBackend`
|
||||
- operational visibility: `HealthTelemetry`
|
||||
|
||||
## Design Goals
|
||||
|
||||
`RuntimeStore` should optimize for:
|
||||
|
||||
- explicit ownership of durable runtime data
|
||||
- predictable disk-backed load and save behavior
|
||||
- minimal knowledge of GL, callbacks, or live playout timing
|
||||
- stable read models for validation and snapshot building
|
||||
- a clean seam for introducing debounced or asynchronous persistence later
|
||||
- testability without GPU or DeckLink dependencies
|
||||
|
||||
## Responsibilities
|
||||
|
||||
`RuntimeStore` owns persisted and operator-authored state.
|
||||
|
||||
Primary responsibilities:
|
||||
|
||||
- load runtime host configuration from disk
|
||||
- load saved runtime state from disk
|
||||
- save runtime state snapshots to disk
|
||||
- own the stored layer stack model
|
||||
- own persisted parameter values and bypass flags
|
||||
- own stack preset serialization and deserialization
|
||||
- own package/manifest metadata needed across renders and reloads
|
||||
- expose query/read APIs over stored state
|
||||
- expose write APIs for coordinator-approved durable mutations
|
||||
- normalize or repair stored data at load boundaries when necessary
|
||||
|
||||
Secondary responsibilities that still fit here:
|
||||
|
||||
- path resolution for runtime state and preset files
|
||||
- preset name normalization/file-stem safety
|
||||
- compatibility handling for older saved-state schemas
|
||||
- default seeding of initial persistent state when no saved runtime exists
|
||||
|
||||
## Non-Responsibilities
|
||||
|
||||
`RuntimeStore` must not become a general convenience layer again.
|
||||
|
||||
It does not own:
|
||||
|
||||
- render-thread timing
|
||||
- GL objects or resource lifetime
|
||||
- shader compilation orchestration
|
||||
- render-local transient state such as temporal history, feedback buffers, preview caches, or playout queues
|
||||
- OSC smoothing, coalescing, or overlay application
|
||||
- websocket broadcast policy
|
||||
- REST or OSC ingress handling
|
||||
- device callbacks, queue-depth policy, or preroll policy
|
||||
- app-wide health aggregation
|
||||
|
||||
It also should not directly decide:
|
||||
|
||||
- whether a mutation is valid in policy terms
|
||||
- whether a change should persist immediately, eventually, or not at all
|
||||
- when a new render snapshot should be published
|
||||
- whether a reload should be treated as config-only, package-only, or render-affecting
|
||||
|
||||
Those are coordinator concerns, not store concerns.
|
||||
|
||||
## State Ownership
|
||||
|
||||
`RuntimeStore` should own the following state categories.
|
||||
|
||||
Phase 5 names this boundary in code through `RuntimeStateLayerModel`: persisted layer stack data, saved parameter values, and stack presets are classified as base persisted state. Operator/session values are owned by `CommittedLiveState`; their mutation policy is committed-live policy owned by the coordinator, not durable-store policy by default.
|
||||
|
||||
Phase 5 also adds `CommittedLiveState` as the physical owner of current session/operator layer state and `CommittedLiveStateReadModel` as the named read boundary for render snapshot publication. `RuntimeStore` still owns file IO, config, package metadata, preset persistence, and persistence requests, but it delegates current-session layer mutations to `CommittedLiveState`.
|
||||
|
||||
### Runtime Configuration
|
||||
|
||||
Examples:
|
||||
|
||||
- server/control ports
|
||||
- OSC bind address
|
||||
- OSC smoothing defaults
|
||||
- runtime paths and directory configuration
|
||||
- any host-side configuration loaded from `config/runtime-host.json`
|
||||
|
||||
This data is durable, file-backed, and not inherently render-local.
|
||||
|
||||
### Persistent Layer Stack State
|
||||
|
||||
Examples:
|
||||
|
||||
- ordered layer list
|
||||
- stable layer ids
|
||||
- selected shader id per layer
|
||||
- bypass state
|
||||
- persisted parameter values
|
||||
|
||||
This is the stored "official" layer model, not a render-thread working copy.
|
||||
|
||||
### Stack Presets
|
||||
|
||||
Examples:
|
||||
|
||||
- preset names
|
||||
- serialized saved layer stacks under `runtime/stack_presets`
|
||||
|
||||
Preset files are durable artifacts and should remain in the store domain even if later phases add async writing.
|
||||
|
||||
### Shader/Package Metadata Needed As Durable Reference Data
|
||||
|
||||
Examples:
|
||||
|
||||
- discovered shader package manifests
|
||||
- parameter definitions used for validation/default restoration
|
||||
- manifest-level capability metadata such as temporal history and feedback declarations
|
||||
- package ordering that should survive across reloads
|
||||
|
||||
Important distinction:
|
||||
|
||||
- manifest and package metadata belongs here
|
||||
- render-ready compiled programs and GPU resources do not
|
||||
|
||||
### Load-Time Compatibility/Repair State
|
||||
|
||||
Examples:
|
||||
|
||||
- schema version adaptation
|
||||
- default value filling for missing parameters
|
||||
- removal or migration of layers that reference missing packages
|
||||
- preset compatibility cleanup
|
||||
|
||||
This should be treated as store hygiene during ingest, not runtime mutation policy.
|
||||
|
||||
## Data Model Boundaries
|
||||
|
||||
`RuntimeStore` should present data in durable-model terms rather than live-render terms.
|
||||
|
||||
Core model groupings:
|
||||
|
||||
- `RuntimeConfigModel`
|
||||
- `PersistentLayerStackModel`
|
||||
- `LayerStoredState`
|
||||
- `StoredParameterValue`
|
||||
- `StackPresetModel`
|
||||
- `ShaderPackageCatalog` or equivalent durable package registry view
|
||||
|
||||
The exact C++ types may differ from these names, but the boundary should hold:
|
||||
|
||||
- store models describe durable intent
|
||||
- snapshot models describe render consumption
|
||||
|
||||
That means `RuntimeStore` should not expose render-optimized structures such as `RuntimeRenderState` directly as its primary interface.
|
||||
|
||||
## Interface Shape
|
||||
|
||||
The Phase 1 architecture doc already sketches the high-level interface. This section expands it.
|
||||
|
||||
### Load / Save Interface
|
||||
|
||||
Expected responsibilities:
|
||||
|
||||
- `LoadConfig()`
|
||||
- `LoadPersistentState()`
|
||||
- `BuildPersistentStateSnapshot(...)`
|
||||
- `RequestPersistence(...)`
|
||||
- `LoadStackPreset(...)`
|
||||
- `SaveStackPreset(...)`
|
||||
- `GetStackPresetNames()`
|
||||
|
||||
Design notes:
|
||||
|
||||
- `Load*` operations should parse and normalize external file content into durable in-memory models.
|
||||
- `Save*` operations should serialize durable models without needing render or control subsystem context.
|
||||
- debounce/background writing wraps these operations rather than redefining store ownership
|
||||
|
||||
### Read Interface
|
||||
|
||||
Expected responsibilities:
|
||||
|
||||
- `GetRuntimeConfig()`
|
||||
- `GetStoredLayerStack()`
|
||||
- `FindStoredLayer(...)`
|
||||
- `GetShaderPackageCatalog()`
|
||||
- `GetStackPresetNames()`
|
||||
- `BuildPersistenceSnapshot()` or equivalent stable serialization input
|
||||
|
||||
Design notes:
|
||||
|
||||
- read APIs should support coordinator validation and snapshot building
|
||||
- read APIs should avoid exposing raw mutable internals across subsystem boundaries
|
||||
- stable read snapshots from the store are fine; render snapshots are still the snapshot provider's job
|
||||
|
||||
### Write Interface
|
||||
|
||||
Expected responsibilities:
|
||||
|
||||
- `SetStoredLayerStack(...)`
|
||||
- `ReplaceStoredLayer(...)`
|
||||
- `SetStoredParameterValue(...)`
|
||||
- `SetStoredBypassState(...)`
|
||||
- `SetStoredShaderSelection(...)`
|
||||
- `ReplaceShaderPackageCatalog(...)`
|
||||
|
||||
Design notes:
|
||||
|
||||
- writes should assume the coordinator already decided the mutation is allowed
|
||||
- store APIs may still enforce structural invariants and shape correctness
|
||||
- writes should not contain ingress-specific policy like OSC smoothing or UI throttling
|
||||
|
||||
### Normalization / Validation-Support Interface
|
||||
|
||||
Expected responsibilities:
|
||||
|
||||
- `NormalizeLoadedState(...)`
|
||||
- `EnsureStoredDefaults(...)`
|
||||
- `MakeSafePresetFileStem(...)`
|
||||
- package lookup helpers for parameter-definition queries
|
||||
|
||||
Design notes:
|
||||
|
||||
- lightweight structure and schema validation belongs here
|
||||
- policy validation belongs in the coordinator
|
||||
- render compatibility translation belongs in the snapshot provider
|
||||
|
||||
## Concurrency Expectations
|
||||
|
||||
`RuntimeStore` should be designed as a shared data authority, but not as the app's global lock for everything.
|
||||
|
||||
Phase 1 design expectations:
|
||||
|
||||
- coordinator-driven writes may still be synchronized internally
|
||||
- read APIs should be safe for coordinator and snapshot-provider use
|
||||
- render should not directly take a large mutable store lock in the target architecture
|
||||
|
||||
This implies:
|
||||
|
||||
- `RuntimeStore` may keep an internal mutex during migration
|
||||
- that mutex should protect durable models only
|
||||
- render-facing consumers should eventually read via `RuntimeSnapshotProvider`, not by reaching into the store
|
||||
|
||||
One of the main goals here is avoiding the old situation where runtime lock scope effectively mixed:
|
||||
|
||||
- persistent state
|
||||
- status reporting
|
||||
- render-state caches
|
||||
- timing stats
|
||||
- reload flags
|
||||
|
||||
`RuntimeStore` should sharply narrow that scope.
|
||||
|
||||
## Dependency Rules
|
||||
|
||||
Per the Phase 1 subsystem design, `RuntimeStore` should sit low in the dependency graph.
|
||||
|
||||
Allowed inbound dependencies:
|
||||
|
||||
- `RuntimeCoordinator -> RuntimeStore`
|
||||
- `RenderSnapshotBuilder -> RuntimeStore`
|
||||
- temporary migration shims from `ControlServices` only where explicitly tolerated
|
||||
|
||||
Allowed outbound dependencies:
|
||||
|
||||
- file/serialization helpers
|
||||
- package manifest parsing helpers
|
||||
- pure utility types
|
||||
|
||||
Not allowed:
|
||||
|
||||
- `RuntimeStore -> RenderEngine`
|
||||
- `RuntimeStore -> VideoBackend`
|
||||
- `RuntimeStore -> ControlServices`
|
||||
- `RuntimeStore -> HealthTelemetry` for behavior control
|
||||
|
||||
The store may emit errors or return result objects, but it should not coordinate the rest of the system directly.
|
||||
|
||||
## Current Code Mapping
|
||||
|
||||
Before the Phase 1 runtime split, `RuntimeHost` contained many responsibilities that needed to move into `RuntimeStore` or adjacent runtime collaborators.
|
||||
|
||||
Previous code paths:
|
||||
|
||||
- config load:
|
||||
- `RuntimeHost.cpp`
|
||||
- persistent state load:
|
||||
- `RuntimeHost.cpp`
|
||||
- persistent state save:
|
||||
- `RuntimeHost.cpp`
|
||||
- preset save/load:
|
||||
- `RuntimeHost.cpp`
|
||||
- `RuntimeHost.cpp`
|
||||
- state serialization helpers:
|
||||
- `RuntimeHost.cpp`
|
||||
- `RuntimeHost.cpp`
|
||||
- `RuntimeHost.cpp`
|
||||
- path and file helpers:
|
||||
- `RuntimeHost.cpp`
|
||||
- `RuntimeHost.cpp`
|
||||
- `RuntimeHost.cpp`
|
||||
|
||||
Durable-state mutation entrypoints that previously lived on `RuntimeHost` but conceptually split between coordinator and store:
|
||||
|
||||
- layer stack edits:
|
||||
- `AddLayer`
|
||||
- `RemoveLayer`
|
||||
- `MoveLayer`
|
||||
- `MoveLayerToIndex`
|
||||
- committed-state edits:
|
||||
- `SetLayerBypass`
|
||||
- `SetLayerShader`
|
||||
- `UpdateLayerParameter`
|
||||
- `ResetLayerParameters`
|
||||
|
||||
The target split should be:
|
||||
|
||||
- validation/policy/orchestration -> `RuntimeCoordinator`
|
||||
- durable state write application -> `RuntimeStore`
|
||||
|
||||
Methods that were intentionally not moved into `RuntimeStore` because they belong under other runtime subsystems:
|
||||
|
||||
- render-state building and caching:
|
||||
- `GetLayerRenderStates`
|
||||
- `TryRefreshCachedLayerStates`
|
||||
- `BuildLayerRenderStatesLocked`
|
||||
- status/timing reporting:
|
||||
- `SetSignalStatus`
|
||||
- `SetPerformanceStats`
|
||||
- `SetFramePacingStats`
|
||||
- `AdvanceFrame`
|
||||
- live reload flags/polling shell:
|
||||
- `PollFileChanges`
|
||||
- `ManualReloadRequested`
|
||||
- `ClearReloadRequest`
|
||||
|
||||
Those belong under other target subsystems.
|
||||
|
||||
## Proposed Internal Subcomponents
|
||||
|
||||
`RuntimeStore` does not need to be one monolithic class forever. A practical internal shape would be:
|
||||
|
||||
- `RuntimeConfigStore`
|
||||
- runtime host config load and resolved paths
|
||||
|
||||
The current codebase has completed this part of the split: `RuntimeConfigStore` owns config parsing, path resolution, configured ports/formats, runtime roots, and shader compiler paths, while `RuntimeStore` exposes compatibility-shaped delegates for existing callers.
|
||||
- `CommittedLiveState`
|
||||
- current committed/session layer stack and parameter values
|
||||
- layer CRUD/reorder and shader selection for the running session
|
||||
- committed-live read model for snapshot publication
|
||||
- `LayerStackStore`
|
||||
- backing layer stack mechanics used by committed-live state
|
||||
- layer CRUD/reorder and shader selection helpers
|
||||
- stack preset value serialization/load helpers
|
||||
- `RuntimeStatePresenter` / `RuntimeStateJson`
|
||||
- runtime-state JSON assembly
|
||||
- layer-stack presentation serialization
|
||||
- `RenderSnapshotBuilder`
|
||||
- render-state assembly and parameter refresh
|
||||
- dynamic frame-field refresh and render snapshot version counters
|
||||
- `ShaderPackageCatalog`
|
||||
- durable manifest/package metadata
|
||||
- shader package scanning, status/order/lookup, and asset/source change comparison
|
||||
- `PersistenceWriter` helper
|
||||
- synchronous at first, async/debounced later
|
||||
|
||||
The current codebase has completed the committed-live split: `CommittedLiveState` owns current committed/session layer state using `LayerStackStore` backing mechanics. `RuntimeStore` keeps file IO, package metadata, persistence serialization, persistence requests, preset file access, and facade methods for existing callers.
|
||||
|
||||
The current codebase has completed the render snapshot split: `RenderSnapshotBuilder` owns render-state assembly, cached parameter refresh, dynamic frame-field refresh, and render snapshot versions. `RuntimeSnapshotProvider` depends on this builder rather than on `RuntimeStore` friendship.
|
||||
|
||||
The current codebase has also completed the presentation split: `RuntimeStatePresenter` owns top-level runtime-state JSON assembly, while `RuntimeStateJson` owns the layer-stack and parameter presentation shape used by runtime state clients.
|
||||
|
||||
The current codebase has also completed the package split: `ShaderPackageCatalog` owns package scanning and registry comparison, while `RuntimeStore` uses it to keep layer state valid and to build compatibility read models.
|
||||
|
||||
These can still be presented through one subsystem façade during migration.
|
||||
|
||||
## Persistence Model
|
||||
|
||||
The store should treat persistence as durable snapshot management, not incremental side-effect spraying.
|
||||
|
||||
Target behavior:
|
||||
|
||||
- in-memory durable models are updated first
|
||||
- serialization snapshots are built from those models
|
||||
- save requests persist a coherent snapshot
|
||||
|
||||
This matters because earlier code called persistent-state saves directly from mutation paths. Phase 6 removed that pressure point: accepted durable mutations now publish persistence requests, and `RuntimeStore::RequestPersistence(...)` builds a coherent snapshot for the background writer.
|
||||
|
||||
The Phase 1 design for `RuntimeStore` should therefore assume:
|
||||
|
||||
- store ownership of serialization remains
|
||||
- persistence requests, not mutation methods, are the durable write boundary
|
||||
|
||||
Phase 6 added that background snapshot writer underneath this subsystem, while keeping the durable model here.
|
||||
|
||||
## Migration Plan From Current Code
|
||||
|
||||
The safest migration path is to extract responsibilities by interface, not by big-bang rename.
|
||||
|
||||
### Step 1: Introduce The `RuntimeStore` Name And Facade
|
||||
|
||||
Create a facade interface for the durable-data parts that used to live in `RuntimeHost`.
|
||||
|
||||
Initial likely contents:
|
||||
|
||||
- config load/save access
|
||||
- persistent layer-stack get/set access
|
||||
- preset load/save access
|
||||
- package catalog read access
|
||||
|
||||
This stage is complete: `RuntimeStore` owns its durable/session backing fields directly rather than wrapping a `RuntimeHost` object.
|
||||
|
||||
### Step 2: Move Pure Persistence Helpers First
|
||||
|
||||
Low-risk extractions:
|
||||
|
||||
- path resolution helpers
|
||||
- file read/write helpers
|
||||
- preset enumeration and serialization helpers
|
||||
- persistent-state serialization/deserialization helpers
|
||||
|
||||
These have relatively low coupling to GL and backend timing.
|
||||
|
||||
### Step 3: Split Durable Models From Render Cache/Status Fields
|
||||
|
||||
Move out or conceptually separate:
|
||||
|
||||
- `mPersistentState`
|
||||
- runtime config fields
|
||||
- preset roots and runtime roots
|
||||
- package catalog/order metadata
|
||||
|
||||
From fields that should stay elsewhere:
|
||||
|
||||
- render-state dirty flags and caches
|
||||
- status/timing counters
|
||||
- reload flags
|
||||
|
||||
This is one of the most important separations in the whole program.
|
||||
|
||||
### Step 4: Route Durable Mutations Through Coordinator-Owned Policy
|
||||
|
||||
Once the coordinator exists, `RuntimeStore` write calls should become lower-level and less policy-rich.
|
||||
|
||||
Examples:
|
||||
|
||||
- `SetStoredParameterValue(...)` rather than `ApplyOscTargetByControlKey(...)`
|
||||
- `ReplaceStoredLayerStack(...)` rather than `LoadStackPreset(...)` directly mutating every downstream concern
|
||||
|
||||
### Step 5: Keep Render Off The Store
|
||||
|
||||
As `RuntimeSnapshotProvider` arrives, render should stop reading store internals directly.
|
||||
|
||||
That is the moment where `RuntimeStore` becomes a proper durable authority instead of a shared mutable app center.
|
||||
|
||||
## Risks
|
||||
|
||||
### 1. Recreating `RuntimeHost` Under A New Name
|
||||
|
||||
The biggest risk is calling something `RuntimeStore` while leaving policy, status, and render-cache behavior attached.
|
||||
|
||||
Guardrail:
|
||||
|
||||
- only durable data and store hygiene belong here
|
||||
|
||||
### 2. Letting Validation Drift Into Persistence
|
||||
|
||||
Store-level shape validation is appropriate. High-level mutation policy is not.
|
||||
|
||||
Risk examples:
|
||||
|
||||
- store decides whether OSC should persist
|
||||
- store decides whether a layer reorder should trigger snapshot publication
|
||||
- store decides whether a reload is render-only or package-affecting
|
||||
|
||||
Those are coordinator decisions.
|
||||
|
||||
### 3. Overexposing Mutable Internals
|
||||
|
||||
If callers keep direct mutable access to the underlying vectors/maps, the subsystem boundary will exist only on paper.
|
||||
|
||||
Guardrail:
|
||||
|
||||
- prefer controlled write methods and stable read models
|
||||
|
||||
### 4. Coupling Package Metadata Too Tightly To Compile Outputs
|
||||
|
||||
Package manifest and parameter-definition metadata belongs here. Compiled program state does not.
|
||||
|
||||
Guardrail:
|
||||
|
||||
- keep compile products and GPU artifacts out of the store
|
||||
|
||||
### 5. Using The Store Lock As A Global Synchronization Shortcut
|
||||
|
||||
This would recreate timing and contention issues in a new form.
|
||||
|
||||
Guardrail:
|
||||
|
||||
- store locking protects durable models only
|
||||
- render synchronization must happen through snapshots, not by sharing the store lock
|
||||
|
||||
## Open Questions
|
||||
|
||||
### 1. How Much Shader Package Data Should Live Here?
|
||||
|
||||
Clear yes:
|
||||
|
||||
- manifest metadata
|
||||
- parameter definitions
|
||||
- package discovery/order information
|
||||
|
||||
Still open:
|
||||
|
||||
- whether compile-ready transformed sources belong here or in a later build-focused subsystem
|
||||
|
||||
Current recommendation:
|
||||
|
||||
- keep only durable reference/package metadata here
|
||||
|
||||
### 2. Should Preset Application Be A Store Operation Or A Coordinator Operation?
|
||||
|
||||
The file load and preset parse clearly belong here.
|
||||
|
||||
The policy question of how a loaded preset affects live state, snapshot publication, overlays, and notifications belongs in the coordinator.
|
||||
|
||||
Current recommendation:
|
||||
|
||||
- `RuntimeStore` loads preset content
|
||||
- `RuntimeCoordinator` decides how to apply it
|
||||
|
||||
### 3. How Early Should Async Persistence Land?
|
||||
|
||||
Phase 1 does not require it, but the store design should not block it.
|
||||
|
||||
Current recommendation:
|
||||
|
||||
- keep synchronous save semantics initially if needed
|
||||
- shape the interfaces so a background writer can be introduced without changing subsystem ownership
|
||||
|
||||
## Success Criteria For This Subsystem
|
||||
|
||||
`RuntimeStore` can be considered well-defined once the codebase can say, without ambiguity:
|
||||
|
||||
- all durable runtime config and saved layer data has one authoritative home
|
||||
- stack presets are owned by that same durable-data subsystem
|
||||
- render does not depend on store internals directly
|
||||
- timing/status/reporting state is no longer mixed into the same subsystem
|
||||
- persistence ownership is clear even before async persistence is introduced
|
||||
|
||||
## Short Version
|
||||
|
||||
`RuntimeStore` is the subsystem that should answer:
|
||||
|
||||
- what durable runtime data exists
|
||||
- what saved layer stack and parameters exist
|
||||
- what presets and package metadata exist
|
||||
- how that durable data is loaded and serialized
|
||||
|
||||
It should not answer:
|
||||
|
||||
- whether a mutation should happen
|
||||
- how rendering should consume state
|
||||
- how hardware pacing should work
|
||||
- what health warnings should be emitted
|
||||
|
||||
If this boundary holds, later phases can continue without recreating the old coupling under a different class name.
|
||||
694
docs/subsystems/VideoBackend.md
Normal file
694
docs/subsystems/VideoBackend.md
Normal file
@@ -0,0 +1,694 @@
|
||||
# VideoBackend Subsystem Design
|
||||
|
||||
This note defines the target design for the `VideoBackend` subsystem introduced in [PHASE_1_SUBSYSTEM_BOUNDARIES_DESIGN.md](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/docs/PHASE_1_SUBSYSTEM_BOUNDARIES_DESIGN.md).
|
||||
|
||||
It focuses on input/output device lifecycle, pacing, buffering, and recovery policy for live video I/O. It does not redefine the whole app architecture. Its job is to make the backend boundary concrete enough that later phases can move current DeckLink and bridge code toward one clear ownership model.
|
||||
|
||||
## Purpose
|
||||
|
||||
`VideoBackend` is the hardware-facing timing subsystem.
|
||||
|
||||
It owns:
|
||||
|
||||
- video device discovery and capability inspection
|
||||
- input and output device configuration
|
||||
- input callback handling
|
||||
- output callback handling
|
||||
- buffer-pool ownership for device-facing frames
|
||||
- playout headroom policy
|
||||
- queueing and pacing policy between render and hardware
|
||||
- input signal presence tracking
|
||||
- backend lifecycle and degraded-state transitions
|
||||
|
||||
It does not own:
|
||||
|
||||
- GL contexts
|
||||
- frame composition
|
||||
- shader execution
|
||||
- persistence
|
||||
- control mutation policy
|
||||
- render snapshot publication
|
||||
|
||||
The core rule is:
|
||||
|
||||
- `RenderEngine` produces frames
|
||||
- `VideoBackend` moves those frames to and from hardware at the right cadence
|
||||
|
||||
## Why This Subsystem Exists
|
||||
|
||||
Today the boundary between render and hardware pacing is still too blurred.
|
||||
|
||||
The main current pressure points are:
|
||||
|
||||
- `OpenGLVideoIOBridge` still performs render-facing work inside the output completion callback:
|
||||
- [OpenGLVideoIOBridge.cpp](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/gl/pipeline/OpenGLVideoIOBridge.cpp:83)
|
||||
- `DeckLinkSession` owns device setup, mutable output frame pools, and schedule timing in one class:
|
||||
- [DeckLinkSession.h](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/videoio/decklink/DeckLinkSession.h:13)
|
||||
- [DeckLinkSession.cpp](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/videoio/decklink/DeckLinkSession.cpp:289)
|
||||
- the output scheduler currently reacts to late and dropped frames with a fixed skip policy:
|
||||
- [VideoPlayoutScheduler.cpp](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/videoio/VideoPlayoutScheduler.cpp:26)
|
||||
- the current output frame pool and preroll depth are not sourced from one policy object:
|
||||
- `DeckLinkSession::ConfigureOutput()` creates `10` mutable output frames
|
||||
- `kPrerollFrameCount` is currently `12`
|
||||
|
||||
Those overlaps make latency, buffering, and recovery behavior harder to reason about.
|
||||
|
||||
## Subsystem Responsibilities
|
||||
|
||||
`VideoBackend` should own the following responsibilities explicitly.
|
||||
|
||||
### 1. Device Discovery and Capability Reporting
|
||||
|
||||
The subsystem should:
|
||||
|
||||
- discover available input and output devices
|
||||
- choose the configured input/output pair
|
||||
- inspect mode support and pixel-format support
|
||||
- expose capability facts needed by higher layers
|
||||
|
||||
Examples:
|
||||
|
||||
- input present or absent
|
||||
- output present or absent
|
||||
- model name
|
||||
- keyer support
|
||||
- internal/external keying availability
|
||||
- supported pixel formats for the configured mode
|
||||
- input/output frame sizes
|
||||
|
||||
This work is currently mostly in:
|
||||
|
||||
- [DeckLinkSession.cpp](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/videoio/decklink/DeckLinkSession.cpp:76)
|
||||
|
||||
### 2. Input Lifecycle and Input Callback Handling
|
||||
|
||||
The subsystem should:
|
||||
|
||||
- configure input mode and pixel format
|
||||
- install and own the input callback delegate
|
||||
- start and stop capture streams
|
||||
- translate hardware input frames into backend-level input frame events
|
||||
- track signal-present versus no-input-source conditions
|
||||
|
||||
It should not decide how uploaded textures are produced. That belongs to `RenderEngine`.
|
||||
|
||||
The backend may expose input frames as:
|
||||
|
||||
- borrowed CPU-accessible frame views
|
||||
- backend-managed input frame objects
|
||||
- typed input events containing signal state and frame payload metadata
|
||||
|
||||
This work is currently split across:
|
||||
|
||||
- [DeckLinkSession::ConfigureInput](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/videoio/decklink/DeckLinkSession.cpp:221)
|
||||
- [CaptureDelegate::VideoInputFrameArrived](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/videoio/decklink/DeckLinkFrameTransfer.cpp:33)
|
||||
- [OpenGLVideoIOBridge::UploadInputFrame](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/gl/pipeline/OpenGLVideoIOBridge.cpp:11)
|
||||
|
||||
### 3. Output Lifecycle and Output Callback Handling
|
||||
|
||||
The subsystem should:
|
||||
|
||||
- configure output mode and pixel format
|
||||
- own the output frame pool
|
||||
- install and own the scheduled-frame completion callback
|
||||
- start scheduled playback
|
||||
- stop scheduled playback
|
||||
- account for completion results such as completed, late, dropped, and flushed
|
||||
|
||||
It should not render the next frame in the callback path.
|
||||
|
||||
This work is currently split across:
|
||||
|
||||
- [DeckLinkSession::ConfigureOutput](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/videoio/decklink/DeckLinkSession.cpp:273)
|
||||
- [DeckLinkSession::Start](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/videoio/decklink/DeckLinkSession.cpp:358)
|
||||
- [PlayoutDelegate::ScheduledFrameCompleted](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/videoio/decklink/DeckLinkFrameTransfer.cpp:79)
|
||||
|
||||
### 4. Pacing and Scheduling Policy
|
||||
|
||||
The subsystem should own:
|
||||
|
||||
- target frame duration and timescale
|
||||
- schedule time generation
|
||||
- preroll policy
|
||||
- spare-buffer policy
|
||||
- queue headroom policy
|
||||
- late-frame and dropped-frame recovery policy
|
||||
|
||||
This is not just a utility detail. It is one of the main timing responsibilities of the subsystem.
|
||||
|
||||
The current `VideoPlayoutScheduler` is a useful seed, but it is too small and too implicit to represent the eventual backend policy by itself.
|
||||
|
||||
### 5. Device-Facing Buffer Pools
|
||||
|
||||
The subsystem should own all device-facing buffers that exist to satisfy the hardware API contract.
|
||||
|
||||
Examples:
|
||||
|
||||
- mutable output frames created through DeckLink
|
||||
- any staging buffers required by a future non-DeckLink backend
|
||||
- reusable CPU frame containers for hardware ingress/egress
|
||||
|
||||
The goal is to make buffer depth and lifetime explicit and measurable.
|
||||
|
||||
`RenderEngine` may own render surfaces and GPU readback resources. `VideoBackend` owns the buffers required to talk to the hardware or OS video I/O API.
|
||||
|
||||
### 6. Backend Health and Degraded State
|
||||
|
||||
The subsystem should publish operational state such as:
|
||||
|
||||
- running normally
|
||||
- prerolling
|
||||
- temporarily late
|
||||
- dropping frames
|
||||
- no input signal
|
||||
- output stopped
|
||||
- failed to configure
|
||||
|
||||
This state should be reported to `HealthTelemetry`, not hidden inside debug logs or modal dialog paths.
|
||||
|
||||
## Boundary With Other Subsystems
|
||||
|
||||
This subsystem must stay aligned with the Phase 1 dependency rules.
|
||||
|
||||
Allowed directions:
|
||||
|
||||
- `VideoBackend -> RenderEngine`
|
||||
- `VideoBackend -> HealthTelemetry`
|
||||
|
||||
Not allowed in the target design:
|
||||
|
||||
- `VideoBackend -> RuntimeStore`
|
||||
- `VideoBackend -> RuntimeCoordinator`
|
||||
- `VideoBackend -> ControlServices`
|
||||
|
||||
The important operational boundary is:
|
||||
|
||||
- `VideoBackend` may request or consume rendered output frames
|
||||
- it may not own frame composition policy
|
||||
|
||||
That means:
|
||||
|
||||
- no shader parameter validation here
|
||||
- no persistence decisions here
|
||||
- no direct mutation of runtime state here
|
||||
|
||||
## State Owned by VideoBackend
|
||||
|
||||
`VideoBackend` should own the following state categories.
|
||||
|
||||
### Device Configuration State
|
||||
|
||||
Examples:
|
||||
|
||||
- selected device handles
|
||||
- configured input/output formats
|
||||
- negotiated pixel formats
|
||||
- keyer configuration
|
||||
- output model name
|
||||
- supported keying flags
|
||||
|
||||
### Session Lifecycle State
|
||||
|
||||
Examples:
|
||||
|
||||
- discovered
|
||||
- configured
|
||||
- prerolling
|
||||
- running
|
||||
- degraded
|
||||
- stopping
|
||||
- stopped
|
||||
- failed
|
||||
|
||||
### Input Runtime State
|
||||
|
||||
Examples:
|
||||
|
||||
- signal present or missing
|
||||
- last observed input format properties
|
||||
- input frame counters
|
||||
- input callback timestamps
|
||||
- queued capture frames awaiting render ingestion
|
||||
|
||||
### Output Runtime State
|
||||
|
||||
Examples:
|
||||
|
||||
- output queue depth
|
||||
- free system-memory playout frame count
|
||||
- ready system-memory playout frame count
|
||||
- scheduled system-memory playout frame count
|
||||
- scheduled frame index
|
||||
- completed frame index
|
||||
- late frame count
|
||||
- dropped frame count
|
||||
- underrun/repeat/drop counters for system-memory playout policy
|
||||
- frame age at schedule time and completion callback time
|
||||
- spare buffer count
|
||||
- current headroom target
|
||||
|
||||
### Backend-Owned Transient Buffers
|
||||
|
||||
Examples:
|
||||
|
||||
- output mutable frame pool
|
||||
- playout ring buffer entries
|
||||
- input frame handoff queue
|
||||
- staging buffers if required by the device API
|
||||
|
||||
This is transient live state, not persisted state.
|
||||
|
||||
## Target Lifecycle Model
|
||||
|
||||
`VideoBackend` should eventually expose an explicit lifecycle state machine rather than relying on scattered imperative calls.
|
||||
|
||||
Suggested states:
|
||||
|
||||
1. `uninitialized`
|
||||
2. `discovering`
|
||||
3. `discovered`
|
||||
4. `configuring`
|
||||
5. `configured`
|
||||
6. `prerolling`
|
||||
7. `running`
|
||||
8. `degraded`
|
||||
9. `stopping`
|
||||
10. `stopped`
|
||||
11. `failed`
|
||||
|
||||
Suggested transition rules:
|
||||
|
||||
- `uninitialized -> discovering`
|
||||
- `discovering -> discovered | failed`
|
||||
- `discovered -> configuring | stopped`
|
||||
- `configuring -> configured | failed`
|
||||
- `configured -> prerolling | stopped`
|
||||
- `prerolling -> running | failed | stopping`
|
||||
- `running -> degraded | stopping | failed`
|
||||
- `degraded -> running | stopping | failed`
|
||||
- `stopping -> stopped`
|
||||
|
||||
Why this matters:
|
||||
|
||||
- startup failure reporting becomes more predictable
|
||||
- backend recovery can become policy-driven
|
||||
- telemetry can report backend state directly
|
||||
- later backends do not need to mimic DeckLink's exact imperative shape
|
||||
|
||||
## Target Timing Model
|
||||
|
||||
The long-term timing design should be producer/consumer playout.
|
||||
|
||||
### Current Model
|
||||
|
||||
Today the callback path effectively does this:
|
||||
|
||||
1. DeckLink signals completion.
|
||||
2. The callback path asks for a new output buffer.
|
||||
3. The callback path requests render-thread output production.
|
||||
4. The render thread renders the next frame.
|
||||
5. The render thread reads it back into the output buffer.
|
||||
6. The callback path schedules the next hardware frame.
|
||||
|
||||
That path is visible in:
|
||||
|
||||
- [OpenGLVideoIOBridge::RenderScheduledFrame](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/gl/pipeline/OpenGLVideoIOBridge.cpp:18)
|
||||
|
||||
This no longer borrows the GL context from the callback thread, but it still couples output timing directly to render-thread work.
|
||||
|
||||
### Target Model
|
||||
|
||||
The target model should be:
|
||||
|
||||
1. `RenderEngine` produces completed output frames at the configured cadence.
|
||||
2. `RenderEngine` places them into a bounded queue owned or mediated by `VideoBackend`.
|
||||
3. `VideoBackend` dequeues ready frames when the device needs them.
|
||||
4. hardware callbacks only:
|
||||
- record completion results
|
||||
- release or recycle buffers
|
||||
- dequeue and schedule the next ready frame
|
||||
- raise underrun or degraded-state signals if needed
|
||||
|
||||
The timing rule becomes:
|
||||
|
||||
- render is the producer
|
||||
- hardware output is the consumer
|
||||
|
||||
This gives the app a clear place to manage:
|
||||
|
||||
- target latency
|
||||
- playout headroom
|
||||
- stale-frame reuse
|
||||
- underrun behavior
|
||||
- spare buffer policy
|
||||
|
||||
## Input Buffering and Pacing
|
||||
|
||||
The input side needs a simpler but still explicit handoff model.
|
||||
|
||||
Recommended target behavior:
|
||||
|
||||
- hardware callbacks push input frames into a bounded ingress queue
|
||||
- `RenderEngine` pulls the newest useful input frame when preparing a render
|
||||
- if the ingress queue overflows, old frames are discarded according to policy
|
||||
|
||||
Recommended default policy for live playout:
|
||||
|
||||
- prefer recency over completeness
|
||||
- drop stale capture frames instead of blocking render or output
|
||||
|
||||
The current latest-input mailbox behavior is directionally correct for live timing:
|
||||
|
||||
- [OpenGLVideoIOBridge::UploadInputFrame](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/gl/pipeline/OpenGLVideoIOBridge.cpp:11)
|
||||
|
||||
The next improvement is to make the backend-to-render handoff policy more explicit in telemetry and playout scheduling, rather than treating it as only a render command mailbox detail.
|
||||
|
||||
Suggested input metrics:
|
||||
|
||||
- input frames received
|
||||
- no-signal transitions
|
||||
- input queue depth
|
||||
- dropped input frames
|
||||
- oldest queued input age
|
||||
|
||||
## Output Buffering and Headroom Policy
|
||||
|
||||
Output buffering should be policy-driven from one source of truth.
|
||||
|
||||
The target design should define a playout buffering policy object with at least:
|
||||
|
||||
- target preroll depth
|
||||
- minimum spare device buffers
|
||||
- maximum queued rendered frames
|
||||
- allowed catch-up depth
|
||||
- underrun behavior
|
||||
|
||||
Example policy fields:
|
||||
|
||||
- `targetPrerollFrames`
|
||||
- `minSpareOutputBuffers`
|
||||
- `maxReadyFrames`
|
||||
- `maxCatchUpFrames`
|
||||
- `reuseLastFrameOnUnderrun`
|
||||
- `allowAdaptiveHeadroom`
|
||||
|
||||
This replaces the current split between:
|
||||
|
||||
- fixed mutable frame pool size in `DeckLinkSession::ConfigureOutput()`
|
||||
- fixed preroll count in `kPrerollFrameCount`
|
||||
- fixed skip-ahead recovery in `VideoPlayoutScheduler`
|
||||
|
||||
## Underrun and Recovery Policy
|
||||
|
||||
The backend should define explicit behavior for when no fresh frame is ready at schedule time.
|
||||
|
||||
Candidate policies:
|
||||
|
||||
1. Reuse the last completed rendered frame.
|
||||
2. Reuse the last scheduled output frame.
|
||||
3. Schedule a known black or degraded frame.
|
||||
4. Temporarily increase headroom if the system is repeatedly catching up.
|
||||
|
||||
Which one is correct may differ by operating mode, but the choice should be explicit rather than incidental.
|
||||
|
||||
Similarly, completion-result handling should become measured rather than fixed.
|
||||
|
||||
The current scheduler does this:
|
||||
|
||||
- late or dropped frame -> `mScheduledFrameIndex += 2`
|
||||
|
||||
That is a useful emergency simplification, but not a durable backend contract.
|
||||
|
||||
The target backend should instead track:
|
||||
|
||||
- scheduled frame index
|
||||
- completed frame index
|
||||
- backlog depth
|
||||
- late streaks
|
||||
- dropped streaks
|
||||
- current operating headroom
|
||||
|
||||
Then recovery can use measured lag, not a hardcoded skip.
|
||||
|
||||
## Suggested Public Interface
|
||||
|
||||
This is not a final class API. It describes the shape the subsystem should move toward.
|
||||
|
||||
### Discovery and Configuration
|
||||
|
||||
- `DiscoverDevices(...)`
|
||||
- `SelectFormats(...)`
|
||||
- `ConfigureInput(...)`
|
||||
- `ConfigureOutput(...)`
|
||||
- `GetCapabilities()`
|
||||
- `GetBackendState()`
|
||||
|
||||
### Lifecycle
|
||||
|
||||
- `StartCapture()`
|
||||
- `StartPlayout()`
|
||||
- `StopCapture()`
|
||||
- `StopPlayout()`
|
||||
- `Shutdown()`
|
||||
|
||||
### Input Handoff
|
||||
|
||||
- `PollInputFrame(...)` or `TryDequeueInputFrame(...)`
|
||||
- `ReportInputSignalState(...)`
|
||||
|
||||
### Output Handoff
|
||||
|
||||
- `QueueRenderedFrame(...)`
|
||||
- `TryDequeueReadyFrameForSchedule(...)`
|
||||
- `RecycleCompletedFrame(...)`
|
||||
|
||||
### Timing and Recovery
|
||||
|
||||
- `SetPlayoutPolicy(...)`
|
||||
- `AccountForCompletionResult(...)`
|
||||
- `BuildBackendTimingSnapshot()`
|
||||
|
||||
### Health Reporting
|
||||
|
||||
- `BuildBackendHealthSnapshot()`
|
||||
- `GetWarningState()`
|
||||
|
||||
## Suggested Internal Components
|
||||
|
||||
The subsystem will likely be easier to evolve if its responsibilities are split internally.
|
||||
|
||||
Possible internal structure:
|
||||
|
||||
### `VideoBackendSession`
|
||||
|
||||
Owns:
|
||||
|
||||
- high-level lifecycle state
|
||||
- configuration
|
||||
- input/output subcomponents
|
||||
- policy objects
|
||||
|
||||
### `InputEndpoint`
|
||||
|
||||
Owns:
|
||||
|
||||
- input device callback registration
|
||||
- input frame queue
|
||||
- signal detection state
|
||||
|
||||
### `OutputEndpoint`
|
||||
|
||||
Owns:
|
||||
|
||||
- output device callback registration
|
||||
- output device buffer pool
|
||||
- schedule/dequeue logic
|
||||
- preroll and output queue management
|
||||
|
||||
### `PlayoutPolicy`
|
||||
|
||||
Owns:
|
||||
|
||||
- preroll target
|
||||
- spare buffer target
|
||||
- underrun behavior
|
||||
- catch-up and lateness rules
|
||||
|
||||
### `BackendTimingState`
|
||||
|
||||
Owns:
|
||||
|
||||
- frame counters
|
||||
- queue depth snapshots
|
||||
- late/dropped streaks
|
||||
- observed intervals
|
||||
|
||||
These can remain implementation details in Phase 1, but the design should leave room for them.
|
||||
|
||||
## Mapping From Current Code
|
||||
|
||||
### Current `DeckLinkSession`
|
||||
|
||||
Should mostly migrate into:
|
||||
|
||||
- `VideoBackend`
|
||||
- device discovery
|
||||
- input configuration
|
||||
- output configuration
|
||||
- keyer capability handling
|
||||
- output frame pool ownership
|
||||
- lifecycle state handling
|
||||
|
||||
Candidates to stay backend-owned:
|
||||
|
||||
- `DiscoverDevicesAndModes(...)`
|
||||
- `SelectPreferredFormats(...)`
|
||||
- `ConfigureInput(...)`
|
||||
- `ConfigureOutput(...)`
|
||||
- `Start()`
|
||||
- `Stop()`
|
||||
- `HandleVideoInputFrame(...)`
|
||||
- `HandlePlayoutFrameCompleted(...)`
|
||||
|
||||
### Current `VideoPlayoutScheduler`
|
||||
|
||||
Should likely become:
|
||||
|
||||
- a backend-owned policy helper or timing component under `VideoBackend`
|
||||
|
||||
It is still a backend concern, but it should be expanded beyond a single counter and fixed skip rule.
|
||||
|
||||
### Current `OpenGLVideoIOBridge`
|
||||
|
||||
Should split between:
|
||||
|
||||
- `RenderEngine`
|
||||
- input texture upload scheduling
|
||||
- render submission
|
||||
- readback or output-frame production
|
||||
- `VideoBackend`
|
||||
- input ingress queue
|
||||
- output callback and scheduling policy
|
||||
- pacing stats
|
||||
|
||||
The most important migration is:
|
||||
|
||||
- remove render work from `PlayoutFrameCompleted()`
|
||||
|
||||
### Previous Runtime Status Updates
|
||||
|
||||
Frame pacing and signal status setters that were historically called from the bridge should route through:
|
||||
|
||||
- `VideoBackend -> HealthTelemetry`
|
||||
|
||||
rather than the old pattern:
|
||||
|
||||
- callback/bridge -> `RuntimeHost`
|
||||
|
||||
## Migration Plan
|
||||
|
||||
The migration should avoid a flag-day rewrite.
|
||||
|
||||
### Step 1. Name the backend boundary explicitly
|
||||
|
||||
Create a conceptual `VideoBackend` interface around the existing `VideoIODevice`/`DeckLinkSession` shape without moving all logic at once.
|
||||
|
||||
### Step 2. Pull timing policy into backend-owned objects
|
||||
|
||||
Move:
|
||||
|
||||
- completion accounting
|
||||
- headroom configuration
|
||||
- frame-pool sizing
|
||||
- queue depth reporting
|
||||
|
||||
behind explicit backend policy types.
|
||||
|
||||
This can happen before changing the render thread model.
|
||||
|
||||
### Step 3. Separate callback work from render work
|
||||
|
||||
Change the output completion path so it stops rendering immediately in the callback chain.
|
||||
|
||||
Intermediate step:
|
||||
|
||||
- callback records completion and wakes a playout worker
|
||||
|
||||
Target step:
|
||||
|
||||
- callback only dequeues and schedules already-ready frames
|
||||
|
||||
### Step 4. Move input handoff to a bounded queue
|
||||
|
||||
Replace direct callback-to-GL upload behavior with:
|
||||
|
||||
- backend-owned input queue
|
||||
- render-owned dequeue/upload policy
|
||||
|
||||
### Step 5. Introduce explicit backend lifecycle states
|
||||
|
||||
Start surfacing:
|
||||
|
||||
- configured
|
||||
- prerolling
|
||||
- running
|
||||
- degraded
|
||||
- failed
|
||||
|
||||
before changing all recovery behavior.
|
||||
|
||||
### Step 6. Route backend health to `HealthTelemetry`
|
||||
|
||||
Move debug-only warnings and ad hoc status strings toward structured counters and backend snapshots.
|
||||
|
||||
## Risks
|
||||
|
||||
### Latency Versus Stability Tradeoff
|
||||
|
||||
Increasing headroom reduces deadline misses but increases end-to-end latency. The backend must make that tradeoff explicit and configurable enough for live use.
|
||||
|
||||
### Hidden Coupling During Migration
|
||||
|
||||
The current bridge still mixes backend and render concerns. Partial extraction can accidentally preserve the old coupling under new names if the callback path is not cleaned up deliberately.
|
||||
|
||||
### Buffer Ownership Ambiguity
|
||||
|
||||
If device-facing buffers and render-facing buffers are not separated clearly, lifetime bugs and timing regressions will remain easy to reintroduce.
|
||||
|
||||
### Backend-Specific Assumptions
|
||||
|
||||
The first target is still DeckLink-centric. The interface should avoid baking in assumptions that would make alternate backends awkward later.
|
||||
|
||||
### Recovery Policy Complexity
|
||||
|
||||
A more explicit backend model will surface choices that are currently hidden:
|
||||
|
||||
- stale frame reuse
|
||||
- black-frame fallback
|
||||
- adaptive headroom
|
||||
- catch-up rules
|
||||
|
||||
That is healthy, but it will require deliberate policy decisions.
|
||||
|
||||
## Open Questions
|
||||
|
||||
- Should `VideoBackend` own both input and output under one session object long-term, or should it expose distinct input and output endpoints under a shared shell?
|
||||
- Should queue ownership sit fully inside `VideoBackend`, or should there be a narrow shared frame-exchange interface between `RenderEngine` and `VideoBackend`?
|
||||
- What should the default underrun policy be for live playout: reuse last frame, reuse newest completed frame, or output black?
|
||||
- Should adaptive headroom be automatic, operator-configurable, or both?
|
||||
- At what point should preview timing be treated as a backend concern versus a render concern? The Phase 1 direction says preview is subordinate to render, not owned by the backend, but later timing work may still require explicit coordination.
|
||||
- How much of the current `VideoIOState` belongs inside `VideoBackend` versus `HealthTelemetry` snapshots?
|
||||
|
||||
## Short Version
|
||||
|
||||
`VideoBackend` should become the subsystem that owns hardware timing, device lifecycle, buffer policy, and playout recovery.
|
||||
|
||||
It should not render frames.
|
||||
|
||||
The target direction is:
|
||||
|
||||
- `RenderEngine` produces frames ahead of need
|
||||
- `VideoBackend` consumes and schedules them
|
||||
- callbacks become lightweight control-plane events
|
||||
- headroom, queue depth, and recovery become explicit backend policy
|
||||
- hardware health is reported structurally instead of being inferred from scattered logs and bridge behavior
|
||||
@@ -40,12 +40,12 @@ float4 sampleWarped(float2 uv, float2 resolution, out bool insideSource)
|
||||
insideSource = uv.x >= 0.0 && uv.x <= 1.0 && uv.y >= 0.0 && uv.y <= 1.0;
|
||||
|
||||
if (edgeMode == 1)
|
||||
return sampleVideo(clamp(uv, 0.0, 1.0));
|
||||
return sampleLayerInput(clamp(uv, 0.0, 1.0));
|
||||
if (edgeMode == 2)
|
||||
return sampleVideo(float2(mirroredCoordinate(uv.x), mirroredCoordinate(uv.y)));
|
||||
return sampleLayerInput(float2(mirroredCoordinate(uv.x), mirroredCoordinate(uv.y)));
|
||||
|
||||
float edgeMask = sourceBoundsMask(uv, resolution);
|
||||
float4 color = sampleVideo(clamp(uv, 0.0, 1.0));
|
||||
float4 color = sampleLayerInput(clamp(uv, 0.0, 1.0));
|
||||
return lerp(outsideColor, color, edgeMask);
|
||||
}
|
||||
|
||||
|
||||
451
src/README.md
Normal file
451
src/README.md
Normal file
@@ -0,0 +1,451 @@
|
||||
# RenderCadenceCompositor
|
||||
|
||||
This app is the modular version of the working DeckLink render-cadence probe.
|
||||
|
||||
Its job is to prove the production-facing foundation before the current compositor's shader/runtime/control features are ported over.
|
||||
|
||||
Before adding features here, read the guardrails in [Render Cadence Golden Rules](../../docs/RENDER_CADENCE_GOLDEN_RULES.md).
|
||||
|
||||
## Architecture
|
||||
|
||||
```text
|
||||
RenderThread
|
||||
owns a hidden OpenGL context
|
||||
polls the oldest ready input frame without waiting
|
||||
uploads input frames into a render-owned GL texture
|
||||
renders simple BGRA8 motion at selected cadence
|
||||
queues async PBO readback
|
||||
publishes completed frames into SystemFrameExchange
|
||||
|
||||
InputFrameMailbox
|
||||
owns bounded FIFO CPU input slots
|
||||
keeps a bounded three-ready-frame input buffer for render
|
||||
trims frames beyond that bound to avoid runaway input latency
|
||||
protects the one frame currently being uploaded by render
|
||||
uses a single contiguous copy when capture row stride matches mailbox row stride
|
||||
|
||||
SystemFrameExchange
|
||||
owns Free / Rendering / Completed / Scheduled slots
|
||||
preserves completed output frames as a bounded FIFO reserve once they are waiting for playout
|
||||
protects scheduled frames until DeckLink completion
|
||||
|
||||
DeckLinkOutputThread
|
||||
consumes completed system-memory frames
|
||||
schedules them into DeckLink up to target depth
|
||||
never renders
|
||||
|
||||
PreviewWindowThread
|
||||
optionally owns a Win32/GDI preview window
|
||||
copies the latest completed or scheduled system-memory frame without consuming it
|
||||
skips preview ticks instead of waiting for the frame exchange lock
|
||||
never calls GL, DeckLink, shader build, or render cadence code
|
||||
```
|
||||
|
||||
Startup builds a small output preroll reserve before DeckLink scheduled playback starts. When DeckLink input is available, startup also waits briefly for three ready input frames before the render thread starts so the first render ticks are deliberate rather than lucky.
|
||||
|
||||
## Current Scope
|
||||
|
||||
Included now:
|
||||
|
||||
- output-only DeckLink
|
||||
- optional DeckLink input edge with BGRA8 capture or raw UYVY8 capture decoded on the render thread
|
||||
- non-blocking startup when DeckLink output is unavailable
|
||||
- hidden render-thread-owned OpenGL context
|
||||
- simple smooth-motion renderer
|
||||
- BGRA8-only output
|
||||
- non-blocking three-frame FIFO input mailbox for render
|
||||
- fast contiguous mailbox copy path for matching input row strides
|
||||
- bounded three-frame input warmup before render cadence starts
|
||||
- render-thread-owned input texture upload
|
||||
- async PBO readback
|
||||
- bounded FIFO system-memory frame exchange
|
||||
- bounded completed-frame output preroll reserve before DeckLink playback, with DeckLink scheduled depth still targeted at four
|
||||
- conservative DeckLink schedule-lead telemetry and recovery
|
||||
- background Slang compile of `shaders/happy-accident`
|
||||
- app-owned display/render layer model for shader build readiness
|
||||
- app-owned submission of a completed shader artifact
|
||||
- render-thread-owned runtime render scene for ready shader layers
|
||||
- shared-context GL prepare worker for runtime shader program compile/link
|
||||
- render-thread-only GL program swap once a prepared program is ready
|
||||
- manifest-driven stateless single-pass shader packages
|
||||
- manifest-driven stateless named-pass shader packages
|
||||
- atomic render-plan swap after every pass program is prepared
|
||||
- HTTP shader list populated from supported stateless full-frame shader packages
|
||||
- default float, vec2, color, boolean, enum, and trigger parameters
|
||||
- small JSON writer for future HTTP/WebSocket payloads
|
||||
- JSON serialization for cadence telemetry snapshots
|
||||
- background logging with `log`, `warning`, and `error` levels
|
||||
- local HTTP control server matching the OpenAPI route surface
|
||||
- HTTP layer controls for add, remove, reorder, bypass, shader change, parameter update, and parameter reset
|
||||
- trigger parameters as latest-pulse controls with shader-visible count/time
|
||||
- startup config provider for `config/runtime-host.json`
|
||||
- quiet telemetry health monitor
|
||||
- optional preview window fed from completed system-memory frames on its own thread
|
||||
- non-GL frame-exchange tests
|
||||
- non-GL input-mailbox tests
|
||||
|
||||
Intentionally not included yet:
|
||||
|
||||
- additional input format conversion/scaling
|
||||
- temporal/history/feedback shader storage
|
||||
- texture/LUT asset upload
|
||||
- text-parameter rasterization
|
||||
- runtime state
|
||||
- OSC control
|
||||
- persistent control/state writes
|
||||
- trigger event history for stacked repeated pulses
|
||||
- screenshots
|
||||
- persistence
|
||||
|
||||
Those features should be ported only after the cadence spine is stable.
|
||||
|
||||
## V1 Feature Parity Checklist
|
||||
|
||||
This tracks parity with `apps/LoopThroughWithOpenGLCompositing`.
|
||||
|
||||
- [x] Stable DeckLink output cadence
|
||||
- [x] BGRA8 system-memory output path
|
||||
- [x] Render thread owns its primary GL context
|
||||
- [x] Output startup warmup before scheduled playback
|
||||
- [x] Non-blocking startup when DeckLink output is unavailable
|
||||
- [x] Runtime shader package discovery
|
||||
- [x] Background Slang shader compile
|
||||
- [x] Shared-context GL shader/program preparation
|
||||
- [x] Render-thread program swap at a frame boundary
|
||||
- [x] Stateless single-pass shader rendering
|
||||
- [x] Stateless named-pass shader rendering
|
||||
- [x] Atomic multipass render-plan commit
|
||||
- [x] Shader add/remove control path
|
||||
- [x] Previous-layer texture handoff for stacked shaders
|
||||
- [x] Supported shader list in HTTP/UI state
|
||||
- [x] Local HTTP server
|
||||
- [x] WebSocket state updates for the UI
|
||||
- [x] OpenAPI document serving
|
||||
- [x] Static control UI serving
|
||||
- [x] Startup config loading from `config/runtime-host.json`
|
||||
- [x] Cadence telemetry JSON
|
||||
- [x] Health logging for schedule/drop/starvation events
|
||||
- [x] Runtime parameter updates from HTTP controls
|
||||
- [x] Layer reorder/bypass/set-shader/update-parameter/reset-parameter HTTP controls
|
||||
- [x] Trigger parameter pulse count/time for latest trigger events
|
||||
- [x] Optional DeckLink input capture
|
||||
- [x] UYVY8 input capture with render-thread GPU decode to shader input texture
|
||||
- [x] Three-frame FIFO CPU input mailbox for render
|
||||
- [x] Fast contiguous input mailbox copy when source/destination stride matches
|
||||
- [x] Bounded three-frame input warmup before render cadence starts
|
||||
- [x] Render-owned input texture upload
|
||||
- [x] Runtime shaders receive input through `gVideoInput`
|
||||
- [x] Live DeckLink input bound to `gVideoInput`
|
||||
- [ ] Input format conversion/scaling
|
||||
- [ ] Temporal history buffers
|
||||
- [ ] Feedback buffers
|
||||
- [ ] Texture asset loading and upload
|
||||
- [ ] LUT asset loading and upload
|
||||
- [ ] Text parameter rasterization
|
||||
- [ ] Trigger history/event buffers for overlapping repeated trigger effects
|
||||
- [ ] Full runtime state store/read model
|
||||
- [ ] Persistent layer stack/config writes
|
||||
- [ ] OSC ingress
|
||||
- [x] Preview output from a non-consuming system-memory tap
|
||||
- [ ] Screenshot capture
|
||||
- [ ] External keying support
|
||||
- [ ] Full V1 health/runtime presentation model
|
||||
|
||||
## Build
|
||||
|
||||
```powershell
|
||||
cmake --build --preset build-debug --target RenderCadenceCompositor -- /m:1
|
||||
```
|
||||
|
||||
The executable is:
|
||||
|
||||
```text
|
||||
build\vs2022-x64-debug\Debug\RenderCadenceCompositor.exe
|
||||
```
|
||||
|
||||
## Run
|
||||
|
||||
Run from VS Code with:
|
||||
|
||||
```text
|
||||
Debug RenderCadenceCompositor
|
||||
```
|
||||
|
||||
Or from a terminal:
|
||||
|
||||
```powershell
|
||||
build\vs2022-x64-debug\Debug\RenderCadenceCompositor.exe
|
||||
```
|
||||
|
||||
Press Enter to stop.
|
||||
|
||||
To test a different compatible shader package:
|
||||
|
||||
```powershell
|
||||
build\vs2022-x64-debug\Debug\RenderCadenceCompositor.exe --shader solid-color
|
||||
```
|
||||
|
||||
Use `--no-shader` to keep the simple motion fallback only.
|
||||
|
||||
## Startup Config
|
||||
|
||||
On startup the app loads `config/runtime-host.json` through `AppConfigProvider`, then applies explicit CLI overrides.
|
||||
|
||||
Currently consumed fields:
|
||||
|
||||
- `serverPort`
|
||||
- `shaderLibrary`
|
||||
- `oscBindAddress`
|
||||
- `oscPort`
|
||||
- `oscSmoothing`
|
||||
- `inputVideoFormat`
|
||||
- `inputFrameRate`
|
||||
- `outputVideoFormat`
|
||||
- `outputFrameRate`
|
||||
- `autoReload`
|
||||
- `maxTemporalHistoryFrames`
|
||||
- `previewEnabled`
|
||||
- `previewFps`
|
||||
- `enableExternalKeying`
|
||||
|
||||
When `previewEnabled` is true, the preview window runs on `PreviewWindowThread`. It paints BGRA8 system-memory frames with Win32/GDI after render readback has already completed, so it does not bind GL and does not consume frames from DeckLink output. `previewFps` controls the preview repaint cadence; the default is 60 fps and `config/runtime-host.json` tracks the shipped 59.94 output cadence.
|
||||
|
||||
The loaded config is treated as a read-only startup snapshot. Subsystems that need config should receive this snapshot or a narrowed config struct from app orchestration; they should not reload files independently.
|
||||
|
||||
Supported CLI overrides:
|
||||
|
||||
- `--shader <shader-id>`
|
||||
- `--no-shader`
|
||||
- `--port <port>`
|
||||
|
||||
## Expected Telemetry
|
||||
|
||||
Startup, shutdown, shader-build, and render-thread event messages are written through the app logger. Telemetry is intentionally separate and remains a compact once-per-second cadence line.
|
||||
|
||||
The logger writes to the console, `OutputDebugStringA`, and `logs/render-cadence-compositor.log` by default. Render-thread log calls use the non-blocking path so diagnostics do not become cadence blockers.
|
||||
|
||||
## HTTP Control Server
|
||||
|
||||
The app starts a local HTTP control server on `http://127.0.0.1:8080` by default, searching nearby ports if that one is busy.
|
||||
|
||||
Current endpoints:
|
||||
|
||||
- `GET /` and UI asset paths: serve the bundled control UI from `ui/dist`
|
||||
- `GET /api/state`: returns OpenAPI-shaped display data with cadence telemetry, supported shaders, output status, and a read-only current runtime layer
|
||||
- `GET /ws`: upgrades to a WebSocket and streams state snapshots when they change
|
||||
- `GET /docs/openapi.yaml` and `GET /openapi.yaml`: serves the OpenAPI document
|
||||
- `GET /docs`: serves Swagger UI
|
||||
- `POST /api/layers/add`, `/remove`, `/reorder`, `/set-bypass`, `/set-shader`, `/update-parameter`, and `/reset-parameters` use the shared runtime control-command path
|
||||
- other OpenAPI POST routes are present but return `{ "ok": false, "error": "Endpoint is not implemented in RenderCadenceCompositor yet." }`
|
||||
|
||||
The HTTP server runs on its own thread. It serves static UI/docs files, samples/copies telemetry through callbacks, and translates POST bodies into runtime control commands. Command execution is app-owned, so future OSC ingress can create the same commands without depending on HTTP route code. Control commands may update the display layer model, start background shader builds, or publish an already-built render-layer snapshot, but they do not call render work or DeckLink scheduling directly.
|
||||
|
||||
## Optional DeckLink Output
|
||||
|
||||
DeckLink output is an optional edge service in this app.
|
||||
|
||||
Startup order is:
|
||||
|
||||
1. start render thread
|
||||
2. build a bounded completed-frame output preroll reserve at normal render cadence
|
||||
3. try to attach DeckLink output
|
||||
4. start telemetry and HTTP either way
|
||||
|
||||
If DeckLink discovery or output setup fails, the app logs a warning and continues running without starting the output scheduler or scheduled playback. This keeps render cadence, runtime shader testing, HTTP state, and logging available on machines without DeckLink hardware or drivers.
|
||||
|
||||
`/api/state` reports the output status in `videoIO.statusMessage`.
|
||||
|
||||
## Optional DeckLink Input
|
||||
|
||||
DeckLink input is an optional edge service in this app.
|
||||
|
||||
Startup order is:
|
||||
|
||||
1. create `InputFrameMailbox`
|
||||
2. try to attach DeckLink input for the configured input mode
|
||||
3. prefer BGRA8 capture, otherwise accept raw UYVY8 capture and configure the mailbox for UYVY8 bytes
|
||||
4. start `DeckLinkInputThread`
|
||||
5. wait briefly for three ready input warmup frames before starting render cadence
|
||||
6. leave input absent if discovery, setup, format support, or stream startup fails
|
||||
|
||||
`DeckLinkInput` and `DeckLinkInputThread` are deliberately narrow. They capture BGRA8 frames directly or raw UYVY8 frames into `InputFrameMailbox`; they do not call GL, render, preview, screenshot, shader, or output scheduling code. UYVY8-to-RGBA decode happens later inside the render-thread-owned input texture upload path, so the DeckLink callback stays a capture/copy edge only. The render upload path consumes the oldest ready input frame from a bounded three-ready-frame queue, so the input behaves like a small jitter buffer instead of a latest-frame preview mailbox. The mailbox trims older frames beyond that bound to avoid runaway latency, uses one contiguous copy when the capture row stride matches the configured mailbox row stride, and falls back to row-by-row copy only for padded or mismatched frames. Unsupported input modes or formats outside BGRA8/UYVY8 are reported explicitly and treated as an unavailable edge rather than silently converted.
|
||||
|
||||
Input warmup is startup-only and bounded. It may delay render-thread startup for a short window, but it does not add waits to the steady-state render cadence loop.
|
||||
|
||||
The app samples telemetry once per second.
|
||||
|
||||
Normal cadence samples are available through `GET /api/state` and are not printed to the console. The telemetry monitor only logs health events:
|
||||
|
||||
- warning when DeckLink late/dropped-frame counters increase, including schedule lead and recovery count
|
||||
- warning when schedule failures increase
|
||||
- error when the app/DeckLink output buffer is starved
|
||||
|
||||
Render cadence telemetry:
|
||||
|
||||
- `clockOverruns`: render cadence overruns where missed time was detected
|
||||
- `clockSkippedFrames`: selected-cadence frame intervals skipped instead of catch-up rendering
|
||||
- `clockOveruns` / `clockSkipped`: compatibility aliases for quick polling scripts
|
||||
|
||||
Input telemetry:
|
||||
|
||||
- `inputFramesReceived`: frames accepted into `InputFrameMailbox`
|
||||
- `inputFramesDropped`: ready input frames dropped or missed because the mailbox was full
|
||||
- `renderFrameMs`: most recent render-thread draw duration, excluding completed-readback copy and readback queue work
|
||||
- `renderFrameBudgetUsedPercent`: most recent render draw time as a percentage of the selected frame budget
|
||||
- `renderFrameMaxMs`: maximum observed render-thread draw duration for this process
|
||||
- `readbackQueueMs`: time spent queueing the most recent async BGRA8 PBO readback
|
||||
- `completedReadbackCopyMs`: time spent mapping/copying the most recent completed readback into system-memory frame storage
|
||||
- `completedDrops`: oldest completed unscheduled system-memory frames dropped because the bounded completed reserve overflowed; this is an app-side reserve drop, not a DeckLink dropped-frame report
|
||||
- `acquireMisses`: times render/readback could not acquire a writable system-memory frame slot; completed frames waiting for playout are preserved instead of being displaced
|
||||
- `deckLinkScheduleLeadAvailable`: whether DeckLink schedule-lead telemetry was available for the latest sample
|
||||
- `deckLinkScheduleLeadFrames`: estimated distance between the DeckLink playback cursor and the next scheduled stream time
|
||||
- `deckLinkPlaybackFrameIndex`: latest sampled DeckLink playback frame index
|
||||
- `deckLinkNextScheduleFrameIndex`: next stream frame index the app intends to schedule
|
||||
- `deckLinkPlaybackStreamTime`: latest sampled DeckLink playback stream time in DeckLink time units
|
||||
- `deckLinkScheduleRealignments`: conservative schedule-cursor recoveries triggered by late/drop pressure or dangerously low lead
|
||||
- `inputConsumeMisses`: render ticks where no ready input frame was available to upload
|
||||
- `inputUploadMisses`: input texture upload attempts that reused the previous GL input texture
|
||||
- `inputReadyFrames`: ready input frames currently queued in `InputFrameMailbox`
|
||||
- `inputReadingFrames`: input frames currently protected while render uploads them
|
||||
- `inputLatestAgeMs`: age of the newest submitted input frame
|
||||
- `inputUploadMs`: render-thread GL upload/decode submission time for the latest uploaded input frame
|
||||
- `inputFormatSupported`: whether the latest frame reaching the render upload path was BGRA8 or UYVY8 compatible
|
||||
- `inputSignalPresent`: whether any input frame has reached the mailbox
|
||||
- `inputCaptureFps`: DeckLink input callback capture rate
|
||||
- `inputConvertMs`: input-edge CPU conversion time; expected to remain `0` for BGRA8 and raw UYVY8 capture because UYVY8 decode is render-thread GPU work
|
||||
- `inputSubmitMs`: time spent copying/submitting the latest captured input frame to `InputFrameMailbox`
|
||||
- `inputCaptureFormat`: selected DeckLink input format (`BGRA8`, `UYVY8`, or `none`)
|
||||
- `inputNoSignalFrames`: DeckLink callbacks reporting no input source
|
||||
- `inputUnsupportedFrames`: input frames rejected before mailbox submission
|
||||
- `inputSubmitMisses`: input frames that could not be submitted to the mailbox
|
||||
|
||||
Runtime shaders continue rendering when input is missing. If no mailbox frame has been uploaded yet, shader samplers use the runtime fallback source texture; once DeckLink input is flowing, shaders such as CRT and trigger-ripple sample the current input through `gVideoInput`.
|
||||
|
||||
Healthy first-run signs:
|
||||
|
||||
- visible DeckLink output is smooth
|
||||
- `renderFps` is close to the selected cadence
|
||||
- `scheduleFps` is close to the selected cadence after warmup
|
||||
- `scheduled` stays near 4
|
||||
- `decklinkBuffered` stays near 4 when available
|
||||
- `deckLinkScheduleLeadFrames` remains positive and stable when available
|
||||
- `deckLinkScheduleRealignments` does not increase continuously
|
||||
- `late` and `dropped` do not increase continuously
|
||||
- `scheduleFailures` does not increase
|
||||
- `shaderCommitted` becomes `1` after the background Happy Accident compile completes
|
||||
- `shaderFailures` remains `0`
|
||||
|
||||
`completedPollMisses` means the DeckLink scheduling thread woke up before a completed frame was available. It is not a DeckLink playout underrun by itself. Treat it as healthy polling noise when `scheduled`, `decklinkBuffered`, `late`, `dropped`, and `scheduleFailures` remain stable.
|
||||
|
||||
## Runtime Slang Shader Test
|
||||
|
||||
On startup the app begins compiling the selected shader package on a background thread owned by the app orchestration layer. The default is `shaders/happy-accident`.
|
||||
|
||||
The render thread keeps drawing the simple motion renderer while Slang compiles. It does not choose packages, launch Slang, or track build lifecycle. Once a completed shader artifact is published, the render-thread-owned runtime scene queues changed layers to a shared-context GL prepare worker. That worker compiles/links runtime shader programs off the cadence thread. The render thread only swaps in an already-prepared GL program at a frame boundary. If either the Slang build or GL preparation fails, the app keeps rendering the current renderer or simple motion fallback.
|
||||
|
||||
Current runtime shader support is deliberately limited to stateless full-frame packages:
|
||||
|
||||
- one or more named passes
|
||||
- one sampled source input per pass
|
||||
- named intermediate outputs routed by the pass manifest
|
||||
- final visible output must be named `layerOutput`
|
||||
- no temporal history
|
||||
- no feedback storage
|
||||
- no texture/LUT assets yet
|
||||
- no text parameters yet
|
||||
- manifest defaults initialize parameters
|
||||
- HTTP controls can update runtime parameter values without rebuilding GL programs when the shader program is unchanged
|
||||
- trigger parameters are treated as latest-pulse controls: each press increments the trigger count and records the current runtime time
|
||||
- repeated trigger history is not stored yet, so effects such as `trigger-ripple` restart from the latest trigger rather than accumulating overlapping ripples
|
||||
- the first layer receives a small fallback source texture until DeckLink input is available
|
||||
- the first layer receives the latest DeckLink input texture through both `gVideoInput` and `gLayerInput` when input frames are available
|
||||
- stacked layers receive the original input through `gVideoInput` and the previous ready layer output through `gLayerInput`
|
||||
|
||||
Shader source semantics:
|
||||
|
||||
- `gVideoInput` means the latest decoded shader-visible video input for every layer.
|
||||
- `gLayerInput` means the previous layer output.
|
||||
- the first layer may receive `gLayerInput = gVideoInput`.
|
||||
- later layers receive `gVideoInput = original input` and `gLayerInput = previous layer`.
|
||||
- named intermediate pass inputs inside a multipass layer are still routed through the selected pass-source slot; layer stacking should use `gLayerInput`.
|
||||
|
||||
The `/api/state` shader list uses the same support rules as runtime shader compilation and reports only packages this app can run today. Unsupported manifest feature sets such as temporal, feedback, texture-backed, font-backed, or text-parameter shaders are hidden from the control UI for now.
|
||||
|
||||
Runtime shaders are exposed through `RuntimeLayerModel` as display layers with manifest parameter definitions, current parameter values, build status, and render-ready artifacts. POST controls mutate this app-owned model and may start background shader builds when the selected shader changes.
|
||||
|
||||
When a layer becomes render-ready, the app publishes the ready render-layer snapshot to the render thread. The render thread owns the GL-side `RuntimeRenderScene`, diffs that snapshot at a frame boundary, queues new or changed pass programs to the shared-context prepare worker, swaps in a full prepared render plan only after every pass is ready, removes obsolete GL programs, and renders ready layers in order. Stacked stateless full-frame shaders render through internal ping-pong targets so each layer can sample the previous layer through `gLayerInput`; multipass shaders route named intermediate outputs through their manifest-declared pass inputs, and the final ready layer renders to the output target.
|
||||
|
||||
Successful handoff signs:
|
||||
|
||||
- telemetry shows `shaderCommitted=1`
|
||||
- output changes from the simple motion pattern to the Happy Accident shader
|
||||
- render/schedule cadence remains near 60 fps during and after the handoff
|
||||
- DeckLink buffer remains stable
|
||||
|
||||
## Baseline Result
|
||||
|
||||
Date: 2026-05-12
|
||||
|
||||
User-visible result:
|
||||
|
||||
- output was smooth
|
||||
- DeckLink held a 4-frame buffer
|
||||
|
||||
Representative telemetry:
|
||||
|
||||
```text
|
||||
renderFps=59.9 scheduleFps=59.9 free=8 completed=0 scheduled=4 completedPollMisses=30 scheduleFailures=0 completions=720 late=0 dropped=0 decklinkBuffered=4 scheduleCallMs=1.2
|
||||
renderFps=59.8 scheduleFps=59.8 free=7 completed=1 scheduled=4 completedPollMisses=36 scheduleFailures=0 completions=1080 late=0 dropped=0 decklinkBuffered=4 scheduleCallMs=4.7
|
||||
renderFps=59.9 scheduleFps=59.9 free=7 completed=1 scheduled=4 completedPollMisses=86 scheduleFailures=0 completions=1381 late=0 dropped=0 decklinkBuffered=4 scheduleCallMs=2.1
|
||||
```
|
||||
|
||||
Read:
|
||||
|
||||
- render cadence and DeckLink schedule cadence both held roughly 60 fps
|
||||
- app scheduled depth stayed at 4
|
||||
- actual DeckLink buffered depth stayed at 4
|
||||
- DeckLink schedule lead remained positive during healthy playback
|
||||
- no late frames, dropped frames, or schedule failures were observed
|
||||
- completed poll misses were benign because playout remained fully fed
|
||||
|
||||
## Tests
|
||||
|
||||
```powershell
|
||||
cmake --build --preset build-debug --target RenderCadenceCompositorFrameExchangeTests -- /m:1
|
||||
ctest --test-dir build\vs2022-x64-debug -C Debug -R RenderCadenceCompositorFrameExchangeTests --output-on-failure
|
||||
```
|
||||
|
||||
## Relationship To The Probe
|
||||
|
||||
`apps/DeckLinkRenderCadenceProbe` proved the timing model in one compact file.
|
||||
|
||||
This app keeps the same core behavior but splits it into modules that can grow:
|
||||
|
||||
- `frames/`: system-memory handoff
|
||||
- `platform/`: COM/Win32/hidden GL context support
|
||||
- `render/`: cadence thread, clock, and simple renderer
|
||||
- `frames/InputFrameMailbox`: non-blocking bounded FIFO CPU input handoff with contiguous-copy fast path for matching row strides
|
||||
- `render/InputFrameTexture`: render-thread-owned upload of the currently acquired CPU input frame into GL, including raw UYVY8 decode into the shader-visible input texture
|
||||
- `render/readback/`: PBO-backed BGRA8 readback and completed-frame publication
|
||||
- `render/runtime/RuntimeRenderScene`: render-thread-owned GL scene for ready runtime shader layers
|
||||
- `render/runtime/RuntimeShaderPrepareWorker`: shared-context runtime shader program compile/link worker
|
||||
- `runtime/`: app-owned shader layer readiness model, runtime Slang build bridge, and completed artifact handoff
|
||||
- `control/`: control action results and runtime-state JSON presentation
|
||||
- `control/http/`: local HTTP API, static UI serving, OpenAPI serving, and WebSocket updates
|
||||
- `json/`: compact JSON serialization helpers
|
||||
- `video/`: DeckLink output wrapper and scheduling thread
|
||||
- `telemetry/`: cadence telemetry
|
||||
- `telemetry/TelemetryHealthMonitor`: quiet health event logging from telemetry samples
|
||||
- `app/`: startup/shutdown orchestration
|
||||
- `app/AppConfigProvider`: startup config loading and CLI overrides
|
||||
|
||||
## Next Porting Steps
|
||||
|
||||
Only after this app matches the probe's smooth output:
|
||||
|
||||
1. replace `SimpleMotionRenderer` with a render-scene interface
|
||||
2. port shader package rendering
|
||||
3. port runtime snapshots/live state
|
||||
4. add control services
|
||||
5. add preview/screenshot from system-memory frames
|
||||
6. add scaling and additional input format support after the BGRA8/raw-UYVY8 input edge is stable
|
||||
228
src/RenderCadenceCompositor.cpp
Normal file
228
src/RenderCadenceCompositor.cpp
Normal file
@@ -0,0 +1,228 @@
|
||||
#include "app/AppConfig.h"
|
||||
#include "app/AppConfigProvider.h"
|
||||
#include "app/RenderCadenceApp.h"
|
||||
#include "frames/InputFrameMailbox.h"
|
||||
#include "frames/SystemFrameExchange.h"
|
||||
#include "logging/Logger.h"
|
||||
#include "render/RenderThread.h"
|
||||
#include "video/DeckLinkInput.h"
|
||||
#include "video/DeckLinkInputThread.h"
|
||||
#include "DeckLinkDisplayMode.h"
|
||||
#include "VideoIOFormat.h"
|
||||
|
||||
#include <windows.h>
|
||||
|
||||
#include <chrono>
|
||||
#include <iostream>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
|
||||
namespace
|
||||
{
|
||||
constexpr std::size_t kDeckLinkTargetBufferedFrames = 4;
|
||||
constexpr std::size_t kReadbackDepth = 6;
|
||||
constexpr std::size_t kWritableOutputReserveFrames = kReadbackDepth + 2;
|
||||
|
||||
class ComInitGuard
|
||||
{
|
||||
public:
|
||||
~ComInitGuard()
|
||||
{
|
||||
if (mInitialized)
|
||||
CoUninitialize();
|
||||
}
|
||||
|
||||
bool Initialize()
|
||||
{
|
||||
const HRESULT result = CoInitialize(nullptr);
|
||||
mInitialized = SUCCEEDED(result);
|
||||
mResult = result;
|
||||
return mInitialized;
|
||||
}
|
||||
|
||||
HRESULT Result() const { return mResult; }
|
||||
|
||||
private:
|
||||
bool mInitialized = false;
|
||||
HRESULT mResult = S_OK;
|
||||
};
|
||||
|
||||
bool WaitForInputWarmup(InputFrameMailbox& mailbox, std::size_t targetReadyFrames, std::chrono::milliseconds timeout)
|
||||
{
|
||||
const auto start = std::chrono::steady_clock::now();
|
||||
while (std::chrono::steady_clock::now() - start < timeout)
|
||||
{
|
||||
const InputFrameMailboxMetrics metrics = mailbox.Metrics();
|
||||
if (metrics.readyCount >= targetReadyFrames)
|
||||
return true;
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(2));
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
int main(int argc, char** argv)
|
||||
{
|
||||
RenderCadenceCompositor::AppConfigProvider configProvider;
|
||||
std::string configError;
|
||||
if (!configProvider.LoadDefault(configError))
|
||||
{
|
||||
RenderCadenceCompositor::Logger::Instance().Start(RenderCadenceCompositor::DefaultAppConfig().logging);
|
||||
RenderCadenceCompositor::LogError("app", "Config load failed: " + configError);
|
||||
RenderCadenceCompositor::Logger::Instance().Stop();
|
||||
return 1;
|
||||
}
|
||||
configProvider.ApplyCommandLine(argc, argv);
|
||||
|
||||
RenderCadenceCompositor::AppConfig appConfig = configProvider.Config();
|
||||
RenderCadenceCompositor::Logger::Instance().Start(appConfig.logging);
|
||||
RenderCadenceCompositor::Log(
|
||||
"app",
|
||||
"RenderCadenceCompositor starting. Starts render cadence, system-memory exchange, DeckLink scheduled output, and telemetry. Press Enter to stop.");
|
||||
RenderCadenceCompositor::Log("app", "Loaded config from " + configProvider.SourcePath().string());
|
||||
|
||||
ComInitGuard com;
|
||||
if (!com.Initialize())
|
||||
{
|
||||
std::ostringstream message;
|
||||
message << "COM initialization failed: 0x" << std::hex << com.Result();
|
||||
RenderCadenceCompositor::LogError("app", message.str());
|
||||
RenderCadenceCompositor::Logger::Instance().Stop();
|
||||
return 1;
|
||||
}
|
||||
|
||||
SystemFrameExchangeConfig frameExchangeConfig;
|
||||
RenderCadenceCompositor::VideoFormatDimensions(
|
||||
appConfig.outputVideoFormat,
|
||||
frameExchangeConfig.width,
|
||||
frameExchangeConfig.height);
|
||||
frameExchangeConfig.pixelFormat = VideoIOPixelFormat::Bgra8;
|
||||
frameExchangeConfig.rowBytes = VideoIORowBytes(frameExchangeConfig.pixelFormat, frameExchangeConfig.width);
|
||||
frameExchangeConfig.capacity =
|
||||
appConfig.warmupCompletedFrames +
|
||||
kDeckLinkTargetBufferedFrames +
|
||||
kWritableOutputReserveFrames;
|
||||
frameExchangeConfig.maxCompletedFrames = appConfig.warmupCompletedFrames;
|
||||
|
||||
SystemFrameExchange frameExchange(frameExchangeConfig);
|
||||
|
||||
InputFrameMailboxConfig inputMailboxConfig;
|
||||
RenderCadenceCompositor::VideoFormatDimensions(
|
||||
appConfig.inputVideoFormat,
|
||||
inputMailboxConfig.width,
|
||||
inputMailboxConfig.height);
|
||||
inputMailboxConfig.pixelFormat = VideoIOPixelFormat::Bgra8;
|
||||
inputMailboxConfig.rowBytes = VideoIORowBytes(inputMailboxConfig.pixelFormat, inputMailboxConfig.width);
|
||||
inputMailboxConfig.capacity = 4;
|
||||
inputMailboxConfig.maxReadyFrames = 3;
|
||||
InputFrameMailbox inputMailbox(inputMailboxConfig);
|
||||
|
||||
VideoFormat inputVideoMode;
|
||||
VideoFormat outputVideoMode;
|
||||
std::string inputVideoModeError;
|
||||
const bool inputVideoModeResolved = ResolveConfiguredVideoFormat(appConfig.inputVideoFormat, appConfig.inputFrameRate, inputVideoMode);
|
||||
const bool outputVideoModeResolved = ResolveConfiguredVideoFormat(appConfig.outputVideoFormat, appConfig.outputFrameRate, outputVideoMode);
|
||||
if (!inputVideoModeResolved)
|
||||
{
|
||||
inputVideoModeError = "Unsupported DeckLink inputVideoFormat/inputFrameRate in config/runtime-host.json: " +
|
||||
appConfig.inputVideoFormat + " / " + appConfig.inputFrameRate;
|
||||
RenderCadenceCompositor::LogWarning("app", inputVideoModeError);
|
||||
}
|
||||
if (!outputVideoModeResolved)
|
||||
{
|
||||
RenderCadenceCompositor::LogWarning(
|
||||
"app",
|
||||
"Unsupported DeckLink outputVideoFormat/outputFrameRate in config/runtime-host.json; render cadence will use parsed frame-rate fallback: " +
|
||||
appConfig.outputVideoFormat + " / " + appConfig.outputFrameRate);
|
||||
}
|
||||
else
|
||||
{
|
||||
appConfig.deckLink.outputVideoMode = outputVideoMode;
|
||||
}
|
||||
|
||||
RenderCadenceCompositor::DeckLinkInput deckLinkInput(inputMailbox);
|
||||
RenderCadenceCompositor::DeckLinkInputThread deckLinkInputThread(deckLinkInput);
|
||||
bool deckLinkInputStarted = false;
|
||||
if (inputVideoModeResolved)
|
||||
{
|
||||
RenderCadenceCompositor::DeckLinkInputConfig deckLinkInputConfig;
|
||||
deckLinkInputConfig.videoFormat = inputVideoMode;
|
||||
std::string deckLinkInputError;
|
||||
if (deckLinkInput.Initialize(deckLinkInputConfig, deckLinkInputError))
|
||||
{
|
||||
inputMailboxConfig.pixelFormat = deckLinkInput.CapturePixelFormat();
|
||||
inputMailboxConfig.rowBytes = VideoIORowBytes(inputMailboxConfig.pixelFormat, inputMailboxConfig.width);
|
||||
inputMailbox.Configure(inputMailboxConfig);
|
||||
}
|
||||
|
||||
if (deckLinkInput.IsInitialized() && deckLinkInputThread.Start(deckLinkInputError))
|
||||
{
|
||||
deckLinkInputStarted = true;
|
||||
RenderCadenceCompositor::Log("app", "DeckLink input edge started for " + inputVideoMode.displayName + ".");
|
||||
RenderCadenceCompositor::Log("app", "Waiting for DeckLink input warmup frames.");
|
||||
constexpr std::size_t kInputStartupBufferedFrames = 3;
|
||||
constexpr std::chrono::milliseconds kInputWarmupTimeout(1000);
|
||||
if (WaitForInputWarmup(inputMailbox, kInputStartupBufferedFrames, kInputWarmupTimeout))
|
||||
{
|
||||
const InputFrameMailboxMetrics metrics = inputMailbox.Metrics();
|
||||
RenderCadenceCompositor::Log(
|
||||
"app",
|
||||
"DeckLink input warmup complete. ready=" + std::to_string(metrics.readyCount) +
|
||||
" submitted=" + std::to_string(metrics.submittedFrames) + ".");
|
||||
}
|
||||
else
|
||||
{
|
||||
const InputFrameMailboxMetrics metrics = inputMailbox.Metrics();
|
||||
RenderCadenceCompositor::LogWarning(
|
||||
"app",
|
||||
"DeckLink input warmup timed out; starting render cadence with current input buffer. ready=" +
|
||||
std::to_string(metrics.readyCount) + " submitted=" + std::to_string(metrics.submittedFrames) + ".");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
RenderCadenceCompositor::LogWarning("app", "DeckLink input edge unavailable; runtime shaders will use fallback input until a real input edge is available. " + deckLinkInputError);
|
||||
deckLinkInput.ReleaseResources();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
RenderCadenceCompositor::LogWarning("app", "DeckLink input mode was not resolved; runtime shaders will use fallback input until a real input edge is available.");
|
||||
}
|
||||
|
||||
RenderThread::Config renderConfig;
|
||||
renderConfig.width = frameExchangeConfig.width;
|
||||
renderConfig.height = frameExchangeConfig.height;
|
||||
const double fallbackFrameDurationMilliseconds = RenderCadenceCompositor::FrameDurationMillisecondsFromRateString(appConfig.outputFrameRate);
|
||||
renderConfig.frameDurationMilliseconds = outputVideoModeResolved
|
||||
? RenderCadenceCompositor::FrameDurationMillisecondsFromDisplayMode(outputVideoMode.displayMode, fallbackFrameDurationMilliseconds)
|
||||
: fallbackFrameDurationMilliseconds;
|
||||
renderConfig.pboDepth = kReadbackDepth;
|
||||
|
||||
RenderThread renderThread(frameExchange, &inputMailbox, renderConfig);
|
||||
|
||||
RenderCadenceCompositor::RenderCadenceApp<RenderThread, SystemFrameExchange> app(renderThread, frameExchange, appConfig);
|
||||
app.SetDeckLinkInputMetricsProvider([&deckLinkInput]() {
|
||||
return deckLinkInput.Metrics();
|
||||
});
|
||||
|
||||
std::string error;
|
||||
if (!app.Start(error))
|
||||
{
|
||||
RenderCadenceCompositor::LogError("app", "RenderCadenceCompositor start failed: " + error);
|
||||
if (deckLinkInputStarted)
|
||||
deckLinkInputThread.Stop();
|
||||
RenderCadenceCompositor::Logger::Instance().Stop();
|
||||
return 1;
|
||||
}
|
||||
|
||||
std::string line;
|
||||
std::getline(std::cin, line);
|
||||
app.Stop();
|
||||
if (deckLinkInputStarted)
|
||||
deckLinkInputThread.Stop();
|
||||
RenderCadenceCompositor::Log("app", "RenderCadenceCompositor stopped.");
|
||||
RenderCadenceCompositor::Logger::Instance().Stop();
|
||||
return 0;
|
||||
}
|
||||
40
src/app/AppConfig.cpp
Normal file
40
src/app/AppConfig.cpp
Normal file
@@ -0,0 +1,40 @@
|
||||
#include "AppConfig.h"
|
||||
|
||||
namespace RenderCadenceCompositor
|
||||
{
|
||||
AppConfig DefaultAppConfig()
|
||||
{
|
||||
AppConfig config;
|
||||
config.deckLink.externalKeyingEnabled = false;
|
||||
config.deckLink.outputAlphaRequired = false;
|
||||
config.outputThread.targetBufferedFrames = 4;
|
||||
config.telemetry.interval = std::chrono::seconds(1);
|
||||
config.logging.minimumLevel = LogLevel::Log;
|
||||
config.logging.writeToConsole = true;
|
||||
config.logging.writeToDebugOutput = true;
|
||||
config.logging.writeToFile = true;
|
||||
config.logging.filePath = "logs/render-cadence-compositor.log";
|
||||
config.logging.maxQueuedMessages = 1024;
|
||||
config.http.preferredPort = 8080;
|
||||
config.http.portSearchCount = 20;
|
||||
config.http.idleSleep = std::chrono::milliseconds(10);
|
||||
config.shaderLibrary = "shaders";
|
||||
config.oscBindAddress = "0.0.0.0";
|
||||
config.oscPort = 9000;
|
||||
config.oscSmoothing = 0.18;
|
||||
config.inputVideoFormat = "1080p";
|
||||
config.inputFrameRate = "59.94";
|
||||
config.outputVideoFormat = "1080p";
|
||||
config.outputFrameRate = "59.94";
|
||||
config.autoReload = true;
|
||||
config.maxTemporalHistoryFrames = 12;
|
||||
config.previewEnabled = false;
|
||||
config.previewFps = kDefaultPreviewFps;
|
||||
config.warmupCompletedFrames = 4;
|
||||
config.warmupTimeout = std::chrono::seconds(3);
|
||||
config.prerollTimeout = std::chrono::seconds(3);
|
||||
config.prerollPoll = std::chrono::milliseconds(2);
|
||||
config.runtimeShaderId = "happy-accident";
|
||||
return config;
|
||||
}
|
||||
}
|
||||
43
src/app/AppConfig.h
Normal file
43
src/app/AppConfig.h
Normal file
@@ -0,0 +1,43 @@
|
||||
#pragma once
|
||||
|
||||
#include "../control/http/HttpControlServer.h"
|
||||
#include "../logging/Logger.h"
|
||||
#include "../preview/PreviewConfig.h"
|
||||
#include "../telemetry/TelemetryHealthMonitor.h"
|
||||
#include "../video/DeckLinkOutput.h"
|
||||
#include "../video/DeckLinkOutputThread.h"
|
||||
|
||||
#include <chrono>
|
||||
#include <cstddef>
|
||||
#include <string>
|
||||
|
||||
namespace RenderCadenceCompositor
|
||||
{
|
||||
struct AppConfig
|
||||
{
|
||||
DeckLinkOutputConfig deckLink;
|
||||
DeckLinkOutputThreadConfig outputThread;
|
||||
TelemetryHealthMonitorConfig telemetry;
|
||||
LoggerConfig logging;
|
||||
HttpControlServerConfig http;
|
||||
std::string shaderLibrary = "shaders";
|
||||
std::string oscBindAddress = "0.0.0.0";
|
||||
unsigned short oscPort = 9000;
|
||||
double oscSmoothing = 0.18;
|
||||
std::string inputVideoFormat = "1080p";
|
||||
std::string inputFrameRate = "59.94";
|
||||
std::string outputVideoFormat = "1080p";
|
||||
std::string outputFrameRate = "59.94";
|
||||
bool autoReload = true;
|
||||
std::size_t maxTemporalHistoryFrames = 12;
|
||||
bool previewEnabled = false;
|
||||
double previewFps = kDefaultPreviewFps;
|
||||
std::size_t warmupCompletedFrames = 4;
|
||||
std::chrono::milliseconds warmupTimeout = std::chrono::seconds(3);
|
||||
std::chrono::milliseconds prerollTimeout = std::chrono::seconds(3);
|
||||
std::chrono::milliseconds prerollPoll = std::chrono::milliseconds(2);
|
||||
std::string runtimeShaderId = "happy-accident";
|
||||
};
|
||||
|
||||
AppConfig DefaultAppConfig();
|
||||
}
|
||||
283
src/app/AppConfigProvider.cpp
Normal file
283
src/app/AppConfigProvider.cpp
Normal file
@@ -0,0 +1,283 @@
|
||||
#include "AppConfigProvider.h"
|
||||
|
||||
#include "RuntimeJson.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
#include <cstdint>
|
||||
#include <cstdlib>
|
||||
#include <fstream>
|
||||
#include <sstream>
|
||||
#include <vector>
|
||||
#include <windows.h>
|
||||
|
||||
namespace RenderCadenceCompositor
|
||||
{
|
||||
namespace
|
||||
{
|
||||
std::filesystem::path ExecutableDirectory()
|
||||
{
|
||||
char path[MAX_PATH] = {};
|
||||
const DWORD length = GetModuleFileNameA(nullptr, path, static_cast<DWORD>(sizeof(path)));
|
||||
if (length == 0 || length >= sizeof(path))
|
||||
return std::filesystem::current_path();
|
||||
return std::filesystem::path(path).parent_path();
|
||||
}
|
||||
|
||||
std::string ReadTextFile(const std::filesystem::path& path, std::string& error)
|
||||
{
|
||||
std::ifstream input(path, std::ios::binary);
|
||||
if (!input)
|
||||
{
|
||||
error = "Could not open config file: " + path.string();
|
||||
return std::string();
|
||||
}
|
||||
|
||||
std::ostringstream buffer;
|
||||
buffer << input.rdbuf();
|
||||
return buffer.str();
|
||||
}
|
||||
|
||||
const JsonValue* Find(const JsonValue& root, const char* key)
|
||||
{
|
||||
return root.find(key);
|
||||
}
|
||||
|
||||
void ApplyString(const JsonValue& root, const char* key, std::string& target)
|
||||
{
|
||||
const JsonValue* value = Find(root, key);
|
||||
if (value && value->isString())
|
||||
target = value->asString();
|
||||
}
|
||||
|
||||
void ApplyBool(const JsonValue& root, const char* key, bool& target)
|
||||
{
|
||||
const JsonValue* value = Find(root, key);
|
||||
if (value && value->isBoolean())
|
||||
target = value->asBoolean();
|
||||
}
|
||||
|
||||
void ApplyDouble(const JsonValue& root, const char* key, double& target)
|
||||
{
|
||||
const JsonValue* value = Find(root, key);
|
||||
if (value && value->isNumber())
|
||||
target = value->asNumber();
|
||||
}
|
||||
|
||||
void ApplySize(const JsonValue& root, const char* key, std::size_t& target)
|
||||
{
|
||||
const JsonValue* value = Find(root, key);
|
||||
if (value && value->isNumber() && value->asNumber() >= 0.0)
|
||||
target = static_cast<std::size_t>(value->asNumber());
|
||||
}
|
||||
|
||||
void ApplyPort(const JsonValue& root, const char* key, unsigned short& target)
|
||||
{
|
||||
const JsonValue* value = Find(root, key);
|
||||
if (!value || !value->isNumber())
|
||||
return;
|
||||
|
||||
const double port = value->asNumber();
|
||||
if (port >= 1.0 && port <= 65535.0)
|
||||
target = static_cast<unsigned short>(port);
|
||||
}
|
||||
}
|
||||
|
||||
AppConfigProvider::AppConfigProvider() :
|
||||
mConfig(DefaultAppConfig())
|
||||
{
|
||||
}
|
||||
|
||||
bool AppConfigProvider::LoadDefault(std::string& error)
|
||||
{
|
||||
const std::filesystem::path path = FindConfigFile();
|
||||
if (path.empty())
|
||||
{
|
||||
error = "Could not locate config/runtime-host.json from current directory or executable directory.";
|
||||
return false;
|
||||
}
|
||||
return Load(path, error);
|
||||
}
|
||||
|
||||
bool AppConfigProvider::Load(const std::filesystem::path& path, std::string& error)
|
||||
{
|
||||
mConfig = DefaultAppConfig();
|
||||
mSourcePath = path;
|
||||
mLoadedFromFile = false;
|
||||
|
||||
std::string fileError;
|
||||
const std::string text = ReadTextFile(path, fileError);
|
||||
if (!fileError.empty())
|
||||
{
|
||||
error = fileError;
|
||||
return false;
|
||||
}
|
||||
|
||||
JsonValue root;
|
||||
std::string parseError;
|
||||
if (!ParseJson(text, root, parseError) || !root.isObject())
|
||||
{
|
||||
error = parseError.empty() ? "Config root must be a JSON object." : parseError;
|
||||
return false;
|
||||
}
|
||||
|
||||
ApplyString(root, "shaderLibrary", mConfig.shaderLibrary);
|
||||
ApplyPort(root, "serverPort", mConfig.http.preferredPort);
|
||||
ApplyString(root, "oscBindAddress", mConfig.oscBindAddress);
|
||||
ApplyPort(root, "oscPort", mConfig.oscPort);
|
||||
ApplyDouble(root, "oscSmoothing", mConfig.oscSmoothing);
|
||||
ApplyString(root, "inputVideoFormat", mConfig.inputVideoFormat);
|
||||
ApplyString(root, "inputFrameRate", mConfig.inputFrameRate);
|
||||
ApplyString(root, "outputVideoFormat", mConfig.outputVideoFormat);
|
||||
ApplyString(root, "outputFrameRate", mConfig.outputFrameRate);
|
||||
ApplyBool(root, "autoReload", mConfig.autoReload);
|
||||
ApplySize(root, "maxTemporalHistoryFrames", mConfig.maxTemporalHistoryFrames);
|
||||
ApplyBool(root, "previewEnabled", mConfig.previewEnabled);
|
||||
ApplyDouble(root, "previewFps", mConfig.previewFps);
|
||||
ApplyBool(root, "enableExternalKeying", mConfig.deckLink.externalKeyingEnabled);
|
||||
|
||||
mLoadedFromFile = true;
|
||||
error.clear();
|
||||
return true;
|
||||
}
|
||||
|
||||
void AppConfigProvider::ApplyCommandLine(int argc, char** argv)
|
||||
{
|
||||
for (int index = 1; index < argc; ++index)
|
||||
{
|
||||
const std::string argument = argv[index];
|
||||
if (argument == "--shader" && index + 1 < argc)
|
||||
{
|
||||
mConfig.runtimeShaderId = argv[++index];
|
||||
continue;
|
||||
}
|
||||
if (argument == "--no-shader")
|
||||
{
|
||||
mConfig.runtimeShaderId.clear();
|
||||
continue;
|
||||
}
|
||||
if (argument == "--port" && index + 1 < argc)
|
||||
{
|
||||
const int port = std::atoi(argv[++index]);
|
||||
if (port >= 1 && port <= 65535)
|
||||
mConfig.http.preferredPort = static_cast<unsigned short>(port);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
double FrameDurationMillisecondsFromRateString(const std::string& rateText, double fallbackRate)
|
||||
{
|
||||
double rate = fallbackRate;
|
||||
try
|
||||
{
|
||||
rate = std::stod(rateText);
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
rate = fallbackRate;
|
||||
}
|
||||
|
||||
if (rate <= 0.0)
|
||||
rate = fallbackRate;
|
||||
return 1000.0 / rate;
|
||||
}
|
||||
|
||||
double FrameDurationMillisecondsFromDisplayMode(BMDDisplayMode displayMode, double fallbackMilliseconds)
|
||||
{
|
||||
struct ModeRate
|
||||
{
|
||||
BMDDisplayMode mode;
|
||||
int64_t frameDuration;
|
||||
int64_t timeScale;
|
||||
};
|
||||
|
||||
static const ModeRate rates[] =
|
||||
{
|
||||
{ bmdModeHD720p50, 1, 50 },
|
||||
{ bmdModeHD720p5994, 1001, 60000 },
|
||||
{ bmdModeHD720p60, 1, 60 },
|
||||
{ bmdModeHD1080i50, 1, 25 },
|
||||
{ bmdModeHD1080i5994, 1001, 30000 },
|
||||
{ bmdModeHD1080i6000, 1, 30 },
|
||||
{ bmdModeHD1080p2398, 1001, 24000 },
|
||||
{ bmdModeHD1080p24, 1, 24 },
|
||||
{ bmdModeHD1080p25, 1, 25 },
|
||||
{ bmdModeHD1080p2997, 1001, 30000 },
|
||||
{ bmdModeHD1080p30, 1, 30 },
|
||||
{ bmdModeHD1080p50, 1, 50 },
|
||||
{ bmdModeHD1080p5994, 1001, 60000 },
|
||||
{ bmdModeHD1080p6000, 1, 60 },
|
||||
{ bmdMode4K2160p2398, 1001, 24000 },
|
||||
{ bmdMode4K2160p24, 1, 24 },
|
||||
{ bmdMode4K2160p25, 1, 25 },
|
||||
{ bmdMode4K2160p2997, 1001, 30000 },
|
||||
{ bmdMode4K2160p30, 1, 30 },
|
||||
{ bmdMode4K2160p50, 1, 50 },
|
||||
{ bmdMode4K2160p5994, 1001, 60000 },
|
||||
{ bmdMode4K2160p60, 1, 60 }
|
||||
};
|
||||
|
||||
for (const ModeRate& rate : rates)
|
||||
{
|
||||
if (rate.mode == displayMode && rate.timeScale > 0)
|
||||
return (static_cast<double>(rate.frameDuration) * 1000.0) / static_cast<double>(rate.timeScale);
|
||||
}
|
||||
|
||||
return fallbackMilliseconds;
|
||||
}
|
||||
|
||||
void VideoFormatDimensions(const std::string& formatName, unsigned& width, unsigned& height)
|
||||
{
|
||||
std::string normalized = formatName;
|
||||
std::transform(normalized.begin(), normalized.end(), normalized.begin(), [](unsigned char character) {
|
||||
return static_cast<char>(std::tolower(character));
|
||||
});
|
||||
|
||||
if (normalized == "720p")
|
||||
{
|
||||
width = 1280;
|
||||
height = 720;
|
||||
return;
|
||||
}
|
||||
|
||||
if (normalized == "2160p" || normalized == "4k" || normalized == "uhd")
|
||||
{
|
||||
width = 3840;
|
||||
height = 2160;
|
||||
return;
|
||||
}
|
||||
|
||||
width = 1920;
|
||||
height = 1080;
|
||||
}
|
||||
|
||||
std::filesystem::path FindConfigFile(const std::filesystem::path& relativePath)
|
||||
{
|
||||
return FindRepoPath(relativePath);
|
||||
}
|
||||
|
||||
std::filesystem::path FindRepoPath(const std::filesystem::path& relativePath)
|
||||
{
|
||||
std::vector<std::filesystem::path> starts;
|
||||
starts.push_back(std::filesystem::current_path());
|
||||
starts.push_back(ExecutableDirectory());
|
||||
|
||||
for (std::filesystem::path start : starts)
|
||||
{
|
||||
for (;;)
|
||||
{
|
||||
const std::filesystem::path candidate = start / relativePath;
|
||||
if (std::filesystem::exists(candidate))
|
||||
return candidate;
|
||||
|
||||
const std::filesystem::path parent = start.parent_path();
|
||||
if (parent.empty() || parent == start)
|
||||
break;
|
||||
start = parent;
|
||||
}
|
||||
}
|
||||
|
||||
return std::filesystem::path();
|
||||
}
|
||||
}
|
||||
35
src/app/AppConfigProvider.h
Normal file
35
src/app/AppConfigProvider.h
Normal file
@@ -0,0 +1,35 @@
|
||||
#pragma once
|
||||
|
||||
#include "AppConfig.h"
|
||||
#include "DeckLinkDisplayMode.h"
|
||||
|
||||
#include <filesystem>
|
||||
#include <string>
|
||||
|
||||
namespace RenderCadenceCompositor
|
||||
{
|
||||
class AppConfigProvider
|
||||
{
|
||||
public:
|
||||
AppConfigProvider();
|
||||
|
||||
bool Load(const std::filesystem::path& path, std::string& error);
|
||||
bool LoadDefault(std::string& error);
|
||||
void ApplyCommandLine(int argc, char** argv);
|
||||
|
||||
const AppConfig& Config() const { return mConfig; }
|
||||
const std::filesystem::path& SourcePath() const { return mSourcePath; }
|
||||
bool LoadedFromFile() const { return mLoadedFromFile; }
|
||||
|
||||
private:
|
||||
AppConfig mConfig;
|
||||
std::filesystem::path mSourcePath;
|
||||
bool mLoadedFromFile = false;
|
||||
};
|
||||
|
||||
double FrameDurationMillisecondsFromRateString(const std::string& rateText, double fallbackRate = 59.94);
|
||||
double FrameDurationMillisecondsFromDisplayMode(BMDDisplayMode displayMode, double fallbackMilliseconds);
|
||||
void VideoFormatDimensions(const std::string& formatName, unsigned& width, unsigned& height);
|
||||
std::filesystem::path FindConfigFile(const std::filesystem::path& relativePath = "config/runtime-host.json");
|
||||
std::filesystem::path FindRepoPath(const std::filesystem::path& relativePath);
|
||||
}
|
||||
313
src/app/RenderCadenceApp.h
Normal file
313
src/app/RenderCadenceApp.h
Normal file
@@ -0,0 +1,313 @@
|
||||
#pragma once
|
||||
|
||||
#include "AppConfig.h"
|
||||
#include "AppConfigProvider.h"
|
||||
#include "RuntimeLayerController.h"
|
||||
#include "../logging/Logger.h"
|
||||
#include "../control/RuntimeStateJson.h"
|
||||
#include "../preview/PreviewWindowThread.h"
|
||||
#include "../telemetry/TelemetryHealthMonitor.h"
|
||||
#include "../video/DeckLinkInput.h"
|
||||
#include "../video/DeckLinkOutput.h"
|
||||
#include "../video/DeckLinkOutputThread.h"
|
||||
|
||||
#include <chrono>
|
||||
#include <filesystem>
|
||||
#include <functional>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
#include <type_traits>
|
||||
|
||||
namespace RenderCadenceCompositor
|
||||
{
|
||||
namespace detail
|
||||
{
|
||||
template <typename RenderThread>
|
||||
auto StartRenderThread(RenderThread& renderThread, std::string& error, int) -> decltype(renderThread.Start(error), bool())
|
||||
{
|
||||
return renderThread.Start(error);
|
||||
}
|
||||
|
||||
template <typename RenderThread>
|
||||
bool StartRenderThreadWithoutError(RenderThread& renderThread, std::true_type)
|
||||
{
|
||||
return renderThread.Start();
|
||||
}
|
||||
|
||||
template <typename RenderThread>
|
||||
bool StartRenderThreadWithoutError(RenderThread& renderThread, std::false_type)
|
||||
{
|
||||
renderThread.Start();
|
||||
return true;
|
||||
}
|
||||
|
||||
template <typename RenderThread>
|
||||
auto StartRenderThread(RenderThread& renderThread, std::string&, long) -> decltype(renderThread.Start(), bool())
|
||||
{
|
||||
return StartRenderThreadWithoutError(renderThread, std::is_same<decltype(renderThread.Start()), bool>());
|
||||
}
|
||||
}
|
||||
|
||||
template <typename RenderThread, typename SystemFrameExchange>
|
||||
class RenderCadenceApp
|
||||
{
|
||||
public:
|
||||
RenderCadenceApp(RenderThread& renderThread, SystemFrameExchange& frameExchange, AppConfig config = DefaultAppConfig()) :
|
||||
mRenderThread(renderThread),
|
||||
mFrameExchange(frameExchange),
|
||||
mConfig(config),
|
||||
mOutputThread(mOutput, mFrameExchange, mConfig.outputThread),
|
||||
mTelemetryHealth(mConfig.telemetry),
|
||||
mRuntimeLayers([this](const std::vector<RuntimeRenderLayerModel>& layers) {
|
||||
mRenderThread.SubmitRuntimeRenderLayers(layers);
|
||||
})
|
||||
{
|
||||
}
|
||||
|
||||
RenderCadenceApp(const RenderCadenceApp&) = delete;
|
||||
RenderCadenceApp& operator=(const RenderCadenceApp&) = delete;
|
||||
|
||||
~RenderCadenceApp()
|
||||
{
|
||||
Stop();
|
||||
}
|
||||
|
||||
bool Start(std::string& error)
|
||||
{
|
||||
mRuntimeLayers.Initialize(
|
||||
mConfig.shaderLibrary,
|
||||
static_cast<unsigned>(mConfig.maxTemporalHistoryFrames),
|
||||
mConfig.runtimeShaderId);
|
||||
|
||||
Log("app", "Starting render thread.");
|
||||
if (!detail::StartRenderThread(mRenderThread, error, 0))
|
||||
{
|
||||
LogError("app", "Render thread start failed: " + error);
|
||||
Stop();
|
||||
return false;
|
||||
}
|
||||
mRuntimeLayers.StartStartupBuild(mConfig.runtimeShaderId);
|
||||
|
||||
if (!BuildSettledOutputReserve(error))
|
||||
{
|
||||
LogError("app", error);
|
||||
Stop();
|
||||
return false;
|
||||
}
|
||||
|
||||
StartPreviewWindow();
|
||||
StartOptionalVideoOutput();
|
||||
mTelemetryHealth.Start(mFrameExchange, mOutput, mOutputThread, mRenderThread);
|
||||
StartHttpServer();
|
||||
Log("app", "RenderCadenceCompositor started.");
|
||||
mStarted = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
void Stop()
|
||||
{
|
||||
mHttpServer.Stop();
|
||||
mTelemetryHealth.Stop();
|
||||
mPreviewWindow.Stop();
|
||||
mOutputThread.Stop();
|
||||
mOutput.Stop();
|
||||
mRuntimeLayers.Stop();
|
||||
mRenderThread.Stop();
|
||||
mOutput.ReleaseResources();
|
||||
if (mStarted)
|
||||
Log("app", "RenderCadenceCompositor shutdown complete.");
|
||||
mStarted = false;
|
||||
}
|
||||
|
||||
bool Started() const { return mStarted; }
|
||||
const DeckLinkOutput& Output() const { return mOutput; }
|
||||
void SetDeckLinkInputMetricsProvider(std::function<DeckLinkInputMetrics()> provider)
|
||||
{
|
||||
mDeckLinkInputMetricsProvider = std::move(provider);
|
||||
}
|
||||
|
||||
private:
|
||||
void StartOptionalVideoOutput()
|
||||
{
|
||||
std::string outputError;
|
||||
Log("app", "Initializing optional DeckLink output.");
|
||||
if (!mOutput.Initialize(
|
||||
mConfig.deckLink,
|
||||
[this](const VideoIOCompletion& completion) {
|
||||
mFrameExchange.ReleaseScheduledByBytes(completion.outputFrameBuffer);
|
||||
},
|
||||
outputError))
|
||||
{
|
||||
DisableVideoOutput("DeckLink output unavailable: " + outputError);
|
||||
return;
|
||||
}
|
||||
|
||||
Log("app", "Starting DeckLink output thread.");
|
||||
if (!mOutputThread.Start())
|
||||
{
|
||||
DisableVideoOutput("DeckLink output thread failed to start.");
|
||||
return;
|
||||
}
|
||||
|
||||
Log("app", "Waiting for DeckLink preroll frames.");
|
||||
if (!WaitForPreroll())
|
||||
{
|
||||
DisableVideoOutput("Timed out waiting for DeckLink preroll frames.");
|
||||
return;
|
||||
}
|
||||
|
||||
Log("app", "Starting DeckLink scheduled playback.");
|
||||
if (!mOutput.StartScheduledPlayback(outputError))
|
||||
{
|
||||
DisableVideoOutput("DeckLink scheduled playback failed: " + outputError);
|
||||
return;
|
||||
}
|
||||
|
||||
mVideoOutputEnabled = true;
|
||||
mVideoOutputStatus = "DeckLink scheduled output running.";
|
||||
Log("app", mVideoOutputStatus);
|
||||
Log(
|
||||
"app",
|
||||
"DeckLink output mode: " + mOutput.State().outputDisplayModeName +
|
||||
", frame budget " + std::to_string(mOutput.State().frameBudgetMilliseconds) + " ms.");
|
||||
}
|
||||
|
||||
bool BuildSettledOutputReserve(std::string& error)
|
||||
{
|
||||
const auto reserveTimeout = mConfig.warmupTimeout;
|
||||
Log("app",
|
||||
"Building output preroll reserve: waiting for " + std::to_string(mConfig.warmupCompletedFrames) +
|
||||
" completed frame(s).");
|
||||
if (mFrameExchange.WaitForCompletedDepth(mConfig.warmupCompletedFrames, reserveTimeout))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
error = "Timed out waiting for output preroll reserve.";
|
||||
return false;
|
||||
}
|
||||
|
||||
void DisableVideoOutput(const std::string& reason)
|
||||
{
|
||||
mOutputThread.Stop();
|
||||
mOutput.Stop();
|
||||
mOutput.ReleaseResources();
|
||||
mFrameExchange.Clear();
|
||||
mVideoOutputEnabled = false;
|
||||
mVideoOutputStatus = reason;
|
||||
LogWarning("app", reason + " Continuing without video output.");
|
||||
}
|
||||
|
||||
void StartHttpServer()
|
||||
{
|
||||
HttpControlServerCallbacks callbacks;
|
||||
callbacks.getStateJson = [this]() {
|
||||
return BuildStateJson();
|
||||
};
|
||||
callbacks.addLayer = [this](const std::string& body) {
|
||||
return mRuntimeLayers.HandleAddLayer(body);
|
||||
};
|
||||
callbacks.removeLayer = [this](const std::string& body) {
|
||||
return mRuntimeLayers.HandleRemoveLayer(body);
|
||||
};
|
||||
callbacks.executePost = [this](const std::string& path, const std::string& body) {
|
||||
RuntimeControlCommand command;
|
||||
std::string error;
|
||||
if (!ParseRuntimeControlCommand(path, body, command, error))
|
||||
return ControlActionResult{ false, error };
|
||||
return mRuntimeLayers.HandleControlCommand(command);
|
||||
};
|
||||
|
||||
std::string error;
|
||||
if (!mHttpServer.Start(
|
||||
FindRepoPath("ui/dist"),
|
||||
FindRepoPath("docs"),
|
||||
mConfig.http,
|
||||
callbacks,
|
||||
error))
|
||||
{
|
||||
LogWarning("http", "HTTP control server did not start: " + error);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
void StartPreviewWindow()
|
||||
{
|
||||
if (!mConfig.previewEnabled)
|
||||
return;
|
||||
|
||||
PreviewWindowConfig previewConfig;
|
||||
previewConfig.enabled = true;
|
||||
previewConfig.fps = mConfig.previewFps;
|
||||
std::string error;
|
||||
if (mPreviewWindow.Start(mFrameExchange, previewConfig, error))
|
||||
{
|
||||
Log("preview", "Preview window thread started.");
|
||||
return;
|
||||
}
|
||||
|
||||
LogWarning("preview", "Preview window did not start: " + error);
|
||||
}
|
||||
|
||||
std::string BuildStateJson()
|
||||
{
|
||||
CadenceTelemetrySnapshot telemetry = mHttpTelemetry.Sample(mFrameExchange, mOutput, mOutputThread, mRenderThread);
|
||||
ApplyDeckLinkInputMetrics(telemetry);
|
||||
RuntimeLayerModelSnapshot layerSnapshot = mRuntimeLayers.Snapshot(telemetry);
|
||||
return RuntimeStateToJson(RuntimeStateJsonInput{
|
||||
mConfig,
|
||||
telemetry,
|
||||
mHttpServer.Port(),
|
||||
mVideoOutputEnabled,
|
||||
mVideoOutputStatus,
|
||||
mRuntimeLayers.ShaderCatalog(),
|
||||
layerSnapshot
|
||||
});
|
||||
}
|
||||
|
||||
void ApplyDeckLinkInputMetrics(CadenceTelemetrySnapshot& telemetry)
|
||||
{
|
||||
if (!mDeckLinkInputMetricsProvider)
|
||||
return;
|
||||
|
||||
const DeckLinkInputMetrics inputMetrics = mDeckLinkInputMetricsProvider();
|
||||
telemetry.inputConvertMilliseconds = inputMetrics.convertMilliseconds;
|
||||
telemetry.inputSubmitMilliseconds = inputMetrics.submitMilliseconds;
|
||||
telemetry.inputNoSignalFrames = inputMetrics.noInputSourceFrames;
|
||||
telemetry.inputUnsupportedFrames = inputMetrics.unsupportedFrames;
|
||||
telemetry.inputSubmitMisses = inputMetrics.submitMisses;
|
||||
telemetry.inputCaptureFormat = inputMetrics.captureFormat ? inputMetrics.captureFormat : "none";
|
||||
if (telemetry.sampleSeconds > 0.0)
|
||||
telemetry.inputCaptureFps = static_cast<double>(inputMetrics.capturedFrames - mLastInputCapturedFrames) / telemetry.sampleSeconds;
|
||||
mLastInputCapturedFrames = inputMetrics.capturedFrames;
|
||||
}
|
||||
|
||||
bool WaitForPreroll() const
|
||||
{
|
||||
const auto deadline = std::chrono::steady_clock::now() + mConfig.prerollTimeout;
|
||||
while (std::chrono::steady_clock::now() < deadline)
|
||||
{
|
||||
if (mFrameExchange.Metrics().scheduledCount >= mConfig.outputThread.targetBufferedFrames)
|
||||
return true;
|
||||
std::this_thread::sleep_for(mConfig.prerollPoll);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
RenderThread& mRenderThread;
|
||||
SystemFrameExchange& mFrameExchange;
|
||||
AppConfig mConfig;
|
||||
DeckLinkOutput mOutput;
|
||||
DeckLinkOutputThread<SystemFrameExchange> mOutputThread;
|
||||
TelemetryHealthMonitor mTelemetryHealth;
|
||||
CadenceTelemetry mHttpTelemetry;
|
||||
HttpControlServer mHttpServer;
|
||||
PreviewWindowThread mPreviewWindow;
|
||||
RuntimeLayerController mRuntimeLayers;
|
||||
std::function<DeckLinkInputMetrics()> mDeckLinkInputMetricsProvider;
|
||||
uint64_t mLastInputCapturedFrames = 0;
|
||||
bool mStarted = false;
|
||||
bool mVideoOutputEnabled = false;
|
||||
std::string mVideoOutputStatus = "DeckLink output not started.";
|
||||
};
|
||||
}
|
||||
92
src/app/RuntimeLayerController.cpp
Normal file
92
src/app/RuntimeLayerController.cpp
Normal file
@@ -0,0 +1,92 @@
|
||||
#include "RuntimeLayerController.h"
|
||||
|
||||
#include "../logging/Logger.h"
|
||||
|
||||
namespace RenderCadenceCompositor
|
||||
{
|
||||
RuntimeLayerController::RuntimeLayerController(RenderLayerPublisher publisher) :
|
||||
mPublisher(std::move(publisher))
|
||||
{
|
||||
}
|
||||
|
||||
RuntimeLayerController::~RuntimeLayerController()
|
||||
{
|
||||
Stop();
|
||||
}
|
||||
|
||||
void RuntimeLayerController::SetPublisher(RenderLayerPublisher publisher)
|
||||
{
|
||||
mPublisher = std::move(publisher);
|
||||
}
|
||||
|
||||
void RuntimeLayerController::Initialize(const std::string& shaderLibrary, unsigned maxTemporalHistoryFrames, std::string& runtimeShaderId)
|
||||
{
|
||||
LoadSupportedShaderCatalog(shaderLibrary, maxTemporalHistoryFrames);
|
||||
InitializeLayerModel(runtimeShaderId);
|
||||
}
|
||||
|
||||
void RuntimeLayerController::StartStartupBuild(const std::string& runtimeShaderId)
|
||||
{
|
||||
if (runtimeShaderId.empty())
|
||||
{
|
||||
Log("runtime-shader", "Runtime shader build disabled.");
|
||||
return;
|
||||
}
|
||||
|
||||
Log("runtime-shader", "Starting background Slang build for shader '" + runtimeShaderId + "'.");
|
||||
const std::string layerId = FirstRuntimeLayerId();
|
||||
if (!layerId.empty())
|
||||
StartLayerShaderBuild(layerId, runtimeShaderId);
|
||||
}
|
||||
|
||||
void RuntimeLayerController::Stop()
|
||||
{
|
||||
StopAllRuntimeShaderBuilds();
|
||||
}
|
||||
|
||||
RuntimeLayerModelSnapshot RuntimeLayerController::Snapshot(const CadenceTelemetrySnapshot& telemetry) const
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mRuntimeLayerMutex);
|
||||
RuntimeLayerModelSnapshot snapshot = mRuntimeLayerModel.Snapshot();
|
||||
if (telemetry.shaderBuildFailures > 0)
|
||||
{
|
||||
snapshot.compileSucceeded = false;
|
||||
snapshot.compileMessage = "Runtime shader GL commit failed; see logs for details.";
|
||||
}
|
||||
return snapshot;
|
||||
}
|
||||
|
||||
void RuntimeLayerController::PublishRuntimeRenderLayers()
|
||||
{
|
||||
if (!mPublisher)
|
||||
return;
|
||||
|
||||
std::vector<RuntimeRenderLayerModel> renderLayers;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mRuntimeLayerMutex);
|
||||
renderLayers = mRuntimeLayerModel.Snapshot().renderLayers;
|
||||
}
|
||||
mPublisher(renderLayers);
|
||||
}
|
||||
|
||||
bool RuntimeLayerController::MarkRuntimeBuildReady(const RuntimeShaderArtifact& artifact)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mRuntimeLayerMutex);
|
||||
std::string error;
|
||||
if (!mRuntimeLayerModel.MarkBuildReady(artifact, error))
|
||||
{
|
||||
LogWarning("runtime-shader", error);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void RuntimeLayerController::MarkRuntimeBuildFailedForLayer(const std::string& layerId, const std::string& message)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mRuntimeLayerMutex);
|
||||
std::string error;
|
||||
if (!mRuntimeLayerModel.MarkBuildFailed(layerId, message, error))
|
||||
LogWarning("runtime-shader", error);
|
||||
}
|
||||
|
||||
}
|
||||
63
src/app/RuntimeLayerController.h
Normal file
63
src/app/RuntimeLayerController.h
Normal file
@@ -0,0 +1,63 @@
|
||||
#pragma once
|
||||
|
||||
#include "../control/ControlActionResult.h"
|
||||
#include "../control/RuntimeControlCommand.h"
|
||||
#include "../runtime/RuntimeLayerModel.h"
|
||||
#include "../runtime/RuntimeShaderBridge.h"
|
||||
#include "../runtime/SupportedShaderCatalog.h"
|
||||
#include "../telemetry/CadenceTelemetry.h"
|
||||
|
||||
#include <functional>
|
||||
#include <map>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace RenderCadenceCompositor
|
||||
{
|
||||
class RuntimeLayerController
|
||||
{
|
||||
public:
|
||||
using RenderLayerPublisher = std::function<void(const std::vector<RuntimeRenderLayerModel>&)>;
|
||||
|
||||
explicit RuntimeLayerController(RenderLayerPublisher publisher = RenderLayerPublisher());
|
||||
RuntimeLayerController(const RuntimeLayerController&) = delete;
|
||||
RuntimeLayerController& operator=(const RuntimeLayerController&) = delete;
|
||||
~RuntimeLayerController();
|
||||
|
||||
void SetPublisher(RenderLayerPublisher publisher);
|
||||
void Initialize(const std::string& shaderLibrary, unsigned maxTemporalHistoryFrames, std::string& runtimeShaderId);
|
||||
void StartStartupBuild(const std::string& runtimeShaderId);
|
||||
void Stop();
|
||||
|
||||
ControlActionResult HandleAddLayer(const std::string& body);
|
||||
ControlActionResult HandleRemoveLayer(const std::string& body);
|
||||
ControlActionResult HandleControlCommand(const RuntimeControlCommand& command);
|
||||
|
||||
RuntimeLayerModelSnapshot Snapshot(const CadenceTelemetrySnapshot& telemetry) const;
|
||||
const SupportedShaderCatalog& ShaderCatalog() const { return mShaderCatalog; }
|
||||
|
||||
private:
|
||||
void LoadSupportedShaderCatalog(const std::string& shaderLibrary, unsigned maxTemporalHistoryFrames);
|
||||
void InitializeLayerModel(std::string& runtimeShaderId);
|
||||
void StartLayerShaderBuild(const std::string& layerId, const std::string& shaderId);
|
||||
void RetireLayerShaderBuild(const std::string& layerId);
|
||||
void CleanupRetiredShaderBuilds();
|
||||
void StopAllRuntimeShaderBuilds();
|
||||
void PublishRuntimeRenderLayers();
|
||||
bool MarkRuntimeBuildReady(const RuntimeShaderArtifact& artifact);
|
||||
void MarkRuntimeBuildFailedForLayer(const std::string& layerId, const std::string& message);
|
||||
|
||||
std::string FirstRuntimeLayerId() const;
|
||||
static bool ExtractStringField(const std::string& body, const char* fieldName, std::string& value, std::string& error);
|
||||
|
||||
RenderLayerPublisher mPublisher;
|
||||
SupportedShaderCatalog mShaderCatalog;
|
||||
mutable std::mutex mRuntimeLayerMutex;
|
||||
RuntimeLayerModel mRuntimeLayerModel;
|
||||
std::mutex mShaderBuildMutex;
|
||||
std::map<std::string, std::unique_ptr<RuntimeShaderBridge>> mShaderBuilds;
|
||||
std::vector<std::unique_ptr<RuntimeShaderBridge>> mRetiredShaderBuilds;
|
||||
};
|
||||
}
|
||||
122
src/app/RuntimeLayerControllerBuild.cpp
Normal file
122
src/app/RuntimeLayerControllerBuild.cpp
Normal file
@@ -0,0 +1,122 @@
|
||||
#include "RuntimeLayerController.h"
|
||||
|
||||
#include "AppConfigProvider.h"
|
||||
#include "../logging/Logger.h"
|
||||
|
||||
#include <filesystem>
|
||||
|
||||
namespace RenderCadenceCompositor
|
||||
{
|
||||
void RuntimeLayerController::LoadSupportedShaderCatalog(const std::string& shaderLibrary, unsigned maxTemporalHistoryFrames)
|
||||
{
|
||||
const std::filesystem::path shaderRoot = FindRepoPath(shaderLibrary);
|
||||
std::string error;
|
||||
if (!mShaderCatalog.Load(shaderRoot, maxTemporalHistoryFrames, error))
|
||||
{
|
||||
LogWarning("runtime-shader", "Supported shader catalog is empty: " + error);
|
||||
return;
|
||||
}
|
||||
|
||||
Log("runtime-shader", "Supported shader catalog loaded with " + std::to_string(mShaderCatalog.Shaders().size()) + " shader(s).");
|
||||
}
|
||||
|
||||
void RuntimeLayerController::InitializeLayerModel(std::string& runtimeShaderId)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mRuntimeLayerMutex);
|
||||
std::string error;
|
||||
if (!mRuntimeLayerModel.InitializeSingleLayer(mShaderCatalog, runtimeShaderId, error))
|
||||
{
|
||||
LogWarning("runtime-shader", error + " Runtime shader build disabled.");
|
||||
runtimeShaderId.clear();
|
||||
mRuntimeLayerModel.Clear();
|
||||
}
|
||||
}
|
||||
|
||||
void RuntimeLayerController::StartLayerShaderBuild(const std::string& layerId, const std::string& shaderId)
|
||||
{
|
||||
CleanupRetiredShaderBuilds();
|
||||
RetireLayerShaderBuild(layerId);
|
||||
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mRuntimeLayerMutex);
|
||||
std::string error;
|
||||
mRuntimeLayerModel.MarkBuildStarted(layerId, "Runtime Slang build started for shader '" + shaderId + "'.", error);
|
||||
}
|
||||
|
||||
auto bridge = std::make_unique<RuntimeShaderBridge>();
|
||||
RuntimeShaderBridge* bridgePtr = bridge.get();
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mShaderBuildMutex);
|
||||
mShaderBuilds[layerId] = std::move(bridge);
|
||||
}
|
||||
|
||||
bridgePtr->Start(
|
||||
layerId,
|
||||
shaderId,
|
||||
[this](const RuntimeShaderArtifact& artifact) {
|
||||
if (MarkRuntimeBuildReady(artifact))
|
||||
PublishRuntimeRenderLayers();
|
||||
},
|
||||
[this, layerId](const std::string& message) {
|
||||
MarkRuntimeBuildFailedForLayer(layerId, message);
|
||||
LogError("runtime-shader", "Runtime Slang build failed: " + message);
|
||||
});
|
||||
}
|
||||
|
||||
void RuntimeLayerController::RetireLayerShaderBuild(const std::string& layerId)
|
||||
{
|
||||
std::unique_ptr<RuntimeShaderBridge> bridge;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mShaderBuildMutex);
|
||||
auto bridgeIt = mShaderBuilds.find(layerId);
|
||||
if (bridgeIt == mShaderBuilds.end())
|
||||
return;
|
||||
bridge = std::move(bridgeIt->second);
|
||||
mShaderBuilds.erase(bridgeIt);
|
||||
bridge->RequestStop();
|
||||
mRetiredShaderBuilds.push_back(std::move(bridge));
|
||||
}
|
||||
}
|
||||
|
||||
void RuntimeLayerController::CleanupRetiredShaderBuilds()
|
||||
{
|
||||
std::vector<std::unique_ptr<RuntimeShaderBridge>> readyToStop;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mShaderBuildMutex);
|
||||
for (auto it = mRetiredShaderBuilds.begin(); it != mRetiredShaderBuilds.end();)
|
||||
{
|
||||
if ((*it)->CanStopWithoutWaiting())
|
||||
{
|
||||
readyToStop.push_back(std::move(*it));
|
||||
it = mRetiredShaderBuilds.erase(it);
|
||||
continue;
|
||||
}
|
||||
++it;
|
||||
}
|
||||
}
|
||||
|
||||
for (std::unique_ptr<RuntimeShaderBridge>& bridge : readyToStop)
|
||||
bridge->Stop();
|
||||
}
|
||||
|
||||
void RuntimeLayerController::StopAllRuntimeShaderBuilds()
|
||||
{
|
||||
std::map<std::string, std::unique_ptr<RuntimeShaderBridge>> builds;
|
||||
std::vector<std::unique_ptr<RuntimeShaderBridge>> retiredBuilds;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mShaderBuildMutex);
|
||||
builds.swap(mShaderBuilds);
|
||||
retiredBuilds.swap(mRetiredShaderBuilds);
|
||||
}
|
||||
for (auto& entry : builds)
|
||||
entry.second->Stop();
|
||||
for (auto& bridge : retiredBuilds)
|
||||
bridge->Stop();
|
||||
}
|
||||
|
||||
std::string RuntimeLayerController::FirstRuntimeLayerId() const
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mRuntimeLayerMutex);
|
||||
return mRuntimeLayerModel.FirstLayerId();
|
||||
}
|
||||
}
|
||||
160
src/app/RuntimeLayerControllerControls.cpp
Normal file
160
src/app/RuntimeLayerControllerControls.cpp
Normal file
@@ -0,0 +1,160 @@
|
||||
#include "RuntimeLayerController.h"
|
||||
|
||||
#include "RuntimeJson.h"
|
||||
#include "../logging/Logger.h"
|
||||
|
||||
namespace RenderCadenceCompositor
|
||||
{
|
||||
ControlActionResult RuntimeLayerController::HandleAddLayer(const std::string& body)
|
||||
{
|
||||
CleanupRetiredShaderBuilds();
|
||||
|
||||
std::string shaderId;
|
||||
std::string error;
|
||||
if (!ExtractStringField(body, "shaderId", shaderId, error))
|
||||
return { false, error };
|
||||
|
||||
std::string layerId;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mRuntimeLayerMutex);
|
||||
if (!mRuntimeLayerModel.AddLayer(mShaderCatalog, shaderId, layerId, error))
|
||||
return { false, error };
|
||||
}
|
||||
|
||||
Log("runtime-shader", "Layer added: " + layerId + " shader=" + shaderId);
|
||||
StartLayerShaderBuild(layerId, shaderId);
|
||||
return { true, std::string() };
|
||||
}
|
||||
|
||||
ControlActionResult RuntimeLayerController::HandleRemoveLayer(const std::string& body)
|
||||
{
|
||||
CleanupRetiredShaderBuilds();
|
||||
|
||||
std::string layerId;
|
||||
std::string error;
|
||||
if (!ExtractStringField(body, "layerId", layerId, error))
|
||||
return { false, error };
|
||||
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mRuntimeLayerMutex);
|
||||
if (!mRuntimeLayerModel.RemoveLayer(layerId, error))
|
||||
return { false, error };
|
||||
}
|
||||
|
||||
Log("runtime-shader", "Layer removed: " + layerId);
|
||||
RetireLayerShaderBuild(layerId);
|
||||
PublishRuntimeRenderLayers();
|
||||
return { true, std::string() };
|
||||
}
|
||||
|
||||
ControlActionResult RuntimeLayerController::HandleControlCommand(const RuntimeControlCommand& command)
|
||||
{
|
||||
CleanupRetiredShaderBuilds();
|
||||
|
||||
std::string error;
|
||||
switch (command.type)
|
||||
{
|
||||
case RuntimeControlCommandType::AddLayer:
|
||||
{
|
||||
std::string layerId;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mRuntimeLayerMutex);
|
||||
if (!mRuntimeLayerModel.AddLayer(mShaderCatalog, command.shaderId, layerId, error))
|
||||
return { false, error };
|
||||
}
|
||||
Log("runtime-shader", "Layer added: " + layerId + " shader=" + command.shaderId);
|
||||
StartLayerShaderBuild(layerId, command.shaderId);
|
||||
return { true, std::string() };
|
||||
}
|
||||
case RuntimeControlCommandType::RemoveLayer:
|
||||
{
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mRuntimeLayerMutex);
|
||||
if (!mRuntimeLayerModel.RemoveLayer(command.layerId, error))
|
||||
return { false, error };
|
||||
}
|
||||
Log("runtime-shader", "Layer removed: " + command.layerId);
|
||||
RetireLayerShaderBuild(command.layerId);
|
||||
PublishRuntimeRenderLayers();
|
||||
return { true, std::string() };
|
||||
}
|
||||
case RuntimeControlCommandType::ReorderLayer:
|
||||
{
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mRuntimeLayerMutex);
|
||||
if (!mRuntimeLayerModel.ReorderLayer(command.layerId, command.targetIndex, error))
|
||||
return { false, error };
|
||||
}
|
||||
PublishRuntimeRenderLayers();
|
||||
return { true, std::string() };
|
||||
}
|
||||
case RuntimeControlCommandType::SetLayerBypass:
|
||||
{
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mRuntimeLayerMutex);
|
||||
if (!mRuntimeLayerModel.SetLayerBypass(command.layerId, command.bypass, error))
|
||||
return { false, error };
|
||||
}
|
||||
PublishRuntimeRenderLayers();
|
||||
return { true, std::string() };
|
||||
}
|
||||
case RuntimeControlCommandType::SetLayerShader:
|
||||
{
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mRuntimeLayerMutex);
|
||||
if (!mRuntimeLayerModel.SetLayerShader(mShaderCatalog, command.layerId, command.shaderId, error))
|
||||
return { false, error };
|
||||
}
|
||||
Log("runtime-shader", "Layer shader changed: " + command.layerId + " shader=" + command.shaderId);
|
||||
StartLayerShaderBuild(command.layerId, command.shaderId);
|
||||
return { true, std::string() };
|
||||
}
|
||||
case RuntimeControlCommandType::UpdateLayerParameter:
|
||||
{
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mRuntimeLayerMutex);
|
||||
if (!mRuntimeLayerModel.UpdateParameter(command.layerId, command.parameterId, command.value, error))
|
||||
return { false, error };
|
||||
}
|
||||
PublishRuntimeRenderLayers();
|
||||
return { true, std::string() };
|
||||
}
|
||||
case RuntimeControlCommandType::ResetLayerParameters:
|
||||
{
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mRuntimeLayerMutex);
|
||||
if (!mRuntimeLayerModel.ResetParameters(command.layerId, error))
|
||||
return { false, error };
|
||||
}
|
||||
PublishRuntimeRenderLayers();
|
||||
return { true, std::string() };
|
||||
}
|
||||
case RuntimeControlCommandType::Unsupported:
|
||||
break;
|
||||
}
|
||||
|
||||
return { false, "Unsupported runtime control command." };
|
||||
}
|
||||
|
||||
bool RuntimeLayerController::ExtractStringField(const std::string& body, const char* fieldName, std::string& value, std::string& error)
|
||||
{
|
||||
JsonValue root;
|
||||
std::string parseError;
|
||||
if (!ParseJson(body.empty() ? "{}" : body, root, parseError) || !root.isObject())
|
||||
{
|
||||
error = parseError.empty() ? "Request body must be a JSON object." : parseError;
|
||||
return false;
|
||||
}
|
||||
|
||||
const JsonValue* field = root.find(fieldName);
|
||||
if (!field || !field->isString() || field->asString().empty())
|
||||
{
|
||||
error = std::string("Request field '") + fieldName + "' must be a non-empty string.";
|
||||
return false;
|
||||
}
|
||||
|
||||
value = field->asString();
|
||||
error.clear();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
12
src/control/ControlActionResult.h
Normal file
12
src/control/ControlActionResult.h
Normal file
@@ -0,0 +1,12 @@
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
|
||||
namespace RenderCadenceCompositor
|
||||
{
|
||||
struct ControlActionResult
|
||||
{
|
||||
bool ok = false;
|
||||
std::string error;
|
||||
};
|
||||
}
|
||||
127
src/control/RuntimeControlCommand.cpp
Normal file
127
src/control/RuntimeControlCommand.cpp
Normal file
@@ -0,0 +1,127 @@
|
||||
#include "RuntimeControlCommand.h"
|
||||
|
||||
namespace RenderCadenceCompositor
|
||||
{
|
||||
namespace
|
||||
{
|
||||
const JsonValue* RequireObjectField(const JsonValue& root, const char* fieldName, std::string& error)
|
||||
{
|
||||
const JsonValue* field = root.find(fieldName);
|
||||
if (!field)
|
||||
error = std::string("Request field '") + fieldName + "' is required.";
|
||||
return field;
|
||||
}
|
||||
|
||||
bool RequireStringField(const JsonValue& root, const char* fieldName, std::string& value, std::string& error)
|
||||
{
|
||||
const JsonValue* field = RequireObjectField(root, fieldName, error);
|
||||
if (!field)
|
||||
return false;
|
||||
if (!field->isString() || field->asString().empty())
|
||||
{
|
||||
error = std::string("Request field '") + fieldName + "' must be a non-empty string.";
|
||||
return false;
|
||||
}
|
||||
value = field->asString();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool RequireBoolField(const JsonValue& root, const char* fieldName, bool& value, std::string& error)
|
||||
{
|
||||
const JsonValue* field = RequireObjectField(root, fieldName, error);
|
||||
if (!field)
|
||||
return false;
|
||||
if (!field->isBoolean())
|
||||
{
|
||||
error = std::string("Request field '") + fieldName + "' must be a boolean.";
|
||||
return false;
|
||||
}
|
||||
value = field->asBoolean();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool RequireIntegerField(const JsonValue& root, const char* fieldName, int& value, std::string& error)
|
||||
{
|
||||
const JsonValue* field = RequireObjectField(root, fieldName, error);
|
||||
if (!field)
|
||||
return false;
|
||||
if (!field->isNumber())
|
||||
{
|
||||
error = std::string("Request field '") + fieldName + "' must be a number.";
|
||||
return false;
|
||||
}
|
||||
value = static_cast<int>(field->asNumber());
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
bool ParseRuntimeControlCommand(
|
||||
const std::string& path,
|
||||
const std::string& body,
|
||||
RuntimeControlCommand& command,
|
||||
std::string& error)
|
||||
{
|
||||
command = RuntimeControlCommand();
|
||||
|
||||
JsonValue root;
|
||||
std::string parseError;
|
||||
if (!ParseJson(body.empty() ? "{}" : body, root, parseError) || !root.isObject())
|
||||
{
|
||||
error = parseError.empty() ? "Request body must be a JSON object." : parseError;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (path == "/api/layers/add")
|
||||
{
|
||||
command.type = RuntimeControlCommandType::AddLayer;
|
||||
return RequireStringField(root, "shaderId", command.shaderId, error);
|
||||
}
|
||||
if (path == "/api/layers/remove")
|
||||
{
|
||||
command.type = RuntimeControlCommandType::RemoveLayer;
|
||||
return RequireStringField(root, "layerId", command.layerId, error);
|
||||
}
|
||||
if (path == "/api/layers/reorder")
|
||||
{
|
||||
command.type = RuntimeControlCommandType::ReorderLayer;
|
||||
return RequireStringField(root, "layerId", command.layerId, error)
|
||||
&& RequireIntegerField(root, "targetIndex", command.targetIndex, error);
|
||||
}
|
||||
if (path == "/api/layers/set-bypass")
|
||||
{
|
||||
command.type = RuntimeControlCommandType::SetLayerBypass;
|
||||
return RequireStringField(root, "layerId", command.layerId, error)
|
||||
&& RequireBoolField(root, "bypass", command.bypass, error);
|
||||
}
|
||||
if (path == "/api/layers/set-shader")
|
||||
{
|
||||
command.type = RuntimeControlCommandType::SetLayerShader;
|
||||
return RequireStringField(root, "layerId", command.layerId, error)
|
||||
&& RequireStringField(root, "shaderId", command.shaderId, error);
|
||||
}
|
||||
if (path == "/api/layers/update-parameter")
|
||||
{
|
||||
command.type = RuntimeControlCommandType::UpdateLayerParameter;
|
||||
const JsonValue* value = nullptr;
|
||||
if (!RequireStringField(root, "layerId", command.layerId, error)
|
||||
|| !RequireStringField(root, "parameterId", command.parameterId, error))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
value = RequireObjectField(root, "value", error);
|
||||
if (!value)
|
||||
return false;
|
||||
command.value = *value;
|
||||
return true;
|
||||
}
|
||||
if (path == "/api/layers/reset-parameters")
|
||||
{
|
||||
command.type = RuntimeControlCommandType::ResetLayerParameters;
|
||||
return RequireStringField(root, "layerId", command.layerId, error);
|
||||
}
|
||||
|
||||
command.type = RuntimeControlCommandType::Unsupported;
|
||||
error = "Endpoint is not implemented in RenderCadenceCompositor yet.";
|
||||
return false;
|
||||
}
|
||||
}
|
||||
37
src/control/RuntimeControlCommand.h
Normal file
37
src/control/RuntimeControlCommand.h
Normal file
@@ -0,0 +1,37 @@
|
||||
#pragma once
|
||||
|
||||
#include "RuntimeJson.h"
|
||||
|
||||
#include <string>
|
||||
|
||||
namespace RenderCadenceCompositor
|
||||
{
|
||||
enum class RuntimeControlCommandType
|
||||
{
|
||||
AddLayer,
|
||||
RemoveLayer,
|
||||
ReorderLayer,
|
||||
SetLayerBypass,
|
||||
SetLayerShader,
|
||||
UpdateLayerParameter,
|
||||
ResetLayerParameters,
|
||||
Unsupported
|
||||
};
|
||||
|
||||
struct RuntimeControlCommand
|
||||
{
|
||||
RuntimeControlCommandType type = RuntimeControlCommandType::Unsupported;
|
||||
std::string layerId;
|
||||
std::string shaderId;
|
||||
std::string parameterId;
|
||||
int targetIndex = 0;
|
||||
bool bypass = false;
|
||||
JsonValue value;
|
||||
};
|
||||
|
||||
bool ParseRuntimeControlCommand(
|
||||
const std::string& path,
|
||||
const std::string& body,
|
||||
RuntimeControlCommand& command,
|
||||
std::string& error);
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user