Compare commits
65 Commits
ab38bfad24
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 |
@@ -7,6 +7,9 @@ on:
|
|||||||
- master
|
- master
|
||||||
- develop
|
- develop
|
||||||
pull_request:
|
pull_request:
|
||||||
|
schedule:
|
||||||
|
# Nightly build at 14:00 UTC, roughly midnight in Australia/Sydney.
|
||||||
|
- cron: "0 14 * * *"
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
@@ -82,6 +85,7 @@ jobs:
|
|||||||
package-windows:
|
package-windows:
|
||||||
name: Windows Release Package
|
name: Windows Release Package
|
||||||
runs-on: windows-2022
|
runs-on: windows-2022
|
||||||
|
if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch'
|
||||||
needs:
|
needs:
|
||||||
- native-windows
|
- native-windows
|
||||||
- ui-ubuntu
|
- ui-ubuntu
|
||||||
|
|||||||
85
.vscode/launch.json
vendored
85
.vscode/launch.json
vendored
@@ -9,7 +9,12 @@
|
|||||||
"args": [],
|
"args": [],
|
||||||
"stopAtEntry": false,
|
"stopAtEntry": false,
|
||||||
"cwd": "${workspaceFolder}\\build\\vs2022-x64-debug\\Debug",
|
"cwd": "${workspaceFolder}\\build\\vs2022-x64-debug\\Debug",
|
||||||
"environment": [],
|
"environment": [
|
||||||
|
{
|
||||||
|
"name": "VST_DISABLE_INPUT_CAPTURE",
|
||||||
|
"value": "1"
|
||||||
|
}
|
||||||
|
],
|
||||||
"console": "internalConsole",
|
"console": "internalConsole",
|
||||||
"symbolSearchPath": "${workspaceFolder}\\build\\vs2022-x64-debug\\Debug",
|
"symbolSearchPath": "${workspaceFolder}\\build\\vs2022-x64-debug\\Debug",
|
||||||
"requireExactSource": true,
|
"requireExactSource": true,
|
||||||
@@ -17,6 +22,84 @@
|
|||||||
"moduleLoad": true
|
"moduleLoad": true
|
||||||
},
|
},
|
||||||
"preLaunchTask": "Build LoopThroughWithOpenGLCompositing Debug x64"
|
"preLaunchTask": "Build LoopThroughWithOpenGLCompositing Debug x64"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Debug LoopThroughWithOpenGLCompositing - sync readback experiment",
|
||||||
|
"type": "cppvsdbg",
|
||||||
|
"request": "launch",
|
||||||
|
"program": "${workspaceFolder}\\build\\vs2022-x64-debug\\Debug\\LoopThroughWithOpenGLCompositing.exe",
|
||||||
|
"args": [],
|
||||||
|
"stopAtEntry": false,
|
||||||
|
"cwd": "${workspaceFolder}\\build\\vs2022-x64-debug\\Debug",
|
||||||
|
"environment": [
|
||||||
|
{
|
||||||
|
"name": "VST_OUTPUT_READBACK_MODE",
|
||||||
|
"value": "sync"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"console": "internalConsole",
|
||||||
|
"symbolSearchPath": "${workspaceFolder}\\build\\vs2022-x64-debug\\Debug",
|
||||||
|
"requireExactSource": true,
|
||||||
|
"logging": {
|
||||||
|
"moduleLoad": true
|
||||||
|
},
|
||||||
|
"preLaunchTask": "Build LoopThroughWithOpenGLCompositing Debug x64"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Debug LoopThroughWithOpenGLCompositing - cached output experiment",
|
||||||
|
"type": "cppvsdbg",
|
||||||
|
"request": "launch",
|
||||||
|
"program": "${workspaceFolder}\\build\\vs2022-x64-debug\\Debug\\LoopThroughWithOpenGLCompositing.exe",
|
||||||
|
"args": [],
|
||||||
|
"stopAtEntry": false,
|
||||||
|
"cwd": "${workspaceFolder}\\build\\vs2022-x64-debug\\Debug",
|
||||||
|
"environment": [
|
||||||
|
{
|
||||||
|
"name": "VST_OUTPUT_READBACK_MODE",
|
||||||
|
"value": "cached_only"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"console": "internalConsole",
|
||||||
|
"symbolSearchPath": "${workspaceFolder}\\build\\vs2022-x64-debug\\Debug",
|
||||||
|
"requireExactSource": true,
|
||||||
|
"logging": {
|
||||||
|
"moduleLoad": true
|
||||||
|
},
|
||||||
|
"preLaunchTask": "Build LoopThroughWithOpenGLCompositing Debug x64"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Debug DeckLinkRenderCadenceProbe",
|
||||||
|
"type": "cppvsdbg",
|
||||||
|
"request": "launch",
|
||||||
|
"program": "${workspaceFolder}\\build\\vs2022-x64-debug\\Debug\\DeckLinkRenderCadenceProbe.exe",
|
||||||
|
"args": [],
|
||||||
|
"stopAtEntry": false,
|
||||||
|
"cwd": "${workspaceFolder}\\build\\vs2022-x64-debug\\Debug",
|
||||||
|
"environment": [],
|
||||||
|
"console": "externalTerminal",
|
||||||
|
"symbolSearchPath": "${workspaceFolder}\\build\\vs2022-x64-debug\\Debug",
|
||||||
|
"requireExactSource": true,
|
||||||
|
"logging": {
|
||||||
|
"moduleLoad": true
|
||||||
|
},
|
||||||
|
"preLaunchTask": "Build DeckLinkRenderCadenceProbe Debug x64"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Debug RenderCadenceCompositor",
|
||||||
|
"type": "cppvsdbg",
|
||||||
|
"request": "launch",
|
||||||
|
"program": "${workspaceFolder}\\build\\vs2022-x64-debug\\Debug\\RenderCadenceCompositor.exe",
|
||||||
|
"args": [],
|
||||||
|
"stopAtEntry": false,
|
||||||
|
"cwd": "${workspaceFolder}\\build\\vs2022-x64-debug\\Debug",
|
||||||
|
"environment": [],
|
||||||
|
"console": "externalTerminal",
|
||||||
|
"symbolSearchPath": "${workspaceFolder}\\build\\vs2022-x64-debug\\Debug",
|
||||||
|
"requireExactSource": true,
|
||||||
|
"logging": {
|
||||||
|
"moduleLoad": true
|
||||||
|
},
|
||||||
|
"preLaunchTask": "Build RenderCadenceCompositor Debug x64"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
32
.vscode/tasks.json
vendored
32
.vscode/tasks.json
vendored
@@ -36,6 +36,38 @@
|
|||||||
"group": "build",
|
"group": "build",
|
||||||
"problemMatcher": "$msCompile"
|
"problemMatcher": "$msCompile"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"label": "Build DeckLinkRenderCadenceProbe Debug x64",
|
||||||
|
"type": "process",
|
||||||
|
"command": "C:\\Program Files\\Microsoft Visual Studio\\2022\\Community\\Common7\\IDE\\CommonExtensions\\Microsoft\\CMake\\CMake\\bin\\cmake.exe",
|
||||||
|
"args": [
|
||||||
|
"--build",
|
||||||
|
"${workspaceFolder}\\build\\vs2022-x64-debug",
|
||||||
|
"--config",
|
||||||
|
"Debug",
|
||||||
|
"--target",
|
||||||
|
"DeckLinkRenderCadenceProbe",
|
||||||
|
"--parallel"
|
||||||
|
],
|
||||||
|
"group": "build",
|
||||||
|
"problemMatcher": "$msCompile"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "Build RenderCadenceCompositor Debug x64",
|
||||||
|
"type": "process",
|
||||||
|
"command": "C:\\Program Files\\Microsoft Visual Studio\\2022\\Community\\Common7\\IDE\\CommonExtensions\\Microsoft\\CMake\\CMake\\bin\\cmake.exe",
|
||||||
|
"args": [
|
||||||
|
"--build",
|
||||||
|
"${workspaceFolder}\\build\\vs2022-x64-debug",
|
||||||
|
"--config",
|
||||||
|
"Debug",
|
||||||
|
"--target",
|
||||||
|
"RenderCadenceCompositor",
|
||||||
|
"--parallel"
|
||||||
|
],
|
||||||
|
"group": "build",
|
||||||
|
"problemMatcher": "$msCompile"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"label": "Clean LoopThroughWithOpenGLCompositing Debug x64",
|
"label": "Clean LoopThroughWithOpenGLCompositing Debug x64",
|
||||||
"type": "process",
|
"type": "process",
|
||||||
|
|||||||
545
CMakeLists.txt
545
CMakeLists.txt
@@ -165,7 +165,18 @@ set(APP_SOURCES
|
|||||||
"${APP_DIR}/videoio/VideoIOFormat.h"
|
"${APP_DIR}/videoio/VideoIOFormat.h"
|
||||||
"${APP_DIR}/videoio/VideoBackend.cpp"
|
"${APP_DIR}/videoio/VideoBackend.cpp"
|
||||||
"${APP_DIR}/videoio/VideoBackend.h"
|
"${APP_DIR}/videoio/VideoBackend.h"
|
||||||
|
"${APP_DIR}/videoio/VideoBackendLifecycle.cpp"
|
||||||
|
"${APP_DIR}/videoio/VideoBackendLifecycle.h"
|
||||||
"${APP_DIR}/videoio/VideoIOTypes.h"
|
"${APP_DIR}/videoio/VideoIOTypes.h"
|
||||||
|
"${APP_DIR}/videoio/OutputProductionController.cpp"
|
||||||
|
"${APP_DIR}/videoio/OutputProductionController.h"
|
||||||
|
"${APP_DIR}/videoio/RenderCadenceController.cpp"
|
||||||
|
"${APP_DIR}/videoio/RenderCadenceController.h"
|
||||||
|
"${APP_DIR}/videoio/RenderOutputQueue.cpp"
|
||||||
|
"${APP_DIR}/videoio/RenderOutputQueue.h"
|
||||||
|
"${APP_DIR}/videoio/SystemOutputFramePool.cpp"
|
||||||
|
"${APP_DIR}/videoio/SystemOutputFramePool.h"
|
||||||
|
"${APP_DIR}/videoio/VideoPlayoutPolicy.h"
|
||||||
"${APP_DIR}/videoio/VideoPlayoutScheduler.cpp"
|
"${APP_DIR}/videoio/VideoPlayoutScheduler.cpp"
|
||||||
"${APP_DIR}/videoio/VideoPlayoutScheduler.h"
|
"${APP_DIR}/videoio/VideoPlayoutScheduler.h"
|
||||||
)
|
)
|
||||||
@@ -218,6 +229,192 @@ if(MSVC)
|
|||||||
target_compile_options(LoopThroughWithOpenGLCompositing PRIVATE /W3)
|
target_compile_options(LoopThroughWithOpenGLCompositing PRIVATE /W3)
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
|
set(PROBE_APP_DIR "${CMAKE_CURRENT_SOURCE_DIR}/apps/DeckLinkRenderCadenceProbe")
|
||||||
|
|
||||||
|
add_executable(DeckLinkRenderCadenceProbe
|
||||||
|
"${APP_DIR}/videoio/decklink/DeckLinkAPI_i.c"
|
||||||
|
"${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}/videoio/VideoIOFormat.cpp"
|
||||||
|
"${APP_DIR}/videoio/VideoIOFormat.h"
|
||||||
|
"${APP_DIR}/videoio/VideoIOTypes.h"
|
||||||
|
"${APP_DIR}/videoio/VideoPlayoutPolicy.h"
|
||||||
|
"${APP_DIR}/videoio/VideoPlayoutScheduler.cpp"
|
||||||
|
"${APP_DIR}/videoio/VideoPlayoutScheduler.h"
|
||||||
|
"${PROBE_APP_DIR}/DeckLinkRenderCadenceProbe.cpp"
|
||||||
|
)
|
||||||
|
|
||||||
|
target_include_directories(DeckLinkRenderCadenceProbe PRIVATE
|
||||||
|
"${APP_DIR}"
|
||||||
|
"${APP_DIR}/gl/renderer"
|
||||||
|
"${APP_DIR}/videoio"
|
||||||
|
"${APP_DIR}/videoio/decklink"
|
||||||
|
)
|
||||||
|
|
||||||
|
target_link_libraries(DeckLinkRenderCadenceProbe PRIVATE
|
||||||
|
opengl32
|
||||||
|
Ole32
|
||||||
|
)
|
||||||
|
|
||||||
|
target_compile_definitions(DeckLinkRenderCadenceProbe PRIVATE
|
||||||
|
_UNICODE
|
||||||
|
UNICODE
|
||||||
|
)
|
||||||
|
|
||||||
|
if(MSVC)
|
||||||
|
target_compile_options(DeckLinkRenderCadenceProbe PRIVATE /W3)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
set(RENDER_CADENCE_APP_DIR "${CMAKE_CURRENT_SOURCE_DIR}/apps/RenderCadenceCompositor")
|
||||||
|
|
||||||
|
set(RENDER_CADENCE_APP_SOURCES
|
||||||
|
"${APP_DIR}/videoio/decklink/DeckLinkAPI_i.c"
|
||||||
|
"${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/Std140Buffer.h"
|
||||||
|
"${APP_DIR}/runtime/support/RuntimeJson.cpp"
|
||||||
|
"${APP_DIR}/runtime/support/RuntimeJson.h"
|
||||||
|
"${APP_DIR}/runtime/support/RuntimeParameterUtils.cpp"
|
||||||
|
"${APP_DIR}/runtime/support/RuntimeParameterUtils.h"
|
||||||
|
"${APP_DIR}/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}/videoio/VideoIOFormat.cpp"
|
||||||
|
"${APP_DIR}/videoio/VideoIOFormat.h"
|
||||||
|
"${APP_DIR}/videoio/VideoIOTypes.h"
|
||||||
|
"${APP_DIR}/videoio/VideoPlayoutPolicy.h"
|
||||||
|
"${APP_DIR}/videoio/VideoPlayoutScheduler.cpp"
|
||||||
|
"${APP_DIR}/videoio/VideoPlayoutScheduler.h"
|
||||||
|
"${RENDER_CADENCE_APP_DIR}/RenderCadenceCompositor.cpp"
|
||||||
|
"${RENDER_CADENCE_APP_DIR}/app/AppConfig.cpp"
|
||||||
|
"${RENDER_CADENCE_APP_DIR}/app/AppConfig.h"
|
||||||
|
"${RENDER_CADENCE_APP_DIR}/app/AppConfigProvider.cpp"
|
||||||
|
"${RENDER_CADENCE_APP_DIR}/app/AppConfigProvider.h"
|
||||||
|
"${RENDER_CADENCE_APP_DIR}/app/RenderCadenceApp.h"
|
||||||
|
"${RENDER_CADENCE_APP_DIR}/app/RuntimeLayerController.cpp"
|
||||||
|
"${RENDER_CADENCE_APP_DIR}/app/RuntimeLayerControllerBuild.cpp"
|
||||||
|
"${RENDER_CADENCE_APP_DIR}/app/RuntimeLayerControllerControls.cpp"
|
||||||
|
"${RENDER_CADENCE_APP_DIR}/app/RuntimeLayerController.h"
|
||||||
|
"${RENDER_CADENCE_APP_DIR}/control/ControlActionResult.h"
|
||||||
|
"${RENDER_CADENCE_APP_DIR}/control/RuntimeControlCommand.cpp"
|
||||||
|
"${RENDER_CADENCE_APP_DIR}/control/RuntimeControlCommand.h"
|
||||||
|
"${RENDER_CADENCE_APP_DIR}/control/http/HttpControlServer.cpp"
|
||||||
|
"${RENDER_CADENCE_APP_DIR}/control/http/HttpControlServer.h"
|
||||||
|
"${RENDER_CADENCE_APP_DIR}/control/http/HttpControlServerRoutes.cpp"
|
||||||
|
"${RENDER_CADENCE_APP_DIR}/control/http/HttpControlServerWebSocket.cpp"
|
||||||
|
"${RENDER_CADENCE_APP_DIR}/control/RuntimeStateJson.h"
|
||||||
|
"${RENDER_CADENCE_APP_DIR}/frames/InputFrameMailbox.cpp"
|
||||||
|
"${RENDER_CADENCE_APP_DIR}/frames/InputFrameMailbox.h"
|
||||||
|
"${RENDER_CADENCE_APP_DIR}/frames/SystemFrameExchange.cpp"
|
||||||
|
"${RENDER_CADENCE_APP_DIR}/frames/SystemFrameExchange.h"
|
||||||
|
"${RENDER_CADENCE_APP_DIR}/frames/SystemFrameTypes.h"
|
||||||
|
"${RENDER_CADENCE_APP_DIR}/json/JsonWriter.cpp"
|
||||||
|
"${RENDER_CADENCE_APP_DIR}/json/JsonWriter.h"
|
||||||
|
"${RENDER_CADENCE_APP_DIR}/logging/Logger.cpp"
|
||||||
|
"${RENDER_CADENCE_APP_DIR}/logging/Logger.h"
|
||||||
|
"${RENDER_CADENCE_APP_DIR}/platform/HiddenGlWindow.cpp"
|
||||||
|
"${RENDER_CADENCE_APP_DIR}/platform/HiddenGlWindow.h"
|
||||||
|
"${RENDER_CADENCE_APP_DIR}/render/InputFrameTexture.cpp"
|
||||||
|
"${RENDER_CADENCE_APP_DIR}/render/InputFrameTexture.h"
|
||||||
|
"${RENDER_CADENCE_APP_DIR}/render/readback/Bgra8ReadbackPipeline.cpp"
|
||||||
|
"${RENDER_CADENCE_APP_DIR}/render/readback/Bgra8ReadbackPipeline.h"
|
||||||
|
"${RENDER_CADENCE_APP_DIR}/render/readback/PboReadbackRing.cpp"
|
||||||
|
"${RENDER_CADENCE_APP_DIR}/render/readback/PboReadbackRing.h"
|
||||||
|
"${RENDER_CADENCE_APP_DIR}/render/RenderCadenceClock.cpp"
|
||||||
|
"${RENDER_CADENCE_APP_DIR}/render/RenderCadenceClock.h"
|
||||||
|
"${RENDER_CADENCE_APP_DIR}/render/RenderThread.cpp"
|
||||||
|
"${RENDER_CADENCE_APP_DIR}/render/RenderThread.h"
|
||||||
|
"${RENDER_CADENCE_APP_DIR}/render/runtime/RuntimeShaderRenderer.cpp"
|
||||||
|
"${RENDER_CADENCE_APP_DIR}/render/runtime/RuntimeShaderRenderer.h"
|
||||||
|
"${RENDER_CADENCE_APP_DIR}/render/runtime/RuntimeShaderParams.cpp"
|
||||||
|
"${RENDER_CADENCE_APP_DIR}/render/runtime/RuntimeShaderParams.h"
|
||||||
|
"${RENDER_CADENCE_APP_DIR}/render/runtime/RuntimeRenderScene.cpp"
|
||||||
|
"${RENDER_CADENCE_APP_DIR}/render/runtime/RuntimeRenderScene.h"
|
||||||
|
"${RENDER_CADENCE_APP_DIR}/render/runtime/RuntimeRenderSceneRender.cpp"
|
||||||
|
"${RENDER_CADENCE_APP_DIR}/render/runtime/RuntimeShaderPrepareWorker.cpp"
|
||||||
|
"${RENDER_CADENCE_APP_DIR}/render/runtime/RuntimeShaderPrepareWorker.h"
|
||||||
|
"${RENDER_CADENCE_APP_DIR}/render/runtime/RuntimeShaderProgram.h"
|
||||||
|
"${RENDER_CADENCE_APP_DIR}/render/SimpleMotionRenderer.cpp"
|
||||||
|
"${RENDER_CADENCE_APP_DIR}/render/SimpleMotionRenderer.h"
|
||||||
|
"${RENDER_CADENCE_APP_DIR}/runtime/RuntimeLayerModel.cpp"
|
||||||
|
"${RENDER_CADENCE_APP_DIR}/runtime/RuntimeLayerModel.h"
|
||||||
|
"${RENDER_CADENCE_APP_DIR}/runtime/RuntimeShaderBridge.cpp"
|
||||||
|
"${RENDER_CADENCE_APP_DIR}/runtime/RuntimeShaderBridge.h"
|
||||||
|
"${RENDER_CADENCE_APP_DIR}/runtime/RuntimeSlangShaderCompiler.cpp"
|
||||||
|
"${RENDER_CADENCE_APP_DIR}/runtime/RuntimeSlangShaderCompiler.h"
|
||||||
|
"${RENDER_CADENCE_APP_DIR}/runtime/SupportedShaderCatalog.cpp"
|
||||||
|
"${RENDER_CADENCE_APP_DIR}/runtime/SupportedShaderCatalog.h"
|
||||||
|
"${RENDER_CADENCE_APP_DIR}/telemetry/CadenceTelemetryJson.h"
|
||||||
|
"${RENDER_CADENCE_APP_DIR}/telemetry/CadenceTelemetry.h"
|
||||||
|
"${RENDER_CADENCE_APP_DIR}/telemetry/TelemetryHealthMonitor.h"
|
||||||
|
"${RENDER_CADENCE_APP_DIR}/video/DeckLinkInput.cpp"
|
||||||
|
"${RENDER_CADENCE_APP_DIR}/video/DeckLinkInput.h"
|
||||||
|
"${RENDER_CADENCE_APP_DIR}/video/DeckLinkInputThread.h"
|
||||||
|
"${RENDER_CADENCE_APP_DIR}/video/DeckLinkOutput.cpp"
|
||||||
|
"${RENDER_CADENCE_APP_DIR}/video/DeckLinkOutput.h"
|
||||||
|
"${RENDER_CADENCE_APP_DIR}/video/DeckLinkOutputThread.h"
|
||||||
|
)
|
||||||
|
|
||||||
|
add_executable(RenderCadenceCompositor ${RENDER_CADENCE_APP_SOURCES})
|
||||||
|
|
||||||
|
target_include_directories(RenderCadenceCompositor PRIVATE
|
||||||
|
"${APP_DIR}"
|
||||||
|
"${APP_DIR}/gl/renderer"
|
||||||
|
"${APP_DIR}/gl/shader"
|
||||||
|
"${APP_DIR}/platform"
|
||||||
|
"${APP_DIR}/runtime/support"
|
||||||
|
"${APP_DIR}/shader"
|
||||||
|
"${APP_DIR}/videoio"
|
||||||
|
"${APP_DIR}/videoio/decklink"
|
||||||
|
"${RENDER_CADENCE_APP_DIR}"
|
||||||
|
"${RENDER_CADENCE_APP_DIR}/app"
|
||||||
|
"${RENDER_CADENCE_APP_DIR}/control"
|
||||||
|
"${RENDER_CADENCE_APP_DIR}/control/http"
|
||||||
|
"${RENDER_CADENCE_APP_DIR}/frames"
|
||||||
|
"${RENDER_CADENCE_APP_DIR}/json"
|
||||||
|
"${RENDER_CADENCE_APP_DIR}/logging"
|
||||||
|
"${RENDER_CADENCE_APP_DIR}/platform"
|
||||||
|
"${RENDER_CADENCE_APP_DIR}/render"
|
||||||
|
"${RENDER_CADENCE_APP_DIR}/render/readback"
|
||||||
|
"${RENDER_CADENCE_APP_DIR}/render/runtime"
|
||||||
|
"${RENDER_CADENCE_APP_DIR}/runtime"
|
||||||
|
"${RENDER_CADENCE_APP_DIR}/telemetry"
|
||||||
|
"${RENDER_CADENCE_APP_DIR}/video"
|
||||||
|
)
|
||||||
|
|
||||||
|
target_link_libraries(RenderCadenceCompositor PRIVATE
|
||||||
|
opengl32
|
||||||
|
Ole32
|
||||||
|
Ws2_32
|
||||||
|
)
|
||||||
|
|
||||||
|
target_compile_definitions(RenderCadenceCompositor PRIVATE
|
||||||
|
_UNICODE
|
||||||
|
UNICODE
|
||||||
|
)
|
||||||
|
|
||||||
|
if(MSVC)
|
||||||
|
target_compile_options(RenderCadenceCompositor PRIVATE /W3)
|
||||||
|
endif()
|
||||||
|
|
||||||
add_executable(RuntimeJsonTests
|
add_executable(RuntimeJsonTests
|
||||||
"${APP_DIR}/runtime/support/RuntimeJson.cpp"
|
"${APP_DIR}/runtime/support/RuntimeJson.cpp"
|
||||||
"${CMAKE_CURRENT_SOURCE_DIR}/tests/RuntimeJsonTests.cpp"
|
"${CMAKE_CURRENT_SOURCE_DIR}/tests/RuntimeJsonTests.cpp"
|
||||||
@@ -345,6 +542,23 @@ endif()
|
|||||||
|
|
||||||
add_test(NAME RuntimeStateLayerModelTests COMMAND RuntimeStateLayerModelTests)
|
add_test(NAME RuntimeStateLayerModelTests COMMAND RuntimeStateLayerModelTests)
|
||||||
|
|
||||||
|
add_executable(PersistenceWriterTests
|
||||||
|
"${APP_DIR}/runtime/persistence/PersistenceWriter.cpp"
|
||||||
|
"${CMAKE_CURRENT_SOURCE_DIR}/tests/PersistenceWriterTests.cpp"
|
||||||
|
)
|
||||||
|
|
||||||
|
target_include_directories(PersistenceWriterTests PRIVATE
|
||||||
|
"${APP_DIR}"
|
||||||
|
"${APP_DIR}/runtime"
|
||||||
|
"${APP_DIR}/runtime/persistence"
|
||||||
|
)
|
||||||
|
|
||||||
|
if(MSVC)
|
||||||
|
target_compile_options(PersistenceWriterTests PRIVATE /W3)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
add_test(NAME PersistenceWriterTests COMMAND PersistenceWriterTests)
|
||||||
|
|
||||||
add_executable(RuntimeSubsystemTests
|
add_executable(RuntimeSubsystemTests
|
||||||
"${APP_DIR}/runtime/coordination/RuntimeCoordinator.cpp"
|
"${APP_DIR}/runtime/coordination/RuntimeCoordinator.cpp"
|
||||||
"${APP_DIR}/runtime/live/CommittedLiveState.cpp"
|
"${APP_DIR}/runtime/live/CommittedLiveState.cpp"
|
||||||
@@ -521,6 +735,337 @@ endif()
|
|||||||
|
|
||||||
add_test(NAME VideoPlayoutSchedulerTests COMMAND VideoPlayoutSchedulerTests)
|
add_test(NAME VideoPlayoutSchedulerTests COMMAND VideoPlayoutSchedulerTests)
|
||||||
|
|
||||||
|
add_executable(OutputProductionControllerTests
|
||||||
|
"${APP_DIR}/videoio/OutputProductionController.cpp"
|
||||||
|
"${CMAKE_CURRENT_SOURCE_DIR}/tests/OutputProductionControllerTests.cpp"
|
||||||
|
)
|
||||||
|
|
||||||
|
target_include_directories(OutputProductionControllerTests PRIVATE
|
||||||
|
"${APP_DIR}"
|
||||||
|
"${APP_DIR}/videoio"
|
||||||
|
)
|
||||||
|
|
||||||
|
if(MSVC)
|
||||||
|
target_compile_options(OutputProductionControllerTests PRIVATE /W3)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
add_test(NAME OutputProductionControllerTests COMMAND OutputProductionControllerTests)
|
||||||
|
|
||||||
|
add_executable(RenderOutputQueueTests
|
||||||
|
"${APP_DIR}/videoio/RenderOutputQueue.cpp"
|
||||||
|
"${CMAKE_CURRENT_SOURCE_DIR}/tests/RenderOutputQueueTests.cpp"
|
||||||
|
)
|
||||||
|
|
||||||
|
target_include_directories(RenderOutputQueueTests PRIVATE
|
||||||
|
"${APP_DIR}"
|
||||||
|
"${APP_DIR}/videoio"
|
||||||
|
"${APP_DIR}/videoio/decklink"
|
||||||
|
)
|
||||||
|
|
||||||
|
if(MSVC)
|
||||||
|
target_compile_options(RenderOutputQueueTests PRIVATE /W3)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
add_test(NAME RenderOutputQueueTests COMMAND RenderOutputQueueTests)
|
||||||
|
|
||||||
|
add_executable(RenderCadenceControllerTests
|
||||||
|
"${APP_DIR}/videoio/RenderCadenceController.cpp"
|
||||||
|
"${CMAKE_CURRENT_SOURCE_DIR}/tests/RenderCadenceControllerTests.cpp"
|
||||||
|
)
|
||||||
|
|
||||||
|
target_include_directories(RenderCadenceControllerTests PRIVATE
|
||||||
|
"${APP_DIR}"
|
||||||
|
"${APP_DIR}/videoio"
|
||||||
|
)
|
||||||
|
|
||||||
|
if(MSVC)
|
||||||
|
target_compile_options(RenderCadenceControllerTests PRIVATE /W3)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
add_test(NAME RenderCadenceControllerTests COMMAND RenderCadenceControllerTests)
|
||||||
|
|
||||||
|
add_executable(RenderCadenceCompositorFrameExchangeTests
|
||||||
|
"${APP_DIR}/videoio/VideoIOFormat.cpp"
|
||||||
|
"${RENDER_CADENCE_APP_DIR}/frames/SystemFrameExchange.cpp"
|
||||||
|
"${CMAKE_CURRENT_SOURCE_DIR}/tests/RenderCadenceCompositorFrameExchangeTests.cpp"
|
||||||
|
)
|
||||||
|
|
||||||
|
target_include_directories(RenderCadenceCompositorFrameExchangeTests PRIVATE
|
||||||
|
"${APP_DIR}/videoio"
|
||||||
|
"${RENDER_CADENCE_APP_DIR}/frames"
|
||||||
|
)
|
||||||
|
|
||||||
|
if(MSVC)
|
||||||
|
target_compile_options(RenderCadenceCompositorFrameExchangeTests PRIVATE /W3)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
add_test(NAME RenderCadenceCompositorFrameExchangeTests COMMAND RenderCadenceCompositorFrameExchangeTests)
|
||||||
|
|
||||||
|
add_executable(RenderCadenceCompositorInputFrameMailboxTests
|
||||||
|
"${APP_DIR}/videoio/VideoIOFormat.cpp"
|
||||||
|
"${RENDER_CADENCE_APP_DIR}/frames/InputFrameMailbox.cpp"
|
||||||
|
"${CMAKE_CURRENT_SOURCE_DIR}/tests/RenderCadenceCompositorInputFrameMailboxTests.cpp"
|
||||||
|
)
|
||||||
|
|
||||||
|
target_include_directories(RenderCadenceCompositorInputFrameMailboxTests PRIVATE
|
||||||
|
"${APP_DIR}/videoio"
|
||||||
|
"${RENDER_CADENCE_APP_DIR}/frames"
|
||||||
|
)
|
||||||
|
|
||||||
|
if(MSVC)
|
||||||
|
target_compile_options(RenderCadenceCompositorInputFrameMailboxTests PRIVATE /W3)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
add_test(NAME RenderCadenceCompositorInputFrameMailboxTests COMMAND RenderCadenceCompositorInputFrameMailboxTests)
|
||||||
|
|
||||||
|
add_executable(RenderCadenceCompositorClockTests
|
||||||
|
"${RENDER_CADENCE_APP_DIR}/render/RenderCadenceClock.cpp"
|
||||||
|
"${CMAKE_CURRENT_SOURCE_DIR}/tests/RenderCadenceCompositorClockTests.cpp"
|
||||||
|
)
|
||||||
|
|
||||||
|
target_include_directories(RenderCadenceCompositorClockTests PRIVATE
|
||||||
|
"${RENDER_CADENCE_APP_DIR}/render"
|
||||||
|
)
|
||||||
|
|
||||||
|
if(MSVC)
|
||||||
|
target_compile_options(RenderCadenceCompositorClockTests PRIVATE /W3)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
add_test(NAME RenderCadenceCompositorClockTests COMMAND RenderCadenceCompositorClockTests)
|
||||||
|
|
||||||
|
add_executable(RenderCadenceCompositorTelemetryTests
|
||||||
|
"${RENDER_CADENCE_APP_DIR}/json/JsonWriter.cpp"
|
||||||
|
"${CMAKE_CURRENT_SOURCE_DIR}/tests/RenderCadenceCompositorTelemetryTests.cpp"
|
||||||
|
)
|
||||||
|
|
||||||
|
target_include_directories(RenderCadenceCompositorTelemetryTests PRIVATE
|
||||||
|
"${RENDER_CADENCE_APP_DIR}/json"
|
||||||
|
"${RENDER_CADENCE_APP_DIR}/telemetry"
|
||||||
|
)
|
||||||
|
|
||||||
|
if(MSVC)
|
||||||
|
target_compile_options(RenderCadenceCompositorTelemetryTests PRIVATE /W3)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
add_test(NAME RenderCadenceCompositorTelemetryTests COMMAND RenderCadenceCompositorTelemetryTests)
|
||||||
|
|
||||||
|
add_executable(RenderCadenceCompositorRuntimeShaderParamsTests
|
||||||
|
"${RENDER_CADENCE_APP_DIR}/render/runtime/RuntimeShaderParams.cpp"
|
||||||
|
"${CMAKE_CURRENT_SOURCE_DIR}/tests/RenderCadenceCompositorRuntimeShaderParamsTests.cpp"
|
||||||
|
)
|
||||||
|
|
||||||
|
target_include_directories(RenderCadenceCompositorRuntimeShaderParamsTests PRIVATE
|
||||||
|
"${APP_DIR}/gl/shader"
|
||||||
|
"${APP_DIR}/shader"
|
||||||
|
"${RENDER_CADENCE_APP_DIR}/render"
|
||||||
|
"${RENDER_CADENCE_APP_DIR}/render/runtime"
|
||||||
|
"${RENDER_CADENCE_APP_DIR}/runtime"
|
||||||
|
)
|
||||||
|
|
||||||
|
if(MSVC)
|
||||||
|
target_compile_options(RenderCadenceCompositorRuntimeShaderParamsTests PRIVATE /W3)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
add_test(NAME RenderCadenceCompositorRuntimeShaderParamsTests COMMAND RenderCadenceCompositorRuntimeShaderParamsTests)
|
||||||
|
|
||||||
|
add_executable(RenderCadenceCompositorRuntimeLayerModelTests
|
||||||
|
"${RENDER_CADENCE_APP_DIR}/runtime/RuntimeLayerModel.cpp"
|
||||||
|
"${RENDER_CADENCE_APP_DIR}/runtime/SupportedShaderCatalog.cpp"
|
||||||
|
"${APP_DIR}/shader/ShaderPackageRegistry.cpp"
|
||||||
|
"${APP_DIR}/runtime/support/RuntimeJson.cpp"
|
||||||
|
"${APP_DIR}/runtime/support/RuntimeParameterUtils.cpp"
|
||||||
|
"${CMAKE_CURRENT_SOURCE_DIR}/tests/RenderCadenceCompositorRuntimeLayerModelTests.cpp"
|
||||||
|
)
|
||||||
|
|
||||||
|
target_include_directories(RenderCadenceCompositorRuntimeLayerModelTests PRIVATE
|
||||||
|
"${APP_DIR}"
|
||||||
|
"${APP_DIR}/runtime/support"
|
||||||
|
"${APP_DIR}/shader"
|
||||||
|
"${RENDER_CADENCE_APP_DIR}/runtime"
|
||||||
|
)
|
||||||
|
|
||||||
|
if(MSVC)
|
||||||
|
target_compile_options(RenderCadenceCompositorRuntimeLayerModelTests PRIVATE /W3)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
add_test(NAME RenderCadenceCompositorRuntimeLayerModelTests COMMAND RenderCadenceCompositorRuntimeLayerModelTests)
|
||||||
|
|
||||||
|
add_executable(RenderCadenceCompositorSupportedShaderCatalogTests
|
||||||
|
"${APP_DIR}/shader/ShaderPackageRegistry.cpp"
|
||||||
|
"${APP_DIR}/runtime/support/RuntimeJson.cpp"
|
||||||
|
"${RENDER_CADENCE_APP_DIR}/runtime/SupportedShaderCatalog.cpp"
|
||||||
|
"${CMAKE_CURRENT_SOURCE_DIR}/tests/RenderCadenceCompositorSupportedShaderCatalogTests.cpp"
|
||||||
|
)
|
||||||
|
|
||||||
|
target_include_directories(RenderCadenceCompositorSupportedShaderCatalogTests PRIVATE
|
||||||
|
"${APP_DIR}"
|
||||||
|
"${APP_DIR}/runtime/support"
|
||||||
|
"${APP_DIR}/shader"
|
||||||
|
"${RENDER_CADENCE_APP_DIR}/runtime"
|
||||||
|
)
|
||||||
|
|
||||||
|
if(MSVC)
|
||||||
|
target_compile_options(RenderCadenceCompositorSupportedShaderCatalogTests PRIVATE /W3)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
add_test(NAME RenderCadenceCompositorSupportedShaderCatalogTests COMMAND RenderCadenceCompositorSupportedShaderCatalogTests)
|
||||||
|
|
||||||
|
add_executable(RenderCadenceCompositorLoggerTests
|
||||||
|
"${RENDER_CADENCE_APP_DIR}/logging/Logger.cpp"
|
||||||
|
"${CMAKE_CURRENT_SOURCE_DIR}/tests/RenderCadenceCompositorLoggerTests.cpp"
|
||||||
|
)
|
||||||
|
|
||||||
|
target_include_directories(RenderCadenceCompositorLoggerTests PRIVATE
|
||||||
|
"${RENDER_CADENCE_APP_DIR}/logging"
|
||||||
|
)
|
||||||
|
|
||||||
|
if(MSVC)
|
||||||
|
target_compile_options(RenderCadenceCompositorLoggerTests PRIVATE /W3)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
add_test(NAME RenderCadenceCompositorLoggerTests COMMAND RenderCadenceCompositorLoggerTests)
|
||||||
|
|
||||||
|
add_executable(RenderCadenceCompositorJsonWriterTests
|
||||||
|
"${RENDER_CADENCE_APP_DIR}/json/JsonWriter.cpp"
|
||||||
|
"${CMAKE_CURRENT_SOURCE_DIR}/tests/RenderCadenceCompositorJsonWriterTests.cpp"
|
||||||
|
)
|
||||||
|
|
||||||
|
target_include_directories(RenderCadenceCompositorJsonWriterTests PRIVATE
|
||||||
|
"${RENDER_CADENCE_APP_DIR}/json"
|
||||||
|
)
|
||||||
|
|
||||||
|
if(MSVC)
|
||||||
|
target_compile_options(RenderCadenceCompositorJsonWriterTests PRIVATE /W3)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
add_test(NAME RenderCadenceCompositorJsonWriterTests COMMAND RenderCadenceCompositorJsonWriterTests)
|
||||||
|
|
||||||
|
add_executable(RenderCadenceCompositorRuntimeStateJsonTests
|
||||||
|
"${APP_DIR}/runtime/support/RuntimeJson.cpp"
|
||||||
|
"${APP_DIR}/runtime/support/RuntimeParameterUtils.cpp"
|
||||||
|
"${APP_DIR}/shader/ShaderPackageRegistry.cpp"
|
||||||
|
"${RENDER_CADENCE_APP_DIR}/app/AppConfig.cpp"
|
||||||
|
"${RENDER_CADENCE_APP_DIR}/app/AppConfigProvider.cpp"
|
||||||
|
"${RENDER_CADENCE_APP_DIR}/json/JsonWriter.cpp"
|
||||||
|
"${RENDER_CADENCE_APP_DIR}/runtime/RuntimeLayerModel.cpp"
|
||||||
|
"${RENDER_CADENCE_APP_DIR}/runtime/SupportedShaderCatalog.cpp"
|
||||||
|
"${CMAKE_CURRENT_SOURCE_DIR}/tests/RenderCadenceCompositorRuntimeStateJsonTests.cpp"
|
||||||
|
)
|
||||||
|
|
||||||
|
target_include_directories(RenderCadenceCompositorRuntimeStateJsonTests PRIVATE
|
||||||
|
"${APP_DIR}"
|
||||||
|
"${APP_DIR}/runtime/support"
|
||||||
|
"${APP_DIR}/shader"
|
||||||
|
"${APP_DIR}/videoio"
|
||||||
|
"${APP_DIR}/videoio/decklink"
|
||||||
|
"${RENDER_CADENCE_APP_DIR}/app"
|
||||||
|
"${RENDER_CADENCE_APP_DIR}/control"
|
||||||
|
"${RENDER_CADENCE_APP_DIR}/json"
|
||||||
|
"${RENDER_CADENCE_APP_DIR}/logging"
|
||||||
|
"${RENDER_CADENCE_APP_DIR}/runtime"
|
||||||
|
"${RENDER_CADENCE_APP_DIR}/telemetry"
|
||||||
|
"${RENDER_CADENCE_APP_DIR}/video"
|
||||||
|
)
|
||||||
|
|
||||||
|
if(MSVC)
|
||||||
|
target_compile_options(RenderCadenceCompositorRuntimeStateJsonTests PRIVATE /W3)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
add_test(NAME RenderCadenceCompositorRuntimeStateJsonTests COMMAND RenderCadenceCompositorRuntimeStateJsonTests)
|
||||||
|
|
||||||
|
add_executable(RenderCadenceCompositorHttpControlServerTests
|
||||||
|
"${APP_DIR}/runtime/support/RuntimeJson.cpp"
|
||||||
|
"${RENDER_CADENCE_APP_DIR}/control/RuntimeControlCommand.cpp"
|
||||||
|
"${RENDER_CADENCE_APP_DIR}/control/http/HttpControlServer.cpp"
|
||||||
|
"${RENDER_CADENCE_APP_DIR}/control/http/HttpControlServerRoutes.cpp"
|
||||||
|
"${RENDER_CADENCE_APP_DIR}/control/http/HttpControlServerWebSocket.cpp"
|
||||||
|
"${RENDER_CADENCE_APP_DIR}/json/JsonWriter.cpp"
|
||||||
|
"${RENDER_CADENCE_APP_DIR}/logging/Logger.cpp"
|
||||||
|
"${CMAKE_CURRENT_SOURCE_DIR}/tests/RenderCadenceCompositorHttpControlServerTests.cpp"
|
||||||
|
)
|
||||||
|
|
||||||
|
target_include_directories(RenderCadenceCompositorHttpControlServerTests PRIVATE
|
||||||
|
"${APP_DIR}"
|
||||||
|
"${APP_DIR}/runtime/support"
|
||||||
|
"${RENDER_CADENCE_APP_DIR}/control"
|
||||||
|
"${RENDER_CADENCE_APP_DIR}/control/http"
|
||||||
|
"${RENDER_CADENCE_APP_DIR}/json"
|
||||||
|
"${RENDER_CADENCE_APP_DIR}/logging"
|
||||||
|
)
|
||||||
|
|
||||||
|
target_link_libraries(RenderCadenceCompositorHttpControlServerTests PRIVATE
|
||||||
|
Ws2_32
|
||||||
|
)
|
||||||
|
|
||||||
|
if(MSVC)
|
||||||
|
target_compile_options(RenderCadenceCompositorHttpControlServerTests PRIVATE /W3)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
add_test(NAME RenderCadenceCompositorHttpControlServerTests COMMAND RenderCadenceCompositorHttpControlServerTests)
|
||||||
|
|
||||||
|
add_executable(RenderCadenceCompositorAppConfigProviderTests
|
||||||
|
"${APP_DIR}/runtime/support/RuntimeJson.cpp"
|
||||||
|
"${RENDER_CADENCE_APP_DIR}/app/AppConfig.cpp"
|
||||||
|
"${RENDER_CADENCE_APP_DIR}/app/AppConfigProvider.cpp"
|
||||||
|
"${CMAKE_CURRENT_SOURCE_DIR}/tests/RenderCadenceCompositorAppConfigProviderTests.cpp"
|
||||||
|
)
|
||||||
|
|
||||||
|
target_include_directories(RenderCadenceCompositorAppConfigProviderTests PRIVATE
|
||||||
|
"${APP_DIR}"
|
||||||
|
"${APP_DIR}/runtime/support"
|
||||||
|
"${RENDER_CADENCE_APP_DIR}/app"
|
||||||
|
"${RENDER_CADENCE_APP_DIR}/control"
|
||||||
|
"${RENDER_CADENCE_APP_DIR}/logging"
|
||||||
|
"${RENDER_CADENCE_APP_DIR}/telemetry"
|
||||||
|
"${RENDER_CADENCE_APP_DIR}/video"
|
||||||
|
"${APP_DIR}/videoio"
|
||||||
|
"${APP_DIR}/videoio/decklink"
|
||||||
|
)
|
||||||
|
|
||||||
|
target_link_libraries(RenderCadenceCompositorAppConfigProviderTests PRIVATE
|
||||||
|
Ws2_32
|
||||||
|
)
|
||||||
|
|
||||||
|
if(MSVC)
|
||||||
|
target_compile_options(RenderCadenceCompositorAppConfigProviderTests PRIVATE /W3)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
add_test(NAME RenderCadenceCompositorAppConfigProviderTests COMMAND RenderCadenceCompositorAppConfigProviderTests)
|
||||||
|
|
||||||
|
add_executable(SystemOutputFramePoolTests
|
||||||
|
"${APP_DIR}/videoio/SystemOutputFramePool.cpp"
|
||||||
|
"${APP_DIR}/videoio/VideoIOFormat.cpp"
|
||||||
|
"${CMAKE_CURRENT_SOURCE_DIR}/tests/SystemOutputFramePoolTests.cpp"
|
||||||
|
)
|
||||||
|
|
||||||
|
target_include_directories(SystemOutputFramePoolTests PRIVATE
|
||||||
|
"${APP_DIR}"
|
||||||
|
"${APP_DIR}/videoio"
|
||||||
|
"${APP_DIR}/videoio/decklink"
|
||||||
|
)
|
||||||
|
|
||||||
|
if(MSVC)
|
||||||
|
target_compile_options(SystemOutputFramePoolTests PRIVATE /W3)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
add_test(NAME SystemOutputFramePoolTests COMMAND SystemOutputFramePoolTests)
|
||||||
|
|
||||||
|
add_executable(VideoBackendLifecycleTests
|
||||||
|
"${APP_DIR}/videoio/VideoBackendLifecycle.cpp"
|
||||||
|
"${CMAKE_CURRENT_SOURCE_DIR}/tests/VideoBackendLifecycleTests.cpp"
|
||||||
|
)
|
||||||
|
|
||||||
|
target_include_directories(VideoBackendLifecycleTests PRIVATE
|
||||||
|
"${APP_DIR}"
|
||||||
|
"${APP_DIR}/videoio"
|
||||||
|
)
|
||||||
|
|
||||||
|
if(MSVC)
|
||||||
|
target_compile_options(VideoBackendLifecycleTests PRIVATE /W3)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
add_test(NAME VideoBackendLifecycleTests COMMAND VideoBackendLifecycleTests)
|
||||||
|
|
||||||
add_executable(VideoIODeviceFakeTests
|
add_executable(VideoIODeviceFakeTests
|
||||||
"${APP_DIR}/videoio/VideoIOFormat.cpp"
|
"${APP_DIR}/videoio/VideoIOFormat.cpp"
|
||||||
"${CMAKE_CURRENT_SOURCE_DIR}/tests/VideoIODeviceFakeTests.cpp"
|
"${CMAKE_CURRENT_SOURCE_DIR}/tests/VideoIODeviceFakeTests.cpp"
|
||||||
|
|||||||
920
apps/DeckLinkRenderCadenceProbe/DeckLinkRenderCadenceProbe.cpp
Normal file
920
apps/DeckLinkRenderCadenceProbe/DeckLinkRenderCadenceProbe.cpp
Normal file
@@ -0,0 +1,920 @@
|
|||||||
|
#include "DeckLinkSession.h"
|
||||||
|
#include "GLExtensions.h"
|
||||||
|
#include "VideoIOFormat.h"
|
||||||
|
#include "VideoPlayoutPolicy.h"
|
||||||
|
|
||||||
|
#include <windows.h>
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <atomic>
|
||||||
|
#include <chrono>
|
||||||
|
#include <cmath>
|
||||||
|
#include <condition_variable>
|
||||||
|
#include <cstdint>
|
||||||
|
#include <deque>
|
||||||
|
#include <iomanip>
|
||||||
|
#include <iostream>
|
||||||
|
#include <mutex>
|
||||||
|
#include <sstream>
|
||||||
|
#include <string>
|
||||||
|
#include <thread>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
constexpr unsigned kDefaultWidth = 1920;
|
||||||
|
constexpr unsigned kDefaultHeight = 1080;
|
||||||
|
constexpr std::size_t kSystemFrameSlots = 12;
|
||||||
|
constexpr std::size_t kPboDepth = 6;
|
||||||
|
constexpr std::size_t kWarmupFrames = 4;
|
||||||
|
constexpr std::size_t kDeckLinkTargetBufferedFrames = 4;
|
||||||
|
|
||||||
|
enum class ProbeSlotState
|
||||||
|
{
|
||||||
|
Free,
|
||||||
|
Rendering,
|
||||||
|
Completed,
|
||||||
|
Scheduled
|
||||||
|
};
|
||||||
|
|
||||||
|
struct ProbeFrame
|
||||||
|
{
|
||||||
|
void* bytes = nullptr;
|
||||||
|
long rowBytes = 0;
|
||||||
|
unsigned width = 0;
|
||||||
|
unsigned height = 0;
|
||||||
|
VideoIOPixelFormat pixelFormat = VideoIOPixelFormat::Bgra8;
|
||||||
|
std::size_t index = 0;
|
||||||
|
uint64_t generation = 0;
|
||||||
|
uint64_t frameIndex = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct ProbeMetrics
|
||||||
|
{
|
||||||
|
uint64_t renderedFrames = 0;
|
||||||
|
uint64_t completedFrames = 0;
|
||||||
|
uint64_t scheduledFrames = 0;
|
||||||
|
uint64_t completedDrops = 0;
|
||||||
|
uint64_t acquireMisses = 0;
|
||||||
|
uint64_t scheduleUnderruns = 0;
|
||||||
|
uint64_t pboQueueMisses = 0;
|
||||||
|
std::size_t freeCount = 0;
|
||||||
|
std::size_t renderingCount = 0;
|
||||||
|
std::size_t completedCount = 0;
|
||||||
|
std::size_t scheduledCount = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
class LatestFrameStore
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
LatestFrameStore(unsigned width, unsigned height, std::size_t capacity) :
|
||||||
|
mWidth(width),
|
||||||
|
mHeight(height),
|
||||||
|
mRowBytes(VideoIORowBytes(VideoIOPixelFormat::Bgra8, width))
|
||||||
|
{
|
||||||
|
mSlots.resize(capacity);
|
||||||
|
const std::size_t byteCount = static_cast<std::size_t>(mRowBytes) * static_cast<std::size_t>(mHeight);
|
||||||
|
for (Slot& slot : mSlots)
|
||||||
|
{
|
||||||
|
slot.bytes.resize(byteCount);
|
||||||
|
slot.generation = 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool AcquireForRender(ProbeFrame& frame)
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(mMutex);
|
||||||
|
if (!AcquireFreeLocked(frame))
|
||||||
|
{
|
||||||
|
if (!DropOldestCompletedLocked() || !AcquireFreeLocked(frame))
|
||||||
|
{
|
||||||
|
++mMetrics.acquireMisses;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool PublishCompleted(const ProbeFrame& frame)
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(mMutex);
|
||||||
|
if (!IsValidLocked(frame))
|
||||||
|
return false;
|
||||||
|
Slot& slot = mSlots[frame.index];
|
||||||
|
if (slot.state != ProbeSlotState::Rendering)
|
||||||
|
return false;
|
||||||
|
slot.state = ProbeSlotState::Completed;
|
||||||
|
slot.frameIndex = frame.frameIndex;
|
||||||
|
mCompletedIndices.push_back(frame.index);
|
||||||
|
++mMetrics.completedFrames;
|
||||||
|
mCondition.notify_all();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ConsumeCompleted(ProbeFrame& frame)
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(mMutex);
|
||||||
|
while (!mCompletedIndices.empty())
|
||||||
|
{
|
||||||
|
const std::size_t index = mCompletedIndices.front();
|
||||||
|
mCompletedIndices.pop_front();
|
||||||
|
if (index >= mSlots.size() || mSlots[index].state != ProbeSlotState::Completed)
|
||||||
|
continue;
|
||||||
|
mSlots[index].state = ProbeSlotState::Scheduled;
|
||||||
|
FillFrameLocked(index, frame);
|
||||||
|
++mMetrics.scheduledFrames;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
++mMetrics.scheduleUnderruns;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ReleaseByBytes(void* bytes)
|
||||||
|
{
|
||||||
|
if (bytes == nullptr)
|
||||||
|
return false;
|
||||||
|
std::lock_guard<std::mutex> lock(mMutex);
|
||||||
|
for (std::size_t index = 0; index < mSlots.size(); ++index)
|
||||||
|
{
|
||||||
|
if (mSlots[index].bytes.data() != bytes)
|
||||||
|
continue;
|
||||||
|
mSlots[index].state = ProbeSlotState::Free;
|
||||||
|
++mSlots[index].generation;
|
||||||
|
RemoveCompletedIndexLocked(index);
|
||||||
|
mCondition.notify_all();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool WaitForCompletedDepth(std::size_t targetDepth, std::chrono::milliseconds timeout)
|
||||||
|
{
|
||||||
|
std::unique_lock<std::mutex> lock(mMutex);
|
||||||
|
return mCondition.wait_for(lock, timeout, [&]() {
|
||||||
|
return CompletedCountLocked() >= targetDepth;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
ProbeMetrics Metrics() const
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(mMutex);
|
||||||
|
ProbeMetrics metrics = mMetrics;
|
||||||
|
for (const Slot& slot : mSlots)
|
||||||
|
{
|
||||||
|
switch (slot.state)
|
||||||
|
{
|
||||||
|
case ProbeSlotState::Free:
|
||||||
|
++metrics.freeCount;
|
||||||
|
break;
|
||||||
|
case ProbeSlotState::Rendering:
|
||||||
|
++metrics.renderingCount;
|
||||||
|
break;
|
||||||
|
case ProbeSlotState::Completed:
|
||||||
|
++metrics.completedCount;
|
||||||
|
break;
|
||||||
|
case ProbeSlotState::Scheduled:
|
||||||
|
++metrics.scheduledCount;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return metrics;
|
||||||
|
}
|
||||||
|
|
||||||
|
void CountRenderedFrame()
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(mMutex);
|
||||||
|
++mMetrics.renderedFrames;
|
||||||
|
}
|
||||||
|
|
||||||
|
void CountPboQueueMiss()
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(mMutex);
|
||||||
|
++mMetrics.pboQueueMisses;
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
struct Slot
|
||||||
|
{
|
||||||
|
std::vector<unsigned char> bytes;
|
||||||
|
ProbeSlotState state = ProbeSlotState::Free;
|
||||||
|
uint64_t generation = 1;
|
||||||
|
uint64_t frameIndex = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
bool AcquireFreeLocked(ProbeFrame& frame)
|
||||||
|
{
|
||||||
|
for (std::size_t index = 0; index < mSlots.size(); ++index)
|
||||||
|
{
|
||||||
|
if (mSlots[index].state != ProbeSlotState::Free)
|
||||||
|
continue;
|
||||||
|
mSlots[index].state = ProbeSlotState::Rendering;
|
||||||
|
++mSlots[index].generation;
|
||||||
|
FillFrameLocked(index, frame);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool DropOldestCompletedLocked()
|
||||||
|
{
|
||||||
|
while (!mCompletedIndices.empty())
|
||||||
|
{
|
||||||
|
const std::size_t index = mCompletedIndices.front();
|
||||||
|
mCompletedIndices.pop_front();
|
||||||
|
if (index >= mSlots.size() || mSlots[index].state != ProbeSlotState::Completed)
|
||||||
|
continue;
|
||||||
|
mSlots[index].state = ProbeSlotState::Free;
|
||||||
|
++mSlots[index].generation;
|
||||||
|
++mMetrics.completedDrops;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void FillFrameLocked(std::size_t index, ProbeFrame& frame) const
|
||||||
|
{
|
||||||
|
const Slot& slot = mSlots[index];
|
||||||
|
frame.bytes = const_cast<unsigned char*>(slot.bytes.data());
|
||||||
|
frame.rowBytes = static_cast<long>(mRowBytes);
|
||||||
|
frame.width = mWidth;
|
||||||
|
frame.height = mHeight;
|
||||||
|
frame.pixelFormat = VideoIOPixelFormat::Bgra8;
|
||||||
|
frame.index = index;
|
||||||
|
frame.generation = slot.generation;
|
||||||
|
frame.frameIndex = slot.frameIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool IsValidLocked(const ProbeFrame& frame) const
|
||||||
|
{
|
||||||
|
return frame.index < mSlots.size() && mSlots[frame.index].generation == frame.generation;
|
||||||
|
}
|
||||||
|
|
||||||
|
void RemoveCompletedIndexLocked(std::size_t index)
|
||||||
|
{
|
||||||
|
mCompletedIndices.erase(std::remove(mCompletedIndices.begin(), mCompletedIndices.end(), index), mCompletedIndices.end());
|
||||||
|
}
|
||||||
|
|
||||||
|
std::size_t CompletedCountLocked() const
|
||||||
|
{
|
||||||
|
std::size_t count = 0;
|
||||||
|
for (const Slot& slot : mSlots)
|
||||||
|
{
|
||||||
|
if (slot.state == ProbeSlotState::Completed)
|
||||||
|
++count;
|
||||||
|
}
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
|
unsigned mWidth = 0;
|
||||||
|
unsigned mHeight = 0;
|
||||||
|
unsigned mRowBytes = 0;
|
||||||
|
std::vector<Slot> mSlots;
|
||||||
|
std::deque<std::size_t> mCompletedIndices;
|
||||||
|
mutable std::mutex mMutex;
|
||||||
|
std::condition_variable mCondition;
|
||||||
|
ProbeMetrics mMetrics;
|
||||||
|
};
|
||||||
|
|
||||||
|
LRESULT CALLBACK ProbeWindowProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)
|
||||||
|
{
|
||||||
|
return DefWindowProc(hwnd, message, wParam, lParam);
|
||||||
|
}
|
||||||
|
|
||||||
|
class HiddenOpenGLContext
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
~HiddenOpenGLContext()
|
||||||
|
{
|
||||||
|
Destroy();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Create(unsigned width, unsigned height, std::string& error)
|
||||||
|
{
|
||||||
|
mInstance = GetModuleHandle(nullptr);
|
||||||
|
WNDCLASSA wc = {};
|
||||||
|
wc.style = CS_OWNDC;
|
||||||
|
wc.lpfnWndProc = ProbeWindowProc;
|
||||||
|
wc.hInstance = mInstance;
|
||||||
|
wc.lpszClassName = "DeckLinkRenderCadenceProbeWindow";
|
||||||
|
RegisterClassA(&wc);
|
||||||
|
|
||||||
|
mWindow = CreateWindowA(
|
||||||
|
wc.lpszClassName,
|
||||||
|
"DeckLink Render Cadence Probe",
|
||||||
|
WS_OVERLAPPEDWINDOW,
|
||||||
|
CW_USEDEFAULT,
|
||||||
|
CW_USEDEFAULT,
|
||||||
|
static_cast<int>(width),
|
||||||
|
static_cast<int>(height),
|
||||||
|
nullptr,
|
||||||
|
nullptr,
|
||||||
|
mInstance,
|
||||||
|
nullptr);
|
||||||
|
if (!mWindow)
|
||||||
|
{
|
||||||
|
error = "CreateWindowA failed.";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
mDc = GetDC(mWindow);
|
||||||
|
if (!mDc)
|
||||||
|
{
|
||||||
|
error = "GetDC failed.";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
PIXELFORMATDESCRIPTOR pfd = {};
|
||||||
|
pfd.nSize = sizeof(pfd);
|
||||||
|
pfd.nVersion = 1;
|
||||||
|
pfd.dwFlags = PFD_DRAW_TO_WINDOW | PFD_SUPPORT_OPENGL | PFD_DOUBLEBUFFER;
|
||||||
|
pfd.iPixelType = PFD_TYPE_RGBA;
|
||||||
|
pfd.cColorBits = 32;
|
||||||
|
pfd.cDepthBits = 0;
|
||||||
|
pfd.iLayerType = PFD_MAIN_PLANE;
|
||||||
|
|
||||||
|
const int pixelFormat = ChoosePixelFormat(mDc, &pfd);
|
||||||
|
if (pixelFormat == 0 || !SetPixelFormat(mDc, pixelFormat, &pfd))
|
||||||
|
{
|
||||||
|
error = "Could not choose/set a pixel format.";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
mGlrc = wglCreateContext(mDc);
|
||||||
|
if (!mGlrc)
|
||||||
|
{
|
||||||
|
error = "wglCreateContext failed.";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool MakeCurrent()
|
||||||
|
{
|
||||||
|
return mDc && mGlrc && wglMakeCurrent(mDc, mGlrc);
|
||||||
|
}
|
||||||
|
|
||||||
|
void ClearCurrent()
|
||||||
|
{
|
||||||
|
wglMakeCurrent(nullptr, nullptr);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Destroy()
|
||||||
|
{
|
||||||
|
ClearCurrent();
|
||||||
|
if (mGlrc)
|
||||||
|
{
|
||||||
|
wglDeleteContext(mGlrc);
|
||||||
|
mGlrc = nullptr;
|
||||||
|
}
|
||||||
|
if (mWindow && mDc)
|
||||||
|
{
|
||||||
|
ReleaseDC(mWindow, mDc);
|
||||||
|
mDc = nullptr;
|
||||||
|
}
|
||||||
|
if (mWindow)
|
||||||
|
{
|
||||||
|
DestroyWindow(mWindow);
|
||||||
|
mWindow = nullptr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
HINSTANCE mInstance = nullptr;
|
||||||
|
HWND mWindow = nullptr;
|
||||||
|
HDC mDc = nullptr;
|
||||||
|
HGLRC mGlrc = nullptr;
|
||||||
|
};
|
||||||
|
|
||||||
|
class RenderCadenceProbe
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
RenderCadenceProbe(LatestFrameStore& frameStore, unsigned width, unsigned height, double frameDurationMs) :
|
||||||
|
mFrameStore(frameStore),
|
||||||
|
mWidth(width),
|
||||||
|
mHeight(height),
|
||||||
|
mFrameDuration(std::chrono::duration_cast<Clock::duration>(std::chrono::duration<double, std::milli>(frameDurationMs)))
|
||||||
|
{
|
||||||
|
if (mFrameDuration <= Clock::duration::zero())
|
||||||
|
mFrameDuration = std::chrono::milliseconds(16);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Start(std::string& error)
|
||||||
|
{
|
||||||
|
mStopping = false;
|
||||||
|
mThread = std::thread([this]() { ThreadMain(); });
|
||||||
|
std::unique_lock<std::mutex> lock(mStartupMutex);
|
||||||
|
if (!mStartupCondition.wait_for(lock, std::chrono::seconds(3), [this]() { return mStarted || !mStartupError.empty(); }))
|
||||||
|
{
|
||||||
|
error = "Timed out starting render thread.";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!mStartupError.empty())
|
||||||
|
{
|
||||||
|
error = mStartupError;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Stop()
|
||||||
|
{
|
||||||
|
mStopping = true;
|
||||||
|
if (mThread.joinable())
|
||||||
|
mThread.join();
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
struct PboSlot
|
||||||
|
{
|
||||||
|
GLuint pbo = 0;
|
||||||
|
GLsync fence = nullptr;
|
||||||
|
bool inFlight = false;
|
||||||
|
uint64_t frameIndex = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
using Clock = std::chrono::steady_clock;
|
||||||
|
|
||||||
|
void ThreadMain()
|
||||||
|
{
|
||||||
|
std::string error;
|
||||||
|
HiddenOpenGLContext context;
|
||||||
|
if (!context.Create(mWidth, mHeight, error) || !context.MakeCurrent())
|
||||||
|
{
|
||||||
|
SignalStartupFailure(error.empty() ? "OpenGL context creation failed." : error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!ResolveGLExtensions())
|
||||||
|
{
|
||||||
|
SignalStartupFailure("OpenGL extension resolution failed.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!CreateRenderTargets())
|
||||||
|
{
|
||||||
|
SignalStartupFailure("OpenGL render target creation failed.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
CreatePbos();
|
||||||
|
SignalStarted();
|
||||||
|
|
||||||
|
auto nextRenderTime = Clock::now();
|
||||||
|
while (!mStopping)
|
||||||
|
{
|
||||||
|
ConsumeCompletedPbos();
|
||||||
|
|
||||||
|
const auto now = Clock::now();
|
||||||
|
if (now < nextRenderTime)
|
||||||
|
{
|
||||||
|
std::this_thread::sleep_for((std::min)(std::chrono::milliseconds(1), std::chrono::duration_cast<std::chrono::milliseconds>(nextRenderTime - now)));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
RenderPattern(mFrameIndex);
|
||||||
|
if (!QueueReadback(mFrameIndex))
|
||||||
|
mFrameStore.CountPboQueueMiss();
|
||||||
|
mFrameStore.CountRenderedFrame();
|
||||||
|
++mFrameIndex;
|
||||||
|
nextRenderTime += mFrameDuration;
|
||||||
|
if (Clock::now() - nextRenderTime > mFrameDuration * 4)
|
||||||
|
nextRenderTime = Clock::now() + mFrameDuration;
|
||||||
|
}
|
||||||
|
|
||||||
|
FlushPbos();
|
||||||
|
DestroyPbos();
|
||||||
|
DestroyRenderTargets();
|
||||||
|
context.ClearCurrent();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool CreateRenderTargets()
|
||||||
|
{
|
||||||
|
glGenFramebuffers(1, &mFramebuffer);
|
||||||
|
glBindFramebuffer(GL_FRAMEBUFFER, mFramebuffer);
|
||||||
|
glGenTextures(1, &mTexture);
|
||||||
|
glBindTexture(GL_TEXTURE_2D, mTexture);
|
||||||
|
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);
|
||||||
|
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, static_cast<GLsizei>(mWidth), static_cast<GLsizei>(mHeight), 0, GL_BGRA, GL_UNSIGNED_INT_8_8_8_8_REV, nullptr);
|
||||||
|
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, mTexture, 0);
|
||||||
|
const bool complete = glCheckFramebufferStatus(GL_FRAMEBUFFER) == GL_FRAMEBUFFER_COMPLETE;
|
||||||
|
glBindTexture(GL_TEXTURE_2D, 0);
|
||||||
|
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||||
|
return complete;
|
||||||
|
}
|
||||||
|
|
||||||
|
void DestroyRenderTargets()
|
||||||
|
{
|
||||||
|
if (mFramebuffer != 0)
|
||||||
|
glDeleteFramebuffers(1, &mFramebuffer);
|
||||||
|
if (mTexture != 0)
|
||||||
|
glDeleteTextures(1, &mTexture);
|
||||||
|
mFramebuffer = 0;
|
||||||
|
mTexture = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
void CreatePbos()
|
||||||
|
{
|
||||||
|
mPbos.resize(kPboDepth);
|
||||||
|
const std::size_t byteCount = static_cast<std::size_t>(VideoIORowBytes(VideoIOPixelFormat::Bgra8, mWidth)) * mHeight;
|
||||||
|
for (PboSlot& slot : mPbos)
|
||||||
|
{
|
||||||
|
glGenBuffers(1, &slot.pbo);
|
||||||
|
glBindBuffer(GL_PIXEL_PACK_BUFFER, slot.pbo);
|
||||||
|
glBufferData(GL_PIXEL_PACK_BUFFER, static_cast<GLsizeiptr>(byteCount), nullptr, GL_STREAM_READ);
|
||||||
|
}
|
||||||
|
glBindBuffer(GL_PIXEL_PACK_BUFFER, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
void DestroyPbos()
|
||||||
|
{
|
||||||
|
for (PboSlot& slot : mPbos)
|
||||||
|
{
|
||||||
|
if (slot.fence)
|
||||||
|
glDeleteSync(slot.fence);
|
||||||
|
if (slot.pbo != 0)
|
||||||
|
glDeleteBuffers(1, &slot.pbo);
|
||||||
|
slot = {};
|
||||||
|
}
|
||||||
|
mPbos.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
void FlushPbos()
|
||||||
|
{
|
||||||
|
for (std::size_t i = 0; i < mPbos.size() * 2; ++i)
|
||||||
|
ConsumeCompletedPbos();
|
||||||
|
}
|
||||||
|
|
||||||
|
void RenderPattern(uint64_t frameIndex)
|
||||||
|
{
|
||||||
|
const float t = static_cast<float>(frameIndex) / 60.0f;
|
||||||
|
const float red = 0.1f + 0.4f * (0.5f + 0.5f * std::sin(t));
|
||||||
|
const float green = 0.1f + 0.4f * (0.5f + 0.5f * std::sin(t * 0.73f + 1.0f));
|
||||||
|
const float blue = 0.15f + 0.3f * (0.5f + 0.5f * std::sin(t * 0.41f + 2.0f));
|
||||||
|
|
||||||
|
glBindFramebuffer(GL_FRAMEBUFFER, mFramebuffer);
|
||||||
|
glViewport(0, 0, static_cast<GLsizei>(mWidth), static_cast<GLsizei>(mHeight));
|
||||||
|
glDisable(GL_SCISSOR_TEST);
|
||||||
|
glClearColor(red, green, blue, 1.0f);
|
||||||
|
glClear(GL_COLOR_BUFFER_BIT);
|
||||||
|
|
||||||
|
const int boxWidth = static_cast<int>(mWidth / 6);
|
||||||
|
const int boxHeight = static_cast<int>(mHeight / 5);
|
||||||
|
const float phase = 0.5f + 0.5f * std::sin(t * 1.7f);
|
||||||
|
const int x = static_cast<int>(phase * static_cast<float>(mWidth - boxWidth));
|
||||||
|
const int y = static_cast<int>((0.5f + 0.5f * std::sin(t * 1.1f + 0.8f)) * static_cast<float>(mHeight - boxHeight));
|
||||||
|
|
||||||
|
glEnable(GL_SCISSOR_TEST);
|
||||||
|
glScissor(x, y, boxWidth, boxHeight);
|
||||||
|
glClearColor(1.0f - red, 0.85f, 0.15f + blue, 1.0f);
|
||||||
|
glClear(GL_COLOR_BUFFER_BIT);
|
||||||
|
glDisable(GL_SCISSOR_TEST);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool QueueReadback(uint64_t frameIndex)
|
||||||
|
{
|
||||||
|
if (mPbos.empty())
|
||||||
|
return false;
|
||||||
|
|
||||||
|
PboSlot& slot = mPbos[mWriteIndex];
|
||||||
|
if (slot.inFlight)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
const std::size_t byteCount = static_cast<std::size_t>(VideoIORowBytes(VideoIOPixelFormat::Bgra8, mWidth)) * mHeight;
|
||||||
|
glBindFramebuffer(GL_READ_FRAMEBUFFER, mFramebuffer);
|
||||||
|
glPixelStorei(GL_PACK_ALIGNMENT, 4);
|
||||||
|
glPixelStorei(GL_PACK_ROW_LENGTH, 0);
|
||||||
|
glBindBuffer(GL_PIXEL_PACK_BUFFER, slot.pbo);
|
||||||
|
glBufferData(GL_PIXEL_PACK_BUFFER, static_cast<GLsizeiptr>(byteCount), nullptr, GL_STREAM_READ);
|
||||||
|
glReadPixels(0, 0, static_cast<GLsizei>(mWidth), static_cast<GLsizei>(mHeight), GL_BGRA, GL_UNSIGNED_INT_8_8_8_8_REV, nullptr);
|
||||||
|
slot.fence = glFenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE, 0);
|
||||||
|
slot.inFlight = slot.fence != nullptr;
|
||||||
|
slot.frameIndex = frameIndex;
|
||||||
|
glBindBuffer(GL_PIXEL_PACK_BUFFER, 0);
|
||||||
|
mWriteIndex = (mWriteIndex + 1) % mPbos.size();
|
||||||
|
return slot.inFlight;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ConsumeCompletedPbos()
|
||||||
|
{
|
||||||
|
for (std::size_t checked = 0; checked < mPbos.size(); ++checked)
|
||||||
|
{
|
||||||
|
PboSlot& slot = mPbos[mReadIndex];
|
||||||
|
if (!slot.inFlight || slot.fence == nullptr)
|
||||||
|
{
|
||||||
|
mReadIndex = (mReadIndex + 1) % mPbos.size();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const GLenum waitResult = glClientWaitSync(slot.fence, 0, 0);
|
||||||
|
if (waitResult != GL_ALREADY_SIGNALED && waitResult != GL_CONDITION_SATISFIED)
|
||||||
|
return;
|
||||||
|
|
||||||
|
ProbeFrame frame;
|
||||||
|
if (mFrameStore.AcquireForRender(frame))
|
||||||
|
{
|
||||||
|
glBindBuffer(GL_PIXEL_PACK_BUFFER, slot.pbo);
|
||||||
|
void* mapped = glMapBuffer(GL_PIXEL_PACK_BUFFER, GL_READ_ONLY);
|
||||||
|
if (mapped)
|
||||||
|
{
|
||||||
|
const std::size_t byteCount = static_cast<std::size_t>(frame.rowBytes) * frame.height;
|
||||||
|
std::memcpy(frame.bytes, mapped, byteCount);
|
||||||
|
glUnmapBuffer(GL_PIXEL_PACK_BUFFER);
|
||||||
|
frame.frameIndex = slot.frameIndex;
|
||||||
|
mFrameStore.PublishCompleted(frame);
|
||||||
|
}
|
||||||
|
glBindBuffer(GL_PIXEL_PACK_BUFFER, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
glDeleteSync(slot.fence);
|
||||||
|
slot.fence = nullptr;
|
||||||
|
slot.inFlight = false;
|
||||||
|
mReadIndex = (mReadIndex + 1) % mPbos.size();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void SignalStarted()
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(mStartupMutex);
|
||||||
|
mStarted = true;
|
||||||
|
mStartupCondition.notify_all();
|
||||||
|
}
|
||||||
|
|
||||||
|
void SignalStartupFailure(const std::string& error)
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(mStartupMutex);
|
||||||
|
mStartupError = error;
|
||||||
|
mStartupCondition.notify_all();
|
||||||
|
}
|
||||||
|
|
||||||
|
LatestFrameStore& mFrameStore;
|
||||||
|
unsigned mWidth = 0;
|
||||||
|
unsigned mHeight = 0;
|
||||||
|
Clock::duration mFrameDuration;
|
||||||
|
std::thread mThread;
|
||||||
|
std::atomic<bool> mStopping{ false };
|
||||||
|
std::mutex mStartupMutex;
|
||||||
|
std::condition_variable mStartupCondition;
|
||||||
|
bool mStarted = false;
|
||||||
|
std::string mStartupError;
|
||||||
|
GLuint mFramebuffer = 0;
|
||||||
|
GLuint mTexture = 0;
|
||||||
|
std::vector<PboSlot> mPbos;
|
||||||
|
std::size_t mWriteIndex = 0;
|
||||||
|
std::size_t mReadIndex = 0;
|
||||||
|
uint64_t mFrameIndex = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
class DeckLinkProbePlayout
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
DeckLinkProbePlayout(DeckLinkSession& session, LatestFrameStore& frameStore) :
|
||||||
|
mSession(session),
|
||||||
|
mFrameStore(frameStore)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Start()
|
||||||
|
{
|
||||||
|
mStopping = false;
|
||||||
|
mThread = std::thread([this]() { ThreadMain(); });
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Stop()
|
||||||
|
{
|
||||||
|
mStopping = true;
|
||||||
|
if (mThread.joinable())
|
||||||
|
mThread.join();
|
||||||
|
}
|
||||||
|
|
||||||
|
void ThreadMain()
|
||||||
|
{
|
||||||
|
while (!mStopping)
|
||||||
|
{
|
||||||
|
const ProbeMetrics metrics = mFrameStore.Metrics();
|
||||||
|
if (metrics.scheduledCount >= kDeckLinkTargetBufferedFrames)
|
||||||
|
{
|
||||||
|
std::this_thread::sleep_for(std::chrono::milliseconds(1));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
ProbeFrame frame;
|
||||||
|
if (!mFrameStore.ConsumeCompleted(frame))
|
||||||
|
{
|
||||||
|
std::this_thread::sleep_for(std::chrono::milliseconds(1));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
VideoIOOutputFrame outputFrame;
|
||||||
|
outputFrame.bytes = frame.bytes;
|
||||||
|
outputFrame.nativeBuffer = frame.bytes;
|
||||||
|
outputFrame.rowBytes = frame.rowBytes;
|
||||||
|
outputFrame.width = frame.width;
|
||||||
|
outputFrame.height = frame.height;
|
||||||
|
outputFrame.pixelFormat = frame.pixelFormat;
|
||||||
|
|
||||||
|
if (!mSession.ScheduleOutputFrame(outputFrame))
|
||||||
|
{
|
||||||
|
mFrameStore.ReleaseByBytes(frame.bytes);
|
||||||
|
std::this_thread::sleep_for(std::chrono::milliseconds(1));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
DeckLinkSession& mSession;
|
||||||
|
LatestFrameStore& mFrameStore;
|
||||||
|
std::thread mThread;
|
||||||
|
std::atomic<bool> mStopping{ false };
|
||||||
|
};
|
||||||
|
|
||||||
|
std::string CompletionResultToString(VideoIOCompletionResult result)
|
||||||
|
{
|
||||||
|
switch (result)
|
||||||
|
{
|
||||||
|
case VideoIOCompletionResult::Completed:
|
||||||
|
return "completed";
|
||||||
|
case VideoIOCompletionResult::DisplayedLate:
|
||||||
|
return "late";
|
||||||
|
case VideoIOCompletionResult::Dropped:
|
||||||
|
return "dropped";
|
||||||
|
case VideoIOCompletionResult::Flushed:
|
||||||
|
return "flushed";
|
||||||
|
case VideoIOCompletionResult::Unknown:
|
||||||
|
default:
|
||||||
|
return "unknown";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void PrintUsage()
|
||||||
|
{
|
||||||
|
std::cout << "DeckLinkRenderCadenceProbe\n"
|
||||||
|
<< " Renders a simple OpenGL BGRA8 motion pattern on one GL thread,\n"
|
||||||
|
<< " copies completed PBO readbacks into latest-N system memory slots,\n"
|
||||||
|
<< " warms up rendered frames, then feeds DeckLink scheduled playback.\n\n"
|
||||||
|
<< "Press Enter to stop.\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
int main()
|
||||||
|
{
|
||||||
|
PrintUsage();
|
||||||
|
|
||||||
|
ComInitGuard com;
|
||||||
|
if (!com.Initialize())
|
||||||
|
{
|
||||||
|
std::cerr << "COM initialization failed: 0x" << std::hex << com.Result() << std::dec << "\n";
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
LatestFrameStore frameStore(kDefaultWidth, kDefaultHeight, kSystemFrameSlots);
|
||||||
|
DeckLinkSession deckLink;
|
||||||
|
std::atomic<uint64_t> completions{ 0 };
|
||||||
|
std::atomic<uint64_t> late{ 0 };
|
||||||
|
std::atomic<uint64_t> dropped{ 0 };
|
||||||
|
|
||||||
|
VideoFormatSelection formats;
|
||||||
|
std::string error;
|
||||||
|
if (!deckLink.DiscoverDevicesAndModes(formats, error))
|
||||||
|
{
|
||||||
|
std::cerr << "DeckLink discovery failed: " << error << "\n";
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
if (!deckLink.SelectPreferredFormats(formats, false, error))
|
||||||
|
{
|
||||||
|
std::cerr << "DeckLink format selection failed: " << error << "\n";
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
if (!deckLink.ConfigureOutput(
|
||||||
|
[&](const VideoIOCompletion& completion) {
|
||||||
|
frameStore.ReleaseByBytes(completion.outputFrameBuffer);
|
||||||
|
++completions;
|
||||||
|
if (completion.result == VideoIOCompletionResult::DisplayedLate)
|
||||||
|
++late;
|
||||||
|
else if (completion.result == VideoIOCompletionResult::Dropped)
|
||||||
|
++dropped;
|
||||||
|
},
|
||||||
|
formats.output,
|
||||||
|
false,
|
||||||
|
error))
|
||||||
|
{
|
||||||
|
std::cerr << "DeckLink output configuration failed: " << error << "\n";
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
if (!deckLink.PrepareOutputSchedule())
|
||||||
|
{
|
||||||
|
std::cerr << "DeckLink schedule preparation failed.\n";
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
const VideoIOState& state = deckLink.State();
|
||||||
|
if (state.outputFrameSize.width != kDefaultWidth || state.outputFrameSize.height != kDefaultHeight)
|
||||||
|
{
|
||||||
|
std::cerr << "This probe currently expects 1920x1080 output. Selected mode is "
|
||||||
|
<< state.outputFrameSize.width << "x" << state.outputFrameSize.height << ".\n";
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
RenderCadenceProbe renderer(frameStore, state.outputFrameSize.width, state.outputFrameSize.height, state.frameBudgetMilliseconds);
|
||||||
|
if (!renderer.Start(error))
|
||||||
|
{
|
||||||
|
std::cerr << "Render thread start failed: " << error << "\n";
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::cout << "Warming up " << kWarmupFrames << " rendered frames at cadence...\n";
|
||||||
|
if (!frameStore.WaitForCompletedDepth(kWarmupFrames, std::chrono::seconds(3)))
|
||||||
|
{
|
||||||
|
std::cerr << "Timed out waiting for rendered warmup frames.\n";
|
||||||
|
renderer.Stop();
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
DeckLinkProbePlayout playout(deckLink, frameStore);
|
||||||
|
playout.Start();
|
||||||
|
|
||||||
|
const auto prerollDeadline = std::chrono::steady_clock::now() + std::chrono::seconds(3);
|
||||||
|
while (std::chrono::steady_clock::now() < prerollDeadline)
|
||||||
|
{
|
||||||
|
if (frameStore.Metrics().scheduledCount >= kDeckLinkTargetBufferedFrames)
|
||||||
|
break;
|
||||||
|
std::this_thread::sleep_for(std::chrono::milliseconds(2));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!deckLink.StartScheduledPlayback())
|
||||||
|
{
|
||||||
|
std::cerr << "DeckLink scheduled playback failed to start.\n";
|
||||||
|
playout.Stop();
|
||||||
|
renderer.Stop();
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::atomic<bool> metricsStopping{ false };
|
||||||
|
std::thread metricsThread([&]() {
|
||||||
|
uint64_t lastRendered = 0;
|
||||||
|
uint64_t lastScheduled = 0;
|
||||||
|
auto lastTime = std::chrono::steady_clock::now();
|
||||||
|
while (!metricsStopping)
|
||||||
|
{
|
||||||
|
std::this_thread::sleep_for(std::chrono::seconds(1));
|
||||||
|
const auto now = std::chrono::steady_clock::now();
|
||||||
|
const double seconds = std::chrono::duration_cast<std::chrono::duration<double>>(now - lastTime).count();
|
||||||
|
const ProbeMetrics metrics = frameStore.Metrics();
|
||||||
|
const double renderFps = seconds > 0.0 ? static_cast<double>(metrics.renderedFrames - lastRendered) / seconds : 0.0;
|
||||||
|
const double scheduleFps = seconds > 0.0 ? static_cast<double>(metrics.scheduledFrames - lastScheduled) / seconds : 0.0;
|
||||||
|
lastRendered = metrics.renderedFrames;
|
||||||
|
lastScheduled = metrics.scheduledFrames;
|
||||||
|
lastTime = now;
|
||||||
|
|
||||||
|
std::cout << std::fixed << std::setprecision(1)
|
||||||
|
<< "renderFps=" << renderFps
|
||||||
|
<< " scheduleFps=" << scheduleFps
|
||||||
|
<< " free=" << metrics.freeCount
|
||||||
|
<< " completed=" << metrics.completedCount
|
||||||
|
<< " scheduled=" << metrics.scheduledCount
|
||||||
|
<< " drops=" << metrics.completedDrops
|
||||||
|
<< " pboMiss=" << metrics.pboQueueMisses
|
||||||
|
<< " completions=" << completions.load()
|
||||||
|
<< " late=" << late.load()
|
||||||
|
<< " dropped=" << dropped.load()
|
||||||
|
<< " decklinkBuffered=" << deckLink.State().actualDeckLinkBufferedFrames
|
||||||
|
<< "\n";
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
std::string line;
|
||||||
|
std::getline(std::cin, line);
|
||||||
|
|
||||||
|
metricsStopping = true;
|
||||||
|
if (metricsThread.joinable())
|
||||||
|
metricsThread.join();
|
||||||
|
playout.Stop();
|
||||||
|
deckLink.Stop();
|
||||||
|
renderer.Stop();
|
||||||
|
deckLink.ReleaseResources();
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
113
apps/DeckLinkRenderCadenceProbe/README.md
Normal file
113
apps/DeckLinkRenderCadenceProbe/README.md
Normal file
@@ -0,0 +1,113 @@
|
|||||||
|
# DeckLink Render Cadence Probe
|
||||||
|
|
||||||
|
This is a deliberately small architecture probe for the Phase 7.7 playout model.
|
||||||
|
|
||||||
|
It is not the main app and does not use the main runtime, shader stack, preview path, input upload path, or render engine.
|
||||||
|
|
||||||
|
## What It Tests
|
||||||
|
|
||||||
|
The probe validates the clean playout spine:
|
||||||
|
|
||||||
|
```text
|
||||||
|
single OpenGL render thread
|
||||||
|
owns its own hidden GL context
|
||||||
|
renders a simple moving BGRA8 pattern at output cadence
|
||||||
|
queues GPU readback through a PBO ring
|
||||||
|
copies completed readbacks into latest-N system-memory slots
|
||||||
|
|
||||||
|
system-memory frame store
|
||||||
|
owns free / rendering / completed / scheduled slots
|
||||||
|
drops old completed unscheduled frames when render cadence needs space
|
||||||
|
protects scheduled frames until DeckLink completion
|
||||||
|
|
||||||
|
DeckLink playout thread
|
||||||
|
consumes completed system-memory frames
|
||||||
|
keeps a small scheduled buffer filled
|
||||||
|
does not render
|
||||||
|
```
|
||||||
|
|
||||||
|
Startup warms up rendered frames before starting DeckLink scheduled playback.
|
||||||
|
|
||||||
|
## How To Build
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
cmake --build --preset build-debug --target DeckLinkRenderCadenceProbe -- /m:1
|
||||||
|
```
|
||||||
|
|
||||||
|
The executable is:
|
||||||
|
|
||||||
|
```text
|
||||||
|
build\vs2022-x64-debug\Debug\DeckLinkRenderCadenceProbe.exe
|
||||||
|
```
|
||||||
|
|
||||||
|
## How To Run
|
||||||
|
|
||||||
|
Run it from a terminal so you can see the telemetry:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
build\vs2022-x64-debug\Debug\DeckLinkRenderCadenceProbe.exe
|
||||||
|
```
|
||||||
|
|
||||||
|
Press Enter to stop.
|
||||||
|
|
||||||
|
The first version assumes `1080p59.94` / `1920x1080` output and BGRA8 system-memory frames.
|
||||||
|
|
||||||
|
## What To Watch
|
||||||
|
|
||||||
|
The probe prints one line per second:
|
||||||
|
|
||||||
|
- `renderFps`: cadence render throughput
|
||||||
|
- `scheduleFps`: DeckLink scheduling throughput
|
||||||
|
- `free`: free system-memory slots
|
||||||
|
- `completed`: rendered, unscheduled slots
|
||||||
|
- `scheduled`: slots currently owned by DeckLink
|
||||||
|
- `drops`: old completed unscheduled frames recycled by the latest-N cache
|
||||||
|
- `pboMiss`: PBO ring was full when trying to queue readback
|
||||||
|
- `late`: DeckLink displayed-late completions
|
||||||
|
- `dropped`: DeckLink dropped completions
|
||||||
|
- `decklinkBuffered`: actual DeckLink buffered-frame count when available
|
||||||
|
|
||||||
|
For a healthy architecture proof, expect:
|
||||||
|
|
||||||
|
- `renderFps` close to the selected output cadence
|
||||||
|
- `scheduleFps` close to the selected output cadence after warmup
|
||||||
|
- `scheduled` hovering near the target buffer depth
|
||||||
|
- `late` and `dropped` not increasing continuously
|
||||||
|
- visible motion that is smooth on the DeckLink output
|
||||||
|
|
||||||
|
## Interpretation
|
||||||
|
|
||||||
|
If this probe is smooth at 59.94/60, the broad architecture is viable and the main app's remaining stutters are likely caused by integration details such as input upload, shared render-thread work, preview/screenshot work, or runtime/render-state coupling.
|
||||||
|
|
||||||
|
If this probe is not smooth, the problem is lower level: DeckLink scheduling, OpenGL readback, Windows scheduling, or hardware/driver behavior.
|
||||||
|
|
||||||
|
## Initial Result
|
||||||
|
|
||||||
|
Date: 2026-05-12
|
||||||
|
|
||||||
|
User-visible result:
|
||||||
|
|
||||||
|
- output looked smooth
|
||||||
|
|
||||||
|
Representative telemetry:
|
||||||
|
|
||||||
|
```text
|
||||||
|
renderFps=59.9 scheduleFps=59.9 free=7 completed=1 scheduled=4 drops=0 pboMiss=0 completions=119 late=0 dropped=0 decklinkBuffered=4
|
||||||
|
renderFps=59.9 scheduleFps=59.9 free=7 completed=1 scheduled=4 drops=0 pboMiss=0 completions=179 late=0 dropped=0 decklinkBuffered=4
|
||||||
|
renderFps=59.8 scheduleFps=59.8 free=7 completed=1 scheduled=4 drops=0 pboMiss=0 completions=239 late=0 dropped=0 decklinkBuffered=4
|
||||||
|
renderFps=60.8 scheduleFps=59.8 free=7 completed=1 scheduled=4 drops=0 pboMiss=0 completions=299 late=0 dropped=0 decklinkBuffered=4
|
||||||
|
renderFps=59.9 scheduleFps=59.9 free=7 completed=1 scheduled=4 drops=0 pboMiss=0 completions=360 late=0 dropped=0 decklinkBuffered=4
|
||||||
|
renderFps=59.8 scheduleFps=60.8 free=8 completed=0 scheduled=4 drops=0 pboMiss=0 completions=420 late=0 dropped=0 decklinkBuffered=4
|
||||||
|
```
|
||||||
|
|
||||||
|
Read:
|
||||||
|
|
||||||
|
- the clean architecture can sustain the selected output cadence on the test machine
|
||||||
|
- BGRA8 PBO readback is viable when isolated from the main app's other render-thread work
|
||||||
|
- latest-N system-memory buffering stayed stable
|
||||||
|
- DeckLink actual buffered depth stayed at 4
|
||||||
|
- there were no late frames, dropped frames, completed-frame drops, or PBO misses in the sampled output
|
||||||
|
|
||||||
|
Implication:
|
||||||
|
|
||||||
|
The main app's remaining stutters are likely integration/ownership issues rather than a fundamental DeckLink/OpenGL/BGRA8 readback limit. The highest-value suspects are input upload before output render, shared render-thread queue contention, preview/screenshot work, and runtime/render-state work on the output path.
|
||||||
@@ -18,6 +18,7 @@ RenderEngine::RenderEngine(
|
|||||||
mRenderPass(mRenderer),
|
mRenderPass(mRenderer),
|
||||||
mRenderPipeline(mRenderer, runtimeSnapshotProvider, healthTelemetry, std::move(renderEffect), std::move(screenshotReady), std::move(previewPaint)),
|
mRenderPipeline(mRenderer, runtimeSnapshotProvider, healthTelemetry, std::move(renderEffect), std::move(screenshotReady), std::move(previewPaint)),
|
||||||
mShaderPrograms(mRenderer, runtimeSnapshotProvider),
|
mShaderPrograms(mRenderer, runtimeSnapshotProvider),
|
||||||
|
mHealthTelemetry(healthTelemetry),
|
||||||
mHdc(hdc),
|
mHdc(hdc),
|
||||||
mHglrc(hglrc),
|
mHglrc(hglrc),
|
||||||
mFrameStateResolver(runtimeSnapshotProvider)
|
mFrameStateResolver(runtimeSnapshotProvider)
|
||||||
@@ -546,7 +547,11 @@ bool RenderEngine::RequestOutputFrame(const RenderPipelineFrameContext& context,
|
|||||||
{
|
{
|
||||||
if (mRenderThreadRunning)
|
if (mRenderThreadRunning)
|
||||||
{
|
{
|
||||||
return TryInvokeOnRenderThread("output-render", [this, &context, &outputFrame]() {
|
const auto queuedAt = std::chrono::steady_clock::now();
|
||||||
|
return TryInvokeOnRenderThread("output-render", [this, &context, &outputFrame, queuedAt]() {
|
||||||
|
const auto startedAt = std::chrono::steady_clock::now();
|
||||||
|
const double queueWaitMilliseconds = std::chrono::duration_cast<std::chrono::duration<double, std::milli>>(startedAt - queuedAt).count();
|
||||||
|
mHealthTelemetry.TryRecordOutputRenderQueueWait(queueWaitMilliseconds);
|
||||||
mRenderCommandQueue.RequestOutputFrame({ context.videoState, context.completion });
|
mRenderCommandQueue.RequestOutputFrame({ context.videoState, context.completion });
|
||||||
RenderOutputFrameRequest request;
|
RenderOutputFrameRequest request;
|
||||||
return mRenderCommandQueue.TryTakeOutputFrame(request) &&
|
return mRenderCommandQueue.TryTakeOutputFrame(request) &&
|
||||||
|
|||||||
@@ -209,6 +209,7 @@ private:
|
|||||||
OpenGLRenderPass mRenderPass;
|
OpenGLRenderPass mRenderPass;
|
||||||
OpenGLRenderPipeline mRenderPipeline;
|
OpenGLRenderPipeline mRenderPipeline;
|
||||||
OpenGLShaderPrograms mShaderPrograms;
|
OpenGLShaderPrograms mShaderPrograms;
|
||||||
|
HealthTelemetry& mHealthTelemetry;
|
||||||
HDC mHdc;
|
HDC mHdc;
|
||||||
HGLRC mHglrc;
|
HGLRC mHglrc;
|
||||||
|
|
||||||
|
|||||||
@@ -58,6 +58,12 @@ OpenGLComposite::~OpenGLComposite()
|
|||||||
mShaderBuildQueue->Stop();
|
mShaderBuildQueue->Stop();
|
||||||
if (mVideoBackend)
|
if (mVideoBackend)
|
||||||
mVideoBackend->ReleaseResources();
|
mVideoBackend->ReleaseResources();
|
||||||
|
if (mRuntimeStore)
|
||||||
|
{
|
||||||
|
std::string persistenceError;
|
||||||
|
if (!mRuntimeStore->FlushPersistenceForShutdown(std::chrono::seconds(2), persistenceError))
|
||||||
|
OutputDebugStringA((std::string("Persistence shutdown flush failed: ") + persistenceError + "\n").c_str());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
bool OpenGLComposite::InitDeckLink()
|
bool OpenGLComposite::InitDeckLink()
|
||||||
@@ -158,6 +164,9 @@ error:
|
|||||||
|
|
||||||
void OpenGLComposite::paintGL(bool force)
|
void OpenGLComposite::paintGL(bool force)
|
||||||
{
|
{
|
||||||
|
if (mRuntimeUpdateController)
|
||||||
|
mRuntimeUpdateController->ProcessRuntimeWork();
|
||||||
|
|
||||||
if (!force)
|
if (!force)
|
||||||
{
|
{
|
||||||
if (IsIconic(hGLWnd))
|
if (IsIconic(hGLWnd))
|
||||||
@@ -165,6 +174,12 @@ void OpenGLComposite::paintGL(bool force)
|
|||||||
}
|
}
|
||||||
|
|
||||||
const unsigned previewFps = mRuntimeStore ? mRuntimeStore->GetConfiguredPreviewFps() : 30u;
|
const unsigned previewFps = mRuntimeStore ? mRuntimeStore->GetConfiguredPreviewFps() : 30u;
|
||||||
|
if (!force && mVideoBackend && mVideoBackend->ShouldPrioritizeOutputOverPreview())
|
||||||
|
{
|
||||||
|
ValidateRect(hGLWnd, NULL);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (!mRenderEngine->TryPresentPreview(force, previewFps, mVideoBackend->OutputFrameWidth(), mVideoBackend->OutputFrameHeight()))
|
if (!mRenderEngine->TryPresentPreview(force, previewFps, mVideoBackend->OutputFrameWidth(), mVideoBackend->OutputFrameHeight()))
|
||||||
{
|
{
|
||||||
ValidateRect(hGLWnd, NULL);
|
ValidateRect(hGLWnd, NULL);
|
||||||
@@ -255,6 +270,9 @@ bool OpenGLComposite::Start()
|
|||||||
if (!mRenderEngine->StartRenderThread())
|
if (!mRenderEngine->StartRenderThread())
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
|
if (mRuntimeUpdateController)
|
||||||
|
mRuntimeUpdateController->ProcessRuntimeWork();
|
||||||
|
|
||||||
if (mVideoBackend->Start())
|
if (mVideoBackend->Start())
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
@@ -277,6 +295,13 @@ bool OpenGLComposite::Stop()
|
|||||||
if (mRenderEngine)
|
if (mRenderEngine)
|
||||||
mRenderEngine->StopRenderThread();
|
mRenderEngine->StopRenderThread();
|
||||||
|
|
||||||
|
if (mRuntimeStore)
|
||||||
|
{
|
||||||
|
std::string persistenceError;
|
||||||
|
if (!mRuntimeStore->FlushPersistenceForShutdown(std::chrono::seconds(2), persistenceError))
|
||||||
|
OutputDebugStringA((std::string("Persistence shutdown flush failed: ") + persistenceError + "\n").c_str());
|
||||||
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -338,9 +363,6 @@ bool OpenGLComposite::RequestScreenshot(std::string& error)
|
|||||||
|
|
||||||
void OpenGLComposite::renderEffect()
|
void OpenGLComposite::renderEffect()
|
||||||
{
|
{
|
||||||
if (mRuntimeUpdateController)
|
|
||||||
mRuntimeUpdateController->ProcessRuntimeWork();
|
|
||||||
|
|
||||||
const RenderFrameInput frameInput = BuildRenderFrameInput();
|
const RenderFrameInput frameInput = BuildRenderFrameInput();
|
||||||
RenderFrame(frameInput);
|
RenderFrame(frameInput);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -48,6 +48,9 @@ RuntimeUpdateController::RuntimeUpdateController(
|
|||||||
mRuntimeEventDispatcher.Subscribe(
|
mRuntimeEventDispatcher.Subscribe(
|
||||||
RuntimeEventType::RuntimeReloadRequested,
|
RuntimeEventType::RuntimeReloadRequested,
|
||||||
[this](const RuntimeEvent& event) { HandleRuntimeReloadRequested(event); });
|
[this](const RuntimeEvent& event) { HandleRuntimeReloadRequested(event); });
|
||||||
|
mRuntimeEventDispatcher.Subscribe(
|
||||||
|
RuntimeEventType::RuntimePersistenceRequested,
|
||||||
|
[this](const RuntimeEvent& event) { HandleRuntimePersistenceRequested(event); });
|
||||||
mRuntimeEventDispatcher.Subscribe(
|
mRuntimeEventDispatcher.Subscribe(
|
||||||
RuntimeEventType::ShaderBuildRequested,
|
RuntimeEventType::ShaderBuildRequested,
|
||||||
[this](const RuntimeEvent& event) { HandleShaderBuildRequested(event); });
|
[this](const RuntimeEvent& event) { HandleShaderBuildRequested(event); });
|
||||||
@@ -158,6 +161,16 @@ void RuntimeUpdateController::HandleRuntimeReloadRequested(const RuntimeEvent& e
|
|||||||
mRuntimeStore.ClearReloadRequest();
|
mRuntimeStore.ClearReloadRequest();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void RuntimeUpdateController::HandleRuntimePersistenceRequested(const RuntimeEvent& event)
|
||||||
|
{
|
||||||
|
const RuntimePersistenceRequestedEvent* payload = std::get_if<RuntimePersistenceRequestedEvent>(&event.payload);
|
||||||
|
if (!payload)
|
||||||
|
return;
|
||||||
|
|
||||||
|
std::string error;
|
||||||
|
mRuntimeStore.RequestPersistence(payload->request, error);
|
||||||
|
}
|
||||||
|
|
||||||
void RuntimeUpdateController::HandleShaderBuildRequested(const RuntimeEvent& event)
|
void RuntimeUpdateController::HandleShaderBuildRequested(const RuntimeEvent& event)
|
||||||
{
|
{
|
||||||
const ShaderBuildEvent* payload = std::get_if<ShaderBuildEvent>(&event.payload);
|
const ShaderBuildEvent* payload = std::get_if<ShaderBuildEvent>(&event.payload);
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ public:
|
|||||||
private:
|
private:
|
||||||
void HandleRuntimeStateBroadcastRequested(const RuntimeEvent& event);
|
void HandleRuntimeStateBroadcastRequested(const RuntimeEvent& event);
|
||||||
void HandleRuntimeReloadRequested(const RuntimeEvent& event);
|
void HandleRuntimeReloadRequested(const RuntimeEvent& event);
|
||||||
|
void HandleRuntimePersistenceRequested(const RuntimeEvent& event);
|
||||||
void HandleShaderBuildRequested(const RuntimeEvent& event);
|
void HandleShaderBuildRequested(const RuntimeEvent& event);
|
||||||
void HandleShaderBuildPrepared(const RuntimeEvent& event);
|
void HandleShaderBuildPrepared(const RuntimeEvent& event);
|
||||||
void HandleShaderBuildFailed(const RuntimeEvent& event);
|
void HandleShaderBuildFailed(const RuntimeEvent& event);
|
||||||
|
|||||||
@@ -8,7 +8,9 @@
|
|||||||
#include <cstring>
|
#include <cstring>
|
||||||
|
|
||||||
#include <chrono>
|
#include <chrono>
|
||||||
|
#include <cstdlib>
|
||||||
#include <gl/gl.h>
|
#include <gl/gl.h>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
OpenGLRenderPipeline::OpenGLRenderPipeline(
|
OpenGLRenderPipeline::OpenGLRenderPipeline(
|
||||||
OpenGLRenderer& renderer,
|
OpenGLRenderer& renderer,
|
||||||
@@ -22,7 +24,9 @@ OpenGLRenderPipeline::OpenGLRenderPipeline(
|
|||||||
mHealthTelemetry(healthTelemetry),
|
mHealthTelemetry(healthTelemetry),
|
||||||
mRenderEffect(renderEffect),
|
mRenderEffect(renderEffect),
|
||||||
mOutputReady(outputReady),
|
mOutputReady(outputReady),
|
||||||
mPaint(paint)
|
mPaint(paint),
|
||||||
|
mOutputReadbackMode(ReadOutputReadbackModeFromEnvironment()),
|
||||||
|
mAsyncReadbackDepth(ReadAsyncReadbackDepthFromEnvironment())
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -44,7 +48,9 @@ bool OpenGLRenderPipeline::RenderFrame(const RenderPipelineFrameContext& context
|
|||||||
glBindFramebuffer(GL_FRAMEBUFFER, mRenderer.OutputFramebuffer());
|
glBindFramebuffer(GL_FRAMEBUFFER, mRenderer.OutputFramebuffer());
|
||||||
if (mOutputReady)
|
if (mOutputReady)
|
||||||
mOutputReady();
|
mOutputReady();
|
||||||
if (state.outputPixelFormat == VideoIOPixelFormat::V210 || state.outputPixelFormat == VideoIOPixelFormat::Yuva10)
|
if (state.outputPixelFormat == VideoIOPixelFormat::Bgra8)
|
||||||
|
PackOutputForBgra8(state);
|
||||||
|
else if (state.outputPixelFormat == VideoIOPixelFormat::V210 || state.outputPixelFormat == VideoIOPixelFormat::Yuva10)
|
||||||
PackOutputFor10Bit(state);
|
PackOutputFor10Bit(state);
|
||||||
glFlush();
|
glFlush();
|
||||||
|
|
||||||
@@ -53,13 +59,44 @@ bool OpenGLRenderPipeline::RenderFrame(const RenderPipelineFrameContext& context
|
|||||||
mHealthTelemetry.TryRecordPerformanceStats(state.frameBudgetMilliseconds, renderMilliseconds);
|
mHealthTelemetry.TryRecordPerformanceStats(state.frameBudgetMilliseconds, renderMilliseconds);
|
||||||
mRuntimeSnapshotProvider.AdvanceFrame();
|
mRuntimeSnapshotProvider.AdvanceFrame();
|
||||||
|
|
||||||
ReadOutputFrame(state, outputFrame);
|
OutputReadbackTiming readbackTiming = ReadOutputFrame(state, outputFrame);
|
||||||
if (mPaint)
|
mHealthTelemetry.TryRecordOutputRenderPipelineTiming(
|
||||||
mPaint();
|
renderMilliseconds,
|
||||||
|
readbackTiming.fenceWaitMilliseconds,
|
||||||
|
readbackTiming.mapMilliseconds,
|
||||||
|
readbackTiming.copyMilliseconds,
|
||||||
|
readbackTiming.cachedCopyMilliseconds,
|
||||||
|
readbackTiming.asyncQueueMilliseconds,
|
||||||
|
readbackTiming.asyncQueueBufferMilliseconds,
|
||||||
|
readbackTiming.asyncQueueSetupMilliseconds,
|
||||||
|
readbackTiming.asyncQueueReadPixelsMilliseconds,
|
||||||
|
readbackTiming.asyncQueueFenceMilliseconds,
|
||||||
|
readbackTiming.syncReadMilliseconds,
|
||||||
|
readbackTiming.asyncReadbackMissed,
|
||||||
|
readbackTiming.cachedFallbackUsed,
|
||||||
|
readbackTiming.syncFallbackUsed);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void OpenGLRenderPipeline::PackOutputForBgra8(const VideoIOState& state)
|
||||||
|
{
|
||||||
|
glBindFramebuffer(GL_READ_FRAMEBUFFER, mRenderer.OutputFramebuffer());
|
||||||
|
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, mRenderer.OutputPackFramebuffer());
|
||||||
|
glBlitFramebuffer(
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
state.outputFrameSize.width,
|
||||||
|
state.outputFrameSize.height,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
state.outputFrameSize.width,
|
||||||
|
state.outputFrameSize.height,
|
||||||
|
GL_COLOR_BUFFER_BIT,
|
||||||
|
GL_NEAREST);
|
||||||
|
glBindFramebuffer(GL_FRAMEBUFFER, mRenderer.OutputPackFramebuffer());
|
||||||
|
}
|
||||||
|
|
||||||
void OpenGLRenderPipeline::PackOutputFor10Bit(const VideoIOState& state)
|
void OpenGLRenderPipeline::PackOutputFor10Bit(const VideoIOState& state)
|
||||||
{
|
{
|
||||||
glBindFramebuffer(GL_FRAMEBUFFER, mRenderer.OutputPackFramebuffer());
|
glBindFramebuffer(GL_FRAMEBUFFER, mRenderer.OutputPackFramebuffer());
|
||||||
@@ -93,11 +130,17 @@ bool OpenGLRenderPipeline::EnsureAsyncReadbackBuffers(std::size_t requiredBytes)
|
|||||||
if (requiredBytes == 0)
|
if (requiredBytes == 0)
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
if (mAsyncReadbackBytes == requiredBytes && mAsyncReadbackSlots[0].pixelPackBuffer != 0)
|
if (mAsyncReadbackBytes == requiredBytes &&
|
||||||
|
mAsyncReadbackSlots.size() == mAsyncReadbackDepth &&
|
||||||
|
!mAsyncReadbackSlots.empty() &&
|
||||||
|
mAsyncReadbackSlots[0].pixelPackBuffer != 0)
|
||||||
|
{
|
||||||
return true;
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
ResetAsyncReadbackState();
|
ResetAsyncReadbackState();
|
||||||
mAsyncReadbackBytes = requiredBytes;
|
mAsyncReadbackBytes = requiredBytes;
|
||||||
|
mAsyncReadbackSlots.resize(mAsyncReadbackDepth);
|
||||||
for (AsyncReadbackSlot& slot : mAsyncReadbackSlots)
|
for (AsyncReadbackSlot& slot : mAsyncReadbackSlots)
|
||||||
{
|
{
|
||||||
glGenBuffers(1, &slot.pixelPackBuffer);
|
glGenBuffers(1, &slot.pixelPackBuffer);
|
||||||
@@ -118,7 +161,7 @@ void OpenGLRenderPipeline::ResetAsyncReadbackState()
|
|||||||
for (AsyncReadbackSlot& slot : mAsyncReadbackSlots)
|
for (AsyncReadbackSlot& slot : mAsyncReadbackSlots)
|
||||||
slot.sizeBytes = 0;
|
slot.sizeBytes = 0;
|
||||||
|
|
||||||
if (mAsyncReadbackSlots[0].pixelPackBuffer != 0)
|
if (!mAsyncReadbackSlots.empty() && mAsyncReadbackSlots[0].pixelPackBuffer != 0)
|
||||||
{
|
{
|
||||||
for (AsyncReadbackSlot& slot : mAsyncReadbackSlots)
|
for (AsyncReadbackSlot& slot : mAsyncReadbackSlots)
|
||||||
{
|
{
|
||||||
@@ -133,6 +176,7 @@ void OpenGLRenderPipeline::ResetAsyncReadbackState()
|
|||||||
mAsyncReadbackWriteIndex = 0;
|
mAsyncReadbackWriteIndex = 0;
|
||||||
mAsyncReadbackReadIndex = 0;
|
mAsyncReadbackReadIndex = 0;
|
||||||
mAsyncReadbackBytes = 0;
|
mAsyncReadbackBytes = 0;
|
||||||
|
mAsyncReadbackSlots.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
void OpenGLRenderPipeline::FlushAsyncReadbackPipeline()
|
void OpenGLRenderPipeline::FlushAsyncReadbackPipeline()
|
||||||
@@ -151,18 +195,29 @@ void OpenGLRenderPipeline::FlushAsyncReadbackPipeline()
|
|||||||
mAsyncReadbackReadIndex = 0;
|
mAsyncReadbackReadIndex = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
void OpenGLRenderPipeline::QueueAsyncReadback(const VideoIOState& state)
|
bool OpenGLRenderPipeline::QueueAsyncReadback(const VideoIOState& state, OutputReadbackTiming& timing)
|
||||||
{
|
{
|
||||||
const bool usePackedOutput = state.outputPixelFormat == VideoIOPixelFormat::V210 || state.outputPixelFormat == VideoIOPixelFormat::Yuva10;
|
const auto queueStartTime = std::chrono::steady_clock::now();
|
||||||
|
const bool useTenBitPackedOutput = state.outputPixelFormat == VideoIOPixelFormat::V210 ||
|
||||||
|
state.outputPixelFormat == VideoIOPixelFormat::Yuva10;
|
||||||
|
const bool usePackFramebuffer = state.outputPixelFormat == VideoIOPixelFormat::Bgra8 || useTenBitPackedOutput;
|
||||||
const std::size_t requiredBytes = static_cast<std::size_t>(state.outputFrameRowBytes) * state.outputFrameSize.height;
|
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 format = useTenBitPackedOutput ? GL_RGBA : GL_BGRA;
|
||||||
const GLenum type = usePackedOutput ? GL_UNSIGNED_BYTE : GL_UNSIGNED_INT_8_8_8_8_REV;
|
const GLenum type = useTenBitPackedOutput ? GL_UNSIGNED_BYTE : GL_UNSIGNED_INT_8_8_8_8_REV;
|
||||||
const GLuint framebuffer = usePackedOutput ? mRenderer.OutputPackFramebuffer() : mRenderer.OutputFramebuffer();
|
const GLuint framebuffer = usePackFramebuffer ? mRenderer.OutputPackFramebuffer() : mRenderer.OutputFramebuffer();
|
||||||
const GLsizei readWidth = static_cast<GLsizei>(usePackedOutput ? state.outputPackTextureWidth : state.outputFrameSize.width);
|
const GLsizei readWidth = static_cast<GLsizei>(useTenBitPackedOutput ? state.outputPackTextureWidth : state.outputFrameSize.width);
|
||||||
const GLsizei readHeight = static_cast<GLsizei>(state.outputFrameSize.height);
|
const GLsizei readHeight = static_cast<GLsizei>(state.outputFrameSize.height);
|
||||||
|
|
||||||
|
const auto finishTiming = [&timing, queueStartTime]() {
|
||||||
|
const auto queueEndTime = std::chrono::steady_clock::now();
|
||||||
|
timing.asyncQueueMilliseconds += std::chrono::duration_cast<std::chrono::duration<double, std::milli>>(queueEndTime - queueStartTime).count();
|
||||||
|
};
|
||||||
|
|
||||||
if (requiredBytes == 0)
|
if (requiredBytes == 0)
|
||||||
return;
|
{
|
||||||
|
finishTiming();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
if (mAsyncReadbackBytes != requiredBytes
|
if (mAsyncReadbackBytes != requiredBytes
|
||||||
|| mAsyncReadbackFormat != format
|
|| mAsyncReadbackFormat != format
|
||||||
@@ -173,30 +228,56 @@ void OpenGLRenderPipeline::QueueAsyncReadback(const VideoIOState& state)
|
|||||||
mAsyncReadbackType = type;
|
mAsyncReadbackType = type;
|
||||||
mAsyncReadbackFramebuffer = framebuffer;
|
mAsyncReadbackFramebuffer = framebuffer;
|
||||||
if (!EnsureAsyncReadbackBuffers(requiredBytes))
|
if (!EnsureAsyncReadbackBuffers(requiredBytes))
|
||||||
return;
|
{
|
||||||
|
finishTiming();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mAsyncReadbackSlots.empty())
|
||||||
|
{
|
||||||
|
finishTiming();
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
AsyncReadbackSlot& slot = mAsyncReadbackSlots[mAsyncReadbackWriteIndex];
|
AsyncReadbackSlot& slot = mAsyncReadbackSlots[mAsyncReadbackWriteIndex];
|
||||||
if (slot.fence != nullptr)
|
if (slot.inFlight)
|
||||||
{
|
{
|
||||||
glDeleteSync(slot.fence);
|
finishTiming();
|
||||||
slot.fence = nullptr;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
auto stageStartTime = std::chrono::steady_clock::now();
|
||||||
glPixelStorei(GL_PACK_ALIGNMENT, 4);
|
glPixelStorei(GL_PACK_ALIGNMENT, 4);
|
||||||
glPixelStorei(GL_PACK_ROW_LENGTH, 0);
|
glPixelStorei(GL_PACK_ROW_LENGTH, 0);
|
||||||
glBindFramebuffer(GL_READ_FRAMEBUFFER, framebuffer);
|
glBindFramebuffer(GL_READ_FRAMEBUFFER, framebuffer);
|
||||||
glBindBuffer(GL_PIXEL_PACK_BUFFER, slot.pixelPackBuffer);
|
glBindBuffer(GL_PIXEL_PACK_BUFFER, slot.pixelPackBuffer);
|
||||||
|
auto stageEndTime = std::chrono::steady_clock::now();
|
||||||
|
timing.asyncQueueSetupMilliseconds += std::chrono::duration_cast<std::chrono::duration<double, std::milli>>(stageEndTime - stageStartTime).count();
|
||||||
|
|
||||||
|
stageStartTime = std::chrono::steady_clock::now();
|
||||||
glBufferData(GL_PIXEL_PACK_BUFFER, static_cast<GLsizeiptr>(requiredBytes), nullptr, GL_STREAM_READ);
|
glBufferData(GL_PIXEL_PACK_BUFFER, static_cast<GLsizeiptr>(requiredBytes), nullptr, GL_STREAM_READ);
|
||||||
|
stageEndTime = std::chrono::steady_clock::now();
|
||||||
|
timing.asyncQueueBufferMilliseconds += std::chrono::duration_cast<std::chrono::duration<double, std::milli>>(stageEndTime - stageStartTime).count();
|
||||||
|
|
||||||
|
stageStartTime = std::chrono::steady_clock::now();
|
||||||
glReadPixels(0, 0, readWidth, readHeight, format, type, nullptr);
|
glReadPixels(0, 0, readWidth, readHeight, format, type, nullptr);
|
||||||
|
stageEndTime = std::chrono::steady_clock::now();
|
||||||
|
timing.asyncQueueReadPixelsMilliseconds += std::chrono::duration_cast<std::chrono::duration<double, std::milli>>(stageEndTime - stageStartTime).count();
|
||||||
|
|
||||||
|
stageStartTime = std::chrono::steady_clock::now();
|
||||||
slot.fence = glFenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE, 0);
|
slot.fence = glFenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE, 0);
|
||||||
|
stageEndTime = std::chrono::steady_clock::now();
|
||||||
|
timing.asyncQueueFenceMilliseconds += std::chrono::duration_cast<std::chrono::duration<double, std::milli>>(stageEndTime - stageStartTime).count();
|
||||||
slot.inFlight = slot.fence != nullptr;
|
slot.inFlight = slot.fence != nullptr;
|
||||||
glBindBuffer(GL_PIXEL_PACK_BUFFER, 0);
|
glBindBuffer(GL_PIXEL_PACK_BUFFER, 0);
|
||||||
|
|
||||||
mAsyncReadbackWriteIndex = (mAsyncReadbackWriteIndex + 1) % mAsyncReadbackSlots.size();
|
mAsyncReadbackWriteIndex = (mAsyncReadbackWriteIndex + 1) % mAsyncReadbackSlots.size();
|
||||||
|
finishTiming();
|
||||||
|
return slot.inFlight;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool OpenGLRenderPipeline::TryConsumeAsyncReadback(VideoIOOutputFrame& outputFrame, GLuint64 timeoutNanoseconds)
|
bool OpenGLRenderPipeline::TryConsumeAsyncReadback(VideoIOOutputFrame& outputFrame, GLuint64 timeoutNanoseconds, OutputReadbackTiming& timing)
|
||||||
{
|
{
|
||||||
if (mAsyncReadbackBytes == 0 || outputFrame.bytes == nullptr)
|
if (mAsyncReadbackBytes == 0 || outputFrame.bytes == nullptr)
|
||||||
return false;
|
return false;
|
||||||
@@ -206,15 +287,24 @@ bool OpenGLRenderPipeline::TryConsumeAsyncReadback(VideoIOOutputFrame& outputFra
|
|||||||
return false;
|
return false;
|
||||||
|
|
||||||
const GLenum waitFlags = timeoutNanoseconds > 0 ? GL_SYNC_FLUSH_COMMANDS_BIT : 0;
|
const GLenum waitFlags = timeoutNanoseconds > 0 ? GL_SYNC_FLUSH_COMMANDS_BIT : 0;
|
||||||
|
const auto waitStartTime = std::chrono::steady_clock::now();
|
||||||
const GLenum waitResult = glClientWaitSync(slot.fence, waitFlags, timeoutNanoseconds);
|
const GLenum waitResult = glClientWaitSync(slot.fence, waitFlags, timeoutNanoseconds);
|
||||||
|
const auto waitEndTime = std::chrono::steady_clock::now();
|
||||||
|
timing.fenceWaitMilliseconds += std::chrono::duration_cast<std::chrono::duration<double, std::milli>>(waitEndTime - waitStartTime).count();
|
||||||
if (waitResult != GL_ALREADY_SIGNALED && waitResult != GL_CONDITION_SATISFIED)
|
if (waitResult != GL_ALREADY_SIGNALED && waitResult != GL_CONDITION_SATISFIED)
|
||||||
|
{
|
||||||
|
timing.asyncReadbackMissed = true;
|
||||||
return false;
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
glDeleteSync(slot.fence);
|
glDeleteSync(slot.fence);
|
||||||
slot.fence = nullptr;
|
slot.fence = nullptr;
|
||||||
|
|
||||||
glBindBuffer(GL_PIXEL_PACK_BUFFER, slot.pixelPackBuffer);
|
glBindBuffer(GL_PIXEL_PACK_BUFFER, slot.pixelPackBuffer);
|
||||||
|
const auto mapStartTime = std::chrono::steady_clock::now();
|
||||||
void* mappedBytes = glMapBuffer(GL_PIXEL_PACK_BUFFER, GL_READ_ONLY);
|
void* mappedBytes = glMapBuffer(GL_PIXEL_PACK_BUFFER, GL_READ_ONLY);
|
||||||
|
const auto mapEndTime = std::chrono::steady_clock::now();
|
||||||
|
timing.mapMilliseconds += std::chrono::duration_cast<std::chrono::duration<double, std::milli>>(mapEndTime - mapStartTime).count();
|
||||||
if (mappedBytes == nullptr)
|
if (mappedBytes == nullptr)
|
||||||
{
|
{
|
||||||
glBindBuffer(GL_PIXEL_PACK_BUFFER, 0);
|
glBindBuffer(GL_PIXEL_PACK_BUFFER, 0);
|
||||||
@@ -223,7 +313,10 @@ bool OpenGLRenderPipeline::TryConsumeAsyncReadback(VideoIOOutputFrame& outputFra
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const auto copyStartTime = std::chrono::steady_clock::now();
|
||||||
std::memcpy(outputFrame.bytes, mappedBytes, slot.sizeBytes);
|
std::memcpy(outputFrame.bytes, mappedBytes, slot.sizeBytes);
|
||||||
|
const auto copyEndTime = std::chrono::steady_clock::now();
|
||||||
|
timing.copyMilliseconds += std::chrono::duration_cast<std::chrono::duration<double, std::milli>>(copyEndTime - copyStartTime).count();
|
||||||
glUnmapBuffer(GL_PIXEL_PACK_BUFFER);
|
glUnmapBuffer(GL_PIXEL_PACK_BUFFER);
|
||||||
glBindBuffer(GL_PIXEL_PACK_BUFFER, 0);
|
glBindBuffer(GL_PIXEL_PACK_BUFFER, 0);
|
||||||
|
|
||||||
@@ -243,40 +336,144 @@ void OpenGLRenderPipeline::CacheOutputFrame(const VideoIOOutputFrame& outputFram
|
|||||||
std::memcpy(mCachedOutputFrame.data(), outputFrame.bytes, byteCount);
|
std::memcpy(mCachedOutputFrame.data(), outputFrame.bytes, byteCount);
|
||||||
}
|
}
|
||||||
|
|
||||||
void OpenGLRenderPipeline::ReadOutputFrameSynchronously(const VideoIOState& state, void* destinationBytes)
|
bool OpenGLRenderPipeline::TryCopyCachedOutputFrame(VideoIOOutputFrame& outputFrame, OutputReadbackTiming& timing) const
|
||||||
{
|
{
|
||||||
|
if (outputFrame.bytes == nullptr || outputFrame.height == 0 || outputFrame.rowBytes <= 0)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
const std::size_t byteCount = static_cast<std::size_t>(outputFrame.rowBytes) * outputFrame.height;
|
||||||
|
if (mCachedOutputFrame.size() != byteCount)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
const auto copyStartTime = std::chrono::steady_clock::now();
|
||||||
|
std::memcpy(outputFrame.bytes, mCachedOutputFrame.data(), byteCount);
|
||||||
|
const auto copyEndTime = std::chrono::steady_clock::now();
|
||||||
|
timing.cachedCopyMilliseconds += std::chrono::duration_cast<std::chrono::duration<double, std::milli>>(copyEndTime - copyStartTime).count();
|
||||||
|
timing.cachedFallbackUsed = true;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void OpenGLRenderPipeline::ReadOutputFrameSynchronously(const VideoIOState& state, void* destinationBytes, OutputReadbackTiming& timing)
|
||||||
|
{
|
||||||
|
const auto readStartTime = std::chrono::steady_clock::now();
|
||||||
const bool usePackedOutput = state.outputPixelFormat == VideoIOPixelFormat::V210 || state.outputPixelFormat == VideoIOPixelFormat::Yuva10;
|
const bool usePackedOutput = state.outputPixelFormat == VideoIOPixelFormat::V210 || state.outputPixelFormat == VideoIOPixelFormat::Yuva10;
|
||||||
|
const bool usePackFramebuffer = state.outputPixelFormat == VideoIOPixelFormat::Bgra8 || usePackedOutput;
|
||||||
|
|
||||||
glPixelStorei(GL_PACK_ALIGNMENT, 4);
|
glPixelStorei(GL_PACK_ALIGNMENT, 4);
|
||||||
glPixelStorei(GL_PACK_ROW_LENGTH, 0);
|
glPixelStorei(GL_PACK_ROW_LENGTH, 0);
|
||||||
if (usePackedOutput)
|
if (usePackFramebuffer)
|
||||||
{
|
{
|
||||||
glBindFramebuffer(GL_READ_FRAMEBUFFER, mRenderer.OutputPackFramebuffer());
|
glBindFramebuffer(GL_READ_FRAMEBUFFER, mRenderer.OutputPackFramebuffer());
|
||||||
|
if (usePackedOutput)
|
||||||
glReadPixels(0, 0, state.outputPackTextureWidth, state.outputFrameSize.height, GL_RGBA, GL_UNSIGNED_BYTE, destinationBytes);
|
glReadPixels(0, 0, state.outputPackTextureWidth, state.outputFrameSize.height, GL_RGBA, GL_UNSIGNED_BYTE, destinationBytes);
|
||||||
|
else
|
||||||
|
glReadPixels(0, 0, state.outputFrameSize.width, state.outputFrameSize.height, GL_BGRA, GL_UNSIGNED_INT_8_8_8_8_REV, destinationBytes);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
glBindFramebuffer(GL_READ_FRAMEBUFFER, mRenderer.OutputFramebuffer());
|
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);
|
glReadPixels(0, 0, state.outputFrameSize.width, state.outputFrameSize.height, GL_BGRA, GL_UNSIGNED_INT_8_8_8_8_REV, destinationBytes);
|
||||||
}
|
}
|
||||||
|
const auto readEndTime = std::chrono::steady_clock::now();
|
||||||
|
timing.syncReadMilliseconds += std::chrono::duration_cast<std::chrono::duration<double, std::milli>>(readEndTime - readStartTime).count();
|
||||||
|
timing.syncFallbackUsed = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
void OpenGLRenderPipeline::ReadOutputFrame(const VideoIOState& state, VideoIOOutputFrame& outputFrame)
|
OpenGLRenderPipeline::OutputReadbackTiming OpenGLRenderPipeline::ReadOutputFrame(const VideoIOState& state, VideoIOOutputFrame& outputFrame)
|
||||||
{
|
{
|
||||||
if (TryConsumeAsyncReadback(outputFrame, 500000))
|
OutputReadbackTiming timing;
|
||||||
{
|
|
||||||
QueueAsyncReadback(state);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// If async readback misses the playout deadline, prefer a fresh synchronous
|
if (mOutputReadbackMode == OutputReadbackMode::Synchronous)
|
||||||
// frame over reusing stale cached output, then restart the async pipeline.
|
{
|
||||||
if (outputFrame.bytes != nullptr)
|
if (outputFrame.bytes != nullptr)
|
||||||
{
|
{
|
||||||
ReadOutputFrameSynchronously(state, outputFrame.bytes);
|
ReadOutputFrameSynchronously(state, outputFrame.bytes, timing);
|
||||||
|
CacheOutputFrame(outputFrame);
|
||||||
|
}
|
||||||
|
return timing;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mOutputReadbackMode == OutputReadbackMode::CachedOnly)
|
||||||
|
{
|
||||||
|
if (TryCopyCachedOutputFrame(outputFrame, timing))
|
||||||
|
return timing;
|
||||||
|
|
||||||
|
if (outputFrame.bytes != nullptr)
|
||||||
|
{
|
||||||
|
ReadOutputFrameSynchronously(state, outputFrame.bytes, timing);
|
||||||
|
CacheOutputFrame(outputFrame);
|
||||||
|
}
|
||||||
|
return timing;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (TryConsumeAsyncReadback(outputFrame, 0, timing))
|
||||||
|
{
|
||||||
|
(void)QueueAsyncReadback(state, timing);
|
||||||
|
return timing;
|
||||||
|
}
|
||||||
|
|
||||||
|
const bool queued = QueueAsyncReadback(state, timing);
|
||||||
|
|
||||||
|
if (queued && TryConsumeAsyncReadback(outputFrame, 0, timing))
|
||||||
|
return timing;
|
||||||
|
|
||||||
|
if (TryCopyCachedOutputFrame(outputFrame, timing))
|
||||||
|
{
|
||||||
|
return timing;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bootstrap only: until the first async readback has produced cached output,
|
||||||
|
// use one synchronous readback so DeckLink has a valid frame to schedule.
|
||||||
|
if (outputFrame.bytes != nullptr && mCachedOutputFrame.empty())
|
||||||
|
{
|
||||||
|
ReadOutputFrameSynchronously(state, outputFrame.bytes, timing);
|
||||||
CacheOutputFrame(outputFrame);
|
CacheOutputFrame(outputFrame);
|
||||||
}
|
}
|
||||||
|
|
||||||
FlushAsyncReadbackPipeline();
|
if (!queued)
|
||||||
QueueAsyncReadback(state);
|
(void)QueueAsyncReadback(state, timing);
|
||||||
|
return timing;
|
||||||
|
}
|
||||||
|
|
||||||
|
OpenGLRenderPipeline::OutputReadbackMode OpenGLRenderPipeline::ReadOutputReadbackModeFromEnvironment()
|
||||||
|
{
|
||||||
|
char* mode = nullptr;
|
||||||
|
std::size_t modeSize = 0;
|
||||||
|
if (_dupenv_s(&mode, &modeSize, "VST_OUTPUT_READBACK_MODE") != 0 || mode == nullptr)
|
||||||
|
return OutputReadbackMode::AsyncPbo;
|
||||||
|
|
||||||
|
const std::string modeValue(mode);
|
||||||
|
std::free(mode);
|
||||||
|
if (modeValue == "async_pbo")
|
||||||
|
return OutputReadbackMode::AsyncPbo;
|
||||||
|
if (modeValue == "sync")
|
||||||
|
return OutputReadbackMode::Synchronous;
|
||||||
|
if (modeValue == "cached_only")
|
||||||
|
return OutputReadbackMode::CachedOnly;
|
||||||
|
|
||||||
|
return OutputReadbackMode::AsyncPbo;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::size_t OpenGLRenderPipeline::ReadAsyncReadbackDepthFromEnvironment()
|
||||||
|
{
|
||||||
|
char* depthValue = nullptr;
|
||||||
|
std::size_t depthValueSize = 0;
|
||||||
|
if (_dupenv_s(&depthValue, &depthValueSize, "VST_OUTPUT_READBACK_DEPTH") != 0 || depthValue == nullptr)
|
||||||
|
return 6;
|
||||||
|
|
||||||
|
const std::string value(depthValue);
|
||||||
|
std::free(depthValue);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
const unsigned long requestedDepth = std::stoul(value);
|
||||||
|
if (requestedDepth < 3)
|
||||||
|
return 3;
|
||||||
|
if (requestedDepth > 12)
|
||||||
|
return 12;
|
||||||
|
return static_cast<std::size_t>(requestedDepth);
|
||||||
|
}
|
||||||
|
catch (...)
|
||||||
|
{
|
||||||
|
return 6;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,6 @@
|
|||||||
#include "GLExtensions.h"
|
#include "GLExtensions.h"
|
||||||
#include "VideoIOTypes.h"
|
#include "VideoIOTypes.h"
|
||||||
|
|
||||||
#include <array>
|
|
||||||
#include <functional>
|
#include <functional>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
@@ -36,6 +35,13 @@ public:
|
|||||||
bool RenderFrame(const RenderPipelineFrameContext& context, VideoIOOutputFrame& outputFrame);
|
bool RenderFrame(const RenderPipelineFrameContext& context, VideoIOOutputFrame& outputFrame);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
enum class OutputReadbackMode
|
||||||
|
{
|
||||||
|
AsyncPbo,
|
||||||
|
Synchronous,
|
||||||
|
CachedOnly
|
||||||
|
};
|
||||||
|
|
||||||
struct AsyncReadbackSlot
|
struct AsyncReadbackSlot
|
||||||
{
|
{
|
||||||
GLuint pixelPackBuffer = 0;
|
GLuint pixelPackBuffer = 0;
|
||||||
@@ -44,15 +50,36 @@ private:
|
|||||||
bool inFlight = false;
|
bool inFlight = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
struct OutputReadbackTiming
|
||||||
|
{
|
||||||
|
double fenceWaitMilliseconds = 0.0;
|
||||||
|
double mapMilliseconds = 0.0;
|
||||||
|
double copyMilliseconds = 0.0;
|
||||||
|
double cachedCopyMilliseconds = 0.0;
|
||||||
|
double asyncQueueMilliseconds = 0.0;
|
||||||
|
double asyncQueueBufferMilliseconds = 0.0;
|
||||||
|
double asyncQueueSetupMilliseconds = 0.0;
|
||||||
|
double asyncQueueReadPixelsMilliseconds = 0.0;
|
||||||
|
double asyncQueueFenceMilliseconds = 0.0;
|
||||||
|
double syncReadMilliseconds = 0.0;
|
||||||
|
bool asyncReadbackMissed = false;
|
||||||
|
bool cachedFallbackUsed = false;
|
||||||
|
bool syncFallbackUsed = false;
|
||||||
|
};
|
||||||
|
|
||||||
bool EnsureAsyncReadbackBuffers(std::size_t requiredBytes);
|
bool EnsureAsyncReadbackBuffers(std::size_t requiredBytes);
|
||||||
void ResetAsyncReadbackState();
|
void ResetAsyncReadbackState();
|
||||||
void FlushAsyncReadbackPipeline();
|
void FlushAsyncReadbackPipeline();
|
||||||
void QueueAsyncReadback(const VideoIOState& state);
|
bool QueueAsyncReadback(const VideoIOState& state, OutputReadbackTiming& timing);
|
||||||
bool TryConsumeAsyncReadback(VideoIOOutputFrame& outputFrame, GLuint64 timeoutNanoseconds);
|
bool TryConsumeAsyncReadback(VideoIOOutputFrame& outputFrame, GLuint64 timeoutNanoseconds, OutputReadbackTiming& timing);
|
||||||
void CacheOutputFrame(const VideoIOOutputFrame& outputFrame);
|
void CacheOutputFrame(const VideoIOOutputFrame& outputFrame);
|
||||||
void ReadOutputFrameSynchronously(const VideoIOState& state, void* destinationBytes);
|
bool TryCopyCachedOutputFrame(VideoIOOutputFrame& outputFrame, OutputReadbackTiming& timing) const;
|
||||||
|
void ReadOutputFrameSynchronously(const VideoIOState& state, void* destinationBytes, OutputReadbackTiming& timing);
|
||||||
|
void PackOutputForBgra8(const VideoIOState& state);
|
||||||
void PackOutputFor10Bit(const VideoIOState& state);
|
void PackOutputFor10Bit(const VideoIOState& state);
|
||||||
void ReadOutputFrame(const VideoIOState& state, VideoIOOutputFrame& outputFrame);
|
OutputReadbackTiming ReadOutputFrame(const VideoIOState& state, VideoIOOutputFrame& outputFrame);
|
||||||
|
static OutputReadbackMode ReadOutputReadbackModeFromEnvironment();
|
||||||
|
static std::size_t ReadAsyncReadbackDepthFromEnvironment();
|
||||||
|
|
||||||
OpenGLRenderer& mRenderer;
|
OpenGLRenderer& mRenderer;
|
||||||
RuntimeSnapshotProvider& mRuntimeSnapshotProvider;
|
RuntimeSnapshotProvider& mRuntimeSnapshotProvider;
|
||||||
@@ -60,7 +87,9 @@ private:
|
|||||||
RenderEffectCallback mRenderEffect;
|
RenderEffectCallback mRenderEffect;
|
||||||
OutputReadyCallback mOutputReady;
|
OutputReadyCallback mOutputReady;
|
||||||
PaintCallback mPaint;
|
PaintCallback mPaint;
|
||||||
std::array<AsyncReadbackSlot, 3> mAsyncReadbackSlots;
|
OutputReadbackMode mOutputReadbackMode = OutputReadbackMode::AsyncPbo;
|
||||||
|
std::vector<AsyncReadbackSlot> mAsyncReadbackSlots;
|
||||||
|
std::size_t mAsyncReadbackDepth = 0;
|
||||||
std::size_t mAsyncReadbackWriteIndex = 0;
|
std::size_t mAsyncReadbackWriteIndex = 0;
|
||||||
std::size_t mAsyncReadbackReadIndex = 0;
|
std::size_t mAsyncReadbackReadIndex = 0;
|
||||||
std::size_t mAsyncReadbackBytes = 0;
|
std::size_t mAsyncReadbackBytes = 0;
|
||||||
|
|||||||
@@ -7,4 +7,3 @@ constexpr GLuint kDecodedVideoTextureUnit = 1;
|
|||||||
constexpr GLuint kSourceHistoryTextureUnitBase = 2;
|
constexpr GLuint kSourceHistoryTextureUnitBase = 2;
|
||||||
constexpr GLuint kPackedVideoTextureUnit = 2;
|
constexpr GLuint kPackedVideoTextureUnit = 2;
|
||||||
constexpr GLuint kGlobalParamsBindingPoint = 0;
|
constexpr GLuint kGlobalParamsBindingPoint = 0;
|
||||||
constexpr unsigned kPrerollFrameCount = 12;
|
|
||||||
|
|||||||
@@ -37,5 +37,8 @@ struct PersistenceSnapshot
|
|||||||
std::filesystem::path targetPath;
|
std::filesystem::path targetPath;
|
||||||
std::string contents;
|
std::string contents;
|
||||||
std::string reason;
|
std::string reason;
|
||||||
|
std::string debounceKey;
|
||||||
|
bool debounceAllowed = false;
|
||||||
|
bool flushRequested = false;
|
||||||
uint64_t generation = 0;
|
uint64_t generation = 0;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,10 +2,125 @@
|
|||||||
|
|
||||||
#include <windows.h>
|
#include <windows.h>
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
#include <filesystem>
|
#include <filesystem>
|
||||||
#include <fstream>
|
#include <fstream>
|
||||||
|
#include <utility>
|
||||||
|
|
||||||
bool PersistenceWriter::WriteSnapshot(const PersistenceSnapshot& snapshot, std::string& error) const
|
PersistenceWriter::PersistenceWriter(std::chrono::milliseconds debounceDelay, SnapshotSink sink) :
|
||||||
|
mDebounceDelay(debounceDelay),
|
||||||
|
mSink(std::move(sink))
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
PersistenceWriter::~PersistenceWriter()
|
||||||
|
{
|
||||||
|
std::string error;
|
||||||
|
StopAndFlush((std::chrono::milliseconds::max)(), error);
|
||||||
|
}
|
||||||
|
|
||||||
|
void PersistenceWriter::SetResultCallback(ResultCallback callback)
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(mMutex);
|
||||||
|
mResultCallback = std::move(callback);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool PersistenceWriter::WriteSnapshot(const PersistenceSnapshot& snapshot, std::string& error)
|
||||||
|
{
|
||||||
|
if (!ValidateSnapshot(snapshot, error))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
const bool succeeded = WriteSnapshotThroughSink(snapshot, error);
|
||||||
|
PublishWriteResult(snapshot, succeeded, error, false);
|
||||||
|
return succeeded;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool PersistenceWriter::EnqueueSnapshot(const PersistenceSnapshot& snapshot, std::string& error)
|
||||||
|
{
|
||||||
|
if (!ValidateSnapshot(snapshot, error))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
std::lock_guard<std::mutex> lock(mMutex);
|
||||||
|
if (!mAcceptingRequests)
|
||||||
|
{
|
||||||
|
error = "Persistence writer is stopping.";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
StartWorkerLocked();
|
||||||
|
|
||||||
|
const auto now = std::chrono::steady_clock::now();
|
||||||
|
if (snapshot.debounceAllowed)
|
||||||
|
{
|
||||||
|
const std::string debounceKey = snapshot.debounceKey.empty() ? snapshot.targetPath.string() : snapshot.debounceKey;
|
||||||
|
PendingSnapshot& pending = mDebouncedSnapshots[debounceKey];
|
||||||
|
if (!pending.snapshot.targetPath.empty())
|
||||||
|
++mCoalescedCount;
|
||||||
|
else
|
||||||
|
++mEnqueuedCount;
|
||||||
|
|
||||||
|
pending.snapshot = snapshot;
|
||||||
|
pending.readyAt = snapshot.flushRequested ? now : now + mDebounceDelay;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
mImmediateSnapshots.push_back(snapshot);
|
||||||
|
++mEnqueuedCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
mCondition.notify_one();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool PersistenceWriter::StopAndFlush(std::chrono::milliseconds timeout, std::string& error)
|
||||||
|
{
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(mMutex);
|
||||||
|
mAcceptingRequests = false;
|
||||||
|
mStopping = true;
|
||||||
|
const auto now = std::chrono::steady_clock::now();
|
||||||
|
for (auto& entry : mDebouncedSnapshots)
|
||||||
|
entry.second.readyAt = now;
|
||||||
|
}
|
||||||
|
mCondition.notify_all();
|
||||||
|
|
||||||
|
std::unique_lock<std::mutex> lock(mMutex);
|
||||||
|
if (mWorkerRunning)
|
||||||
|
{
|
||||||
|
if (timeout == (std::chrono::milliseconds::max)())
|
||||||
|
{
|
||||||
|
mCondition.wait(lock, [this]() { return !mWorkerRunning; });
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
const auto deadline = std::chrono::steady_clock::now() + timeout;
|
||||||
|
if (!mCondition.wait_until(lock, deadline, [this]() { return !mWorkerRunning; }))
|
||||||
|
{
|
||||||
|
error = "Timed out while flushing persistence writer.";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
lock.unlock();
|
||||||
|
|
||||||
|
if (mWorker.joinable())
|
||||||
|
mWorker.join();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
PersistenceWriterMetrics PersistenceWriter::GetMetrics() const
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(mMutex);
|
||||||
|
PersistenceWriterMetrics metrics;
|
||||||
|
metrics.pendingCount = PendingCountLocked();
|
||||||
|
metrics.enqueuedCount = mEnqueuedCount;
|
||||||
|
metrics.coalescedCount = mCoalescedCount;
|
||||||
|
metrics.writtenCount = mWrittenCount;
|
||||||
|
metrics.failedCount = mFailedCount;
|
||||||
|
return metrics;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool PersistenceWriter::ValidateSnapshot(const PersistenceSnapshot& snapshot, std::string& error) const
|
||||||
{
|
{
|
||||||
if (snapshot.targetPath.empty())
|
if (snapshot.targetPath.empty())
|
||||||
{
|
{
|
||||||
@@ -13,6 +128,14 @@ bool PersistenceWriter::WriteSnapshot(const PersistenceSnapshot& snapshot, std::
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool PersistenceWriter::WriteSnapshotThroughSink(const PersistenceSnapshot& snapshot, std::string& error) const
|
||||||
|
{
|
||||||
|
if (mSink)
|
||||||
|
return mSink(snapshot, error);
|
||||||
|
|
||||||
std::error_code fsError;
|
std::error_code fsError;
|
||||||
std::filesystem::create_directories(snapshot.targetPath.parent_path(), fsError);
|
std::filesystem::create_directories(snapshot.targetPath.parent_path(), fsError);
|
||||||
|
|
||||||
@@ -42,3 +165,107 @@ bool PersistenceWriter::WriteSnapshot(const PersistenceSnapshot& snapshot, std::
|
|||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void PersistenceWriter::PublishWriteResult(const PersistenceSnapshot& snapshot, bool succeeded, const std::string& errorMessage, bool newerRequestPending)
|
||||||
|
{
|
||||||
|
ResultCallback callback;
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(mMutex);
|
||||||
|
callback = mResultCallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!callback)
|
||||||
|
return;
|
||||||
|
|
||||||
|
PersistenceWriteResult result;
|
||||||
|
result.targetKind = snapshot.targetKind;
|
||||||
|
result.targetPath = snapshot.targetPath.string();
|
||||||
|
result.reason = snapshot.reason;
|
||||||
|
result.succeeded = succeeded;
|
||||||
|
result.errorMessage = errorMessage;
|
||||||
|
result.newerRequestPending = newerRequestPending;
|
||||||
|
callback(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
void PersistenceWriter::StartWorkerLocked()
|
||||||
|
{
|
||||||
|
if (mWorkerRunning)
|
||||||
|
return;
|
||||||
|
|
||||||
|
mWorkerRunning = true;
|
||||||
|
mWorker = std::thread([this]() { WorkerMain(); });
|
||||||
|
}
|
||||||
|
|
||||||
|
void PersistenceWriter::WorkerMain()
|
||||||
|
{
|
||||||
|
for (;;)
|
||||||
|
{
|
||||||
|
PersistenceSnapshot snapshot;
|
||||||
|
{
|
||||||
|
std::unique_lock<std::mutex> lock(mMutex);
|
||||||
|
for (;;)
|
||||||
|
{
|
||||||
|
if (!mImmediateSnapshots.empty())
|
||||||
|
{
|
||||||
|
snapshot = std::move(mImmediateSnapshots.front());
|
||||||
|
mImmediateSnapshots.pop_front();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!mDebouncedSnapshots.empty())
|
||||||
|
{
|
||||||
|
const auto now = std::chrono::steady_clock::now();
|
||||||
|
auto readyIt = mDebouncedSnapshots.end();
|
||||||
|
auto nextReadyAt = (std::chrono::steady_clock::time_point::max)();
|
||||||
|
for (auto it = mDebouncedSnapshots.begin(); it != mDebouncedSnapshots.end(); ++it)
|
||||||
|
{
|
||||||
|
if (it->second.readyAt <= now)
|
||||||
|
{
|
||||||
|
readyIt = it;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (it->second.readyAt < nextReadyAt)
|
||||||
|
nextReadyAt = it->second.readyAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (readyIt != mDebouncedSnapshots.end())
|
||||||
|
{
|
||||||
|
snapshot = std::move(readyIt->second.snapshot);
|
||||||
|
mDebouncedSnapshots.erase(readyIt);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
mCondition.wait_until(lock, nextReadyAt);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mStopping)
|
||||||
|
{
|
||||||
|
mWorkerRunning = false;
|
||||||
|
mCondition.notify_all();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
mCondition.wait(lock);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string error;
|
||||||
|
const bool succeeded = WriteSnapshotThroughSink(snapshot, error);
|
||||||
|
bool newerRequestPending = false;
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(mMutex);
|
||||||
|
if (succeeded)
|
||||||
|
++mWrittenCount;
|
||||||
|
else
|
||||||
|
++mFailedCount;
|
||||||
|
newerRequestPending = PendingCountLocked() > 0;
|
||||||
|
}
|
||||||
|
PublishWriteResult(snapshot, succeeded, error, newerRequestPending);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
std::size_t PersistenceWriter::PendingCountLocked() const
|
||||||
|
{
|
||||||
|
return mImmediateSnapshots.size() + mDebouncedSnapshots.size();
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,10 +2,79 @@
|
|||||||
|
|
||||||
#include "PersistenceRequest.h"
|
#include "PersistenceRequest.h"
|
||||||
|
|
||||||
|
#include <chrono>
|
||||||
|
#include <condition_variable>
|
||||||
|
#include <cstdint>
|
||||||
|
#include <deque>
|
||||||
|
#include <functional>
|
||||||
|
#include <mutex>
|
||||||
#include <string>
|
#include <string>
|
||||||
|
#include <thread>
|
||||||
|
#include <unordered_map>
|
||||||
|
|
||||||
|
struct PersistenceWriterMetrics
|
||||||
|
{
|
||||||
|
std::size_t pendingCount = 0;
|
||||||
|
uint64_t enqueuedCount = 0;
|
||||||
|
uint64_t coalescedCount = 0;
|
||||||
|
uint64_t writtenCount = 0;
|
||||||
|
uint64_t failedCount = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct PersistenceWriteResult
|
||||||
|
{
|
||||||
|
PersistenceTargetKind targetKind = PersistenceTargetKind::RuntimeState;
|
||||||
|
std::string targetPath;
|
||||||
|
std::string reason;
|
||||||
|
bool succeeded = false;
|
||||||
|
std::string errorMessage;
|
||||||
|
bool newerRequestPending = false;
|
||||||
|
};
|
||||||
|
|
||||||
class PersistenceWriter
|
class PersistenceWriter
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
bool WriteSnapshot(const PersistenceSnapshot& snapshot, std::string& error) const;
|
using SnapshotSink = std::function<bool(const PersistenceSnapshot&, std::string&)>;
|
||||||
|
using ResultCallback = std::function<void(const PersistenceWriteResult&)>;
|
||||||
|
|
||||||
|
explicit PersistenceWriter(
|
||||||
|
std::chrono::milliseconds debounceDelay = std::chrono::milliseconds(50),
|
||||||
|
SnapshotSink sink = SnapshotSink());
|
||||||
|
~PersistenceWriter();
|
||||||
|
|
||||||
|
void SetResultCallback(ResultCallback callback);
|
||||||
|
bool WriteSnapshot(const PersistenceSnapshot& snapshot, std::string& error);
|
||||||
|
bool EnqueueSnapshot(const PersistenceSnapshot& snapshot, std::string& error);
|
||||||
|
bool StopAndFlush(std::chrono::milliseconds timeout, std::string& error);
|
||||||
|
PersistenceWriterMetrics GetMetrics() const;
|
||||||
|
|
||||||
|
private:
|
||||||
|
struct PendingSnapshot
|
||||||
|
{
|
||||||
|
PersistenceSnapshot snapshot;
|
||||||
|
std::chrono::steady_clock::time_point readyAt;
|
||||||
|
};
|
||||||
|
|
||||||
|
bool ValidateSnapshot(const PersistenceSnapshot& snapshot, std::string& error) const;
|
||||||
|
bool WriteSnapshotThroughSink(const PersistenceSnapshot& snapshot, std::string& error) const;
|
||||||
|
void PublishWriteResult(const PersistenceSnapshot& snapshot, bool succeeded, const std::string& errorMessage, bool newerRequestPending);
|
||||||
|
void StartWorkerLocked();
|
||||||
|
void WorkerMain();
|
||||||
|
std::size_t PendingCountLocked() const;
|
||||||
|
|
||||||
|
std::chrono::milliseconds mDebounceDelay;
|
||||||
|
SnapshotSink mSink;
|
||||||
|
ResultCallback mResultCallback;
|
||||||
|
mutable std::mutex mMutex;
|
||||||
|
std::condition_variable mCondition;
|
||||||
|
std::thread mWorker;
|
||||||
|
bool mWorkerRunning = false;
|
||||||
|
bool mStopping = false;
|
||||||
|
bool mAcceptingRequests = true;
|
||||||
|
std::unordered_map<std::string, PendingSnapshot> mDebouncedSnapshots;
|
||||||
|
std::deque<PersistenceSnapshot> mImmediateSnapshots;
|
||||||
|
uint64_t mEnqueuedCount = 0;
|
||||||
|
uint64_t mCoalescedCount = 0;
|
||||||
|
uint64_t mWrittenCount = 0;
|
||||||
|
uint64_t mFailedCount = 0;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -51,6 +51,11 @@ JsonValue RuntimeStatePresenter::BuildRuntimeStateValue(const RuntimeStore& runt
|
|||||||
deckLink.set("externalKeyingRequested", JsonValue(telemetrySnapshot.videoIO.externalKeyingRequested));
|
deckLink.set("externalKeyingRequested", JsonValue(telemetrySnapshot.videoIO.externalKeyingRequested));
|
||||||
deckLink.set("externalKeyingActive", JsonValue(telemetrySnapshot.videoIO.externalKeyingActive));
|
deckLink.set("externalKeyingActive", JsonValue(telemetrySnapshot.videoIO.externalKeyingActive));
|
||||||
deckLink.set("statusMessage", JsonValue(telemetrySnapshot.videoIO.statusMessage));
|
deckLink.set("statusMessage", JsonValue(telemetrySnapshot.videoIO.statusMessage));
|
||||||
|
deckLink.set("actualBufferedFramesAvailable", JsonValue(telemetrySnapshot.backendPlayout.actualDeckLinkBufferedFramesAvailable));
|
||||||
|
deckLink.set("actualBufferedFrames", JsonValue(static_cast<double>(telemetrySnapshot.backendPlayout.actualDeckLinkBufferedFrames)));
|
||||||
|
deckLink.set("targetBufferedFrames", JsonValue(static_cast<double>(telemetrySnapshot.backendPlayout.targetDeckLinkBufferedFrames)));
|
||||||
|
deckLink.set("scheduleCallMs", JsonValue(telemetrySnapshot.backendPlayout.deckLinkScheduleCallMilliseconds));
|
||||||
|
deckLink.set("scheduleFailures", JsonValue(static_cast<double>(telemetrySnapshot.backendPlayout.deckLinkScheduleFailureCount)));
|
||||||
root.set("decklink", deckLink);
|
root.set("decklink", deckLink);
|
||||||
|
|
||||||
JsonValue videoIO = JsonValue::MakeObject();
|
JsonValue videoIO = JsonValue::MakeObject();
|
||||||
@@ -80,6 +85,86 @@ JsonValue RuntimeStatePresenter::BuildRuntimeStateValue(const RuntimeStore& runt
|
|||||||
performance.set("flushedFrameCount", JsonValue(static_cast<double>(telemetrySnapshot.performance.flushedFrameCount)));
|
performance.set("flushedFrameCount", JsonValue(static_cast<double>(telemetrySnapshot.performance.flushedFrameCount)));
|
||||||
root.set("performance", performance);
|
root.set("performance", performance);
|
||||||
|
|
||||||
|
JsonValue readyQueue = JsonValue::MakeObject();
|
||||||
|
readyQueue.set("depth", JsonValue(static_cast<double>(telemetrySnapshot.backendPlayout.readyQueueDepth)));
|
||||||
|
readyQueue.set("capacity", JsonValue(static_cast<double>(telemetrySnapshot.backendPlayout.readyQueueCapacity)));
|
||||||
|
readyQueue.set("minDepth", JsonValue(static_cast<double>(telemetrySnapshot.backendPlayout.minReadyQueueDepth)));
|
||||||
|
readyQueue.set("maxDepth", JsonValue(static_cast<double>(telemetrySnapshot.backendPlayout.maxReadyQueueDepth)));
|
||||||
|
readyQueue.set("zeroDepthCount", JsonValue(static_cast<double>(telemetrySnapshot.backendPlayout.readyQueueZeroDepthCount)));
|
||||||
|
readyQueue.set("pushedCount", JsonValue(static_cast<double>(telemetrySnapshot.backendPlayout.readyQueuePushedCount)));
|
||||||
|
readyQueue.set("poppedCount", JsonValue(static_cast<double>(telemetrySnapshot.backendPlayout.readyQueuePoppedCount)));
|
||||||
|
readyQueue.set("droppedCount", JsonValue(static_cast<double>(telemetrySnapshot.backendPlayout.readyQueueDroppedCount)));
|
||||||
|
readyQueue.set("underrunCount", JsonValue(static_cast<double>(telemetrySnapshot.backendPlayout.readyQueueUnderrunCount)));
|
||||||
|
|
||||||
|
JsonValue systemMemory = JsonValue::MakeObject();
|
||||||
|
systemMemory.set("freeFrameCount", JsonValue(static_cast<double>(telemetrySnapshot.backendPlayout.systemFramePoolFree)));
|
||||||
|
systemMemory.set("readyFrameCount", JsonValue(static_cast<double>(telemetrySnapshot.backendPlayout.systemFramePoolReady)));
|
||||||
|
systemMemory.set("scheduledFrameCount", JsonValue(static_cast<double>(telemetrySnapshot.backendPlayout.systemFramePoolScheduled)));
|
||||||
|
systemMemory.set("underrunCount", JsonValue(static_cast<double>(telemetrySnapshot.backendPlayout.systemFrameUnderrunCount)));
|
||||||
|
systemMemory.set("repeatCount", JsonValue(static_cast<double>(telemetrySnapshot.backendPlayout.systemFrameRepeatCount)));
|
||||||
|
systemMemory.set("dropCount", JsonValue(static_cast<double>(telemetrySnapshot.backendPlayout.systemFrameDropCount)));
|
||||||
|
systemMemory.set("ageAtScheduleMs", JsonValue(telemetrySnapshot.backendPlayout.systemFrameAgeAtScheduleMilliseconds));
|
||||||
|
systemMemory.set("ageAtCompletionMs", JsonValue(telemetrySnapshot.backendPlayout.systemFrameAgeAtCompletionMilliseconds));
|
||||||
|
|
||||||
|
JsonValue outputRender = JsonValue::MakeObject();
|
||||||
|
outputRender.set("renderMs", JsonValue(telemetrySnapshot.backendPlayout.outputRenderMilliseconds));
|
||||||
|
outputRender.set("smoothedRenderMs", JsonValue(telemetrySnapshot.backendPlayout.smoothedOutputRenderMilliseconds));
|
||||||
|
outputRender.set("maxRenderMs", JsonValue(telemetrySnapshot.backendPlayout.maxOutputRenderMilliseconds));
|
||||||
|
outputRender.set("acquireFrameMs", JsonValue(telemetrySnapshot.backendPlayout.outputFrameAcquireMilliseconds));
|
||||||
|
outputRender.set("renderRequestMs", JsonValue(telemetrySnapshot.backendPlayout.outputFrameRenderRequestMilliseconds));
|
||||||
|
outputRender.set("endAccessMs", JsonValue(telemetrySnapshot.backendPlayout.outputFrameEndAccessMilliseconds));
|
||||||
|
outputRender.set("queueWaitMs", JsonValue(telemetrySnapshot.backendPlayout.outputRenderQueueWaitMilliseconds));
|
||||||
|
outputRender.set("drawMs", JsonValue(telemetrySnapshot.backendPlayout.outputRenderDrawMilliseconds));
|
||||||
|
outputRender.set("fenceWaitMs", JsonValue(telemetrySnapshot.backendPlayout.outputReadbackFenceWaitMilliseconds));
|
||||||
|
outputRender.set("mapMs", JsonValue(telemetrySnapshot.backendPlayout.outputReadbackMapMilliseconds));
|
||||||
|
outputRender.set("readbackCopyMs", JsonValue(telemetrySnapshot.backendPlayout.outputReadbackCopyMilliseconds));
|
||||||
|
outputRender.set("cachedCopyMs", JsonValue(telemetrySnapshot.backendPlayout.outputCachedCopyMilliseconds));
|
||||||
|
outputRender.set("asyncQueueMs", JsonValue(telemetrySnapshot.backendPlayout.outputAsyncQueueMilliseconds));
|
||||||
|
outputRender.set("asyncQueueBufferMs", JsonValue(telemetrySnapshot.backendPlayout.outputAsyncQueueBufferMilliseconds));
|
||||||
|
outputRender.set("asyncQueueSetupMs", JsonValue(telemetrySnapshot.backendPlayout.outputAsyncQueueSetupMilliseconds));
|
||||||
|
outputRender.set("asyncQueueReadPixelsMs", JsonValue(telemetrySnapshot.backendPlayout.outputAsyncQueueReadPixelsMilliseconds));
|
||||||
|
outputRender.set("asyncQueueFenceMs", JsonValue(telemetrySnapshot.backendPlayout.outputAsyncQueueFenceMilliseconds));
|
||||||
|
outputRender.set("syncReadMs", JsonValue(telemetrySnapshot.backendPlayout.outputSyncReadMilliseconds));
|
||||||
|
outputRender.set("asyncReadbackMissCount", JsonValue(static_cast<double>(telemetrySnapshot.backendPlayout.outputAsyncReadbackMissCount)));
|
||||||
|
outputRender.set("cachedFallbackCount", JsonValue(static_cast<double>(telemetrySnapshot.backendPlayout.outputCachedFallbackCount)));
|
||||||
|
outputRender.set("syncFallbackCount", JsonValue(static_cast<double>(telemetrySnapshot.backendPlayout.outputSyncFallbackCount)));
|
||||||
|
|
||||||
|
JsonValue recovery = JsonValue::MakeObject();
|
||||||
|
recovery.set("completionResult", JsonValue(telemetrySnapshot.backendPlayout.completionResult));
|
||||||
|
recovery.set("completedFrameIndex", JsonValue(static_cast<double>(telemetrySnapshot.backendPlayout.completedFrameIndex)));
|
||||||
|
recovery.set("scheduledFrameIndex", JsonValue(static_cast<double>(telemetrySnapshot.backendPlayout.scheduledFrameIndex)));
|
||||||
|
recovery.set("scheduledLeadFrames", JsonValue(static_cast<double>(telemetrySnapshot.backendPlayout.scheduledLeadFrames)));
|
||||||
|
recovery.set("syntheticScheduledLeadFrames", JsonValue(static_cast<double>(telemetrySnapshot.backendPlayout.scheduledLeadFrames)));
|
||||||
|
recovery.set("measuredLagFrames", JsonValue(static_cast<double>(telemetrySnapshot.backendPlayout.measuredLagFrames)));
|
||||||
|
recovery.set("catchUpFrames", JsonValue(static_cast<double>(telemetrySnapshot.backendPlayout.catchUpFrames)));
|
||||||
|
recovery.set("lateStreak", JsonValue(static_cast<double>(telemetrySnapshot.backendPlayout.lateStreak)));
|
||||||
|
recovery.set("dropStreak", JsonValue(static_cast<double>(telemetrySnapshot.backendPlayout.dropStreak)));
|
||||||
|
|
||||||
|
JsonValue deckLinkPlayout = JsonValue::MakeObject();
|
||||||
|
deckLinkPlayout.set("actualBufferedFramesAvailable", JsonValue(telemetrySnapshot.backendPlayout.actualDeckLinkBufferedFramesAvailable));
|
||||||
|
deckLinkPlayout.set("actualBufferedFrames", JsonValue(static_cast<double>(telemetrySnapshot.backendPlayout.actualDeckLinkBufferedFrames)));
|
||||||
|
deckLinkPlayout.set("targetBufferedFrames", JsonValue(static_cast<double>(telemetrySnapshot.backendPlayout.targetDeckLinkBufferedFrames)));
|
||||||
|
deckLinkPlayout.set("scheduleCallMs", JsonValue(telemetrySnapshot.backendPlayout.deckLinkScheduleCallMilliseconds));
|
||||||
|
deckLinkPlayout.set("scheduleFailures", JsonValue(static_cast<double>(telemetrySnapshot.backendPlayout.deckLinkScheduleFailureCount)));
|
||||||
|
|
||||||
|
JsonValue scheduler = JsonValue::MakeObject();
|
||||||
|
scheduler.set("syntheticLeadFrames", JsonValue(static_cast<double>(telemetrySnapshot.backendPlayout.scheduledLeadFrames)));
|
||||||
|
|
||||||
|
JsonValue backendPlayout = JsonValue::MakeObject();
|
||||||
|
backendPlayout.set("lifecycleState", JsonValue(telemetrySnapshot.backendPlayout.lifecycleState));
|
||||||
|
backendPlayout.set("degraded", JsonValue(telemetrySnapshot.backendPlayout.degraded));
|
||||||
|
backendPlayout.set("statusMessage", JsonValue(telemetrySnapshot.backendPlayout.statusMessage));
|
||||||
|
backendPlayout.set("lateFrameCount", JsonValue(static_cast<double>(telemetrySnapshot.backendPlayout.lateFrameCount)));
|
||||||
|
backendPlayout.set("droppedFrameCount", JsonValue(static_cast<double>(telemetrySnapshot.backendPlayout.droppedFrameCount)));
|
||||||
|
backendPlayout.set("flushedFrameCount", JsonValue(static_cast<double>(telemetrySnapshot.backendPlayout.flushedFrameCount)));
|
||||||
|
backendPlayout.set("readyQueue", readyQueue);
|
||||||
|
backendPlayout.set("systemMemory", systemMemory);
|
||||||
|
backendPlayout.set("outputRender", outputRender);
|
||||||
|
backendPlayout.set("decklink", deckLinkPlayout);
|
||||||
|
backendPlayout.set("scheduler", scheduler);
|
||||||
|
backendPlayout.set("recovery", recovery);
|
||||||
|
root.set("backendPlayout", backendPlayout);
|
||||||
|
|
||||||
JsonValue eventQueue = JsonValue::MakeObject();
|
JsonValue eventQueue = JsonValue::MakeObject();
|
||||||
eventQueue.set("name", JsonValue(telemetrySnapshot.runtimeEvents.queue.queueName));
|
eventQueue.set("name", JsonValue(telemetrySnapshot.runtimeEvents.queue.queueName));
|
||||||
eventQueue.set("depth", JsonValue(static_cast<double>(telemetrySnapshot.runtimeEvents.queue.depth)));
|
eventQueue.set("depth", JsonValue(static_cast<double>(telemetrySnapshot.runtimeEvents.queue.depth)));
|
||||||
|
|||||||
@@ -24,11 +24,25 @@ double GenerateStartupRandom()
|
|||||||
return distribution(randomDevice);
|
return distribution(randomDevice);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
std::string PersistenceTargetKindName(PersistenceTargetKind targetKind)
|
||||||
|
{
|
||||||
|
switch (targetKind)
|
||||||
|
{
|
||||||
|
case PersistenceTargetKind::RuntimeState:
|
||||||
|
return "runtime-state";
|
||||||
|
case PersistenceTargetKind::StackPreset:
|
||||||
|
return "stack-preset";
|
||||||
|
case PersistenceTargetKind::RuntimeConfig:
|
||||||
|
return "runtime-config";
|
||||||
|
default:
|
||||||
|
return "unknown";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
RuntimeStore::RuntimeStore() :
|
RuntimeStore::RuntimeStore() :
|
||||||
mRenderSnapshotBuilder(*this),
|
mRenderSnapshotBuilder(*this),
|
||||||
mHealthTelemetry(),
|
|
||||||
mReloadRequested(false),
|
mReloadRequested(false),
|
||||||
mCompileSucceeded(false),
|
mCompileSucceeded(false),
|
||||||
mStartupRandom(GenerateStartupRandom()),
|
mStartupRandom(GenerateStartupRandom()),
|
||||||
@@ -37,6 +51,15 @@ RuntimeStore::RuntimeStore() :
|
|||||||
mStartTime(std::chrono::steady_clock::now()),
|
mStartTime(std::chrono::steady_clock::now()),
|
||||||
mLastScanTime((std::chrono::steady_clock::time_point::min)())
|
mLastScanTime((std::chrono::steady_clock::time_point::min)())
|
||||||
{
|
{
|
||||||
|
mPersistenceWriter.SetResultCallback([this](const PersistenceWriteResult& result) {
|
||||||
|
mHealthTelemetry.RecordPersistenceWriteResult(
|
||||||
|
result.succeeded,
|
||||||
|
PersistenceTargetKindName(result.targetKind),
|
||||||
|
result.targetPath,
|
||||||
|
result.reason,
|
||||||
|
result.errorMessage,
|
||||||
|
result.newerRequestPending);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
HealthTelemetry& RuntimeStore::GetHealthTelemetry()
|
HealthTelemetry& RuntimeStore::GetHealthTelemetry()
|
||||||
@@ -104,6 +127,50 @@ PersistenceSnapshot RuntimeStore::BuildRuntimeStatePersistenceSnapshot(const Per
|
|||||||
return BuildRuntimeStatePersistenceSnapshotLocked(request);
|
return BuildRuntimeStatePersistenceSnapshotLocked(request);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool RuntimeStore::RequestPersistence(const PersistenceRequest& request, std::string& error)
|
||||||
|
{
|
||||||
|
if (request.targetKind != PersistenceTargetKind::RuntimeState)
|
||||||
|
{
|
||||||
|
error = "Unsupported persistence request target: " + PersistenceTargetKindName(request.targetKind);
|
||||||
|
mHealthTelemetry.RecordPersistenceWriteResult(
|
||||||
|
false,
|
||||||
|
PersistenceTargetKindName(request.targetKind),
|
||||||
|
std::string(),
|
||||||
|
request.reason,
|
||||||
|
error,
|
||||||
|
false);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const PersistenceSnapshot snapshot = BuildRuntimeStatePersistenceSnapshot(request);
|
||||||
|
if (mPersistenceWriter.EnqueueSnapshot(snapshot, error))
|
||||||
|
return true;
|
||||||
|
|
||||||
|
mHealthTelemetry.RecordPersistenceWriteResult(
|
||||||
|
false,
|
||||||
|
PersistenceTargetKindName(request.targetKind),
|
||||||
|
snapshot.targetPath.string(),
|
||||||
|
request.reason,
|
||||||
|
error,
|
||||||
|
false);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool RuntimeStore::FlushPersistenceForShutdown(std::chrono::milliseconds timeout, std::string& error)
|
||||||
|
{
|
||||||
|
if (mPersistenceWriter.StopAndFlush(timeout, error))
|
||||||
|
return true;
|
||||||
|
|
||||||
|
mHealthTelemetry.RecordPersistenceWriteResult(
|
||||||
|
false,
|
||||||
|
PersistenceTargetKindName(PersistenceTargetKind::RuntimeState),
|
||||||
|
std::string(),
|
||||||
|
"shutdown-flush",
|
||||||
|
error,
|
||||||
|
true);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
PersistenceSnapshot RuntimeStore::BuildRuntimeStatePersistenceSnapshotLocked(const PersistenceRequest& request) const
|
PersistenceSnapshot RuntimeStore::BuildRuntimeStatePersistenceSnapshotLocked(const PersistenceRequest& request) const
|
||||||
{
|
{
|
||||||
PersistenceSnapshot snapshot;
|
PersistenceSnapshot snapshot;
|
||||||
@@ -111,6 +178,9 @@ PersistenceSnapshot RuntimeStore::BuildRuntimeStatePersistenceSnapshotLocked(con
|
|||||||
snapshot.targetPath = mConfigStore.GetRuntimeStatePath();
|
snapshot.targetPath = mConfigStore.GetRuntimeStatePath();
|
||||||
snapshot.contents = SerializeJson(mCommittedLiveState.BuildPersistentStateValue(mShaderCatalog), true);
|
snapshot.contents = SerializeJson(mCommittedLiveState.BuildPersistentStateValue(mShaderCatalog), true);
|
||||||
snapshot.reason = request.reason;
|
snapshot.reason = request.reason;
|
||||||
|
snapshot.debounceKey = request.debounceKey;
|
||||||
|
snapshot.debounceAllowed = request.debounceAllowed;
|
||||||
|
snapshot.flushRequested = request.flushRequested;
|
||||||
snapshot.generation = request.sequence;
|
snapshot.generation = request.sequence;
|
||||||
return snapshot;
|
return snapshot;
|
||||||
}
|
}
|
||||||
@@ -184,7 +254,7 @@ bool RuntimeStore::CreateStoredLayer(const std::string& shaderId, std::string& e
|
|||||||
|
|
||||||
mReloadRequested = true;
|
mReloadRequested = true;
|
||||||
MarkRenderStateDirtyLocked();
|
MarkRenderStateDirtyLocked();
|
||||||
return SavePersistentState(error);
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool RuntimeStore::DeleteStoredLayer(const std::string& layerId, std::string& error)
|
bool RuntimeStore::DeleteStoredLayer(const std::string& layerId, std::string& error)
|
||||||
@@ -195,7 +265,7 @@ bool RuntimeStore::DeleteStoredLayer(const std::string& layerId, std::string& er
|
|||||||
|
|
||||||
mReloadRequested = true;
|
mReloadRequested = true;
|
||||||
MarkRenderStateDirtyLocked();
|
MarkRenderStateDirtyLocked();
|
||||||
return SavePersistentState(error);
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool RuntimeStore::MoveStoredLayer(const std::string& layerId, int direction, std::string& error)
|
bool RuntimeStore::MoveStoredLayer(const std::string& layerId, int direction, std::string& error)
|
||||||
@@ -212,7 +282,7 @@ bool RuntimeStore::MoveStoredLayer(const std::string& layerId, int direction, st
|
|||||||
|
|
||||||
mReloadRequested = true;
|
mReloadRequested = true;
|
||||||
MarkRenderStateDirtyLocked();
|
MarkRenderStateDirtyLocked();
|
||||||
return SavePersistentState(error);
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool RuntimeStore::MoveStoredLayerToIndex(const std::string& layerId, std::size_t targetIndex, std::string& error)
|
bool RuntimeStore::MoveStoredLayerToIndex(const std::string& layerId, std::size_t targetIndex, std::string& error)
|
||||||
@@ -229,7 +299,7 @@ bool RuntimeStore::MoveStoredLayerToIndex(const std::string& layerId, std::size_
|
|||||||
|
|
||||||
mReloadRequested = true;
|
mReloadRequested = true;
|
||||||
MarkRenderStateDirtyLocked();
|
MarkRenderStateDirtyLocked();
|
||||||
return SavePersistentState(error);
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool RuntimeStore::SetStoredLayerBypassState(const std::string& layerId, bool bypassed, std::string& error)
|
bool RuntimeStore::SetStoredLayerBypassState(const std::string& layerId, bool bypassed, std::string& error)
|
||||||
@@ -240,7 +310,7 @@ bool RuntimeStore::SetStoredLayerBypassState(const std::string& layerId, bool by
|
|||||||
|
|
||||||
mReloadRequested = true;
|
mReloadRequested = true;
|
||||||
MarkParameterStateDirtyLocked();
|
MarkParameterStateDirtyLocked();
|
||||||
return SavePersistentState(error);
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool RuntimeStore::SetStoredLayerShaderSelection(const std::string& layerId, const std::string& shaderId, std::string& error)
|
bool RuntimeStore::SetStoredLayerShaderSelection(const std::string& layerId, const std::string& shaderId, std::string& error)
|
||||||
@@ -251,18 +321,19 @@ bool RuntimeStore::SetStoredLayerShaderSelection(const std::string& layerId, con
|
|||||||
|
|
||||||
mReloadRequested = true;
|
mReloadRequested = true;
|
||||||
MarkRenderStateDirtyLocked();
|
MarkRenderStateDirtyLocked();
|
||||||
return SavePersistentState(error);
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool RuntimeStore::SetStoredParameterValue(const std::string& layerId, const std::string& parameterId, const ShaderParameterValue& value, bool persistState, std::string& error)
|
bool RuntimeStore::SetStoredParameterValue(const std::string& layerId, const std::string& parameterId, const ShaderParameterValue& value, bool persistState, std::string& error)
|
||||||
{
|
{
|
||||||
|
(void)persistState;
|
||||||
std::lock_guard<std::mutex> lock(mMutex);
|
std::lock_guard<std::mutex> lock(mMutex);
|
||||||
|
|
||||||
if (!mCommittedLiveState.SetParameterValue(layerId, parameterId, value, error))
|
if (!mCommittedLiveState.SetParameterValue(layerId, parameterId, value, error))
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
MarkParameterStateDirtyLocked();
|
MarkParameterStateDirtyLocked();
|
||||||
return !persistState || SavePersistentState(error);
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool RuntimeStore::ResetStoredLayerParameterValues(const std::string& layerId, std::string& error)
|
bool RuntimeStore::ResetStoredLayerParameterValues(const std::string& layerId, std::string& error)
|
||||||
@@ -273,7 +344,7 @@ bool RuntimeStore::ResetStoredLayerParameterValues(const std::string& layerId, s
|
|||||||
return false;
|
return false;
|
||||||
|
|
||||||
MarkParameterStateDirtyLocked();
|
MarkParameterStateDirtyLocked();
|
||||||
return SavePersistentState(error);
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool RuntimeStore::SaveStackPresetSnapshot(const std::string& presetName, std::string& error) const
|
bool RuntimeStore::SaveStackPresetSnapshot(const std::string& presetName, std::string& error) const
|
||||||
@@ -313,7 +384,7 @@ bool RuntimeStore::LoadStackPresetSnapshot(const std::string& presetName, std::s
|
|||||||
|
|
||||||
mReloadRequested = true;
|
mReloadRequested = true;
|
||||||
MarkRenderStateDirtyLocked();
|
MarkRenderStateDirtyLocked();
|
||||||
return SavePersistentState(error);
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool RuntimeStore::HasStoredLayer(const std::string& layerId) const
|
bool RuntimeStore::HasStoredLayer(const std::string& layerId) const
|
||||||
@@ -476,11 +547,6 @@ bool RuntimeStore::LoadPersistentState(std::string& error)
|
|||||||
return mCommittedLiveState.LoadPersistentStateValue(root);
|
return mCommittedLiveState.LoadPersistentStateValue(root);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool RuntimeStore::SavePersistentState(std::string& error) const
|
|
||||||
{
|
|
||||||
return mPersistenceWriter.WriteSnapshot(BuildRuntimeStatePersistenceSnapshotLocked(PersistenceRequest::RuntimeStateRequest("SavePersistentState")), error);
|
|
||||||
}
|
|
||||||
|
|
||||||
PersistenceSnapshot RuntimeStore::BuildStackPresetPersistenceSnapshot(const std::string& presetName) const
|
PersistenceSnapshot RuntimeStore::BuildStackPresetPersistenceSnapshot(const std::string& presetName) const
|
||||||
{
|
{
|
||||||
const std::string safeStem = LayerStackStore::MakeSafePresetFileStem(presetName);
|
const std::string safeStem = LayerStackStore::MakeSafePresetFileStem(presetName);
|
||||||
@@ -490,6 +556,9 @@ PersistenceSnapshot RuntimeStore::BuildStackPresetPersistenceSnapshot(const std:
|
|||||||
snapshot.targetPath = mConfigStore.GetPresetRoot() / (safeStem + ".json");
|
snapshot.targetPath = mConfigStore.GetPresetRoot() / (safeStem + ".json");
|
||||||
snapshot.contents = SerializeJson(mCommittedLiveState.BuildStackPresetValue(mShaderCatalog, presetName), true);
|
snapshot.contents = SerializeJson(mCommittedLiveState.BuildStackPresetValue(mShaderCatalog, presetName), true);
|
||||||
snapshot.reason = "SaveStackPreset";
|
snapshot.reason = "SaveStackPreset";
|
||||||
|
snapshot.debounceKey = "stack-preset:" + safeStem;
|
||||||
|
snapshot.debounceAllowed = false;
|
||||||
|
snapshot.flushRequested = true;
|
||||||
snapshot.generation = 0;
|
snapshot.generation = 0;
|
||||||
return snapshot;
|
return snapshot;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,6 +32,8 @@ public:
|
|||||||
bool InitializeStore(std::string& error);
|
bool InitializeStore(std::string& error);
|
||||||
std::string BuildPersistentStateJson() const;
|
std::string BuildPersistentStateJson() const;
|
||||||
PersistenceSnapshot BuildRuntimeStatePersistenceSnapshot(const PersistenceRequest& request) const;
|
PersistenceSnapshot BuildRuntimeStatePersistenceSnapshot(const PersistenceRequest& request) const;
|
||||||
|
bool RequestPersistence(const PersistenceRequest& request, std::string& error);
|
||||||
|
bool FlushPersistenceForShutdown(std::chrono::milliseconds timeout, std::string& error);
|
||||||
bool PollStoredFileChanges(bool& registryChanged, bool& reloadRequested, std::string& error);
|
bool PollStoredFileChanges(bool& registryChanged, bool& reloadRequested, std::string& error);
|
||||||
|
|
||||||
bool CreateStoredLayer(const std::string& shaderId, std::string& error);
|
bool CreateStoredLayer(const std::string& shaderId, std::string& error);
|
||||||
@@ -83,7 +85,6 @@ public:
|
|||||||
|
|
||||||
private:
|
private:
|
||||||
bool LoadPersistentState(std::string& error);
|
bool LoadPersistentState(std::string& error);
|
||||||
bool SavePersistentState(std::string& error) const;
|
|
||||||
PersistenceSnapshot BuildRuntimeStatePersistenceSnapshotLocked(const PersistenceRequest& request) const;
|
PersistenceSnapshot BuildRuntimeStatePersistenceSnapshotLocked(const PersistenceRequest& request) const;
|
||||||
PersistenceSnapshot BuildStackPresetPersistenceSnapshot(const std::string& presetName) const;
|
PersistenceSnapshot BuildStackPresetPersistenceSnapshot(const std::string& presetName) const;
|
||||||
bool ScanShaderPackages(std::string& error);
|
bool ScanShaderPackages(std::string& error);
|
||||||
@@ -93,11 +94,11 @@ private:
|
|||||||
void MarkParameterStateDirtyLocked();
|
void MarkParameterStateDirtyLocked();
|
||||||
|
|
||||||
RenderSnapshotBuilder mRenderSnapshotBuilder;
|
RenderSnapshotBuilder mRenderSnapshotBuilder;
|
||||||
PersistenceWriter mPersistenceWriter;
|
|
||||||
RuntimeConfigStore mConfigStore;
|
RuntimeConfigStore mConfigStore;
|
||||||
ShaderPackageCatalog mShaderCatalog;
|
ShaderPackageCatalog mShaderCatalog;
|
||||||
CommittedLiveState mCommittedLiveState;
|
CommittedLiveState mCommittedLiveState;
|
||||||
HealthTelemetry mHealthTelemetry;
|
HealthTelemetry mHealthTelemetry;
|
||||||
|
mutable PersistenceWriter mPersistenceWriter;
|
||||||
mutable std::mutex mMutex;
|
mutable std::mutex mMutex;
|
||||||
bool mReloadRequested;
|
bool mReloadRequested;
|
||||||
bool mCompileSucceeded;
|
bool mCompileSucceeded;
|
||||||
|
|||||||
@@ -169,6 +169,286 @@ bool HealthTelemetry::TryRecordRuntimeEventDispatchStats(std::size_t dispatchedE
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void HealthTelemetry::RecordPersistenceWriteResult(bool succeeded, const std::string& targetKind, const std::string& targetPath,
|
||||||
|
const std::string& reason, const std::string& errorMessage, bool newerRequestPending)
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(mMutex);
|
||||||
|
if (succeeded)
|
||||||
|
++mPersistence.writeSuccessCount;
|
||||||
|
else
|
||||||
|
++mPersistence.writeFailureCount;
|
||||||
|
mPersistence.lastWriteSucceeded = succeeded;
|
||||||
|
mPersistence.unsavedChanges = !succeeded || newerRequestPending;
|
||||||
|
mPersistence.newerRequestPending = newerRequestPending;
|
||||||
|
mPersistence.lastTargetKind = targetKind;
|
||||||
|
mPersistence.lastTargetPath = targetPath;
|
||||||
|
mPersistence.lastReason = reason;
|
||||||
|
mPersistence.lastErrorMessage = errorMessage;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool HealthTelemetry::TryRecordPersistenceWriteResult(bool succeeded, const std::string& targetKind, const std::string& targetPath,
|
||||||
|
const std::string& reason, const std::string& errorMessage, bool newerRequestPending)
|
||||||
|
{
|
||||||
|
std::unique_lock<std::mutex> lock(mMutex, std::try_to_lock);
|
||||||
|
if (!lock.owns_lock())
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if (succeeded)
|
||||||
|
++mPersistence.writeSuccessCount;
|
||||||
|
else
|
||||||
|
++mPersistence.writeFailureCount;
|
||||||
|
mPersistence.lastWriteSucceeded = succeeded;
|
||||||
|
mPersistence.unsavedChanges = !succeeded || newerRequestPending;
|
||||||
|
mPersistence.newerRequestPending = newerRequestPending;
|
||||||
|
mPersistence.lastTargetKind = targetKind;
|
||||||
|
mPersistence.lastTargetPath = targetPath;
|
||||||
|
mPersistence.lastReason = reason;
|
||||||
|
mPersistence.lastErrorMessage = errorMessage;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void HealthTelemetry::RecordBackendPlayoutHealth(const std::string& lifecycleState, const std::string& completionResult,
|
||||||
|
std::size_t readyQueueDepth, std::size_t readyQueueCapacity, uint64_t readyQueuePushedCount,
|
||||||
|
std::size_t minReadyQueueDepth, std::size_t maxReadyQueueDepth, uint64_t readyQueueZeroDepthCount,
|
||||||
|
uint64_t readyQueuePoppedCount, uint64_t readyQueueDroppedCount, uint64_t readyQueueUnderrunCount,
|
||||||
|
double outputRenderMilliseconds, double smoothedOutputRenderMilliseconds, double maxOutputRenderMilliseconds,
|
||||||
|
double outputFrameAcquireMilliseconds, double outputFrameRenderRequestMilliseconds, double outputFrameEndAccessMilliseconds,
|
||||||
|
uint64_t completedFrameIndex, uint64_t scheduledFrameIndex, uint64_t scheduledLeadFrames,
|
||||||
|
uint64_t measuredLagFrames, uint64_t catchUpFrames, uint64_t lateStreak, uint64_t dropStreak,
|
||||||
|
uint64_t lateFrameCount, uint64_t droppedFrameCount, uint64_t flushedFrameCount,
|
||||||
|
bool degraded, const std::string& statusMessage)
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(mMutex);
|
||||||
|
mBackendPlayout.lifecycleState = lifecycleState;
|
||||||
|
mBackendPlayout.completionResult = completionResult;
|
||||||
|
mBackendPlayout.readyQueueDepth = readyQueueDepth;
|
||||||
|
mBackendPlayout.readyQueueCapacity = readyQueueCapacity;
|
||||||
|
mBackendPlayout.minReadyQueueDepth = minReadyQueueDepth;
|
||||||
|
mBackendPlayout.maxReadyQueueDepth = maxReadyQueueDepth;
|
||||||
|
mBackendPlayout.readyQueueZeroDepthCount = readyQueueZeroDepthCount;
|
||||||
|
mBackendPlayout.readyQueuePushedCount = readyQueuePushedCount;
|
||||||
|
mBackendPlayout.readyQueuePoppedCount = readyQueuePoppedCount;
|
||||||
|
mBackendPlayout.readyQueueDroppedCount = readyQueueDroppedCount;
|
||||||
|
mBackendPlayout.readyQueueUnderrunCount = readyQueueUnderrunCount;
|
||||||
|
mBackendPlayout.outputRenderMilliseconds = std::max(outputRenderMilliseconds, 0.0);
|
||||||
|
mBackendPlayout.smoothedOutputRenderMilliseconds = std::max(smoothedOutputRenderMilliseconds, 0.0);
|
||||||
|
mBackendPlayout.maxOutputRenderMilliseconds = std::max(maxOutputRenderMilliseconds, 0.0);
|
||||||
|
mBackendPlayout.outputFrameAcquireMilliseconds = std::max(outputFrameAcquireMilliseconds, 0.0);
|
||||||
|
mBackendPlayout.outputFrameRenderRequestMilliseconds = std::max(outputFrameRenderRequestMilliseconds, 0.0);
|
||||||
|
mBackendPlayout.outputFrameEndAccessMilliseconds = std::max(outputFrameEndAccessMilliseconds, 0.0);
|
||||||
|
mBackendPlayout.completedFrameIndex = completedFrameIndex;
|
||||||
|
mBackendPlayout.scheduledFrameIndex = scheduledFrameIndex;
|
||||||
|
mBackendPlayout.scheduledLeadFrames = scheduledLeadFrames;
|
||||||
|
mBackendPlayout.measuredLagFrames = measuredLagFrames;
|
||||||
|
mBackendPlayout.catchUpFrames = catchUpFrames;
|
||||||
|
mBackendPlayout.lateStreak = lateStreak;
|
||||||
|
mBackendPlayout.dropStreak = dropStreak;
|
||||||
|
mBackendPlayout.lateFrameCount = lateFrameCount;
|
||||||
|
mBackendPlayout.droppedFrameCount = droppedFrameCount;
|
||||||
|
mBackendPlayout.flushedFrameCount = flushedFrameCount;
|
||||||
|
mBackendPlayout.degraded = degraded;
|
||||||
|
mBackendPlayout.statusMessage = statusMessage;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool HealthTelemetry::TryRecordBackendPlayoutHealth(const std::string& lifecycleState, const std::string& completionResult,
|
||||||
|
std::size_t readyQueueDepth, std::size_t readyQueueCapacity, uint64_t readyQueuePushedCount,
|
||||||
|
std::size_t minReadyQueueDepth, std::size_t maxReadyQueueDepth, uint64_t readyQueueZeroDepthCount,
|
||||||
|
uint64_t readyQueuePoppedCount, uint64_t readyQueueDroppedCount, uint64_t readyQueueUnderrunCount,
|
||||||
|
double outputRenderMilliseconds, double smoothedOutputRenderMilliseconds, double maxOutputRenderMilliseconds,
|
||||||
|
double outputFrameAcquireMilliseconds, double outputFrameRenderRequestMilliseconds, double outputFrameEndAccessMilliseconds,
|
||||||
|
uint64_t completedFrameIndex, uint64_t scheduledFrameIndex, uint64_t scheduledLeadFrames,
|
||||||
|
uint64_t measuredLagFrames, uint64_t catchUpFrames, uint64_t lateStreak, uint64_t dropStreak,
|
||||||
|
uint64_t lateFrameCount, uint64_t droppedFrameCount, uint64_t flushedFrameCount,
|
||||||
|
bool degraded, const std::string& statusMessage)
|
||||||
|
{
|
||||||
|
std::unique_lock<std::mutex> lock(mMutex, std::try_to_lock);
|
||||||
|
if (!lock.owns_lock())
|
||||||
|
return false;
|
||||||
|
|
||||||
|
mBackendPlayout.lifecycleState = lifecycleState;
|
||||||
|
mBackendPlayout.completionResult = completionResult;
|
||||||
|
mBackendPlayout.readyQueueDepth = readyQueueDepth;
|
||||||
|
mBackendPlayout.readyQueueCapacity = readyQueueCapacity;
|
||||||
|
mBackendPlayout.minReadyQueueDepth = minReadyQueueDepth;
|
||||||
|
mBackendPlayout.maxReadyQueueDepth = maxReadyQueueDepth;
|
||||||
|
mBackendPlayout.readyQueueZeroDepthCount = readyQueueZeroDepthCount;
|
||||||
|
mBackendPlayout.readyQueuePushedCount = readyQueuePushedCount;
|
||||||
|
mBackendPlayout.readyQueuePoppedCount = readyQueuePoppedCount;
|
||||||
|
mBackendPlayout.readyQueueDroppedCount = readyQueueDroppedCount;
|
||||||
|
mBackendPlayout.readyQueueUnderrunCount = readyQueueUnderrunCount;
|
||||||
|
mBackendPlayout.outputRenderMilliseconds = std::max(outputRenderMilliseconds, 0.0);
|
||||||
|
mBackendPlayout.smoothedOutputRenderMilliseconds = std::max(smoothedOutputRenderMilliseconds, 0.0);
|
||||||
|
mBackendPlayout.maxOutputRenderMilliseconds = std::max(maxOutputRenderMilliseconds, 0.0);
|
||||||
|
mBackendPlayout.outputFrameAcquireMilliseconds = std::max(outputFrameAcquireMilliseconds, 0.0);
|
||||||
|
mBackendPlayout.outputFrameRenderRequestMilliseconds = std::max(outputFrameRenderRequestMilliseconds, 0.0);
|
||||||
|
mBackendPlayout.outputFrameEndAccessMilliseconds = std::max(outputFrameEndAccessMilliseconds, 0.0);
|
||||||
|
mBackendPlayout.completedFrameIndex = completedFrameIndex;
|
||||||
|
mBackendPlayout.scheduledFrameIndex = scheduledFrameIndex;
|
||||||
|
mBackendPlayout.scheduledLeadFrames = scheduledLeadFrames;
|
||||||
|
mBackendPlayout.measuredLagFrames = measuredLagFrames;
|
||||||
|
mBackendPlayout.catchUpFrames = catchUpFrames;
|
||||||
|
mBackendPlayout.lateStreak = lateStreak;
|
||||||
|
mBackendPlayout.dropStreak = dropStreak;
|
||||||
|
mBackendPlayout.lateFrameCount = lateFrameCount;
|
||||||
|
mBackendPlayout.droppedFrameCount = droppedFrameCount;
|
||||||
|
mBackendPlayout.flushedFrameCount = flushedFrameCount;
|
||||||
|
mBackendPlayout.degraded = degraded;
|
||||||
|
mBackendPlayout.statusMessage = statusMessage;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void HealthTelemetry::RecordOutputRenderQueueWait(double queueWaitMilliseconds)
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(mMutex);
|
||||||
|
mBackendPlayout.outputRenderQueueWaitMilliseconds = std::max(queueWaitMilliseconds, 0.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool HealthTelemetry::TryRecordOutputRenderQueueWait(double queueWaitMilliseconds)
|
||||||
|
{
|
||||||
|
std::unique_lock<std::mutex> lock(mMutex, std::try_to_lock);
|
||||||
|
if (!lock.owns_lock())
|
||||||
|
return false;
|
||||||
|
|
||||||
|
mBackendPlayout.outputRenderQueueWaitMilliseconds = std::max(queueWaitMilliseconds, 0.0);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void HealthTelemetry::RecordSystemMemoryPlayoutStats(std::size_t freeFrameCount, std::size_t readyFrameCount,
|
||||||
|
std::size_t scheduledFrameCount, uint64_t underrunCount, uint64_t repeatCount, uint64_t dropCount,
|
||||||
|
double frameAgeAtScheduleMilliseconds, double frameAgeAtCompletionMilliseconds)
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(mMutex);
|
||||||
|
mBackendPlayout.systemFramePoolFree = freeFrameCount;
|
||||||
|
mBackendPlayout.systemFramePoolReady = readyFrameCount;
|
||||||
|
mBackendPlayout.systemFramePoolScheduled = scheduledFrameCount;
|
||||||
|
mBackendPlayout.systemFrameUnderrunCount = underrunCount;
|
||||||
|
mBackendPlayout.systemFrameRepeatCount = repeatCount;
|
||||||
|
mBackendPlayout.systemFrameDropCount = dropCount;
|
||||||
|
mBackendPlayout.systemFrameAgeAtScheduleMilliseconds = std::max(frameAgeAtScheduleMilliseconds, 0.0);
|
||||||
|
mBackendPlayout.systemFrameAgeAtCompletionMilliseconds = std::max(frameAgeAtCompletionMilliseconds, 0.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool HealthTelemetry::TryRecordSystemMemoryPlayoutStats(std::size_t freeFrameCount, std::size_t readyFrameCount,
|
||||||
|
std::size_t scheduledFrameCount, uint64_t underrunCount, uint64_t repeatCount, uint64_t dropCount,
|
||||||
|
double frameAgeAtScheduleMilliseconds, double frameAgeAtCompletionMilliseconds)
|
||||||
|
{
|
||||||
|
std::unique_lock<std::mutex> lock(mMutex, std::try_to_lock);
|
||||||
|
if (!lock.owns_lock())
|
||||||
|
return false;
|
||||||
|
|
||||||
|
mBackendPlayout.systemFramePoolFree = freeFrameCount;
|
||||||
|
mBackendPlayout.systemFramePoolReady = readyFrameCount;
|
||||||
|
mBackendPlayout.systemFramePoolScheduled = scheduledFrameCount;
|
||||||
|
mBackendPlayout.systemFrameUnderrunCount = underrunCount;
|
||||||
|
mBackendPlayout.systemFrameRepeatCount = repeatCount;
|
||||||
|
mBackendPlayout.systemFrameDropCount = dropCount;
|
||||||
|
mBackendPlayout.systemFrameAgeAtScheduleMilliseconds = std::max(frameAgeAtScheduleMilliseconds, 0.0);
|
||||||
|
mBackendPlayout.systemFrameAgeAtCompletionMilliseconds = std::max(frameAgeAtCompletionMilliseconds, 0.0);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void HealthTelemetry::RecordDeckLinkBufferTelemetry(bool actualBufferedFramesAvailable, uint64_t actualBufferedFrames,
|
||||||
|
std::size_t targetBufferedFrames, double scheduleCallMilliseconds, uint64_t scheduleFailureCount)
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(mMutex);
|
||||||
|
mBackendPlayout.actualDeckLinkBufferedFramesAvailable = actualBufferedFramesAvailable;
|
||||||
|
mBackendPlayout.actualDeckLinkBufferedFrames = actualBufferedFramesAvailable ? actualBufferedFrames : 0;
|
||||||
|
mBackendPlayout.targetDeckLinkBufferedFrames = targetBufferedFrames;
|
||||||
|
mBackendPlayout.deckLinkScheduleCallMilliseconds = std::max(scheduleCallMilliseconds, 0.0);
|
||||||
|
mBackendPlayout.deckLinkScheduleFailureCount = scheduleFailureCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool HealthTelemetry::TryRecordDeckLinkBufferTelemetry(bool actualBufferedFramesAvailable, uint64_t actualBufferedFrames,
|
||||||
|
std::size_t targetBufferedFrames, double scheduleCallMilliseconds, uint64_t scheduleFailureCount)
|
||||||
|
{
|
||||||
|
std::unique_lock<std::mutex> lock(mMutex, std::try_to_lock);
|
||||||
|
if (!lock.owns_lock())
|
||||||
|
return false;
|
||||||
|
|
||||||
|
mBackendPlayout.actualDeckLinkBufferedFramesAvailable = actualBufferedFramesAvailable;
|
||||||
|
mBackendPlayout.actualDeckLinkBufferedFrames = actualBufferedFramesAvailable ? actualBufferedFrames : 0;
|
||||||
|
mBackendPlayout.targetDeckLinkBufferedFrames = targetBufferedFrames;
|
||||||
|
mBackendPlayout.deckLinkScheduleCallMilliseconds = std::max(scheduleCallMilliseconds, 0.0);
|
||||||
|
mBackendPlayout.deckLinkScheduleFailureCount = scheduleFailureCount;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void HealthTelemetry::RecordOutputRenderPipelineTiming(
|
||||||
|
double drawMilliseconds,
|
||||||
|
double fenceWaitMilliseconds,
|
||||||
|
double mapMilliseconds,
|
||||||
|
double readbackCopyMilliseconds,
|
||||||
|
double cachedCopyMilliseconds,
|
||||||
|
double asyncQueueMilliseconds,
|
||||||
|
double asyncQueueBufferMilliseconds,
|
||||||
|
double asyncQueueSetupMilliseconds,
|
||||||
|
double asyncQueueReadPixelsMilliseconds,
|
||||||
|
double asyncQueueFenceMilliseconds,
|
||||||
|
double syncReadMilliseconds,
|
||||||
|
bool asyncReadbackMissed,
|
||||||
|
bool cachedFallbackUsed,
|
||||||
|
bool syncFallbackUsed)
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(mMutex);
|
||||||
|
mBackendPlayout.outputRenderDrawMilliseconds = std::max(drawMilliseconds, 0.0);
|
||||||
|
mBackendPlayout.outputReadbackFenceWaitMilliseconds = std::max(fenceWaitMilliseconds, 0.0);
|
||||||
|
mBackendPlayout.outputReadbackMapMilliseconds = std::max(mapMilliseconds, 0.0);
|
||||||
|
mBackendPlayout.outputReadbackCopyMilliseconds = std::max(readbackCopyMilliseconds, 0.0);
|
||||||
|
mBackendPlayout.outputCachedCopyMilliseconds = std::max(cachedCopyMilliseconds, 0.0);
|
||||||
|
mBackendPlayout.outputAsyncQueueMilliseconds = std::max(asyncQueueMilliseconds, 0.0);
|
||||||
|
mBackendPlayout.outputAsyncQueueBufferMilliseconds = std::max(asyncQueueBufferMilliseconds, 0.0);
|
||||||
|
mBackendPlayout.outputAsyncQueueSetupMilliseconds = std::max(asyncQueueSetupMilliseconds, 0.0);
|
||||||
|
mBackendPlayout.outputAsyncQueueReadPixelsMilliseconds = std::max(asyncQueueReadPixelsMilliseconds, 0.0);
|
||||||
|
mBackendPlayout.outputAsyncQueueFenceMilliseconds = std::max(asyncQueueFenceMilliseconds, 0.0);
|
||||||
|
mBackendPlayout.outputSyncReadMilliseconds = std::max(syncReadMilliseconds, 0.0);
|
||||||
|
if (asyncReadbackMissed)
|
||||||
|
++mBackendPlayout.outputAsyncReadbackMissCount;
|
||||||
|
if (cachedFallbackUsed)
|
||||||
|
++mBackendPlayout.outputCachedFallbackCount;
|
||||||
|
if (syncFallbackUsed)
|
||||||
|
++mBackendPlayout.outputSyncFallbackCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool HealthTelemetry::TryRecordOutputRenderPipelineTiming(
|
||||||
|
double drawMilliseconds,
|
||||||
|
double fenceWaitMilliseconds,
|
||||||
|
double mapMilliseconds,
|
||||||
|
double readbackCopyMilliseconds,
|
||||||
|
double cachedCopyMilliseconds,
|
||||||
|
double asyncQueueMilliseconds,
|
||||||
|
double asyncQueueBufferMilliseconds,
|
||||||
|
double asyncQueueSetupMilliseconds,
|
||||||
|
double asyncQueueReadPixelsMilliseconds,
|
||||||
|
double asyncQueueFenceMilliseconds,
|
||||||
|
double syncReadMilliseconds,
|
||||||
|
bool asyncReadbackMissed,
|
||||||
|
bool cachedFallbackUsed,
|
||||||
|
bool syncFallbackUsed)
|
||||||
|
{
|
||||||
|
std::unique_lock<std::mutex> lock(mMutex, std::try_to_lock);
|
||||||
|
if (!lock.owns_lock())
|
||||||
|
return false;
|
||||||
|
|
||||||
|
mBackendPlayout.outputRenderDrawMilliseconds = std::max(drawMilliseconds, 0.0);
|
||||||
|
mBackendPlayout.outputReadbackFenceWaitMilliseconds = std::max(fenceWaitMilliseconds, 0.0);
|
||||||
|
mBackendPlayout.outputReadbackMapMilliseconds = std::max(mapMilliseconds, 0.0);
|
||||||
|
mBackendPlayout.outputReadbackCopyMilliseconds = std::max(readbackCopyMilliseconds, 0.0);
|
||||||
|
mBackendPlayout.outputCachedCopyMilliseconds = std::max(cachedCopyMilliseconds, 0.0);
|
||||||
|
mBackendPlayout.outputAsyncQueueMilliseconds = std::max(asyncQueueMilliseconds, 0.0);
|
||||||
|
mBackendPlayout.outputAsyncQueueBufferMilliseconds = std::max(asyncQueueBufferMilliseconds, 0.0);
|
||||||
|
mBackendPlayout.outputAsyncQueueSetupMilliseconds = std::max(asyncQueueSetupMilliseconds, 0.0);
|
||||||
|
mBackendPlayout.outputAsyncQueueReadPixelsMilliseconds = std::max(asyncQueueReadPixelsMilliseconds, 0.0);
|
||||||
|
mBackendPlayout.outputAsyncQueueFenceMilliseconds = std::max(asyncQueueFenceMilliseconds, 0.0);
|
||||||
|
mBackendPlayout.outputSyncReadMilliseconds = std::max(syncReadMilliseconds, 0.0);
|
||||||
|
if (asyncReadbackMissed)
|
||||||
|
++mBackendPlayout.outputAsyncReadbackMissCount;
|
||||||
|
if (cachedFallbackUsed)
|
||||||
|
++mBackendPlayout.outputCachedFallbackCount;
|
||||||
|
if (syncFallbackUsed)
|
||||||
|
++mBackendPlayout.outputSyncFallbackCount;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
HealthTelemetry::SignalStatusSnapshot HealthTelemetry::GetSignalStatusSnapshot() const
|
HealthTelemetry::SignalStatusSnapshot HealthTelemetry::GetSignalStatusSnapshot() const
|
||||||
{
|
{
|
||||||
std::lock_guard<std::mutex> lock(mMutex);
|
std::lock_guard<std::mutex> lock(mMutex);
|
||||||
@@ -193,6 +473,18 @@ HealthTelemetry::RuntimeEventMetricsSnapshot HealthTelemetry::GetRuntimeEventMet
|
|||||||
return mRuntimeEvents;
|
return mRuntimeEvents;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
HealthTelemetry::PersistenceSnapshot HealthTelemetry::GetPersistenceSnapshot() const
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(mMutex);
|
||||||
|
return mPersistence;
|
||||||
|
}
|
||||||
|
|
||||||
|
HealthTelemetry::BackendPlayoutSnapshot HealthTelemetry::GetBackendPlayoutSnapshot() const
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(mMutex);
|
||||||
|
return mBackendPlayout;
|
||||||
|
}
|
||||||
|
|
||||||
HealthTelemetry::Snapshot HealthTelemetry::GetSnapshot() const
|
HealthTelemetry::Snapshot HealthTelemetry::GetSnapshot() const
|
||||||
{
|
{
|
||||||
std::lock_guard<std::mutex> lock(mMutex);
|
std::lock_guard<std::mutex> lock(mMutex);
|
||||||
@@ -202,5 +494,7 @@ HealthTelemetry::Snapshot HealthTelemetry::GetSnapshot() const
|
|||||||
snapshot.videoIO = mVideoIOStatus;
|
snapshot.videoIO = mVideoIOStatus;
|
||||||
snapshot.performance = mPerformance;
|
snapshot.performance = mPerformance;
|
||||||
snapshot.runtimeEvents = mRuntimeEvents;
|
snapshot.runtimeEvents = mRuntimeEvents;
|
||||||
|
snapshot.persistence = mPersistence;
|
||||||
|
snapshot.backendPlayout = mBackendPlayout;
|
||||||
return snapshot;
|
return snapshot;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,9 +5,8 @@
|
|||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
#include <string>
|
#include <string>
|
||||||
|
|
||||||
// Phase 1 compatibility seam for status and timing reporting. HealthTelemetry
|
// HealthTelemetry owns the current operational status snapshot directly, so
|
||||||
// owns the current operational status snapshot directly, so callers can report
|
// callers can report health without sharing runtime-store state.
|
||||||
// health without sharing runtime-store state.
|
|
||||||
class HealthTelemetry
|
class HealthTelemetry
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
@@ -69,12 +68,88 @@ public:
|
|||||||
RuntimeEventDispatchSnapshot dispatch;
|
RuntimeEventDispatchSnapshot dispatch;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
struct PersistenceSnapshot
|
||||||
|
{
|
||||||
|
uint64_t writeSuccessCount = 0;
|
||||||
|
uint64_t writeFailureCount = 0;
|
||||||
|
bool lastWriteSucceeded = true;
|
||||||
|
bool unsavedChanges = false;
|
||||||
|
bool newerRequestPending = false;
|
||||||
|
std::string lastTargetKind;
|
||||||
|
std::string lastTargetPath;
|
||||||
|
std::string lastReason;
|
||||||
|
std::string lastErrorMessage;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct BackendPlayoutSnapshot
|
||||||
|
{
|
||||||
|
std::string lifecycleState = "NotStarted";
|
||||||
|
std::string completionResult = "Unknown";
|
||||||
|
std::size_t readyQueueDepth = 0;
|
||||||
|
std::size_t readyQueueCapacity = 0;
|
||||||
|
std::size_t minReadyQueueDepth = 0;
|
||||||
|
std::size_t maxReadyQueueDepth = 0;
|
||||||
|
uint64_t readyQueueZeroDepthCount = 0;
|
||||||
|
uint64_t readyQueuePushedCount = 0;
|
||||||
|
uint64_t readyQueuePoppedCount = 0;
|
||||||
|
uint64_t readyQueueDroppedCount = 0;
|
||||||
|
uint64_t readyQueueUnderrunCount = 0;
|
||||||
|
std::size_t systemFramePoolFree = 0;
|
||||||
|
std::size_t systemFramePoolReady = 0;
|
||||||
|
std::size_t systemFramePoolScheduled = 0;
|
||||||
|
uint64_t systemFrameUnderrunCount = 0;
|
||||||
|
uint64_t systemFrameRepeatCount = 0;
|
||||||
|
uint64_t systemFrameDropCount = 0;
|
||||||
|
double systemFrameAgeAtScheduleMilliseconds = 0.0;
|
||||||
|
double systemFrameAgeAtCompletionMilliseconds = 0.0;
|
||||||
|
double outputRenderMilliseconds = 0.0;
|
||||||
|
double smoothedOutputRenderMilliseconds = 0.0;
|
||||||
|
double maxOutputRenderMilliseconds = 0.0;
|
||||||
|
double outputFrameAcquireMilliseconds = 0.0;
|
||||||
|
double outputFrameRenderRequestMilliseconds = 0.0;
|
||||||
|
double outputFrameEndAccessMilliseconds = 0.0;
|
||||||
|
double outputRenderQueueWaitMilliseconds = 0.0;
|
||||||
|
double outputRenderDrawMilliseconds = 0.0;
|
||||||
|
double outputReadbackFenceWaitMilliseconds = 0.0;
|
||||||
|
double outputReadbackMapMilliseconds = 0.0;
|
||||||
|
double outputReadbackCopyMilliseconds = 0.0;
|
||||||
|
double outputCachedCopyMilliseconds = 0.0;
|
||||||
|
double outputAsyncQueueMilliseconds = 0.0;
|
||||||
|
double outputAsyncQueueBufferMilliseconds = 0.0;
|
||||||
|
double outputAsyncQueueSetupMilliseconds = 0.0;
|
||||||
|
double outputAsyncQueueReadPixelsMilliseconds = 0.0;
|
||||||
|
double outputAsyncQueueFenceMilliseconds = 0.0;
|
||||||
|
double outputSyncReadMilliseconds = 0.0;
|
||||||
|
uint64_t outputAsyncReadbackMissCount = 0;
|
||||||
|
uint64_t outputCachedFallbackCount = 0;
|
||||||
|
uint64_t outputSyncFallbackCount = 0;
|
||||||
|
uint64_t completedFrameIndex = 0;
|
||||||
|
uint64_t scheduledFrameIndex = 0;
|
||||||
|
uint64_t scheduledLeadFrames = 0;
|
||||||
|
bool actualDeckLinkBufferedFramesAvailable = false;
|
||||||
|
uint64_t actualDeckLinkBufferedFrames = 0;
|
||||||
|
std::size_t targetDeckLinkBufferedFrames = 0;
|
||||||
|
double deckLinkScheduleCallMilliseconds = 0.0;
|
||||||
|
uint64_t deckLinkScheduleFailureCount = 0;
|
||||||
|
uint64_t measuredLagFrames = 0;
|
||||||
|
uint64_t catchUpFrames = 0;
|
||||||
|
uint64_t lateStreak = 0;
|
||||||
|
uint64_t dropStreak = 0;
|
||||||
|
uint64_t lateFrameCount = 0;
|
||||||
|
uint64_t droppedFrameCount = 0;
|
||||||
|
uint64_t flushedFrameCount = 0;
|
||||||
|
bool degraded = false;
|
||||||
|
std::string statusMessage;
|
||||||
|
};
|
||||||
|
|
||||||
struct Snapshot
|
struct Snapshot
|
||||||
{
|
{
|
||||||
SignalStatusSnapshot signal;
|
SignalStatusSnapshot signal;
|
||||||
VideoIOStatusSnapshot videoIO;
|
VideoIOStatusSnapshot videoIO;
|
||||||
PerformanceSnapshot performance;
|
PerformanceSnapshot performance;
|
||||||
RuntimeEventMetricsSnapshot runtimeEvents;
|
RuntimeEventMetricsSnapshot runtimeEvents;
|
||||||
|
PersistenceSnapshot persistence;
|
||||||
|
BackendPlayoutSnapshot backendPlayout;
|
||||||
};
|
};
|
||||||
|
|
||||||
HealthTelemetry() = default;
|
HealthTelemetry() = default;
|
||||||
@@ -107,10 +182,84 @@ public:
|
|||||||
bool TryRecordRuntimeEventDispatchStats(std::size_t dispatchedEvents, std::size_t handlerInvocations,
|
bool TryRecordRuntimeEventDispatchStats(std::size_t dispatchedEvents, std::size_t handlerInvocations,
|
||||||
std::size_t handlerFailures, double dispatchDurationMilliseconds);
|
std::size_t handlerFailures, double dispatchDurationMilliseconds);
|
||||||
|
|
||||||
|
void RecordPersistenceWriteResult(bool succeeded, const std::string& targetKind, const std::string& targetPath,
|
||||||
|
const std::string& reason, const std::string& errorMessage, bool newerRequestPending);
|
||||||
|
bool TryRecordPersistenceWriteResult(bool succeeded, const std::string& targetKind, const std::string& targetPath,
|
||||||
|
const std::string& reason, const std::string& errorMessage, bool newerRequestPending);
|
||||||
|
|
||||||
|
void RecordBackendPlayoutHealth(const std::string& lifecycleState, const std::string& completionResult,
|
||||||
|
std::size_t readyQueueDepth, std::size_t readyQueueCapacity, uint64_t readyQueuePushedCount,
|
||||||
|
std::size_t minReadyQueueDepth, std::size_t maxReadyQueueDepth, uint64_t readyQueueZeroDepthCount,
|
||||||
|
uint64_t readyQueuePoppedCount, uint64_t readyQueueDroppedCount, uint64_t readyQueueUnderrunCount,
|
||||||
|
double outputRenderMilliseconds, double smoothedOutputRenderMilliseconds, double maxOutputRenderMilliseconds,
|
||||||
|
double outputFrameAcquireMilliseconds, double outputFrameRenderRequestMilliseconds, double outputFrameEndAccessMilliseconds,
|
||||||
|
uint64_t completedFrameIndex, uint64_t scheduledFrameIndex, uint64_t scheduledLeadFrames,
|
||||||
|
uint64_t measuredLagFrames, uint64_t catchUpFrames, uint64_t lateStreak, uint64_t dropStreak,
|
||||||
|
uint64_t lateFrameCount, uint64_t droppedFrameCount, uint64_t flushedFrameCount,
|
||||||
|
bool degraded, const std::string& statusMessage);
|
||||||
|
bool TryRecordBackendPlayoutHealth(const std::string& lifecycleState, const std::string& completionResult,
|
||||||
|
std::size_t readyQueueDepth, std::size_t readyQueueCapacity, uint64_t readyQueuePushedCount,
|
||||||
|
std::size_t minReadyQueueDepth, std::size_t maxReadyQueueDepth, uint64_t readyQueueZeroDepthCount,
|
||||||
|
uint64_t readyQueuePoppedCount, uint64_t readyQueueDroppedCount, uint64_t readyQueueUnderrunCount,
|
||||||
|
double outputRenderMilliseconds, double smoothedOutputRenderMilliseconds, double maxOutputRenderMilliseconds,
|
||||||
|
double outputFrameAcquireMilliseconds, double outputFrameRenderRequestMilliseconds, double outputFrameEndAccessMilliseconds,
|
||||||
|
uint64_t completedFrameIndex, uint64_t scheduledFrameIndex, uint64_t scheduledLeadFrames,
|
||||||
|
uint64_t measuredLagFrames, uint64_t catchUpFrames, uint64_t lateStreak, uint64_t dropStreak,
|
||||||
|
uint64_t lateFrameCount, uint64_t droppedFrameCount, uint64_t flushedFrameCount,
|
||||||
|
bool degraded, const std::string& statusMessage);
|
||||||
|
|
||||||
|
void RecordOutputRenderQueueWait(double queueWaitMilliseconds);
|
||||||
|
bool TryRecordOutputRenderQueueWait(double queueWaitMilliseconds);
|
||||||
|
|
||||||
|
void RecordSystemMemoryPlayoutStats(std::size_t freeFrameCount, std::size_t readyFrameCount,
|
||||||
|
std::size_t scheduledFrameCount, uint64_t underrunCount, uint64_t repeatCount, uint64_t dropCount,
|
||||||
|
double frameAgeAtScheduleMilliseconds, double frameAgeAtCompletionMilliseconds);
|
||||||
|
bool TryRecordSystemMemoryPlayoutStats(std::size_t freeFrameCount, std::size_t readyFrameCount,
|
||||||
|
std::size_t scheduledFrameCount, uint64_t underrunCount, uint64_t repeatCount, uint64_t dropCount,
|
||||||
|
double frameAgeAtScheduleMilliseconds, double frameAgeAtCompletionMilliseconds);
|
||||||
|
|
||||||
|
void RecordDeckLinkBufferTelemetry(bool actualBufferedFramesAvailable, uint64_t actualBufferedFrames,
|
||||||
|
std::size_t targetBufferedFrames, double scheduleCallMilliseconds, uint64_t scheduleFailureCount);
|
||||||
|
bool TryRecordDeckLinkBufferTelemetry(bool actualBufferedFramesAvailable, uint64_t actualBufferedFrames,
|
||||||
|
std::size_t targetBufferedFrames, double scheduleCallMilliseconds, uint64_t scheduleFailureCount);
|
||||||
|
|
||||||
|
void RecordOutputRenderPipelineTiming(
|
||||||
|
double drawMilliseconds,
|
||||||
|
double fenceWaitMilliseconds,
|
||||||
|
double mapMilliseconds,
|
||||||
|
double readbackCopyMilliseconds,
|
||||||
|
double cachedCopyMilliseconds,
|
||||||
|
double asyncQueueMilliseconds,
|
||||||
|
double asyncQueueBufferMilliseconds,
|
||||||
|
double asyncQueueSetupMilliseconds,
|
||||||
|
double asyncQueueReadPixelsMilliseconds,
|
||||||
|
double asyncQueueFenceMilliseconds,
|
||||||
|
double syncReadMilliseconds,
|
||||||
|
bool asyncReadbackMissed,
|
||||||
|
bool cachedFallbackUsed,
|
||||||
|
bool syncFallbackUsed);
|
||||||
|
bool TryRecordOutputRenderPipelineTiming(
|
||||||
|
double drawMilliseconds,
|
||||||
|
double fenceWaitMilliseconds,
|
||||||
|
double mapMilliseconds,
|
||||||
|
double readbackCopyMilliseconds,
|
||||||
|
double cachedCopyMilliseconds,
|
||||||
|
double asyncQueueMilliseconds,
|
||||||
|
double asyncQueueBufferMilliseconds,
|
||||||
|
double asyncQueueSetupMilliseconds,
|
||||||
|
double asyncQueueReadPixelsMilliseconds,
|
||||||
|
double asyncQueueFenceMilliseconds,
|
||||||
|
double syncReadMilliseconds,
|
||||||
|
bool asyncReadbackMissed,
|
||||||
|
bool cachedFallbackUsed,
|
||||||
|
bool syncFallbackUsed);
|
||||||
|
|
||||||
SignalStatusSnapshot GetSignalStatusSnapshot() const;
|
SignalStatusSnapshot GetSignalStatusSnapshot() const;
|
||||||
VideoIOStatusSnapshot GetVideoIOStatusSnapshot() const;
|
VideoIOStatusSnapshot GetVideoIOStatusSnapshot() const;
|
||||||
PerformanceSnapshot GetPerformanceSnapshot() const;
|
PerformanceSnapshot GetPerformanceSnapshot() const;
|
||||||
RuntimeEventMetricsSnapshot GetRuntimeEventMetricsSnapshot() const;
|
RuntimeEventMetricsSnapshot GetRuntimeEventMetricsSnapshot() const;
|
||||||
|
PersistenceSnapshot GetPersistenceSnapshot() const;
|
||||||
|
BackendPlayoutSnapshot GetBackendPlayoutSnapshot() const;
|
||||||
Snapshot GetSnapshot() const;
|
Snapshot GetSnapshot() const;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
@@ -119,4 +268,6 @@ private:
|
|||||||
VideoIOStatusSnapshot mVideoIOStatus;
|
VideoIOStatusSnapshot mVideoIOStatus;
|
||||||
PerformanceSnapshot mPerformance;
|
PerformanceSnapshot mPerformance;
|
||||||
RuntimeEventMetricsSnapshot mRuntimeEvents;
|
RuntimeEventMetricsSnapshot mRuntimeEvents;
|
||||||
|
PersistenceSnapshot mPersistence;
|
||||||
|
BackendPlayoutSnapshot mBackendPlayout;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,89 @@
|
|||||||
|
#include "OutputProductionController.h"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
std::size_t ClampReadyLimit(unsigned value, std::size_t capacity)
|
||||||
|
{
|
||||||
|
const std::size_t requested = static_cast<std::size_t>(value);
|
||||||
|
if (capacity == 0)
|
||||||
|
return requested;
|
||||||
|
return (std::min)(requested, capacity);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
OutputProductionController::OutputProductionController(const VideoPlayoutPolicy& policy) :
|
||||||
|
mPolicy(NormalizeVideoPlayoutPolicy(policy))
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
void OutputProductionController::Configure(const VideoPlayoutPolicy& policy)
|
||||||
|
{
|
||||||
|
mPolicy = NormalizeVideoPlayoutPolicy(policy);
|
||||||
|
}
|
||||||
|
|
||||||
|
OutputProductionDecision OutputProductionController::Decide(const OutputProductionPressure& pressure) const
|
||||||
|
{
|
||||||
|
OutputProductionDecision decision;
|
||||||
|
|
||||||
|
const std::size_t configuredMaxReadyFrames = static_cast<std::size_t>(mPolicy.maxReadyFrames);
|
||||||
|
const std::size_t effectiveMaxReadyFrames = pressure.readyQueueCapacity > 0
|
||||||
|
? (std::min)(configuredMaxReadyFrames, pressure.readyQueueCapacity)
|
||||||
|
: configuredMaxReadyFrames;
|
||||||
|
const std::size_t effectiveTargetReadyFrames = (std::min)(
|
||||||
|
ClampReadyLimit(mPolicy.targetReadyFrames, pressure.readyQueueCapacity),
|
||||||
|
effectiveMaxReadyFrames);
|
||||||
|
|
||||||
|
decision.targetReadyFrames = effectiveTargetReadyFrames;
|
||||||
|
decision.maxReadyFrames = effectiveMaxReadyFrames;
|
||||||
|
|
||||||
|
if (effectiveMaxReadyFrames == 0)
|
||||||
|
{
|
||||||
|
decision.action = OutputProductionAction::Throttle;
|
||||||
|
decision.reason = "no-ready-frame-capacity";
|
||||||
|
return decision;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pressure.readyQueueDepth >= effectiveMaxReadyFrames)
|
||||||
|
{
|
||||||
|
decision.action = OutputProductionAction::Throttle;
|
||||||
|
decision.reason = "ready-queue-full";
|
||||||
|
return decision;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pressure.readyQueueDepth < effectiveTargetReadyFrames)
|
||||||
|
{
|
||||||
|
decision.action = OutputProductionAction::Produce;
|
||||||
|
decision.requestedFrames = effectiveTargetReadyFrames - pressure.readyQueueDepth;
|
||||||
|
decision.reason = "ready-queue-below-target";
|
||||||
|
return decision;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((pressure.lateStreak > 0 || pressure.dropStreak > 0 || pressure.readyQueueUnderrunCount > 0) &&
|
||||||
|
pressure.readyQueueDepth < effectiveMaxReadyFrames)
|
||||||
|
{
|
||||||
|
decision.action = OutputProductionAction::Produce;
|
||||||
|
decision.requestedFrames = 1;
|
||||||
|
decision.reason = "playout-pressure";
|
||||||
|
return decision;
|
||||||
|
}
|
||||||
|
|
||||||
|
decision.action = OutputProductionAction::Wait;
|
||||||
|
decision.reason = "ready-queue-at-target";
|
||||||
|
return decision;
|
||||||
|
}
|
||||||
|
|
||||||
|
const char* OutputProductionActionName(OutputProductionAction action)
|
||||||
|
{
|
||||||
|
switch (action)
|
||||||
|
{
|
||||||
|
case OutputProductionAction::Produce:
|
||||||
|
return "Produce";
|
||||||
|
case OutputProductionAction::Throttle:
|
||||||
|
return "Throttle";
|
||||||
|
case OutputProductionAction::Wait:
|
||||||
|
default:
|
||||||
|
return "Wait";
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "VideoPlayoutPolicy.h"
|
||||||
|
|
||||||
|
#include <cstddef>
|
||||||
|
#include <cstdint>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
enum class OutputProductionAction
|
||||||
|
{
|
||||||
|
Produce,
|
||||||
|
Wait,
|
||||||
|
Throttle
|
||||||
|
};
|
||||||
|
|
||||||
|
struct OutputProductionPressure
|
||||||
|
{
|
||||||
|
std::size_t readyQueueDepth = 0;
|
||||||
|
std::size_t readyQueueCapacity = 0;
|
||||||
|
uint64_t readyQueueUnderrunCount = 0;
|
||||||
|
uint64_t lateStreak = 0;
|
||||||
|
uint64_t dropStreak = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct OutputProductionDecision
|
||||||
|
{
|
||||||
|
OutputProductionAction action = OutputProductionAction::Wait;
|
||||||
|
std::size_t requestedFrames = 0;
|
||||||
|
std::size_t targetReadyFrames = 0;
|
||||||
|
std::size_t maxReadyFrames = 0;
|
||||||
|
std::string reason;
|
||||||
|
};
|
||||||
|
|
||||||
|
class OutputProductionController
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
explicit OutputProductionController(const VideoPlayoutPolicy& policy = VideoPlayoutPolicy());
|
||||||
|
|
||||||
|
void Configure(const VideoPlayoutPolicy& policy);
|
||||||
|
OutputProductionDecision Decide(const OutputProductionPressure& pressure) const;
|
||||||
|
|
||||||
|
private:
|
||||||
|
VideoPlayoutPolicy mPolicy;
|
||||||
|
};
|
||||||
|
|
||||||
|
const char* OutputProductionActionName(OutputProductionAction action);
|
||||||
@@ -0,0 +1,102 @@
|
|||||||
|
#include "RenderCadenceController.h"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cmath>
|
||||||
|
|
||||||
|
void RenderCadenceController::Configure(Duration targetFrameDuration, TimePoint firstRenderTime, const RenderCadencePolicy& policy)
|
||||||
|
{
|
||||||
|
mTargetFrameDuration = IsPositive(targetFrameDuration) ? targetFrameDuration : std::chrono::milliseconds(1);
|
||||||
|
mPolicy = policy;
|
||||||
|
if (mPolicy.skipThresholdFrames < 1.0)
|
||||||
|
mPolicy.skipThresholdFrames = 1.0;
|
||||||
|
Reset(firstRenderTime);
|
||||||
|
}
|
||||||
|
|
||||||
|
void RenderCadenceController::Reset(TimePoint firstRenderTime)
|
||||||
|
{
|
||||||
|
mNextRenderTime = firstRenderTime;
|
||||||
|
mNextFrameIndex = 0;
|
||||||
|
mMetrics = RenderCadenceMetrics();
|
||||||
|
}
|
||||||
|
|
||||||
|
RenderCadenceDecision RenderCadenceController::Tick(TimePoint now)
|
||||||
|
{
|
||||||
|
RenderCadenceDecision decision;
|
||||||
|
decision.frameIndex = mNextFrameIndex;
|
||||||
|
decision.renderTargetTime = mNextRenderTime;
|
||||||
|
decision.nextRenderTime = mNextRenderTime;
|
||||||
|
|
||||||
|
if (now < mNextRenderTime)
|
||||||
|
{
|
||||||
|
decision.action = RenderCadenceAction::Wait;
|
||||||
|
decision.waitDuration = mNextRenderTime - now;
|
||||||
|
decision.reason = "waiting-for-next-render-tick";
|
||||||
|
return decision;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Duration lateness = now - mNextRenderTime;
|
||||||
|
const uint64_t skippedTicks = SkippedTicksForLateness(lateness);
|
||||||
|
if (skippedTicks > 0)
|
||||||
|
{
|
||||||
|
decision.skippedTicks = skippedTicks;
|
||||||
|
decision.frameIndex = mNextFrameIndex + skippedTicks;
|
||||||
|
decision.renderTargetTime = mNextRenderTime + (mTargetFrameDuration * skippedTicks);
|
||||||
|
decision.reason = "late-skip-render-ticks";
|
||||||
|
mMetrics.skippedTickCount += skippedTicks;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
decision.reason = IsPositive(lateness) ? "late-render-now" : "on-time-render";
|
||||||
|
}
|
||||||
|
|
||||||
|
decision.action = RenderCadenceAction::Render;
|
||||||
|
decision.lateness = now > decision.renderTargetTime
|
||||||
|
? now - decision.renderTargetTime
|
||||||
|
: Duration::zero();
|
||||||
|
mNextFrameIndex = decision.frameIndex + 1;
|
||||||
|
mNextRenderTime = decision.renderTargetTime + mTargetFrameDuration;
|
||||||
|
decision.nextRenderTime = mNextRenderTime;
|
||||||
|
|
||||||
|
++mMetrics.renderedFrameCount;
|
||||||
|
mMetrics.nextFrameIndex = mNextFrameIndex;
|
||||||
|
mMetrics.lastLateness = decision.lateness;
|
||||||
|
if (IsPositive(decision.lateness))
|
||||||
|
{
|
||||||
|
++mMetrics.lateFrameCount;
|
||||||
|
mMetrics.maxLateness = (std::max)(mMetrics.maxLateness, decision.lateness);
|
||||||
|
}
|
||||||
|
|
||||||
|
return decision;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint64_t RenderCadenceController::SkippedTicksForLateness(Duration lateness) const
|
||||||
|
{
|
||||||
|
if (!mPolicy.skipLateTicks || !IsPositive(lateness) || !IsPositive(mTargetFrameDuration))
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
const double lateFrames = static_cast<double>(lateness.count()) / static_cast<double>(mTargetFrameDuration.count());
|
||||||
|
if (lateFrames < mPolicy.skipThresholdFrames)
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
const uint64_t elapsedTicks = static_cast<uint64_t>(std::floor(lateFrames));
|
||||||
|
if (elapsedTicks == 0)
|
||||||
|
return 0;
|
||||||
|
return (std::min)(elapsedTicks, mPolicy.maxSkippedTicksPerDecision);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool RenderCadenceController::IsPositive(Duration duration)
|
||||||
|
{
|
||||||
|
return duration > Duration::zero();
|
||||||
|
}
|
||||||
|
|
||||||
|
const char* RenderCadenceActionName(RenderCadenceAction action)
|
||||||
|
{
|
||||||
|
switch (action)
|
||||||
|
{
|
||||||
|
case RenderCadenceAction::Render:
|
||||||
|
return "Render";
|
||||||
|
case RenderCadenceAction::Wait:
|
||||||
|
default:
|
||||||
|
return "Wait";
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <chrono>
|
||||||
|
#include <cstdint>
|
||||||
|
|
||||||
|
enum class RenderCadenceAction
|
||||||
|
{
|
||||||
|
Wait,
|
||||||
|
Render
|
||||||
|
};
|
||||||
|
|
||||||
|
struct RenderCadencePolicy
|
||||||
|
{
|
||||||
|
bool skipLateTicks = true;
|
||||||
|
uint64_t maxSkippedTicksPerDecision = 4;
|
||||||
|
double skipThresholdFrames = 2.0;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct RenderCadenceDecision
|
||||||
|
{
|
||||||
|
RenderCadenceAction action = RenderCadenceAction::Wait;
|
||||||
|
uint64_t frameIndex = 0;
|
||||||
|
uint64_t skippedTicks = 0;
|
||||||
|
std::chrono::steady_clock::time_point renderTargetTime;
|
||||||
|
std::chrono::steady_clock::time_point nextRenderTime;
|
||||||
|
std::chrono::steady_clock::duration waitDuration = std::chrono::steady_clock::duration::zero();
|
||||||
|
std::chrono::steady_clock::duration lateness = std::chrono::steady_clock::duration::zero();
|
||||||
|
const char* reason = "waiting-for-next-render-tick";
|
||||||
|
};
|
||||||
|
|
||||||
|
struct RenderCadenceMetrics
|
||||||
|
{
|
||||||
|
uint64_t nextFrameIndex = 0;
|
||||||
|
uint64_t renderedFrameCount = 0;
|
||||||
|
uint64_t skippedTickCount = 0;
|
||||||
|
uint64_t lateFrameCount = 0;
|
||||||
|
std::chrono::steady_clock::duration lastLateness = std::chrono::steady_clock::duration::zero();
|
||||||
|
std::chrono::steady_clock::duration maxLateness = std::chrono::steady_clock::duration::zero();
|
||||||
|
};
|
||||||
|
|
||||||
|
class RenderCadenceController
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
using Clock = std::chrono::steady_clock;
|
||||||
|
using TimePoint = Clock::time_point;
|
||||||
|
using Duration = Clock::duration;
|
||||||
|
|
||||||
|
void Configure(Duration targetFrameDuration, TimePoint firstRenderTime, const RenderCadencePolicy& policy = RenderCadencePolicy());
|
||||||
|
void Reset(TimePoint firstRenderTime);
|
||||||
|
RenderCadenceDecision Tick(TimePoint now);
|
||||||
|
|
||||||
|
Duration TargetFrameDuration() const { return mTargetFrameDuration; }
|
||||||
|
TimePoint NextRenderTime() const { return mNextRenderTime; }
|
||||||
|
uint64_t NextFrameIndex() const { return mNextFrameIndex; }
|
||||||
|
const RenderCadenceMetrics& Metrics() const { return mMetrics; }
|
||||||
|
|
||||||
|
private:
|
||||||
|
uint64_t SkippedTicksForLateness(Duration lateness) const;
|
||||||
|
static bool IsPositive(Duration duration);
|
||||||
|
|
||||||
|
Duration mTargetFrameDuration = std::chrono::milliseconds(16);
|
||||||
|
TimePoint mNextRenderTime;
|
||||||
|
uint64_t mNextFrameIndex = 0;
|
||||||
|
RenderCadencePolicy mPolicy;
|
||||||
|
RenderCadenceMetrics mMetrics;
|
||||||
|
};
|
||||||
|
|
||||||
|
const char* RenderCadenceActionName(RenderCadenceAction action);
|
||||||
@@ -0,0 +1,93 @@
|
|||||||
|
#include "RenderOutputQueue.h"
|
||||||
|
|
||||||
|
RenderOutputQueue::RenderOutputQueue(const VideoPlayoutPolicy& policy) :
|
||||||
|
mPolicy(NormalizeVideoPlayoutPolicy(policy))
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
void RenderOutputQueue::Configure(const VideoPlayoutPolicy& policy)
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(mMutex);
|
||||||
|
mPolicy = NormalizeVideoPlayoutPolicy(policy);
|
||||||
|
while (mReadyFrames.size() > CapacityLocked())
|
||||||
|
{
|
||||||
|
ReleaseFrame(mReadyFrames.front());
|
||||||
|
mReadyFrames.pop_front();
|
||||||
|
++mDroppedCount;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool RenderOutputQueue::Push(RenderOutputFrame frame)
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(mMutex);
|
||||||
|
if (mReadyFrames.size() >= CapacityLocked())
|
||||||
|
{
|
||||||
|
ReleaseFrame(mReadyFrames.front());
|
||||||
|
mReadyFrames.pop_front();
|
||||||
|
++mDroppedCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
mReadyFrames.push_back(frame);
|
||||||
|
++mPushedCount;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool RenderOutputQueue::TryPop(RenderOutputFrame& frame)
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(mMutex);
|
||||||
|
if (mReadyFrames.empty())
|
||||||
|
{
|
||||||
|
++mUnderrunCount;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
frame = mReadyFrames.front();
|
||||||
|
mReadyFrames.pop_front();
|
||||||
|
++mPoppedCount;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool RenderOutputQueue::DropOldestFrame()
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(mMutex);
|
||||||
|
if (mReadyFrames.empty())
|
||||||
|
return false;
|
||||||
|
|
||||||
|
ReleaseFrame(mReadyFrames.front());
|
||||||
|
mReadyFrames.pop_front();
|
||||||
|
++mDroppedCount;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void RenderOutputQueue::Clear()
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(mMutex);
|
||||||
|
for (RenderOutputFrame& frame : mReadyFrames)
|
||||||
|
ReleaseFrame(frame);
|
||||||
|
mReadyFrames.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
RenderOutputQueueMetrics RenderOutputQueue::GetMetrics() const
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(mMutex);
|
||||||
|
RenderOutputQueueMetrics metrics;
|
||||||
|
metrics.depth = mReadyFrames.size();
|
||||||
|
metrics.capacity = CapacityLocked();
|
||||||
|
metrics.pushedCount = mPushedCount;
|
||||||
|
metrics.poppedCount = mPoppedCount;
|
||||||
|
metrics.droppedCount = mDroppedCount;
|
||||||
|
metrics.underrunCount = mUnderrunCount;
|
||||||
|
return metrics;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::size_t RenderOutputQueue::CapacityLocked() const
|
||||||
|
{
|
||||||
|
return static_cast<std::size_t>(mPolicy.maxReadyFrames);
|
||||||
|
}
|
||||||
|
|
||||||
|
void RenderOutputQueue::ReleaseFrame(RenderOutputFrame& frame)
|
||||||
|
{
|
||||||
|
if (frame.releaseFrame)
|
||||||
|
frame.releaseFrame(frame.frame);
|
||||||
|
frame.releaseFrame = {};
|
||||||
|
}
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "VideoIOTypes.h"
|
||||||
|
#include "VideoPlayoutPolicy.h"
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
#include <deque>
|
||||||
|
#include <functional>
|
||||||
|
#include <mutex>
|
||||||
|
|
||||||
|
struct RenderOutputFrame
|
||||||
|
{
|
||||||
|
VideoIOOutputFrame frame;
|
||||||
|
uint64_t frameIndex = 0;
|
||||||
|
bool stale = false;
|
||||||
|
std::function<void(VideoIOOutputFrame& frame)> releaseFrame;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct RenderOutputQueueMetrics
|
||||||
|
{
|
||||||
|
std::size_t depth = 0;
|
||||||
|
std::size_t capacity = 0;
|
||||||
|
uint64_t pushedCount = 0;
|
||||||
|
uint64_t poppedCount = 0;
|
||||||
|
uint64_t droppedCount = 0;
|
||||||
|
uint64_t underrunCount = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
class RenderOutputQueue
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
explicit RenderOutputQueue(const VideoPlayoutPolicy& policy = VideoPlayoutPolicy());
|
||||||
|
|
||||||
|
void Configure(const VideoPlayoutPolicy& policy);
|
||||||
|
bool Push(RenderOutputFrame frame);
|
||||||
|
bool TryPop(RenderOutputFrame& frame);
|
||||||
|
bool DropOldestFrame();
|
||||||
|
void Clear();
|
||||||
|
RenderOutputQueueMetrics GetMetrics() const;
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::size_t CapacityLocked() const;
|
||||||
|
static void ReleaseFrame(RenderOutputFrame& frame);
|
||||||
|
|
||||||
|
mutable std::mutex mMutex;
|
||||||
|
VideoPlayoutPolicy mPolicy;
|
||||||
|
std::deque<RenderOutputFrame> mReadyFrames;
|
||||||
|
uint64_t mPushedCount = 0;
|
||||||
|
uint64_t mPoppedCount = 0;
|
||||||
|
uint64_t mDroppedCount = 0;
|
||||||
|
uint64_t mUnderrunCount = 0;
|
||||||
|
};
|
||||||
@@ -0,0 +1,260 @@
|
|||||||
|
#include "SystemOutputFramePool.h"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
SystemOutputFramePoolConfig NormalizeConfig(SystemOutputFramePoolConfig config)
|
||||||
|
{
|
||||||
|
if (config.rowBytes == 0)
|
||||||
|
config.rowBytes = VideoIORowBytes(config.pixelFormat, config.width);
|
||||||
|
return config;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SystemOutputFramePool::SystemOutputFramePool(const SystemOutputFramePoolConfig& config)
|
||||||
|
{
|
||||||
|
Configure(config);
|
||||||
|
}
|
||||||
|
|
||||||
|
void SystemOutputFramePool::Configure(const SystemOutputFramePoolConfig& config)
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(mMutex);
|
||||||
|
mConfig = NormalizeConfig(config);
|
||||||
|
mReadySlots.clear();
|
||||||
|
mSlots.clear();
|
||||||
|
mSlots.resize(mConfig.capacity);
|
||||||
|
|
||||||
|
const std::size_t byteCount = FrameByteCount();
|
||||||
|
for (StoredSlot& slot : mSlots)
|
||||||
|
{
|
||||||
|
slot.bytes.resize(byteCount);
|
||||||
|
slot.state = OutputFrameSlotState::Free;
|
||||||
|
++slot.generation;
|
||||||
|
}
|
||||||
|
|
||||||
|
mAcquireMissCount = 0;
|
||||||
|
mReadyUnderrunCount = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
SystemOutputFramePoolConfig SystemOutputFramePool::Config() const
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(mMutex);
|
||||||
|
return mConfig;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool SystemOutputFramePool::AcquireFreeSlot(OutputFrameSlot& slot)
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(mMutex);
|
||||||
|
for (std::size_t index = 0; index < mSlots.size(); ++index)
|
||||||
|
{
|
||||||
|
if (mSlots[index].state != OutputFrameSlotState::Free)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
mSlots[index].state = OutputFrameSlotState::Rendering;
|
||||||
|
++mSlots[index].generation;
|
||||||
|
FillOutputSlotLocked(index, slot);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
slot = OutputFrameSlot();
|
||||||
|
++mAcquireMissCount;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool SystemOutputFramePool::AcquireRenderingSlot(OutputFrameSlot& slot)
|
||||||
|
{
|
||||||
|
return AcquireFreeSlot(slot);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool SystemOutputFramePool::PublishReadySlot(const OutputFrameSlot& slot)
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(mMutex);
|
||||||
|
if (!TransitionSlotLocked(slot, OutputFrameSlotState::Rendering, OutputFrameSlotState::Completed))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
mReadySlots.push_back(slot.index);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool SystemOutputFramePool::PublishCompletedSlot(const OutputFrameSlot& slot)
|
||||||
|
{
|
||||||
|
return PublishReadySlot(slot);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool SystemOutputFramePool::ConsumeReadySlot(OutputFrameSlot& slot)
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(mMutex);
|
||||||
|
while (!mReadySlots.empty())
|
||||||
|
{
|
||||||
|
const std::size_t index = mReadySlots.front();
|
||||||
|
mReadySlots.pop_front();
|
||||||
|
if (index >= mSlots.size() || mSlots[index].state != OutputFrameSlotState::Completed)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
FillOutputSlotLocked(index, slot);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
slot = OutputFrameSlot();
|
||||||
|
++mReadyUnderrunCount;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool SystemOutputFramePool::ConsumeCompletedSlot(OutputFrameSlot& slot)
|
||||||
|
{
|
||||||
|
return ConsumeReadySlot(slot);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool SystemOutputFramePool::MarkScheduled(const OutputFrameSlot& slot)
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(mMutex);
|
||||||
|
if (!IsValidSlotLocked(slot))
|
||||||
|
return false;
|
||||||
|
if (mSlots[slot.index].state != OutputFrameSlotState::Completed)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
RemoveReadyIndexLocked(slot.index);
|
||||||
|
mSlots[slot.index].state = OutputFrameSlotState::Scheduled;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool SystemOutputFramePool::MarkScheduledByBuffer(void* bytes)
|
||||||
|
{
|
||||||
|
if (bytes == nullptr)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
std::lock_guard<std::mutex> lock(mMutex);
|
||||||
|
for (std::size_t index = 0; index < mSlots.size(); ++index)
|
||||||
|
{
|
||||||
|
if (mSlots[index].bytes.empty() || mSlots[index].bytes.data() != bytes)
|
||||||
|
continue;
|
||||||
|
if (mSlots[index].state != OutputFrameSlotState::Completed)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
RemoveReadyIndexLocked(index);
|
||||||
|
mSlots[index].state = OutputFrameSlotState::Scheduled;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool SystemOutputFramePool::ReleaseSlot(const OutputFrameSlot& slot)
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(mMutex);
|
||||||
|
if (!IsValidSlotLocked(slot) || mSlots[slot.index].state == OutputFrameSlotState::Free)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
return ReleaseSlotByIndexLocked(slot.index);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool SystemOutputFramePool::ReleaseScheduledSlot(const OutputFrameSlot& slot)
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(mMutex);
|
||||||
|
return TransitionSlotLocked(slot, OutputFrameSlotState::Scheduled, OutputFrameSlotState::Free);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool SystemOutputFramePool::ReleaseSlotByBuffer(void* bytes)
|
||||||
|
{
|
||||||
|
if (bytes == nullptr)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
std::lock_guard<std::mutex> lock(mMutex);
|
||||||
|
for (std::size_t index = 0; index < mSlots.size(); ++index)
|
||||||
|
{
|
||||||
|
if (!mSlots[index].bytes.empty() && mSlots[index].bytes.data() == bytes)
|
||||||
|
return ReleaseSlotByIndexLocked(index);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void SystemOutputFramePool::Clear()
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(mMutex);
|
||||||
|
mReadySlots.clear();
|
||||||
|
for (StoredSlot& slot : mSlots)
|
||||||
|
{
|
||||||
|
slot.state = OutputFrameSlotState::Free;
|
||||||
|
++slot.generation;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SystemOutputFramePoolMetrics SystemOutputFramePool::GetMetrics() const
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(mMutex);
|
||||||
|
SystemOutputFramePoolMetrics metrics;
|
||||||
|
metrics.capacity = mSlots.size();
|
||||||
|
metrics.readyCount = mReadySlots.size();
|
||||||
|
metrics.acquireMissCount = mAcquireMissCount;
|
||||||
|
metrics.readyUnderrunCount = mReadyUnderrunCount;
|
||||||
|
|
||||||
|
for (const StoredSlot& slot : mSlots)
|
||||||
|
{
|
||||||
|
switch (slot.state)
|
||||||
|
{
|
||||||
|
case OutputFrameSlotState::Free:
|
||||||
|
++metrics.freeCount;
|
||||||
|
break;
|
||||||
|
case OutputFrameSlotState::Rendering:
|
||||||
|
++metrics.renderingCount;
|
||||||
|
++metrics.acquiredCount;
|
||||||
|
break;
|
||||||
|
case OutputFrameSlotState::Completed:
|
||||||
|
++metrics.completedCount;
|
||||||
|
break;
|
||||||
|
case OutputFrameSlotState::Scheduled:
|
||||||
|
++metrics.scheduledCount;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return metrics;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool SystemOutputFramePool::IsValidSlotLocked(const OutputFrameSlot& slot) const
|
||||||
|
{
|
||||||
|
return slot.index < mSlots.size() && mSlots[slot.index].generation == slot.generation;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool SystemOutputFramePool::TransitionSlotLocked(const OutputFrameSlot& slot, OutputFrameSlotState expectedState, OutputFrameSlotState nextState)
|
||||||
|
{
|
||||||
|
if (!IsValidSlotLocked(slot) || mSlots[slot.index].state != expectedState)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
mSlots[slot.index].state = nextState;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void SystemOutputFramePool::FillOutputSlotLocked(std::size_t index, OutputFrameSlot& slot)
|
||||||
|
{
|
||||||
|
StoredSlot& storedSlot = mSlots[index];
|
||||||
|
slot.index = index;
|
||||||
|
slot.generation = storedSlot.generation;
|
||||||
|
slot.frame.bytes = storedSlot.bytes.empty() ? nullptr : storedSlot.bytes.data();
|
||||||
|
slot.frame.rowBytes = static_cast<long>(mConfig.rowBytes);
|
||||||
|
slot.frame.width = mConfig.width;
|
||||||
|
slot.frame.height = mConfig.height;
|
||||||
|
slot.frame.pixelFormat = mConfig.pixelFormat;
|
||||||
|
slot.frame.nativeFrame = nullptr;
|
||||||
|
slot.frame.nativeBuffer = slot.frame.bytes;
|
||||||
|
}
|
||||||
|
|
||||||
|
void SystemOutputFramePool::RemoveReadyIndexLocked(std::size_t index)
|
||||||
|
{
|
||||||
|
mReadySlots.erase(std::remove(mReadySlots.begin(), mReadySlots.end(), index), mReadySlots.end());
|
||||||
|
}
|
||||||
|
|
||||||
|
bool SystemOutputFramePool::ReleaseSlotByIndexLocked(std::size_t index)
|
||||||
|
{
|
||||||
|
if (index >= mSlots.size() || mSlots[index].state == OutputFrameSlotState::Free)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
RemoveReadyIndexLocked(index);
|
||||||
|
mSlots[index].state = OutputFrameSlotState::Free;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::size_t SystemOutputFramePool::FrameByteCount() const
|
||||||
|
{
|
||||||
|
return static_cast<std::size_t>(mConfig.rowBytes) * static_cast<std::size_t>(mConfig.height);
|
||||||
|
}
|
||||||
@@ -0,0 +1,94 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "VideoIOTypes.h"
|
||||||
|
|
||||||
|
#include <cstddef>
|
||||||
|
#include <cstdint>
|
||||||
|
#include <deque>
|
||||||
|
#include <mutex>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
enum class OutputFrameSlotState
|
||||||
|
{
|
||||||
|
Free,
|
||||||
|
Rendering,
|
||||||
|
Completed,
|
||||||
|
Scheduled
|
||||||
|
};
|
||||||
|
|
||||||
|
struct SystemOutputFramePoolConfig
|
||||||
|
{
|
||||||
|
unsigned width = 0;
|
||||||
|
unsigned height = 0;
|
||||||
|
VideoIOPixelFormat pixelFormat = VideoIOPixelFormat::Bgra8;
|
||||||
|
unsigned rowBytes = 0;
|
||||||
|
std::size_t capacity = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct OutputFrameSlot
|
||||||
|
{
|
||||||
|
VideoIOOutputFrame frame;
|
||||||
|
std::size_t index = 0;
|
||||||
|
uint64_t generation = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct SystemOutputFramePoolMetrics
|
||||||
|
{
|
||||||
|
std::size_t capacity = 0;
|
||||||
|
std::size_t freeCount = 0;
|
||||||
|
std::size_t renderingCount = 0;
|
||||||
|
std::size_t completedCount = 0;
|
||||||
|
std::size_t scheduledCount = 0;
|
||||||
|
std::size_t acquiredCount = 0;
|
||||||
|
std::size_t readyCount = 0;
|
||||||
|
std::size_t consumedCount = 0;
|
||||||
|
uint64_t acquireMissCount = 0;
|
||||||
|
uint64_t readyUnderrunCount = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
class SystemOutputFramePool
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
SystemOutputFramePool() = default;
|
||||||
|
explicit SystemOutputFramePool(const SystemOutputFramePoolConfig& config);
|
||||||
|
|
||||||
|
void Configure(const SystemOutputFramePoolConfig& config);
|
||||||
|
SystemOutputFramePoolConfig Config() const;
|
||||||
|
|
||||||
|
bool AcquireFreeSlot(OutputFrameSlot& slot);
|
||||||
|
bool AcquireRenderingSlot(OutputFrameSlot& slot);
|
||||||
|
bool PublishReadySlot(const OutputFrameSlot& slot);
|
||||||
|
bool PublishCompletedSlot(const OutputFrameSlot& slot);
|
||||||
|
bool ConsumeReadySlot(OutputFrameSlot& slot);
|
||||||
|
bool ConsumeCompletedSlot(OutputFrameSlot& slot);
|
||||||
|
bool MarkScheduled(const OutputFrameSlot& slot);
|
||||||
|
bool MarkScheduledByBuffer(void* bytes);
|
||||||
|
bool ReleaseSlot(const OutputFrameSlot& slot);
|
||||||
|
bool ReleaseScheduledSlot(const OutputFrameSlot& slot);
|
||||||
|
bool ReleaseSlotByBuffer(void* bytes);
|
||||||
|
void Clear();
|
||||||
|
|
||||||
|
SystemOutputFramePoolMetrics GetMetrics() const;
|
||||||
|
|
||||||
|
private:
|
||||||
|
struct StoredSlot
|
||||||
|
{
|
||||||
|
std::vector<unsigned char> bytes;
|
||||||
|
OutputFrameSlotState state = OutputFrameSlotState::Free;
|
||||||
|
uint64_t generation = 1;
|
||||||
|
};
|
||||||
|
|
||||||
|
bool IsValidSlotLocked(const OutputFrameSlot& slot) const;
|
||||||
|
bool TransitionSlotLocked(const OutputFrameSlot& slot, OutputFrameSlotState expectedState, OutputFrameSlotState nextState);
|
||||||
|
void FillOutputSlotLocked(std::size_t index, OutputFrameSlot& slot);
|
||||||
|
void RemoveReadyIndexLocked(std::size_t index);
|
||||||
|
bool ReleaseSlotByIndexLocked(std::size_t index);
|
||||||
|
std::size_t FrameByteCount() const;
|
||||||
|
|
||||||
|
mutable std::mutex mMutex;
|
||||||
|
SystemOutputFramePoolConfig mConfig;
|
||||||
|
std::vector<StoredSlot> mSlots;
|
||||||
|
std::deque<std::size_t> mReadySlots;
|
||||||
|
uint64_t mAcquireMissCount = 0;
|
||||||
|
uint64_t mReadyUnderrunCount = 0;
|
||||||
|
};
|
||||||
@@ -6,14 +6,22 @@
|
|||||||
#include "RenderEngine.h"
|
#include "RenderEngine.h"
|
||||||
#include "RuntimeEventDispatcher.h"
|
#include "RuntimeEventDispatcher.h"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
#include <chrono>
|
#include <chrono>
|
||||||
|
#include <cstdlib>
|
||||||
|
#include <cstring>
|
||||||
|
#include <cmath>
|
||||||
#include <windows.h>
|
#include <windows.h>
|
||||||
|
|
||||||
VideoBackend::VideoBackend(RenderEngine& renderEngine, HealthTelemetry& healthTelemetry, RuntimeEventDispatcher& runtimeEventDispatcher) :
|
VideoBackend::VideoBackend(RenderEngine& renderEngine, HealthTelemetry& healthTelemetry, RuntimeEventDispatcher& runtimeEventDispatcher) :
|
||||||
mHealthTelemetry(healthTelemetry),
|
mHealthTelemetry(healthTelemetry),
|
||||||
mRuntimeEventDispatcher(runtimeEventDispatcher),
|
mRuntimeEventDispatcher(runtimeEventDispatcher),
|
||||||
|
mPlayoutPolicy(NormalizeVideoPlayoutPolicy(VideoPlayoutPolicy())),
|
||||||
|
mOutputProductionController(mPlayoutPolicy),
|
||||||
|
mReadyOutputQueue(mPlayoutPolicy),
|
||||||
mVideoIODevice(std::make_unique<DeckLinkSession>()),
|
mVideoIODevice(std::make_unique<DeckLinkSession>()),
|
||||||
mBridge(std::make_unique<OpenGLVideoIOBridge>(renderEngine))
|
mBridge(std::make_unique<OpenGLVideoIOBridge>(renderEngine)),
|
||||||
|
mInputCaptureDisabled(IsEnvironmentFlagEnabled("VST_DISABLE_INPUT_CAPTURE"))
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -24,48 +32,140 @@ VideoBackend::~VideoBackend()
|
|||||||
|
|
||||||
void VideoBackend::ReleaseResources()
|
void VideoBackend::ReleaseResources()
|
||||||
{
|
{
|
||||||
|
StopOutputCompletionWorker();
|
||||||
|
mReadyOutputQueue.Clear();
|
||||||
if (mVideoIODevice)
|
if (mVideoIODevice)
|
||||||
mVideoIODevice->ReleaseResources();
|
mVideoIODevice->ReleaseResources();
|
||||||
|
mSystemOutputFramePool.Clear();
|
||||||
|
if (!VideoBackendLifecycle::CanTransition(mLifecycle.State(), VideoBackendLifecycleState::Stopped))
|
||||||
|
ApplyLifecycleFailure("Video backend resources released before lifecycle completed.");
|
||||||
|
ApplyLifecycleTransition(VideoBackendLifecycleState::Stopped, "Video backend resources released.");
|
||||||
|
}
|
||||||
|
|
||||||
|
VideoBackendLifecycleState VideoBackend::LifecycleState() const
|
||||||
|
{
|
||||||
|
return mLifecycle.State();
|
||||||
}
|
}
|
||||||
|
|
||||||
bool VideoBackend::DiscoverDevicesAndModes(const VideoFormatSelection& videoModes, std::string& error)
|
bool VideoBackend::DiscoverDevicesAndModes(const VideoFormatSelection& videoModes, std::string& error)
|
||||||
{
|
{
|
||||||
return mVideoIODevice->DiscoverDevicesAndModes(videoModes, error);
|
ApplyLifecycleTransition(VideoBackendLifecycleState::Discovering, "Discovering video backend devices and modes.");
|
||||||
|
if (mVideoIODevice->DiscoverDevicesAndModes(videoModes, error))
|
||||||
|
return ApplyLifecycleTransition(VideoBackendLifecycleState::Discovered, "Video backend devices and modes discovered.");
|
||||||
|
|
||||||
|
ApplyLifecycleFailure(error.empty() ? "Video backend discovery failed." : error);
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool VideoBackend::SelectPreferredFormats(const VideoFormatSelection& videoModes, bool outputAlphaRequired, std::string& error)
|
bool VideoBackend::SelectPreferredFormats(const VideoFormatSelection& videoModes, bool outputAlphaRequired, std::string& error)
|
||||||
{
|
{
|
||||||
return mVideoIODevice->SelectPreferredFormats(videoModes, outputAlphaRequired, error);
|
ApplyLifecycleTransition(VideoBackendLifecycleState::Configuring, "Selecting preferred video backend formats.");
|
||||||
|
if (mVideoIODevice->SelectPreferredFormats(videoModes, outputAlphaRequired, error))
|
||||||
|
return true;
|
||||||
|
|
||||||
|
ApplyLifecycleFailure(error.empty() ? "Video backend format selection failed." : error);
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool VideoBackend::ConfigureInput(const VideoFormat& inputVideoMode, std::string& error)
|
bool VideoBackend::ConfigureInput(const VideoFormat& inputVideoMode, std::string& error)
|
||||||
{
|
{
|
||||||
return mVideoIODevice->ConfigureInput(
|
if (mLifecycle.State() != VideoBackendLifecycleState::Configuring)
|
||||||
|
ApplyLifecycleTransition(VideoBackendLifecycleState::Configuring, "Configuring video backend input.");
|
||||||
|
if (mInputCaptureDisabled)
|
||||||
|
{
|
||||||
|
MutableState().hasInputSource = false;
|
||||||
|
MutableState().statusMessage = "DeckLink input capture disabled by VST_DISABLE_INPUT_CAPTURE for output timing isolation.";
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (!mVideoIODevice->ConfigureInput(
|
||||||
[this](const VideoIOFrame& frame) { HandleInputFrame(frame); },
|
[this](const VideoIOFrame& frame) { HandleInputFrame(frame); },
|
||||||
inputVideoMode,
|
inputVideoMode,
|
||||||
error);
|
error))
|
||||||
|
{
|
||||||
|
ApplyLifecycleFailure(error.empty() ? "Video backend input configuration failed." : error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool VideoBackend::ConfigureOutput(const VideoFormat& outputVideoMode, bool externalKeyingEnabled, std::string& error)
|
bool VideoBackend::ConfigureOutput(const VideoFormat& outputVideoMode, bool externalKeyingEnabled, std::string& error)
|
||||||
{
|
{
|
||||||
return mVideoIODevice->ConfigureOutput(
|
mPlayoutPolicy = NormalizeVideoPlayoutPolicy(mPlayoutPolicy);
|
||||||
|
mOutputProductionController.Configure(mPlayoutPolicy);
|
||||||
|
mReadyOutputQueue.Configure(mPlayoutPolicy);
|
||||||
|
if (mLifecycle.State() != VideoBackendLifecycleState::Configuring)
|
||||||
|
ApplyLifecycleTransition(VideoBackendLifecycleState::Configuring, "Configuring video backend output.");
|
||||||
|
if (!mVideoIODevice->ConfigureOutput(
|
||||||
[this](const VideoIOCompletion& completion) { HandleOutputFrameCompletion(completion); },
|
[this](const VideoIOCompletion& completion) { HandleOutputFrameCompletion(completion); },
|
||||||
outputVideoMode,
|
outputVideoMode,
|
||||||
externalKeyingEnabled,
|
externalKeyingEnabled,
|
||||||
error);
|
error))
|
||||||
|
{
|
||||||
|
ApplyLifecycleFailure(error.empty() ? "Video backend output configuration failed." : error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
SystemOutputFramePoolConfig poolConfig;
|
||||||
|
poolConfig.width = mVideoIODevice->OutputFrameWidth();
|
||||||
|
poolConfig.height = mVideoIODevice->OutputFrameHeight();
|
||||||
|
poolConfig.pixelFormat = mVideoIODevice->OutputPixelFormat();
|
||||||
|
poolConfig.rowBytes = mVideoIODevice->OutputFrameRowBytes();
|
||||||
|
poolConfig.capacity = mPlayoutPolicy.outputFramePoolSize;
|
||||||
|
mSystemOutputFramePool.Configure(poolConfig);
|
||||||
|
RecordSystemMemoryPlayoutStats();
|
||||||
|
return ApplyLifecycleTransition(VideoBackendLifecycleState::Configured, "Video backend configured.");
|
||||||
}
|
}
|
||||||
|
|
||||||
bool VideoBackend::Start()
|
bool VideoBackend::Start()
|
||||||
{
|
{
|
||||||
const bool started = mVideoIODevice->Start();
|
ApplyLifecycleTransition(VideoBackendLifecycleState::Prerolling, "Video backend preroll starting.");
|
||||||
PublishBackendStateChanged(started ? "started" : "start-failed", started ? "Video backend started." : StatusMessage());
|
if (!mVideoIODevice->PrepareOutputSchedule())
|
||||||
return started;
|
{
|
||||||
|
ApplyLifecycleFailure(StatusMessage().empty() ? "Video backend output schedule preparation failed." : StatusMessage());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
StartOutputCompletionWorker();
|
||||||
|
StartOutputProducerWorker();
|
||||||
|
|
||||||
|
if (!WarmupOutputPreroll())
|
||||||
|
{
|
||||||
|
StopOutputProducerWorker();
|
||||||
|
StopOutputCompletionWorker();
|
||||||
|
ApplyLifecycleFailure(StatusMessage().empty() ? "Video backend preroll warmup failed." : StatusMessage());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!mInputCaptureDisabled && !mVideoIODevice->StartInputStreams())
|
||||||
|
{
|
||||||
|
StopOutputProducerWorker();
|
||||||
|
StopOutputCompletionWorker();
|
||||||
|
ApplyLifecycleFailure(StatusMessage().empty() ? "Video backend input stream start failed." : StatusMessage());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!mVideoIODevice->StartScheduledPlayback())
|
||||||
|
{
|
||||||
|
StopOutputProducerWorker();
|
||||||
|
mVideoIODevice->Stop();
|
||||||
|
StopOutputCompletionWorker();
|
||||||
|
ApplyLifecycleFailure(StatusMessage().empty() ? "Video backend scheduled playback start failed." : StatusMessage());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
ApplyLifecycleTransition(VideoBackendLifecycleState::Running, "Video backend started.");
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool VideoBackend::Stop()
|
bool VideoBackend::Stop()
|
||||||
{
|
{
|
||||||
|
ApplyLifecycleTransition(VideoBackendLifecycleState::Stopping, "Video backend stopping.");
|
||||||
|
StopOutputProducerWorker();
|
||||||
const bool stopped = mVideoIODevice->Stop();
|
const bool stopped = mVideoIODevice->Stop();
|
||||||
PublishBackendStateChanged(stopped ? "stopped" : "stop-failed", stopped ? "Video backend stopped." : StatusMessage());
|
StopOutputCompletionWorker();
|
||||||
|
if (stopped)
|
||||||
|
ApplyLifecycleTransition(VideoBackendLifecycleState::Stopped, "Video backend stopped.");
|
||||||
|
else
|
||||||
|
ApplyLifecycleFailure(StatusMessage().empty() ? "Video backend stop failed." : StatusMessage());
|
||||||
return stopped;
|
return stopped;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -94,9 +194,9 @@ bool VideoBackend::ScheduleOutputFrame(const VideoIOOutputFrame& frame)
|
|||||||
return mVideoIODevice->ScheduleOutputFrame(frame);
|
return mVideoIODevice->ScheduleOutputFrame(frame);
|
||||||
}
|
}
|
||||||
|
|
||||||
void VideoBackend::AccountForCompletionResult(VideoIOCompletionResult result)
|
VideoPlayoutRecoveryDecision VideoBackend::AccountForCompletionResult(VideoIOCompletionResult result, uint64_t readyQueueDepth)
|
||||||
{
|
{
|
||||||
mVideoIODevice->AccountForCompletionResult(result);
|
return mVideoIODevice->AccountForCompletionResult(result, readyQueueDepth);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool VideoBackend::HasInputDevice() const
|
bool VideoBackend::HasInputDevice() const
|
||||||
@@ -106,6 +206,8 @@ bool VideoBackend::HasInputDevice() const
|
|||||||
|
|
||||||
bool VideoBackend::HasInputSource() const
|
bool VideoBackend::HasInputSource() const
|
||||||
{
|
{
|
||||||
|
if (mInputCaptureDisabled)
|
||||||
|
return false;
|
||||||
return mVideoIODevice->HasInputSource();
|
return mVideoIODevice->HasInputSource();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -179,6 +281,12 @@ const std::string& VideoBackend::StatusMessage() const
|
|||||||
return mVideoIODevice->StatusMessage();
|
return mVideoIODevice->StatusMessage();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool VideoBackend::ShouldPrioritizeOutputOverPreview() const
|
||||||
|
{
|
||||||
|
const RenderOutputQueueMetrics metrics = mReadyOutputQueue.GetMetrics();
|
||||||
|
return metrics.depth < static_cast<std::size_t>(mPlayoutPolicy.targetReadyFrames);
|
||||||
|
}
|
||||||
|
|
||||||
void VideoBackend::SetStatusMessage(const std::string& message)
|
void VideoBackend::SetStatusMessage(const std::string& message)
|
||||||
{
|
{
|
||||||
mVideoIODevice->SetStatusMessage(message);
|
mVideoIODevice->SetStatusMessage(message);
|
||||||
@@ -198,7 +306,7 @@ void VideoBackend::PublishStatus(bool externalKeyingConfigured, const std::strin
|
|||||||
externalKeyingConfigured,
|
externalKeyingConfigured,
|
||||||
ExternalKeyingActive(),
|
ExternalKeyingActive(),
|
||||||
StatusMessage());
|
StatusMessage());
|
||||||
PublishBackendStateChanged("status", StatusMessage());
|
PublishBackendStateChanged(VideoBackendLifecycle::StateName(mLifecycle.State()), StatusMessage());
|
||||||
}
|
}
|
||||||
|
|
||||||
void VideoBackend::ReportNoInputDeviceSignalStatus()
|
void VideoBackend::ReportNoInputDeviceSignalStatus()
|
||||||
@@ -213,6 +321,9 @@ void VideoBackend::ReportNoInputDeviceSignalStatus()
|
|||||||
|
|
||||||
void VideoBackend::HandleInputFrame(const VideoIOFrame& frame)
|
void VideoBackend::HandleInputFrame(const VideoIOFrame& frame)
|
||||||
{
|
{
|
||||||
|
if (mInputCaptureDisabled)
|
||||||
|
return;
|
||||||
|
|
||||||
const VideoIOState& state = mVideoIODevice->State();
|
const VideoIOState& state = mVideoIODevice->State();
|
||||||
mHealthTelemetry.TryReportSignalStatus(!frame.hasNoInputSource, state.inputFrameSize.width, state.inputFrameSize.height, state.inputDisplayModeName);
|
mHealthTelemetry.TryReportSignalStatus(!frame.hasNoInputSource, state.inputFrameSize.width, state.inputFrameSize.height, state.inputDisplayModeName);
|
||||||
PublishInputSignalChanged(frame, state);
|
PublishInputSignalChanged(frame, state);
|
||||||
@@ -224,30 +335,477 @@ void VideoBackend::HandleInputFrame(const VideoIOFrame& frame)
|
|||||||
|
|
||||||
void VideoBackend::HandleOutputFrameCompletion(const VideoIOCompletion& completion)
|
void VideoBackend::HandleOutputFrameCompletion(const VideoIOCompletion& completion)
|
||||||
{
|
{
|
||||||
RecordFramePacing(completion.result);
|
|
||||||
PublishOutputFrameCompleted(completion);
|
|
||||||
|
|
||||||
VideoIOOutputFrame outputFrame;
|
|
||||||
if (!BeginOutputFrame(outputFrame))
|
|
||||||
return;
|
|
||||||
|
|
||||||
const VideoIOState& state = mVideoIODevice->State();
|
|
||||||
bool rendered = true;
|
|
||||||
if (mBridge)
|
|
||||||
rendered = mBridge->RenderScheduledFrame(state, completion, outputFrame);
|
|
||||||
|
|
||||||
EndOutputFrame(outputFrame);
|
|
||||||
AccountForCompletionResult(completion.result);
|
|
||||||
if (!rendered)
|
|
||||||
{
|
{
|
||||||
PublishBackendStateChanged("output-render-failed", "Output frame render request failed; skipping schedule for this frame.");
|
std::lock_guard<std::mutex> lock(mOutputCompletionMutex);
|
||||||
|
if (!mOutputCompletionWorkerRunning || mOutputCompletionWorkerStopping)
|
||||||
return;
|
return;
|
||||||
|
mPendingOutputCompletions.push_back(completion);
|
||||||
|
}
|
||||||
|
mOutputCompletionCondition.notify_one();
|
||||||
|
}
|
||||||
|
|
||||||
|
void VideoBackend::StartOutputCompletionWorker()
|
||||||
|
{
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(mOutputCompletionMutex);
|
||||||
|
if (mOutputCompletionWorkerRunning)
|
||||||
|
return;
|
||||||
|
|
||||||
|
mPendingOutputCompletions.clear();
|
||||||
|
mReadyOutputQueue.Clear();
|
||||||
|
mNextReadyOutputFrameIndex = 0;
|
||||||
|
mHasReadyQueueDepthBaseline = false;
|
||||||
|
mMinReadyQueueDepth = 0;
|
||||||
|
mMaxReadyQueueDepth = 0;
|
||||||
|
mReadyQueueZeroDepthCount = 0;
|
||||||
|
mOutputRenderMilliseconds = 0.0;
|
||||||
|
mSmoothedOutputRenderMilliseconds = 0.0;
|
||||||
|
mMaxOutputRenderMilliseconds = 0.0;
|
||||||
|
mOutputFrameAcquireMilliseconds = 0.0;
|
||||||
|
mOutputFrameRenderRequestMilliseconds = 0.0;
|
||||||
|
mOutputFrameEndAccessMilliseconds = 0.0;
|
||||||
|
mLastLateStreak = 0;
|
||||||
|
mLastDropStreak = 0;
|
||||||
|
mOutputCompletionWorkerStopping = false;
|
||||||
|
mOutputCompletionWorkerRunning = true;
|
||||||
|
mOutputCompletionWorker = std::thread(&VideoBackend::OutputCompletionWorkerMain, this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void VideoBackend::StopOutputCompletionWorker()
|
||||||
|
{
|
||||||
|
StopOutputProducerWorker();
|
||||||
|
|
||||||
|
bool shouldJoin = false;
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(mOutputCompletionMutex);
|
||||||
|
if (mOutputCompletionWorkerRunning)
|
||||||
|
mOutputCompletionWorkerStopping = true;
|
||||||
|
shouldJoin = mOutputCompletionWorker.joinable();
|
||||||
|
}
|
||||||
|
mOutputCompletionCondition.notify_one();
|
||||||
|
|
||||||
|
if (shouldJoin)
|
||||||
|
mOutputCompletionWorker.join();
|
||||||
|
}
|
||||||
|
|
||||||
|
void VideoBackend::StartOutputProducerWorker()
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(mOutputProducerMutex);
|
||||||
|
if (mOutputProducerWorkerRunning)
|
||||||
|
return;
|
||||||
|
|
||||||
|
const double frameBudgetMilliseconds = State().frameBudgetMilliseconds;
|
||||||
|
const auto frameDuration = frameBudgetMilliseconds > 0.0
|
||||||
|
? std::chrono::duration_cast<RenderCadenceController::Duration>(
|
||||||
|
std::chrono::duration<double, std::milli>(frameBudgetMilliseconds))
|
||||||
|
: std::chrono::milliseconds(16);
|
||||||
|
mRenderCadenceController.Configure(frameDuration, std::chrono::steady_clock::now());
|
||||||
|
mLastOutputProductionCompletion = VideoIOCompletion();
|
||||||
|
mLastOutputProductionTime = std::chrono::steady_clock::time_point();
|
||||||
|
mOutputProducerWorkerStopping = false;
|
||||||
|
mOutputProducerWorkerRunning = true;
|
||||||
|
mOutputProducerWorker = std::thread(&VideoBackend::OutputProducerWorkerMain, this);
|
||||||
|
mOutputProducerCondition.notify_one();
|
||||||
|
}
|
||||||
|
|
||||||
|
void VideoBackend::StopOutputProducerWorker()
|
||||||
|
{
|
||||||
|
bool shouldJoin = false;
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(mOutputProducerMutex);
|
||||||
|
if (mOutputProducerWorkerRunning)
|
||||||
|
mOutputProducerWorkerStopping = true;
|
||||||
|
shouldJoin = mOutputProducerWorker.joinable();
|
||||||
|
}
|
||||||
|
mOutputProducerCondition.notify_one();
|
||||||
|
|
||||||
|
if (shouldJoin)
|
||||||
|
mOutputProducerWorker.join();
|
||||||
|
}
|
||||||
|
|
||||||
|
void VideoBackend::NotifyOutputProducer()
|
||||||
|
{
|
||||||
|
mOutputProducerCondition.notify_one();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool VideoBackend::WarmupOutputPreroll()
|
||||||
|
{
|
||||||
|
const VideoPlayoutPolicy policy = NormalizeVideoPlayoutPolicy(mPlayoutPolicy);
|
||||||
|
const std::size_t targetPrerollFrames = static_cast<std::size_t>(policy.targetPrerollFrames);
|
||||||
|
if (targetPrerollFrames == 0)
|
||||||
|
return true;
|
||||||
|
|
||||||
|
const double frameBudgetMilliseconds = State().frameBudgetMilliseconds > 0.0 ? State().frameBudgetMilliseconds : 16.0;
|
||||||
|
const auto estimatedCadenceTime = std::chrono::duration_cast<std::chrono::milliseconds>(
|
||||||
|
std::chrono::duration<double, std::milli>(frameBudgetMilliseconds * static_cast<double>(targetPrerollFrames + 2)));
|
||||||
|
const auto timeout = (std::max)(std::chrono::milliseconds(1000), estimatedCadenceTime + std::chrono::milliseconds(500));
|
||||||
|
const auto deadline = std::chrono::steady_clock::now() + timeout;
|
||||||
|
|
||||||
|
while (std::chrono::steady_clock::now() < deadline)
|
||||||
|
{
|
||||||
|
ScheduleReadyOutputFramesToTarget();
|
||||||
|
const SystemOutputFramePoolMetrics metrics = mSystemOutputFramePool.GetMetrics();
|
||||||
|
RecordSystemMemoryPlayoutStats();
|
||||||
|
if (metrics.scheduledCount >= targetPrerollFrames)
|
||||||
|
return true;
|
||||||
|
|
||||||
|
NotifyOutputProducer();
|
||||||
|
const auto waitDuration = (std::min)(OutputProducerWakeInterval(), std::chrono::milliseconds(5));
|
||||||
|
std::unique_lock<std::mutex> lock(mOutputProducerMutex);
|
||||||
|
mOutputProducerCondition.wait_for(lock, waitDuration);
|
||||||
|
if (mOutputProducerWorkerStopping)
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Schedule the next frame after render work is complete so device-side
|
SetStatusMessage("Timed out warming up DeckLink preroll from rendered system-memory frames.");
|
||||||
// bookkeeping stays with the backend seam and the bridge stays render-only.
|
return false;
|
||||||
if (ScheduleOutputFrame(outputFrame))
|
}
|
||||||
|
|
||||||
|
void VideoBackend::OutputCompletionWorkerMain()
|
||||||
|
{
|
||||||
|
for (;;)
|
||||||
|
{
|
||||||
|
VideoIOCompletion completion;
|
||||||
|
{
|
||||||
|
std::unique_lock<std::mutex> lock(mOutputCompletionMutex);
|
||||||
|
mOutputCompletionCondition.wait(lock, [this]() {
|
||||||
|
return mOutputCompletionWorkerStopping || !mPendingOutputCompletions.empty();
|
||||||
|
});
|
||||||
|
|
||||||
|
if (mPendingOutputCompletions.empty())
|
||||||
|
{
|
||||||
|
if (mOutputCompletionWorkerStopping)
|
||||||
|
{
|
||||||
|
mOutputCompletionWorkerRunning = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
completion = mPendingOutputCompletions.front();
|
||||||
|
mPendingOutputCompletions.pop_front();
|
||||||
|
}
|
||||||
|
|
||||||
|
ProcessOutputFrameCompletion(completion);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void VideoBackend::OutputProducerWorkerMain()
|
||||||
|
{
|
||||||
|
for (;;)
|
||||||
|
{
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(mOutputProducerMutex);
|
||||||
|
if (mOutputProducerWorkerStopping)
|
||||||
|
{
|
||||||
|
mOutputProducerWorkerRunning = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ScheduleReadyOutputFramesToTarget();
|
||||||
|
|
||||||
|
const RenderOutputQueueMetrics metrics = mReadyOutputQueue.GetMetrics();
|
||||||
|
RecordReadyQueueDepthSample(metrics);
|
||||||
|
|
||||||
|
const auto now = std::chrono::steady_clock::now();
|
||||||
|
RenderCadenceDecision cadenceDecision = mRenderCadenceController.Tick(now);
|
||||||
|
if (cadenceDecision.action == RenderCadenceAction::Wait)
|
||||||
|
{
|
||||||
|
const auto waitDuration = (std::min)(
|
||||||
|
std::chrono::duration_cast<std::chrono::milliseconds>(cadenceDecision.waitDuration),
|
||||||
|
OutputProducerWakeInterval());
|
||||||
|
std::unique_lock<std::mutex> lock(mOutputProducerMutex);
|
||||||
|
mOutputProducerCondition.wait_for(lock, waitDuration);
|
||||||
|
if (mOutputProducerWorkerStopping)
|
||||||
|
{
|
||||||
|
mOutputProducerWorkerRunning = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
VideoIOCompletion completion;
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(mOutputProducerMutex);
|
||||||
|
if (mOutputProducerWorkerStopping)
|
||||||
|
continue;
|
||||||
|
completion = mLastOutputProductionCompletion;
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::size_t producedFrames = ProduceReadyOutputFrames(completion, 1);
|
||||||
|
if (producedFrames > 0)
|
||||||
|
{
|
||||||
|
mLastOutputProductionTime = std::chrono::steady_clock::now();
|
||||||
|
ScheduleReadyOutputFramesToTarget();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
std::unique_lock<std::mutex> lock(mOutputProducerMutex);
|
||||||
|
mOutputProducerCondition.wait_for(lock, OutputProducerWakeInterval());
|
||||||
|
if (mOutputProducerWorkerStopping)
|
||||||
|
{
|
||||||
|
mOutputProducerWorkerRunning = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
std::chrono::milliseconds VideoBackend::OutputProducerWakeInterval() const
|
||||||
|
{
|
||||||
|
const double frameBudgetMilliseconds = State().frameBudgetMilliseconds;
|
||||||
|
if (frameBudgetMilliseconds <= 0.0)
|
||||||
|
return std::chrono::milliseconds(8);
|
||||||
|
|
||||||
|
const int intervalMilliseconds = (std::max)(1, static_cast<int>(std::floor(frameBudgetMilliseconds * 0.75)));
|
||||||
|
return std::chrono::milliseconds(intervalMilliseconds);
|
||||||
|
}
|
||||||
|
|
||||||
|
void VideoBackend::ProcessOutputFrameCompletion(const VideoIOCompletion& completion)
|
||||||
|
{
|
||||||
|
if (completion.outputFrameBuffer != nullptr)
|
||||||
|
mSystemOutputFramePool.ReleaseSlotByBuffer(completion.outputFrameBuffer);
|
||||||
|
RecordFramePacing(completion.result);
|
||||||
|
PublishOutputFrameCompleted(completion);
|
||||||
|
const RenderOutputQueueMetrics initialQueueMetrics = mReadyOutputQueue.GetMetrics();
|
||||||
|
RecordReadyQueueDepthSample(initialQueueMetrics);
|
||||||
|
const VideoPlayoutRecoveryDecision recoveryDecision = AccountForCompletionResult(completion.result, initialQueueMetrics.depth);
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(mOutputMetricsMutex);
|
||||||
|
mLastLateStreak = recoveryDecision.lateStreak;
|
||||||
|
mLastDropStreak = recoveryDecision.dropStreak;
|
||||||
|
}
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(mOutputProducerMutex);
|
||||||
|
mLastOutputProductionCompletion = completion;
|
||||||
|
}
|
||||||
|
NotifyOutputProducer();
|
||||||
|
|
||||||
|
RecordBackendPlayoutHealth(completion.result, recoveryDecision);
|
||||||
|
RecordSystemMemoryPlayoutStats();
|
||||||
|
}
|
||||||
|
|
||||||
|
std::size_t VideoBackend::ScheduleReadyOutputFramesToTarget()
|
||||||
|
{
|
||||||
|
const std::size_t targetScheduledFrames = static_cast<std::size_t>(mPlayoutPolicy.targetPrerollFrames);
|
||||||
|
std::size_t scheduledFrames = 0;
|
||||||
|
for (;;)
|
||||||
|
{
|
||||||
|
const SystemOutputFramePoolMetrics poolMetrics = mSystemOutputFramePool.GetMetrics();
|
||||||
|
if (poolMetrics.scheduledCount >= targetScheduledFrames)
|
||||||
|
break;
|
||||||
|
if (!ScheduleReadyOutputFrame())
|
||||||
|
break;
|
||||||
|
++scheduledFrames;
|
||||||
|
}
|
||||||
|
return scheduledFrames;
|
||||||
|
}
|
||||||
|
|
||||||
|
void VideoBackend::RecordBackendPlayoutHealth(VideoIOCompletionResult result, const VideoPlayoutRecoveryDecision& recoveryDecision)
|
||||||
|
{
|
||||||
|
const RenderOutputQueueMetrics queueMetrics = mReadyOutputQueue.GetMetrics();
|
||||||
|
std::size_t minReadyQueueDepth = 0;
|
||||||
|
std::size_t maxReadyQueueDepth = 0;
|
||||||
|
uint64_t readyQueueZeroDepthCount = 0;
|
||||||
|
double outputRenderMilliseconds = 0.0;
|
||||||
|
double smoothedOutputRenderMilliseconds = 0.0;
|
||||||
|
double maxOutputRenderMilliseconds = 0.0;
|
||||||
|
double outputFrameAcquireMilliseconds = 0.0;
|
||||||
|
double outputFrameRenderRequestMilliseconds = 0.0;
|
||||||
|
double outputFrameEndAccessMilliseconds = 0.0;
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(mOutputMetricsMutex);
|
||||||
|
minReadyQueueDepth = mMinReadyQueueDepth;
|
||||||
|
maxReadyQueueDepth = mMaxReadyQueueDepth;
|
||||||
|
readyQueueZeroDepthCount = mReadyQueueZeroDepthCount;
|
||||||
|
outputRenderMilliseconds = mOutputRenderMilliseconds;
|
||||||
|
smoothedOutputRenderMilliseconds = mSmoothedOutputRenderMilliseconds;
|
||||||
|
maxOutputRenderMilliseconds = mMaxOutputRenderMilliseconds;
|
||||||
|
outputFrameAcquireMilliseconds = mOutputFrameAcquireMilliseconds;
|
||||||
|
outputFrameRenderRequestMilliseconds = mOutputFrameRenderRequestMilliseconds;
|
||||||
|
outputFrameEndAccessMilliseconds = mOutputFrameEndAccessMilliseconds;
|
||||||
|
}
|
||||||
|
|
||||||
|
mHealthTelemetry.TryRecordBackendPlayoutHealth(
|
||||||
|
VideoBackendLifecycle::StateName(mLifecycle.State()),
|
||||||
|
CompletionResultName(result),
|
||||||
|
queueMetrics.depth,
|
||||||
|
queueMetrics.capacity,
|
||||||
|
queueMetrics.pushedCount,
|
||||||
|
minReadyQueueDepth,
|
||||||
|
maxReadyQueueDepth,
|
||||||
|
readyQueueZeroDepthCount,
|
||||||
|
queueMetrics.poppedCount,
|
||||||
|
queueMetrics.droppedCount,
|
||||||
|
queueMetrics.underrunCount,
|
||||||
|
outputRenderMilliseconds,
|
||||||
|
smoothedOutputRenderMilliseconds,
|
||||||
|
maxOutputRenderMilliseconds,
|
||||||
|
outputFrameAcquireMilliseconds,
|
||||||
|
outputFrameRenderRequestMilliseconds,
|
||||||
|
outputFrameEndAccessMilliseconds,
|
||||||
|
recoveryDecision.completedFrameIndex,
|
||||||
|
recoveryDecision.scheduledFrameIndex,
|
||||||
|
recoveryDecision.scheduledLeadFrames,
|
||||||
|
recoveryDecision.measuredLagFrames,
|
||||||
|
recoveryDecision.catchUpFrames,
|
||||||
|
recoveryDecision.lateStreak,
|
||||||
|
recoveryDecision.dropStreak,
|
||||||
|
mLateFrameCount,
|
||||||
|
mDroppedFrameCount,
|
||||||
|
mFlushedFrameCount,
|
||||||
|
mLifecycle.State() == VideoBackendLifecycleState::Degraded,
|
||||||
|
StatusMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
std::size_t VideoBackend::ProduceReadyOutputFrames(const VideoIOCompletion& completion, std::size_t maxFrames)
|
||||||
|
{
|
||||||
|
if (maxFrames == 0)
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
std::lock_guard<std::mutex> productionLock(mOutputProductionMutex);
|
||||||
|
RenderOutputQueueMetrics metrics = mReadyOutputQueue.GetMetrics();
|
||||||
|
std::size_t producedFrames = 0;
|
||||||
|
while (producedFrames < maxFrames)
|
||||||
|
{
|
||||||
|
if (!RenderReadyOutputFrame(mVideoIODevice->State(), completion))
|
||||||
|
break;
|
||||||
|
++producedFrames;
|
||||||
|
metrics = mReadyOutputQueue.GetMetrics();
|
||||||
|
RecordReadyQueueDepthSample(metrics);
|
||||||
|
}
|
||||||
|
return producedFrames;
|
||||||
|
}
|
||||||
|
|
||||||
|
OutputProductionPressure VideoBackend::BuildOutputProductionPressure(const RenderOutputQueueMetrics& metrics) const
|
||||||
|
{
|
||||||
|
OutputProductionPressure pressure;
|
||||||
|
pressure.readyQueueDepth = metrics.depth;
|
||||||
|
pressure.readyQueueCapacity = metrics.capacity;
|
||||||
|
pressure.readyQueueUnderrunCount = metrics.underrunCount;
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(mOutputMetricsMutex);
|
||||||
|
pressure.lateStreak = mLastLateStreak;
|
||||||
|
pressure.dropStreak = mLastDropStreak;
|
||||||
|
}
|
||||||
|
return pressure;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool VideoBackend::RenderReadyOutputFrame(const VideoIOState& state, const VideoIOCompletion& completion)
|
||||||
|
{
|
||||||
|
const auto renderStart = std::chrono::steady_clock::now();
|
||||||
|
OutputFrameSlot outputSlot;
|
||||||
|
VideoIOOutputFrame outputFrame;
|
||||||
|
const auto acquireStart = std::chrono::steady_clock::now();
|
||||||
|
if (!mSystemOutputFramePool.AcquireFreeSlot(outputSlot))
|
||||||
|
{
|
||||||
|
if (!mReadyOutputQueue.DropOldestFrame() || !mSystemOutputFramePool.AcquireFreeSlot(outputSlot))
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
outputFrame = outputSlot.frame;
|
||||||
|
const auto acquireEnd = std::chrono::steady_clock::now();
|
||||||
|
|
||||||
|
bool rendered = true;
|
||||||
|
const auto renderRequestStart = std::chrono::steady_clock::now();
|
||||||
|
if (mBridge)
|
||||||
|
rendered = mBridge->RenderScheduledFrame(state, completion, outputFrame);
|
||||||
|
const auto renderRequestEnd = std::chrono::steady_clock::now();
|
||||||
|
|
||||||
|
const auto endAccessStart = std::chrono::steady_clock::now();
|
||||||
|
const bool publishedReady = mSystemOutputFramePool.PublishReadySlot(outputSlot);
|
||||||
|
const auto endAccessEnd = std::chrono::steady_clock::now();
|
||||||
|
const double acquireMilliseconds = std::chrono::duration_cast<std::chrono::duration<double, std::milli>>(acquireEnd - acquireStart).count();
|
||||||
|
const double renderRequestMilliseconds = std::chrono::duration_cast<std::chrono::duration<double, std::milli>>(renderRequestEnd - renderRequestStart).count();
|
||||||
|
const double endAccessMilliseconds = std::chrono::duration_cast<std::chrono::duration<double, std::milli>>(endAccessEnd - endAccessStart).count();
|
||||||
|
|
||||||
|
if (!rendered)
|
||||||
|
{
|
||||||
|
mSystemOutputFramePool.ReleaseSlot(outputSlot);
|
||||||
|
ApplyLifecycleTransition(VideoBackendLifecycleState::Degraded, "Output frame render request failed; skipping schedule for this frame.");
|
||||||
|
const double renderMilliseconds = std::chrono::duration_cast<std::chrono::duration<double, std::milli>>(
|
||||||
|
std::chrono::steady_clock::now() - renderStart).count();
|
||||||
|
RecordOutputRenderDuration(renderMilliseconds, acquireMilliseconds, renderRequestMilliseconds, endAccessMilliseconds);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!publishedReady)
|
||||||
|
{
|
||||||
|
mSystemOutputFramePool.ReleaseSlot(outputSlot);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const double renderMilliseconds = std::chrono::duration_cast<std::chrono::duration<double, std::milli>>(
|
||||||
|
std::chrono::steady_clock::now() - renderStart).count();
|
||||||
|
RecordOutputRenderDuration(renderMilliseconds, acquireMilliseconds, renderRequestMilliseconds, endAccessMilliseconds);
|
||||||
|
|
||||||
|
RenderOutputFrame readyFrame;
|
||||||
|
readyFrame.frame = outputFrame;
|
||||||
|
readyFrame.frameIndex = ++mNextReadyOutputFrameIndex;
|
||||||
|
readyFrame.releaseFrame = [this](VideoIOOutputFrame& frame) {
|
||||||
|
mSystemOutputFramePool.ReleaseSlotByBuffer(frame.bytes);
|
||||||
|
};
|
||||||
|
const bool pushed = mReadyOutputQueue.Push(readyFrame);
|
||||||
|
if (!pushed)
|
||||||
|
mSystemOutputFramePool.ReleaseSlot(outputSlot);
|
||||||
|
RecordSystemMemoryPlayoutStats();
|
||||||
|
return pushed;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool VideoBackend::ScheduleReadyOutputFrame()
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> schedulingLock(mOutputSchedulingMutex);
|
||||||
|
RenderOutputFrame readyFrame;
|
||||||
|
if (!mReadyOutputQueue.TryPop(readyFrame))
|
||||||
|
return false;
|
||||||
|
RecordReadyQueueDepthSample(mReadyOutputQueue.GetMetrics());
|
||||||
|
|
||||||
|
if (!mSystemOutputFramePool.MarkScheduledByBuffer(readyFrame.frame.bytes))
|
||||||
|
{
|
||||||
|
if (readyFrame.releaseFrame)
|
||||||
|
readyFrame.releaseFrame(readyFrame.frame);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!ScheduleOutputFrame(readyFrame.frame))
|
||||||
|
{
|
||||||
|
RecordDeckLinkBufferTelemetry();
|
||||||
|
mSystemOutputFramePool.ReleaseSlotByBuffer(readyFrame.frame.bytes);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
RecordDeckLinkBufferTelemetry();
|
||||||
|
PublishOutputFrameScheduled(readyFrame.frame);
|
||||||
|
RecordSystemMemoryPlayoutStats();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool VideoBackend::ScheduleBlackUnderrunFrame()
|
||||||
|
{
|
||||||
|
VideoIOOutputFrame outputFrame;
|
||||||
|
if (!BeginOutputFrame(outputFrame))
|
||||||
|
{
|
||||||
|
ApplyLifecycleTransition(VideoBackendLifecycleState::Degraded, "Output underrun: no output frame was available for fallback scheduling.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (outputFrame.bytes != nullptr && outputFrame.rowBytes > 0 && outputFrame.height > 0)
|
||||||
|
std::memset(outputFrame.bytes, 0, static_cast<std::size_t>(outputFrame.rowBytes) * outputFrame.height);
|
||||||
|
EndOutputFrame(outputFrame);
|
||||||
|
|
||||||
|
if (!ScheduleOutputFrame(outputFrame))
|
||||||
|
{
|
||||||
|
RecordDeckLinkBufferTelemetry();
|
||||||
|
ApplyLifecycleTransition(VideoBackendLifecycleState::Degraded, "Output underrun: black fallback frame scheduling failed.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
RecordDeckLinkBufferTelemetry();
|
||||||
|
ApplyLifecycleTransition(VideoBackendLifecycleState::Degraded, "Output underrun: scheduled black fallback frame.");
|
||||||
PublishOutputFrameScheduled(outputFrame);
|
PublishOutputFrameScheduled(outputFrame);
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
void VideoBackend::RecordFramePacing(VideoIOCompletionResult completionResult)
|
void VideoBackend::RecordFramePacing(VideoIOCompletionResult completionResult)
|
||||||
@@ -283,6 +841,98 @@ void VideoBackend::RecordFramePacing(VideoIOCompletionResult completionResult)
|
|||||||
PublishTimingSample("VideoBackend", "smoothedCompletionInterval", mSmoothedCompletionIntervalMilliseconds, "ms");
|
PublishTimingSample("VideoBackend", "smoothedCompletionInterval", mSmoothedCompletionIntervalMilliseconds, "ms");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void VideoBackend::RecordReadyQueueDepthSample(const RenderOutputQueueMetrics& metrics)
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(mOutputMetricsMutex);
|
||||||
|
if (!mHasReadyQueueDepthBaseline)
|
||||||
|
{
|
||||||
|
mHasReadyQueueDepthBaseline = true;
|
||||||
|
mMinReadyQueueDepth = metrics.depth;
|
||||||
|
mMaxReadyQueueDepth = metrics.depth;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
mMinReadyQueueDepth = (std::min)(mMinReadyQueueDepth, metrics.depth);
|
||||||
|
mMaxReadyQueueDepth = (std::max)(mMaxReadyQueueDepth, metrics.depth);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (metrics.depth == 0)
|
||||||
|
++mReadyQueueZeroDepthCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
void VideoBackend::RecordDeckLinkBufferTelemetry()
|
||||||
|
{
|
||||||
|
if (!mVideoIODevice)
|
||||||
|
return;
|
||||||
|
|
||||||
|
const VideoIOState& state = mVideoIODevice->State();
|
||||||
|
mHealthTelemetry.TryRecordDeckLinkBufferTelemetry(
|
||||||
|
state.actualDeckLinkBufferedFramesAvailable,
|
||||||
|
state.actualDeckLinkBufferedFrames,
|
||||||
|
static_cast<std::size_t>(mPlayoutPolicy.targetPrerollFrames),
|
||||||
|
state.deckLinkScheduleCallMilliseconds,
|
||||||
|
state.deckLinkScheduleFailureCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
void VideoBackend::RecordSystemMemoryPlayoutStats()
|
||||||
|
{
|
||||||
|
const SystemOutputFramePoolMetrics poolMetrics = mSystemOutputFramePool.GetMetrics();
|
||||||
|
const RenderOutputQueueMetrics queueMetrics = mReadyOutputQueue.GetMetrics();
|
||||||
|
RecordDeckLinkBufferTelemetry();
|
||||||
|
mHealthTelemetry.TryRecordSystemMemoryPlayoutStats(
|
||||||
|
poolMetrics.freeCount,
|
||||||
|
poolMetrics.readyCount,
|
||||||
|
poolMetrics.scheduledCount,
|
||||||
|
poolMetrics.readyUnderrunCount,
|
||||||
|
0,
|
||||||
|
queueMetrics.droppedCount,
|
||||||
|
0.0,
|
||||||
|
0.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
void VideoBackend::RecordOutputRenderDuration(double renderMilliseconds, double acquireMilliseconds, double renderRequestMilliseconds, double endAccessMilliseconds)
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(mOutputMetricsMutex);
|
||||||
|
mOutputRenderMilliseconds = (std::max)(renderMilliseconds, 0.0);
|
||||||
|
if (mSmoothedOutputRenderMilliseconds <= 0.0)
|
||||||
|
mSmoothedOutputRenderMilliseconds = mOutputRenderMilliseconds;
|
||||||
|
else
|
||||||
|
mSmoothedOutputRenderMilliseconds = mSmoothedOutputRenderMilliseconds * 0.9 + mOutputRenderMilliseconds * 0.1;
|
||||||
|
mMaxOutputRenderMilliseconds = (std::max)(mMaxOutputRenderMilliseconds, mOutputRenderMilliseconds);
|
||||||
|
mOutputFrameAcquireMilliseconds = (std::max)(acquireMilliseconds, 0.0);
|
||||||
|
mOutputFrameRenderRequestMilliseconds = (std::max)(renderRequestMilliseconds, 0.0);
|
||||||
|
mOutputFrameEndAccessMilliseconds = (std::max)(endAccessMilliseconds, 0.0);
|
||||||
|
|
||||||
|
PublishTimingSample("VideoBackend", "outputRender", mOutputRenderMilliseconds, "ms");
|
||||||
|
PublishTimingSample("VideoBackend", "smoothedOutputRender", mSmoothedOutputRenderMilliseconds, "ms");
|
||||||
|
}
|
||||||
|
|
||||||
|
bool VideoBackend::ApplyLifecycleTransition(VideoBackendLifecycleState state, const std::string& message)
|
||||||
|
{
|
||||||
|
const VideoBackendLifecycleTransition transition = mLifecycle.TransitionTo(state, message);
|
||||||
|
if (!transition.accepted)
|
||||||
|
{
|
||||||
|
PublishBackendStateChanged(VideoBackendLifecycle::StateName(transition.current), transition.errorMessage);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
PublishBackendStateChanged(VideoBackendLifecycle::StateName(transition.current), message);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool VideoBackend::ApplyLifecycleFailure(const std::string& message)
|
||||||
|
{
|
||||||
|
const VideoBackendLifecycleTransition transition = mLifecycle.Fail(message);
|
||||||
|
if (!transition.accepted)
|
||||||
|
{
|
||||||
|
PublishBackendStateChanged(VideoBackendLifecycle::StateName(transition.current), transition.errorMessage);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
PublishBackendStateChanged(VideoBackendLifecycle::StateName(transition.current), message);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
void VideoBackend::PublishBackendStateChanged(const std::string& state, const std::string& message)
|
void VideoBackend::PublishBackendStateChanged(const std::string& state, const std::string& message)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
@@ -428,3 +1078,18 @@ std::string VideoBackend::PixelFormatName(VideoIOPixelFormat pixelFormat)
|
|||||||
{
|
{
|
||||||
return std::string(VideoIOPixelFormatName(pixelFormat));
|
return std::string(VideoIOPixelFormatName(pixelFormat));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool VideoBackend::IsEnvironmentFlagEnabled(const char* name)
|
||||||
|
{
|
||||||
|
if (name == nullptr || name[0] == '\0')
|
||||||
|
return false;
|
||||||
|
|
||||||
|
char* value = nullptr;
|
||||||
|
std::size_t valueSize = 0;
|
||||||
|
if (_dupenv_s(&value, &valueSize, name) != 0 || value == nullptr)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
const std::string flag(value);
|
||||||
|
std::free(value);
|
||||||
|
return flag == "1" || flag == "true" || flag == "TRUE" || flag == "yes" || flag == "on";
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,11 +1,21 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
#include "OutputProductionController.h"
|
||||||
|
#include "RenderCadenceController.h"
|
||||||
|
#include "RenderOutputQueue.h"
|
||||||
|
#include "SystemOutputFramePool.h"
|
||||||
|
#include "VideoBackendLifecycle.h"
|
||||||
#include "VideoIOTypes.h"
|
#include "VideoIOTypes.h"
|
||||||
|
#include "VideoPlayoutPolicy.h"
|
||||||
|
|
||||||
#include <chrono>
|
#include <chrono>
|
||||||
|
#include <condition_variable>
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
|
#include <deque>
|
||||||
#include <memory>
|
#include <memory>
|
||||||
|
#include <mutex>
|
||||||
#include <string>
|
#include <string>
|
||||||
|
#include <thread>
|
||||||
|
|
||||||
class HealthTelemetry;
|
class HealthTelemetry;
|
||||||
class OpenGLVideoIOBridge;
|
class OpenGLVideoIOBridge;
|
||||||
@@ -20,6 +30,7 @@ public:
|
|||||||
~VideoBackend();
|
~VideoBackend();
|
||||||
|
|
||||||
void ReleaseResources();
|
void ReleaseResources();
|
||||||
|
VideoBackendLifecycleState LifecycleState() const;
|
||||||
bool DiscoverDevicesAndModes(const VideoFormatSelection& videoModes, std::string& error);
|
bool DiscoverDevicesAndModes(const VideoFormatSelection& videoModes, std::string& error);
|
||||||
bool SelectPreferredFormats(const VideoFormatSelection& videoModes, bool outputAlphaRequired, std::string& error);
|
bool SelectPreferredFormats(const VideoFormatSelection& videoModes, bool outputAlphaRequired, std::string& error);
|
||||||
bool ConfigureInput(const VideoFormat& inputVideoMode, std::string& error);
|
bool ConfigureInput(const VideoFormat& inputVideoMode, std::string& error);
|
||||||
@@ -32,7 +43,8 @@ public:
|
|||||||
bool BeginOutputFrame(VideoIOOutputFrame& frame);
|
bool BeginOutputFrame(VideoIOOutputFrame& frame);
|
||||||
void EndOutputFrame(VideoIOOutputFrame& frame);
|
void EndOutputFrame(VideoIOOutputFrame& frame);
|
||||||
bool ScheduleOutputFrame(const VideoIOOutputFrame& frame);
|
bool ScheduleOutputFrame(const VideoIOOutputFrame& frame);
|
||||||
void AccountForCompletionResult(VideoIOCompletionResult result);
|
VideoPlayoutRecoveryDecision AccountForCompletionResult(VideoIOCompletionResult result, uint64_t readyQueueDepth);
|
||||||
|
void RecordBackendPlayoutHealth(VideoIOCompletionResult result, const VideoPlayoutRecoveryDecision& recoveryDecision);
|
||||||
|
|
||||||
bool HasInputDevice() const;
|
bool HasInputDevice() const;
|
||||||
bool HasInputSource() const;
|
bool HasInputSource() const;
|
||||||
@@ -50,6 +62,7 @@ public:
|
|||||||
bool KeyerInterfaceAvailable() const;
|
bool KeyerInterfaceAvailable() const;
|
||||||
bool ExternalKeyingActive() const;
|
bool ExternalKeyingActive() const;
|
||||||
const std::string& StatusMessage() const;
|
const std::string& StatusMessage() const;
|
||||||
|
bool ShouldPrioritizeOutputOverPreview() const;
|
||||||
void SetStatusMessage(const std::string& message);
|
void SetStatusMessage(const std::string& message);
|
||||||
void PublishStatus(bool externalKeyingConfigured, const std::string& statusMessage = std::string());
|
void PublishStatus(bool externalKeyingConfigured, const std::string& statusMessage = std::string());
|
||||||
void ReportNoInputDeviceSignalStatus();
|
void ReportNoInputDeviceSignalStatus();
|
||||||
@@ -57,7 +70,29 @@ public:
|
|||||||
private:
|
private:
|
||||||
void HandleInputFrame(const VideoIOFrame& frame);
|
void HandleInputFrame(const VideoIOFrame& frame);
|
||||||
void HandleOutputFrameCompletion(const VideoIOCompletion& completion);
|
void HandleOutputFrameCompletion(const VideoIOCompletion& completion);
|
||||||
|
void StartOutputCompletionWorker();
|
||||||
|
void StopOutputCompletionWorker();
|
||||||
|
void OutputCompletionWorkerMain();
|
||||||
|
void StartOutputProducerWorker();
|
||||||
|
void StopOutputProducerWorker();
|
||||||
|
void OutputProducerWorkerMain();
|
||||||
|
void NotifyOutputProducer();
|
||||||
|
bool WarmupOutputPreroll();
|
||||||
|
std::chrono::milliseconds OutputProducerWakeInterval() const;
|
||||||
|
void ProcessOutputFrameCompletion(const VideoIOCompletion& completion);
|
||||||
|
std::size_t ProduceReadyOutputFrames(const VideoIOCompletion& completion, std::size_t maxFrames);
|
||||||
|
OutputProductionPressure BuildOutputProductionPressure(const RenderOutputQueueMetrics& metrics) const;
|
||||||
|
bool RenderReadyOutputFrame(const VideoIOState& state, const VideoIOCompletion& completion);
|
||||||
|
std::size_t ScheduleReadyOutputFramesToTarget();
|
||||||
|
bool ScheduleReadyOutputFrame();
|
||||||
|
bool ScheduleBlackUnderrunFrame();
|
||||||
void RecordFramePacing(VideoIOCompletionResult completionResult);
|
void RecordFramePacing(VideoIOCompletionResult completionResult);
|
||||||
|
void RecordReadyQueueDepthSample(const RenderOutputQueueMetrics& metrics);
|
||||||
|
void RecordDeckLinkBufferTelemetry();
|
||||||
|
void RecordSystemMemoryPlayoutStats();
|
||||||
|
void RecordOutputRenderDuration(double renderMilliseconds, double acquireMilliseconds, double renderRequestMilliseconds, double endAccessMilliseconds);
|
||||||
|
bool ApplyLifecycleTransition(VideoBackendLifecycleState state, const std::string& message);
|
||||||
|
bool ApplyLifecycleFailure(const std::string& message);
|
||||||
void PublishBackendStateChanged(const std::string& state, const std::string& message);
|
void PublishBackendStateChanged(const std::string& state, const std::string& message);
|
||||||
void PublishInputSignalChanged(const VideoIOFrame& frame, const VideoIOState& state);
|
void PublishInputSignalChanged(const VideoIOFrame& frame, const VideoIOState& state);
|
||||||
void PublishInputFrameArrived(const VideoIOFrame& frame);
|
void PublishInputFrameArrived(const VideoIOFrame& frame);
|
||||||
@@ -66,11 +101,36 @@ private:
|
|||||||
void PublishTimingSample(const std::string& subsystem, const std::string& metric, double value, const std::string& unit);
|
void PublishTimingSample(const std::string& subsystem, const std::string& metric, double value, const std::string& unit);
|
||||||
static std::string CompletionResultName(VideoIOCompletionResult result);
|
static std::string CompletionResultName(VideoIOCompletionResult result);
|
||||||
static std::string PixelFormatName(VideoIOPixelFormat pixelFormat);
|
static std::string PixelFormatName(VideoIOPixelFormat pixelFormat);
|
||||||
|
static bool IsEnvironmentFlagEnabled(const char* name);
|
||||||
|
|
||||||
HealthTelemetry& mHealthTelemetry;
|
HealthTelemetry& mHealthTelemetry;
|
||||||
RuntimeEventDispatcher& mRuntimeEventDispatcher;
|
RuntimeEventDispatcher& mRuntimeEventDispatcher;
|
||||||
|
VideoBackendLifecycle mLifecycle;
|
||||||
|
VideoPlayoutPolicy mPlayoutPolicy;
|
||||||
|
OutputProductionController mOutputProductionController;
|
||||||
|
RenderCadenceController mRenderCadenceController;
|
||||||
|
RenderOutputQueue mReadyOutputQueue;
|
||||||
|
SystemOutputFramePool mSystemOutputFramePool;
|
||||||
std::unique_ptr<VideoIODevice> mVideoIODevice;
|
std::unique_ptr<VideoIODevice> mVideoIODevice;
|
||||||
std::unique_ptr<OpenGLVideoIOBridge> mBridge;
|
std::unique_ptr<OpenGLVideoIOBridge> mBridge;
|
||||||
|
std::mutex mOutputCompletionMutex;
|
||||||
|
std::condition_variable mOutputCompletionCondition;
|
||||||
|
std::deque<VideoIOCompletion> mPendingOutputCompletions;
|
||||||
|
std::thread mOutputCompletionWorker;
|
||||||
|
std::mutex mOutputProducerMutex;
|
||||||
|
std::condition_variable mOutputProducerCondition;
|
||||||
|
std::thread mOutputProducerWorker;
|
||||||
|
VideoIOCompletion mLastOutputProductionCompletion;
|
||||||
|
std::chrono::steady_clock::time_point mLastOutputProductionTime;
|
||||||
|
std::mutex mOutputProductionMutex;
|
||||||
|
std::mutex mOutputSchedulingMutex;
|
||||||
|
mutable std::mutex mOutputMetricsMutex;
|
||||||
|
bool mOutputCompletionWorkerRunning = false;
|
||||||
|
bool mOutputCompletionWorkerStopping = false;
|
||||||
|
bool mOutputProducerWorkerRunning = false;
|
||||||
|
bool mOutputProducerWorkerStopping = false;
|
||||||
|
bool mInputCaptureDisabled = false;
|
||||||
|
uint64_t mNextReadyOutputFrameIndex = 0;
|
||||||
uint64_t mInputFrameIndex = 0;
|
uint64_t mInputFrameIndex = 0;
|
||||||
uint64_t mOutputFrameScheduleIndex = 0;
|
uint64_t mOutputFrameScheduleIndex = 0;
|
||||||
uint64_t mOutputFrameCompletionIndex = 0;
|
uint64_t mOutputFrameCompletionIndex = 0;
|
||||||
@@ -83,6 +143,18 @@ private:
|
|||||||
double mCompletionIntervalMilliseconds = 0.0;
|
double mCompletionIntervalMilliseconds = 0.0;
|
||||||
double mSmoothedCompletionIntervalMilliseconds = 0.0;
|
double mSmoothedCompletionIntervalMilliseconds = 0.0;
|
||||||
double mMaxCompletionIntervalMilliseconds = 0.0;
|
double mMaxCompletionIntervalMilliseconds = 0.0;
|
||||||
|
bool mHasReadyQueueDepthBaseline = false;
|
||||||
|
std::size_t mMinReadyQueueDepth = 0;
|
||||||
|
std::size_t mMaxReadyQueueDepth = 0;
|
||||||
|
uint64_t mReadyQueueZeroDepthCount = 0;
|
||||||
|
double mOutputRenderMilliseconds = 0.0;
|
||||||
|
double mSmoothedOutputRenderMilliseconds = 0.0;
|
||||||
|
double mMaxOutputRenderMilliseconds = 0.0;
|
||||||
|
double mOutputFrameAcquireMilliseconds = 0.0;
|
||||||
|
double mOutputFrameRenderRequestMilliseconds = 0.0;
|
||||||
|
double mOutputFrameEndAccessMilliseconds = 0.0;
|
||||||
|
uint64_t mLastLateStreak = 0;
|
||||||
|
uint64_t mLastDropStreak = 0;
|
||||||
uint64_t mLateFrameCount = 0;
|
uint64_t mLateFrameCount = 0;
|
||||||
uint64_t mDroppedFrameCount = 0;
|
uint64_t mDroppedFrameCount = 0;
|
||||||
uint64_t mFlushedFrameCount = 0;
|
uint64_t mFlushedFrameCount = 0;
|
||||||
|
|||||||
@@ -0,0 +1,123 @@
|
|||||||
|
#include "VideoBackendLifecycle.h"
|
||||||
|
|
||||||
|
VideoBackendLifecycleState VideoBackendLifecycle::State() const
|
||||||
|
{
|
||||||
|
return mState;
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::string& VideoBackendLifecycle::FailureReason() const
|
||||||
|
{
|
||||||
|
return mFailureReason;
|
||||||
|
}
|
||||||
|
|
||||||
|
VideoBackendLifecycleTransition VideoBackendLifecycle::TransitionTo(VideoBackendLifecycleState next, const std::string& reason)
|
||||||
|
{
|
||||||
|
VideoBackendLifecycleTransition transition;
|
||||||
|
transition.previous = mState;
|
||||||
|
transition.current = next;
|
||||||
|
transition.reason = reason;
|
||||||
|
transition.accepted = CanTransition(mState, next);
|
||||||
|
if (!transition.accepted)
|
||||||
|
{
|
||||||
|
transition.current = mState;
|
||||||
|
transition.errorMessage = std::string("Invalid video backend lifecycle transition from ") +
|
||||||
|
StateName(mState) + " to " + StateName(next) + ".";
|
||||||
|
return transition;
|
||||||
|
}
|
||||||
|
|
||||||
|
mState = next;
|
||||||
|
transition.current = mState;
|
||||||
|
if (mState != VideoBackendLifecycleState::Failed)
|
||||||
|
mFailureReason.clear();
|
||||||
|
return transition;
|
||||||
|
}
|
||||||
|
|
||||||
|
VideoBackendLifecycleTransition VideoBackendLifecycle::Fail(const std::string& reason)
|
||||||
|
{
|
||||||
|
VideoBackendLifecycleTransition transition = TransitionTo(VideoBackendLifecycleState::Failed, reason);
|
||||||
|
if (transition.accepted)
|
||||||
|
mFailureReason = reason;
|
||||||
|
return transition;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool VideoBackendLifecycle::CanTransition(VideoBackendLifecycleState current, VideoBackendLifecycleState next)
|
||||||
|
{
|
||||||
|
if (current == next)
|
||||||
|
return true;
|
||||||
|
|
||||||
|
switch (current)
|
||||||
|
{
|
||||||
|
case VideoBackendLifecycleState::Uninitialized:
|
||||||
|
return next == VideoBackendLifecycleState::Discovering ||
|
||||||
|
next == VideoBackendLifecycleState::Stopped ||
|
||||||
|
next == VideoBackendLifecycleState::Failed;
|
||||||
|
case VideoBackendLifecycleState::Discovering:
|
||||||
|
return next == VideoBackendLifecycleState::Discovered ||
|
||||||
|
next == VideoBackendLifecycleState::Failed;
|
||||||
|
case VideoBackendLifecycleState::Discovered:
|
||||||
|
return next == VideoBackendLifecycleState::Configuring ||
|
||||||
|
next == VideoBackendLifecycleState::Stopped ||
|
||||||
|
next == VideoBackendLifecycleState::Failed;
|
||||||
|
case VideoBackendLifecycleState::Configuring:
|
||||||
|
return next == VideoBackendLifecycleState::Configured ||
|
||||||
|
next == VideoBackendLifecycleState::Failed;
|
||||||
|
case VideoBackendLifecycleState::Configured:
|
||||||
|
return next == VideoBackendLifecycleState::Prerolling ||
|
||||||
|
next == VideoBackendLifecycleState::Stopped ||
|
||||||
|
next == VideoBackendLifecycleState::Failed;
|
||||||
|
case VideoBackendLifecycleState::Prerolling:
|
||||||
|
return next == VideoBackendLifecycleState::Running ||
|
||||||
|
next == VideoBackendLifecycleState::Stopping ||
|
||||||
|
next == VideoBackendLifecycleState::Failed;
|
||||||
|
case VideoBackendLifecycleState::Running:
|
||||||
|
return next == VideoBackendLifecycleState::Degraded ||
|
||||||
|
next == VideoBackendLifecycleState::Stopping ||
|
||||||
|
next == VideoBackendLifecycleState::Failed;
|
||||||
|
case VideoBackendLifecycleState::Degraded:
|
||||||
|
return next == VideoBackendLifecycleState::Running ||
|
||||||
|
next == VideoBackendLifecycleState::Stopping ||
|
||||||
|
next == VideoBackendLifecycleState::Failed;
|
||||||
|
case VideoBackendLifecycleState::Stopping:
|
||||||
|
return next == VideoBackendLifecycleState::Stopped ||
|
||||||
|
next == VideoBackendLifecycleState::Failed;
|
||||||
|
case VideoBackendLifecycleState::Stopped:
|
||||||
|
return next == VideoBackendLifecycleState::Discovering ||
|
||||||
|
next == VideoBackendLifecycleState::Failed;
|
||||||
|
case VideoBackendLifecycleState::Failed:
|
||||||
|
return next == VideoBackendLifecycleState::Stopped ||
|
||||||
|
next == VideoBackendLifecycleState::Discovering;
|
||||||
|
default:
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const char* VideoBackendLifecycle::StateName(VideoBackendLifecycleState state)
|
||||||
|
{
|
||||||
|
switch (state)
|
||||||
|
{
|
||||||
|
case VideoBackendLifecycleState::Uninitialized:
|
||||||
|
return "uninitialized";
|
||||||
|
case VideoBackendLifecycleState::Discovering:
|
||||||
|
return "discovering";
|
||||||
|
case VideoBackendLifecycleState::Discovered:
|
||||||
|
return "discovered";
|
||||||
|
case VideoBackendLifecycleState::Configuring:
|
||||||
|
return "configuring";
|
||||||
|
case VideoBackendLifecycleState::Configured:
|
||||||
|
return "configured";
|
||||||
|
case VideoBackendLifecycleState::Prerolling:
|
||||||
|
return "prerolling";
|
||||||
|
case VideoBackendLifecycleState::Running:
|
||||||
|
return "running";
|
||||||
|
case VideoBackendLifecycleState::Degraded:
|
||||||
|
return "degraded";
|
||||||
|
case VideoBackendLifecycleState::Stopping:
|
||||||
|
return "stopping";
|
||||||
|
case VideoBackendLifecycleState::Stopped:
|
||||||
|
return "stopped";
|
||||||
|
case VideoBackendLifecycleState::Failed:
|
||||||
|
return "failed";
|
||||||
|
default:
|
||||||
|
return "unknown";
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
enum class VideoBackendLifecycleState
|
||||||
|
{
|
||||||
|
Uninitialized,
|
||||||
|
Discovering,
|
||||||
|
Discovered,
|
||||||
|
Configuring,
|
||||||
|
Configured,
|
||||||
|
Prerolling,
|
||||||
|
Running,
|
||||||
|
Degraded,
|
||||||
|
Stopping,
|
||||||
|
Stopped,
|
||||||
|
Failed
|
||||||
|
};
|
||||||
|
|
||||||
|
struct VideoBackendLifecycleTransition
|
||||||
|
{
|
||||||
|
VideoBackendLifecycleState previous = VideoBackendLifecycleState::Uninitialized;
|
||||||
|
VideoBackendLifecycleState current = VideoBackendLifecycleState::Uninitialized;
|
||||||
|
bool accepted = false;
|
||||||
|
std::string reason;
|
||||||
|
std::string errorMessage;
|
||||||
|
};
|
||||||
|
|
||||||
|
class VideoBackendLifecycle
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
VideoBackendLifecycleState State() const;
|
||||||
|
const std::string& FailureReason() const;
|
||||||
|
VideoBackendLifecycleTransition TransitionTo(VideoBackendLifecycleState next, const std::string& reason);
|
||||||
|
VideoBackendLifecycleTransition Fail(const std::string& reason);
|
||||||
|
|
||||||
|
static bool CanTransition(VideoBackendLifecycleState current, VideoBackendLifecycleState next);
|
||||||
|
static const char* StateName(VideoBackendLifecycleState state);
|
||||||
|
|
||||||
|
private:
|
||||||
|
VideoBackendLifecycleState mState = VideoBackendLifecycleState::Uninitialized;
|
||||||
|
std::string mFailureReason;
|
||||||
|
};
|
||||||
@@ -50,6 +50,16 @@ struct VideoIOState
|
|||||||
bool keyerInterfaceAvailable = false;
|
bool keyerInterfaceAvailable = false;
|
||||||
bool externalKeyingActive = false;
|
bool externalKeyingActive = false;
|
||||||
double frameBudgetMilliseconds = 0.0;
|
double frameBudgetMilliseconds = 0.0;
|
||||||
|
bool actualDeckLinkBufferedFramesAvailable = false;
|
||||||
|
uint64_t actualDeckLinkBufferedFrames = 0;
|
||||||
|
double deckLinkScheduleCallMilliseconds = 0.0;
|
||||||
|
uint64_t deckLinkScheduleFailureCount = 0;
|
||||||
|
bool deckLinkScheduleLeadAvailable = false;
|
||||||
|
int64_t deckLinkPlaybackStreamTime = 0;
|
||||||
|
uint64_t deckLinkPlaybackFrameIndex = 0;
|
||||||
|
uint64_t deckLinkNextScheduleFrameIndex = 0;
|
||||||
|
int64_t deckLinkScheduleLeadFrames = 0;
|
||||||
|
uint64_t deckLinkScheduleRealignmentCount = 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
struct VideoIOFrame
|
struct VideoIOFrame
|
||||||
@@ -76,6 +86,7 @@ struct VideoIOOutputFrame
|
|||||||
struct VideoIOCompletion
|
struct VideoIOCompletion
|
||||||
{
|
{
|
||||||
VideoIOCompletionResult result = VideoIOCompletionResult::Completed;
|
VideoIOCompletionResult result = VideoIOCompletionResult::Completed;
|
||||||
|
void* outputFrameBuffer = nullptr;
|
||||||
};
|
};
|
||||||
|
|
||||||
struct VideoIOScheduleTime
|
struct VideoIOScheduleTime
|
||||||
@@ -86,6 +97,19 @@ struct VideoIOScheduleTime
|
|||||||
uint64_t frameIndex = 0;
|
uint64_t frameIndex = 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
struct VideoPlayoutRecoveryDecision
|
||||||
|
{
|
||||||
|
VideoIOCompletionResult result = VideoIOCompletionResult::Completed;
|
||||||
|
uint64_t completedFrameIndex = 0;
|
||||||
|
uint64_t scheduledFrameIndex = 0;
|
||||||
|
uint64_t readyQueueDepth = 0;
|
||||||
|
uint64_t scheduledLeadFrames = 0;
|
||||||
|
uint64_t measuredLagFrames = 0;
|
||||||
|
uint64_t catchUpFrames = 0;
|
||||||
|
uint64_t lateStreak = 0;
|
||||||
|
uint64_t dropStreak = 0;
|
||||||
|
};
|
||||||
|
|
||||||
class VideoIODevice
|
class VideoIODevice
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
@@ -98,6 +122,9 @@ public:
|
|||||||
virtual bool SelectPreferredFormats(const VideoFormatSelection& videoModes, bool outputAlphaRequired, std::string& error) = 0;
|
virtual bool SelectPreferredFormats(const VideoFormatSelection& videoModes, bool outputAlphaRequired, std::string& error) = 0;
|
||||||
virtual bool ConfigureInput(InputFrameCallback callback, const VideoFormat& inputVideoMode, std::string& error) = 0;
|
virtual bool ConfigureInput(InputFrameCallback callback, const VideoFormat& inputVideoMode, std::string& error) = 0;
|
||||||
virtual bool ConfigureOutput(OutputFrameCallback callback, const VideoFormat& outputVideoMode, bool externalKeyingEnabled, std::string& error) = 0;
|
virtual bool ConfigureOutput(OutputFrameCallback callback, const VideoFormat& outputVideoMode, bool externalKeyingEnabled, std::string& error) = 0;
|
||||||
|
virtual bool PrepareOutputSchedule() = 0;
|
||||||
|
virtual bool StartInputStreams() = 0;
|
||||||
|
virtual bool StartScheduledPlayback() = 0;
|
||||||
virtual bool Start() = 0;
|
virtual bool Start() = 0;
|
||||||
virtual bool Stop() = 0;
|
virtual bool Stop() = 0;
|
||||||
virtual const VideoIOState& State() const = 0;
|
virtual const VideoIOState& State() const = 0;
|
||||||
@@ -105,7 +132,7 @@ public:
|
|||||||
virtual bool BeginOutputFrame(VideoIOOutputFrame& frame) = 0;
|
virtual bool BeginOutputFrame(VideoIOOutputFrame& frame) = 0;
|
||||||
virtual void EndOutputFrame(VideoIOOutputFrame& frame) = 0;
|
virtual void EndOutputFrame(VideoIOOutputFrame& frame) = 0;
|
||||||
virtual bool ScheduleOutputFrame(const VideoIOOutputFrame& frame) = 0;
|
virtual bool ScheduleOutputFrame(const VideoIOOutputFrame& frame) = 0;
|
||||||
virtual void AccountForCompletionResult(VideoIOCompletionResult result) = 0;
|
virtual VideoPlayoutRecoveryDecision AccountForCompletionResult(VideoIOCompletionResult result, uint64_t readyQueueDepth) = 0;
|
||||||
|
|
||||||
bool HasInputDevice() const { return State().hasInputDevice; }
|
bool HasInputDevice() const { return State().hasInputDevice; }
|
||||||
bool HasInputSource() const { return State().hasInputSource; }
|
bool HasInputSource() const { return State().hasInputSource; }
|
||||||
|
|||||||
@@ -0,0 +1,37 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
|
||||||
|
enum class VideoUnderrunBehavior
|
||||||
|
{
|
||||||
|
ReuseLastCompletedFrame,
|
||||||
|
BlackFrame
|
||||||
|
};
|
||||||
|
|
||||||
|
struct VideoPlayoutPolicy
|
||||||
|
{
|
||||||
|
unsigned outputFramePoolSize = 10;
|
||||||
|
unsigned targetPrerollFrames = 4;
|
||||||
|
unsigned targetReadyFrames = 2;
|
||||||
|
unsigned maxReadyFrames = 4;
|
||||||
|
unsigned minimumSpareDeviceFrames = 1;
|
||||||
|
uint64_t lateOrDropCatchUpFrames = 0;
|
||||||
|
VideoUnderrunBehavior underrunBehavior = VideoUnderrunBehavior::ReuseLastCompletedFrame;
|
||||||
|
bool adaptiveHeadroomEnabled = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
inline VideoPlayoutPolicy NormalizeVideoPlayoutPolicy(VideoPlayoutPolicy policy)
|
||||||
|
{
|
||||||
|
if (policy.outputFramePoolSize == 0)
|
||||||
|
policy.outputFramePoolSize = 1;
|
||||||
|
if (policy.targetPrerollFrames == 0)
|
||||||
|
policy.targetPrerollFrames = 1;
|
||||||
|
if (policy.targetReadyFrames == 0)
|
||||||
|
policy.targetReadyFrames = 1;
|
||||||
|
if (policy.maxReadyFrames < policy.targetReadyFrames)
|
||||||
|
policy.maxReadyFrames = policy.targetReadyFrames;
|
||||||
|
const unsigned minimumOutputFramePoolSize = policy.targetPrerollFrames + policy.maxReadyFrames + policy.minimumSpareDeviceFrames;
|
||||||
|
if (policy.outputFramePoolSize < minimumOutputFramePoolSize)
|
||||||
|
policy.outputFramePoolSize = minimumOutputFramePoolSize;
|
||||||
|
return policy;
|
||||||
|
}
|
||||||
@@ -1,15 +1,24 @@
|
|||||||
#include "VideoPlayoutScheduler.h"
|
#include "VideoPlayoutScheduler.h"
|
||||||
|
|
||||||
void VideoPlayoutScheduler::Configure(int64_t frameDuration, int64_t timeScale)
|
void VideoPlayoutScheduler::Configure(int64_t frameDuration, int64_t timeScale)
|
||||||
|
{
|
||||||
|
Configure(frameDuration, timeScale, VideoPlayoutPolicy());
|
||||||
|
}
|
||||||
|
|
||||||
|
void VideoPlayoutScheduler::Configure(int64_t frameDuration, int64_t timeScale, const VideoPlayoutPolicy& policy)
|
||||||
{
|
{
|
||||||
mFrameDuration = frameDuration;
|
mFrameDuration = frameDuration;
|
||||||
mTimeScale = timeScale;
|
mTimeScale = timeScale;
|
||||||
|
mPolicy = NormalizeVideoPlayoutPolicy(policy);
|
||||||
Reset();
|
Reset();
|
||||||
}
|
}
|
||||||
|
|
||||||
void VideoPlayoutScheduler::Reset()
|
void VideoPlayoutScheduler::Reset()
|
||||||
{
|
{
|
||||||
mScheduledFrameIndex = 0;
|
mScheduledFrameIndex = 0;
|
||||||
|
mCompletedFrameIndex = 0;
|
||||||
|
mLateStreak = 0;
|
||||||
|
mDropStreak = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
VideoIOScheduleTime VideoPlayoutScheduler::NextScheduleTime()
|
VideoIOScheduleTime VideoPlayoutScheduler::NextScheduleTime()
|
||||||
@@ -23,10 +32,49 @@ VideoIOScheduleTime VideoPlayoutScheduler::NextScheduleTime()
|
|||||||
return time;
|
return time;
|
||||||
}
|
}
|
||||||
|
|
||||||
void VideoPlayoutScheduler::AccountForCompletionResult(VideoIOCompletionResult result)
|
void VideoPlayoutScheduler::AlignNextScheduleTimeToPlayback(int64_t streamTime, uint64_t leadFrames)
|
||||||
{
|
{
|
||||||
if (result == VideoIOCompletionResult::DisplayedLate || result == VideoIOCompletionResult::Dropped)
|
if (mFrameDuration <= 0 || streamTime < 0)
|
||||||
mScheduledFrameIndex += 2;
|
return;
|
||||||
|
|
||||||
|
const uint64_t playbackFrameIndex = static_cast<uint64_t>(streamTime / mFrameDuration);
|
||||||
|
const uint64_t minimumScheduleIndex = playbackFrameIndex + leadFrames;
|
||||||
|
if (minimumScheduleIndex > mScheduledFrameIndex)
|
||||||
|
mScheduledFrameIndex = minimumScheduleIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
VideoPlayoutRecoveryDecision VideoPlayoutScheduler::AccountForCompletionResult(VideoIOCompletionResult result, uint64_t readyQueueDepth)
|
||||||
|
{
|
||||||
|
++mCompletedFrameIndex;
|
||||||
|
if (result == VideoIOCompletionResult::DisplayedLate)
|
||||||
|
++mLateStreak;
|
||||||
|
else
|
||||||
|
mLateStreak = 0;
|
||||||
|
if (result == VideoIOCompletionResult::Dropped)
|
||||||
|
++mDropStreak;
|
||||||
|
else
|
||||||
|
mDropStreak = 0;
|
||||||
|
|
||||||
|
const uint64_t measuredLagFrames = MeasureLag(result, readyQueueDepth);
|
||||||
|
const uint64_t catchUpFrames = measuredLagFrames < mPolicy.lateOrDropCatchUpFrames
|
||||||
|
? measuredLagFrames
|
||||||
|
: mPolicy.lateOrDropCatchUpFrames;
|
||||||
|
if (catchUpFrames > 0)
|
||||||
|
mScheduledFrameIndex += catchUpFrames;
|
||||||
|
|
||||||
|
VideoPlayoutRecoveryDecision decision;
|
||||||
|
decision.result = result;
|
||||||
|
decision.completedFrameIndex = mCompletedFrameIndex;
|
||||||
|
decision.scheduledFrameIndex = mScheduledFrameIndex;
|
||||||
|
decision.readyQueueDepth = readyQueueDepth;
|
||||||
|
decision.scheduledLeadFrames = mScheduledFrameIndex > mCompletedFrameIndex
|
||||||
|
? mScheduledFrameIndex - mCompletedFrameIndex
|
||||||
|
: 0;
|
||||||
|
decision.measuredLagFrames = measuredLagFrames;
|
||||||
|
decision.catchUpFrames = catchUpFrames;
|
||||||
|
decision.lateStreak = mLateStreak;
|
||||||
|
decision.dropStreak = mDropStreak;
|
||||||
|
return decision;
|
||||||
}
|
}
|
||||||
|
|
||||||
double VideoPlayoutScheduler::FrameBudgetMilliseconds() const
|
double VideoPlayoutScheduler::FrameBudgetMilliseconds() const
|
||||||
@@ -35,3 +83,26 @@ double VideoPlayoutScheduler::FrameBudgetMilliseconds() const
|
|||||||
? (static_cast<double>(mFrameDuration) * 1000.0) / static_cast<double>(mTimeScale)
|
? (static_cast<double>(mFrameDuration) * 1000.0) / static_cast<double>(mTimeScale)
|
||||||
: 0.0;
|
: 0.0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
uint64_t VideoPlayoutScheduler::MeasureLag(VideoIOCompletionResult result, uint64_t readyQueueDepth) const
|
||||||
|
{
|
||||||
|
if (result != VideoIOCompletionResult::DisplayedLate && result != VideoIOCompletionResult::Dropped)
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
uint64_t lagFrames = 1;
|
||||||
|
if (result == VideoIOCompletionResult::DisplayedLate && mLateStreak > lagFrames)
|
||||||
|
lagFrames = mLateStreak;
|
||||||
|
if (result == VideoIOCompletionResult::Dropped && mDropStreak * 2 > lagFrames)
|
||||||
|
lagFrames = mDropStreak * 2;
|
||||||
|
|
||||||
|
if (mCompletedFrameIndex >= mScheduledFrameIndex)
|
||||||
|
{
|
||||||
|
const uint64_t scheduleLagFrames = mCompletedFrameIndex - mScheduledFrameIndex + 1;
|
||||||
|
if (scheduleLagFrames > lagFrames)
|
||||||
|
lagFrames = scheduleLagFrames;
|
||||||
|
}
|
||||||
|
if (readyQueueDepth < mPolicy.targetReadyFrames && mPolicy.targetReadyFrames - readyQueueDepth > lagFrames)
|
||||||
|
lagFrames = mPolicy.targetReadyFrames - readyQueueDepth;
|
||||||
|
|
||||||
|
return lagFrames;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include "VideoIOTypes.h"
|
#include "VideoIOTypes.h"
|
||||||
|
#include "VideoPlayoutPolicy.h"
|
||||||
|
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
|
|
||||||
@@ -8,15 +9,28 @@ class VideoPlayoutScheduler
|
|||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
void Configure(int64_t frameDuration, int64_t timeScale);
|
void Configure(int64_t frameDuration, int64_t timeScale);
|
||||||
|
void Configure(int64_t frameDuration, int64_t timeScale, const VideoPlayoutPolicy& policy);
|
||||||
void Reset();
|
void Reset();
|
||||||
VideoIOScheduleTime NextScheduleTime();
|
VideoIOScheduleTime NextScheduleTime();
|
||||||
void AccountForCompletionResult(VideoIOCompletionResult result);
|
void AlignNextScheduleTimeToPlayback(int64_t streamTime, uint64_t leadFrames);
|
||||||
|
VideoPlayoutRecoveryDecision AccountForCompletionResult(VideoIOCompletionResult result, uint64_t readyQueueDepth = 0);
|
||||||
double FrameBudgetMilliseconds() const;
|
double FrameBudgetMilliseconds() const;
|
||||||
uint64_t ScheduledFrameIndex() const { return mScheduledFrameIndex; }
|
uint64_t ScheduledFrameIndex() const { return mScheduledFrameIndex; }
|
||||||
|
uint64_t CompletedFrameIndex() const { return mCompletedFrameIndex; }
|
||||||
|
int64_t FrameDuration() const { return mFrameDuration; }
|
||||||
|
uint64_t LateStreak() const { return mLateStreak; }
|
||||||
|
uint64_t DropStreak() const { return mDropStreak; }
|
||||||
int64_t TimeScale() const { return mTimeScale; }
|
int64_t TimeScale() const { return mTimeScale; }
|
||||||
|
const VideoPlayoutPolicy& Policy() const { return mPolicy; }
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
uint64_t MeasureLag(VideoIOCompletionResult result, uint64_t readyQueueDepth) const;
|
||||||
|
|
||||||
int64_t mFrameDuration = 0;
|
int64_t mFrameDuration = 0;
|
||||||
int64_t mTimeScale = 0;
|
int64_t mTimeScale = 0;
|
||||||
uint64_t mScheduledFrameIndex = 0;
|
uint64_t mScheduledFrameIndex = 0;
|
||||||
|
uint64_t mCompletedFrameIndex = 0;
|
||||||
|
uint64_t mLateStreak = 0;
|
||||||
|
uint64_t mDropStreak = 0;
|
||||||
|
VideoPlayoutPolicy mPolicy;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
#include "DeckLinkSession.h"
|
#include "DeckLinkSession.h"
|
||||||
|
|
||||||
#include "GlRenderConstants.h"
|
|
||||||
|
|
||||||
#include <atlbase.h>
|
#include <atlbase.h>
|
||||||
|
#include <atomic>
|
||||||
|
#include <chrono>
|
||||||
#include <cstdio>
|
#include <cstdio>
|
||||||
#include <cstring>
|
#include <cstring>
|
||||||
#include <new>
|
#include <new>
|
||||||
@@ -12,6 +12,78 @@
|
|||||||
|
|
||||||
namespace
|
namespace
|
||||||
{
|
{
|
||||||
|
constexpr int64_t kMinimumHealthyScheduleLeadFrames = 4;
|
||||||
|
constexpr int64_t kProactiveScheduleLeadFloorFrames = 1;
|
||||||
|
|
||||||
|
class SystemMemoryDeckLinkVideoBuffer : public IDeckLinkVideoBuffer
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
SystemMemoryDeckLinkVideoBuffer(void* bytes, unsigned long long sizeBytes) :
|
||||||
|
mBytes(bytes),
|
||||||
|
mSizeBytes(sizeBytes),
|
||||||
|
mRefCount(1)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
HRESULT STDMETHODCALLTYPE QueryInterface(REFIID iid, LPVOID* ppv) override
|
||||||
|
{
|
||||||
|
if (ppv == nullptr)
|
||||||
|
return E_POINTER;
|
||||||
|
if (iid == IID_IUnknown || iid == IID_IDeckLinkVideoBuffer)
|
||||||
|
{
|
||||||
|
*ppv = static_cast<IDeckLinkVideoBuffer*>(this);
|
||||||
|
AddRef();
|
||||||
|
return S_OK;
|
||||||
|
}
|
||||||
|
*ppv = nullptr;
|
||||||
|
return E_NOINTERFACE;
|
||||||
|
}
|
||||||
|
|
||||||
|
ULONG STDMETHODCALLTYPE AddRef() override
|
||||||
|
{
|
||||||
|
return ++mRefCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
ULONG STDMETHODCALLTYPE Release() override
|
||||||
|
{
|
||||||
|
const ULONG refCount = --mRefCount;
|
||||||
|
if (refCount == 0)
|
||||||
|
delete this;
|
||||||
|
return refCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
HRESULT STDMETHODCALLTYPE GetBytes(void** buffer) override
|
||||||
|
{
|
||||||
|
if (buffer == nullptr)
|
||||||
|
return E_POINTER;
|
||||||
|
*buffer = mBytes;
|
||||||
|
return mBytes != nullptr ? S_OK : E_FAIL;
|
||||||
|
}
|
||||||
|
|
||||||
|
HRESULT STDMETHODCALLTYPE GetSize(unsigned long long* bufferSize) override
|
||||||
|
{
|
||||||
|
if (bufferSize == nullptr)
|
||||||
|
return E_POINTER;
|
||||||
|
*bufferSize = mSizeBytes;
|
||||||
|
return S_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
HRESULT STDMETHODCALLTYPE StartAccess(BMDBufferAccessFlags) override
|
||||||
|
{
|
||||||
|
return S_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
HRESULT STDMETHODCALLTYPE EndAccess(BMDBufferAccessFlags) override
|
||||||
|
{
|
||||||
|
return S_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
void* mBytes = nullptr;
|
||||||
|
unsigned long long mSizeBytes = 0;
|
||||||
|
std::atomic<ULONG> mRefCount;
|
||||||
|
};
|
||||||
|
|
||||||
std::string BstrToUtf8(BSTR value)
|
std::string BstrToUtf8(BSTR value)
|
||||||
{
|
{
|
||||||
if (value == nullptr)
|
if (value == nullptr)
|
||||||
@@ -210,7 +282,7 @@ bool DeckLinkSession::DiscoverDevicesAndModes(const VideoFormatSelection& videoM
|
|||||||
BMDTimeValue frameDuration = 0;
|
BMDTimeValue frameDuration = 0;
|
||||||
BMDTimeScale frameTimescale = 0;
|
BMDTimeScale frameTimescale = 0;
|
||||||
outputMode->GetFrameRate(&frameDuration, &frameTimescale);
|
outputMode->GetFrameRate(&frameDuration, &frameTimescale);
|
||||||
mScheduler.Configure(frameDuration, frameTimescale);
|
mScheduler.Configure(frameDuration, frameTimescale, mPlayoutPolicy);
|
||||||
mState.frameBudgetMilliseconds = mScheduler.FrameBudgetMilliseconds();
|
mState.frameBudgetMilliseconds = mScheduler.FrameBudgetMilliseconds();
|
||||||
|
|
||||||
mState.inputFrameRowBytes = mState.inputFrameSize.width * 2u;
|
mState.inputFrameRowBytes = mState.inputFrameSize.width * 2u;
|
||||||
@@ -379,7 +451,9 @@ bool DeckLinkSession::ConfigureOutput(OutputFrameCallback callback, const VideoF
|
|||||||
mState.statusMessage = "Selected DeckLink output supports external keying. Set enableExternalKeying to true in runtime-host.json to request it.";
|
mState.statusMessage = "Selected DeckLink output supports external keying. Set enableExternalKeying to true in runtime-host.json to request it.";
|
||||||
}
|
}
|
||||||
|
|
||||||
for (int i = 0; i < 10; i++)
|
const VideoPlayoutPolicy policy = NormalizeVideoPlayoutPolicy(mPlayoutPolicy);
|
||||||
|
mPlayoutPolicy = policy;
|
||||||
|
for (unsigned i = 0; i < policy.outputFramePoolSize; i++)
|
||||||
{
|
{
|
||||||
CComPtr<IDeckLinkMutableVideoFrame> outputFrame;
|
CComPtr<IDeckLinkMutableVideoFrame> outputFrame;
|
||||||
|
|
||||||
@@ -423,7 +497,6 @@ bool DeckLinkSession::AcquireNextOutputVideoFrame(CComPtr<IDeckLinkMutableVideoF
|
|||||||
return false;
|
return false;
|
||||||
|
|
||||||
outputVideoFrame = outputVideoFrameQueue.front();
|
outputVideoFrame = outputVideoFrameQueue.front();
|
||||||
outputVideoFrameQueue.push_back(outputVideoFrame);
|
|
||||||
outputVideoFrameQueue.pop_front();
|
outputVideoFrameQueue.pop_front();
|
||||||
return outputVideoFrame != nullptr;
|
return outputVideoFrame != nullptr;
|
||||||
}
|
}
|
||||||
@@ -448,6 +521,7 @@ bool DeckLinkSession::PopulateOutputFrame(IDeckLinkMutableVideoFrame* outputVide
|
|||||||
frame.width = mState.outputFrameSize.width;
|
frame.width = mState.outputFrameSize.width;
|
||||||
frame.height = mState.outputFrameSize.height;
|
frame.height = mState.outputFrameSize.height;
|
||||||
frame.pixelFormat = mState.outputPixelFormat;
|
frame.pixelFormat = mState.outputPixelFormat;
|
||||||
|
outputVideoFrame->AddRef();
|
||||||
frame.nativeFrame = outputVideoFrame;
|
frame.nativeFrame = outputVideoFrame;
|
||||||
frame.nativeBuffer = outputVideoFrameBuffer.Detach();
|
frame.nativeBuffer = outputVideoFrameBuffer.Detach();
|
||||||
return true;
|
return true;
|
||||||
@@ -455,9 +529,132 @@ bool DeckLinkSession::PopulateOutputFrame(IDeckLinkMutableVideoFrame* outputVide
|
|||||||
|
|
||||||
bool DeckLinkSession::ScheduleFrame(IDeckLinkMutableVideoFrame* outputVideoFrame)
|
bool DeckLinkSession::ScheduleFrame(IDeckLinkMutableVideoFrame* outputVideoFrame)
|
||||||
{
|
{
|
||||||
|
if (outputVideoFrame == nullptr || output == nullptr)
|
||||||
|
{
|
||||||
|
++mState.deckLinkScheduleFailureCount;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mScheduleRealignmentPending)
|
||||||
|
{
|
||||||
|
RealignScheduleCursorToPlayback();
|
||||||
|
mScheduleRealignmentPending = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
UpdateScheduleLeadTelemetry();
|
||||||
|
MaybeRealignScheduleCursorForLowLead();
|
||||||
const VideoIOScheduleTime scheduleTime = mScheduler.NextScheduleTime();
|
const VideoIOScheduleTime scheduleTime = mScheduler.NextScheduleTime();
|
||||||
return outputVideoFrame != nullptr &&
|
const auto scheduleStart = std::chrono::steady_clock::now();
|
||||||
output->ScheduleVideoFrame(outputVideoFrame, scheduleTime.streamTime, scheduleTime.duration, scheduleTime.timeScale) == S_OK;
|
const HRESULT result = output->ScheduleVideoFrame(outputVideoFrame, scheduleTime.streamTime, scheduleTime.duration, scheduleTime.timeScale);
|
||||||
|
const auto scheduleEnd = std::chrono::steady_clock::now();
|
||||||
|
mState.deckLinkScheduleCallMilliseconds = std::chrono::duration_cast<std::chrono::duration<double, std::milli>>(scheduleEnd - scheduleStart).count();
|
||||||
|
if (result != S_OK)
|
||||||
|
++mState.deckLinkScheduleFailureCount;
|
||||||
|
RefreshBufferedVideoFrameCount();
|
||||||
|
return result == S_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
void DeckLinkSession::UpdateScheduleLeadTelemetry()
|
||||||
|
{
|
||||||
|
if (output == nullptr)
|
||||||
|
{
|
||||||
|
mState.deckLinkScheduleLeadAvailable = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
BMDTimeValue streamTime = 0;
|
||||||
|
double playbackSpeed = 0.0;
|
||||||
|
if (output->GetScheduledStreamTime(mScheduler.TimeScale(), &streamTime, &playbackSpeed) != S_OK || playbackSpeed <= 0.0)
|
||||||
|
{
|
||||||
|
mState.deckLinkScheduleLeadAvailable = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const uint64_t playbackFrameIndex = streamTime >= 0 && mScheduler.FrameDuration() > 0
|
||||||
|
? static_cast<uint64_t>(streamTime / mScheduler.FrameDuration())
|
||||||
|
: 0;
|
||||||
|
const uint64_t nextScheduleFrameIndex = mScheduler.ScheduledFrameIndex();
|
||||||
|
mState.deckLinkScheduleLeadAvailable = true;
|
||||||
|
mState.deckLinkPlaybackStreamTime = streamTime;
|
||||||
|
mState.deckLinkPlaybackFrameIndex = playbackFrameIndex;
|
||||||
|
mState.deckLinkNextScheduleFrameIndex = nextScheduleFrameIndex;
|
||||||
|
mState.deckLinkScheduleLeadFrames = static_cast<int64_t>(nextScheduleFrameIndex) - static_cast<int64_t>(playbackFrameIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
void DeckLinkSession::MaybeRealignScheduleCursorForLowLead()
|
||||||
|
{
|
||||||
|
if (!mState.deckLinkScheduleLeadAvailable)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (mState.deckLinkScheduleLeadFrames >= kMinimumHealthyScheduleLeadFrames)
|
||||||
|
{
|
||||||
|
mProactiveScheduleRealignmentArmed = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!mProactiveScheduleRealignmentArmed || mState.deckLinkScheduleLeadFrames > kProactiveScheduleLeadFloorFrames)
|
||||||
|
return;
|
||||||
|
|
||||||
|
RealignScheduleCursorToPlayback();
|
||||||
|
mProactiveScheduleRealignmentArmed = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void DeckLinkSession::RealignScheduleCursorToPlayback()
|
||||||
|
{
|
||||||
|
if (output == nullptr)
|
||||||
|
return;
|
||||||
|
|
||||||
|
BMDTimeValue streamTime = 0;
|
||||||
|
double playbackSpeed = 0.0;
|
||||||
|
if (output->GetScheduledStreamTime(mScheduler.TimeScale(), &streamTime, &playbackSpeed) != S_OK || playbackSpeed <= 0.0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
const VideoPlayoutPolicy policy = NormalizeVideoPlayoutPolicy(mPlayoutPolicy);
|
||||||
|
mScheduler.AlignNextScheduleTimeToPlayback(streamTime, policy.targetPrerollFrames);
|
||||||
|
++mState.deckLinkScheduleRealignmentCount;
|
||||||
|
UpdateScheduleLeadTelemetry();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool DeckLinkSession::ScheduleSystemMemoryFrame(const VideoIOOutputFrame& frame)
|
||||||
|
{
|
||||||
|
if (output == nullptr || frame.bytes == nullptr || frame.rowBytes <= 0 || frame.height == 0)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
CComPtr<IDeckLinkVideoBuffer> videoBuffer;
|
||||||
|
videoBuffer.Attach(new (std::nothrow) SystemMemoryDeckLinkVideoBuffer(
|
||||||
|
frame.bytes,
|
||||||
|
static_cast<unsigned long long>(frame.rowBytes) * static_cast<unsigned long long>(frame.height)));
|
||||||
|
if (videoBuffer == nullptr)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
CComPtr<IDeckLinkMutableVideoFrame> outputVideoFrame;
|
||||||
|
const BMDPixelFormat pixelFormat = DeckLinkPixelFormatForVideoIO(frame.pixelFormat);
|
||||||
|
if (output->CreateVideoFrameWithBuffer(
|
||||||
|
frame.width,
|
||||||
|
frame.height,
|
||||||
|
frame.rowBytes,
|
||||||
|
pixelFormat,
|
||||||
|
bmdFrameFlagFlipVertical,
|
||||||
|
videoBuffer,
|
||||||
|
&outputVideoFrame) != S_OK)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
IDeckLinkVideoFrame* scheduledFrame = outputVideoFrame;
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(mScheduledSystemFrameMutex);
|
||||||
|
mScheduledSystemFrameBuffers[scheduledFrame] = frame.bytes;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ScheduleFrame(outputVideoFrame))
|
||||||
|
return true;
|
||||||
|
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(mScheduledSystemFrameMutex);
|
||||||
|
mScheduledSystemFrameBuffers.erase(scheduledFrame);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool DeckLinkSession::ScheduleBlackFrame(IDeckLinkMutableVideoFrame* outputVideoFrame)
|
bool DeckLinkSession::ScheduleBlackFrame(IDeckLinkMutableVideoFrame* outputVideoFrame)
|
||||||
@@ -480,6 +677,26 @@ bool DeckLinkSession::ScheduleBlackFrame(IDeckLinkMutableVideoFrame* outputVideo
|
|||||||
return ScheduleFrame(outputVideoFrame);
|
return ScheduleFrame(outputVideoFrame);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void DeckLinkSession::RefreshBufferedVideoFrameCount()
|
||||||
|
{
|
||||||
|
if (output == nullptr)
|
||||||
|
{
|
||||||
|
mState.actualDeckLinkBufferedFramesAvailable = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
unsigned int bufferedFrameCount = 0;
|
||||||
|
if (output->GetBufferedVideoFrameCount(&bufferedFrameCount) == S_OK)
|
||||||
|
{
|
||||||
|
mState.actualDeckLinkBufferedFrames = bufferedFrameCount;
|
||||||
|
mState.actualDeckLinkBufferedFramesAvailable = true;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
mState.actualDeckLinkBufferedFramesAvailable = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
bool DeckLinkSession::BeginOutputFrame(VideoIOOutputFrame& frame)
|
bool DeckLinkSession::BeginOutputFrame(VideoIOOutputFrame& frame)
|
||||||
{
|
{
|
||||||
CComPtr<IDeckLinkMutableVideoFrame> outputVideoFrame;
|
CComPtr<IDeckLinkMutableVideoFrame> outputVideoFrame;
|
||||||
@@ -498,20 +715,62 @@ void DeckLinkSession::EndOutputFrame(VideoIOOutputFrame& frame)
|
|||||||
frame.bytes = nullptr;
|
frame.bytes = nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
void DeckLinkSession::AccountForCompletionResult(VideoIOCompletionResult completionResult)
|
VideoPlayoutRecoveryDecision DeckLinkSession::AccountForCompletionResult(VideoIOCompletionResult completionResult, uint64_t readyQueueDepth)
|
||||||
{
|
{
|
||||||
mScheduler.AccountForCompletionResult(completionResult);
|
return mScheduler.AccountForCompletionResult(completionResult, readyQueueDepth);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool DeckLinkSession::ScheduleOutputFrame(const VideoIOOutputFrame& frame)
|
bool DeckLinkSession::ScheduleOutputFrame(const VideoIOOutputFrame& frame)
|
||||||
{
|
{
|
||||||
|
if (frame.nativeFrame == nullptr)
|
||||||
|
return ScheduleSystemMemoryFrame(frame);
|
||||||
|
|
||||||
IDeckLinkMutableVideoFrame* outputVideoFrame = static_cast<IDeckLinkMutableVideoFrame*>(frame.nativeFrame);
|
IDeckLinkMutableVideoFrame* outputVideoFrame = static_cast<IDeckLinkMutableVideoFrame*>(frame.nativeFrame);
|
||||||
return ScheduleFrame(outputVideoFrame);
|
const bool scheduled = ScheduleFrame(outputVideoFrame);
|
||||||
|
if (outputVideoFrame != nullptr)
|
||||||
|
outputVideoFrame->Release();
|
||||||
|
return scheduled;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool DeckLinkSession::PrepareOutputSchedule()
|
||||||
|
{
|
||||||
|
mScheduler.Reset();
|
||||||
|
RefreshBufferedVideoFrameCount();
|
||||||
|
return output != nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool DeckLinkSession::StartInputStreams()
|
||||||
|
{
|
||||||
|
if (!input)
|
||||||
|
return true;
|
||||||
|
|
||||||
|
if (input->StartStreams() != S_OK)
|
||||||
|
{
|
||||||
|
MessageBoxA(NULL, "Could not start the DeckLink input stream.", "DeckLink start failed", MB_OK | MB_ICONERROR);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool DeckLinkSession::StartScheduledPlayback()
|
||||||
|
{
|
||||||
|
if (!output)
|
||||||
|
{
|
||||||
|
MessageBoxA(NULL, "Cannot start playout because no DeckLink output device is available.", "DeckLink start failed", MB_OK | MB_ICONERROR);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (output->StartScheduledPlayback(0, mScheduler.TimeScale(), 1.0) != S_OK)
|
||||||
|
{
|
||||||
|
MessageBoxA(NULL, "Could not start DeckLink scheduled playback.", "DeckLink start failed", MB_OK | MB_ICONERROR);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
RefreshBufferedVideoFrameCount();
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool DeckLinkSession::Start()
|
bool DeckLinkSession::Start()
|
||||||
{
|
{
|
||||||
mScheduler.Reset();
|
|
||||||
if (!output)
|
if (!output)
|
||||||
{
|
{
|
||||||
MessageBoxA(NULL, "Cannot start playout because no DeckLink output device is available.", "DeckLink start failed", MB_OK | MB_ICONERROR);
|
MessageBoxA(NULL, "Cannot start playout because no DeckLink output device is available.", "DeckLink start failed", MB_OK | MB_ICONERROR);
|
||||||
@@ -523,7 +782,12 @@ bool DeckLinkSession::Start()
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
for (unsigned i = 0; i < kPrerollFrameCount; i++)
|
const VideoPlayoutPolicy policy = NormalizeVideoPlayoutPolicy(mPlayoutPolicy);
|
||||||
|
mPlayoutPolicy = policy;
|
||||||
|
if (!PrepareOutputSchedule())
|
||||||
|
return false;
|
||||||
|
|
||||||
|
for (unsigned i = 0; i < policy.targetPrerollFrames; i++)
|
||||||
{
|
{
|
||||||
CComPtr<IDeckLinkMutableVideoFrame> outputVideoFrame;
|
CComPtr<IDeckLinkMutableVideoFrame> outputVideoFrame;
|
||||||
if (!AcquireNextOutputVideoFrame(outputVideoFrame))
|
if (!AcquireNextOutputVideoFrame(outputVideoFrame))
|
||||||
@@ -538,21 +802,7 @@ bool DeckLinkSession::Start()
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (input)
|
return StartInputStreams() && StartScheduledPlayback();
|
||||||
{
|
|
||||||
if (input->StartStreams() != S_OK)
|
|
||||||
{
|
|
||||||
MessageBoxA(NULL, "Could not start the DeckLink input stream.", "DeckLink start failed", MB_OK | MB_ICONERROR);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (output->StartScheduledPlayback(0, mScheduler.TimeScale(), 1.0) != S_OK)
|
|
||||||
{
|
|
||||||
MessageBoxA(NULL, "Could not start DeckLink scheduled playback.", "DeckLink start failed", MB_OK | MB_ICONERROR);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
bool DeckLinkSession::Stop()
|
bool DeckLinkSession::Stop()
|
||||||
@@ -614,13 +864,54 @@ void DeckLinkSession::HandleVideoInputFrame(IDeckLinkVideoInputFrame* inputFrame
|
|||||||
inputFrameBuffer->EndAccess(bmdBufferAccessRead);
|
inputFrameBuffer->EndAccess(bmdBufferAccessRead);
|
||||||
}
|
}
|
||||||
|
|
||||||
void DeckLinkSession::HandlePlayoutFrameCompleted(IDeckLinkVideoFrame*, BMDOutputFrameCompletionResult completionResult)
|
void DeckLinkSession::HandlePlayoutFrameCompleted(IDeckLinkVideoFrame* completedFrame, BMDOutputFrameCompletionResult completionResult)
|
||||||
{
|
{
|
||||||
|
RefreshBufferedVideoFrameCount();
|
||||||
|
|
||||||
|
void* completedSystemBuffer = nullptr;
|
||||||
|
if (completedFrame != nullptr)
|
||||||
|
{
|
||||||
|
bool externalSystemFrame = false;
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(mScheduledSystemFrameMutex);
|
||||||
|
auto externalFrame = mScheduledSystemFrameBuffers.find(completedFrame);
|
||||||
|
if (externalFrame != mScheduledSystemFrameBuffers.end())
|
||||||
|
{
|
||||||
|
completedSystemBuffer = externalFrame->second;
|
||||||
|
mScheduledSystemFrameBuffers.erase(externalFrame);
|
||||||
|
externalSystemFrame = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!externalSystemFrame)
|
||||||
|
{
|
||||||
|
CComPtr<IDeckLinkMutableVideoFrame> reusableFrame;
|
||||||
|
if (completedFrame->QueryInterface(IID_IDeckLinkMutableVideoFrame, reinterpret_cast<void**>(&reusableFrame)) == S_OK &&
|
||||||
|
reusableFrame != nullptr)
|
||||||
|
{
|
||||||
|
outputVideoFrameQueue.push_back(reusableFrame);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (!mOutputFrameCallback)
|
if (!mOutputFrameCallback)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
VideoIOCompletion completion;
|
VideoIOCompletion completion;
|
||||||
completion.result = TranslateCompletionResult(completionResult);
|
completion.result = TranslateCompletionResult(completionResult);
|
||||||
|
if (completion.result == VideoIOCompletionResult::DisplayedLate || completion.result == VideoIOCompletionResult::Dropped)
|
||||||
|
{
|
||||||
|
if (mScheduleRealignmentArmed)
|
||||||
|
{
|
||||||
|
mScheduleRealignmentPending = true;
|
||||||
|
mScheduleRealignmentArmed = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (completion.result == VideoIOCompletionResult::Completed)
|
||||||
|
{
|
||||||
|
mScheduleRealignmentArmed = true;
|
||||||
|
}
|
||||||
|
completion.outputFrameBuffer = completedSystemBuffer;
|
||||||
mOutputFrameCallback(completion);
|
mOutputFrameCallback(completion);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,11 +6,14 @@
|
|||||||
#include "DeckLinkVideoIOFormat.h"
|
#include "DeckLinkVideoIOFormat.h"
|
||||||
#include "VideoIOFormat.h"
|
#include "VideoIOFormat.h"
|
||||||
#include "VideoIOTypes.h"
|
#include "VideoIOTypes.h"
|
||||||
|
#include "VideoPlayoutPolicy.h"
|
||||||
#include "VideoPlayoutScheduler.h"
|
#include "VideoPlayoutScheduler.h"
|
||||||
|
|
||||||
#include <atlbase.h>
|
#include <atlbase.h>
|
||||||
#include <deque>
|
#include <deque>
|
||||||
|
#include <mutex>
|
||||||
#include <string>
|
#include <string>
|
||||||
|
#include <unordered_map>
|
||||||
|
|
||||||
class OpenGLComposite;
|
class OpenGLComposite;
|
||||||
|
|
||||||
@@ -25,6 +28,9 @@ public:
|
|||||||
bool SelectPreferredFormats(const VideoFormatSelection& videoModes, bool outputAlphaRequired, std::string& error) override;
|
bool SelectPreferredFormats(const VideoFormatSelection& videoModes, bool outputAlphaRequired, std::string& error) override;
|
||||||
bool ConfigureInput(InputFrameCallback callback, const VideoFormat& inputVideoMode, std::string& error) override;
|
bool ConfigureInput(InputFrameCallback callback, const VideoFormat& inputVideoMode, std::string& error) override;
|
||||||
bool ConfigureOutput(OutputFrameCallback callback, const VideoFormat& outputVideoMode, bool externalKeyingEnabled, std::string& error) override;
|
bool ConfigureOutput(OutputFrameCallback callback, const VideoFormat& outputVideoMode, bool externalKeyingEnabled, std::string& error) override;
|
||||||
|
bool PrepareOutputSchedule() override;
|
||||||
|
bool StartInputStreams() override;
|
||||||
|
bool StartScheduledPlayback() override;
|
||||||
bool Start() override;
|
bool Start() override;
|
||||||
bool Stop() override;
|
bool Stop() override;
|
||||||
|
|
||||||
@@ -58,7 +64,7 @@ public:
|
|||||||
const VideoIOState& State() const override { return mState; }
|
const VideoIOState& State() const override { return mState; }
|
||||||
VideoIOState& MutableState() override { return mState; }
|
VideoIOState& MutableState() override { return mState; }
|
||||||
double FrameBudgetMilliseconds() const;
|
double FrameBudgetMilliseconds() const;
|
||||||
void AccountForCompletionResult(VideoIOCompletionResult completionResult) override;
|
VideoPlayoutRecoveryDecision AccountForCompletionResult(VideoIOCompletionResult completionResult, uint64_t readyQueueDepth) override;
|
||||||
bool BeginOutputFrame(VideoIOOutputFrame& frame) override;
|
bool BeginOutputFrame(VideoIOOutputFrame& frame) override;
|
||||||
void EndOutputFrame(VideoIOOutputFrame& frame) override;
|
void EndOutputFrame(VideoIOOutputFrame& frame) override;
|
||||||
bool ScheduleOutputFrame(const VideoIOOutputFrame& frame) override;
|
bool ScheduleOutputFrame(const VideoIOOutputFrame& frame) override;
|
||||||
@@ -69,7 +75,12 @@ private:
|
|||||||
bool AcquireNextOutputVideoFrame(CComPtr<IDeckLinkMutableVideoFrame>& outputVideoFrame);
|
bool AcquireNextOutputVideoFrame(CComPtr<IDeckLinkMutableVideoFrame>& outputVideoFrame);
|
||||||
bool PopulateOutputFrame(IDeckLinkMutableVideoFrame* outputVideoFrame, VideoIOOutputFrame& frame);
|
bool PopulateOutputFrame(IDeckLinkMutableVideoFrame* outputVideoFrame, VideoIOOutputFrame& frame);
|
||||||
bool ScheduleFrame(IDeckLinkMutableVideoFrame* outputVideoFrame);
|
bool ScheduleFrame(IDeckLinkMutableVideoFrame* outputVideoFrame);
|
||||||
|
void UpdateScheduleLeadTelemetry();
|
||||||
|
void MaybeRealignScheduleCursorForLowLead();
|
||||||
|
void RealignScheduleCursorToPlayback();
|
||||||
|
bool ScheduleSystemMemoryFrame(const VideoIOOutputFrame& frame);
|
||||||
bool ScheduleBlackFrame(IDeckLinkMutableVideoFrame* outputVideoFrame);
|
bool ScheduleBlackFrame(IDeckLinkMutableVideoFrame* outputVideoFrame);
|
||||||
|
void RefreshBufferedVideoFrameCount();
|
||||||
static VideoIOCompletionResult TranslateCompletionResult(BMDOutputFrameCompletionResult completionResult);
|
static VideoIOCompletionResult TranslateCompletionResult(BMDOutputFrameCompletionResult completionResult);
|
||||||
|
|
||||||
CComPtr<CaptureDelegate> captureDelegate;
|
CComPtr<CaptureDelegate> captureDelegate;
|
||||||
@@ -78,8 +89,14 @@ private:
|
|||||||
CComPtr<IDeckLinkOutput> output;
|
CComPtr<IDeckLinkOutput> output;
|
||||||
CComPtr<IDeckLinkKeyer> keyer;
|
CComPtr<IDeckLinkKeyer> keyer;
|
||||||
std::deque<CComPtr<IDeckLinkMutableVideoFrame>> outputVideoFrameQueue;
|
std::deque<CComPtr<IDeckLinkMutableVideoFrame>> outputVideoFrameQueue;
|
||||||
|
std::mutex mScheduledSystemFrameMutex;
|
||||||
|
std::unordered_map<IDeckLinkVideoFrame*, void*> mScheduledSystemFrameBuffers;
|
||||||
VideoIOState mState;
|
VideoIOState mState;
|
||||||
|
VideoPlayoutPolicy mPlayoutPolicy;
|
||||||
VideoPlayoutScheduler mScheduler;
|
VideoPlayoutScheduler mScheduler;
|
||||||
|
bool mScheduleRealignmentPending = false;
|
||||||
|
bool mScheduleRealignmentArmed = true;
|
||||||
|
bool mProactiveScheduleRealignmentArmed = true;
|
||||||
InputFrameCallback mInputFrameCallback;
|
InputFrameCallback mInputFrameCallback;
|
||||||
OutputFrameCallback mOutputFrameCallback;
|
OutputFrameCallback mOutputFrameCallback;
|
||||||
};
|
};
|
||||||
|
|||||||
442
apps/RenderCadenceCompositor/README.md
Normal file
442
apps/RenderCadenceCompositor/README.md
Normal file
@@ -0,0 +1,442 @@
|
|||||||
|
# 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
|
||||||
|
```
|
||||||
|
|
||||||
|
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
|
||||||
|
- 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
|
||||||
|
- preview
|
||||||
|
- 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
|
||||||
|
- [ ] Preview output
|
||||||
|
- [ ] 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`
|
||||||
|
- `previewFps`
|
||||||
|
- `enableExternalKeying`
|
||||||
|
|
||||||
|
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
apps/RenderCadenceCompositor/RenderCadenceCompositor.cpp
Normal file
228
apps/RenderCadenceCompositor/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;
|
||||||
|
}
|
||||||
39
apps/RenderCadenceCompositor/app/AppConfig.cpp
Normal file
39
apps/RenderCadenceCompositor/app/AppConfig.cpp
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
#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.previewFps = 30.0;
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
41
apps/RenderCadenceCompositor/app/AppConfig.h
Normal file
41
apps/RenderCadenceCompositor/app/AppConfig.h
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "../control/http/HttpControlServer.h"
|
||||||
|
#include "../logging/Logger.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;
|
||||||
|
double previewFps = 30.0;
|
||||||
|
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();
|
||||||
|
}
|
||||||
282
apps/RenderCadenceCompositor/app/AppConfigProvider.cpp
Normal file
282
apps/RenderCadenceCompositor/app/AppConfigProvider.cpp
Normal file
@@ -0,0 +1,282 @@
|
|||||||
|
#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);
|
||||||
|
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
apps/RenderCadenceCompositor/app/AppConfigProvider.h
Normal file
35
apps/RenderCadenceCompositor/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);
|
||||||
|
}
|
||||||
291
apps/RenderCadenceCompositor/app/RenderCadenceApp.h
Normal file
291
apps/RenderCadenceCompositor/app/RenderCadenceApp.h
Normal file
@@ -0,0 +1,291 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "AppConfig.h"
|
||||||
|
#include "AppConfigProvider.h"
|
||||||
|
#include "RuntimeLayerController.h"
|
||||||
|
#include "../logging/Logger.h"
|
||||||
|
#include "../control/RuntimeStateJson.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;
|
||||||
|
}
|
||||||
|
|
||||||
|
StartOptionalVideoOutput();
|
||||||
|
mTelemetryHealth.Start(mFrameExchange, mOutput, mOutputThread, mRenderThread);
|
||||||
|
StartHttpServer();
|
||||||
|
Log("app", "RenderCadenceCompositor started.");
|
||||||
|
mStarted = true;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Stop()
|
||||||
|
{
|
||||||
|
mHttpServer.Stop();
|
||||||
|
mTelemetryHealth.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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
RuntimeLayerController mRuntimeLayers;
|
||||||
|
std::function<DeckLinkInputMetrics()> mDeckLinkInputMetricsProvider;
|
||||||
|
uint64_t mLastInputCapturedFrames = 0;
|
||||||
|
bool mStarted = false;
|
||||||
|
bool mVideoOutputEnabled = false;
|
||||||
|
std::string mVideoOutputStatus = "DeckLink output not started.";
|
||||||
|
};
|
||||||
|
}
|
||||||
92
apps/RenderCadenceCompositor/app/RuntimeLayerController.cpp
Normal file
92
apps/RenderCadenceCompositor/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
apps/RenderCadenceCompositor/app/RuntimeLayerController.h
Normal file
63
apps/RenderCadenceCompositor/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
apps/RenderCadenceCompositor/app/RuntimeLayerControllerBuild.cpp
Normal file
122
apps/RenderCadenceCompositor/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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
apps/RenderCadenceCompositor/control/ControlActionResult.h
Normal file
12
apps/RenderCadenceCompositor/control/ControlActionResult.h
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
namespace RenderCadenceCompositor
|
||||||
|
{
|
||||||
|
struct ControlActionResult
|
||||||
|
{
|
||||||
|
bool ok = false;
|
||||||
|
std::string error;
|
||||||
|
};
|
||||||
|
}
|
||||||
127
apps/RenderCadenceCompositor/control/RuntimeControlCommand.cpp
Normal file
127
apps/RenderCadenceCompositor/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
apps/RenderCadenceCompositor/control/RuntimeControlCommand.h
Normal file
37
apps/RenderCadenceCompositor/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);
|
||||||
|
}
|
||||||
324
apps/RenderCadenceCompositor/control/RuntimeStateJson.h
Normal file
324
apps/RenderCadenceCompositor/control/RuntimeStateJson.h
Normal file
@@ -0,0 +1,324 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "../app/AppConfig.h"
|
||||||
|
#include "../app/AppConfigProvider.h"
|
||||||
|
#include "../json/JsonWriter.h"
|
||||||
|
#include "../runtime/RuntimeLayerModel.h"
|
||||||
|
#include "../runtime/SupportedShaderCatalog.h"
|
||||||
|
#include "../telemetry/CadenceTelemetryJson.h"
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
namespace RenderCadenceCompositor
|
||||||
|
{
|
||||||
|
struct RuntimeStateJsonInput
|
||||||
|
{
|
||||||
|
const AppConfig& config;
|
||||||
|
const CadenceTelemetrySnapshot& telemetry;
|
||||||
|
unsigned short serverPort = 0;
|
||||||
|
bool videoOutputEnabled = false;
|
||||||
|
std::string videoOutputStatus;
|
||||||
|
const SupportedShaderCatalog& shaderCatalog;
|
||||||
|
const RuntimeLayerModelSnapshot& runtimeLayers;
|
||||||
|
};
|
||||||
|
|
||||||
|
inline void WriteVideoIoStatusJson(JsonWriter& writer, const RuntimeStateJsonInput& input)
|
||||||
|
{
|
||||||
|
writer.BeginObject();
|
||||||
|
writer.KeyString("backend", "decklink");
|
||||||
|
writer.KeyNull("modelName");
|
||||||
|
writer.KeyBool("supportsInternalKeying", false);
|
||||||
|
writer.KeyBool("supportsExternalKeying", false);
|
||||||
|
writer.KeyBool("keyerInterfaceAvailable", false);
|
||||||
|
writer.KeyBool("externalKeyingRequested", input.config.deckLink.externalKeyingEnabled);
|
||||||
|
writer.KeyBool("externalKeyingActive", input.videoOutputEnabled && input.config.deckLink.externalKeyingEnabled);
|
||||||
|
writer.KeyString("statusMessage", input.videoOutputStatus);
|
||||||
|
writer.EndObject();
|
||||||
|
}
|
||||||
|
|
||||||
|
inline void OutputDimensions(const RuntimeStateJsonInput& input, unsigned& width, unsigned& height)
|
||||||
|
{
|
||||||
|
VideoFormatDimensions(input.config.outputVideoFormat, width, height);
|
||||||
|
}
|
||||||
|
|
||||||
|
inline const char* ShaderParameterTypeName(ShaderParameterType type)
|
||||||
|
{
|
||||||
|
switch (type)
|
||||||
|
{
|
||||||
|
case ShaderParameterType::Float: return "float";
|
||||||
|
case ShaderParameterType::Vec2: return "vec2";
|
||||||
|
case ShaderParameterType::Color: return "color";
|
||||||
|
case ShaderParameterType::Boolean: return "bool";
|
||||||
|
case ShaderParameterType::Enum: return "enum";
|
||||||
|
case ShaderParameterType::Text: return "text";
|
||||||
|
case ShaderParameterType::Trigger: return "trigger";
|
||||||
|
}
|
||||||
|
return "unknown";
|
||||||
|
}
|
||||||
|
|
||||||
|
inline void WriteNumberArray(JsonWriter& writer, const std::vector<double>& values)
|
||||||
|
{
|
||||||
|
writer.BeginArray();
|
||||||
|
for (double value : values)
|
||||||
|
writer.Double(value);
|
||||||
|
writer.EndArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
inline void WriteDefaultParameterValue(JsonWriter& writer, const ShaderParameterDefinition& parameter)
|
||||||
|
{
|
||||||
|
switch (parameter.type)
|
||||||
|
{
|
||||||
|
case ShaderParameterType::Boolean:
|
||||||
|
writer.Bool(parameter.defaultBoolean);
|
||||||
|
return;
|
||||||
|
case ShaderParameterType::Enum:
|
||||||
|
writer.String(parameter.defaultEnumValue);
|
||||||
|
return;
|
||||||
|
case ShaderParameterType::Text:
|
||||||
|
writer.String(parameter.defaultTextValue);
|
||||||
|
return;
|
||||||
|
case ShaderParameterType::Trigger:
|
||||||
|
writer.Double(0.0);
|
||||||
|
return;
|
||||||
|
case ShaderParameterType::Float:
|
||||||
|
writer.Double(parameter.defaultNumbers.empty() ? 0.0 : parameter.defaultNumbers.front());
|
||||||
|
return;
|
||||||
|
case ShaderParameterType::Vec2:
|
||||||
|
case ShaderParameterType::Color:
|
||||||
|
WriteNumberArray(writer, parameter.defaultNumbers);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
writer.Null();
|
||||||
|
}
|
||||||
|
|
||||||
|
inline void WriteParameterValue(JsonWriter& writer, const ShaderParameterDefinition& parameter, const ShaderParameterValue& value)
|
||||||
|
{
|
||||||
|
switch (parameter.type)
|
||||||
|
{
|
||||||
|
case ShaderParameterType::Boolean:
|
||||||
|
writer.Bool(value.booleanValue);
|
||||||
|
return;
|
||||||
|
case ShaderParameterType::Enum:
|
||||||
|
writer.String(value.enumValue);
|
||||||
|
return;
|
||||||
|
case ShaderParameterType::Text:
|
||||||
|
writer.String(value.textValue);
|
||||||
|
return;
|
||||||
|
case ShaderParameterType::Trigger:
|
||||||
|
case ShaderParameterType::Float:
|
||||||
|
writer.Double(value.numberValues.empty() ? 0.0 : value.numberValues.front());
|
||||||
|
return;
|
||||||
|
case ShaderParameterType::Vec2:
|
||||||
|
case ShaderParameterType::Color:
|
||||||
|
WriteNumberArray(writer, value.numberValues);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
writer.Null();
|
||||||
|
}
|
||||||
|
|
||||||
|
inline void WriteTemporalJson(JsonWriter& writer, const TemporalSettings& temporal)
|
||||||
|
{
|
||||||
|
writer.BeginObject();
|
||||||
|
writer.KeyBool("enabled", temporal.enabled);
|
||||||
|
writer.KeyString("historySource", "none");
|
||||||
|
writer.KeyUInt("requestedHistoryLength", temporal.requestedHistoryLength);
|
||||||
|
writer.KeyUInt("effectiveHistoryLength", temporal.effectiveHistoryLength);
|
||||||
|
writer.EndObject();
|
||||||
|
}
|
||||||
|
|
||||||
|
inline void WriteFeedbackJson(JsonWriter& writer, const FeedbackSettings& feedback)
|
||||||
|
{
|
||||||
|
writer.BeginObject();
|
||||||
|
writer.KeyBool("enabled", feedback.enabled);
|
||||||
|
writer.KeyString("writePass", feedback.writePassId);
|
||||||
|
writer.EndObject();
|
||||||
|
}
|
||||||
|
|
||||||
|
inline const char* RuntimeLayerBuildStateName(RuntimeLayerBuildState state)
|
||||||
|
{
|
||||||
|
switch (state)
|
||||||
|
{
|
||||||
|
case RuntimeLayerBuildState::Pending: return "pending";
|
||||||
|
case RuntimeLayerBuildState::Ready: return "ready";
|
||||||
|
case RuntimeLayerBuildState::Failed: return "failed";
|
||||||
|
}
|
||||||
|
return "unknown";
|
||||||
|
}
|
||||||
|
|
||||||
|
inline void WriteParameterDefinitionJson(JsonWriter& writer, const ShaderParameterDefinition& parameter, const ShaderParameterValue* value)
|
||||||
|
{
|
||||||
|
writer.BeginObject();
|
||||||
|
writer.KeyString("id", parameter.id);
|
||||||
|
writer.KeyString("label", parameter.label.empty() ? parameter.id : parameter.label);
|
||||||
|
writer.KeyString("description", parameter.description);
|
||||||
|
writer.KeyString("type", ShaderParameterTypeName(parameter.type));
|
||||||
|
writer.Key("defaultValue");
|
||||||
|
WriteDefaultParameterValue(writer, parameter);
|
||||||
|
writer.Key("value");
|
||||||
|
if (value)
|
||||||
|
WriteParameterValue(writer, parameter, *value);
|
||||||
|
else
|
||||||
|
WriteDefaultParameterValue(writer, parameter);
|
||||||
|
|
||||||
|
if (!parameter.minNumbers.empty())
|
||||||
|
{
|
||||||
|
writer.Key("min");
|
||||||
|
WriteNumberArray(writer, parameter.minNumbers);
|
||||||
|
}
|
||||||
|
if (!parameter.maxNumbers.empty())
|
||||||
|
{
|
||||||
|
writer.Key("max");
|
||||||
|
WriteNumberArray(writer, parameter.maxNumbers);
|
||||||
|
}
|
||||||
|
if (!parameter.stepNumbers.empty())
|
||||||
|
{
|
||||||
|
writer.Key("step");
|
||||||
|
WriteNumberArray(writer, parameter.stepNumbers);
|
||||||
|
}
|
||||||
|
if (parameter.type == ShaderParameterType::Enum)
|
||||||
|
{
|
||||||
|
writer.Key("options");
|
||||||
|
writer.BeginArray();
|
||||||
|
for (const ShaderParameterOption& option : parameter.enumOptions)
|
||||||
|
{
|
||||||
|
writer.BeginObject();
|
||||||
|
writer.KeyString("value", option.value);
|
||||||
|
writer.KeyString("label", option.label.empty() ? option.value : option.label);
|
||||||
|
writer.EndObject();
|
||||||
|
}
|
||||||
|
writer.EndArray();
|
||||||
|
}
|
||||||
|
if (parameter.type == ShaderParameterType::Text)
|
||||||
|
{
|
||||||
|
writer.KeyUInt("maxLength", parameter.maxLength);
|
||||||
|
if (!parameter.fontId.empty())
|
||||||
|
writer.KeyString("font", parameter.fontId);
|
||||||
|
}
|
||||||
|
writer.EndObject();
|
||||||
|
}
|
||||||
|
|
||||||
|
inline void WriteLayersJson(JsonWriter& writer, const RuntimeStateJsonInput& input)
|
||||||
|
{
|
||||||
|
writer.BeginArray();
|
||||||
|
for (const RuntimeLayerReadModel& layer : input.runtimeLayers.displayLayers)
|
||||||
|
{
|
||||||
|
const ShaderPackage* shaderPackage = input.shaderCatalog.FindPackage(layer.shaderId);
|
||||||
|
writer.BeginObject();
|
||||||
|
writer.KeyString("id", layer.id);
|
||||||
|
writer.KeyString("shaderId", layer.shaderId);
|
||||||
|
writer.KeyString("shaderName", layer.shaderName);
|
||||||
|
writer.KeyBool("bypass", layer.bypass);
|
||||||
|
writer.KeyString("buildState", RuntimeLayerBuildStateName(layer.buildState));
|
||||||
|
writer.KeyBool("renderReady", layer.renderReady);
|
||||||
|
writer.KeyString("message", layer.message);
|
||||||
|
writer.Key("temporal");
|
||||||
|
if (shaderPackage)
|
||||||
|
WriteTemporalJson(writer, shaderPackage->temporal);
|
||||||
|
else
|
||||||
|
WriteTemporalJson(writer, TemporalSettings());
|
||||||
|
writer.Key("feedback");
|
||||||
|
if (shaderPackage)
|
||||||
|
WriteFeedbackJson(writer, shaderPackage->feedback);
|
||||||
|
else
|
||||||
|
WriteFeedbackJson(writer, FeedbackSettings());
|
||||||
|
writer.Key("parameters");
|
||||||
|
writer.BeginArray();
|
||||||
|
for (const ShaderParameterDefinition& parameter : layer.parameterDefinitions)
|
||||||
|
{
|
||||||
|
const auto valueIt = layer.parameterValues.find(parameter.id);
|
||||||
|
WriteParameterDefinitionJson(writer, parameter, valueIt == layer.parameterValues.end() ? nullptr : &valueIt->second);
|
||||||
|
}
|
||||||
|
writer.EndArray();
|
||||||
|
writer.EndObject();
|
||||||
|
}
|
||||||
|
writer.EndArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
inline std::string RuntimeStateToJson(const RuntimeStateJsonInput& input)
|
||||||
|
{
|
||||||
|
JsonWriter writer;
|
||||||
|
writer.BeginObject();
|
||||||
|
|
||||||
|
writer.Key("app");
|
||||||
|
writer.BeginObject();
|
||||||
|
writer.KeyUInt("serverPort", input.serverPort);
|
||||||
|
writer.KeyUInt("oscPort", input.config.oscPort);
|
||||||
|
writer.KeyString("oscBindAddress", input.config.oscBindAddress);
|
||||||
|
writer.KeyDouble("oscSmoothing", input.config.oscSmoothing);
|
||||||
|
writer.KeyBool("autoReload", input.config.autoReload);
|
||||||
|
writer.KeyUInt("maxTemporalHistoryFrames", static_cast<uint64_t>(input.config.maxTemporalHistoryFrames));
|
||||||
|
writer.KeyDouble("previewFps", input.config.previewFps);
|
||||||
|
writer.KeyBool("enableExternalKeying", input.config.deckLink.externalKeyingEnabled);
|
||||||
|
writer.KeyString("inputVideoFormat", input.config.inputVideoFormat);
|
||||||
|
writer.KeyString("inputFrameRate", input.config.inputFrameRate);
|
||||||
|
writer.KeyString("outputVideoFormat", input.config.outputVideoFormat);
|
||||||
|
writer.KeyString("outputFrameRate", input.config.outputFrameRate);
|
||||||
|
writer.EndObject();
|
||||||
|
|
||||||
|
writer.Key("runtime");
|
||||||
|
writer.BeginObject();
|
||||||
|
writer.KeyUInt("layerCount", static_cast<uint64_t>(input.runtimeLayers.displayLayers.size()));
|
||||||
|
writer.KeyBool("compileSucceeded", input.runtimeLayers.compileSucceeded);
|
||||||
|
writer.KeyString("compileMessage", input.runtimeLayers.compileMessage);
|
||||||
|
writer.EndObject();
|
||||||
|
|
||||||
|
writer.Key("video");
|
||||||
|
writer.BeginObject();
|
||||||
|
unsigned outputWidth = 0;
|
||||||
|
unsigned outputHeight = 0;
|
||||||
|
OutputDimensions(input, outputWidth, outputHeight);
|
||||||
|
writer.KeyBool("hasSignal", input.videoOutputEnabled);
|
||||||
|
writer.KeyUInt("width", outputWidth);
|
||||||
|
writer.KeyUInt("height", outputHeight);
|
||||||
|
writer.KeyString("modeName", input.config.outputVideoFormat + " output-only");
|
||||||
|
writer.EndObject();
|
||||||
|
|
||||||
|
writer.Key("decklink");
|
||||||
|
WriteVideoIoStatusJson(writer, input);
|
||||||
|
writer.Key("videoIO");
|
||||||
|
WriteVideoIoStatusJson(writer, input);
|
||||||
|
|
||||||
|
writer.Key("performance");
|
||||||
|
writer.BeginObject();
|
||||||
|
writer.KeyDouble("frameBudgetMs", FrameDurationMillisecondsFromRateString(input.config.outputFrameRate));
|
||||||
|
writer.KeyDouble("renderMs", input.telemetry.renderFrameMilliseconds);
|
||||||
|
writer.KeyNull("smoothedRenderMs");
|
||||||
|
writer.KeyDouble("budgetUsedPercent", input.telemetry.renderFrameBudgetUsedPercent);
|
||||||
|
writer.KeyNull("completionIntervalMs");
|
||||||
|
writer.KeyNull("smoothedCompletionIntervalMs");
|
||||||
|
writer.KeyNull("maxCompletionIntervalMs");
|
||||||
|
writer.KeyUInt("lateFrameCount", input.telemetry.displayedLate);
|
||||||
|
writer.KeyUInt("droppedFrameCount", input.telemetry.dropped);
|
||||||
|
writer.KeyNull("flushedFrameCount");
|
||||||
|
writer.Key("cadence");
|
||||||
|
WriteCadenceTelemetryJson(writer, input.telemetry);
|
||||||
|
writer.EndObject();
|
||||||
|
|
||||||
|
writer.KeyNull("backendPlayout");
|
||||||
|
writer.KeyNull("runtimeEvents");
|
||||||
|
writer.Key("shaders");
|
||||||
|
writer.BeginArray();
|
||||||
|
for (const SupportedShaderSummary& shader : input.shaderCatalog.Shaders())
|
||||||
|
{
|
||||||
|
writer.BeginObject();
|
||||||
|
writer.KeyString("id", shader.id);
|
||||||
|
writer.KeyString("name", shader.name);
|
||||||
|
writer.KeyString("description", shader.description);
|
||||||
|
writer.KeyString("category", shader.category);
|
||||||
|
writer.KeyBool("available", true);
|
||||||
|
writer.KeyNull("error");
|
||||||
|
writer.EndObject();
|
||||||
|
}
|
||||||
|
writer.EndArray();
|
||||||
|
writer.Key("stackPresets");
|
||||||
|
writer.BeginArray();
|
||||||
|
writer.EndArray();
|
||||||
|
writer.Key("layers");
|
||||||
|
WriteLayersJson(writer, input);
|
||||||
|
|
||||||
|
writer.EndObject();
|
||||||
|
return writer.StringValue();
|
||||||
|
}
|
||||||
|
}
|
||||||
293
apps/RenderCadenceCompositor/control/http/HttpControlServer.cpp
Normal file
293
apps/RenderCadenceCompositor/control/http/HttpControlServer.cpp
Normal file
@@ -0,0 +1,293 @@
|
|||||||
|
#include "HttpControlServer.h"
|
||||||
|
|
||||||
|
#include "../logging/Logger.h"
|
||||||
|
|
||||||
|
#include <ws2tcpip.h>
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <sstream>
|
||||||
|
|
||||||
|
namespace RenderCadenceCompositor
|
||||||
|
{
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
UniqueSocket::UniqueSocket(SOCKET socket) :
|
||||||
|
mSocket(socket)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
UniqueSocket::~UniqueSocket()
|
||||||
|
{
|
||||||
|
reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
UniqueSocket::UniqueSocket(UniqueSocket&& other) noexcept :
|
||||||
|
mSocket(other.release())
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
UniqueSocket& UniqueSocket::operator=(UniqueSocket&& other) noexcept
|
||||||
|
{
|
||||||
|
if (this != &other)
|
||||||
|
reset(other.release());
|
||||||
|
return *this;
|
||||||
|
}
|
||||||
|
|
||||||
|
SOCKET UniqueSocket::release()
|
||||||
|
{
|
||||||
|
const SOCKET socket = mSocket;
|
||||||
|
mSocket = INVALID_SOCKET;
|
||||||
|
return socket;
|
||||||
|
}
|
||||||
|
|
||||||
|
void UniqueSocket::reset(SOCKET socket)
|
||||||
|
{
|
||||||
|
if (valid())
|
||||||
|
closesocket(mSocket);
|
||||||
|
mSocket = socket;
|
||||||
|
}
|
||||||
|
|
||||||
|
HttpControlServer::~HttpControlServer()
|
||||||
|
{
|
||||||
|
Stop();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool HttpControlServer::Start(
|
||||||
|
const std::filesystem::path& uiRoot,
|
||||||
|
const std::filesystem::path& docsRoot,
|
||||||
|
HttpControlServerConfig config,
|
||||||
|
HttpControlServerCallbacks callbacks,
|
||||||
|
std::string& error)
|
||||||
|
{
|
||||||
|
Stop();
|
||||||
|
|
||||||
|
if (!InitializeWinsock(error))
|
||||||
|
return false;
|
||||||
|
mWinsockStarted = true;
|
||||||
|
|
||||||
|
mUiRoot = uiRoot;
|
||||||
|
mDocsRoot = docsRoot;
|
||||||
|
mConfig = config;
|
||||||
|
mCallbacks = std::move(callbacks);
|
||||||
|
|
||||||
|
mListenSocket.reset(socket(AF_INET, SOCK_STREAM, IPPROTO_TCP));
|
||||||
|
if (!mListenSocket.valid())
|
||||||
|
{
|
||||||
|
error = "Could not create HTTP control server socket.";
|
||||||
|
Stop();
|
||||||
|
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 < mConfig.portSearchCount; ++offset)
|
||||||
|
{
|
||||||
|
address.sin_port = htons(static_cast<u_short>(mConfig.preferredPort + offset));
|
||||||
|
if (bind(mListenSocket.get(), reinterpret_cast<sockaddr*>(&address), sizeof(address)) == 0)
|
||||||
|
{
|
||||||
|
mPort = static_cast<unsigned short>(mConfig.preferredPort + offset);
|
||||||
|
bound = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!bound)
|
||||||
|
{
|
||||||
|
error = "Could not bind HTTP control server to loopback.";
|
||||||
|
Stop();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (listen(mListenSocket.get(), SOMAXCONN) != 0)
|
||||||
|
{
|
||||||
|
error = "Could not listen on HTTP control server socket.";
|
||||||
|
Stop();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
mRunning.store(true, std::memory_order_release);
|
||||||
|
mThread = std::thread([this]() { ThreadMain(); });
|
||||||
|
Log("http", "HTTP control server listening on http://127.0.0.1:" + std::to_string(mPort));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void HttpControlServer::Stop()
|
||||||
|
{
|
||||||
|
mRunning.store(false, std::memory_order_release);
|
||||||
|
mListenSocket.reset();
|
||||||
|
|
||||||
|
if (mThread.joinable())
|
||||||
|
mThread.join();
|
||||||
|
|
||||||
|
std::vector<std::thread> clientThreads;
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(mClientThreadsMutex);
|
||||||
|
clientThreads.swap(mClientThreads);
|
||||||
|
for (std::thread& thread : mFinishedClientThreads)
|
||||||
|
clientThreads.push_back(std::move(thread));
|
||||||
|
mFinishedClientThreads.clear();
|
||||||
|
}
|
||||||
|
for (std::thread& thread : clientThreads)
|
||||||
|
{
|
||||||
|
if (thread.joinable())
|
||||||
|
thread.join();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mWinsockStarted)
|
||||||
|
{
|
||||||
|
WSACleanup();
|
||||||
|
mWinsockStarted = false;
|
||||||
|
}
|
||||||
|
mPort = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
HttpControlServer::HttpResponse HttpControlServer::RouteRequestForTest(const HttpRequest& request) const
|
||||||
|
{
|
||||||
|
return RouteRequest(request);
|
||||||
|
}
|
||||||
|
|
||||||
|
void HttpControlServer::SetCallbacksForTest(HttpControlServerCallbacks callbacks)
|
||||||
|
{
|
||||||
|
mCallbacks = std::move(callbacks);
|
||||||
|
}
|
||||||
|
|
||||||
|
void HttpControlServer::SetRootsForTest(const std::filesystem::path& uiRoot, const std::filesystem::path& docsRoot)
|
||||||
|
{
|
||||||
|
mUiRoot = uiRoot;
|
||||||
|
mDocsRoot = docsRoot;
|
||||||
|
}
|
||||||
|
|
||||||
|
void HttpControlServer::ThreadMain()
|
||||||
|
{
|
||||||
|
while (mRunning.load(std::memory_order_acquire))
|
||||||
|
{
|
||||||
|
JoinFinishedClientThreads();
|
||||||
|
TryAcceptClient();
|
||||||
|
std::this_thread::sleep_for(mConfig.idleSleep);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool HttpControlServer::TryAcceptClient()
|
||||||
|
{
|
||||||
|
sockaddr_in clientAddress = {};
|
||||||
|
int addressSize = sizeof(clientAddress);
|
||||||
|
UniqueSocket clientSocket(accept(mListenSocket.get(), reinterpret_cast<sockaddr*>(&clientAddress), &addressSize));
|
||||||
|
if (!clientSocket.valid())
|
||||||
|
return false;
|
||||||
|
|
||||||
|
return HandleClient(std::move(clientSocket));
|
||||||
|
}
|
||||||
|
|
||||||
|
bool HttpControlServer::HandleClient(UniqueSocket clientSocket)
|
||||||
|
{
|
||||||
|
char buffer[16384];
|
||||||
|
const int received = recv(clientSocket.get(), buffer, sizeof(buffer), 0);
|
||||||
|
if (received <= 0)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
HttpRequest request;
|
||||||
|
if (!ParseHttpRequest(std::string(buffer, buffer + received), request))
|
||||||
|
return SendResponse(clientSocket.get(), TextResponse("400 Bad Request", "Bad Request"));
|
||||||
|
|
||||||
|
if (request.path == "/ws")
|
||||||
|
return HandleWebSocketClient(std::move(clientSocket), request);
|
||||||
|
|
||||||
|
return SendResponse(clientSocket.get(), RouteRequest(request));
|
||||||
|
}
|
||||||
|
|
||||||
|
bool HttpControlServer::SendResponse(SOCKET clientSocket, const HttpResponse& response) const
|
||||||
|
{
|
||||||
|
std::ostringstream stream;
|
||||||
|
stream << "HTTP/1.1 " << response.status << "\r\n"
|
||||||
|
<< "Content-Type: " << response.contentType << "\r\n"
|
||||||
|
<< "Content-Length: " << response.body.size() << "\r\n"
|
||||||
|
<< "Access-Control-Allow-Origin: *\r\n"
|
||||||
|
<< "Connection: close\r\n\r\n"
|
||||||
|
<< response.body;
|
||||||
|
|
||||||
|
const std::string payload = stream.str();
|
||||||
|
return send(clientSocket, payload.c_str(), static_cast<int>(payload.size()), 0) == static_cast<int>(payload.size());
|
||||||
|
}
|
||||||
|
|
||||||
|
HttpControlServer::HttpResponse HttpControlServer::RouteRequest(const HttpRequest& request) const
|
||||||
|
{
|
||||||
|
if (request.method == "GET")
|
||||||
|
return ServeGet(request);
|
||||||
|
if (request.method == "POST")
|
||||||
|
return ServePost(request);
|
||||||
|
if (request.method == "OPTIONS")
|
||||||
|
return TextResponse("204 No Content", std::string());
|
||||||
|
return TextResponse("404 Not Found", "Not Found");
|
||||||
|
}
|
||||||
|
|
||||||
|
bool HttpControlServer::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 queryStart = request.path.find('?');
|
||||||
|
if (queryStart != std::string::npos)
|
||||||
|
request.path = request.path.substr(0, queryStart);
|
||||||
|
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
135
apps/RenderCadenceCompositor/control/http/HttpControlServer.h
Normal file
135
apps/RenderCadenceCompositor/control/http/HttpControlServer.h
Normal file
@@ -0,0 +1,135 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "ControlActionResult.h"
|
||||||
|
|
||||||
|
#include <winsock2.h>
|
||||||
|
|
||||||
|
#include <atomic>
|
||||||
|
#include <chrono>
|
||||||
|
#include <filesystem>
|
||||||
|
#include <functional>
|
||||||
|
#include <map>
|
||||||
|
#include <mutex>
|
||||||
|
#include <string>
|
||||||
|
#include <thread>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
namespace RenderCadenceCompositor
|
||||||
|
{
|
||||||
|
struct HttpControlServerConfig
|
||||||
|
{
|
||||||
|
unsigned short preferredPort = 8080;
|
||||||
|
unsigned short portSearchCount = 20;
|
||||||
|
std::chrono::milliseconds idleSleep = std::chrono::milliseconds(10);
|
||||||
|
};
|
||||||
|
|
||||||
|
struct HttpControlServerCallbacks
|
||||||
|
{
|
||||||
|
std::function<std::string()> getStateJson;
|
||||||
|
std::function<ControlActionResult(const std::string&)> addLayer;
|
||||||
|
std::function<ControlActionResult(const std::string&)> removeLayer;
|
||||||
|
std::function<ControlActionResult(const std::string&, const std::string&)> executePost;
|
||||||
|
};
|
||||||
|
|
||||||
|
class UniqueSocket
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
explicit UniqueSocket(SOCKET socket = INVALID_SOCKET);
|
||||||
|
~UniqueSocket();
|
||||||
|
|
||||||
|
UniqueSocket(const UniqueSocket&) = delete;
|
||||||
|
UniqueSocket& operator=(const UniqueSocket&) = delete;
|
||||||
|
|
||||||
|
UniqueSocket(UniqueSocket&& other) noexcept;
|
||||||
|
UniqueSocket& operator=(UniqueSocket&& other) noexcept;
|
||||||
|
|
||||||
|
SOCKET get() const { return mSocket; }
|
||||||
|
bool valid() const { return mSocket != INVALID_SOCKET; }
|
||||||
|
SOCKET release();
|
||||||
|
void reset(SOCKET socket = INVALID_SOCKET);
|
||||||
|
|
||||||
|
private:
|
||||||
|
SOCKET mSocket = INVALID_SOCKET;
|
||||||
|
};
|
||||||
|
|
||||||
|
class HttpControlServer
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
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;
|
||||||
|
};
|
||||||
|
|
||||||
|
HttpControlServer() = default;
|
||||||
|
~HttpControlServer();
|
||||||
|
|
||||||
|
HttpControlServer(const HttpControlServer&) = delete;
|
||||||
|
HttpControlServer& operator=(const HttpControlServer&) = delete;
|
||||||
|
|
||||||
|
bool Start(
|
||||||
|
const std::filesystem::path& uiRoot,
|
||||||
|
const std::filesystem::path& docsRoot,
|
||||||
|
HttpControlServerConfig config,
|
||||||
|
HttpControlServerCallbacks callbacks,
|
||||||
|
std::string& error);
|
||||||
|
void Stop();
|
||||||
|
|
||||||
|
bool IsRunning() const { return mRunning.load(std::memory_order_acquire); }
|
||||||
|
unsigned short Port() const { return mPort; }
|
||||||
|
|
||||||
|
void SetCallbacksForTest(HttpControlServerCallbacks callbacks);
|
||||||
|
void SetRootsForTest(const std::filesystem::path& uiRoot, const std::filesystem::path& docsRoot);
|
||||||
|
HttpResponse RouteRequestForTest(const HttpRequest& request) const;
|
||||||
|
|
||||||
|
static bool ParseHttpRequest(const std::string& rawRequest, HttpRequest& request);
|
||||||
|
static std::string WebSocketAcceptKey(const std::string& clientKey);
|
||||||
|
|
||||||
|
private:
|
||||||
|
void ThreadMain();
|
||||||
|
bool TryAcceptClient();
|
||||||
|
bool HandleClient(UniqueSocket clientSocket);
|
||||||
|
bool HandleWebSocketClient(UniqueSocket clientSocket, const HttpRequest& request);
|
||||||
|
void WebSocketClientMain(UniqueSocket clientSocket);
|
||||||
|
void JoinFinishedClientThreads();
|
||||||
|
bool SendResponse(SOCKET clientSocket, const HttpResponse& response) const;
|
||||||
|
HttpResponse RouteRequest(const HttpRequest& request) const;
|
||||||
|
HttpResponse ServeGet(const HttpRequest& request) const;
|
||||||
|
HttpResponse ServePost(const HttpRequest& request) const;
|
||||||
|
HttpResponse ServeOpenApiSpec() const;
|
||||||
|
HttpResponse ServeSwaggerDocs() const;
|
||||||
|
HttpResponse ServeUiAsset(const std::string& relativePath) const;
|
||||||
|
std::string LoadTextFile(const std::filesystem::path& path) const;
|
||||||
|
|
||||||
|
static HttpResponse JsonResponse(const std::string& status, const std::string& body);
|
||||||
|
static HttpResponse TextResponse(const std::string& status, const std::string& body);
|
||||||
|
static HttpResponse HtmlResponse(const std::string& status, const std::string& body);
|
||||||
|
static std::string ActionResponse(bool ok, const std::string& error = std::string());
|
||||||
|
static bool SendWebSocketText(SOCKET clientSocket, const std::string& text);
|
||||||
|
static std::string GuessContentType(const std::filesystem::path& path);
|
||||||
|
static bool IsSafeRelativePath(const std::filesystem::path& path);
|
||||||
|
static std::string ToLower(std::string text);
|
||||||
|
|
||||||
|
std::filesystem::path mUiRoot;
|
||||||
|
std::filesystem::path mDocsRoot;
|
||||||
|
HttpControlServerConfig mConfig;
|
||||||
|
HttpControlServerCallbacks mCallbacks;
|
||||||
|
UniqueSocket mListenSocket;
|
||||||
|
std::thread mThread;
|
||||||
|
std::mutex mClientThreadsMutex;
|
||||||
|
std::vector<std::thread> mClientThreads;
|
||||||
|
std::vector<std::thread> mFinishedClientThreads;
|
||||||
|
std::atomic<bool> mRunning{ false };
|
||||||
|
unsigned short mPort = 0;
|
||||||
|
bool mWinsockStarted = false;
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,203 @@
|
|||||||
|
#include "HttpControlServer.h"
|
||||||
|
|
||||||
|
#include "../json/JsonWriter.h"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cctype>
|
||||||
|
#include <fstream>
|
||||||
|
#include <sstream>
|
||||||
|
|
||||||
|
namespace RenderCadenceCompositor
|
||||||
|
{
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
bool IsKnownPostEndpoint(const std::string& path)
|
||||||
|
{
|
||||||
|
return path == "/api/layers/add"
|
||||||
|
|| path == "/api/layers/remove"
|
||||||
|
|| path == "/api/layers/move"
|
||||||
|
|| path == "/api/layers/reorder"
|
||||||
|
|| path == "/api/layers/set-bypass"
|
||||||
|
|| path == "/api/layers/set-shader"
|
||||||
|
|| path == "/api/layers/update-parameter"
|
||||||
|
|| path == "/api/layers/reset-parameters"
|
||||||
|
|| path == "/api/stack-presets/save"
|
||||||
|
|| path == "/api/stack-presets/load"
|
||||||
|
|| path == "/api/reload"
|
||||||
|
|| path == "/api/screenshot";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
HttpControlServer::HttpResponse HttpControlServer::ServeGet(const HttpRequest& request) const
|
||||||
|
{
|
||||||
|
if (request.path == "/api/state")
|
||||||
|
return JsonResponse("200 OK", 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();
|
||||||
|
if (request.path == "/" || request.path == "/index.html")
|
||||||
|
return ServeUiAsset("index.html");
|
||||||
|
if (request.path.rfind("/assets/", 0) == 0)
|
||||||
|
return ServeUiAsset(request.path.substr(1));
|
||||||
|
if (request.path.size() > 1)
|
||||||
|
{
|
||||||
|
const HttpResponse asset = ServeUiAsset(request.path.substr(1));
|
||||||
|
if (asset.status != "404 Not Found")
|
||||||
|
return asset;
|
||||||
|
}
|
||||||
|
return ServeUiAsset("index.html");
|
||||||
|
}
|
||||||
|
|
||||||
|
HttpControlServer::HttpResponse HttpControlServer::ServePost(const HttpRequest& request) const
|
||||||
|
{
|
||||||
|
if (!IsKnownPostEndpoint(request.path))
|
||||||
|
return TextResponse("404 Not Found", "Not Found");
|
||||||
|
|
||||||
|
if (mCallbacks.executePost)
|
||||||
|
{
|
||||||
|
const ControlActionResult result = mCallbacks.executePost(request.path, request.body);
|
||||||
|
return JsonResponse(result.ok ? "200 OK" : "400 Bad Request", ActionResponse(result.ok, result.error));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (request.path == "/api/layers/add" && mCallbacks.addLayer)
|
||||||
|
{
|
||||||
|
const ControlActionResult result = mCallbacks.addLayer(request.body);
|
||||||
|
return JsonResponse(result.ok ? "200 OK" : "400 Bad Request", ActionResponse(result.ok, result.error));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (request.path == "/api/layers/remove" && mCallbacks.removeLayer)
|
||||||
|
{
|
||||||
|
const ControlActionResult result = mCallbacks.removeLayer(request.body);
|
||||||
|
return JsonResponse(result.ok ? "200 OK" : "400 Bad Request", ActionResponse(result.ok, result.error));
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
"400 Bad Request",
|
||||||
|
"application/json",
|
||||||
|
ActionResponse(false, "Endpoint is not implemented in RenderCadenceCompositor yet.")
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
HttpControlServer::HttpResponse HttpControlServer::ServeOpenApiSpec() const
|
||||||
|
{
|
||||||
|
const std::filesystem::path path = mDocsRoot / "openapi.yaml";
|
||||||
|
const std::string body = LoadTextFile(path);
|
||||||
|
return body.empty()
|
||||||
|
? TextResponse("404 Not Found", "OpenAPI spec not found")
|
||||||
|
: HttpResponse{ "200 OK", GuessContentType(path), body };
|
||||||
|
}
|
||||||
|
|
||||||
|
HttpControlServer::HttpResponse HttpControlServer::ServeSwaggerDocs() const
|
||||||
|
{
|
||||||
|
std::ostringstream html;
|
||||||
|
html << "<!doctype html>\n"
|
||||||
|
<< "<html><head><meta charset=\"utf-8\"><title>Video Shader Toys API Docs</title>\n"
|
||||||
|
<< "<link rel=\"stylesheet\" href=\"https://unpkg.com/swagger-ui-dist@5/swagger-ui.css\"></head>\n"
|
||||||
|
<< "<body><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></html>\n";
|
||||||
|
return { "200 OK", "text/html", html.str() };
|
||||||
|
}
|
||||||
|
|
||||||
|
HttpControlServer::HttpResponse HttpControlServer::ServeUiAsset(const std::string& relativePath) const
|
||||||
|
{
|
||||||
|
if (mUiRoot.empty())
|
||||||
|
return TextResponse("404 Not Found", "UI root is not configured");
|
||||||
|
|
||||||
|
const std::filesystem::path sanitizedPath = std::filesystem::path(relativePath).lexically_normal();
|
||||||
|
if (!IsSafeRelativePath(sanitizedPath))
|
||||||
|
return TextResponse("404 Not Found", "Not Found");
|
||||||
|
|
||||||
|
const std::filesystem::path path = mUiRoot / sanitizedPath;
|
||||||
|
const std::string body = LoadTextFile(path);
|
||||||
|
if (body.empty())
|
||||||
|
return TextResponse("404 Not Found", "Not Found");
|
||||||
|
return { "200 OK", GuessContentType(path), body };
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string HttpControlServer::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();
|
||||||
|
}
|
||||||
|
|
||||||
|
HttpControlServer::HttpResponse HttpControlServer::JsonResponse(const std::string& status, const std::string& body)
|
||||||
|
{
|
||||||
|
return { status, "application/json", body };
|
||||||
|
}
|
||||||
|
|
||||||
|
HttpControlServer::HttpResponse HttpControlServer::TextResponse(const std::string& status, const std::string& body)
|
||||||
|
{
|
||||||
|
return { status, "text/plain", body };
|
||||||
|
}
|
||||||
|
|
||||||
|
HttpControlServer::HttpResponse HttpControlServer::HtmlResponse(const std::string& status, const std::string& body)
|
||||||
|
{
|
||||||
|
return { status, "text/html", body };
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string HttpControlServer::ActionResponse(bool ok, const std::string& error)
|
||||||
|
{
|
||||||
|
JsonWriter writer;
|
||||||
|
writer.BeginObject();
|
||||||
|
writer.KeyBool("ok", ok);
|
||||||
|
if (!error.empty())
|
||||||
|
writer.KeyString("error", error);
|
||||||
|
writer.EndObject();
|
||||||
|
return writer.StringValue();
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string HttpControlServer::GuessContentType(const std::filesystem::path& path)
|
||||||
|
{
|
||||||
|
const std::string extension = ToLower(path.extension().string());
|
||||||
|
if (extension == ".yaml" || extension == ".yml")
|
||||||
|
return "application/yaml";
|
||||||
|
if (extension == ".json")
|
||||||
|
return "application/json";
|
||||||
|
if (extension == ".js" || extension == ".mjs")
|
||||||
|
return "text/javascript";
|
||||||
|
if (extension == ".css")
|
||||||
|
return "text/css";
|
||||||
|
if (extension == ".html" || extension == ".htm")
|
||||||
|
return "text/html";
|
||||||
|
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";
|
||||||
|
return "text/plain";
|
||||||
|
}
|
||||||
|
|
||||||
|
bool HttpControlServer::IsSafeRelativePath(const std::filesystem::path& path)
|
||||||
|
{
|
||||||
|
if (path.empty() || path.is_absolute())
|
||||||
|
return false;
|
||||||
|
|
||||||
|
for (const std::filesystem::path& part : path)
|
||||||
|
{
|
||||||
|
if (part == "..")
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string HttpControlServer::ToLower(std::string text)
|
||||||
|
{
|
||||||
|
std::transform(text.begin(), text.end(), text.begin(), [](unsigned char character) {
|
||||||
|
return static_cast<char>(std::tolower(character));
|
||||||
|
});
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,248 @@
|
|||||||
|
#include "HttpControlServer.h"
|
||||||
|
|
||||||
|
#include <array>
|
||||||
|
#include <cstdint>
|
||||||
|
#include <sstream>
|
||||||
|
#include <utility>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
namespace RenderCadenceCompositor
|
||||||
|
{
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
std::array<uint8_t, 20> Sha1(const std::string& input)
|
||||||
|
{
|
||||||
|
auto leftRotate = [](uint32_t value, uint32_t bits) {
|
||||||
|
return (value << bits) | (value >> (32U - bits));
|
||||||
|
};
|
||||||
|
|
||||||
|
std::vector<uint8_t> data(input.begin(), input.end());
|
||||||
|
const uint64_t bitLength = static_cast<uint64_t>(data.size()) * 8ULL;
|
||||||
|
data.push_back(0x80);
|
||||||
|
while ((data.size() % 64) != 56)
|
||||||
|
data.push_back(0);
|
||||||
|
for (int shift = 56; shift >= 0; shift -= 8)
|
||||||
|
data.push_back(static_cast<uint8_t>((bitLength >> shift) & 0xff));
|
||||||
|
|
||||||
|
uint32_t h0 = 0x67452301;
|
||||||
|
uint32_t h1 = 0xefcdab89;
|
||||||
|
uint32_t h2 = 0x98badcfe;
|
||||||
|
uint32_t h3 = 0x10325476;
|
||||||
|
uint32_t h4 = 0xc3d2e1f0;
|
||||||
|
|
||||||
|
for (std::size_t offset = 0; offset < data.size(); offset += 64)
|
||||||
|
{
|
||||||
|
uint32_t words[80] = {};
|
||||||
|
for (std::size_t i = 0; i < 16; ++i)
|
||||||
|
{
|
||||||
|
const std::size_t index = offset + i * 4;
|
||||||
|
words[i] = (static_cast<uint32_t>(data[index]) << 24)
|
||||||
|
| (static_cast<uint32_t>(data[index + 1]) << 16)
|
||||||
|
| (static_cast<uint32_t>(data[index + 2]) << 8)
|
||||||
|
| static_cast<uint32_t>(data[index + 3]);
|
||||||
|
}
|
||||||
|
for (std::size_t i = 16; i < 80; ++i)
|
||||||
|
words[i] = leftRotate(words[i - 3] ^ words[i - 8] ^ words[i - 14] ^ words[i - 16], 1);
|
||||||
|
|
||||||
|
uint32_t a = h0;
|
||||||
|
uint32_t b = h1;
|
||||||
|
uint32_t c = h2;
|
||||||
|
uint32_t d = h3;
|
||||||
|
uint32_t e = h4;
|
||||||
|
|
||||||
|
for (std::size_t i = 0; i < 80; ++i)
|
||||||
|
{
|
||||||
|
uint32_t f = 0;
|
||||||
|
uint32_t k = 0;
|
||||||
|
if (i < 20)
|
||||||
|
{
|
||||||
|
f = (b & c) | ((~b) & d);
|
||||||
|
k = 0x5a827999;
|
||||||
|
}
|
||||||
|
else if (i < 40)
|
||||||
|
{
|
||||||
|
f = b ^ c ^ d;
|
||||||
|
k = 0x6ed9eba1;
|
||||||
|
}
|
||||||
|
else if (i < 60)
|
||||||
|
{
|
||||||
|
f = (b & c) | (b & d) | (c & d);
|
||||||
|
k = 0x8f1bbcdc;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
f = b ^ c ^ d;
|
||||||
|
k = 0xca62c1d6;
|
||||||
|
}
|
||||||
|
|
||||||
|
const uint32_t temp = leftRotate(a, 5) + f + e + k + words[i];
|
||||||
|
e = d;
|
||||||
|
d = c;
|
||||||
|
c = leftRotate(b, 30);
|
||||||
|
b = a;
|
||||||
|
a = temp;
|
||||||
|
}
|
||||||
|
|
||||||
|
h0 += a;
|
||||||
|
h1 += b;
|
||||||
|
h2 += c;
|
||||||
|
h3 += d;
|
||||||
|
h4 += e;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::array<uint8_t, 20> digest = {};
|
||||||
|
const uint32_t parts[] = { h0, h1, h2, h3, h4 };
|
||||||
|
for (std::size_t i = 0; i < 5; ++i)
|
||||||
|
{
|
||||||
|
digest[i * 4] = static_cast<uint8_t>((parts[i] >> 24) & 0xff);
|
||||||
|
digest[i * 4 + 1] = static_cast<uint8_t>((parts[i] >> 16) & 0xff);
|
||||||
|
digest[i * 4 + 2] = static_cast<uint8_t>((parts[i] >> 8) & 0xff);
|
||||||
|
digest[i * 4 + 3] = static_cast<uint8_t>(parts[i] & 0xff);
|
||||||
|
}
|
||||||
|
return digest;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string Base64Encode(const uint8_t* data, std::size_t size)
|
||||||
|
{
|
||||||
|
static constexpr char kAlphabet[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
||||||
|
std::string output;
|
||||||
|
output.reserve(((size + 2) / 3) * 4);
|
||||||
|
for (std::size_t i = 0; i < size; i += 3)
|
||||||
|
{
|
||||||
|
const uint32_t a = data[i];
|
||||||
|
const uint32_t b = i + 1 < size ? data[i + 1] : 0;
|
||||||
|
const uint32_t c = i + 2 < size ? data[i + 2] : 0;
|
||||||
|
const uint32_t triple = (a << 16) | (b << 8) | c;
|
||||||
|
output.push_back(kAlphabet[(triple >> 18) & 0x3f]);
|
||||||
|
output.push_back(kAlphabet[(triple >> 12) & 0x3f]);
|
||||||
|
output.push_back(i + 1 < size ? kAlphabet[(triple >> 6) & 0x3f] : '=');
|
||||||
|
output.push_back(i + 2 < size ? kAlphabet[triple & 0x3f] : '=');
|
||||||
|
}
|
||||||
|
return output;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool HttpControlServer::HandleWebSocketClient(UniqueSocket clientSocket, const HttpRequest& request)
|
||||||
|
{
|
||||||
|
const auto keyIt = request.headers.find("sec-websocket-key");
|
||||||
|
if (keyIt == request.headers.end() || keyIt->second.empty())
|
||||||
|
return SendResponse(clientSocket.get(), TextResponse("400 Bad Request", "Missing WebSocket key"));
|
||||||
|
|
||||||
|
std::ostringstream stream;
|
||||||
|
stream << "HTTP/1.1 101 Switching Protocols\r\n"
|
||||||
|
<< "Upgrade: websocket\r\n"
|
||||||
|
<< "Connection: Upgrade\r\n"
|
||||||
|
<< "Sec-WebSocket-Accept: " << WebSocketAcceptKey(keyIt->second) << "\r\n\r\n";
|
||||||
|
const std::string response = stream.str();
|
||||||
|
if (send(clientSocket.get(), response.c_str(), static_cast<int>(response.size()), 0) != static_cast<int>(response.size()))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
u_long nonBlocking = 1;
|
||||||
|
ioctlsocket(clientSocket.get(), FIONBIO, &nonBlocking);
|
||||||
|
|
||||||
|
std::thread thread([this, socket = std::move(clientSocket)]() mutable {
|
||||||
|
WebSocketClientMain(std::move(socket));
|
||||||
|
});
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(mClientThreadsMutex);
|
||||||
|
mClientThreads.push_back(std::move(thread));
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void HttpControlServer::WebSocketClientMain(UniqueSocket clientSocket)
|
||||||
|
{
|
||||||
|
std::string previousState;
|
||||||
|
while (mRunning.load(std::memory_order_acquire))
|
||||||
|
{
|
||||||
|
const std::string state = mCallbacks.getStateJson ? mCallbacks.getStateJson() : "{}";
|
||||||
|
if (state != previousState)
|
||||||
|
{
|
||||||
|
if (!SendWebSocketText(clientSocket.get(), state))
|
||||||
|
break;
|
||||||
|
previousState = state;
|
||||||
|
}
|
||||||
|
std::this_thread::sleep_for(std::chrono::milliseconds(250));
|
||||||
|
}
|
||||||
|
|
||||||
|
std::lock_guard<std::mutex> lock(mClientThreadsMutex);
|
||||||
|
const std::thread::id currentId = std::this_thread::get_id();
|
||||||
|
for (auto it = mClientThreads.begin(); it != mClientThreads.end(); ++it)
|
||||||
|
{
|
||||||
|
if (it->get_id() != currentId)
|
||||||
|
continue;
|
||||||
|
mFinishedClientThreads.push_back(std::move(*it));
|
||||||
|
mClientThreads.erase(it);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void HttpControlServer::JoinFinishedClientThreads()
|
||||||
|
{
|
||||||
|
std::vector<std::thread> finished;
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(mClientThreadsMutex);
|
||||||
|
finished.swap(mFinishedClientThreads);
|
||||||
|
}
|
||||||
|
for (std::thread& thread : finished)
|
||||||
|
{
|
||||||
|
if (thread.joinable())
|
||||||
|
thread.join();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool HttpControlServer::SendWebSocketText(SOCKET clientSocket, const std::string& text)
|
||||||
|
{
|
||||||
|
if (clientSocket == INVALID_SOCKET)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
std::vector<unsigned char> frame;
|
||||||
|
frame.reserve(text.size() + 16);
|
||||||
|
frame.push_back(0x81);
|
||||||
|
if (text.size() <= 125)
|
||||||
|
{
|
||||||
|
frame.push_back(static_cast<unsigned char>(text.size()));
|
||||||
|
}
|
||||||
|
else if (text.size() <= 0xffff)
|
||||||
|
{
|
||||||
|
frame.push_back(126);
|
||||||
|
frame.push_back(static_cast<unsigned char>((text.size() >> 8) & 0xff));
|
||||||
|
frame.push_back(static_cast<unsigned char>(text.size() & 0xff));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
frame.push_back(127);
|
||||||
|
const uint64_t length = static_cast<uint64_t>(text.size());
|
||||||
|
for (int shift = 56; shift >= 0; shift -= 8)
|
||||||
|
frame.push_back(static_cast<unsigned char>((length >> shift) & 0xff));
|
||||||
|
}
|
||||||
|
frame.insert(frame.end(), text.begin(), text.end());
|
||||||
|
|
||||||
|
const char* data = reinterpret_cast<const char*>(frame.data());
|
||||||
|
int remaining = static_cast<int>(frame.size());
|
||||||
|
while (remaining > 0)
|
||||||
|
{
|
||||||
|
const int sent = send(clientSocket, data, remaining, 0);
|
||||||
|
if (sent <= 0)
|
||||||
|
{
|
||||||
|
const int error = WSAGetLastError();
|
||||||
|
if (error == WSAEWOULDBLOCK)
|
||||||
|
{
|
||||||
|
std::this_thread::sleep_for(std::chrono::milliseconds(2));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
data += sent;
|
||||||
|
remaining -= sent;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string HttpControlServer::WebSocketAcceptKey(const std::string& clientKey)
|
||||||
|
{
|
||||||
|
static constexpr const char* kWebSocketGuid = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
|
||||||
|
const std::array<uint8_t, 20> digest = Sha1(clientKey + kWebSocketGuid);
|
||||||
|
return Base64Encode(digest.data(), digest.size());
|
||||||
|
}
|
||||||
|
}
|
||||||
250
apps/RenderCadenceCompositor/frames/InputFrameMailbox.cpp
Normal file
250
apps/RenderCadenceCompositor/frames/InputFrameMailbox.cpp
Normal file
@@ -0,0 +1,250 @@
|
|||||||
|
#include "InputFrameMailbox.h"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <chrono>
|
||||||
|
#include <cstring>
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
InputFrameMailboxConfig NormalizeConfig(InputFrameMailboxConfig config)
|
||||||
|
{
|
||||||
|
if (config.rowBytes == 0)
|
||||||
|
config.rowBytes = VideoIORowBytes(config.pixelFormat, config.width);
|
||||||
|
return config;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
InputFrameMailbox::InputFrameMailbox(const InputFrameMailboxConfig& config)
|
||||||
|
{
|
||||||
|
Configure(config);
|
||||||
|
}
|
||||||
|
|
||||||
|
void InputFrameMailbox::Configure(const InputFrameMailboxConfig& config)
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(mMutex);
|
||||||
|
mConfig = NormalizeConfig(config);
|
||||||
|
mReadyIndices.clear();
|
||||||
|
mSlots.clear();
|
||||||
|
mSlots.resize(mConfig.capacity);
|
||||||
|
|
||||||
|
const std::size_t byteCount = FrameByteCount();
|
||||||
|
for (Slot& slot : mSlots)
|
||||||
|
{
|
||||||
|
slot.bytes.resize(byteCount);
|
||||||
|
slot.state = InputFrameSlotState::Free;
|
||||||
|
slot.frameIndex = 0;
|
||||||
|
++slot.generation;
|
||||||
|
}
|
||||||
|
|
||||||
|
mCounters = InputFrameMailboxMetrics();
|
||||||
|
}
|
||||||
|
|
||||||
|
InputFrameMailboxConfig InputFrameMailbox::Config() const
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(mMutex);
|
||||||
|
return mConfig;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool InputFrameMailbox::SubmitFrame(const void* bytes, unsigned rowBytes, uint64_t frameIndex)
|
||||||
|
{
|
||||||
|
if (bytes == nullptr || rowBytes == 0)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
std::lock_guard<std::mutex> lock(mMutex);
|
||||||
|
if (mSlots.empty() || mConfig.width == 0 || mConfig.height == 0)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
std::size_t slotIndex = mSlots.size();
|
||||||
|
for (std::size_t index = 0; index < mSlots.size(); ++index)
|
||||||
|
{
|
||||||
|
if (mSlots[index].state == InputFrameSlotState::Free)
|
||||||
|
{
|
||||||
|
slotIndex = index;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (slotIndex == mSlots.size())
|
||||||
|
{
|
||||||
|
if (!DropOldestReadyLocked())
|
||||||
|
{
|
||||||
|
++mCounters.submitMisses;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (std::size_t index = 0; index < mSlots.size(); ++index)
|
||||||
|
{
|
||||||
|
if (mSlots[index].state == InputFrameSlotState::Free)
|
||||||
|
{
|
||||||
|
slotIndex = index;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (slotIndex == mSlots.size())
|
||||||
|
{
|
||||||
|
++mCounters.submitMisses;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
Slot& slot = mSlots[slotIndex];
|
||||||
|
const std::size_t destinationRowBytes = mConfig.rowBytes;
|
||||||
|
const std::size_t sourceRowBytes = static_cast<std::size_t>(rowBytes);
|
||||||
|
const unsigned char* source = static_cast<const unsigned char*>(bytes);
|
||||||
|
if (sourceRowBytes == destinationRowBytes)
|
||||||
|
{
|
||||||
|
std::memcpy(slot.bytes.data(), source, destinationRowBytes * static_cast<std::size_t>(mConfig.height));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
const std::size_t copyRowBytes = (std::min)(sourceRowBytes, destinationRowBytes);
|
||||||
|
for (unsigned y = 0; y < mConfig.height; ++y)
|
||||||
|
{
|
||||||
|
std::memcpy(
|
||||||
|
slot.bytes.data() + static_cast<std::size_t>(y) * destinationRowBytes,
|
||||||
|
source + static_cast<std::size_t>(y) * sourceRowBytes,
|
||||||
|
copyRowBytes);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
slot.state = InputFrameSlotState::Ready;
|
||||||
|
slot.frameIndex = frameIndex;
|
||||||
|
++slot.generation;
|
||||||
|
mReadyIndices.push_back(slotIndex);
|
||||||
|
TrimReadyFramesLocked();
|
||||||
|
++mCounters.submittedFrames;
|
||||||
|
mCounters.latestFrameIndex = frameIndex;
|
||||||
|
mCounters.hasSubmittedFrame = true;
|
||||||
|
mLatestSubmitTime = std::chrono::steady_clock::now();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool InputFrameMailbox::TryAcquireOldest(InputFrame& frame)
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(mMutex);
|
||||||
|
while (!mReadyIndices.empty())
|
||||||
|
{
|
||||||
|
const std::size_t index = mReadyIndices.front();
|
||||||
|
mReadyIndices.pop_front();
|
||||||
|
if (index >= mSlots.size() || mSlots[index].state != InputFrameSlotState::Ready)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
mSlots[index].state = InputFrameSlotState::Reading;
|
||||||
|
FillFrameLocked(index, frame);
|
||||||
|
++mCounters.consumedFrames;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
frame = InputFrame();
|
||||||
|
++mCounters.consumeMisses;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool InputFrameMailbox::Release(const InputFrame& frame)
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(mMutex);
|
||||||
|
if (!IsValidLocked(frame))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
Slot& slot = mSlots[frame.index];
|
||||||
|
if (slot.state != InputFrameSlotState::Reading)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
slot.state = InputFrameSlotState::Free;
|
||||||
|
slot.frameIndex = 0;
|
||||||
|
++slot.generation;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void InputFrameMailbox::Clear()
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(mMutex);
|
||||||
|
mReadyIndices.clear();
|
||||||
|
for (Slot& slot : mSlots)
|
||||||
|
{
|
||||||
|
slot.state = InputFrameSlotState::Free;
|
||||||
|
slot.frameIndex = 0;
|
||||||
|
++slot.generation;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
InputFrameMailboxMetrics InputFrameMailbox::Metrics() const
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(mMutex);
|
||||||
|
InputFrameMailboxMetrics metrics = mCounters;
|
||||||
|
metrics.capacity = mSlots.size();
|
||||||
|
if (metrics.hasSubmittedFrame)
|
||||||
|
{
|
||||||
|
metrics.latestFrameAgeMilliseconds = std::chrono::duration_cast<std::chrono::duration<double, std::milli>>(
|
||||||
|
std::chrono::steady_clock::now() - mLatestSubmitTime).count();
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const Slot& slot : mSlots)
|
||||||
|
{
|
||||||
|
switch (slot.state)
|
||||||
|
{
|
||||||
|
case InputFrameSlotState::Free:
|
||||||
|
++metrics.freeCount;
|
||||||
|
break;
|
||||||
|
case InputFrameSlotState::Ready:
|
||||||
|
++metrics.readyCount;
|
||||||
|
break;
|
||||||
|
case InputFrameSlotState::Reading:
|
||||||
|
++metrics.readingCount;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return metrics;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool InputFrameMailbox::IsValidLocked(const InputFrame& frame) const
|
||||||
|
{
|
||||||
|
return frame.index < mSlots.size() && mSlots[frame.index].generation == frame.generation;
|
||||||
|
}
|
||||||
|
|
||||||
|
void InputFrameMailbox::FillFrameLocked(std::size_t index, InputFrame& frame) const
|
||||||
|
{
|
||||||
|
const Slot& slot = mSlots[index];
|
||||||
|
frame.bytes = slot.bytes.empty() ? nullptr : slot.bytes.data();
|
||||||
|
frame.rowBytes = static_cast<long>(mConfig.rowBytes);
|
||||||
|
frame.width = mConfig.width;
|
||||||
|
frame.height = mConfig.height;
|
||||||
|
frame.pixelFormat = mConfig.pixelFormat;
|
||||||
|
frame.index = index;
|
||||||
|
frame.generation = slot.generation;
|
||||||
|
frame.frameIndex = slot.frameIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool InputFrameMailbox::DropOldestReadyLocked()
|
||||||
|
{
|
||||||
|
while (!mReadyIndices.empty())
|
||||||
|
{
|
||||||
|
const std::size_t index = mReadyIndices.front();
|
||||||
|
mReadyIndices.pop_front();
|
||||||
|
if (index >= mSlots.size() || mSlots[index].state != InputFrameSlotState::Ready)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
mSlots[index].state = InputFrameSlotState::Free;
|
||||||
|
mSlots[index].frameIndex = 0;
|
||||||
|
++mSlots[index].generation;
|
||||||
|
++mCounters.droppedReadyFrames;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void InputFrameMailbox::TrimReadyFramesLocked()
|
||||||
|
{
|
||||||
|
if (mConfig.maxReadyFrames == 0)
|
||||||
|
return;
|
||||||
|
while (mReadyIndices.size() > mConfig.maxReadyFrames)
|
||||||
|
DropOldestReadyLocked();
|
||||||
|
}
|
||||||
|
|
||||||
|
std::size_t InputFrameMailbox::FrameByteCount() const
|
||||||
|
{
|
||||||
|
return static_cast<std::size_t>(mConfig.rowBytes) * static_cast<std::size_t>(mConfig.height);
|
||||||
|
}
|
||||||
93
apps/RenderCadenceCompositor/frames/InputFrameMailbox.h
Normal file
93
apps/RenderCadenceCompositor/frames/InputFrameMailbox.h
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "VideoIOFormat.h"
|
||||||
|
|
||||||
|
#include <cstddef>
|
||||||
|
#include <cstdint>
|
||||||
|
#include <chrono>
|
||||||
|
#include <deque>
|
||||||
|
#include <mutex>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
enum class InputFrameSlotState
|
||||||
|
{
|
||||||
|
Free,
|
||||||
|
Ready,
|
||||||
|
Reading
|
||||||
|
};
|
||||||
|
|
||||||
|
struct InputFrameMailboxConfig
|
||||||
|
{
|
||||||
|
unsigned width = 0;
|
||||||
|
unsigned height = 0;
|
||||||
|
VideoIOPixelFormat pixelFormat = VideoIOPixelFormat::Bgra8;
|
||||||
|
unsigned rowBytes = 0;
|
||||||
|
std::size_t capacity = 0;
|
||||||
|
std::size_t maxReadyFrames = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct InputFrame
|
||||||
|
{
|
||||||
|
const void* bytes = nullptr;
|
||||||
|
long rowBytes = 0;
|
||||||
|
unsigned width = 0;
|
||||||
|
unsigned height = 0;
|
||||||
|
VideoIOPixelFormat pixelFormat = VideoIOPixelFormat::Bgra8;
|
||||||
|
std::size_t index = 0;
|
||||||
|
uint64_t generation = 0;
|
||||||
|
uint64_t frameIndex = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct InputFrameMailboxMetrics
|
||||||
|
{
|
||||||
|
std::size_t capacity = 0;
|
||||||
|
std::size_t freeCount = 0;
|
||||||
|
std::size_t readyCount = 0;
|
||||||
|
std::size_t readingCount = 0;
|
||||||
|
uint64_t submittedFrames = 0;
|
||||||
|
uint64_t consumedFrames = 0;
|
||||||
|
uint64_t droppedReadyFrames = 0;
|
||||||
|
uint64_t submitMisses = 0;
|
||||||
|
uint64_t consumeMisses = 0;
|
||||||
|
uint64_t latestFrameIndex = 0;
|
||||||
|
bool hasSubmittedFrame = false;
|
||||||
|
double latestFrameAgeMilliseconds = 0.0;
|
||||||
|
};
|
||||||
|
|
||||||
|
class InputFrameMailbox
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
InputFrameMailbox() = default;
|
||||||
|
explicit InputFrameMailbox(const InputFrameMailboxConfig& config);
|
||||||
|
|
||||||
|
void Configure(const InputFrameMailboxConfig& config);
|
||||||
|
InputFrameMailboxConfig Config() const;
|
||||||
|
|
||||||
|
bool SubmitFrame(const void* bytes, unsigned rowBytes, uint64_t frameIndex);
|
||||||
|
bool TryAcquireOldest(InputFrame& frame);
|
||||||
|
bool Release(const InputFrame& frame);
|
||||||
|
void Clear();
|
||||||
|
InputFrameMailboxMetrics Metrics() const;
|
||||||
|
|
||||||
|
private:
|
||||||
|
struct Slot
|
||||||
|
{
|
||||||
|
std::vector<unsigned char> bytes;
|
||||||
|
InputFrameSlotState state = InputFrameSlotState::Free;
|
||||||
|
uint64_t frameIndex = 0;
|
||||||
|
uint64_t generation = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
bool IsValidLocked(const InputFrame& frame) const;
|
||||||
|
void FillFrameLocked(std::size_t index, InputFrame& frame) const;
|
||||||
|
bool DropOldestReadyLocked();
|
||||||
|
void TrimReadyFramesLocked();
|
||||||
|
std::size_t FrameByteCount() const;
|
||||||
|
|
||||||
|
mutable std::mutex mMutex;
|
||||||
|
InputFrameMailboxConfig mConfig;
|
||||||
|
std::vector<Slot> mSlots;
|
||||||
|
std::deque<std::size_t> mReadyIndices;
|
||||||
|
InputFrameMailboxMetrics mCounters;
|
||||||
|
std::chrono::steady_clock::time_point mLatestSubmitTime;
|
||||||
|
};
|
||||||
299
apps/RenderCadenceCompositor/frames/SystemFrameExchange.cpp
Normal file
299
apps/RenderCadenceCompositor/frames/SystemFrameExchange.cpp
Normal file
@@ -0,0 +1,299 @@
|
|||||||
|
#include "SystemFrameExchange.h"
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
SystemFrameExchangeConfig NormalizeConfig(SystemFrameExchangeConfig config)
|
||||||
|
{
|
||||||
|
if (config.rowBytes == 0)
|
||||||
|
config.rowBytes = VideoIORowBytes(config.pixelFormat, config.width);
|
||||||
|
return config;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SystemFrameExchange::SystemFrameExchange(const SystemFrameExchangeConfig& config)
|
||||||
|
{
|
||||||
|
Configure(config);
|
||||||
|
}
|
||||||
|
|
||||||
|
void SystemFrameExchange::Configure(const SystemFrameExchangeConfig& config)
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(mMutex);
|
||||||
|
mConfig = NormalizeConfig(config);
|
||||||
|
mCompletedIndices.clear();
|
||||||
|
mSlots.clear();
|
||||||
|
mSlots.resize(mConfig.capacity);
|
||||||
|
|
||||||
|
const std::size_t byteCount = FrameByteCount();
|
||||||
|
for (Slot& slot : mSlots)
|
||||||
|
{
|
||||||
|
slot.bytes.resize(byteCount);
|
||||||
|
slot.state = SystemFrameSlotState::Free;
|
||||||
|
slot.frameIndex = 0;
|
||||||
|
++slot.generation;
|
||||||
|
}
|
||||||
|
|
||||||
|
mCounters = SystemFrameExchangeMetrics();
|
||||||
|
mCondition.notify_all();
|
||||||
|
}
|
||||||
|
|
||||||
|
SystemFrameExchangeConfig SystemFrameExchange::Config() const
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(mMutex);
|
||||||
|
return mConfig;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool SystemFrameExchange::AcquireForRender(SystemFrame& frame)
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(mMutex);
|
||||||
|
if (!AcquireFreeLocked(frame))
|
||||||
|
{
|
||||||
|
frame = SystemFrame();
|
||||||
|
++mCounters.acquireMisses;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
++mCounters.acquiredFrames;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool SystemFrameExchange::PublishCompleted(const SystemFrame& frame)
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(mMutex);
|
||||||
|
if (!IsValidLocked(frame))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
Slot& slot = mSlots[frame.index];
|
||||||
|
if (slot.state != SystemFrameSlotState::Rendering)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
slot.state = SystemFrameSlotState::Completed;
|
||||||
|
slot.frameIndex = frame.frameIndex;
|
||||||
|
mCompletedIndices.push_back(frame.index);
|
||||||
|
TrimCompletedLocked();
|
||||||
|
++mCounters.completedFrames;
|
||||||
|
mCondition.notify_all();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool SystemFrameExchange::ConsumeCompletedForSchedule(SystemFrame& frame)
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(mMutex);
|
||||||
|
while (!mCompletedIndices.empty())
|
||||||
|
{
|
||||||
|
const std::size_t index = mCompletedIndices.front();
|
||||||
|
mCompletedIndices.pop_front();
|
||||||
|
if (index >= mSlots.size() || mSlots[index].state != SystemFrameSlotState::Completed)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
mSlots[index].state = SystemFrameSlotState::Scheduled;
|
||||||
|
FillFrameLocked(index, frame);
|
||||||
|
++mCounters.scheduledFrames;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
frame = SystemFrame();
|
||||||
|
++mCounters.completedPollMisses;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool SystemFrameExchange::ReleaseScheduledByBytes(void* bytes)
|
||||||
|
{
|
||||||
|
if (bytes == nullptr)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
std::lock_guard<std::mutex> lock(mMutex);
|
||||||
|
for (std::size_t index = 0; index < mSlots.size(); ++index)
|
||||||
|
{
|
||||||
|
Slot& slot = mSlots[index];
|
||||||
|
if (slot.bytes.empty() || slot.bytes.data() != bytes)
|
||||||
|
continue;
|
||||||
|
if (slot.state != SystemFrameSlotState::Scheduled)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
slot.state = SystemFrameSlotState::Free;
|
||||||
|
slot.frameIndex = 0;
|
||||||
|
++slot.generation;
|
||||||
|
mCondition.notify_all();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool SystemFrameExchange::WaitForCompletedDepth(std::size_t targetDepth, std::chrono::milliseconds timeout)
|
||||||
|
{
|
||||||
|
std::unique_lock<std::mutex> lock(mMutex);
|
||||||
|
return mCondition.wait_for(lock, timeout, [&]() {
|
||||||
|
return CompletedCountLocked() >= targetDepth;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
bool SystemFrameExchange::WaitForStableCompletedDepth(
|
||||||
|
std::size_t targetDepth,
|
||||||
|
std::chrono::milliseconds stableDuration,
|
||||||
|
std::chrono::milliseconds timeout)
|
||||||
|
{
|
||||||
|
if (targetDepth == 0)
|
||||||
|
return true;
|
||||||
|
|
||||||
|
const auto deadline = std::chrono::steady_clock::now() + timeout;
|
||||||
|
std::unique_lock<std::mutex> lock(mMutex);
|
||||||
|
bool stableWindowStarted = false;
|
||||||
|
std::chrono::steady_clock::time_point stableSince;
|
||||||
|
|
||||||
|
while (true)
|
||||||
|
{
|
||||||
|
const auto now = std::chrono::steady_clock::now();
|
||||||
|
if (now >= deadline)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if (CompletedCountLocked() >= targetDepth)
|
||||||
|
{
|
||||||
|
if (stableDuration <= std::chrono::milliseconds::zero())
|
||||||
|
return true;
|
||||||
|
|
||||||
|
if (!stableWindowStarted)
|
||||||
|
{
|
||||||
|
stableSince = now;
|
||||||
|
stableWindowStarted = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto stableDeadline = stableSince + stableDuration;
|
||||||
|
if (now >= stableDeadline)
|
||||||
|
return true;
|
||||||
|
|
||||||
|
mCondition.wait_until(lock, stableDeadline < deadline ? stableDeadline : deadline);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
stableWindowStarted = false;
|
||||||
|
mCondition.wait_until(lock, deadline, [&]() {
|
||||||
|
return CompletedCountLocked() >= targetDepth;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void SystemFrameExchange::Clear()
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(mMutex);
|
||||||
|
mCompletedIndices.clear();
|
||||||
|
for (Slot& slot : mSlots)
|
||||||
|
{
|
||||||
|
slot.state = SystemFrameSlotState::Free;
|
||||||
|
slot.frameIndex = 0;
|
||||||
|
++slot.generation;
|
||||||
|
}
|
||||||
|
mCondition.notify_all();
|
||||||
|
}
|
||||||
|
|
||||||
|
SystemFrameExchangeMetrics SystemFrameExchange::Metrics() const
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(mMutex);
|
||||||
|
SystemFrameExchangeMetrics metrics = mCounters;
|
||||||
|
metrics.capacity = mSlots.size();
|
||||||
|
metrics.completedDepth = mCompletedIndices.size();
|
||||||
|
|
||||||
|
for (const Slot& slot : mSlots)
|
||||||
|
{
|
||||||
|
switch (slot.state)
|
||||||
|
{
|
||||||
|
case SystemFrameSlotState::Free:
|
||||||
|
++metrics.freeCount;
|
||||||
|
break;
|
||||||
|
case SystemFrameSlotState::Rendering:
|
||||||
|
++metrics.renderingCount;
|
||||||
|
break;
|
||||||
|
case SystemFrameSlotState::Completed:
|
||||||
|
++metrics.completedCount;
|
||||||
|
break;
|
||||||
|
case SystemFrameSlotState::Scheduled:
|
||||||
|
++metrics.scheduledCount;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return metrics;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool SystemFrameExchange::AcquireFreeLocked(SystemFrame& frame)
|
||||||
|
{
|
||||||
|
for (std::size_t index = 0; index < mSlots.size(); ++index)
|
||||||
|
{
|
||||||
|
Slot& slot = mSlots[index];
|
||||||
|
if (slot.state != SystemFrameSlotState::Free)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
slot.state = SystemFrameSlotState::Rendering;
|
||||||
|
++slot.generation;
|
||||||
|
FillFrameLocked(index, frame);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool SystemFrameExchange::DropOldestCompletedLocked()
|
||||||
|
{
|
||||||
|
while (!mCompletedIndices.empty())
|
||||||
|
{
|
||||||
|
const std::size_t index = mCompletedIndices.front();
|
||||||
|
mCompletedIndices.pop_front();
|
||||||
|
if (index >= mSlots.size() || mSlots[index].state != SystemFrameSlotState::Completed)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
Slot& slot = mSlots[index];
|
||||||
|
slot.state = SystemFrameSlotState::Free;
|
||||||
|
slot.frameIndex = 0;
|
||||||
|
++slot.generation;
|
||||||
|
++mCounters.completedDrops;
|
||||||
|
mCondition.notify_all();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void SystemFrameExchange::TrimCompletedLocked()
|
||||||
|
{
|
||||||
|
if (mConfig.maxCompletedFrames == 0)
|
||||||
|
return;
|
||||||
|
while (CompletedCountLocked() > mConfig.maxCompletedFrames)
|
||||||
|
{
|
||||||
|
if (!DropOldestCompletedLocked())
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool SystemFrameExchange::IsValidLocked(const SystemFrame& frame) const
|
||||||
|
{
|
||||||
|
return frame.index < mSlots.size() && mSlots[frame.index].generation == frame.generation;
|
||||||
|
}
|
||||||
|
|
||||||
|
void SystemFrameExchange::FillFrameLocked(std::size_t index, SystemFrame& frame)
|
||||||
|
{
|
||||||
|
Slot& slot = mSlots[index];
|
||||||
|
frame.bytes = slot.bytes.empty() ? nullptr : slot.bytes.data();
|
||||||
|
frame.rowBytes = static_cast<long>(mConfig.rowBytes);
|
||||||
|
frame.width = mConfig.width;
|
||||||
|
frame.height = mConfig.height;
|
||||||
|
frame.pixelFormat = mConfig.pixelFormat;
|
||||||
|
frame.index = index;
|
||||||
|
frame.generation = slot.generation;
|
||||||
|
frame.frameIndex = slot.frameIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::size_t SystemFrameExchange::CompletedCountLocked() const
|
||||||
|
{
|
||||||
|
std::size_t count = 0;
|
||||||
|
for (const Slot& slot : mSlots)
|
||||||
|
{
|
||||||
|
if (slot.state == SystemFrameSlotState::Completed)
|
||||||
|
++count;
|
||||||
|
}
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::size_t SystemFrameExchange::FrameByteCount() const
|
||||||
|
{
|
||||||
|
return static_cast<std::size_t>(mConfig.rowBytes) * static_cast<std::size_t>(mConfig.height);
|
||||||
|
}
|
||||||
56
apps/RenderCadenceCompositor/frames/SystemFrameExchange.h
Normal file
56
apps/RenderCadenceCompositor/frames/SystemFrameExchange.h
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "SystemFrameTypes.h"
|
||||||
|
|
||||||
|
#include <chrono>
|
||||||
|
#include <condition_variable>
|
||||||
|
#include <deque>
|
||||||
|
#include <mutex>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
class SystemFrameExchange
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
SystemFrameExchange() = default;
|
||||||
|
explicit SystemFrameExchange(const SystemFrameExchangeConfig& config);
|
||||||
|
|
||||||
|
void Configure(const SystemFrameExchangeConfig& config);
|
||||||
|
SystemFrameExchangeConfig Config() const;
|
||||||
|
|
||||||
|
bool AcquireForRender(SystemFrame& frame);
|
||||||
|
bool PublishCompleted(const SystemFrame& frame);
|
||||||
|
bool ConsumeCompletedForSchedule(SystemFrame& frame);
|
||||||
|
bool ReleaseScheduledByBytes(void* bytes);
|
||||||
|
bool WaitForCompletedDepth(std::size_t targetDepth, std::chrono::milliseconds timeout);
|
||||||
|
bool WaitForStableCompletedDepth(
|
||||||
|
std::size_t targetDepth,
|
||||||
|
std::chrono::milliseconds stableDuration,
|
||||||
|
std::chrono::milliseconds timeout);
|
||||||
|
void Clear();
|
||||||
|
|
||||||
|
SystemFrameExchangeMetrics Metrics() const;
|
||||||
|
|
||||||
|
private:
|
||||||
|
struct Slot
|
||||||
|
{
|
||||||
|
std::vector<unsigned char> bytes;
|
||||||
|
SystemFrameSlotState state = SystemFrameSlotState::Free;
|
||||||
|
uint64_t generation = 1;
|
||||||
|
uint64_t frameIndex = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
bool AcquireFreeLocked(SystemFrame& frame);
|
||||||
|
bool DropOldestCompletedLocked();
|
||||||
|
void TrimCompletedLocked();
|
||||||
|
bool IsValidLocked(const SystemFrame& frame) const;
|
||||||
|
void FillFrameLocked(std::size_t index, SystemFrame& frame);
|
||||||
|
std::size_t CompletedCountLocked() const;
|
||||||
|
std::size_t FrameByteCount() const;
|
||||||
|
|
||||||
|
mutable std::mutex mMutex;
|
||||||
|
std::condition_variable mCondition;
|
||||||
|
SystemFrameExchangeConfig mConfig;
|
||||||
|
std::vector<Slot> mSlots;
|
||||||
|
std::deque<std::size_t> mCompletedIndices;
|
||||||
|
SystemFrameExchangeMetrics mCounters;
|
||||||
|
};
|
||||||
52
apps/RenderCadenceCompositor/frames/SystemFrameTypes.h
Normal file
52
apps/RenderCadenceCompositor/frames/SystemFrameTypes.h
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "VideoIOFormat.h"
|
||||||
|
|
||||||
|
#include <cstddef>
|
||||||
|
#include <cstdint>
|
||||||
|
|
||||||
|
enum class SystemFrameSlotState
|
||||||
|
{
|
||||||
|
Free,
|
||||||
|
Rendering,
|
||||||
|
Completed,
|
||||||
|
Scheduled
|
||||||
|
};
|
||||||
|
|
||||||
|
struct SystemFrameExchangeConfig
|
||||||
|
{
|
||||||
|
unsigned width = 0;
|
||||||
|
unsigned height = 0;
|
||||||
|
VideoIOPixelFormat pixelFormat = VideoIOPixelFormat::Bgra8;
|
||||||
|
unsigned rowBytes = 0;
|
||||||
|
std::size_t capacity = 0;
|
||||||
|
std::size_t maxCompletedFrames = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct SystemFrame
|
||||||
|
{
|
||||||
|
void* bytes = nullptr;
|
||||||
|
long rowBytes = 0;
|
||||||
|
unsigned width = 0;
|
||||||
|
unsigned height = 0;
|
||||||
|
VideoIOPixelFormat pixelFormat = VideoIOPixelFormat::Bgra8;
|
||||||
|
std::size_t index = 0;
|
||||||
|
uint64_t generation = 0;
|
||||||
|
uint64_t frameIndex = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct SystemFrameExchangeMetrics
|
||||||
|
{
|
||||||
|
std::size_t capacity = 0;
|
||||||
|
std::size_t freeCount = 0;
|
||||||
|
std::size_t renderingCount = 0;
|
||||||
|
std::size_t completedCount = 0;
|
||||||
|
std::size_t scheduledCount = 0;
|
||||||
|
std::size_t completedDepth = 0;
|
||||||
|
uint64_t acquiredFrames = 0;
|
||||||
|
uint64_t completedFrames = 0;
|
||||||
|
uint64_t scheduledFrames = 0;
|
||||||
|
uint64_t completedDrops = 0;
|
||||||
|
uint64_t acquireMisses = 0;
|
||||||
|
uint64_t completedPollMisses = 0;
|
||||||
|
};
|
||||||
235
apps/RenderCadenceCompositor/json/JsonWriter.cpp
Normal file
235
apps/RenderCadenceCompositor/json/JsonWriter.cpp
Normal file
@@ -0,0 +1,235 @@
|
|||||||
|
#include "JsonWriter.h"
|
||||||
|
|
||||||
|
#include <iomanip>
|
||||||
|
#include <stdexcept>
|
||||||
|
|
||||||
|
namespace RenderCadenceCompositor
|
||||||
|
{
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
constexpr int kMaxDepth = 32;
|
||||||
|
|
||||||
|
void AppendHexEscape(std::ostringstream& stream, unsigned char value)
|
||||||
|
{
|
||||||
|
stream << "\\u"
|
||||||
|
<< std::hex << std::uppercase << std::setw(4) << std::setfill('0')
|
||||||
|
<< static_cast<int>(value)
|
||||||
|
<< std::dec << std::nouppercase << std::setfill(' ');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void JsonWriter::BeginObject()
|
||||||
|
{
|
||||||
|
BeginValue();
|
||||||
|
mStream << "{";
|
||||||
|
PushScope(ScopeKind::Object);
|
||||||
|
}
|
||||||
|
|
||||||
|
void JsonWriter::EndObject()
|
||||||
|
{
|
||||||
|
PopScope(ScopeKind::Object);
|
||||||
|
mStream << "}";
|
||||||
|
}
|
||||||
|
|
||||||
|
void JsonWriter::BeginArray()
|
||||||
|
{
|
||||||
|
BeginValue();
|
||||||
|
mStream << "[";
|
||||||
|
PushScope(ScopeKind::Array);
|
||||||
|
}
|
||||||
|
|
||||||
|
void JsonWriter::EndArray()
|
||||||
|
{
|
||||||
|
PopScope(ScopeKind::Array);
|
||||||
|
mStream << "]";
|
||||||
|
}
|
||||||
|
|
||||||
|
void JsonWriter::Key(const std::string& name)
|
||||||
|
{
|
||||||
|
BeginKey();
|
||||||
|
mStream << "\"" << EscapeString(name) << "\":";
|
||||||
|
}
|
||||||
|
|
||||||
|
void JsonWriter::String(const std::string& value)
|
||||||
|
{
|
||||||
|
BeginValue();
|
||||||
|
mStream << "\"" << EscapeString(value) << "\"";
|
||||||
|
}
|
||||||
|
|
||||||
|
void JsonWriter::Bool(bool value)
|
||||||
|
{
|
||||||
|
BeginValue();
|
||||||
|
mStream << (value ? "true" : "false");
|
||||||
|
}
|
||||||
|
|
||||||
|
void JsonWriter::Null()
|
||||||
|
{
|
||||||
|
BeginValue();
|
||||||
|
mStream << "null";
|
||||||
|
}
|
||||||
|
|
||||||
|
void JsonWriter::Int(int64_t value)
|
||||||
|
{
|
||||||
|
BeginValue();
|
||||||
|
mStream << value;
|
||||||
|
}
|
||||||
|
|
||||||
|
void JsonWriter::UInt(uint64_t value)
|
||||||
|
{
|
||||||
|
BeginValue();
|
||||||
|
mStream << value;
|
||||||
|
}
|
||||||
|
|
||||||
|
void JsonWriter::Double(double value)
|
||||||
|
{
|
||||||
|
BeginValue();
|
||||||
|
mStream << std::setprecision(15) << value;
|
||||||
|
}
|
||||||
|
|
||||||
|
void JsonWriter::KeyString(const std::string& name, const std::string& value)
|
||||||
|
{
|
||||||
|
Key(name);
|
||||||
|
String(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
void JsonWriter::KeyBool(const std::string& name, bool value)
|
||||||
|
{
|
||||||
|
Key(name);
|
||||||
|
Bool(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
void JsonWriter::KeyNull(const std::string& name)
|
||||||
|
{
|
||||||
|
Key(name);
|
||||||
|
Null();
|
||||||
|
}
|
||||||
|
|
||||||
|
void JsonWriter::KeyInt(const std::string& name, int64_t value)
|
||||||
|
{
|
||||||
|
Key(name);
|
||||||
|
Int(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
void JsonWriter::KeyUInt(const std::string& name, uint64_t value)
|
||||||
|
{
|
||||||
|
Key(name);
|
||||||
|
UInt(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
void JsonWriter::KeyDouble(const std::string& name, double value)
|
||||||
|
{
|
||||||
|
Key(name);
|
||||||
|
Double(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string JsonWriter::StringValue() const
|
||||||
|
{
|
||||||
|
if (mScopeDepth != 0)
|
||||||
|
throw std::logic_error("JSON document has unclosed scopes.");
|
||||||
|
return mStream.str();
|
||||||
|
}
|
||||||
|
|
||||||
|
void JsonWriter::Reset()
|
||||||
|
{
|
||||||
|
mStream.str(std::string());
|
||||||
|
mStream.clear();
|
||||||
|
mScopeDepth = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string JsonWriter::EscapeString(const std::string& value)
|
||||||
|
{
|
||||||
|
std::ostringstream stream;
|
||||||
|
for (unsigned char character : value)
|
||||||
|
{
|
||||||
|
switch (character)
|
||||||
|
{
|
||||||
|
case '"':
|
||||||
|
stream << "\\\"";
|
||||||
|
break;
|
||||||
|
case '\\':
|
||||||
|
stream << "\\\\";
|
||||||
|
break;
|
||||||
|
case '\b':
|
||||||
|
stream << "\\b";
|
||||||
|
break;
|
||||||
|
case '\f':
|
||||||
|
stream << "\\f";
|
||||||
|
break;
|
||||||
|
case '\n':
|
||||||
|
stream << "\\n";
|
||||||
|
break;
|
||||||
|
case '\r':
|
||||||
|
stream << "\\r";
|
||||||
|
break;
|
||||||
|
case '\t':
|
||||||
|
stream << "\\t";
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
if (character < 0x20)
|
||||||
|
AppendHexEscape(stream, character);
|
||||||
|
else
|
||||||
|
stream << character;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return stream.str();
|
||||||
|
}
|
||||||
|
|
||||||
|
void JsonWriter::BeginValue()
|
||||||
|
{
|
||||||
|
if (mScopeDepth == 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
Scope& scope = mScopes[mScopeDepth - 1];
|
||||||
|
if (scope.kind == ScopeKind::Object)
|
||||||
|
{
|
||||||
|
if (!scope.expectingValue)
|
||||||
|
throw std::logic_error("JSON object value must follow a key.");
|
||||||
|
scope.expectingValue = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!scope.first)
|
||||||
|
mStream << ",";
|
||||||
|
scope.first = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void JsonWriter::BeginKey()
|
||||||
|
{
|
||||||
|
if (mScopeDepth == 0)
|
||||||
|
throw std::logic_error("JSON key cannot be written outside an object.");
|
||||||
|
|
||||||
|
Scope& scope = mScopes[mScopeDepth - 1];
|
||||||
|
if (scope.kind != ScopeKind::Object)
|
||||||
|
throw std::logic_error("JSON key cannot be written inside an array.");
|
||||||
|
if (scope.expectingValue)
|
||||||
|
throw std::logic_error("JSON object key cannot be written before its previous value.");
|
||||||
|
|
||||||
|
if (!scope.first)
|
||||||
|
mStream << ",";
|
||||||
|
scope.first = false;
|
||||||
|
scope.expectingValue = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void JsonWriter::PushScope(ScopeKind kind)
|
||||||
|
{
|
||||||
|
if (mScopeDepth >= kMaxDepth)
|
||||||
|
throw std::logic_error("JSON nesting is too deep.");
|
||||||
|
|
||||||
|
mScopes[mScopeDepth++] = Scope{ kind, true, false };
|
||||||
|
}
|
||||||
|
|
||||||
|
void JsonWriter::PopScope(ScopeKind kind)
|
||||||
|
{
|
||||||
|
if (mScopeDepth == 0)
|
||||||
|
throw std::logic_error("JSON scope underflow.");
|
||||||
|
|
||||||
|
Scope& scope = mScopes[mScopeDepth - 1];
|
||||||
|
if (scope.kind != kind)
|
||||||
|
throw std::logic_error("JSON scope kind mismatch.");
|
||||||
|
if (scope.expectingValue)
|
||||||
|
throw std::logic_error("JSON object key is missing a value.");
|
||||||
|
|
||||||
|
--mScopeDepth;
|
||||||
|
}
|
||||||
|
}
|
||||||
60
apps/RenderCadenceCompositor/json/JsonWriter.h
Normal file
60
apps/RenderCadenceCompositor/json/JsonWriter.h
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
#include <sstream>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
namespace RenderCadenceCompositor
|
||||||
|
{
|
||||||
|
class JsonWriter
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
void BeginObject();
|
||||||
|
void EndObject();
|
||||||
|
void BeginArray();
|
||||||
|
void EndArray();
|
||||||
|
|
||||||
|
void Key(const std::string& name);
|
||||||
|
void String(const std::string& value);
|
||||||
|
void Bool(bool value);
|
||||||
|
void Null();
|
||||||
|
void Int(int64_t value);
|
||||||
|
void UInt(uint64_t value);
|
||||||
|
void Double(double value);
|
||||||
|
|
||||||
|
void KeyString(const std::string& name, const std::string& value);
|
||||||
|
void KeyBool(const std::string& name, bool value);
|
||||||
|
void KeyNull(const std::string& name);
|
||||||
|
void KeyInt(const std::string& name, int64_t value);
|
||||||
|
void KeyUInt(const std::string& name, uint64_t value);
|
||||||
|
void KeyDouble(const std::string& name, double value);
|
||||||
|
|
||||||
|
std::string StringValue() const;
|
||||||
|
void Reset();
|
||||||
|
|
||||||
|
static std::string EscapeString(const std::string& value);
|
||||||
|
|
||||||
|
private:
|
||||||
|
enum class ScopeKind
|
||||||
|
{
|
||||||
|
Object,
|
||||||
|
Array
|
||||||
|
};
|
||||||
|
|
||||||
|
struct Scope
|
||||||
|
{
|
||||||
|
ScopeKind kind = ScopeKind::Object;
|
||||||
|
bool first = true;
|
||||||
|
bool expectingValue = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
void BeginValue();
|
||||||
|
void BeginKey();
|
||||||
|
void PushScope(ScopeKind kind);
|
||||||
|
void PopScope(ScopeKind kind);
|
||||||
|
|
||||||
|
std::ostringstream mStream;
|
||||||
|
Scope mScopes[32];
|
||||||
|
int mScopeDepth = 0;
|
||||||
|
};
|
||||||
|
}
|
||||||
283
apps/RenderCadenceCompositor/logging/Logger.cpp
Normal file
283
apps/RenderCadenceCompositor/logging/Logger.cpp
Normal file
@@ -0,0 +1,283 @@
|
|||||||
|
#include "Logger.h"
|
||||||
|
|
||||||
|
#include <windows.h>
|
||||||
|
|
||||||
|
#include <iomanip>
|
||||||
|
#include <iostream>
|
||||||
|
#include <sstream>
|
||||||
|
#include <ctime>
|
||||||
|
#include <filesystem>
|
||||||
|
|
||||||
|
namespace RenderCadenceCompositor
|
||||||
|
{
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
int Rank(LogLevel level)
|
||||||
|
{
|
||||||
|
switch (level)
|
||||||
|
{
|
||||||
|
case LogLevel::Log:
|
||||||
|
return 0;
|
||||||
|
case LogLevel::Warning:
|
||||||
|
return 1;
|
||||||
|
case LogLevel::Error:
|
||||||
|
return 2;
|
||||||
|
default:
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string FormatTimestamp(std::chrono::system_clock::time_point timestamp)
|
||||||
|
{
|
||||||
|
const std::time_t time = std::chrono::system_clock::to_time_t(timestamp);
|
||||||
|
std::tm localTime = {};
|
||||||
|
localtime_s(&localTime, &time);
|
||||||
|
|
||||||
|
std::ostringstream stream;
|
||||||
|
stream << std::put_time(&localTime, "%Y-%m-%d %H:%M:%S");
|
||||||
|
return stream.str();
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string FormatRecord(const Logger::Record& record)
|
||||||
|
{
|
||||||
|
std::ostringstream stream;
|
||||||
|
stream << FormatTimestamp(record.timestamp)
|
||||||
|
<< " [" << LogLevelName(record.level) << "]"
|
||||||
|
<< " [" << record.subsystem << "] "
|
||||||
|
<< record.message;
|
||||||
|
return stream.str();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const char* LogLevelName(LogLevel level)
|
||||||
|
{
|
||||||
|
switch (level)
|
||||||
|
{
|
||||||
|
case LogLevel::Log:
|
||||||
|
return "log";
|
||||||
|
case LogLevel::Warning:
|
||||||
|
return "warning";
|
||||||
|
case LogLevel::Error:
|
||||||
|
return "error";
|
||||||
|
default:
|
||||||
|
return "log";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Logger& Logger::Instance()
|
||||||
|
{
|
||||||
|
static Logger logger;
|
||||||
|
return logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
Logger::~Logger()
|
||||||
|
{
|
||||||
|
Stop();
|
||||||
|
}
|
||||||
|
|
||||||
|
void Logger::Start(LoggerConfig config)
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(mMutex);
|
||||||
|
if (mRunning)
|
||||||
|
return;
|
||||||
|
|
||||||
|
mConfig = config;
|
||||||
|
mStopping = false;
|
||||||
|
mRunning = true;
|
||||||
|
OpenFileSink();
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> countersLock(mCountersMutex);
|
||||||
|
mCounters = LoggerCounters();
|
||||||
|
}
|
||||||
|
mThread = std::thread([this]() { ThreadMain(); });
|
||||||
|
}
|
||||||
|
|
||||||
|
void Logger::Stop()
|
||||||
|
{
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(mMutex);
|
||||||
|
if (!mRunning && !mThread.joinable())
|
||||||
|
return;
|
||||||
|
mStopping = true;
|
||||||
|
}
|
||||||
|
mCondition.notify_all();
|
||||||
|
|
||||||
|
if (mThread.joinable())
|
||||||
|
mThread.join();
|
||||||
|
|
||||||
|
std::lock_guard<std::mutex> lock(mMutex);
|
||||||
|
CloseFileSink();
|
||||||
|
mRunning = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Logger::Write(LogLevel level, const std::string& subsystem, const std::string& message)
|
||||||
|
{
|
||||||
|
Enqueue(Record{ std::chrono::system_clock::now(), std::this_thread::get_id(), level, subsystem, message }, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Logger::TryWrite(LogLevel level, const std::string& subsystem, const std::string& message)
|
||||||
|
{
|
||||||
|
return Enqueue(Record{ std::chrono::system_clock::now(), std::this_thread::get_id(), level, subsystem, message }, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Logger::Log(const std::string& subsystem, const std::string& message)
|
||||||
|
{
|
||||||
|
Write(LogLevel::Log, subsystem, message);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Logger::Warning(const std::string& subsystem, const std::string& message)
|
||||||
|
{
|
||||||
|
Write(LogLevel::Warning, subsystem, message);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Logger::Error(const std::string& subsystem, const std::string& message)
|
||||||
|
{
|
||||||
|
Write(LogLevel::Error, subsystem, message);
|
||||||
|
}
|
||||||
|
|
||||||
|
LoggerCounters Logger::Counters() const
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(mCountersMutex);
|
||||||
|
return mCounters;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Logger::IsRunning() const
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(mMutex);
|
||||||
|
return mRunning;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Logger::ShouldWrite(LogLevel level) const
|
||||||
|
{
|
||||||
|
return Rank(level) >= Rank(mConfig.minimumLevel);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Logger::Enqueue(Record record, bool block)
|
||||||
|
{
|
||||||
|
if (!ShouldWrite(record.level))
|
||||||
|
return true;
|
||||||
|
|
||||||
|
std::unique_lock<std::mutex> lock(mMutex, std::defer_lock);
|
||||||
|
if (block)
|
||||||
|
{
|
||||||
|
lock.lock();
|
||||||
|
}
|
||||||
|
else if (!lock.try_lock())
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> countersLock(mCountersMutex);
|
||||||
|
++mCounters.dropped;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!mRunning)
|
||||||
|
{
|
||||||
|
lock.unlock();
|
||||||
|
WriteRecord(record);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mQueue.size() >= mConfig.maxQueuedMessages)
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> countersLock(mCountersMutex);
|
||||||
|
++mCounters.dropped;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
mQueue.push_back(std::move(record));
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> countersLock(mCountersMutex);
|
||||||
|
++mCounters.queued;
|
||||||
|
}
|
||||||
|
lock.unlock();
|
||||||
|
mCondition.notify_one();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Logger::ThreadMain()
|
||||||
|
{
|
||||||
|
for (;;)
|
||||||
|
{
|
||||||
|
Record record;
|
||||||
|
{
|
||||||
|
std::unique_lock<std::mutex> lock(mMutex);
|
||||||
|
mCondition.wait(lock, [this]() {
|
||||||
|
return mStopping || !mQueue.empty();
|
||||||
|
});
|
||||||
|
|
||||||
|
if (mQueue.empty())
|
||||||
|
{
|
||||||
|
if (mStopping)
|
||||||
|
return;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
record = std::move(mQueue.front());
|
||||||
|
mQueue.pop_front();
|
||||||
|
}
|
||||||
|
WriteRecord(record);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Logger::WriteRecord(const Record& record)
|
||||||
|
{
|
||||||
|
const std::string line = FormatRecord(record);
|
||||||
|
|
||||||
|
if (mConfig.writeToDebugOutput)
|
||||||
|
OutputDebugStringA((line + "\n").c_str());
|
||||||
|
|
||||||
|
if (mConfig.writeToConsole)
|
||||||
|
{
|
||||||
|
if (record.level == LogLevel::Error)
|
||||||
|
std::cerr << line << "\n";
|
||||||
|
else
|
||||||
|
std::cout << line << "\n";
|
||||||
|
}
|
||||||
|
if (mFile.is_open())
|
||||||
|
{
|
||||||
|
mFile << line << "\n";
|
||||||
|
mFile.flush();
|
||||||
|
}
|
||||||
|
|
||||||
|
std::lock_guard<std::mutex> lock(mCountersMutex);
|
||||||
|
++mCounters.written;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Logger::OpenFileSink()
|
||||||
|
{
|
||||||
|
CloseFileSink();
|
||||||
|
if (!mConfig.writeToFile || mConfig.filePath.empty())
|
||||||
|
return;
|
||||||
|
|
||||||
|
std::error_code error;
|
||||||
|
const std::filesystem::path logPath(mConfig.filePath);
|
||||||
|
if (logPath.has_parent_path())
|
||||||
|
std::filesystem::create_directories(logPath.parent_path(), error);
|
||||||
|
mFile.open(logPath, std::ios::out | std::ios::app);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Logger::CloseFileSink()
|
||||||
|
{
|
||||||
|
if (mFile.is_open())
|
||||||
|
mFile.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool TryLog(LogLevel level, const std::string& subsystem, const std::string& message)
|
||||||
|
{
|
||||||
|
return Logger::Instance().TryWrite(level, subsystem, message);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Log(const std::string& subsystem, const std::string& message)
|
||||||
|
{
|
||||||
|
Logger::Instance().Log(subsystem, message);
|
||||||
|
}
|
||||||
|
|
||||||
|
void LogWarning(const std::string& subsystem, const std::string& message)
|
||||||
|
{
|
||||||
|
Logger::Instance().Warning(subsystem, message);
|
||||||
|
}
|
||||||
|
|
||||||
|
void LogError(const std::string& subsystem, const std::string& message)
|
||||||
|
{
|
||||||
|
Logger::Instance().Error(subsystem, message);
|
||||||
|
}
|
||||||
|
}
|
||||||
101
apps/RenderCadenceCompositor/logging/Logger.h
Normal file
101
apps/RenderCadenceCompositor/logging/Logger.h
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <atomic>
|
||||||
|
#include <chrono>
|
||||||
|
#include <condition_variable>
|
||||||
|
#include <cstddef>
|
||||||
|
#include <cstdint>
|
||||||
|
#include <deque>
|
||||||
|
#include <fstream>
|
||||||
|
#include <mutex>
|
||||||
|
#include <string>
|
||||||
|
#include <thread>
|
||||||
|
|
||||||
|
namespace RenderCadenceCompositor
|
||||||
|
{
|
||||||
|
enum class LogLevel
|
||||||
|
{
|
||||||
|
Log,
|
||||||
|
Warning,
|
||||||
|
Error
|
||||||
|
};
|
||||||
|
|
||||||
|
struct LoggerConfig
|
||||||
|
{
|
||||||
|
LogLevel minimumLevel = LogLevel::Log;
|
||||||
|
bool writeToConsole = true;
|
||||||
|
bool writeToDebugOutput = true;
|
||||||
|
bool writeToFile = true;
|
||||||
|
std::string filePath = "logs/render-cadence-compositor.log";
|
||||||
|
std::size_t maxQueuedMessages = 1024;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct LoggerCounters
|
||||||
|
{
|
||||||
|
uint64_t queued = 0;
|
||||||
|
uint64_t written = 0;
|
||||||
|
uint64_t dropped = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
const char* LogLevelName(LogLevel level);
|
||||||
|
|
||||||
|
class Logger
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
struct Record
|
||||||
|
{
|
||||||
|
std::chrono::system_clock::time_point timestamp;
|
||||||
|
std::thread::id threadId;
|
||||||
|
LogLevel level = LogLevel::Log;
|
||||||
|
std::string subsystem;
|
||||||
|
std::string message;
|
||||||
|
};
|
||||||
|
|
||||||
|
static Logger& Instance();
|
||||||
|
|
||||||
|
Logger(const Logger&) = delete;
|
||||||
|
Logger& operator=(const Logger&) = delete;
|
||||||
|
|
||||||
|
~Logger();
|
||||||
|
|
||||||
|
void Start(LoggerConfig config = LoggerConfig());
|
||||||
|
void Stop();
|
||||||
|
|
||||||
|
void Write(LogLevel level, const std::string& subsystem, const std::string& message);
|
||||||
|
bool TryWrite(LogLevel level, const std::string& subsystem, const std::string& message);
|
||||||
|
|
||||||
|
void Log(const std::string& subsystem, const std::string& message);
|
||||||
|
void Warning(const std::string& subsystem, const std::string& message);
|
||||||
|
void Error(const std::string& subsystem, const std::string& message);
|
||||||
|
|
||||||
|
LoggerCounters Counters() const;
|
||||||
|
bool IsRunning() const;
|
||||||
|
|
||||||
|
private:
|
||||||
|
Logger() = default;
|
||||||
|
|
||||||
|
bool ShouldWrite(LogLevel level) const;
|
||||||
|
bool Enqueue(Record record, bool block);
|
||||||
|
void ThreadMain();
|
||||||
|
void WriteRecord(const Record& record);
|
||||||
|
void OpenFileSink();
|
||||||
|
void CloseFileSink();
|
||||||
|
|
||||||
|
LoggerConfig mConfig;
|
||||||
|
std::ofstream mFile;
|
||||||
|
std::thread mThread;
|
||||||
|
mutable std::mutex mMutex;
|
||||||
|
std::condition_variable mCondition;
|
||||||
|
std::deque<Record> mQueue;
|
||||||
|
bool mStopping = false;
|
||||||
|
bool mRunning = false;
|
||||||
|
|
||||||
|
mutable std::mutex mCountersMutex;
|
||||||
|
LoggerCounters mCounters;
|
||||||
|
};
|
||||||
|
|
||||||
|
bool TryLog(LogLevel level, const std::string& subsystem, const std::string& message);
|
||||||
|
void Log(const std::string& subsystem, const std::string& message);
|
||||||
|
void LogWarning(const std::string& subsystem, const std::string& message);
|
||||||
|
void LogError(const std::string& subsystem, const std::string& message);
|
||||||
|
}
|
||||||
134
apps/RenderCadenceCompositor/platform/HiddenGlWindow.cpp
Normal file
134
apps/RenderCadenceCompositor/platform/HiddenGlWindow.cpp
Normal file
@@ -0,0 +1,134 @@
|
|||||||
|
#include "HiddenGlWindow.h"
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
constexpr const char* kWindowClassName = "RenderCadenceCompositorHiddenGlWindow";
|
||||||
|
}
|
||||||
|
|
||||||
|
HiddenGlWindow::~HiddenGlWindow()
|
||||||
|
{
|
||||||
|
Destroy();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool HiddenGlWindow::Create(unsigned width, unsigned height, std::string& error)
|
||||||
|
{
|
||||||
|
return CreateShared(width, height, nullptr, nullptr, error);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool HiddenGlWindow::CreateShared(unsigned width, unsigned height, HDC sharedDeviceContext, HGLRC sharedContext, std::string& error)
|
||||||
|
{
|
||||||
|
Destroy();
|
||||||
|
|
||||||
|
mInstance = GetModuleHandle(nullptr);
|
||||||
|
|
||||||
|
WNDCLASSA wc = {};
|
||||||
|
wc.style = CS_OWNDC;
|
||||||
|
wc.lpfnWndProc = HiddenGlWindow::WindowProc;
|
||||||
|
wc.hInstance = mInstance;
|
||||||
|
wc.lpszClassName = kWindowClassName;
|
||||||
|
|
||||||
|
mClassAtom = RegisterClassA(&wc);
|
||||||
|
if (mClassAtom == 0 && GetLastError() != ERROR_CLASS_ALREADY_EXISTS)
|
||||||
|
{
|
||||||
|
error = "RegisterClassA failed for hidden OpenGL window.";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
mWindow = CreateWindowA(
|
||||||
|
kWindowClassName,
|
||||||
|
"Render Cadence Compositor Hidden GL",
|
||||||
|
WS_OVERLAPPEDWINDOW,
|
||||||
|
CW_USEDEFAULT,
|
||||||
|
CW_USEDEFAULT,
|
||||||
|
static_cast<int>(width),
|
||||||
|
static_cast<int>(height),
|
||||||
|
nullptr,
|
||||||
|
nullptr,
|
||||||
|
mInstance,
|
||||||
|
nullptr);
|
||||||
|
if (!mWindow)
|
||||||
|
{
|
||||||
|
error = "CreateWindowA failed for hidden OpenGL window.";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
mDc = GetDC(mWindow);
|
||||||
|
if (!mDc)
|
||||||
|
{
|
||||||
|
error = "GetDC failed for hidden OpenGL window.";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
PIXELFORMATDESCRIPTOR pfd = {};
|
||||||
|
pfd.nSize = sizeof(pfd);
|
||||||
|
pfd.nVersion = 1;
|
||||||
|
pfd.dwFlags = PFD_DRAW_TO_WINDOW | PFD_SUPPORT_OPENGL | PFD_DOUBLEBUFFER;
|
||||||
|
pfd.iPixelType = PFD_TYPE_RGBA;
|
||||||
|
pfd.cColorBits = 32;
|
||||||
|
pfd.cDepthBits = 0;
|
||||||
|
pfd.iLayerType = PFD_MAIN_PLANE;
|
||||||
|
|
||||||
|
int pixelFormat = 0;
|
||||||
|
if (sharedDeviceContext != nullptr)
|
||||||
|
pixelFormat = GetPixelFormat(sharedDeviceContext);
|
||||||
|
if (pixelFormat == 0)
|
||||||
|
pixelFormat = ChoosePixelFormat(mDc, &pfd);
|
||||||
|
if (pixelFormat == 0 || !SetPixelFormat(mDc, pixelFormat, &pfd))
|
||||||
|
{
|
||||||
|
error = "Could not choose/set pixel format for hidden OpenGL window.";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
mGlrc = wglCreateContext(mDc);
|
||||||
|
if (!mGlrc)
|
||||||
|
{
|
||||||
|
error = "wglCreateContext failed for hidden OpenGL window.";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (sharedContext != nullptr && wglShareLists(sharedContext, mGlrc) != TRUE)
|
||||||
|
{
|
||||||
|
error = "wglShareLists failed for hidden OpenGL shared context.";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool HiddenGlWindow::MakeCurrent() const
|
||||||
|
{
|
||||||
|
return mDc != nullptr && mGlrc != nullptr && wglMakeCurrent(mDc, mGlrc) == TRUE;
|
||||||
|
}
|
||||||
|
|
||||||
|
void HiddenGlWindow::ClearCurrent() const
|
||||||
|
{
|
||||||
|
wglMakeCurrent(nullptr, nullptr);
|
||||||
|
}
|
||||||
|
|
||||||
|
void HiddenGlWindow::Destroy()
|
||||||
|
{
|
||||||
|
ClearCurrent();
|
||||||
|
|
||||||
|
if (mGlrc)
|
||||||
|
{
|
||||||
|
wglDeleteContext(mGlrc);
|
||||||
|
mGlrc = nullptr;
|
||||||
|
}
|
||||||
|
if (mWindow && mDc)
|
||||||
|
{
|
||||||
|
ReleaseDC(mWindow, mDc);
|
||||||
|
mDc = nullptr;
|
||||||
|
}
|
||||||
|
if (mWindow)
|
||||||
|
{
|
||||||
|
DestroyWindow(mWindow);
|
||||||
|
mWindow = nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
mInstance = nullptr;
|
||||||
|
mClassAtom = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
LRESULT CALLBACK HiddenGlWindow::WindowProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)
|
||||||
|
{
|
||||||
|
return DefWindowProc(hwnd, message, wParam, lParam);
|
||||||
|
}
|
||||||
32
apps/RenderCadenceCompositor/platform/HiddenGlWindow.h
Normal file
32
apps/RenderCadenceCompositor/platform/HiddenGlWindow.h
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <windows.h>
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
class HiddenGlWindow
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
HiddenGlWindow() = default;
|
||||||
|
HiddenGlWindow(const HiddenGlWindow&) = delete;
|
||||||
|
HiddenGlWindow& operator=(const HiddenGlWindow&) = delete;
|
||||||
|
~HiddenGlWindow();
|
||||||
|
|
||||||
|
bool Create(unsigned width, unsigned height, std::string& error);
|
||||||
|
bool CreateShared(unsigned width, unsigned height, HDC sharedDeviceContext, HGLRC sharedContext, std::string& error);
|
||||||
|
bool MakeCurrent() const;
|
||||||
|
void ClearCurrent() const;
|
||||||
|
void Destroy();
|
||||||
|
|
||||||
|
HDC DeviceContext() const { return mDc; }
|
||||||
|
HGLRC Context() const { return mGlrc; }
|
||||||
|
|
||||||
|
private:
|
||||||
|
static LRESULT CALLBACK WindowProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam);
|
||||||
|
|
||||||
|
HINSTANCE mInstance = nullptr;
|
||||||
|
HWND mWindow = nullptr;
|
||||||
|
HDC mDc = nullptr;
|
||||||
|
HGLRC mGlrc = nullptr;
|
||||||
|
ATOM mClassAtom = 0;
|
||||||
|
};
|
||||||
341
apps/RenderCadenceCompositor/render/InputFrameTexture.cpp
Normal file
341
apps/RenderCadenceCompositor/render/InputFrameTexture.cpp
Normal file
@@ -0,0 +1,341 @@
|
|||||||
|
#include "InputFrameTexture.h"
|
||||||
|
|
||||||
|
#include <chrono>
|
||||||
|
|
||||||
|
#ifndef GL_FRAMEBUFFER_BINDING
|
||||||
|
#define GL_FRAMEBUFFER_BINDING 0x8CA6
|
||||||
|
#endif
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
constexpr GLuint kUyvyTextureUnit = 0;
|
||||||
|
|
||||||
|
const char* kDecodeVertexShader = R"GLSL(
|
||||||
|
#version 430 core
|
||||||
|
out vec2 vTexCoord;
|
||||||
|
void main()
|
||||||
|
{
|
||||||
|
vec2 positions[3] = vec2[3](
|
||||||
|
vec2(-1.0, -1.0),
|
||||||
|
vec2( 3.0, -1.0),
|
||||||
|
vec2(-1.0, 3.0));
|
||||||
|
vec2 texCoords[3] = vec2[3](
|
||||||
|
vec2(0.0, 0.0),
|
||||||
|
vec2(2.0, 0.0),
|
||||||
|
vec2(0.0, 2.0));
|
||||||
|
gl_Position = vec4(positions[gl_VertexID], 0.0, 1.0);
|
||||||
|
vTexCoord = texCoords[gl_VertexID];
|
||||||
|
}
|
||||||
|
)GLSL";
|
||||||
|
|
||||||
|
const char* kUyvyDecodeFragmentShader = R"GLSL(
|
||||||
|
#version 430 core
|
||||||
|
layout(binding = 0) uniform sampler2D uPackedUyvy;
|
||||||
|
uniform vec2 uDecodedSize;
|
||||||
|
in vec2 vTexCoord;
|
||||||
|
out vec4 fragColor;
|
||||||
|
|
||||||
|
vec4 rec709YCbCr2rgba(float yByte, float cbByte, float crByte)
|
||||||
|
{
|
||||||
|
float y = (yByte - 16.0) / 219.0;
|
||||||
|
float cb = (cbByte - 16.0) / 224.0 - 0.5;
|
||||||
|
float cr = (crByte - 16.0) / 224.0 - 0.5;
|
||||||
|
return vec4(
|
||||||
|
y + 1.5748 * cr,
|
||||||
|
y - 0.1873 * cb - 0.4681 * cr,
|
||||||
|
y + 1.8556 * cb,
|
||||||
|
1.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
void main()
|
||||||
|
{
|
||||||
|
ivec2 decodedSize = ivec2(uDecodedSize);
|
||||||
|
ivec2 outputCoord = ivec2(clamp(gl_FragCoord.xy, vec2(0.0), vec2(decodedSize - ivec2(1))));
|
||||||
|
int sourceY = decodedSize.y - 1 - outputCoord.y;
|
||||||
|
ivec2 packedCoord = ivec2(clamp(outputCoord.x / 2, 0, max(decodedSize.x / 2 - 1, 0)), sourceY);
|
||||||
|
vec4 macroPixel = texelFetch(uPackedUyvy, packedCoord, 0) * 255.0;
|
||||||
|
float ySample = (outputCoord.x & 1) != 0 ? macroPixel.a : macroPixel.g;
|
||||||
|
fragColor = clamp(rec709YCbCr2rgba(ySample, macroPixel.r, macroPixel.b), vec4(0.0), vec4(1.0));
|
||||||
|
}
|
||||||
|
)GLSL";
|
||||||
|
}
|
||||||
|
|
||||||
|
InputFrameTexture::~InputFrameTexture()
|
||||||
|
{
|
||||||
|
ShutdownGl();
|
||||||
|
}
|
||||||
|
|
||||||
|
GLuint InputFrameTexture::PollAndUpload(InputFrameMailbox* mailbox)
|
||||||
|
{
|
||||||
|
if (mailbox == nullptr)
|
||||||
|
return mTexture;
|
||||||
|
|
||||||
|
InputFrame frame;
|
||||||
|
if (!mailbox->TryAcquireOldest(frame))
|
||||||
|
{
|
||||||
|
++mUploadMisses;
|
||||||
|
mLastUploadMilliseconds = 0.0;
|
||||||
|
return mTexture;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (frame.bytes != nullptr && frame.pixelFormat == VideoIOPixelFormat::Bgra8 && EnsureTexture(frame))
|
||||||
|
{
|
||||||
|
mLastFrameFormatSupported = true;
|
||||||
|
const auto uploadStart = std::chrono::steady_clock::now();
|
||||||
|
UploadBgra8FrameFlippedVertically(frame);
|
||||||
|
const auto uploadEnd = std::chrono::steady_clock::now();
|
||||||
|
mLastUploadMilliseconds = std::chrono::duration_cast<std::chrono::duration<double, std::milli>>(uploadEnd - uploadStart).count();
|
||||||
|
++mUploadedFrames;
|
||||||
|
}
|
||||||
|
else if (frame.bytes != nullptr && frame.pixelFormat == VideoIOPixelFormat::Uyvy8 && EnsureTexture(frame) && EnsureRawUyvyTexture(frame) && EnsureDecodeProgram())
|
||||||
|
{
|
||||||
|
mLastFrameFormatSupported = true;
|
||||||
|
const auto uploadStart = std::chrono::steady_clock::now();
|
||||||
|
UploadUyvy8Frame(frame);
|
||||||
|
DecodeUyvy8Frame(frame);
|
||||||
|
const auto uploadEnd = std::chrono::steady_clock::now();
|
||||||
|
mLastUploadMilliseconds = std::chrono::duration_cast<std::chrono::duration<double, std::milli>>(uploadEnd - uploadStart).count();
|
||||||
|
++mUploadedFrames;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
mLastFrameFormatSupported = frame.pixelFormat == VideoIOPixelFormat::Bgra8 || frame.pixelFormat == VideoIOPixelFormat::Uyvy8;
|
||||||
|
mLastUploadMilliseconds = 0.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
mailbox->Release(frame);
|
||||||
|
return mTexture;
|
||||||
|
}
|
||||||
|
|
||||||
|
void InputFrameTexture::ShutdownGl()
|
||||||
|
{
|
||||||
|
if (mTexture != 0)
|
||||||
|
glDeleteTextures(1, &mTexture);
|
||||||
|
if (mRawTexture != 0)
|
||||||
|
glDeleteTextures(1, &mRawTexture);
|
||||||
|
mTexture = 0;
|
||||||
|
mRawTexture = 0;
|
||||||
|
mWidth = 0;
|
||||||
|
mHeight = 0;
|
||||||
|
mRawWidth = 0;
|
||||||
|
mRawHeight = 0;
|
||||||
|
DestroyDecodeResources();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool InputFrameTexture::EnsureTexture(const InputFrame& frame)
|
||||||
|
{
|
||||||
|
if (frame.width == 0 || frame.height == 0)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if (mTexture != 0 && mWidth == frame.width && mHeight == frame.height)
|
||||||
|
return true;
|
||||||
|
|
||||||
|
ShutdownGl();
|
||||||
|
glGenTextures(1, &mTexture);
|
||||||
|
glBindTexture(GL_TEXTURE_2D, mTexture);
|
||||||
|
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>(frame.width),
|
||||||
|
static_cast<GLsizei>(frame.height),
|
||||||
|
0,
|
||||||
|
GL_BGRA,
|
||||||
|
GL_UNSIGNED_INT_8_8_8_8_REV,
|
||||||
|
nullptr);
|
||||||
|
glBindTexture(GL_TEXTURE_2D, 0);
|
||||||
|
|
||||||
|
mWidth = frame.width;
|
||||||
|
mHeight = frame.height;
|
||||||
|
return mTexture != 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool InputFrameTexture::EnsureRawUyvyTexture(const InputFrame& frame)
|
||||||
|
{
|
||||||
|
if (frame.width == 0 || frame.height == 0)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
const unsigned rawWidth = (frame.width + 1u) / 2u;
|
||||||
|
if (mRawTexture != 0 && mRawWidth == rawWidth && mRawHeight == frame.height)
|
||||||
|
return true;
|
||||||
|
|
||||||
|
if (mRawTexture != 0)
|
||||||
|
glDeleteTextures(1, &mRawTexture);
|
||||||
|
mRawTexture = 0;
|
||||||
|
glGenTextures(1, &mRawTexture);
|
||||||
|
glBindTexture(GL_TEXTURE_2D, mRawTexture);
|
||||||
|
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);
|
||||||
|
glTexImage2D(
|
||||||
|
GL_TEXTURE_2D,
|
||||||
|
0,
|
||||||
|
GL_RGBA8,
|
||||||
|
static_cast<GLsizei>(rawWidth),
|
||||||
|
static_cast<GLsizei>(frame.height),
|
||||||
|
0,
|
||||||
|
GL_RGBA,
|
||||||
|
GL_UNSIGNED_BYTE,
|
||||||
|
nullptr);
|
||||||
|
glBindTexture(GL_TEXTURE_2D, 0);
|
||||||
|
|
||||||
|
mRawWidth = rawWidth;
|
||||||
|
mRawHeight = frame.height;
|
||||||
|
return mRawTexture != 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
void InputFrameTexture::UploadBgra8FrameFlippedVertically(const InputFrame& frame)
|
||||||
|
{
|
||||||
|
glBindTexture(GL_TEXTURE_2D, mTexture);
|
||||||
|
glPixelStorei(GL_UNPACK_ALIGNMENT, 4);
|
||||||
|
glPixelStorei(GL_UNPACK_ROW_LENGTH, frame.rowBytes > 0 ? static_cast<GLint>(frame.rowBytes / 4) : 0);
|
||||||
|
|
||||||
|
const unsigned char* sourceBytes = static_cast<const unsigned char*>(frame.bytes);
|
||||||
|
for (unsigned destinationY = 0; destinationY < frame.height; ++destinationY)
|
||||||
|
{
|
||||||
|
const unsigned sourceY = frame.height - 1u - destinationY;
|
||||||
|
const unsigned char* sourceRow = sourceBytes + static_cast<std::size_t>(sourceY) * static_cast<std::size_t>(frame.rowBytes);
|
||||||
|
glTexSubImage2D(
|
||||||
|
GL_TEXTURE_2D,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
static_cast<GLint>(destinationY),
|
||||||
|
static_cast<GLsizei>(frame.width),
|
||||||
|
1,
|
||||||
|
GL_BGRA,
|
||||||
|
GL_UNSIGNED_INT_8_8_8_8_REV,
|
||||||
|
sourceRow);
|
||||||
|
}
|
||||||
|
|
||||||
|
glPixelStorei(GL_UNPACK_ROW_LENGTH, 0);
|
||||||
|
glBindTexture(GL_TEXTURE_2D, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
void InputFrameTexture::UploadUyvy8Frame(const InputFrame& frame)
|
||||||
|
{
|
||||||
|
glBindTexture(GL_TEXTURE_2D, mRawTexture);
|
||||||
|
glPixelStorei(GL_UNPACK_ALIGNMENT, 4);
|
||||||
|
glPixelStorei(GL_UNPACK_ROW_LENGTH, frame.rowBytes > 0 ? static_cast<GLint>(frame.rowBytes / 4) : 0);
|
||||||
|
glTexSubImage2D(
|
||||||
|
GL_TEXTURE_2D,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
static_cast<GLsizei>((frame.width + 1u) / 2u),
|
||||||
|
static_cast<GLsizei>(frame.height),
|
||||||
|
GL_RGBA,
|
||||||
|
GL_UNSIGNED_BYTE,
|
||||||
|
frame.bytes);
|
||||||
|
glPixelStorei(GL_UNPACK_ROW_LENGTH, 0);
|
||||||
|
glBindTexture(GL_TEXTURE_2D, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
void InputFrameTexture::DecodeUyvy8Frame(const InputFrame& frame)
|
||||||
|
{
|
||||||
|
GLint previousFramebuffer = 0;
|
||||||
|
glGetIntegerv(GL_FRAMEBUFFER_BINDING, &previousFramebuffer);
|
||||||
|
|
||||||
|
if (mDecodeFramebuffer == 0)
|
||||||
|
glGenFramebuffers(1, &mDecodeFramebuffer);
|
||||||
|
glBindFramebuffer(GL_FRAMEBUFFER, mDecodeFramebuffer);
|
||||||
|
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, mTexture, 0);
|
||||||
|
if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE)
|
||||||
|
{
|
||||||
|
glBindFramebuffer(GL_FRAMEBUFFER, static_cast<GLuint>(previousFramebuffer));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
glViewport(0, 0, static_cast<GLsizei>(frame.width), static_cast<GLsizei>(frame.height));
|
||||||
|
glDisable(GL_SCISSOR_TEST);
|
||||||
|
glDisable(GL_DEPTH_TEST);
|
||||||
|
glDisable(GL_BLEND);
|
||||||
|
glUseProgram(mDecodeProgram);
|
||||||
|
const GLint decodedSizeLocation = glGetUniformLocation(mDecodeProgram, "uDecodedSize");
|
||||||
|
if (decodedSizeLocation >= 0)
|
||||||
|
glUniform2f(decodedSizeLocation, static_cast<GLfloat>(frame.width), static_cast<GLfloat>(frame.height));
|
||||||
|
glActiveTexture(GL_TEXTURE0 + kUyvyTextureUnit);
|
||||||
|
glBindTexture(GL_TEXTURE_2D, mRawTexture);
|
||||||
|
glBindVertexArray(mDecodeVertexArray);
|
||||||
|
glDrawArrays(GL_TRIANGLES, 0, 3);
|
||||||
|
glBindVertexArray(0);
|
||||||
|
glBindTexture(GL_TEXTURE_2D, 0);
|
||||||
|
glActiveTexture(GL_TEXTURE0);
|
||||||
|
glUseProgram(0);
|
||||||
|
glBindFramebuffer(GL_FRAMEBUFFER, static_cast<GLuint>(previousFramebuffer));
|
||||||
|
}
|
||||||
|
|
||||||
|
bool InputFrameTexture::EnsureDecodeProgram()
|
||||||
|
{
|
||||||
|
if (mDecodeProgram != 0)
|
||||||
|
return true;
|
||||||
|
|
||||||
|
if (!CompileShader(GL_VERTEX_SHADER, kDecodeVertexShader, mDecodeVertexShader))
|
||||||
|
return false;
|
||||||
|
if (!CompileShader(GL_FRAGMENT_SHADER, kUyvyDecodeFragmentShader, mDecodeFragmentShader))
|
||||||
|
return false;
|
||||||
|
if (!LinkProgram(mDecodeVertexShader, mDecodeFragmentShader, mDecodeProgram))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
glUseProgram(mDecodeProgram);
|
||||||
|
const GLint samplerLocation = glGetUniformLocation(mDecodeProgram, "uPackedUyvy");
|
||||||
|
if (samplerLocation >= 0)
|
||||||
|
glUniform1i(samplerLocation, static_cast<GLint>(kUyvyTextureUnit));
|
||||||
|
glUseProgram(0);
|
||||||
|
|
||||||
|
if (mDecodeVertexArray == 0)
|
||||||
|
glGenVertexArrays(1, &mDecodeVertexArray);
|
||||||
|
return mDecodeProgram != 0 && mDecodeVertexArray != 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
void InputFrameTexture::DestroyDecodeResources()
|
||||||
|
{
|
||||||
|
if (mDecodeFramebuffer != 0)
|
||||||
|
glDeleteFramebuffers(1, &mDecodeFramebuffer);
|
||||||
|
if (mDecodeVertexArray != 0)
|
||||||
|
glDeleteVertexArrays(1, &mDecodeVertexArray);
|
||||||
|
if (mDecodeProgram != 0)
|
||||||
|
glDeleteProgram(mDecodeProgram);
|
||||||
|
if (mDecodeVertexShader != 0)
|
||||||
|
glDeleteShader(mDecodeVertexShader);
|
||||||
|
if (mDecodeFragmentShader != 0)
|
||||||
|
glDeleteShader(mDecodeFragmentShader);
|
||||||
|
mDecodeFramebuffer = 0;
|
||||||
|
mDecodeVertexArray = 0;
|
||||||
|
mDecodeProgram = 0;
|
||||||
|
mDecodeVertexShader = 0;
|
||||||
|
mDecodeFragmentShader = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool InputFrameTexture::CompileShader(GLenum shaderType, const char* source, GLuint& shader)
|
||||||
|
{
|
||||||
|
shader = glCreateShader(shaderType);
|
||||||
|
glShaderSource(shader, 1, &source, nullptr);
|
||||||
|
glCompileShader(shader);
|
||||||
|
GLint compileResult = GL_FALSE;
|
||||||
|
glGetShaderiv(shader, GL_COMPILE_STATUS, &compileResult);
|
||||||
|
if (compileResult != GL_FALSE)
|
||||||
|
return true;
|
||||||
|
glDeleteShader(shader);
|
||||||
|
shader = 0;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool InputFrameTexture::LinkProgram(GLuint vertexShader, GLuint fragmentShader, GLuint& program)
|
||||||
|
{
|
||||||
|
program = glCreateProgram();
|
||||||
|
glAttachShader(program, vertexShader);
|
||||||
|
glAttachShader(program, fragmentShader);
|
||||||
|
glLinkProgram(program);
|
||||||
|
GLint linkResult = GL_FALSE;
|
||||||
|
glGetProgramiv(program, GL_LINK_STATUS, &linkResult);
|
||||||
|
if (linkResult != GL_FALSE)
|
||||||
|
return true;
|
||||||
|
glDeleteProgram(program);
|
||||||
|
program = 0;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
50
apps/RenderCadenceCompositor/render/InputFrameTexture.h
Normal file
50
apps/RenderCadenceCompositor/render/InputFrameTexture.h
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "../frames/InputFrameMailbox.h"
|
||||||
|
#include "GLExtensions.h"
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
|
||||||
|
class InputFrameTexture
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
InputFrameTexture() = default;
|
||||||
|
InputFrameTexture(const InputFrameTexture&) = delete;
|
||||||
|
InputFrameTexture& operator=(const InputFrameTexture&) = delete;
|
||||||
|
~InputFrameTexture();
|
||||||
|
|
||||||
|
GLuint PollAndUpload(InputFrameMailbox* mailbox);
|
||||||
|
GLuint Texture() const { return mTexture; }
|
||||||
|
uint64_t UploadedFrames() const { return mUploadedFrames; }
|
||||||
|
uint64_t UploadMisses() const { return mUploadMisses; }
|
||||||
|
double LastUploadMilliseconds() const { return mLastUploadMilliseconds; }
|
||||||
|
bool LastFrameFormatSupported() const { return mLastFrameFormatSupported; }
|
||||||
|
void ShutdownGl();
|
||||||
|
|
||||||
|
private:
|
||||||
|
bool EnsureTexture(const InputFrame& frame);
|
||||||
|
bool EnsureRawUyvyTexture(const InputFrame& frame);
|
||||||
|
bool EnsureDecodeProgram();
|
||||||
|
void UploadBgra8FrameFlippedVertically(const InputFrame& frame);
|
||||||
|
void UploadUyvy8Frame(const InputFrame& frame);
|
||||||
|
void DecodeUyvy8Frame(const InputFrame& frame);
|
||||||
|
void DestroyDecodeResources();
|
||||||
|
static bool CompileShader(GLenum shaderType, const char* source, GLuint& shader);
|
||||||
|
static bool LinkProgram(GLuint vertexShader, GLuint fragmentShader, GLuint& program);
|
||||||
|
|
||||||
|
GLuint mTexture = 0;
|
||||||
|
GLuint mRawTexture = 0;
|
||||||
|
GLuint mDecodeFramebuffer = 0;
|
||||||
|
GLuint mDecodeVertexArray = 0;
|
||||||
|
GLuint mDecodeProgram = 0;
|
||||||
|
GLuint mDecodeVertexShader = 0;
|
||||||
|
GLuint mDecodeFragmentShader = 0;
|
||||||
|
unsigned mWidth = 0;
|
||||||
|
unsigned mHeight = 0;
|
||||||
|
unsigned mRawWidth = 0;
|
||||||
|
unsigned mRawHeight = 0;
|
||||||
|
uint64_t mUploadedFrames = 0;
|
||||||
|
uint64_t mUploadMisses = 0;
|
||||||
|
double mLastUploadMilliseconds = 0.0;
|
||||||
|
bool mLastFrameFormatSupported = true;
|
||||||
|
};
|
||||||
49
apps/RenderCadenceCompositor/render/RenderCadenceClock.cpp
Normal file
49
apps/RenderCadenceCompositor/render/RenderCadenceClock.cpp
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
#include "RenderCadenceClock.h"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
|
||||||
|
RenderCadenceClock::RenderCadenceClock(double frameDurationMilliseconds)
|
||||||
|
{
|
||||||
|
mFrameDuration = std::chrono::duration_cast<Duration>(std::chrono::duration<double, std::milli>(frameDurationMilliseconds));
|
||||||
|
if (mFrameDuration <= Duration::zero())
|
||||||
|
mFrameDuration = std::chrono::milliseconds(16);
|
||||||
|
Reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
void RenderCadenceClock::Reset(TimePoint now)
|
||||||
|
{
|
||||||
|
mNextRenderTime = now;
|
||||||
|
mPendingFrameAdvance = 1;
|
||||||
|
mOverrunCount = 0;
|
||||||
|
mSkippedFrameCount = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
RenderCadenceClock::Tick RenderCadenceClock::Poll(TimePoint now)
|
||||||
|
{
|
||||||
|
Tick tick;
|
||||||
|
if (now < mNextRenderTime)
|
||||||
|
{
|
||||||
|
tick.sleepFor = std::min(Duration(std::chrono::milliseconds(1)), mNextRenderTime - now);
|
||||||
|
return tick;
|
||||||
|
}
|
||||||
|
|
||||||
|
tick.due = true;
|
||||||
|
mPendingFrameAdvance = 1;
|
||||||
|
const Duration lateBy = now - mNextRenderTime;
|
||||||
|
if (lateBy > mFrameDuration)
|
||||||
|
{
|
||||||
|
tick.skippedFrames = static_cast<uint64_t>(lateBy / mFrameDuration);
|
||||||
|
mPendingFrameAdvance += tick.skippedFrames;
|
||||||
|
++mOverrunCount;
|
||||||
|
mSkippedFrameCount += tick.skippedFrames;
|
||||||
|
}
|
||||||
|
return tick;
|
||||||
|
}
|
||||||
|
|
||||||
|
void RenderCadenceClock::MarkRendered(TimePoint now)
|
||||||
|
{
|
||||||
|
mNextRenderTime += mFrameDuration * mPendingFrameAdvance;
|
||||||
|
mPendingFrameAdvance = 1;
|
||||||
|
if (now - mNextRenderTime > mFrameDuration * 4)
|
||||||
|
mNextRenderTime = now + mFrameDuration;
|
||||||
|
}
|
||||||
37
apps/RenderCadenceCompositor/render/RenderCadenceClock.h
Normal file
37
apps/RenderCadenceCompositor/render/RenderCadenceClock.h
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <chrono>
|
||||||
|
#include <cstdint>
|
||||||
|
|
||||||
|
class RenderCadenceClock
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
using Clock = std::chrono::steady_clock;
|
||||||
|
using Duration = Clock::duration;
|
||||||
|
using TimePoint = Clock::time_point;
|
||||||
|
|
||||||
|
struct Tick
|
||||||
|
{
|
||||||
|
bool due = false;
|
||||||
|
uint64_t skippedFrames = 0;
|
||||||
|
Duration sleepFor = Duration::zero();
|
||||||
|
};
|
||||||
|
|
||||||
|
explicit RenderCadenceClock(double frameDurationMilliseconds = 1000.0 / 60.0);
|
||||||
|
|
||||||
|
void Reset(TimePoint now = Clock::now());
|
||||||
|
Tick Poll(TimePoint now = Clock::now());
|
||||||
|
void MarkRendered(TimePoint now = Clock::now());
|
||||||
|
|
||||||
|
Duration FrameDuration() const { return mFrameDuration; }
|
||||||
|
TimePoint NextRenderTime() const { return mNextRenderTime; }
|
||||||
|
uint64_t OverrunCount() const { return mOverrunCount; }
|
||||||
|
uint64_t SkippedFrameCount() const { return mSkippedFrameCount; }
|
||||||
|
|
||||||
|
private:
|
||||||
|
Duration mFrameDuration;
|
||||||
|
TimePoint mNextRenderTime = Clock::now();
|
||||||
|
uint64_t mPendingFrameAdvance = 1;
|
||||||
|
uint64_t mOverrunCount = 0;
|
||||||
|
uint64_t mSkippedFrameCount = 0;
|
||||||
|
};
|
||||||
389
apps/RenderCadenceCompositor/render/RenderThread.cpp
Normal file
389
apps/RenderCadenceCompositor/render/RenderThread.cpp
Normal file
@@ -0,0 +1,389 @@
|
|||||||
|
#include "RenderThread.h"
|
||||||
|
|
||||||
|
#include "../frames/InputFrameMailbox.h"
|
||||||
|
#include "../frames/SystemFrameExchange.h"
|
||||||
|
#include "../frames/SystemFrameTypes.h"
|
||||||
|
#include "../logging/Logger.h"
|
||||||
|
#include "../platform/HiddenGlWindow.h"
|
||||||
|
#include "InputFrameTexture.h"
|
||||||
|
#include "readback/Bgra8ReadbackPipeline.h"
|
||||||
|
#include "GLExtensions.h"
|
||||||
|
#include "runtime/RuntimeRenderScene.h"
|
||||||
|
#include "runtime/RuntimeShaderRenderer.h"
|
||||||
|
#include "SimpleMotionRenderer.h"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <memory>
|
||||||
|
#include <thread>
|
||||||
|
|
||||||
|
RenderThread::RenderThread(SystemFrameExchange& frameExchange, Config config) :
|
||||||
|
mFrameExchange(frameExchange),
|
||||||
|
mConfig(config)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
RenderThread::RenderThread(SystemFrameExchange& frameExchange, InputFrameMailbox* inputMailbox, Config config) :
|
||||||
|
mFrameExchange(frameExchange),
|
||||||
|
mInputMailbox(inputMailbox),
|
||||||
|
mConfig(config)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
RenderThread::~RenderThread()
|
||||||
|
{
|
||||||
|
Stop();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool RenderThread::Start(std::string& error)
|
||||||
|
{
|
||||||
|
if (mThread.joinable())
|
||||||
|
return true;
|
||||||
|
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(mStartupMutex);
|
||||||
|
mStarted = false;
|
||||||
|
mStartupError.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
mStopping.store(false, std::memory_order_release);
|
||||||
|
mThread = std::thread([this]() { ThreadMain(); });
|
||||||
|
|
||||||
|
std::unique_lock<std::mutex> lock(mStartupMutex);
|
||||||
|
if (!mStartupCondition.wait_for(lock, std::chrono::seconds(3), [this]() {
|
||||||
|
return mStarted || !mStartupError.empty();
|
||||||
|
}))
|
||||||
|
{
|
||||||
|
error = "Timed out starting render thread.";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!mStartupError.empty())
|
||||||
|
{
|
||||||
|
error = mStartupError;
|
||||||
|
lock.unlock();
|
||||||
|
if (mThread.joinable())
|
||||||
|
mThread.join();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void RenderThread::Stop()
|
||||||
|
{
|
||||||
|
mStopping.store(true, std::memory_order_release);
|
||||||
|
if (mThread.joinable())
|
||||||
|
mThread.join();
|
||||||
|
}
|
||||||
|
|
||||||
|
RenderThread::Metrics RenderThread::GetMetrics() const
|
||||||
|
{
|
||||||
|
Metrics metrics;
|
||||||
|
metrics.renderedFrames = mRenderedFrames.load(std::memory_order_relaxed);
|
||||||
|
metrics.completedReadbacks = mCompletedReadbacks.load(std::memory_order_relaxed);
|
||||||
|
metrics.acquireMisses = mAcquireMisses.load(std::memory_order_relaxed);
|
||||||
|
metrics.pboQueueMisses = mPboQueueMisses.load(std::memory_order_relaxed);
|
||||||
|
metrics.clockOverruns = mClockOverruns.load(std::memory_order_relaxed);
|
||||||
|
metrics.skippedFrames = mSkippedFrames.load(std::memory_order_relaxed);
|
||||||
|
metrics.shaderBuildsCommitted = mShaderBuildsCommitted.load(std::memory_order_relaxed);
|
||||||
|
metrics.shaderBuildFailures = mShaderBuildFailures.load(std::memory_order_relaxed);
|
||||||
|
metrics.renderFrameMilliseconds = mRenderFrameMilliseconds.load(std::memory_order_relaxed);
|
||||||
|
metrics.renderFrameBudgetUsedPercent = mRenderFrameBudgetUsedPercent.load(std::memory_order_relaxed);
|
||||||
|
metrics.renderFrameMaxMilliseconds = mRenderFrameMaxMilliseconds.load(std::memory_order_relaxed);
|
||||||
|
metrics.readbackQueueMilliseconds = mReadbackQueueMilliseconds.load(std::memory_order_relaxed);
|
||||||
|
metrics.completedReadbackCopyMilliseconds = mCompletedReadbackCopyMilliseconds.load(std::memory_order_relaxed);
|
||||||
|
metrics.inputFramesReceived = mInputFramesReceived.load(std::memory_order_relaxed);
|
||||||
|
metrics.inputFramesDropped = mInputFramesDropped.load(std::memory_order_relaxed);
|
||||||
|
metrics.inputConsumeMisses = mInputConsumeMisses.load(std::memory_order_relaxed);
|
||||||
|
metrics.inputUploadMisses = mInputUploadMisses.load(std::memory_order_relaxed);
|
||||||
|
metrics.inputReadyFrames = mInputReadyFrames.load(std::memory_order_relaxed);
|
||||||
|
metrics.inputReadingFrames = mInputReadingFrames.load(std::memory_order_relaxed);
|
||||||
|
metrics.inputLatestAgeMilliseconds = mInputLatestAgeMilliseconds.load(std::memory_order_relaxed);
|
||||||
|
metrics.inputUploadMilliseconds = mInputUploadMilliseconds.load(std::memory_order_relaxed);
|
||||||
|
metrics.inputFormatSupported = mInputFormatSupported.load(std::memory_order_relaxed);
|
||||||
|
metrics.inputSignalPresent = mInputSignalPresent.load(std::memory_order_relaxed);
|
||||||
|
return metrics;
|
||||||
|
}
|
||||||
|
|
||||||
|
void RenderThread::ThreadMain()
|
||||||
|
{
|
||||||
|
RenderCadenceCompositor::TryLog(RenderCadenceCompositor::LogLevel::Log, "render-thread", "Render thread starting.");
|
||||||
|
HiddenGlWindow window;
|
||||||
|
std::string error;
|
||||||
|
if (!window.Create(mConfig.width, mConfig.height, error))
|
||||||
|
{
|
||||||
|
SignalStartupFailure(error.empty() ? "OpenGL context creation failed." : error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
std::unique_ptr<HiddenGlWindow> prepareWindow = std::make_unique<HiddenGlWindow>();
|
||||||
|
if (!prepareWindow->CreateShared(mConfig.width, mConfig.height, window.DeviceContext(), window.Context(), error))
|
||||||
|
{
|
||||||
|
SignalStartupFailure(error.empty() ? "Runtime shader prepare shared context creation failed." : error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!window.MakeCurrent())
|
||||||
|
{
|
||||||
|
SignalStartupFailure("OpenGL context creation failed.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!ResolveGLExtensions())
|
||||||
|
{
|
||||||
|
SignalStartupFailure("OpenGL extension resolution failed.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
SimpleMotionRenderer renderer;
|
||||||
|
RuntimeRenderScene runtimeRenderScene;
|
||||||
|
Bgra8ReadbackPipeline readback;
|
||||||
|
InputFrameTexture inputTexture;
|
||||||
|
if (!runtimeRenderScene.StartPrepareWorker(std::move(prepareWindow), error))
|
||||||
|
{
|
||||||
|
SignalStartupFailure(error.empty() ? "Runtime shader prepare worker initialization failed." : error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!renderer.InitializeGl(mConfig.width, mConfig.height) || !readback.Initialize(mConfig.width, mConfig.height, mConfig.pboDepth))
|
||||||
|
{
|
||||||
|
SignalStartupFailure("Render pipeline initialization failed.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
RenderCadenceClock clock(mConfig.frameDurationMilliseconds);
|
||||||
|
uint64_t frameIndex = 0;
|
||||||
|
mRunning.store(true, std::memory_order_release);
|
||||||
|
SignalStarted();
|
||||||
|
|
||||||
|
while (!mStopping.load(std::memory_order_acquire))
|
||||||
|
{
|
||||||
|
readback.ConsumeCompleted(
|
||||||
|
[this](SystemFrame& frame) { return mFrameExchange.AcquireForRender(frame); },
|
||||||
|
[this](const SystemFrame& frame) { return mFrameExchange.PublishCompleted(frame); },
|
||||||
|
[this]() {
|
||||||
|
CountAcquireMiss();
|
||||||
|
},
|
||||||
|
[this]() { CountCompleted(); });
|
||||||
|
PublishReadbackMetrics(readback);
|
||||||
|
|
||||||
|
const auto now = RenderCadenceClock::Clock::now();
|
||||||
|
const RenderCadenceClock::Tick tick = clock.Poll(now);
|
||||||
|
if (!tick.due)
|
||||||
|
{
|
||||||
|
if (tick.sleepFor > RenderCadenceClock::Duration::zero())
|
||||||
|
std::this_thread::sleep_for(tick.sleepFor);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
TryCommitReadyRuntimeShader(runtimeRenderScene);
|
||||||
|
const GLuint videoInputTexture = inputTexture.PollAndUpload(mInputMailbox);
|
||||||
|
PublishInputMetrics(inputTexture);
|
||||||
|
if (!readback.RenderAndQueue(frameIndex, [this, &renderer, &runtimeRenderScene, videoInputTexture](uint64_t index) {
|
||||||
|
if (runtimeRenderScene.HasLayers())
|
||||||
|
runtimeRenderScene.RenderFrame(index, mConfig.width, mConfig.height, videoInputTexture);
|
||||||
|
else if (videoInputTexture != 0)
|
||||||
|
renderer.RenderTexture(videoInputTexture);
|
||||||
|
else
|
||||||
|
renderer.RenderFrame(index);
|
||||||
|
}))
|
||||||
|
{
|
||||||
|
mPboQueueMisses.fetch_add(1, std::memory_order_relaxed);
|
||||||
|
}
|
||||||
|
PublishReadbackMetrics(readback);
|
||||||
|
|
||||||
|
CountRendered();
|
||||||
|
++frameIndex;
|
||||||
|
clock.MarkRendered(RenderCadenceClock::Clock::now());
|
||||||
|
|
||||||
|
mClockOverruns.store(clock.OverrunCount(), std::memory_order_relaxed);
|
||||||
|
mSkippedFrames.store(clock.SkippedFrameCount(), std::memory_order_relaxed);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (std::size_t i = 0; i < mConfig.pboDepth * 2; ++i)
|
||||||
|
{
|
||||||
|
readback.ConsumeCompleted(
|
||||||
|
[this](SystemFrame& frame) { return mFrameExchange.AcquireForRender(frame); },
|
||||||
|
[this](const SystemFrame& frame) { return mFrameExchange.PublishCompleted(frame); },
|
||||||
|
[this]() {
|
||||||
|
CountAcquireMiss();
|
||||||
|
},
|
||||||
|
[this]() { CountCompleted(); });
|
||||||
|
PublishReadbackMetrics(readback);
|
||||||
|
}
|
||||||
|
|
||||||
|
readback.Shutdown();
|
||||||
|
inputTexture.ShutdownGl();
|
||||||
|
runtimeRenderScene.ShutdownGl();
|
||||||
|
renderer.ShutdownGl();
|
||||||
|
window.ClearCurrent();
|
||||||
|
mRunning.store(false, std::memory_order_release);
|
||||||
|
RenderCadenceCompositor::TryLog(RenderCadenceCompositor::LogLevel::Log, "render-thread", "Render thread stopped.");
|
||||||
|
}
|
||||||
|
|
||||||
|
void RenderThread::SignalStarted()
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(mStartupMutex);
|
||||||
|
mStarted = true;
|
||||||
|
mStartupCondition.notify_all();
|
||||||
|
}
|
||||||
|
|
||||||
|
void RenderThread::SignalStartupFailure(const std::string& error)
|
||||||
|
{
|
||||||
|
RenderCadenceCompositor::TryLog(RenderCadenceCompositor::LogLevel::Error, "render-thread", error);
|
||||||
|
std::lock_guard<std::mutex> lock(mStartupMutex);
|
||||||
|
mStartupError = error;
|
||||||
|
mStartupCondition.notify_all();
|
||||||
|
}
|
||||||
|
|
||||||
|
void RenderThread::CountRendered()
|
||||||
|
{
|
||||||
|
mRenderedFrames.fetch_add(1, std::memory_order_relaxed);
|
||||||
|
}
|
||||||
|
|
||||||
|
void RenderThread::CountCompleted()
|
||||||
|
{
|
||||||
|
mCompletedReadbacks.fetch_add(1, std::memory_order_relaxed);
|
||||||
|
}
|
||||||
|
|
||||||
|
void RenderThread::CountAcquireMiss()
|
||||||
|
{
|
||||||
|
mAcquireMisses.fetch_add(1, std::memory_order_relaxed);
|
||||||
|
}
|
||||||
|
|
||||||
|
void RenderThread::PublishReadbackMetrics(const Bgra8ReadbackPipeline& readback)
|
||||||
|
{
|
||||||
|
const double renderMilliseconds = readback.LastRenderFrameMilliseconds();
|
||||||
|
mRenderFrameMilliseconds.store(renderMilliseconds, std::memory_order_relaxed);
|
||||||
|
if (mConfig.frameDurationMilliseconds > 0.0)
|
||||||
|
{
|
||||||
|
mRenderFrameBudgetUsedPercent.store(
|
||||||
|
(renderMilliseconds / mConfig.frameDurationMilliseconds) * 100.0,
|
||||||
|
std::memory_order_relaxed);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
mRenderFrameBudgetUsedPercent.store(0.0, std::memory_order_relaxed);
|
||||||
|
}
|
||||||
|
|
||||||
|
const double previousMax = mRenderFrameMaxMilliseconds.load(std::memory_order_relaxed);
|
||||||
|
if (renderMilliseconds > previousMax)
|
||||||
|
mRenderFrameMaxMilliseconds.store(renderMilliseconds, std::memory_order_relaxed);
|
||||||
|
|
||||||
|
mReadbackQueueMilliseconds.store(readback.LastReadbackQueueMilliseconds(), std::memory_order_relaxed);
|
||||||
|
mCompletedReadbackCopyMilliseconds.store(readback.LastCompletedReadbackCopyMilliseconds(), std::memory_order_relaxed);
|
||||||
|
}
|
||||||
|
|
||||||
|
void RenderThread::PublishInputMetrics(const InputFrameTexture& inputTexture)
|
||||||
|
{
|
||||||
|
if (mInputMailbox != nullptr)
|
||||||
|
{
|
||||||
|
const InputFrameMailboxMetrics mailboxMetrics = mInputMailbox->Metrics();
|
||||||
|
mInputFramesReceived.store(mailboxMetrics.submittedFrames, std::memory_order_relaxed);
|
||||||
|
mInputFramesDropped.store(mailboxMetrics.droppedReadyFrames + mailboxMetrics.submitMisses, std::memory_order_relaxed);
|
||||||
|
mInputConsumeMisses.store(mailboxMetrics.consumeMisses, std::memory_order_relaxed);
|
||||||
|
mInputReadyFrames.store(mailboxMetrics.readyCount, std::memory_order_relaxed);
|
||||||
|
mInputReadingFrames.store(mailboxMetrics.readingCount, std::memory_order_relaxed);
|
||||||
|
mInputLatestAgeMilliseconds.store(mailboxMetrics.latestFrameAgeMilliseconds, std::memory_order_relaxed);
|
||||||
|
mInputSignalPresent.store(mailboxMetrics.hasSubmittedFrame, std::memory_order_relaxed);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
mInputFramesReceived.store(0, std::memory_order_relaxed);
|
||||||
|
mInputFramesDropped.store(0, std::memory_order_relaxed);
|
||||||
|
mInputConsumeMisses.store(0, std::memory_order_relaxed);
|
||||||
|
mInputReadyFrames.store(0, std::memory_order_relaxed);
|
||||||
|
mInputReadingFrames.store(0, std::memory_order_relaxed);
|
||||||
|
mInputLatestAgeMilliseconds.store(0.0, std::memory_order_relaxed);
|
||||||
|
mInputSignalPresent.store(false, std::memory_order_relaxed);
|
||||||
|
}
|
||||||
|
|
||||||
|
mInputUploadMisses.store(inputTexture.UploadMisses(), std::memory_order_relaxed);
|
||||||
|
mInputUploadMilliseconds.store(inputTexture.LastUploadMilliseconds(), std::memory_order_relaxed);
|
||||||
|
mInputFormatSupported.store(inputTexture.LastFrameFormatSupported(), std::memory_order_relaxed);
|
||||||
|
}
|
||||||
|
|
||||||
|
void RenderThread::SubmitRuntimeShaderArtifact(const RuntimeShaderArtifact& artifact)
|
||||||
|
{
|
||||||
|
if (artifact.fragmentShaderSource.empty())
|
||||||
|
return;
|
||||||
|
|
||||||
|
std::lock_guard<std::mutex> lock(mShaderArtifactMutex);
|
||||||
|
mPendingShaderArtifact = artifact;
|
||||||
|
mHasPendingShaderArtifact = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void RenderThread::SubmitRuntimeRenderLayers(const std::vector<RenderCadenceCompositor::RuntimeRenderLayerModel>& layers)
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(mRenderLayersMutex);
|
||||||
|
mPendingRenderLayers = layers;
|
||||||
|
mHasPendingRenderLayers = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool RenderThread::TryTakePendingRuntimeShaderArtifact(RuntimeShaderArtifact& artifact)
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(mShaderArtifactMutex);
|
||||||
|
if (!mHasPendingShaderArtifact)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
artifact = std::move(mPendingShaderArtifact);
|
||||||
|
mPendingShaderArtifact = RuntimeShaderArtifact();
|
||||||
|
mHasPendingShaderArtifact = false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool RenderThread::TryTakePendingRenderLayers(std::vector<RenderCadenceCompositor::RuntimeRenderLayerModel>& layers)
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(mRenderLayersMutex);
|
||||||
|
if (!mHasPendingRenderLayers)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
layers = std::move(mPendingRenderLayers);
|
||||||
|
mPendingRenderLayers.clear();
|
||||||
|
mHasPendingRenderLayers = false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void RenderThread::TryCommitReadyRuntimeShader(RuntimeRenderScene& runtimeRenderScene)
|
||||||
|
{
|
||||||
|
std::vector<RenderCadenceCompositor::RuntimeRenderLayerModel> layers;
|
||||||
|
std::string commitError;
|
||||||
|
if (TryTakePendingRenderLayers(layers))
|
||||||
|
{
|
||||||
|
if (!runtimeRenderScene.CommitRenderLayers(layers, commitError))
|
||||||
|
{
|
||||||
|
RenderCadenceCompositor::TryLog(
|
||||||
|
RenderCadenceCompositor::LogLevel::Error,
|
||||||
|
"render-thread",
|
||||||
|
"Runtime render-layer commit failed: " + commitError);
|
||||||
|
mShaderBuildFailures.fetch_add(1, std::memory_order_relaxed);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
RenderCadenceCompositor::TryLog(
|
||||||
|
RenderCadenceCompositor::LogLevel::Log,
|
||||||
|
"render-thread",
|
||||||
|
"Runtime render layer snapshot committed.");
|
||||||
|
mShaderBuildsCommitted.fetch_add(1, std::memory_order_relaxed);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
RuntimeShaderArtifact artifact;
|
||||||
|
if (!TryTakePendingRuntimeShaderArtifact(artifact))
|
||||||
|
return;
|
||||||
|
|
||||||
|
RenderCadenceCompositor::RuntimeRenderLayerModel layer;
|
||||||
|
layer.id = artifact.layerId.empty() ? "runtime-layer-1" : artifact.layerId;
|
||||||
|
layer.shaderId = artifact.shaderId;
|
||||||
|
layer.artifact = artifact;
|
||||||
|
layers.push_back(std::move(layer));
|
||||||
|
if (!runtimeRenderScene.CommitRenderLayers(layers, commitError))
|
||||||
|
{
|
||||||
|
RenderCadenceCompositor::TryLog(
|
||||||
|
RenderCadenceCompositor::LogLevel::Error,
|
||||||
|
"render-thread",
|
||||||
|
"Runtime shader GL commit failed: " + commitError);
|
||||||
|
mShaderBuildFailures.fetch_add(1, std::memory_order_relaxed);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
RenderCadenceCompositor::TryLog(
|
||||||
|
RenderCadenceCompositor::LogLevel::Log,
|
||||||
|
"render-thread",
|
||||||
|
"Runtime shader committed: " + artifact.shaderId + ". " + artifact.message);
|
||||||
|
mShaderBuildsCommitted.fetch_add(1, std::memory_order_relaxed);
|
||||||
|
}
|
||||||
129
apps/RenderCadenceCompositor/render/RenderThread.h
Normal file
129
apps/RenderCadenceCompositor/render/RenderThread.h
Normal file
@@ -0,0 +1,129 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "RenderCadenceClock.h"
|
||||||
|
#include "../runtime/RuntimeLayerModel.h"
|
||||||
|
#include "../runtime/RuntimeShaderArtifact.h"
|
||||||
|
#include "runtime/RuntimeRenderScene.h"
|
||||||
|
|
||||||
|
#include <atomic>
|
||||||
|
#include <condition_variable>
|
||||||
|
#include <cstddef>
|
||||||
|
#include <cstdint>
|
||||||
|
#include <mutex>
|
||||||
|
#include <string>
|
||||||
|
#include <thread>
|
||||||
|
|
||||||
|
class SystemFrameExchange;
|
||||||
|
class InputFrameMailbox;
|
||||||
|
class InputFrameTexture;
|
||||||
|
class Bgra8ReadbackPipeline;
|
||||||
|
|
||||||
|
class RenderThread
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
struct Config
|
||||||
|
{
|
||||||
|
unsigned width = 1920;
|
||||||
|
unsigned height = 1080;
|
||||||
|
double frameDurationMilliseconds = 1000.0 / 59.94;
|
||||||
|
std::size_t pboDepth = 6;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct Metrics
|
||||||
|
{
|
||||||
|
uint64_t renderedFrames = 0;
|
||||||
|
uint64_t completedReadbacks = 0;
|
||||||
|
uint64_t acquireMisses = 0;
|
||||||
|
uint64_t pboQueueMisses = 0;
|
||||||
|
uint64_t clockOverruns = 0;
|
||||||
|
uint64_t skippedFrames = 0;
|
||||||
|
uint64_t shaderBuildsCommitted = 0;
|
||||||
|
uint64_t shaderBuildFailures = 0;
|
||||||
|
double renderFrameMilliseconds = 0.0;
|
||||||
|
double renderFrameBudgetUsedPercent = 0.0;
|
||||||
|
double renderFrameMaxMilliseconds = 0.0;
|
||||||
|
double readbackQueueMilliseconds = 0.0;
|
||||||
|
double completedReadbackCopyMilliseconds = 0.0;
|
||||||
|
uint64_t inputFramesReceived = 0;
|
||||||
|
uint64_t inputFramesDropped = 0;
|
||||||
|
uint64_t inputConsumeMisses = 0;
|
||||||
|
uint64_t inputUploadMisses = 0;
|
||||||
|
std::size_t inputReadyFrames = 0;
|
||||||
|
std::size_t inputReadingFrames = 0;
|
||||||
|
double inputLatestAgeMilliseconds = 0.0;
|
||||||
|
double inputUploadMilliseconds = 0.0;
|
||||||
|
bool inputFormatSupported = true;
|
||||||
|
bool inputSignalPresent = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
RenderThread(SystemFrameExchange& frameExchange, Config config);
|
||||||
|
RenderThread(SystemFrameExchange& frameExchange, InputFrameMailbox* inputMailbox, Config config);
|
||||||
|
RenderThread(const RenderThread&) = delete;
|
||||||
|
RenderThread& operator=(const RenderThread&) = delete;
|
||||||
|
~RenderThread();
|
||||||
|
|
||||||
|
bool Start(std::string& error);
|
||||||
|
void Stop();
|
||||||
|
void SubmitRuntimeShaderArtifact(const RuntimeShaderArtifact& artifact);
|
||||||
|
void SubmitRuntimeRenderLayers(const std::vector<RenderCadenceCompositor::RuntimeRenderLayerModel>& layers);
|
||||||
|
|
||||||
|
Metrics GetMetrics() const;
|
||||||
|
bool IsRunning() const { return mRunning.load(std::memory_order_acquire); }
|
||||||
|
|
||||||
|
private:
|
||||||
|
void ThreadMain();
|
||||||
|
void SignalStarted();
|
||||||
|
void SignalStartupFailure(const std::string& error);
|
||||||
|
void CountRendered();
|
||||||
|
void CountCompleted();
|
||||||
|
void CountAcquireMiss();
|
||||||
|
void PublishReadbackMetrics(const Bgra8ReadbackPipeline& readback);
|
||||||
|
void PublishInputMetrics(const InputFrameTexture& inputTexture);
|
||||||
|
void TryCommitReadyRuntimeShader(RuntimeRenderScene& runtimeRenderScene);
|
||||||
|
bool TryTakePendingRuntimeShaderArtifact(RuntimeShaderArtifact& artifact);
|
||||||
|
bool TryTakePendingRenderLayers(std::vector<RenderCadenceCompositor::RuntimeRenderLayerModel>& layers);
|
||||||
|
|
||||||
|
SystemFrameExchange& mFrameExchange;
|
||||||
|
InputFrameMailbox* mInputMailbox = nullptr;
|
||||||
|
Config mConfig;
|
||||||
|
std::thread mThread;
|
||||||
|
std::atomic<bool> mStopping{ false };
|
||||||
|
std::atomic<bool> mRunning{ false };
|
||||||
|
|
||||||
|
mutable std::mutex mStartupMutex;
|
||||||
|
std::condition_variable mStartupCondition;
|
||||||
|
bool mStarted = false;
|
||||||
|
std::string mStartupError;
|
||||||
|
|
||||||
|
std::atomic<uint64_t> mRenderedFrames{ 0 };
|
||||||
|
std::atomic<uint64_t> mCompletedReadbacks{ 0 };
|
||||||
|
std::atomic<uint64_t> mAcquireMisses{ 0 };
|
||||||
|
std::atomic<uint64_t> mPboQueueMisses{ 0 };
|
||||||
|
std::atomic<uint64_t> mClockOverruns{ 0 };
|
||||||
|
std::atomic<uint64_t> mSkippedFrames{ 0 };
|
||||||
|
std::atomic<uint64_t> mShaderBuildsCommitted{ 0 };
|
||||||
|
std::atomic<uint64_t> mShaderBuildFailures{ 0 };
|
||||||
|
std::atomic<double> mRenderFrameMilliseconds{ 0.0 };
|
||||||
|
std::atomic<double> mRenderFrameBudgetUsedPercent{ 0.0 };
|
||||||
|
std::atomic<double> mRenderFrameMaxMilliseconds{ 0.0 };
|
||||||
|
std::atomic<double> mReadbackQueueMilliseconds{ 0.0 };
|
||||||
|
std::atomic<double> mCompletedReadbackCopyMilliseconds{ 0.0 };
|
||||||
|
std::atomic<uint64_t> mInputFramesReceived{ 0 };
|
||||||
|
std::atomic<uint64_t> mInputFramesDropped{ 0 };
|
||||||
|
std::atomic<uint64_t> mInputConsumeMisses{ 0 };
|
||||||
|
std::atomic<uint64_t> mInputUploadMisses{ 0 };
|
||||||
|
std::atomic<std::size_t> mInputReadyFrames{ 0 };
|
||||||
|
std::atomic<std::size_t> mInputReadingFrames{ 0 };
|
||||||
|
std::atomic<double> mInputLatestAgeMilliseconds{ 0.0 };
|
||||||
|
std::atomic<double> mInputUploadMilliseconds{ 0.0 };
|
||||||
|
std::atomic<bool> mInputFormatSupported{ true };
|
||||||
|
std::atomic<bool> mInputSignalPresent{ false };
|
||||||
|
|
||||||
|
std::mutex mShaderArtifactMutex;
|
||||||
|
bool mHasPendingShaderArtifact = false;
|
||||||
|
RuntimeShaderArtifact mPendingShaderArtifact;
|
||||||
|
|
||||||
|
std::mutex mRenderLayersMutex;
|
||||||
|
bool mHasPendingRenderLayers = false;
|
||||||
|
std::vector<RenderCadenceCompositor::RuntimeRenderLayerModel> mPendingRenderLayers;
|
||||||
|
};
|
||||||
313
apps/RenderCadenceCompositor/render/SimpleMotionRenderer.cpp
Normal file
313
apps/RenderCadenceCompositor/render/SimpleMotionRenderer.cpp
Normal file
@@ -0,0 +1,313 @@
|
|||||||
|
#include "SimpleMotionRenderer.h"
|
||||||
|
|
||||||
|
#include "GLExtensions.h"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <array>
|
||||||
|
#include <cmath>
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
constexpr GLuint kInputTextureUnit = 0;
|
||||||
|
|
||||||
|
const char* kTextureVertexShader = R"GLSL(
|
||||||
|
#version 430 core
|
||||||
|
out vec2 vTexCoord;
|
||||||
|
void main()
|
||||||
|
{
|
||||||
|
vec2 positions[3] = vec2[3](
|
||||||
|
vec2(-1.0, -1.0),
|
||||||
|
vec2( 3.0, -1.0),
|
||||||
|
vec2(-1.0, 3.0));
|
||||||
|
vec2 texCoords[3] = vec2[3](
|
||||||
|
vec2(0.0, 0.0),
|
||||||
|
vec2(2.0, 0.0),
|
||||||
|
vec2(0.0, 2.0));
|
||||||
|
gl_Position = vec4(positions[gl_VertexID], 0.0, 1.0);
|
||||||
|
vTexCoord = texCoords[gl_VertexID];
|
||||||
|
}
|
||||||
|
)GLSL";
|
||||||
|
|
||||||
|
const char* kTextureFragmentShader = R"GLSL(
|
||||||
|
#version 430 core
|
||||||
|
layout(binding = 0) uniform sampler2D uInputTexture;
|
||||||
|
in vec2 vTexCoord;
|
||||||
|
out vec4 fragColor;
|
||||||
|
void main()
|
||||||
|
{
|
||||||
|
fragColor = texture(uInputTexture, clamp(vTexCoord, vec2(0.0), vec2(1.0)));
|
||||||
|
}
|
||||||
|
)GLSL";
|
||||||
|
|
||||||
|
const char* kPatternFragmentShader = R"GLSL(
|
||||||
|
#version 430 core
|
||||||
|
uniform vec2 uResolution;
|
||||||
|
uniform float uFrameIndex;
|
||||||
|
in vec2 vTexCoord;
|
||||||
|
out vec4 fragColor;
|
||||||
|
|
||||||
|
vec3 hexColor(float r, float g, float b)
|
||||||
|
{
|
||||||
|
return vec3(r, g, b) / 255.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
vec3 smpteTop(float x)
|
||||||
|
{
|
||||||
|
if (x < 240.0) return hexColor(102.0, 102.0, 102.0);
|
||||||
|
if (x < 445.0) return hexColor(191.0, 191.0, 191.0);
|
||||||
|
if (x < 651.0) return hexColor(191.0, 191.0, 0.0);
|
||||||
|
if (x < 857.0) return hexColor(0.0, 191.0, 191.0);
|
||||||
|
if (x < 1063.0) return hexColor(0.0, 191.0, 0.0);
|
||||||
|
if (x < 1269.0) return hexColor(191.0, 0.0, 191.0);
|
||||||
|
if (x < 1475.0) return hexColor(191.0, 0.0, 0.0);
|
||||||
|
if (x < 1680.0) return hexColor(0.0, 0.0, 191.0);
|
||||||
|
return hexColor(102.0, 102.0, 102.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
vec3 smpteMiddleA(float x)
|
||||||
|
{
|
||||||
|
if (x < 240.0) return hexColor(0.0, 255.0, 255.0);
|
||||||
|
if (x < 445.0) return hexColor(0.0, 63.0, 105.0);
|
||||||
|
if (x < 1680.0) return hexColor(191.0, 191.0, 191.0);
|
||||||
|
return hexColor(0.0, 0.0, 255.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
vec3 smpteMiddleB(float x)
|
||||||
|
{
|
||||||
|
if (x < 240.0) return hexColor(255.0, 255.0, 0.0);
|
||||||
|
if (x < 445.0) return hexColor(65.0, 0.0, 119.0);
|
||||||
|
if (x < 1475.0)
|
||||||
|
{
|
||||||
|
float ramp = clamp((x - 445.0) / (1475.0 - 445.0), 0.0, 1.0);
|
||||||
|
return vec3(ramp);
|
||||||
|
}
|
||||||
|
if (x < 1680.0) return vec3(1.0);
|
||||||
|
return hexColor(255.0, 0.0, 0.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
vec3 smpteBottom(float x)
|
||||||
|
{
|
||||||
|
if (x < 240.0) return hexColor(38.0, 38.0, 38.0);
|
||||||
|
if (x < 549.0) return vec3(0.0);
|
||||||
|
if (x < 960.0) return vec3(1.0);
|
||||||
|
if (x < 1268.0) return vec3(0.0);
|
||||||
|
if (x < 1337.0) return hexColor(5.0, 5.0, 5.0);
|
||||||
|
if (x < 1405.0) return vec3(0.0);
|
||||||
|
if (x < 1474.0) return hexColor(10.0, 10.0, 10.0);
|
||||||
|
if (x < 1680.0) return vec3(0.0);
|
||||||
|
return hexColor(38.0, 38.0, 38.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
vec3 smpteColor(vec2 uv)
|
||||||
|
{
|
||||||
|
vec2 pixel = vec2(clamp(uv.x, 0.0, 1.0), 1.0 - clamp(uv.y, 0.0, 1.0)) * vec2(1920.0, 1080.0);
|
||||||
|
if (pixel.y < 630.0) return smpteTop(pixel.x);
|
||||||
|
if (pixel.y < 720.0) return smpteMiddleA(pixel.x);
|
||||||
|
if (pixel.y < 810.0) return smpteMiddleB(pixel.x);
|
||||||
|
return smpteBottom(pixel.x);
|
||||||
|
}
|
||||||
|
|
||||||
|
void main()
|
||||||
|
{
|
||||||
|
vec2 uv = clamp(vTexCoord, vec2(0.0), vec2(1.0));
|
||||||
|
vec3 color = smpteColor(uv);
|
||||||
|
|
||||||
|
float t = uFrameIndex / 60.0;
|
||||||
|
vec2 cubeSize = vec2(0.16, 0.20);
|
||||||
|
vec2 cubeMin = vec2(
|
||||||
|
(0.5 + 0.5 * sin(t * 1.7)) * (1.0 - cubeSize.x),
|
||||||
|
(0.5 + 0.5 * sin(t * 1.1 + 0.8)) * (1.0 - cubeSize.y));
|
||||||
|
vec2 cubeMax = cubeMin + cubeSize;
|
||||||
|
bool insideCube = uv.x >= cubeMin.x && uv.x <= cubeMax.x && uv.y >= cubeMin.y && uv.y <= cubeMax.y;
|
||||||
|
if (insideCube)
|
||||||
|
{
|
||||||
|
vec2 local = (uv - cubeMin) / cubeSize;
|
||||||
|
vec3 cubeColor = vec3(1.0, 0.74 + 0.18 * sin(t * 2.1), 0.08);
|
||||||
|
float edge = step(local.x, 0.04) + step(local.y, 0.04) + step(0.96, local.x) + step(0.96, local.y);
|
||||||
|
color = edge > 0.0 ? vec3(1.0) : cubeColor;
|
||||||
|
}
|
||||||
|
|
||||||
|
fragColor = vec4(color, 1.0);
|
||||||
|
}
|
||||||
|
)GLSL";
|
||||||
|
}
|
||||||
|
|
||||||
|
bool SimpleMotionRenderer::InitializeGl(unsigned width, unsigned height)
|
||||||
|
{
|
||||||
|
mWidth = width;
|
||||||
|
mHeight = height;
|
||||||
|
return mWidth > 0 && mHeight > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
void SimpleMotionRenderer::RenderFrame(uint64_t frameIndex)
|
||||||
|
{
|
||||||
|
if (!EnsurePatternProgram())
|
||||||
|
{
|
||||||
|
glViewport(0, 0, static_cast<GLsizei>(mWidth), static_cast<GLsizei>(mHeight));
|
||||||
|
glDisable(GL_SCISSOR_TEST);
|
||||||
|
glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
|
||||||
|
glClear(GL_COLOR_BUFFER_BIT);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
glViewport(0, 0, static_cast<GLsizei>(mWidth), static_cast<GLsizei>(mHeight));
|
||||||
|
glDisable(GL_SCISSOR_TEST);
|
||||||
|
glDisable(GL_DEPTH_TEST);
|
||||||
|
glDisable(GL_BLEND);
|
||||||
|
glUseProgram(mPatternProgram);
|
||||||
|
const GLint resolutionLocation = glGetUniformLocation(mPatternProgram, "uResolution");
|
||||||
|
if (resolutionLocation >= 0)
|
||||||
|
glUniform2f(resolutionLocation, static_cast<float>(mWidth), static_cast<float>(mHeight));
|
||||||
|
const GLint frameIndexLocation = glGetUniformLocation(mPatternProgram, "uFrameIndex");
|
||||||
|
if (frameIndexLocation >= 0)
|
||||||
|
glUniform1f(frameIndexLocation, static_cast<float>(frameIndex));
|
||||||
|
glBindVertexArray(mPatternVertexArray);
|
||||||
|
glDrawArrays(GL_TRIANGLES, 0, 3);
|
||||||
|
glBindVertexArray(0);
|
||||||
|
glUseProgram(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
void SimpleMotionRenderer::RenderTexture(GLuint texture)
|
||||||
|
{
|
||||||
|
if (texture == 0 || !EnsureTextureProgram())
|
||||||
|
{
|
||||||
|
RenderFrame(0);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
glViewport(0, 0, static_cast<GLsizei>(mWidth), static_cast<GLsizei>(mHeight));
|
||||||
|
glDisable(GL_SCISSOR_TEST);
|
||||||
|
glDisable(GL_DEPTH_TEST);
|
||||||
|
glDisable(GL_BLEND);
|
||||||
|
glActiveTexture(GL_TEXTURE0 + kInputTextureUnit);
|
||||||
|
glBindTexture(GL_TEXTURE_2D, texture);
|
||||||
|
glUseProgram(mTextureProgram);
|
||||||
|
glBindVertexArray(mTextureVertexArray);
|
||||||
|
glDrawArrays(GL_TRIANGLES, 0, 3);
|
||||||
|
glBindVertexArray(0);
|
||||||
|
glUseProgram(0);
|
||||||
|
glBindTexture(GL_TEXTURE_2D, 0);
|
||||||
|
glActiveTexture(GL_TEXTURE0);
|
||||||
|
}
|
||||||
|
|
||||||
|
void SimpleMotionRenderer::ShutdownGl()
|
||||||
|
{
|
||||||
|
DestroyPatternProgram();
|
||||||
|
DestroyTextureProgram();
|
||||||
|
mWidth = 0;
|
||||||
|
mHeight = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool SimpleMotionRenderer::EnsurePatternProgram()
|
||||||
|
{
|
||||||
|
if (mPatternProgram != 0)
|
||||||
|
return true;
|
||||||
|
|
||||||
|
if (!CompileShader(GL_VERTEX_SHADER, kTextureVertexShader, mPatternVertexShader))
|
||||||
|
return false;
|
||||||
|
if (!CompileShader(GL_FRAGMENT_SHADER, kPatternFragmentShader, mPatternFragmentShader))
|
||||||
|
{
|
||||||
|
DestroyPatternProgram();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
mPatternProgram = glCreateProgram();
|
||||||
|
glAttachShader(mPatternProgram, mPatternVertexShader);
|
||||||
|
glAttachShader(mPatternProgram, mPatternFragmentShader);
|
||||||
|
glLinkProgram(mPatternProgram);
|
||||||
|
|
||||||
|
GLint linkResult = GL_FALSE;
|
||||||
|
glGetProgramiv(mPatternProgram, GL_LINK_STATUS, &linkResult);
|
||||||
|
if (linkResult == GL_FALSE)
|
||||||
|
{
|
||||||
|
DestroyPatternProgram();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
glGenVertexArrays(1, &mPatternVertexArray);
|
||||||
|
return mPatternVertexArray != 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool SimpleMotionRenderer::EnsureTextureProgram()
|
||||||
|
{
|
||||||
|
if (mTextureProgram != 0)
|
||||||
|
return true;
|
||||||
|
|
||||||
|
if (!CompileShader(GL_VERTEX_SHADER, kTextureVertexShader, mTextureVertexShader))
|
||||||
|
return false;
|
||||||
|
if (!CompileShader(GL_FRAGMENT_SHADER, kTextureFragmentShader, mTextureFragmentShader))
|
||||||
|
{
|
||||||
|
DestroyTextureProgram();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
mTextureProgram = glCreateProgram();
|
||||||
|
glAttachShader(mTextureProgram, mTextureVertexShader);
|
||||||
|
glAttachShader(mTextureProgram, mTextureFragmentShader);
|
||||||
|
glLinkProgram(mTextureProgram);
|
||||||
|
|
||||||
|
GLint linkResult = GL_FALSE;
|
||||||
|
glGetProgramiv(mTextureProgram, GL_LINK_STATUS, &linkResult);
|
||||||
|
if (linkResult == GL_FALSE)
|
||||||
|
{
|
||||||
|
DestroyTextureProgram();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
glUseProgram(mTextureProgram);
|
||||||
|
const GLint inputLocation = glGetUniformLocation(mTextureProgram, "uInputTexture");
|
||||||
|
if (inputLocation >= 0)
|
||||||
|
glUniform1i(inputLocation, static_cast<GLint>(kInputTextureUnit));
|
||||||
|
glUseProgram(0);
|
||||||
|
|
||||||
|
glGenVertexArrays(1, &mTextureVertexArray);
|
||||||
|
return mTextureVertexArray != 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
void SimpleMotionRenderer::DestroyTextureProgram()
|
||||||
|
{
|
||||||
|
if (mTextureProgram != 0)
|
||||||
|
glDeleteProgram(mTextureProgram);
|
||||||
|
if (mTextureVertexShader != 0)
|
||||||
|
glDeleteShader(mTextureVertexShader);
|
||||||
|
if (mTextureFragmentShader != 0)
|
||||||
|
glDeleteShader(mTextureFragmentShader);
|
||||||
|
if (mTextureVertexArray != 0)
|
||||||
|
glDeleteVertexArrays(1, &mTextureVertexArray);
|
||||||
|
mTextureProgram = 0;
|
||||||
|
mTextureVertexShader = 0;
|
||||||
|
mTextureFragmentShader = 0;
|
||||||
|
mTextureVertexArray = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
void SimpleMotionRenderer::DestroyPatternProgram()
|
||||||
|
{
|
||||||
|
if (mPatternProgram != 0)
|
||||||
|
glDeleteProgram(mPatternProgram);
|
||||||
|
if (mPatternVertexShader != 0)
|
||||||
|
glDeleteShader(mPatternVertexShader);
|
||||||
|
if (mPatternFragmentShader != 0)
|
||||||
|
glDeleteShader(mPatternFragmentShader);
|
||||||
|
if (mPatternVertexArray != 0)
|
||||||
|
glDeleteVertexArrays(1, &mPatternVertexArray);
|
||||||
|
mPatternProgram = 0;
|
||||||
|
mPatternVertexShader = 0;
|
||||||
|
mPatternFragmentShader = 0;
|
||||||
|
mPatternVertexArray = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool SimpleMotionRenderer::CompileShader(GLenum shaderType, const char* source, GLuint& shader)
|
||||||
|
{
|
||||||
|
shader = glCreateShader(shaderType);
|
||||||
|
glShaderSource(shader, 1, &source, nullptr);
|
||||||
|
glCompileShader(shader);
|
||||||
|
|
||||||
|
GLint compileResult = GL_FALSE;
|
||||||
|
glGetShaderiv(shader, GL_COMPILE_STATUS, &compileResult);
|
||||||
|
if (compileResult != GL_FALSE)
|
||||||
|
return true;
|
||||||
|
|
||||||
|
glDeleteShader(shader);
|
||||||
|
shader = 0;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
37
apps/RenderCadenceCompositor/render/SimpleMotionRenderer.h
Normal file
37
apps/RenderCadenceCompositor/render/SimpleMotionRenderer.h
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "GLExtensions.h"
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
|
||||||
|
class SimpleMotionRenderer
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
SimpleMotionRenderer() = default;
|
||||||
|
|
||||||
|
bool InitializeGl(unsigned width, unsigned height);
|
||||||
|
void RenderFrame(uint64_t frameIndex);
|
||||||
|
void RenderTexture(GLuint texture);
|
||||||
|
void ShutdownGl();
|
||||||
|
|
||||||
|
unsigned Width() const { return mWidth; }
|
||||||
|
unsigned Height() const { return mHeight; }
|
||||||
|
|
||||||
|
private:
|
||||||
|
bool EnsureTextureProgram();
|
||||||
|
bool EnsurePatternProgram();
|
||||||
|
void DestroyTextureProgram();
|
||||||
|
void DestroyPatternProgram();
|
||||||
|
static bool CompileShader(GLenum shaderType, const char* source, GLuint& shader);
|
||||||
|
|
||||||
|
unsigned mWidth = 0;
|
||||||
|
unsigned mHeight = 0;
|
||||||
|
GLuint mPatternProgram = 0;
|
||||||
|
GLuint mPatternVertexShader = 0;
|
||||||
|
GLuint mPatternFragmentShader = 0;
|
||||||
|
GLuint mPatternVertexArray = 0;
|
||||||
|
GLuint mTextureProgram = 0;
|
||||||
|
GLuint mTextureVertexShader = 0;
|
||||||
|
GLuint mTextureFragmentShader = 0;
|
||||||
|
GLuint mTextureVertexArray = 0;
|
||||||
|
};
|
||||||
@@ -0,0 +1,160 @@
|
|||||||
|
#include "Bgra8ReadbackPipeline.h"
|
||||||
|
|
||||||
|
#include "../frames/SystemFrameTypes.h"
|
||||||
|
|
||||||
|
#include <chrono>
|
||||||
|
#include <cstring>
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
double MillisecondsSince(std::chrono::steady_clock::time_point start)
|
||||||
|
{
|
||||||
|
return std::chrono::duration_cast<std::chrono::duration<double, std::milli>>(
|
||||||
|
std::chrono::steady_clock::now() - start).count();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Bgra8ReadbackPipeline::~Bgra8ReadbackPipeline()
|
||||||
|
{
|
||||||
|
Shutdown();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Bgra8ReadbackPipeline::Initialize(unsigned width, unsigned height, std::size_t pboDepth)
|
||||||
|
{
|
||||||
|
Shutdown();
|
||||||
|
|
||||||
|
mWidth = width;
|
||||||
|
mHeight = height;
|
||||||
|
mRowBytes = VideoIORowBytes(VideoIOPixelFormat::Bgra8, width);
|
||||||
|
if (mWidth == 0 || mHeight == 0 || mRowBytes == 0)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if (!CreateRenderTarget())
|
||||||
|
{
|
||||||
|
Shutdown();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::size_t byteCount = static_cast<std::size_t>(mRowBytes) * static_cast<std::size_t>(mHeight);
|
||||||
|
if (!mPboRing.Initialize(pboDepth, byteCount))
|
||||||
|
{
|
||||||
|
Shutdown();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Bgra8ReadbackPipeline::Shutdown()
|
||||||
|
{
|
||||||
|
mPboRing.Shutdown();
|
||||||
|
DestroyRenderTarget();
|
||||||
|
mWidth = 0;
|
||||||
|
mHeight = 0;
|
||||||
|
mRowBytes = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Bgra8ReadbackPipeline::RenderAndQueue(uint64_t frameIndex, const RenderCallback& renderFrame)
|
||||||
|
{
|
||||||
|
if (mFramebuffer == 0 || !renderFrame)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
glBindFramebuffer(GL_FRAMEBUFFER, mFramebuffer);
|
||||||
|
const auto renderStart = std::chrono::steady_clock::now();
|
||||||
|
renderFrame(frameIndex);
|
||||||
|
mLastRenderFrameMilliseconds = MillisecondsSince(renderStart);
|
||||||
|
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||||
|
|
||||||
|
const auto queueStart = std::chrono::steady_clock::now();
|
||||||
|
const bool queued = mPboRing.QueueReadback(mFramebuffer, mWidth, mHeight, frameIndex);
|
||||||
|
mLastReadbackQueueMilliseconds = MillisecondsSince(queueStart);
|
||||||
|
return queued;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Bgra8ReadbackPipeline::ConsumeCompleted(
|
||||||
|
const AcquireFrameCallback& acquireFrame,
|
||||||
|
const PublishFrameCallback& publishFrame,
|
||||||
|
const CounterCallback& onAcquireMiss,
|
||||||
|
const CounterCallback& onCompleted)
|
||||||
|
{
|
||||||
|
if (!acquireFrame || !publishFrame)
|
||||||
|
return;
|
||||||
|
|
||||||
|
PboReadbackRing::CompletedReadback readback;
|
||||||
|
while (mPboRing.TryAcquireCompleted(readback))
|
||||||
|
{
|
||||||
|
const auto copyStart = std::chrono::steady_clock::now();
|
||||||
|
glBindBuffer(GL_PIXEL_PACK_BUFFER, readback.pbo);
|
||||||
|
void* mapped = glMapBuffer(GL_PIXEL_PACK_BUFFER, GL_READ_ONLY);
|
||||||
|
if (!mapped)
|
||||||
|
{
|
||||||
|
glBindBuffer(GL_PIXEL_PACK_BUFFER, 0);
|
||||||
|
mPboRing.ReleaseCompleted(readback);
|
||||||
|
mLastCompletedReadbackCopyMilliseconds = MillisecondsSince(copyStart);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
SystemFrame frame;
|
||||||
|
if (acquireFrame(frame))
|
||||||
|
{
|
||||||
|
const std::size_t byteCount = static_cast<std::size_t>(frame.rowBytes) * static_cast<std::size_t>(frame.height);
|
||||||
|
if (frame.bytes != nullptr && byteCount <= readback.byteCount)
|
||||||
|
{
|
||||||
|
std::memcpy(frame.bytes, mapped, byteCount);
|
||||||
|
frame.frameIndex = readback.frameIndex;
|
||||||
|
frame.pixelFormat = VideoIOPixelFormat::Bgra8;
|
||||||
|
publishFrame(frame);
|
||||||
|
if (onCompleted)
|
||||||
|
onCompleted();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (onAcquireMiss)
|
||||||
|
{
|
||||||
|
onAcquireMiss();
|
||||||
|
}
|
||||||
|
|
||||||
|
glUnmapBuffer(GL_PIXEL_PACK_BUFFER);
|
||||||
|
glBindBuffer(GL_PIXEL_PACK_BUFFER, 0);
|
||||||
|
mPboRing.ReleaseCompleted(readback);
|
||||||
|
mLastCompletedReadbackCopyMilliseconds = MillisecondsSince(copyStart);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Bgra8ReadbackPipeline::CreateRenderTarget()
|
||||||
|
{
|
||||||
|
glGenFramebuffers(1, &mFramebuffer);
|
||||||
|
glBindFramebuffer(GL_FRAMEBUFFER, mFramebuffer);
|
||||||
|
|
||||||
|
glGenTextures(1, &mTexture);
|
||||||
|
glBindTexture(GL_TEXTURE_2D, mTexture);
|
||||||
|
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);
|
||||||
|
glTexImage2D(
|
||||||
|
GL_TEXTURE_2D,
|
||||||
|
0,
|
||||||
|
GL_RGBA8,
|
||||||
|
static_cast<GLsizei>(mWidth),
|
||||||
|
static_cast<GLsizei>(mHeight),
|
||||||
|
0,
|
||||||
|
GL_BGRA,
|
||||||
|
GL_UNSIGNED_INT_8_8_8_8_REV,
|
||||||
|
nullptr);
|
||||||
|
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, mTexture, 0);
|
||||||
|
|
||||||
|
const bool complete = glCheckFramebufferStatus(GL_FRAMEBUFFER) == GL_FRAMEBUFFER_COMPLETE;
|
||||||
|
glBindTexture(GL_TEXTURE_2D, 0);
|
||||||
|
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||||
|
return complete;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Bgra8ReadbackPipeline::DestroyRenderTarget()
|
||||||
|
{
|
||||||
|
if (mFramebuffer != 0)
|
||||||
|
glDeleteFramebuffers(1, &mFramebuffer);
|
||||||
|
if (mTexture != 0)
|
||||||
|
glDeleteTextures(1, &mTexture);
|
||||||
|
mFramebuffer = 0;
|
||||||
|
mTexture = 0;
|
||||||
|
}
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "PboReadbackRing.h"
|
||||||
|
#include "VideoIOFormat.h"
|
||||||
|
|
||||||
|
#include <cstddef>
|
||||||
|
#include <cstdint>
|
||||||
|
#include <functional>
|
||||||
|
|
||||||
|
struct SystemFrame;
|
||||||
|
|
||||||
|
class Bgra8ReadbackPipeline
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
using RenderCallback = std::function<void(uint64_t frameIndex)>;
|
||||||
|
using AcquireFrameCallback = std::function<bool(SystemFrame& frame)>;
|
||||||
|
using PublishFrameCallback = std::function<bool(const SystemFrame& frame)>;
|
||||||
|
using CounterCallback = std::function<void()>;
|
||||||
|
|
||||||
|
Bgra8ReadbackPipeline() = default;
|
||||||
|
Bgra8ReadbackPipeline(const Bgra8ReadbackPipeline&) = delete;
|
||||||
|
Bgra8ReadbackPipeline& operator=(const Bgra8ReadbackPipeline&) = delete;
|
||||||
|
~Bgra8ReadbackPipeline();
|
||||||
|
|
||||||
|
bool Initialize(unsigned width, unsigned height, std::size_t pboDepth);
|
||||||
|
void Shutdown();
|
||||||
|
|
||||||
|
bool RenderAndQueue(uint64_t frameIndex, const RenderCallback& renderFrame);
|
||||||
|
void ConsumeCompleted(
|
||||||
|
const AcquireFrameCallback& acquireFrame,
|
||||||
|
const PublishFrameCallback& publishFrame,
|
||||||
|
const CounterCallback& onAcquireMiss = {},
|
||||||
|
const CounterCallback& onCompleted = {});
|
||||||
|
|
||||||
|
GLuint Framebuffer() const { return mFramebuffer; }
|
||||||
|
unsigned Width() const { return mWidth; }
|
||||||
|
unsigned Height() const { return mHeight; }
|
||||||
|
unsigned RowBytes() const { return mRowBytes; }
|
||||||
|
VideoIOPixelFormat PixelFormat() const { return VideoIOPixelFormat::Bgra8; }
|
||||||
|
uint64_t PboQueueMisses() const { return mPboRing.QueueMisses(); }
|
||||||
|
double LastRenderFrameMilliseconds() const { return mLastRenderFrameMilliseconds; }
|
||||||
|
double LastReadbackQueueMilliseconds() const { return mLastReadbackQueueMilliseconds; }
|
||||||
|
double LastCompletedReadbackCopyMilliseconds() const { return mLastCompletedReadbackCopyMilliseconds; }
|
||||||
|
|
||||||
|
private:
|
||||||
|
bool CreateRenderTarget();
|
||||||
|
void DestroyRenderTarget();
|
||||||
|
|
||||||
|
unsigned mWidth = 0;
|
||||||
|
unsigned mHeight = 0;
|
||||||
|
unsigned mRowBytes = 0;
|
||||||
|
GLuint mFramebuffer = 0;
|
||||||
|
GLuint mTexture = 0;
|
||||||
|
double mLastRenderFrameMilliseconds = 0.0;
|
||||||
|
double mLastReadbackQueueMilliseconds = 0.0;
|
||||||
|
double mLastCompletedReadbackCopyMilliseconds = 0.0;
|
||||||
|
PboReadbackRing mPboRing;
|
||||||
|
};
|
||||||
138
apps/RenderCadenceCompositor/render/readback/PboReadbackRing.cpp
Normal file
138
apps/RenderCadenceCompositor/render/readback/PboReadbackRing.cpp
Normal file
@@ -0,0 +1,138 @@
|
|||||||
|
#include "PboReadbackRing.h"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
|
||||||
|
PboReadbackRing::~PboReadbackRing()
|
||||||
|
{
|
||||||
|
Shutdown();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool PboReadbackRing::Initialize(std::size_t depth, std::size_t byteCount)
|
||||||
|
{
|
||||||
|
Shutdown();
|
||||||
|
if (depth == 0 || byteCount == 0)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
mSlots.resize(depth);
|
||||||
|
mByteCount = byteCount;
|
||||||
|
for (Slot& slot : mSlots)
|
||||||
|
{
|
||||||
|
glGenBuffers(1, &slot.pbo);
|
||||||
|
if (slot.pbo == 0)
|
||||||
|
{
|
||||||
|
Shutdown();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
glBindBuffer(GL_PIXEL_PACK_BUFFER, slot.pbo);
|
||||||
|
glBufferData(GL_PIXEL_PACK_BUFFER, static_cast<GLsizeiptr>(mByteCount), nullptr, GL_STREAM_READ);
|
||||||
|
}
|
||||||
|
glBindBuffer(GL_PIXEL_PACK_BUFFER, 0);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void PboReadbackRing::Shutdown()
|
||||||
|
{
|
||||||
|
for (Slot& slot : mSlots)
|
||||||
|
{
|
||||||
|
if (slot.fence)
|
||||||
|
glDeleteSync(slot.fence);
|
||||||
|
if (slot.pbo != 0)
|
||||||
|
glDeleteBuffers(1, &slot.pbo);
|
||||||
|
slot = {};
|
||||||
|
}
|
||||||
|
mSlots.clear();
|
||||||
|
mWriteIndex = 0;
|
||||||
|
mReadIndex = 0;
|
||||||
|
mByteCount = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool PboReadbackRing::QueueReadback(GLuint framebuffer, unsigned width, unsigned height, uint64_t frameIndex)
|
||||||
|
{
|
||||||
|
if (mSlots.empty())
|
||||||
|
return false;
|
||||||
|
|
||||||
|
Slot& slot = mSlots[mWriteIndex];
|
||||||
|
if (slot.inFlight || slot.acquired)
|
||||||
|
{
|
||||||
|
++mQueueMisses;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
glBindFramebuffer(GL_READ_FRAMEBUFFER, framebuffer);
|
||||||
|
glPixelStorei(GL_PACK_ALIGNMENT, 4);
|
||||||
|
glPixelStorei(GL_PACK_ROW_LENGTH, 0);
|
||||||
|
glBindBuffer(GL_PIXEL_PACK_BUFFER, slot.pbo);
|
||||||
|
glBufferData(GL_PIXEL_PACK_BUFFER, static_cast<GLsizeiptr>(mByteCount), nullptr, GL_STREAM_READ);
|
||||||
|
glReadPixels(0, 0, static_cast<GLsizei>(width), static_cast<GLsizei>(height), GL_BGRA, GL_UNSIGNED_INT_8_8_8_8_REV, nullptr);
|
||||||
|
slot.fence = glFenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE, 0);
|
||||||
|
slot.inFlight = slot.fence != nullptr;
|
||||||
|
slot.frameIndex = frameIndex;
|
||||||
|
glBindBuffer(GL_PIXEL_PACK_BUFFER, 0);
|
||||||
|
|
||||||
|
if (!slot.inFlight)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
mWriteIndex = (mWriteIndex + 1) % mSlots.size();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool PboReadbackRing::TryAcquireCompleted(CompletedReadback& readback)
|
||||||
|
{
|
||||||
|
if (mSlots.empty())
|
||||||
|
return false;
|
||||||
|
|
||||||
|
for (std::size_t checked = 0; checked < mSlots.size(); ++checked)
|
||||||
|
{
|
||||||
|
Slot& slot = mSlots[mReadIndex];
|
||||||
|
if (!slot.inFlight || slot.acquired || slot.fence == nullptr)
|
||||||
|
{
|
||||||
|
mReadIndex = (mReadIndex + 1) % mSlots.size();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const GLenum waitResult = glClientWaitSync(slot.fence, 0, 0);
|
||||||
|
if (waitResult != GL_ALREADY_SIGNALED && waitResult != GL_CONDITION_SATISFIED)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
slot.acquired = true;
|
||||||
|
readback.pbo = slot.pbo;
|
||||||
|
readback.frameIndex = slot.frameIndex;
|
||||||
|
readback.byteCount = mByteCount;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void PboReadbackRing::ReleaseCompleted(const CompletedReadback& readback)
|
||||||
|
{
|
||||||
|
for (std::size_t index = 0; index < mSlots.size(); ++index)
|
||||||
|
{
|
||||||
|
Slot& slot = mSlots[index];
|
||||||
|
if (!slot.acquired || slot.pbo != readback.pbo)
|
||||||
|
continue;
|
||||||
|
ResetSlot(slot);
|
||||||
|
mReadIndex = (index + 1) % mSlots.size();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void PboReadbackRing::DrainCompleted()
|
||||||
|
{
|
||||||
|
for (std::size_t pass = 0; pass < mSlots.size() * 2; ++pass)
|
||||||
|
{
|
||||||
|
CompletedReadback readback;
|
||||||
|
if (!TryAcquireCompleted(readback))
|
||||||
|
break;
|
||||||
|
ReleaseCompleted(readback);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void PboReadbackRing::ResetSlot(Slot& slot)
|
||||||
|
{
|
||||||
|
if (slot.fence)
|
||||||
|
glDeleteSync(slot.fence);
|
||||||
|
slot.fence = nullptr;
|
||||||
|
slot.inFlight = false;
|
||||||
|
slot.acquired = false;
|
||||||
|
slot.frameIndex = 0;
|
||||||
|
}
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "GLExtensions.h"
|
||||||
|
|
||||||
|
#include <cstddef>
|
||||||
|
#include <cstdint>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
class PboReadbackRing
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
struct CompletedReadback
|
||||||
|
{
|
||||||
|
GLuint pbo = 0;
|
||||||
|
uint64_t frameIndex = 0;
|
||||||
|
std::size_t byteCount = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
PboReadbackRing() = default;
|
||||||
|
PboReadbackRing(const PboReadbackRing&) = delete;
|
||||||
|
PboReadbackRing& operator=(const PboReadbackRing&) = delete;
|
||||||
|
~PboReadbackRing();
|
||||||
|
|
||||||
|
bool Initialize(std::size_t depth, std::size_t byteCount);
|
||||||
|
void Shutdown();
|
||||||
|
|
||||||
|
bool QueueReadback(GLuint framebuffer, unsigned width, unsigned height, uint64_t frameIndex);
|
||||||
|
bool TryAcquireCompleted(CompletedReadback& readback);
|
||||||
|
void ReleaseCompleted(const CompletedReadback& readback);
|
||||||
|
void DrainCompleted();
|
||||||
|
|
||||||
|
std::size_t Depth() const { return mSlots.size(); }
|
||||||
|
uint64_t QueueMisses() const { return mQueueMisses; }
|
||||||
|
|
||||||
|
private:
|
||||||
|
struct Slot
|
||||||
|
{
|
||||||
|
GLuint pbo = 0;
|
||||||
|
GLsync fence = nullptr;
|
||||||
|
bool inFlight = false;
|
||||||
|
bool acquired = false;
|
||||||
|
uint64_t frameIndex = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
void ResetSlot(Slot& slot);
|
||||||
|
|
||||||
|
std::vector<Slot> mSlots;
|
||||||
|
std::size_t mWriteIndex = 0;
|
||||||
|
std::size_t mReadIndex = 0;
|
||||||
|
std::size_t mByteCount = 0;
|
||||||
|
uint64_t mQueueMisses = 0;
|
||||||
|
};
|
||||||
@@ -0,0 +1,271 @@
|
|||||||
|
#include "RuntimeRenderScene.h"
|
||||||
|
|
||||||
|
#include "../../platform/HiddenGlWindow.h"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <functional>
|
||||||
|
#include <utility>
|
||||||
|
|
||||||
|
#ifndef GL_FRAMEBUFFER_BINDING
|
||||||
|
#define GL_FRAMEBUFFER_BINDING 0x8CA6
|
||||||
|
#endif
|
||||||
|
|
||||||
|
RuntimeRenderScene::~RuntimeRenderScene()
|
||||||
|
{
|
||||||
|
ShutdownGl();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool RuntimeRenderScene::StartPrepareWorker(std::unique_ptr<HiddenGlWindow> sharedWindow, std::string& error)
|
||||||
|
{
|
||||||
|
return mPrepareWorker.Start(std::move(sharedWindow), error);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool RuntimeRenderScene::CommitRenderLayers(const std::vector<RenderCadenceCompositor::RuntimeRenderLayerModel>& layers, std::string& error)
|
||||||
|
{
|
||||||
|
ConsumePreparedPrograms();
|
||||||
|
|
||||||
|
std::vector<std::string> nextOrder;
|
||||||
|
std::vector<RenderCadenceCompositor::RuntimeRenderLayerModel> layersToPrepare;
|
||||||
|
nextOrder.reserve(layers.size());
|
||||||
|
for (const RenderCadenceCompositor::RuntimeRenderLayerModel& layer : layers)
|
||||||
|
{
|
||||||
|
if (!layer.bypass)
|
||||||
|
nextOrder.push_back(layer.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (auto layerIt = mLayers.begin(); layerIt != mLayers.end();)
|
||||||
|
{
|
||||||
|
const bool stillPresent = std::find(nextOrder.begin(), nextOrder.end(), layerIt->layerId) != nextOrder.end();
|
||||||
|
if (stillPresent)
|
||||||
|
{
|
||||||
|
++layerIt;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (LayerProgram::PassProgram& pass : layerIt->passes)
|
||||||
|
{
|
||||||
|
if (pass.renderer)
|
||||||
|
pass.renderer->ShutdownGl();
|
||||||
|
}
|
||||||
|
ReleasePendingPrograms(*layerIt);
|
||||||
|
layerIt = mLayers.erase(layerIt);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const RenderCadenceCompositor::RuntimeRenderLayerModel& layer : layers)
|
||||||
|
{
|
||||||
|
if (layer.bypass)
|
||||||
|
continue;
|
||||||
|
if (layer.artifact.passes.empty() && layer.artifact.fragmentShaderSource.empty())
|
||||||
|
continue;
|
||||||
|
|
||||||
|
const std::string fingerprint = Fingerprint(layer.artifact);
|
||||||
|
LayerProgram* program = FindLayer(layer.id);
|
||||||
|
if (!program)
|
||||||
|
{
|
||||||
|
LayerProgram next;
|
||||||
|
next.layerId = layer.id;
|
||||||
|
mLayers.push_back(std::move(next));
|
||||||
|
program = &mLayers.back();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool hasReadyPass = false;
|
||||||
|
for (const LayerProgram::PassProgram& pass : program->passes)
|
||||||
|
{
|
||||||
|
if (pass.renderer && pass.renderer->HasProgram())
|
||||||
|
{
|
||||||
|
hasReadyPass = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (program->shaderId == layer.shaderId && program->sourceFingerprint == fingerprint && hasReadyPass)
|
||||||
|
{
|
||||||
|
for (LayerProgram::PassProgram& pass : program->passes)
|
||||||
|
{
|
||||||
|
if (pass.renderer)
|
||||||
|
pass.renderer->UpdateArtifactState(layer.artifact);
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (program->pendingFingerprint == fingerprint)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
ReleasePendingPrograms(*program);
|
||||||
|
program->shaderId = layer.shaderId;
|
||||||
|
program->pendingFingerprint = fingerprint;
|
||||||
|
layersToPrepare.push_back(layer);
|
||||||
|
}
|
||||||
|
|
||||||
|
mLayerOrder = std::move(nextOrder);
|
||||||
|
if (!layersToPrepare.empty())
|
||||||
|
mPrepareWorker.Submit(layersToPrepare);
|
||||||
|
error.clear();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void RuntimeRenderScene::ShutdownGl()
|
||||||
|
{
|
||||||
|
mPrepareWorker.Stop();
|
||||||
|
for (LayerProgram& layer : mLayers)
|
||||||
|
{
|
||||||
|
for (LayerProgram::PassProgram& pass : layer.passes)
|
||||||
|
{
|
||||||
|
if (pass.renderer)
|
||||||
|
pass.renderer->ShutdownGl();
|
||||||
|
}
|
||||||
|
ReleasePendingPrograms(layer);
|
||||||
|
}
|
||||||
|
mLayers.clear();
|
||||||
|
mLayerOrder.clear();
|
||||||
|
DestroyLayerTargets();
|
||||||
|
}
|
||||||
|
|
||||||
|
void RuntimeRenderScene::ConsumePreparedPrograms()
|
||||||
|
{
|
||||||
|
RuntimePreparedShaderProgram preparedProgram;
|
||||||
|
while (mPrepareWorker.TryConsume(preparedProgram))
|
||||||
|
{
|
||||||
|
if (!preparedProgram.succeeded)
|
||||||
|
{
|
||||||
|
preparedProgram.ReleaseGl();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
LayerProgram* layer = FindLayer(preparedProgram.layerId);
|
||||||
|
if (!layer || layer->pendingFingerprint != preparedProgram.sourceFingerprint)
|
||||||
|
{
|
||||||
|
preparedProgram.ReleaseGl();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool replacesExistingPendingPass = false;
|
||||||
|
for (RuntimePreparedShaderProgram& existing : layer->pendingPreparedPrograms)
|
||||||
|
{
|
||||||
|
if (existing.passId != preparedProgram.passId)
|
||||||
|
continue;
|
||||||
|
existing.ReleaseGl();
|
||||||
|
existing = std::move(preparedProgram);
|
||||||
|
replacesExistingPendingPass = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (!replacesExistingPendingPass)
|
||||||
|
layer->pendingPreparedPrograms.push_back(std::move(preparedProgram));
|
||||||
|
TryCommitPendingPrograms(*layer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void RuntimeRenderScene::ReleasePendingPrograms(LayerProgram& layer)
|
||||||
|
{
|
||||||
|
for (RuntimePreparedShaderProgram& program : layer.pendingPreparedPrograms)
|
||||||
|
program.ReleaseGl();
|
||||||
|
layer.pendingPreparedPrograms.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
void RuntimeRenderScene::TryCommitPendingPrograms(LayerProgram& layer)
|
||||||
|
{
|
||||||
|
if (layer.pendingPreparedPrograms.empty())
|
||||||
|
return;
|
||||||
|
|
||||||
|
const RuntimeShaderArtifact& artifact = layer.pendingPreparedPrograms.front().artifact;
|
||||||
|
const std::size_t expectedPassCount = artifact.passes.empty() ? 1 : artifact.passes.size();
|
||||||
|
if (layer.pendingPreparedPrograms.size() < expectedPassCount)
|
||||||
|
return;
|
||||||
|
|
||||||
|
std::vector<LayerProgram::PassProgram> nextPasses;
|
||||||
|
nextPasses.reserve(expectedPassCount);
|
||||||
|
for (const RuntimeShaderPassArtifact& passArtifact : artifact.passes)
|
||||||
|
{
|
||||||
|
auto preparedIt = std::find_if(
|
||||||
|
layer.pendingPreparedPrograms.begin(),
|
||||||
|
layer.pendingPreparedPrograms.end(),
|
||||||
|
[&passArtifact](const RuntimePreparedShaderProgram& prepared) {
|
||||||
|
return prepared.passId == passArtifact.passId;
|
||||||
|
});
|
||||||
|
if (preparedIt == layer.pendingPreparedPrograms.end())
|
||||||
|
return;
|
||||||
|
|
||||||
|
std::unique_ptr<RuntimeShaderRenderer> nextRenderer = std::make_unique<RuntimeShaderRenderer>();
|
||||||
|
std::string error;
|
||||||
|
if (!nextRenderer->CommitPreparedProgram(*preparedIt, error))
|
||||||
|
{
|
||||||
|
ReleasePendingPrograms(layer);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
LayerProgram::PassProgram nextPass;
|
||||||
|
nextPass.passId = preparedIt->passId;
|
||||||
|
nextPass.inputNames = preparedIt->inputNames;
|
||||||
|
nextPass.outputName = preparedIt->outputName.empty() ? preparedIt->passId : preparedIt->outputName;
|
||||||
|
nextPass.renderer = std::move(nextRenderer);
|
||||||
|
nextPasses.push_back(std::move(nextPass));
|
||||||
|
}
|
||||||
|
if (artifact.passes.empty())
|
||||||
|
{
|
||||||
|
RuntimePreparedShaderProgram& prepared = layer.pendingPreparedPrograms.front();
|
||||||
|
std::unique_ptr<RuntimeShaderRenderer> nextRenderer = std::make_unique<RuntimeShaderRenderer>();
|
||||||
|
std::string error;
|
||||||
|
if (!nextRenderer->CommitPreparedProgram(prepared, error))
|
||||||
|
{
|
||||||
|
ReleasePendingPrograms(layer);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
LayerProgram::PassProgram nextPass;
|
||||||
|
nextPass.passId = prepared.passId;
|
||||||
|
nextPass.inputNames = prepared.inputNames;
|
||||||
|
nextPass.outputName = prepared.outputName.empty() ? prepared.passId : prepared.outputName;
|
||||||
|
nextPass.renderer = std::move(nextRenderer);
|
||||||
|
nextPasses.push_back(std::move(nextPass));
|
||||||
|
}
|
||||||
|
|
||||||
|
for (LayerProgram::PassProgram& pass : layer.passes)
|
||||||
|
{
|
||||||
|
if (pass.renderer)
|
||||||
|
pass.renderer->ShutdownGl();
|
||||||
|
}
|
||||||
|
layer.passes = std::move(nextPasses);
|
||||||
|
layer.shaderId = artifact.shaderId;
|
||||||
|
layer.sourceFingerprint = layer.pendingPreparedPrograms.front().sourceFingerprint;
|
||||||
|
layer.pendingFingerprint.clear();
|
||||||
|
layer.pendingPreparedPrograms.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
RuntimeRenderScene::LayerProgram* RuntimeRenderScene::FindLayer(const std::string& layerId)
|
||||||
|
{
|
||||||
|
for (LayerProgram& layer : mLayers)
|
||||||
|
{
|
||||||
|
if (layer.layerId == layerId)
|
||||||
|
return &layer;
|
||||||
|
}
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
const RuntimeRenderScene::LayerProgram* RuntimeRenderScene::FindLayer(const std::string& layerId) const
|
||||||
|
{
|
||||||
|
for (const LayerProgram& layer : mLayers)
|
||||||
|
{
|
||||||
|
if (layer.layerId == layerId)
|
||||||
|
return &layer;
|
||||||
|
}
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
RuntimeRenderScene::LayerProgram::PassProgram* RuntimeRenderScene::FindPass(LayerProgram& layer, const std::string& passId)
|
||||||
|
{
|
||||||
|
for (LayerProgram::PassProgram& pass : layer.passes)
|
||||||
|
{
|
||||||
|
if (pass.passId == passId)
|
||||||
|
return &pass;
|
||||||
|
}
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string RuntimeRenderScene::Fingerprint(const RuntimeShaderArtifact& artifact)
|
||||||
|
{
|
||||||
|
const std::hash<std::string> hasher;
|
||||||
|
std::string source;
|
||||||
|
for (const RuntimeShaderPassArtifact& pass : artifact.passes)
|
||||||
|
source += pass.passId + ":" + pass.outputName + ":" + pass.fragmentShaderSource + "\n";
|
||||||
|
if (source.empty())
|
||||||
|
source = artifact.fragmentShaderSource;
|
||||||
|
return artifact.shaderId + ":" + std::to_string(source.size()) + ":" + std::to_string(hasher(source));
|
||||||
|
}
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "../runtime/RuntimeLayerModel.h"
|
||||||
|
#include "RuntimeShaderPrepareWorker.h"
|
||||||
|
#include "RuntimeShaderRenderer.h"
|
||||||
|
|
||||||
|
#include <windows.h>
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
#include <memory>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
class RuntimeRenderScene
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
RuntimeRenderScene() = default;
|
||||||
|
RuntimeRenderScene(const RuntimeRenderScene&) = delete;
|
||||||
|
RuntimeRenderScene& operator=(const RuntimeRenderScene&) = delete;
|
||||||
|
~RuntimeRenderScene();
|
||||||
|
|
||||||
|
bool StartPrepareWorker(std::unique_ptr<HiddenGlWindow> sharedWindow, std::string& error);
|
||||||
|
bool CommitRenderLayers(const std::vector<RenderCadenceCompositor::RuntimeRenderLayerModel>& layers, std::string& error);
|
||||||
|
bool HasLayers();
|
||||||
|
void RenderFrame(uint64_t frameIndex, unsigned width, unsigned height, GLuint videoInputTexture = 0);
|
||||||
|
void ShutdownGl();
|
||||||
|
|
||||||
|
private:
|
||||||
|
struct LayerProgram
|
||||||
|
{
|
||||||
|
std::string layerId;
|
||||||
|
std::string shaderId;
|
||||||
|
std::string sourceFingerprint;
|
||||||
|
std::string pendingFingerprint;
|
||||||
|
std::vector<RuntimePreparedShaderProgram> pendingPreparedPrograms;
|
||||||
|
struct PassProgram
|
||||||
|
{
|
||||||
|
std::string passId;
|
||||||
|
std::vector<std::string> inputNames;
|
||||||
|
std::string outputName;
|
||||||
|
std::unique_ptr<RuntimeShaderRenderer> renderer;
|
||||||
|
};
|
||||||
|
std::vector<PassProgram> passes;
|
||||||
|
};
|
||||||
|
|
||||||
|
void ConsumePreparedPrograms();
|
||||||
|
void ReleasePendingPrograms(LayerProgram& layer);
|
||||||
|
void TryCommitPendingPrograms(LayerProgram& layer);
|
||||||
|
bool EnsureLayerTargets(unsigned width, unsigned height);
|
||||||
|
void DestroyLayerTargets();
|
||||||
|
GLuint RenderLayer(LayerProgram& layer, uint64_t frameIndex, unsigned width, unsigned height, GLuint videoInputTexture, GLuint layerInputTexture, GLuint outputFramebuffer, bool renderToOutput);
|
||||||
|
LayerProgram* FindLayer(const std::string& layerId);
|
||||||
|
const LayerProgram* FindLayer(const std::string& layerId) const;
|
||||||
|
LayerProgram::PassProgram* FindPass(LayerProgram& layer, const std::string& passId);
|
||||||
|
static std::string Fingerprint(const RuntimeShaderArtifact& artifact);
|
||||||
|
|
||||||
|
RuntimeShaderPrepareWorker mPrepareWorker;
|
||||||
|
std::vector<LayerProgram> mLayers;
|
||||||
|
std::vector<std::string> mLayerOrder;
|
||||||
|
GLuint mLayerFramebuffers[4] = {};
|
||||||
|
GLuint mLayerTextures[4] = {};
|
||||||
|
unsigned mLayerTargetWidth = 0;
|
||||||
|
unsigned mLayerTargetHeight = 0;
|
||||||
|
};
|
||||||
@@ -0,0 +1,219 @@
|
|||||||
|
#include "RuntimeRenderScene.h"
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#ifndef GL_FRAMEBUFFER_BINDING
|
||||||
|
#define GL_FRAMEBUFFER_BINDING 0x8CA6
|
||||||
|
#endif
|
||||||
|
|
||||||
|
bool RuntimeRenderScene::HasLayers()
|
||||||
|
{
|
||||||
|
ConsumePreparedPrograms();
|
||||||
|
|
||||||
|
for (const std::string& layerId : mLayerOrder)
|
||||||
|
{
|
||||||
|
const LayerProgram* layer = FindLayer(layerId);
|
||||||
|
if (!layer)
|
||||||
|
continue;
|
||||||
|
for (const LayerProgram::PassProgram& pass : layer->passes)
|
||||||
|
{
|
||||||
|
if (pass.renderer && pass.renderer->HasProgram())
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void RuntimeRenderScene::RenderFrame(uint64_t frameIndex, unsigned width, unsigned height, GLuint videoInputTexture)
|
||||||
|
{
|
||||||
|
ConsumePreparedPrograms();
|
||||||
|
|
||||||
|
std::vector<LayerProgram*> readyLayers;
|
||||||
|
for (const std::string& layerId : mLayerOrder)
|
||||||
|
{
|
||||||
|
LayerProgram* layer = FindLayer(layerId);
|
||||||
|
if (!layer)
|
||||||
|
continue;
|
||||||
|
for (const LayerProgram::PassProgram& pass : layer->passes)
|
||||||
|
{
|
||||||
|
if (pass.renderer && pass.renderer->HasProgram())
|
||||||
|
{
|
||||||
|
readyLayers.push_back(layer);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (readyLayers.empty())
|
||||||
|
return;
|
||||||
|
|
||||||
|
GLint outputFramebuffer = 0;
|
||||||
|
glGetIntegerv(GL_FRAMEBUFFER_BINDING, &outputFramebuffer);
|
||||||
|
|
||||||
|
if (readyLayers.size() == 1)
|
||||||
|
{
|
||||||
|
RenderLayer(*readyLayers.front(), frameIndex, width, height, videoInputTexture, videoInputTexture, static_cast<GLuint>(outputFramebuffer), true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!EnsureLayerTargets(width, height))
|
||||||
|
{
|
||||||
|
glBindFramebuffer(GL_FRAMEBUFFER, static_cast<GLuint>(outputFramebuffer));
|
||||||
|
RenderLayer(*readyLayers.back(), frameIndex, width, height, videoInputTexture, videoInputTexture, static_cast<GLuint>(outputFramebuffer), true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Shader source contract:
|
||||||
|
// - gVideoInput is the decoded current input texture for every layer in the stack.
|
||||||
|
// - gLayerInput starts as gVideoInput for the first layer, then becomes the previous layer output.
|
||||||
|
GLuint layerInputTexture = videoInputTexture;
|
||||||
|
std::size_t nextTargetIndex = 0;
|
||||||
|
for (std::size_t layerIndex = 0; layerIndex < readyLayers.size(); ++layerIndex)
|
||||||
|
{
|
||||||
|
const bool isFinalLayer = layerIndex == readyLayers.size() - 1;
|
||||||
|
if (isFinalLayer)
|
||||||
|
{
|
||||||
|
glBindFramebuffer(GL_FRAMEBUFFER, static_cast<GLuint>(outputFramebuffer));
|
||||||
|
RenderLayer(*readyLayers[layerIndex], frameIndex, width, height, videoInputTexture, layerInputTexture, static_cast<GLuint>(outputFramebuffer), true);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
RenderLayer(*readyLayers[layerIndex], frameIndex, width, height, videoInputTexture, layerInputTexture, mLayerFramebuffers[nextTargetIndex], true);
|
||||||
|
layerInputTexture = mLayerTextures[nextTargetIndex];
|
||||||
|
nextTargetIndex = 1 - nextTargetIndex;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
GLuint RuntimeRenderScene::RenderLayer(
|
||||||
|
LayerProgram& layer,
|
||||||
|
uint64_t frameIndex,
|
||||||
|
unsigned width,
|
||||||
|
unsigned height,
|
||||||
|
GLuint videoInputTexture,
|
||||||
|
GLuint layerInputTexture,
|
||||||
|
GLuint outputFramebuffer,
|
||||||
|
bool renderToOutput)
|
||||||
|
{
|
||||||
|
GLuint namedOutputs[2] = {};
|
||||||
|
std::string namedOutputNames[2];
|
||||||
|
std::size_t nextTargetIndex = 2;
|
||||||
|
GLuint lastOutputTexture = layerInputTexture;
|
||||||
|
|
||||||
|
for (LayerProgram::PassProgram& pass : layer.passes)
|
||||||
|
{
|
||||||
|
if (!pass.renderer || !pass.renderer->HasProgram())
|
||||||
|
continue;
|
||||||
|
|
||||||
|
GLuint sourceTexture = videoInputTexture;
|
||||||
|
if (!pass.inputNames.empty())
|
||||||
|
{
|
||||||
|
const std::string& inputName = pass.inputNames.front();
|
||||||
|
if (inputName == "videoInput")
|
||||||
|
{
|
||||||
|
sourceTexture = videoInputTexture;
|
||||||
|
}
|
||||||
|
else if (inputName != "layerInput")
|
||||||
|
{
|
||||||
|
// Named intermediate pass inputs currently use the gVideoInput binding slot as the
|
||||||
|
// selected pass source. Layer stack shaders should use gLayerInput for previous-layer
|
||||||
|
// sampling and gVideoInput for the original input frame.
|
||||||
|
for (std::size_t index = 0; index < 2; ++index)
|
||||||
|
{
|
||||||
|
if (namedOutputNames[index] == inputName)
|
||||||
|
{
|
||||||
|
sourceTexture = namedOutputs[index];
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const bool writesLayerOutput = pass.outputName == "layerOutput";
|
||||||
|
if (writesLayerOutput && renderToOutput)
|
||||||
|
{
|
||||||
|
glBindFramebuffer(GL_FRAMEBUFFER, outputFramebuffer);
|
||||||
|
pass.renderer->RenderFrame(frameIndex, width, height, sourceTexture, layerInputTexture);
|
||||||
|
lastOutputTexture = 0;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!EnsureLayerTargets(width, height))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
const std::size_t targetIndex = nextTargetIndex;
|
||||||
|
nextTargetIndex = nextTargetIndex == 2 ? 3 : 2;
|
||||||
|
glBindFramebuffer(GL_FRAMEBUFFER, mLayerFramebuffers[targetIndex]);
|
||||||
|
pass.renderer->RenderFrame(frameIndex, width, height, sourceTexture, layerInputTexture);
|
||||||
|
const std::size_t namedIndex = targetIndex - 2;
|
||||||
|
namedOutputs[namedIndex] = mLayerTextures[targetIndex];
|
||||||
|
namedOutputNames[namedIndex] = pass.outputName;
|
||||||
|
lastOutputTexture = mLayerTextures[targetIndex];
|
||||||
|
}
|
||||||
|
|
||||||
|
return lastOutputTexture;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool RuntimeRenderScene::EnsureLayerTargets(unsigned width, unsigned height)
|
||||||
|
{
|
||||||
|
if (width == 0 || height == 0)
|
||||||
|
return false;
|
||||||
|
if (mLayerFramebuffers[0] != 0 && mLayerFramebuffers[1] != 0 && mLayerFramebuffers[2] != 0 && mLayerFramebuffers[3] != 0
|
||||||
|
&& mLayerTextures[0] != 0 && mLayerTextures[1] != 0 && mLayerTextures[2] != 0 && mLayerTextures[3] != 0
|
||||||
|
&& mLayerTargetWidth == width && mLayerTargetHeight == height)
|
||||||
|
return true;
|
||||||
|
|
||||||
|
DestroyLayerTargets();
|
||||||
|
mLayerTargetWidth = width;
|
||||||
|
mLayerTargetHeight = height;
|
||||||
|
|
||||||
|
glGenFramebuffers(4, mLayerFramebuffers);
|
||||||
|
glGenTextures(4, mLayerTextures);
|
||||||
|
for (int index = 0; index < 4; ++index)
|
||||||
|
{
|
||||||
|
glBindTexture(GL_TEXTURE_2D, mLayerTextures[index]);
|
||||||
|
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_INT_8_8_8_8_REV,
|
||||||
|
nullptr);
|
||||||
|
|
||||||
|
glBindFramebuffer(GL_FRAMEBUFFER, mLayerFramebuffers[index]);
|
||||||
|
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, mLayerTextures[index], 0);
|
||||||
|
if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE)
|
||||||
|
{
|
||||||
|
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||||
|
glBindTexture(GL_TEXTURE_2D, 0);
|
||||||
|
DestroyLayerTargets();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||||
|
glBindTexture(GL_TEXTURE_2D, 0);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void RuntimeRenderScene::DestroyLayerTargets()
|
||||||
|
{
|
||||||
|
if (mLayerFramebuffers[0] != 0 || mLayerFramebuffers[1] != 0 || mLayerFramebuffers[2] != 0 || mLayerFramebuffers[3] != 0)
|
||||||
|
glDeleteFramebuffers(4, mLayerFramebuffers);
|
||||||
|
if (mLayerTextures[0] != 0 || mLayerTextures[1] != 0 || mLayerTextures[2] != 0 || mLayerTextures[3] != 0)
|
||||||
|
glDeleteTextures(4, mLayerTextures);
|
||||||
|
for (int index = 0; index < 4; ++index)
|
||||||
|
{
|
||||||
|
mLayerFramebuffers[index] = 0;
|
||||||
|
mLayerTextures[index] = 0;
|
||||||
|
}
|
||||||
|
mLayerTargetWidth = 0;
|
||||||
|
mLayerTargetHeight = 0;
|
||||||
|
}
|
||||||
@@ -0,0 +1,123 @@
|
|||||||
|
#include "RuntimeShaderParams.h"
|
||||||
|
|
||||||
|
#include "Std140Buffer.h"
|
||||||
|
|
||||||
|
#include <chrono>
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
ShaderParameterValue DefaultValueForDefinition(const ShaderParameterDefinition& definition)
|
||||||
|
{
|
||||||
|
ShaderParameterValue value;
|
||||||
|
switch (definition.type)
|
||||||
|
{
|
||||||
|
case ShaderParameterType::Float:
|
||||||
|
value.numberValues = definition.defaultNumbers.empty() ? std::vector<double>{ 0.0 } : definition.defaultNumbers;
|
||||||
|
break;
|
||||||
|
case ShaderParameterType::Vec2:
|
||||||
|
value.numberValues = definition.defaultNumbers.size() == 2 ? definition.defaultNumbers : std::vector<double>{ 0.0, 0.0 };
|
||||||
|
break;
|
||||||
|
case ShaderParameterType::Color:
|
||||||
|
value.numberValues = definition.defaultNumbers.size() == 4 ? definition.defaultNumbers : std::vector<double>{ 1.0, 1.0, 1.0, 1.0 };
|
||||||
|
break;
|
||||||
|
case ShaderParameterType::Boolean:
|
||||||
|
value.booleanValue = definition.defaultBoolean;
|
||||||
|
break;
|
||||||
|
case ShaderParameterType::Enum:
|
||||||
|
value.enumValue = definition.defaultEnumValue;
|
||||||
|
break;
|
||||||
|
case ShaderParameterType::Text:
|
||||||
|
value.textValue = definition.defaultTextValue;
|
||||||
|
break;
|
||||||
|
case ShaderParameterType::Trigger:
|
||||||
|
value.numberValues = { 0.0, -1000000.0 };
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
int EnumIndexForDefault(const ShaderParameterDefinition& definition, const ShaderParameterValue& value)
|
||||||
|
{
|
||||||
|
for (std::size_t optionIndex = 0; optionIndex < definition.enumOptions.size(); ++optionIndex)
|
||||||
|
{
|
||||||
|
if (definition.enumOptions[optionIndex].value == value.enumValue)
|
||||||
|
return static_cast<int>(optionIndex);
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
double UtcSecondsOfDay()
|
||||||
|
{
|
||||||
|
const auto now = std::chrono::system_clock::now();
|
||||||
|
const auto seconds = std::chrono::duration_cast<std::chrono::seconds>(now.time_since_epoch()).count();
|
||||||
|
const long long secondsPerDay = 24 * 60 * 60;
|
||||||
|
long long daySeconds = seconds % secondsPerDay;
|
||||||
|
if (daySeconds < 0)
|
||||||
|
daySeconds += secondsPerDay;
|
||||||
|
return static_cast<double>(daySeconds);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<unsigned char> BuildRuntimeShaderGlobalParamsStd140(
|
||||||
|
const RuntimeShaderArtifact& artifact,
|
||||||
|
uint64_t frameIndex,
|
||||||
|
unsigned width,
|
||||||
|
unsigned height)
|
||||||
|
{
|
||||||
|
std::vector<unsigned char> buffer;
|
||||||
|
buffer.reserve(512);
|
||||||
|
|
||||||
|
AppendStd140Float(buffer, static_cast<float>(frameIndex) / 60.0f);
|
||||||
|
AppendStd140Vec2(buffer, static_cast<float>(width), static_cast<float>(height));
|
||||||
|
AppendStd140Vec2(buffer, static_cast<float>(width), static_cast<float>(height));
|
||||||
|
AppendStd140Float(buffer, static_cast<float>(UtcSecondsOfDay()));
|
||||||
|
AppendStd140Float(buffer, 0.0f);
|
||||||
|
AppendStd140Float(buffer, 0.37f);
|
||||||
|
AppendStd140Float(buffer, static_cast<float>(frameIndex));
|
||||||
|
AppendStd140Float(buffer, 1.0f);
|
||||||
|
AppendStd140Float(buffer, 0.0f);
|
||||||
|
AppendStd140Int(buffer, 0);
|
||||||
|
AppendStd140Int(buffer, 0);
|
||||||
|
AppendStd140Int(buffer, 0);
|
||||||
|
|
||||||
|
for (const ShaderParameterDefinition& definition : artifact.parameterDefinitions)
|
||||||
|
{
|
||||||
|
const auto valueIt = artifact.parameterValues.find(definition.id);
|
||||||
|
const ShaderParameterValue value = valueIt == artifact.parameterValues.end()
|
||||||
|
? DefaultValueForDefinition(definition)
|
||||||
|
: valueIt->second;
|
||||||
|
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:
|
||||||
|
AppendStd140Int(buffer, EnumIndexForDefault(definition, value));
|
||||||
|
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);
|
||||||
|
return buffer;
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "../runtime/RuntimeShaderArtifact.h"
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
std::vector<unsigned char> BuildRuntimeShaderGlobalParamsStd140(
|
||||||
|
const RuntimeShaderArtifact& artifact,
|
||||||
|
uint64_t frameIndex,
|
||||||
|
unsigned width,
|
||||||
|
unsigned height);
|
||||||
@@ -0,0 +1,179 @@
|
|||||||
|
#include "RuntimeShaderPrepareWorker.h"
|
||||||
|
|
||||||
|
#include "../../platform/HiddenGlWindow.h"
|
||||||
|
#include "RuntimeShaderRenderer.h"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <chrono>
|
||||||
|
#include <functional>
|
||||||
|
#include <utility>
|
||||||
|
|
||||||
|
RuntimeShaderPrepareWorker::~RuntimeShaderPrepareWorker()
|
||||||
|
{
|
||||||
|
Stop();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool RuntimeShaderPrepareWorker::Start(std::unique_ptr<HiddenGlWindow> sharedWindow, std::string& error)
|
||||||
|
{
|
||||||
|
if (mThread.joinable())
|
||||||
|
return true;
|
||||||
|
if (!sharedWindow || sharedWindow->DeviceContext() == nullptr || sharedWindow->Context() == nullptr)
|
||||||
|
{
|
||||||
|
error = "Runtime shader prepare worker needs an existing shared GL context.";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
mWindow = std::move(sharedWindow);
|
||||||
|
mStopping.store(false, std::memory_order_release);
|
||||||
|
mStarted.store(false, std::memory_order_release);
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(mMutex);
|
||||||
|
mStartupReady = false;
|
||||||
|
mStartupError.clear();
|
||||||
|
}
|
||||||
|
mThread = std::thread([this]() { ThreadMain(); });
|
||||||
|
|
||||||
|
std::unique_lock<std::mutex> lock(mMutex);
|
||||||
|
if (!mStartupCondition.wait_for(lock, std::chrono::seconds(3), [this]() {
|
||||||
|
return mStartupReady || !mStartupError.empty();
|
||||||
|
}))
|
||||||
|
{
|
||||||
|
error = "Timed out starting runtime shader prepare worker.";
|
||||||
|
lock.unlock();
|
||||||
|
Stop();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!mStartupError.empty())
|
||||||
|
{
|
||||||
|
error = mStartupError;
|
||||||
|
lock.unlock();
|
||||||
|
Stop();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void RuntimeShaderPrepareWorker::Stop()
|
||||||
|
{
|
||||||
|
mStopping.store(true, std::memory_order_release);
|
||||||
|
mCondition.notify_all();
|
||||||
|
if (mThread.joinable())
|
||||||
|
mThread.join();
|
||||||
|
|
||||||
|
std::deque<RuntimePreparedShaderProgram> completed;
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(mMutex);
|
||||||
|
mRequests.clear();
|
||||||
|
completed.swap(mCompleted);
|
||||||
|
}
|
||||||
|
for (RuntimePreparedShaderProgram& program : completed)
|
||||||
|
program.ReleaseGl();
|
||||||
|
mWindow.reset();
|
||||||
|
mStarted.store(false, std::memory_order_release);
|
||||||
|
}
|
||||||
|
|
||||||
|
void RuntimeShaderPrepareWorker::Submit(const std::vector<RenderCadenceCompositor::RuntimeRenderLayerModel>& layers)
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(mMutex);
|
||||||
|
for (const RenderCadenceCompositor::RuntimeRenderLayerModel& layer : layers)
|
||||||
|
{
|
||||||
|
if (layer.artifact.passes.empty() && layer.artifact.fragmentShaderSource.empty())
|
||||||
|
continue;
|
||||||
|
|
||||||
|
std::vector<RuntimeShaderPassArtifact> passes = layer.artifact.passes;
|
||||||
|
if (passes.empty())
|
||||||
|
{
|
||||||
|
RuntimeShaderPassArtifact pass;
|
||||||
|
pass.passId = "main";
|
||||||
|
pass.fragmentShaderSource = layer.artifact.fragmentShaderSource;
|
||||||
|
pass.outputName = "layerOutput";
|
||||||
|
passes.push_back(std::move(pass));
|
||||||
|
}
|
||||||
|
|
||||||
|
auto sameLayer = [&layer](const PrepareRequest& existing) {
|
||||||
|
return existing.layerId == layer.id;
|
||||||
|
};
|
||||||
|
mRequests.erase(std::remove_if(mRequests.begin(), mRequests.end(), sameLayer), mRequests.end());
|
||||||
|
|
||||||
|
for (const RuntimeShaderPassArtifact& pass : passes)
|
||||||
|
{
|
||||||
|
PrepareRequest request;
|
||||||
|
request.layerId = layer.id;
|
||||||
|
request.shaderId = layer.shaderId;
|
||||||
|
request.passId = pass.passId;
|
||||||
|
request.sourceFingerprint = Fingerprint(layer.artifact);
|
||||||
|
request.artifact = layer.artifact;
|
||||||
|
request.passArtifact = pass;
|
||||||
|
mRequests.push_back(std::move(request));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
mCondition.notify_one();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool RuntimeShaderPrepareWorker::TryConsume(RuntimePreparedShaderProgram& preparedProgram)
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(mMutex);
|
||||||
|
if (mCompleted.empty())
|
||||||
|
return false;
|
||||||
|
|
||||||
|
preparedProgram = std::move(mCompleted.front());
|
||||||
|
mCompleted.pop_front();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void RuntimeShaderPrepareWorker::ThreadMain()
|
||||||
|
{
|
||||||
|
if (!mWindow || !mWindow->MakeCurrent())
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(mMutex);
|
||||||
|
mStartupError = "Runtime shader prepare worker could not make shared GL context current.";
|
||||||
|
mStartupCondition.notify_all();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(mMutex);
|
||||||
|
mStartupReady = true;
|
||||||
|
}
|
||||||
|
mStarted.store(true, std::memory_order_release);
|
||||||
|
mStartupCondition.notify_all();
|
||||||
|
|
||||||
|
while (!mStopping.load(std::memory_order_acquire))
|
||||||
|
{
|
||||||
|
PrepareRequest request;
|
||||||
|
{
|
||||||
|
std::unique_lock<std::mutex> lock(mMutex);
|
||||||
|
mCondition.wait(lock, [this]() {
|
||||||
|
return mStopping.load(std::memory_order_acquire) || !mRequests.empty();
|
||||||
|
});
|
||||||
|
if (mStopping.load(std::memory_order_acquire))
|
||||||
|
break;
|
||||||
|
request = std::move(mRequests.front());
|
||||||
|
mRequests.pop_front();
|
||||||
|
}
|
||||||
|
|
||||||
|
RuntimePreparedShaderProgram preparedProgram;
|
||||||
|
RuntimeShaderRenderer::BuildPreparedPassProgram(
|
||||||
|
request.layerId,
|
||||||
|
request.sourceFingerprint,
|
||||||
|
request.artifact,
|
||||||
|
request.passArtifact,
|
||||||
|
preparedProgram);
|
||||||
|
glFlush();
|
||||||
|
|
||||||
|
std::lock_guard<std::mutex> lock(mMutex);
|
||||||
|
mCompleted.push_back(std::move(preparedProgram));
|
||||||
|
}
|
||||||
|
|
||||||
|
mWindow->ClearCurrent();
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string RuntimeShaderPrepareWorker::Fingerprint(const RuntimeShaderArtifact& artifact)
|
||||||
|
{
|
||||||
|
const std::hash<std::string> hasher;
|
||||||
|
std::string source;
|
||||||
|
for (const RuntimeShaderPassArtifact& pass : artifact.passes)
|
||||||
|
source += pass.passId + ":" + pass.outputName + ":" + pass.fragmentShaderSource + "\n";
|
||||||
|
if (source.empty())
|
||||||
|
source = artifact.fragmentShaderSource;
|
||||||
|
return artifact.shaderId + ":" + std::to_string(source.size()) + ":" + std::to_string(hasher(source));
|
||||||
|
}
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "RuntimeShaderProgram.h"
|
||||||
|
#include "../../runtime/RuntimeLayerModel.h"
|
||||||
|
|
||||||
|
#include <windows.h>
|
||||||
|
|
||||||
|
#include <atomic>
|
||||||
|
#include <condition_variable>
|
||||||
|
#include <deque>
|
||||||
|
#include <memory>
|
||||||
|
#include <mutex>
|
||||||
|
#include <string>
|
||||||
|
#include <thread>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
class HiddenGlWindow;
|
||||||
|
|
||||||
|
class RuntimeShaderPrepareWorker
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
RuntimeShaderPrepareWorker() = default;
|
||||||
|
RuntimeShaderPrepareWorker(const RuntimeShaderPrepareWorker&) = delete;
|
||||||
|
RuntimeShaderPrepareWorker& operator=(const RuntimeShaderPrepareWorker&) = delete;
|
||||||
|
~RuntimeShaderPrepareWorker();
|
||||||
|
|
||||||
|
bool Start(std::unique_ptr<HiddenGlWindow> sharedWindow, std::string& error);
|
||||||
|
void Stop();
|
||||||
|
|
||||||
|
void Submit(const std::vector<RenderCadenceCompositor::RuntimeRenderLayerModel>& layers);
|
||||||
|
bool TryConsume(RuntimePreparedShaderProgram& preparedProgram);
|
||||||
|
|
||||||
|
private:
|
||||||
|
struct PrepareRequest
|
||||||
|
{
|
||||||
|
std::string layerId;
|
||||||
|
std::string shaderId;
|
||||||
|
std::string passId;
|
||||||
|
std::string sourceFingerprint;
|
||||||
|
RuntimeShaderArtifact artifact;
|
||||||
|
RuntimeShaderPassArtifact passArtifact;
|
||||||
|
};
|
||||||
|
|
||||||
|
void ThreadMain();
|
||||||
|
static std::string Fingerprint(const RuntimeShaderArtifact& artifact);
|
||||||
|
|
||||||
|
std::unique_ptr<HiddenGlWindow> mWindow;
|
||||||
|
std::mutex mMutex;
|
||||||
|
std::condition_variable mCondition;
|
||||||
|
std::deque<PrepareRequest> mRequests;
|
||||||
|
std::deque<RuntimePreparedShaderProgram> mCompleted;
|
||||||
|
std::condition_variable mStartupCondition;
|
||||||
|
std::thread mThread;
|
||||||
|
std::atomic<bool> mStopping{ false };
|
||||||
|
std::atomic<bool> mStarted{ false };
|
||||||
|
bool mStartupReady = false;
|
||||||
|
std::string mStartupError;
|
||||||
|
};
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "GLExtensions.h"
|
||||||
|
#include "../../runtime/RuntimeShaderArtifact.h"
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
struct RuntimePreparedShaderProgram
|
||||||
|
{
|
||||||
|
std::string layerId;
|
||||||
|
std::string shaderId;
|
||||||
|
std::string passId;
|
||||||
|
std::string sourceFingerprint;
|
||||||
|
RuntimeShaderArtifact artifact;
|
||||||
|
RuntimeShaderPassArtifact passArtifact;
|
||||||
|
std::vector<std::string> inputNames;
|
||||||
|
std::string outputName;
|
||||||
|
GLuint program = 0;
|
||||||
|
GLuint vertexShader = 0;
|
||||||
|
GLuint fragmentShader = 0;
|
||||||
|
bool succeeded = false;
|
||||||
|
std::string error;
|
||||||
|
|
||||||
|
void ReleaseGl()
|
||||||
|
{
|
||||||
|
if (program != 0)
|
||||||
|
glDeleteProgram(program);
|
||||||
|
if (vertexShader != 0)
|
||||||
|
glDeleteShader(vertexShader);
|
||||||
|
if (fragmentShader != 0)
|
||||||
|
glDeleteShader(fragmentShader);
|
||||||
|
program = 0;
|
||||||
|
vertexShader = 0;
|
||||||
|
fragmentShader = 0;
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,320 @@
|
|||||||
|
#include "RuntimeShaderRenderer.h"
|
||||||
|
|
||||||
|
#include "RuntimeShaderParams.h"
|
||||||
|
|
||||||
|
#include <array>
|
||||||
|
#include <cstring>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
constexpr GLuint kGlobalParamsBindingPoint = 0;
|
||||||
|
constexpr GLuint kVideoInputTextureUnit = 0;
|
||||||
|
constexpr GLuint kLayerInputTextureUnit = 1;
|
||||||
|
|
||||||
|
const char* kVertexShaderSource = R"GLSL(
|
||||||
|
#version 430 core
|
||||||
|
out vec2 vTexCoord;
|
||||||
|
void main()
|
||||||
|
{
|
||||||
|
vec2 positions[3] = vec2[3](
|
||||||
|
vec2(-1.0, -1.0),
|
||||||
|
vec2( 3.0, -1.0),
|
||||||
|
vec2(-1.0, 3.0));
|
||||||
|
vec2 texCoords[3] = vec2[3](
|
||||||
|
vec2(0.0, 0.0),
|
||||||
|
vec2(2.0, 0.0),
|
||||||
|
vec2(0.0, 2.0));
|
||||||
|
gl_Position = vec4(positions[gl_VertexID], 0.0, 1.0);
|
||||||
|
vTexCoord = texCoords[gl_VertexID];
|
||||||
|
}
|
||||||
|
)GLSL";
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
RuntimeShaderRenderer::~RuntimeShaderRenderer()
|
||||||
|
{
|
||||||
|
ShutdownGl();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool RuntimeShaderRenderer::CommitPreparedProgram(RuntimePreparedShaderProgram& preparedProgram, std::string& error)
|
||||||
|
{
|
||||||
|
if (!preparedProgram.succeeded || preparedProgram.program == 0)
|
||||||
|
{
|
||||||
|
error = preparedProgram.error.empty() ? "Prepared runtime shader program is not valid." : preparedProgram.error;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!EnsureStaticGlResources(error))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
DestroyProgram();
|
||||||
|
mProgram = preparedProgram.program;
|
||||||
|
mVertexShader = preparedProgram.vertexShader;
|
||||||
|
mFragmentShader = preparedProgram.fragmentShader;
|
||||||
|
mArtifact = preparedProgram.artifact;
|
||||||
|
preparedProgram.program = 0;
|
||||||
|
preparedProgram.vertexShader = 0;
|
||||||
|
preparedProgram.fragmentShader = 0;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void RuntimeShaderRenderer::UpdateArtifactState(const RuntimeShaderArtifact& artifact)
|
||||||
|
{
|
||||||
|
mArtifact.parameterDefinitions = artifact.parameterDefinitions;
|
||||||
|
mArtifact.parameterValues = artifact.parameterValues;
|
||||||
|
mArtifact.message = artifact.message;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool RuntimeShaderRenderer::BuildPreparedProgram(
|
||||||
|
const std::string& layerId,
|
||||||
|
const std::string& sourceFingerprint,
|
||||||
|
const RuntimeShaderArtifact& artifact,
|
||||||
|
RuntimePreparedShaderProgram& preparedProgram)
|
||||||
|
{
|
||||||
|
RuntimeShaderPassArtifact passArtifact;
|
||||||
|
passArtifact.passId = "main";
|
||||||
|
passArtifact.fragmentShaderSource = artifact.fragmentShaderSource;
|
||||||
|
passArtifact.outputName = "layerOutput";
|
||||||
|
if (!artifact.passes.empty())
|
||||||
|
passArtifact = artifact.passes.front();
|
||||||
|
return BuildPreparedPassProgram(layerId, sourceFingerprint, artifact, passArtifact, preparedProgram);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool RuntimeShaderRenderer::BuildPreparedPassProgram(
|
||||||
|
const std::string& layerId,
|
||||||
|
const std::string& sourceFingerprint,
|
||||||
|
const RuntimeShaderArtifact& artifact,
|
||||||
|
const RuntimeShaderPassArtifact& passArtifact,
|
||||||
|
RuntimePreparedShaderProgram& preparedProgram)
|
||||||
|
{
|
||||||
|
preparedProgram = RuntimePreparedShaderProgram();
|
||||||
|
preparedProgram.layerId = layerId;
|
||||||
|
preparedProgram.shaderId = artifact.shaderId;
|
||||||
|
preparedProgram.passId = passArtifact.passId;
|
||||||
|
preparedProgram.sourceFingerprint = sourceFingerprint;
|
||||||
|
preparedProgram.artifact = artifact;
|
||||||
|
preparedProgram.passArtifact = passArtifact;
|
||||||
|
preparedProgram.inputNames = passArtifact.inputNames;
|
||||||
|
preparedProgram.outputName = passArtifact.outputName.empty() ? passArtifact.passId : passArtifact.outputName;
|
||||||
|
|
||||||
|
if (passArtifact.fragmentShaderSource.empty())
|
||||||
|
{
|
||||||
|
preparedProgram.error = "Cannot prepare an empty fragment shader.";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!BuildProgram(
|
||||||
|
passArtifact.fragmentShaderSource,
|
||||||
|
preparedProgram.program,
|
||||||
|
preparedProgram.vertexShader,
|
||||||
|
preparedProgram.fragmentShader,
|
||||||
|
preparedProgram.error))
|
||||||
|
{
|
||||||
|
preparedProgram.ReleaseGl();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
preparedProgram.succeeded = true;
|
||||||
|
AssignSamplerUniforms(preparedProgram.program);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void RuntimeShaderRenderer::RenderFrame(uint64_t frameIndex, unsigned width, unsigned height, GLuint sourceTexture, GLuint layerInputTexture)
|
||||||
|
{
|
||||||
|
if (mProgram == 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
glViewport(0, 0, static_cast<GLsizei>(width), static_cast<GLsizei>(height));
|
||||||
|
glDisable(GL_SCISSOR_TEST);
|
||||||
|
glDisable(GL_DEPTH_TEST);
|
||||||
|
glDisable(GL_BLEND);
|
||||||
|
UpdateGlobalParams(frameIndex, width, height);
|
||||||
|
BindRuntimeTextures(sourceTexture, layerInputTexture);
|
||||||
|
glBindVertexArray(mVertexArray);
|
||||||
|
glUseProgram(mProgram);
|
||||||
|
glDrawArrays(GL_TRIANGLES, 0, 3);
|
||||||
|
glUseProgram(0);
|
||||||
|
glBindVertexArray(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
void RuntimeShaderRenderer::ShutdownGl()
|
||||||
|
{
|
||||||
|
DestroyProgram();
|
||||||
|
DestroyStaticGlResources();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool RuntimeShaderRenderer::EnsureStaticGlResources(std::string& error)
|
||||||
|
{
|
||||||
|
if (mVertexArray == 0)
|
||||||
|
glGenVertexArrays(1, &mVertexArray);
|
||||||
|
if (mGlobalParamsBuffer == 0)
|
||||||
|
{
|
||||||
|
glGenBuffers(1, &mGlobalParamsBuffer);
|
||||||
|
glBindBuffer(GL_UNIFORM_BUFFER, mGlobalParamsBuffer);
|
||||||
|
glBufferData(GL_UNIFORM_BUFFER, 1024, nullptr, GL_DYNAMIC_DRAW);
|
||||||
|
glBindBuffer(GL_UNIFORM_BUFFER, 0);
|
||||||
|
}
|
||||||
|
if (mFallbackSourceTexture == 0)
|
||||||
|
{
|
||||||
|
const unsigned char pixels[] = {
|
||||||
|
0, 0, 0, 255,
|
||||||
|
96, 64, 32, 255,
|
||||||
|
64, 96, 160, 255,
|
||||||
|
255, 255, 255, 255
|
||||||
|
};
|
||||||
|
glGenTextures(1, &mFallbackSourceTexture);
|
||||||
|
glBindTexture(GL_TEXTURE_2D, mFallbackSourceTexture);
|
||||||
|
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, 2, 2, 0, GL_BGRA, GL_UNSIGNED_BYTE, pixels);
|
||||||
|
glBindTexture(GL_TEXTURE_2D, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mVertexArray == 0 || mGlobalParamsBuffer == 0 || mFallbackSourceTexture == 0)
|
||||||
|
{
|
||||||
|
error = "Failed to create runtime shader GL resources.";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool RuntimeShaderRenderer::BuildProgram(const std::string& fragmentShaderSource, GLuint& program, GLuint& vertexShader, GLuint& fragmentShader, std::string& error)
|
||||||
|
{
|
||||||
|
program = 0;
|
||||||
|
vertexShader = 0;
|
||||||
|
fragmentShader = 0;
|
||||||
|
|
||||||
|
if (!CompileShader(GL_VERTEX_SHADER, kVertexShaderSource, vertexShader, error))
|
||||||
|
return false;
|
||||||
|
if (!CompileShader(GL_FRAGMENT_SHADER, fragmentShaderSource.c_str(), fragmentShader, error))
|
||||||
|
{
|
||||||
|
glDeleteShader(vertexShader);
|
||||||
|
vertexShader = 0;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
program = glCreateProgram();
|
||||||
|
glAttachShader(program, vertexShader);
|
||||||
|
glAttachShader(program, fragmentShader);
|
||||||
|
glLinkProgram(program);
|
||||||
|
|
||||||
|
GLint linkResult = GL_FALSE;
|
||||||
|
glGetProgramiv(program, GL_LINK_STATUS, &linkResult);
|
||||||
|
if (linkResult == GL_FALSE)
|
||||||
|
{
|
||||||
|
std::array<char, 4096> log = {};
|
||||||
|
GLsizei length = 0;
|
||||||
|
glGetProgramInfoLog(program, static_cast<GLsizei>(log.size()), &length, log.data());
|
||||||
|
error = std::string(log.data(), static_cast<std::size_t>(length));
|
||||||
|
glDeleteProgram(program);
|
||||||
|
glDeleteShader(vertexShader);
|
||||||
|
glDeleteShader(fragmentShader);
|
||||||
|
program = 0;
|
||||||
|
vertexShader = 0;
|
||||||
|
fragmentShader = 0;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const GLuint globalParamsIndex = glGetUniformBlockIndex(program, "GlobalParams");
|
||||||
|
if (globalParamsIndex != GL_INVALID_INDEX)
|
||||||
|
glUniformBlockBinding(program, globalParamsIndex, kGlobalParamsBindingPoint);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void RuntimeShaderRenderer::AssignSamplerUniforms(GLuint program)
|
||||||
|
{
|
||||||
|
glUseProgram(program);
|
||||||
|
const GLint videoInputLocation = glGetUniformLocation(program, "gVideoInput");
|
||||||
|
if (videoInputLocation >= 0)
|
||||||
|
glUniform1i(videoInputLocation, static_cast<GLint>(kVideoInputTextureUnit));
|
||||||
|
const GLint videoInputArrayLocation = glGetUniformLocation(program, "gVideoInput_0");
|
||||||
|
if (videoInputArrayLocation >= 0)
|
||||||
|
glUniform1i(videoInputArrayLocation, static_cast<GLint>(kVideoInputTextureUnit));
|
||||||
|
const GLint layerInputLocation = glGetUniformLocation(program, "gLayerInput");
|
||||||
|
if (layerInputLocation >= 0)
|
||||||
|
glUniform1i(layerInputLocation, static_cast<GLint>(kLayerInputTextureUnit));
|
||||||
|
const GLint layerInputArrayLocation = glGetUniformLocation(program, "gLayerInput_0");
|
||||||
|
if (layerInputArrayLocation >= 0)
|
||||||
|
glUniform1i(layerInputArrayLocation, static_cast<GLint>(kLayerInputTextureUnit));
|
||||||
|
glUseProgram(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
void RuntimeShaderRenderer::UpdateGlobalParams(uint64_t frameIndex, unsigned width, unsigned height)
|
||||||
|
{
|
||||||
|
std::vector<unsigned char>& buffer = mGlobalParamsScratch;
|
||||||
|
buffer = BuildRuntimeShaderGlobalParamsStd140(mArtifact, frameIndex, width, height);
|
||||||
|
glBindBuffer(GL_UNIFORM_BUFFER, mGlobalParamsBuffer);
|
||||||
|
const GLsizeiptr bufferSize = static_cast<GLsizeiptr>(buffer.size());
|
||||||
|
if (mGlobalParamsBufferSize != bufferSize)
|
||||||
|
{
|
||||||
|
glBufferData(GL_UNIFORM_BUFFER, bufferSize, buffer.data(), GL_DYNAMIC_DRAW);
|
||||||
|
mGlobalParamsBufferSize = bufferSize;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
glBufferSubData(GL_UNIFORM_BUFFER, 0, bufferSize, buffer.data());
|
||||||
|
}
|
||||||
|
glBindBufferBase(GL_UNIFORM_BUFFER, kGlobalParamsBindingPoint, mGlobalParamsBuffer);
|
||||||
|
glBindBuffer(GL_UNIFORM_BUFFER, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
void RuntimeShaderRenderer::BindRuntimeTextures(GLuint sourceTexture, GLuint layerInputTexture)
|
||||||
|
{
|
||||||
|
const GLuint resolvedSourceTexture = sourceTexture != 0 ? sourceTexture : mFallbackSourceTexture;
|
||||||
|
const GLuint resolvedLayerInputTexture = layerInputTexture != 0 ? layerInputTexture : resolvedSourceTexture;
|
||||||
|
glActiveTexture(GL_TEXTURE0 + kVideoInputTextureUnit);
|
||||||
|
glBindTexture(GL_TEXTURE_2D, resolvedSourceTexture);
|
||||||
|
glActiveTexture(GL_TEXTURE0 + kLayerInputTextureUnit);
|
||||||
|
glBindTexture(GL_TEXTURE_2D, resolvedLayerInputTexture);
|
||||||
|
glActiveTexture(GL_TEXTURE0);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool RuntimeShaderRenderer::CompileShader(GLenum shaderType, const char* source, GLuint& shader, std::string& error)
|
||||||
|
{
|
||||||
|
shader = glCreateShader(shaderType);
|
||||||
|
glShaderSource(shader, 1, &source, nullptr);
|
||||||
|
glCompileShader(shader);
|
||||||
|
|
||||||
|
GLint compileResult = GL_FALSE;
|
||||||
|
glGetShaderiv(shader, GL_COMPILE_STATUS, &compileResult);
|
||||||
|
if (compileResult != GL_FALSE)
|
||||||
|
return true;
|
||||||
|
|
||||||
|
std::array<char, 4096> log = {};
|
||||||
|
GLsizei length = 0;
|
||||||
|
glGetShaderInfoLog(shader, static_cast<GLsizei>(log.size()), &length, log.data());
|
||||||
|
error = std::string(log.data(), static_cast<std::size_t>(length));
|
||||||
|
glDeleteShader(shader);
|
||||||
|
shader = 0;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void RuntimeShaderRenderer::DestroyProgram()
|
||||||
|
{
|
||||||
|
if (mProgram != 0)
|
||||||
|
glDeleteProgram(mProgram);
|
||||||
|
if (mVertexShader != 0)
|
||||||
|
glDeleteShader(mVertexShader);
|
||||||
|
if (mFragmentShader != 0)
|
||||||
|
glDeleteShader(mFragmentShader);
|
||||||
|
mProgram = 0;
|
||||||
|
mVertexShader = 0;
|
||||||
|
mFragmentShader = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
void RuntimeShaderRenderer::DestroyStaticGlResources()
|
||||||
|
{
|
||||||
|
if (mGlobalParamsBuffer != 0)
|
||||||
|
glDeleteBuffers(1, &mGlobalParamsBuffer);
|
||||||
|
if (mVertexArray != 0)
|
||||||
|
glDeleteVertexArrays(1, &mVertexArray);
|
||||||
|
if (mFallbackSourceTexture != 0)
|
||||||
|
glDeleteTextures(1, &mFallbackSourceTexture);
|
||||||
|
mGlobalParamsBuffer = 0;
|
||||||
|
mGlobalParamsBufferSize = 0;
|
||||||
|
mVertexArray = 0;
|
||||||
|
mFallbackSourceTexture = 0;
|
||||||
|
}
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "GLExtensions.h"
|
||||||
|
#include "RuntimeShaderProgram.h"
|
||||||
|
#include "../../runtime/RuntimeShaderArtifact.h"
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
class RuntimeShaderRenderer
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
RuntimeShaderRenderer() = default;
|
||||||
|
RuntimeShaderRenderer(const RuntimeShaderRenderer&) = delete;
|
||||||
|
RuntimeShaderRenderer& operator=(const RuntimeShaderRenderer&) = delete;
|
||||||
|
~RuntimeShaderRenderer();
|
||||||
|
|
||||||
|
bool CommitPreparedProgram(RuntimePreparedShaderProgram& preparedProgram, std::string& error);
|
||||||
|
bool HasProgram() const { return mProgram != 0; }
|
||||||
|
void UpdateArtifactState(const RuntimeShaderArtifact& artifact);
|
||||||
|
void RenderFrame(uint64_t frameIndex, unsigned width, unsigned height, GLuint sourceTexture = 0, GLuint layerInputTexture = 0);
|
||||||
|
void ShutdownGl();
|
||||||
|
|
||||||
|
static bool BuildPreparedProgram(
|
||||||
|
const std::string& layerId,
|
||||||
|
const std::string& sourceFingerprint,
|
||||||
|
const RuntimeShaderArtifact& artifact,
|
||||||
|
RuntimePreparedShaderProgram& preparedProgram);
|
||||||
|
static bool BuildPreparedPassProgram(
|
||||||
|
const std::string& layerId,
|
||||||
|
const std::string& sourceFingerprint,
|
||||||
|
const RuntimeShaderArtifact& artifact,
|
||||||
|
const RuntimeShaderPassArtifact& passArtifact,
|
||||||
|
RuntimePreparedShaderProgram& preparedProgram);
|
||||||
|
|
||||||
|
private:
|
||||||
|
bool EnsureStaticGlResources(std::string& error);
|
||||||
|
static bool CompileShader(GLenum shaderType, const char* source, GLuint& shader, std::string& error);
|
||||||
|
static bool BuildProgram(const std::string& fragmentShaderSource, GLuint& program, GLuint& vertexShader, GLuint& fragmentShader, std::string& error);
|
||||||
|
static void AssignSamplerUniforms(GLuint program);
|
||||||
|
void UpdateGlobalParams(uint64_t frameIndex, unsigned width, unsigned height);
|
||||||
|
void BindRuntimeTextures(GLuint sourceTexture, GLuint layerInputTexture);
|
||||||
|
void DestroyProgram();
|
||||||
|
void DestroyStaticGlResources();
|
||||||
|
|
||||||
|
RuntimeShaderArtifact mArtifact;
|
||||||
|
GLuint mProgram = 0;
|
||||||
|
GLuint mVertexShader = 0;
|
||||||
|
GLuint mFragmentShader = 0;
|
||||||
|
GLuint mVertexArray = 0;
|
||||||
|
GLuint mGlobalParamsBuffer = 0;
|
||||||
|
GLsizeiptr mGlobalParamsBufferSize = 0;
|
||||||
|
GLuint mFallbackSourceTexture = 0;
|
||||||
|
std::vector<unsigned char> mGlobalParamsScratch;
|
||||||
|
};
|
||||||
379
apps/RenderCadenceCompositor/runtime/RuntimeLayerModel.cpp
Normal file
379
apps/RenderCadenceCompositor/runtime/RuntimeLayerModel.cpp
Normal file
@@ -0,0 +1,379 @@
|
|||||||
|
#include "RuntimeLayerModel.h"
|
||||||
|
|
||||||
|
#include "RuntimeParameterUtils.h"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <chrono>
|
||||||
|
#include <utility>
|
||||||
|
|
||||||
|
namespace RenderCadenceCompositor
|
||||||
|
{
|
||||||
|
bool RuntimeLayerModel::InitializeSingleLayer(const SupportedShaderCatalog& shaderCatalog, const std::string& shaderId, std::string& error)
|
||||||
|
{
|
||||||
|
Clear();
|
||||||
|
if (shaderId.empty())
|
||||||
|
{
|
||||||
|
error.clear();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ShaderPackage* shaderPackage = shaderCatalog.FindPackage(shaderId);
|
||||||
|
if (!shaderPackage)
|
||||||
|
{
|
||||||
|
error = "Shader '" + shaderId + "' is not in the supported shader catalog.";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
Layer layer;
|
||||||
|
layer.id = AllocateLayerId();
|
||||||
|
layer.shaderId = shaderPackage->id;
|
||||||
|
layer.shaderName = shaderPackage->displayName.empty() ? shaderPackage->id : shaderPackage->displayName;
|
||||||
|
layer.buildState = RuntimeLayerBuildState::Pending;
|
||||||
|
layer.message = "Runtime Slang build is waiting to start.";
|
||||||
|
InitializeDefaultParameterValues(layer, *shaderPackage);
|
||||||
|
mLayers.push_back(std::move(layer));
|
||||||
|
error.clear();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool RuntimeLayerModel::AddLayer(const SupportedShaderCatalog& shaderCatalog, const std::string& shaderId, std::string& layerId, std::string& error)
|
||||||
|
{
|
||||||
|
const ShaderPackage* shaderPackage = shaderCatalog.FindPackage(shaderId);
|
||||||
|
if (!shaderPackage)
|
||||||
|
{
|
||||||
|
error = "Shader '" + shaderId + "' is not in the supported shader catalog.";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
Layer layer;
|
||||||
|
layer.id = AllocateLayerId();
|
||||||
|
layer.shaderId = shaderPackage->id;
|
||||||
|
layer.shaderName = shaderPackage->displayName.empty() ? shaderPackage->id : shaderPackage->displayName;
|
||||||
|
layer.buildState = RuntimeLayerBuildState::Pending;
|
||||||
|
layer.message = "Runtime Slang build is waiting to start.";
|
||||||
|
InitializeDefaultParameterValues(layer, *shaderPackage);
|
||||||
|
layerId = layer.id;
|
||||||
|
mLayers.push_back(std::move(layer));
|
||||||
|
error.clear();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool RuntimeLayerModel::RemoveLayer(const std::string& layerId, std::string& error)
|
||||||
|
{
|
||||||
|
for (auto layerIt = mLayers.begin(); layerIt != mLayers.end(); ++layerIt)
|
||||||
|
{
|
||||||
|
if (layerIt->id != layerId)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
mLayers.erase(layerIt);
|
||||||
|
error.clear();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
error = "Unknown runtime layer id: " + layerId;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool RuntimeLayerModel::ReorderLayer(const std::string& layerId, int targetIndex, std::string& error)
|
||||||
|
{
|
||||||
|
auto layerIt = std::find_if(mLayers.begin(), mLayers.end(), [&layerId](const Layer& layer) {
|
||||||
|
return layer.id == layerId;
|
||||||
|
});
|
||||||
|
if (layerIt == mLayers.end())
|
||||||
|
{
|
||||||
|
error = "Unknown runtime layer id: " + layerId;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (targetIndex < 0)
|
||||||
|
targetIndex = 0;
|
||||||
|
if (targetIndex >= static_cast<int>(mLayers.size()))
|
||||||
|
targetIndex = static_cast<int>(mLayers.size()) - 1;
|
||||||
|
|
||||||
|
Layer layer = std::move(*layerIt);
|
||||||
|
mLayers.erase(layerIt);
|
||||||
|
std::size_t destinationIndex = static_cast<std::size_t>(targetIndex);
|
||||||
|
if (destinationIndex > mLayers.size())
|
||||||
|
destinationIndex = mLayers.size();
|
||||||
|
mLayers.insert(mLayers.begin() + static_cast<std::ptrdiff_t>(destinationIndex), std::move(layer));
|
||||||
|
error.clear();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool RuntimeLayerModel::SetLayerBypass(const std::string& layerId, bool bypass, std::string& error)
|
||||||
|
{
|
||||||
|
Layer* layer = FindLayer(layerId);
|
||||||
|
if (!layer)
|
||||||
|
{
|
||||||
|
error = "Unknown runtime layer id: " + layerId;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
layer->bypass = bypass;
|
||||||
|
error.clear();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool RuntimeLayerModel::SetLayerShader(const SupportedShaderCatalog& shaderCatalog, const std::string& layerId, const std::string& shaderId, std::string& error)
|
||||||
|
{
|
||||||
|
Layer* layer = FindLayer(layerId);
|
||||||
|
if (!layer)
|
||||||
|
{
|
||||||
|
error = "Unknown runtime layer id: " + layerId;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ShaderPackage* shaderPackage = shaderCatalog.FindPackage(shaderId);
|
||||||
|
if (!shaderPackage)
|
||||||
|
{
|
||||||
|
error = "Shader '" + shaderId + "' is not in the supported shader catalog.";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
layer->shaderId = shaderPackage->id;
|
||||||
|
layer->shaderName = shaderPackage->displayName.empty() ? shaderPackage->id : shaderPackage->displayName;
|
||||||
|
layer->buildState = RuntimeLayerBuildState::Pending;
|
||||||
|
layer->message = "Runtime Slang build is waiting to start.";
|
||||||
|
layer->renderReady = false;
|
||||||
|
layer->artifact = RuntimeShaderArtifact();
|
||||||
|
InitializeDefaultParameterValues(*layer, *shaderPackage);
|
||||||
|
error.clear();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool RuntimeLayerModel::UpdateParameter(const std::string& layerId, const std::string& parameterId, const JsonValue& value, std::string& error)
|
||||||
|
{
|
||||||
|
Layer* layer = FindLayer(layerId);
|
||||||
|
if (!layer)
|
||||||
|
{
|
||||||
|
error = "Unknown runtime layer id: " + layerId;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ShaderParameterDefinition* definition = FindParameterDefinition(*layer, parameterId);
|
||||||
|
if (!definition)
|
||||||
|
{
|
||||||
|
error = "Unknown parameter id '" + parameterId + "' for layer " + layerId + ".";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
ShaderParameterValue normalizedValue;
|
||||||
|
if (definition->type == ShaderParameterType::Trigger)
|
||||||
|
{
|
||||||
|
const auto currentIt = layer->parameterValues.find(parameterId);
|
||||||
|
const double previousCount = currentIt == layer->parameterValues.end() || currentIt->second.numberValues.empty()
|
||||||
|
? 0.0
|
||||||
|
: currentIt->second.numberValues.front();
|
||||||
|
normalizedValue.numberValues = { previousCount + 1.0, RuntimeElapsedSeconds() };
|
||||||
|
}
|
||||||
|
else if (!NormalizeAndValidateParameterValue(*definition, value, normalizedValue, error))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
layer->parameterValues[parameterId] = normalizedValue;
|
||||||
|
if (layer->renderReady)
|
||||||
|
layer->artifact.parameterValues = layer->parameterValues;
|
||||||
|
error.clear();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool RuntimeLayerModel::ResetParameters(const std::string& layerId, std::string& error)
|
||||||
|
{
|
||||||
|
Layer* layer = FindLayer(layerId);
|
||||||
|
if (!layer)
|
||||||
|
{
|
||||||
|
error = "Unknown runtime layer id: " + layerId;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
layer->parameterValues.clear();
|
||||||
|
for (const ShaderParameterDefinition& definition : layer->parameterDefinitions)
|
||||||
|
layer->parameterValues[definition.id] = DefaultValueForDefinition(definition);
|
||||||
|
if (layer->renderReady)
|
||||||
|
layer->artifact.parameterValues = layer->parameterValues;
|
||||||
|
error.clear();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void RuntimeLayerModel::Clear()
|
||||||
|
{
|
||||||
|
mLayers.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool RuntimeLayerModel::MarkBuildStarted(const std::string& layerId, const std::string& message, std::string& error)
|
||||||
|
{
|
||||||
|
Layer* layer = FindLayer(layerId);
|
||||||
|
if (!layer)
|
||||||
|
{
|
||||||
|
error = "Unknown runtime layer id: " + layerId;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
layer->buildState = RuntimeLayerBuildState::Pending;
|
||||||
|
layer->message = message;
|
||||||
|
layer->renderReady = false;
|
||||||
|
layer->artifact = RuntimeShaderArtifact();
|
||||||
|
error.clear();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool RuntimeLayerModel::MarkBuildReady(const RuntimeShaderArtifact& artifact, std::string& error)
|
||||||
|
{
|
||||||
|
Layer* layer = artifact.layerId.empty() ? FindFirstLayerForShader(artifact.shaderId) : FindLayer(artifact.layerId);
|
||||||
|
if (!layer)
|
||||||
|
{
|
||||||
|
error = artifact.layerId.empty()
|
||||||
|
? "No runtime layer is waiting for shader artifact: " + artifact.shaderId
|
||||||
|
: "No runtime layer is waiting for shader artifact on layer: " + artifact.layerId;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
layer->shaderName = artifact.displayName.empty() ? artifact.shaderId : artifact.displayName;
|
||||||
|
layer->buildState = RuntimeLayerBuildState::Ready;
|
||||||
|
layer->message = artifact.message;
|
||||||
|
layer->renderReady = true;
|
||||||
|
layer->artifact = artifact;
|
||||||
|
layer->artifact.parameterValues = layer->parameterValues;
|
||||||
|
error.clear();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool RuntimeLayerModel::MarkBuildFailedForShader(const std::string& shaderId, const std::string& message)
|
||||||
|
{
|
||||||
|
Layer* layer = FindFirstLayerForShader(shaderId);
|
||||||
|
if (!layer)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
std::string error;
|
||||||
|
return MarkBuildFailed(layer->id, message, error);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool RuntimeLayerModel::MarkBuildFailed(const std::string& layerId, const std::string& message, std::string& error)
|
||||||
|
{
|
||||||
|
Layer* layer = FindLayer(layerId);
|
||||||
|
if (!layer)
|
||||||
|
{
|
||||||
|
error = "Unknown runtime layer id: " + layerId;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
layer->buildState = RuntimeLayerBuildState::Failed;
|
||||||
|
layer->message = message;
|
||||||
|
layer->renderReady = false;
|
||||||
|
layer->artifact = RuntimeShaderArtifact();
|
||||||
|
error.clear();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool RuntimeLayerModel::MarkRenderCommitFailed(const std::string& layerId, const std::string& message, std::string& error)
|
||||||
|
{
|
||||||
|
return MarkBuildFailed(layerId, message, error);
|
||||||
|
}
|
||||||
|
|
||||||
|
RuntimeLayerModelSnapshot RuntimeLayerModel::Snapshot() const
|
||||||
|
{
|
||||||
|
RuntimeLayerModelSnapshot snapshot;
|
||||||
|
snapshot.compileSucceeded = true;
|
||||||
|
|
||||||
|
for (const Layer& layer : mLayers)
|
||||||
|
{
|
||||||
|
snapshot.displayLayers.push_back(ToReadModel(layer));
|
||||||
|
if (!layer.message.empty() && snapshot.compileMessage.empty())
|
||||||
|
snapshot.compileMessage = layer.message;
|
||||||
|
if (layer.buildState == RuntimeLayerBuildState::Failed)
|
||||||
|
snapshot.compileSucceeded = false;
|
||||||
|
if (layer.renderReady)
|
||||||
|
{
|
||||||
|
RuntimeRenderLayerModel renderLayer;
|
||||||
|
renderLayer.id = layer.id;
|
||||||
|
renderLayer.shaderId = layer.shaderId;
|
||||||
|
renderLayer.bypass = layer.bypass;
|
||||||
|
renderLayer.artifact = layer.artifact;
|
||||||
|
renderLayer.artifact.parameterValues = layer.parameterValues;
|
||||||
|
snapshot.renderLayers.push_back(std::move(renderLayer));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (snapshot.compileMessage.empty())
|
||||||
|
snapshot.compileMessage = mLayers.empty() ? "Runtime shader build disabled." : "Runtime shader build has not completed yet.";
|
||||||
|
return snapshot;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string RuntimeLayerModel::FirstLayerId() const
|
||||||
|
{
|
||||||
|
return mLayers.empty() ? std::string() : mLayers.front().id;
|
||||||
|
}
|
||||||
|
|
||||||
|
RuntimeLayerModel::Layer* RuntimeLayerModel::FindLayer(const std::string& layerId)
|
||||||
|
{
|
||||||
|
for (Layer& layer : mLayers)
|
||||||
|
{
|
||||||
|
if (layer.id == layerId)
|
||||||
|
return &layer;
|
||||||
|
}
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
const RuntimeLayerModel::Layer* RuntimeLayerModel::FindLayer(const std::string& layerId) const
|
||||||
|
{
|
||||||
|
for (const Layer& layer : mLayers)
|
||||||
|
{
|
||||||
|
if (layer.id == layerId)
|
||||||
|
return &layer;
|
||||||
|
}
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
RuntimeLayerModel::Layer* RuntimeLayerModel::FindFirstLayerForShader(const std::string& shaderId)
|
||||||
|
{
|
||||||
|
for (Layer& layer : mLayers)
|
||||||
|
{
|
||||||
|
if (layer.shaderId == shaderId)
|
||||||
|
return &layer;
|
||||||
|
}
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
void RuntimeLayerModel::InitializeDefaultParameterValues(Layer& layer, const ShaderPackage& shaderPackage)
|
||||||
|
{
|
||||||
|
layer.parameterDefinitions = shaderPackage.parameters;
|
||||||
|
layer.parameterValues.clear();
|
||||||
|
for (const ShaderParameterDefinition& definition : layer.parameterDefinitions)
|
||||||
|
layer.parameterValues[definition.id] = DefaultValueForDefinition(definition);
|
||||||
|
}
|
||||||
|
|
||||||
|
const ShaderParameterDefinition* RuntimeLayerModel::FindParameterDefinition(const Layer& layer, const std::string& parameterId)
|
||||||
|
{
|
||||||
|
for (const ShaderParameterDefinition& definition : layer.parameterDefinitions)
|
||||||
|
{
|
||||||
|
if (definition.id == parameterId)
|
||||||
|
return &definition;
|
||||||
|
}
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string RuntimeLayerModel::AllocateLayerId()
|
||||||
|
{
|
||||||
|
return "runtime-layer-" + std::to_string(mNextLayerNumber++);
|
||||||
|
}
|
||||||
|
|
||||||
|
RuntimeLayerReadModel RuntimeLayerModel::ToReadModel(const Layer& layer)
|
||||||
|
{
|
||||||
|
RuntimeLayerReadModel readModel;
|
||||||
|
readModel.id = layer.id;
|
||||||
|
readModel.shaderId = layer.shaderId;
|
||||||
|
readModel.shaderName = layer.shaderName;
|
||||||
|
readModel.bypass = layer.bypass;
|
||||||
|
readModel.buildState = layer.buildState;
|
||||||
|
readModel.message = layer.message;
|
||||||
|
readModel.renderReady = layer.renderReady;
|
||||||
|
readModel.parameterDefinitions = layer.parameterDefinitions;
|
||||||
|
readModel.parameterValues = layer.parameterValues;
|
||||||
|
return readModel;
|
||||||
|
}
|
||||||
|
|
||||||
|
double RuntimeLayerModel::RuntimeElapsedSeconds() const
|
||||||
|
{
|
||||||
|
return std::chrono::duration_cast<std::chrono::duration<double>>(std::chrono::steady_clock::now() - mStartTime).count();
|
||||||
|
}
|
||||||
|
}
|
||||||
101
apps/RenderCadenceCompositor/runtime/RuntimeLayerModel.h
Normal file
101
apps/RenderCadenceCompositor/runtime/RuntimeLayerModel.h
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "RuntimeJson.h"
|
||||||
|
#include "RuntimeShaderArtifact.h"
|
||||||
|
#include "SupportedShaderCatalog.h"
|
||||||
|
|
||||||
|
#include <chrono>
|
||||||
|
#include <cstdint>
|
||||||
|
#include <map>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
namespace RenderCadenceCompositor
|
||||||
|
{
|
||||||
|
enum class RuntimeLayerBuildState
|
||||||
|
{
|
||||||
|
Pending,
|
||||||
|
Ready,
|
||||||
|
Failed
|
||||||
|
};
|
||||||
|
|
||||||
|
struct RuntimeLayerReadModel
|
||||||
|
{
|
||||||
|
std::string id;
|
||||||
|
std::string shaderId;
|
||||||
|
std::string shaderName;
|
||||||
|
bool bypass = false;
|
||||||
|
RuntimeLayerBuildState buildState = RuntimeLayerBuildState::Pending;
|
||||||
|
std::string message;
|
||||||
|
bool renderReady = false;
|
||||||
|
std::vector<ShaderParameterDefinition> parameterDefinitions;
|
||||||
|
std::map<std::string, ShaderParameterValue> parameterValues;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct RuntimeRenderLayerModel
|
||||||
|
{
|
||||||
|
std::string id;
|
||||||
|
std::string shaderId;
|
||||||
|
bool bypass = false;
|
||||||
|
RuntimeShaderArtifact artifact;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct RuntimeLayerModelSnapshot
|
||||||
|
{
|
||||||
|
bool compileSucceeded = true;
|
||||||
|
std::string compileMessage;
|
||||||
|
std::vector<RuntimeLayerReadModel> displayLayers;
|
||||||
|
std::vector<RuntimeRenderLayerModel> renderLayers;
|
||||||
|
};
|
||||||
|
|
||||||
|
class RuntimeLayerModel
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
bool InitializeSingleLayer(const SupportedShaderCatalog& shaderCatalog, const std::string& shaderId, std::string& error);
|
||||||
|
void Clear();
|
||||||
|
|
||||||
|
bool AddLayer(const SupportedShaderCatalog& shaderCatalog, const std::string& shaderId, std::string& layerId, std::string& error);
|
||||||
|
bool RemoveLayer(const std::string& layerId, std::string& error);
|
||||||
|
bool ReorderLayer(const std::string& layerId, int targetIndex, std::string& error);
|
||||||
|
bool SetLayerBypass(const std::string& layerId, bool bypass, std::string& error);
|
||||||
|
bool SetLayerShader(const SupportedShaderCatalog& shaderCatalog, const std::string& layerId, const std::string& shaderId, std::string& error);
|
||||||
|
bool UpdateParameter(const std::string& layerId, const std::string& parameterId, const JsonValue& value, std::string& error);
|
||||||
|
bool ResetParameters(const std::string& layerId, std::string& error);
|
||||||
|
bool MarkBuildStarted(const std::string& layerId, const std::string& message, std::string& error);
|
||||||
|
bool MarkBuildReady(const RuntimeShaderArtifact& artifact, std::string& error);
|
||||||
|
bool MarkBuildFailedForShader(const std::string& shaderId, const std::string& message);
|
||||||
|
bool MarkBuildFailed(const std::string& layerId, const std::string& message, std::string& error);
|
||||||
|
bool MarkRenderCommitFailed(const std::string& layerId, const std::string& message, std::string& error);
|
||||||
|
|
||||||
|
RuntimeLayerModelSnapshot Snapshot() const;
|
||||||
|
std::string FirstLayerId() const;
|
||||||
|
|
||||||
|
private:
|
||||||
|
struct Layer
|
||||||
|
{
|
||||||
|
std::string id;
|
||||||
|
std::string shaderId;
|
||||||
|
std::string shaderName;
|
||||||
|
bool bypass = false;
|
||||||
|
RuntimeLayerBuildState buildState = RuntimeLayerBuildState::Pending;
|
||||||
|
std::string message;
|
||||||
|
bool renderReady = false;
|
||||||
|
std::vector<ShaderParameterDefinition> parameterDefinitions;
|
||||||
|
std::map<std::string, ShaderParameterValue> parameterValues;
|
||||||
|
RuntimeShaderArtifact artifact;
|
||||||
|
};
|
||||||
|
|
||||||
|
Layer* FindLayer(const std::string& layerId);
|
||||||
|
const Layer* FindLayer(const std::string& layerId) const;
|
||||||
|
Layer* FindFirstLayerForShader(const std::string& shaderId);
|
||||||
|
static void InitializeDefaultParameterValues(Layer& layer, const ShaderPackage& shaderPackage);
|
||||||
|
static const ShaderParameterDefinition* FindParameterDefinition(const Layer& layer, const std::string& parameterId);
|
||||||
|
std::string AllocateLayerId();
|
||||||
|
static RuntimeLayerReadModel ToReadModel(const Layer& layer);
|
||||||
|
double RuntimeElapsedSeconds() const;
|
||||||
|
|
||||||
|
std::vector<Layer> mLayers;
|
||||||
|
uint64_t mNextLayerNumber = 1;
|
||||||
|
std::chrono::steady_clock::time_point mStartTime = std::chrono::steady_clock::now();
|
||||||
|
};
|
||||||
|
}
|
||||||
27
apps/RenderCadenceCompositor/runtime/RuntimeShaderArtifact.h
Normal file
27
apps/RenderCadenceCompositor/runtime/RuntimeShaderArtifact.h
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "ShaderTypes.h"
|
||||||
|
|
||||||
|
#include <map>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
struct RuntimeShaderPassArtifact
|
||||||
|
{
|
||||||
|
std::string passId;
|
||||||
|
std::string fragmentShaderSource;
|
||||||
|
std::vector<std::string> inputNames;
|
||||||
|
std::string outputName;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct RuntimeShaderArtifact
|
||||||
|
{
|
||||||
|
std::string layerId;
|
||||||
|
std::string shaderId;
|
||||||
|
std::string displayName;
|
||||||
|
std::string fragmentShaderSource;
|
||||||
|
std::vector<RuntimeShaderPassArtifact> passes;
|
||||||
|
std::string message;
|
||||||
|
std::vector<ShaderParameterDefinition> parameterDefinitions;
|
||||||
|
std::map<std::string, ShaderParameterValue> parameterValues;
|
||||||
|
};
|
||||||
75
apps/RenderCadenceCompositor/runtime/RuntimeShaderBridge.cpp
Normal file
75
apps/RenderCadenceCompositor/runtime/RuntimeShaderBridge.cpp
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
#include "RuntimeShaderBridge.h"
|
||||||
|
|
||||||
|
#include <chrono>
|
||||||
|
|
||||||
|
RuntimeShaderBridge::~RuntimeShaderBridge()
|
||||||
|
{
|
||||||
|
Stop();
|
||||||
|
}
|
||||||
|
|
||||||
|
void RuntimeShaderBridge::Start(const std::string& shaderId, ArtifactCallback onArtifactReady, ErrorCallback onError)
|
||||||
|
{
|
||||||
|
Start(std::string(), shaderId, std::move(onArtifactReady), std::move(onError));
|
||||||
|
}
|
||||||
|
|
||||||
|
void RuntimeShaderBridge::Start(const std::string& layerId, const std::string& shaderId, ArtifactCallback onArtifactReady, ErrorCallback onError)
|
||||||
|
{
|
||||||
|
Stop();
|
||||||
|
if (shaderId.empty())
|
||||||
|
return;
|
||||||
|
|
||||||
|
mLayerId = layerId;
|
||||||
|
mOnArtifactReady = std::move(onArtifactReady);
|
||||||
|
mOnError = std::move(onError);
|
||||||
|
mStopping.store(false, std::memory_order_release);
|
||||||
|
mFinished.store(false, std::memory_order_release);
|
||||||
|
mCompiler.StartShaderBuild(shaderId);
|
||||||
|
mThread = std::thread([this]() { ThreadMain(); });
|
||||||
|
}
|
||||||
|
|
||||||
|
void RuntimeShaderBridge::RequestStop()
|
||||||
|
{
|
||||||
|
mStopping.store(true, std::memory_order_release);
|
||||||
|
}
|
||||||
|
|
||||||
|
void RuntimeShaderBridge::Stop()
|
||||||
|
{
|
||||||
|
RequestStop();
|
||||||
|
if (mThread.joinable())
|
||||||
|
mThread.join();
|
||||||
|
mCompiler.Stop();
|
||||||
|
mLayerId.clear();
|
||||||
|
mOnArtifactReady = ArtifactCallback();
|
||||||
|
mOnError = ErrorCallback();
|
||||||
|
mFinished.store(true, std::memory_order_release);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool RuntimeShaderBridge::CanStopWithoutWaiting() const
|
||||||
|
{
|
||||||
|
return mFinished.load(std::memory_order_acquire) && !mCompiler.Running();
|
||||||
|
}
|
||||||
|
|
||||||
|
void RuntimeShaderBridge::ThreadMain()
|
||||||
|
{
|
||||||
|
while (!mStopping.load(std::memory_order_acquire))
|
||||||
|
{
|
||||||
|
RuntimeSlangShaderBuild build;
|
||||||
|
if (mCompiler.TryConsume(build))
|
||||||
|
{
|
||||||
|
if (build.succeeded)
|
||||||
|
{
|
||||||
|
build.artifact.layerId = mLayerId;
|
||||||
|
if (mOnArtifactReady)
|
||||||
|
mOnArtifactReady(build.artifact);
|
||||||
|
}
|
||||||
|
else if (mOnError)
|
||||||
|
{
|
||||||
|
mOnError(build.message);
|
||||||
|
}
|
||||||
|
mFinished.store(true, std::memory_order_release);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
std::this_thread::sleep_for(std::chrono::milliseconds(5));
|
||||||
|
}
|
||||||
|
mFinished.store(true, std::memory_order_release);
|
||||||
|
}
|
||||||
38
apps/RenderCadenceCompositor/runtime/RuntimeShaderBridge.h
Normal file
38
apps/RenderCadenceCompositor/runtime/RuntimeShaderBridge.h
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "RuntimeShaderArtifact.h"
|
||||||
|
#include "RuntimeSlangShaderCompiler.h"
|
||||||
|
|
||||||
|
#include <atomic>
|
||||||
|
#include <functional>
|
||||||
|
#include <string>
|
||||||
|
#include <thread>
|
||||||
|
|
||||||
|
class RuntimeShaderBridge
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
using ArtifactCallback = std::function<void(const RuntimeShaderArtifact&)>;
|
||||||
|
using ErrorCallback = std::function<void(const std::string&)>;
|
||||||
|
|
||||||
|
RuntimeShaderBridge() = default;
|
||||||
|
RuntimeShaderBridge(const RuntimeShaderBridge&) = delete;
|
||||||
|
RuntimeShaderBridge& operator=(const RuntimeShaderBridge&) = delete;
|
||||||
|
~RuntimeShaderBridge();
|
||||||
|
|
||||||
|
void Start(const std::string& shaderId, ArtifactCallback onArtifactReady, ErrorCallback onError);
|
||||||
|
void Start(const std::string& layerId, const std::string& shaderId, ArtifactCallback onArtifactReady, ErrorCallback onError);
|
||||||
|
void RequestStop();
|
||||||
|
void Stop();
|
||||||
|
bool CanStopWithoutWaiting() const;
|
||||||
|
|
||||||
|
private:
|
||||||
|
void ThreadMain();
|
||||||
|
|
||||||
|
RuntimeSlangShaderCompiler mCompiler;
|
||||||
|
std::thread mThread;
|
||||||
|
std::atomic<bool> mStopping{ false };
|
||||||
|
std::atomic<bool> mFinished{ true };
|
||||||
|
std::string mLayerId;
|
||||||
|
ArtifactCallback mOnArtifactReady;
|
||||||
|
ErrorCallback mOnError;
|
||||||
|
};
|
||||||
@@ -0,0 +1,159 @@
|
|||||||
|
#include "RuntimeSlangShaderCompiler.h"
|
||||||
|
|
||||||
|
#include "ShaderCompiler.h"
|
||||||
|
#include "ShaderPackageRegistry.h"
|
||||||
|
#include "ShaderTypes.h"
|
||||||
|
#include "SupportedShaderCatalog.h"
|
||||||
|
|
||||||
|
#include <chrono>
|
||||||
|
#include <filesystem>
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
std::filesystem::path FindRepoRoot()
|
||||||
|
{
|
||||||
|
std::filesystem::path current = std::filesystem::current_path();
|
||||||
|
for (;;)
|
||||||
|
{
|
||||||
|
if (std::filesystem::exists(current / "shaders" / "happy-accident" / "shader.slang") &&
|
||||||
|
std::filesystem::exists(current / "runtime" / "templates" / "shader_wrapper.slang.in"))
|
||||||
|
{
|
||||||
|
return current;
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::filesystem::path parent = current.parent_path();
|
||||||
|
if (parent.empty() || parent == current)
|
||||||
|
return std::filesystem::current_path();
|
||||||
|
current = parent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
RuntimeSlangShaderCompiler::~RuntimeSlangShaderCompiler()
|
||||||
|
{
|
||||||
|
Stop();
|
||||||
|
}
|
||||||
|
|
||||||
|
void RuntimeSlangShaderCompiler::StartHappyAccidentBuild()
|
||||||
|
{
|
||||||
|
StartShaderBuild("happy-accident");
|
||||||
|
}
|
||||||
|
|
||||||
|
void RuntimeSlangShaderCompiler::StartShaderBuild(const std::string& shaderId)
|
||||||
|
{
|
||||||
|
if (mRunning.load(std::memory_order_acquire))
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (mThread.joinable())
|
||||||
|
mThread.join();
|
||||||
|
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(mMutex);
|
||||||
|
mReadyBuild = RuntimeSlangShaderBuild();
|
||||||
|
}
|
||||||
|
|
||||||
|
mRunning.store(true, std::memory_order_release);
|
||||||
|
mThread = std::thread([this, shaderId]() {
|
||||||
|
RuntimeSlangShaderBuild build = BuildShader(shaderId);
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(mMutex);
|
||||||
|
mReadyBuild = std::move(build);
|
||||||
|
mReadyBuild.available = true;
|
||||||
|
}
|
||||||
|
mRunning.store(false, std::memory_order_release);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void RuntimeSlangShaderCompiler::Stop()
|
||||||
|
{
|
||||||
|
if (mThread.joinable())
|
||||||
|
mThread.join();
|
||||||
|
mRunning.store(false, std::memory_order_release);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool RuntimeSlangShaderCompiler::TryConsume(RuntimeSlangShaderBuild& build)
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(mMutex);
|
||||||
|
if (!mReadyBuild.available)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
build = std::move(mReadyBuild);
|
||||||
|
mReadyBuild = RuntimeSlangShaderBuild();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
RuntimeSlangShaderBuild RuntimeSlangShaderCompiler::BuildShader(const std::string& shaderId) const
|
||||||
|
{
|
||||||
|
RuntimeSlangShaderBuild build;
|
||||||
|
build.artifact.shaderId = shaderId;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
const std::filesystem::path repoRoot = FindRepoRoot();
|
||||||
|
const std::filesystem::path shaderDir = repoRoot / "shaders" / shaderId;
|
||||||
|
const std::filesystem::path runtimeBuildDir = repoRoot / "runtime" / "generated" / "render-cadence-compositor";
|
||||||
|
|
||||||
|
ShaderPackageRegistry registry(0);
|
||||||
|
ShaderPackage shaderPackage;
|
||||||
|
std::string error;
|
||||||
|
if (!registry.ParseManifest(shaderDir / "shader.json", shaderPackage, error))
|
||||||
|
{
|
||||||
|
build.succeeded = false;
|
||||||
|
build.message = error.empty() ? "Shader manifest parse failed." : error;
|
||||||
|
return build;
|
||||||
|
}
|
||||||
|
const RenderCadenceCompositor::ShaderSupportResult support =
|
||||||
|
RenderCadenceCompositor::CheckStatelessSinglePassShaderSupport(shaderPackage);
|
||||||
|
if (!support.supported)
|
||||||
|
{
|
||||||
|
build.succeeded = false;
|
||||||
|
build.message = support.reason;
|
||||||
|
return build;
|
||||||
|
}
|
||||||
|
|
||||||
|
ShaderCompiler compiler(
|
||||||
|
repoRoot,
|
||||||
|
runtimeBuildDir / (shaderId + ".wrapper.slang"),
|
||||||
|
runtimeBuildDir / (shaderId + ".generated.glsl"),
|
||||||
|
runtimeBuildDir / (shaderId + ".patched.glsl"),
|
||||||
|
0);
|
||||||
|
|
||||||
|
const auto start = std::chrono::steady_clock::now();
|
||||||
|
for (const ShaderPassDefinition& pass : shaderPackage.passes)
|
||||||
|
{
|
||||||
|
std::string fragmentShaderSource;
|
||||||
|
if (!compiler.BuildPassFragmentShaderSource(shaderPackage, pass, fragmentShaderSource, error))
|
||||||
|
{
|
||||||
|
build.succeeded = false;
|
||||||
|
build.message = error.empty() ? "Slang compile failed." : error;
|
||||||
|
return build;
|
||||||
|
}
|
||||||
|
|
||||||
|
RuntimeShaderPassArtifact passArtifact;
|
||||||
|
passArtifact.passId = pass.id;
|
||||||
|
passArtifact.fragmentShaderSource = std::move(fragmentShaderSource);
|
||||||
|
passArtifact.inputNames = pass.inputNames;
|
||||||
|
passArtifact.outputName = pass.outputName;
|
||||||
|
build.artifact.passes.push_back(std::move(passArtifact));
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto end = std::chrono::steady_clock::now();
|
||||||
|
const double milliseconds = std::chrono::duration_cast<std::chrono::duration<double, std::milli>>(end - start).count();
|
||||||
|
build.succeeded = true;
|
||||||
|
build.artifact.shaderId = shaderPackage.id;
|
||||||
|
build.artifact.displayName = shaderPackage.displayName;
|
||||||
|
build.artifact.parameterDefinitions = shaderPackage.parameters;
|
||||||
|
if (!build.artifact.passes.empty())
|
||||||
|
build.artifact.fragmentShaderSource = build.artifact.passes.front().fragmentShaderSource;
|
||||||
|
build.artifact.message = shaderPackage.displayName + " Slang compile completed in " + std::to_string(milliseconds) + " ms.";
|
||||||
|
build.message = build.artifact.message;
|
||||||
|
return build;
|
||||||
|
}
|
||||||
|
catch (const std::exception& exception)
|
||||||
|
{
|
||||||
|
build.succeeded = false;
|
||||||
|
build.message = exception.what();
|
||||||
|
return build;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "RuntimeShaderArtifact.h"
|
||||||
|
|
||||||
|
#include <atomic>
|
||||||
|
#include <mutex>
|
||||||
|
#include <string>
|
||||||
|
#include <thread>
|
||||||
|
|
||||||
|
struct RuntimeSlangShaderBuild
|
||||||
|
{
|
||||||
|
bool available = false;
|
||||||
|
bool succeeded = false;
|
||||||
|
RuntimeShaderArtifact artifact;
|
||||||
|
std::string message;
|
||||||
|
};
|
||||||
|
|
||||||
|
class RuntimeSlangShaderCompiler
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
RuntimeSlangShaderCompiler() = default;
|
||||||
|
RuntimeSlangShaderCompiler(const RuntimeSlangShaderCompiler&) = delete;
|
||||||
|
RuntimeSlangShaderCompiler& operator=(const RuntimeSlangShaderCompiler&) = delete;
|
||||||
|
~RuntimeSlangShaderCompiler();
|
||||||
|
|
||||||
|
void StartHappyAccidentBuild();
|
||||||
|
void StartShaderBuild(const std::string& shaderId);
|
||||||
|
void Stop();
|
||||||
|
bool TryConsume(RuntimeSlangShaderBuild& build);
|
||||||
|
bool Running() const { return mRunning.load(std::memory_order_acquire); }
|
||||||
|
|
||||||
|
private:
|
||||||
|
RuntimeSlangShaderBuild BuildShader(const std::string& shaderId) const;
|
||||||
|
|
||||||
|
std::thread mThread;
|
||||||
|
std::atomic<bool> mRunning{ false };
|
||||||
|
std::mutex mMutex;
|
||||||
|
RuntimeSlangShaderBuild mReadyBuild;
|
||||||
|
};
|
||||||
112
apps/RenderCadenceCompositor/runtime/SupportedShaderCatalog.cpp
Normal file
112
apps/RenderCadenceCompositor/runtime/SupportedShaderCatalog.cpp
Normal file
@@ -0,0 +1,112 @@
|
|||||||
|
#include "SupportedShaderCatalog.h"
|
||||||
|
|
||||||
|
#include "ShaderPackageRegistry.h"
|
||||||
|
|
||||||
|
#include <map>
|
||||||
|
#include <utility>
|
||||||
|
|
||||||
|
namespace RenderCadenceCompositor
|
||||||
|
{
|
||||||
|
ShaderSupportResult CheckStatelessSinglePassShaderSupport(const ShaderPackage& shaderPackage)
|
||||||
|
{
|
||||||
|
if (shaderPackage.passes.empty())
|
||||||
|
return { false, "Shader package has no render passes." };
|
||||||
|
|
||||||
|
if (shaderPackage.temporal.enabled)
|
||||||
|
return { false, "RenderCadenceCompositor currently supports only stateless shaders; temporal history is not enabled in this app." };
|
||||||
|
|
||||||
|
if (shaderPackage.feedback.enabled)
|
||||||
|
return { false, "RenderCadenceCompositor currently supports only stateless shaders; feedback storage is not enabled in this app." };
|
||||||
|
|
||||||
|
if (!shaderPackage.textureAssets.empty())
|
||||||
|
return { false, "RenderCadenceCompositor does not load shader texture assets yet; texture-backed shaders need a CPU-prepared asset handoff first." };
|
||||||
|
|
||||||
|
if (!shaderPackage.fontAssets.empty())
|
||||||
|
return { false, "RenderCadenceCompositor does not load shader font assets yet; text shaders need a CPU-prepared asset handoff first." };
|
||||||
|
|
||||||
|
for (const ShaderParameterDefinition& parameter : shaderPackage.parameters)
|
||||||
|
{
|
||||||
|
if (parameter.type == ShaderParameterType::Text)
|
||||||
|
return { false, "RenderCadenceCompositor currently skips text parameters because they require per-shader text texture storage." };
|
||||||
|
}
|
||||||
|
|
||||||
|
bool writesLayerOutput = false;
|
||||||
|
for (const ShaderPassDefinition& pass : shaderPackage.passes)
|
||||||
|
{
|
||||||
|
if (pass.sourcePath.empty())
|
||||||
|
{
|
||||||
|
return { false, "Shader pass '" + pass.id + "' has no source." };
|
||||||
|
}
|
||||||
|
if (pass.outputName == "layerOutput")
|
||||||
|
writesLayerOutput = true;
|
||||||
|
for (const std::string& inputName : pass.inputNames)
|
||||||
|
{
|
||||||
|
if (inputName == "videoInput" || inputName == "layerInput")
|
||||||
|
continue;
|
||||||
|
bool matchesNamedOutput = false;
|
||||||
|
for (const ShaderPassDefinition& outputPass : shaderPackage.passes)
|
||||||
|
{
|
||||||
|
if (outputPass.outputName == inputName)
|
||||||
|
{
|
||||||
|
matchesNamedOutput = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!matchesNamedOutput)
|
||||||
|
return { false, "Shader pass '" + pass.id + "' references unknown input '" + inputName + "'." };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!writesLayerOutput)
|
||||||
|
return { false, "Shader package must write a pass output named 'layerOutput'." };
|
||||||
|
|
||||||
|
return { true, std::string() };
|
||||||
|
}
|
||||||
|
|
||||||
|
bool SupportedShaderCatalog::Load(const std::filesystem::path& shaderRoot, unsigned maxTemporalHistoryFrames, std::string& error)
|
||||||
|
{
|
||||||
|
mShaders.clear();
|
||||||
|
mPackagesById.clear();
|
||||||
|
|
||||||
|
if (shaderRoot.empty())
|
||||||
|
{
|
||||||
|
error = "Shader library path is empty.";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
ShaderPackageRegistry registry(maxTemporalHistoryFrames);
|
||||||
|
std::map<std::string, ShaderPackage> packagesById;
|
||||||
|
std::vector<std::string> packageOrder;
|
||||||
|
std::vector<ShaderPackageStatus> packageStatuses;
|
||||||
|
if (!registry.Scan(shaderRoot, packagesById, packageOrder, packageStatuses, error))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
for (const std::string& packageId : packageOrder)
|
||||||
|
{
|
||||||
|
const auto packageIt = packagesById.find(packageId);
|
||||||
|
if (packageIt == packagesById.end())
|
||||||
|
continue;
|
||||||
|
|
||||||
|
const ShaderPackage& shaderPackage = packageIt->second;
|
||||||
|
const ShaderSupportResult support = CheckStatelessSinglePassShaderSupport(shaderPackage);
|
||||||
|
if (!support.supported)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
SupportedShaderSummary summary;
|
||||||
|
summary.id = shaderPackage.id;
|
||||||
|
summary.name = shaderPackage.displayName.empty() ? shaderPackage.id : shaderPackage.displayName;
|
||||||
|
summary.description = shaderPackage.description;
|
||||||
|
summary.category = shaderPackage.category;
|
||||||
|
mShaders.push_back(std::move(summary));
|
||||||
|
mPackagesById[shaderPackage.id] = shaderPackage;
|
||||||
|
}
|
||||||
|
|
||||||
|
error.clear();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ShaderPackage* SupportedShaderCatalog::FindPackage(const std::string& shaderId) const
|
||||||
|
{
|
||||||
|
const auto packageIt = mPackagesById.find(shaderId);
|
||||||
|
return packageIt == mPackagesById.end() ? nullptr : &packageIt->second;
|
||||||
|
}
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user