Compare commits
33 Commits
old-app
...
a39be6fb20
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 |
@@ -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
|
||||||
|
|||||||
17
.vscode/launch.json
vendored
17
.vscode/launch.json
vendored
@@ -83,6 +83,23 @@
|
|||||||
"moduleLoad": true
|
"moduleLoad": true
|
||||||
},
|
},
|
||||||
"preLaunchTask": "Build DeckLinkRenderCadenceProbe Debug x64"
|
"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"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
16
.vscode/tasks.json
vendored
16
.vscode/tasks.json
vendored
@@ -52,6 +52,22 @@
|
|||||||
"group": "build",
|
"group": "build",
|
||||||
"problemMatcher": "$msCompile"
|
"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",
|
||||||
|
|||||||
385
CMakeLists.txt
385
CMakeLists.txt
@@ -273,6 +273,144 @@ if(MSVC)
|
|||||||
target_compile_options(DeckLinkRenderCadenceProbe PRIVATE /W3)
|
target_compile_options(DeckLinkRenderCadenceProbe PRIVATE /W3)
|
||||||
endif()
|
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/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/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/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"
|
||||||
@@ -642,6 +780,253 @@ endif()
|
|||||||
|
|
||||||
add_test(NAME RenderCadenceControllerTests COMMAND RenderCadenceControllerTests)
|
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/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
|
add_executable(SystemOutputFramePoolTests
|
||||||
"${APP_DIR}/videoio/SystemOutputFramePool.cpp"
|
"${APP_DIR}/videoio/SystemOutputFramePool.cpp"
|
||||||
"${APP_DIR}/videoio/VideoIOFormat.cpp"
|
"${APP_DIR}/videoio/VideoIOFormat.cpp"
|
||||||
|
|||||||
409
apps/RenderCadenceCompositor/README.md
Normal file
409
apps/RenderCadenceCompositor/README.md
Normal file
@@ -0,0 +1,409 @@
|
|||||||
|
# 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 latest input frames 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 latest disposable CPU input slots
|
||||||
|
drops older unsampled input frames when newer frames arrive
|
||||||
|
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
|
||||||
|
drops old completed unscheduled frames when render needs space
|
||||||
|
protects scheduled frames until DeckLink completion
|
||||||
|
|
||||||
|
DeckLinkOutputThread
|
||||||
|
consumes completed system-memory frames
|
||||||
|
schedules them into DeckLink up to target depth
|
||||||
|
never renders
|
||||||
|
```
|
||||||
|
|
||||||
|
Startup warms up real rendered frames before DeckLink scheduled playback starts.
|
||||||
|
|
||||||
|
## 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 latest-frame input mailbox
|
||||||
|
- fast contiguous mailbox copy path for matching input row strides
|
||||||
|
- render-thread-owned input texture upload
|
||||||
|
- async PBO readback
|
||||||
|
- latest-N system-memory frame exchange
|
||||||
|
- rendered-frame warmup
|
||||||
|
- 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] Latest-frame CPU input mailbox
|
||||||
|
- [x] Fast contiguous input mailbox copy when source/destination stride matches
|
||||||
|
- [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. warm up rendered system-memory frames
|
||||||
|
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. 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 mailbox 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.
|
||||||
|
|
||||||
|
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
|
||||||
|
- warning when schedule failures increase
|
||||||
|
- error when the app/DeckLink output buffer is starved
|
||||||
|
|
||||||
|
Input telemetry:
|
||||||
|
|
||||||
|
- `inputFramesReceived`: frames accepted into `InputFrameMailbox`
|
||||||
|
- `inputFramesDropped`: ready input frames dropped or missed because the mailbox was full
|
||||||
|
- `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 real/latest 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
|
||||||
|
- `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
|
||||||
|
- 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 latest-frame CPU input handoff with contiguous-copy fast path for matching row strides
|
||||||
|
- `render/InputFrameTexture`: render-thread-owned upload of the latest 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
|
||||||
169
apps/RenderCadenceCompositor/RenderCadenceCompositor.cpp
Normal file
169
apps/RenderCadenceCompositor/RenderCadenceCompositor.cpp
Normal file
@@ -0,0 +1,169 @@
|
|||||||
|
#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 <iostream>
|
||||||
|
#include <sstream>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
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(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 = 12;
|
||||||
|
|
||||||
|
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;
|
||||||
|
InputFrameMailbox inputMailbox(inputMailboxConfig);
|
||||||
|
|
||||||
|
VideoFormat inputVideoMode;
|
||||||
|
std::string inputVideoModeError;
|
||||||
|
const bool inputVideoModeResolved = ResolveConfiguredVideoFormat(appConfig.inputVideoFormat, appConfig.inputFrameRate, inputVideoMode);
|
||||||
|
if (!inputVideoModeResolved)
|
||||||
|
{
|
||||||
|
inputVideoModeError = "Unsupported DeckLink inputVideoFormat/inputFrameRate in config/runtime-host.json: " +
|
||||||
|
appConfig.inputVideoFormat + " / " + appConfig.inputFrameRate;
|
||||||
|
RenderCadenceCompositor::LogWarning("app", inputVideoModeError);
|
||||||
|
}
|
||||||
|
|
||||||
|
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 + ".");
|
||||||
|
}
|
||||||
|
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;
|
||||||
|
renderConfig.frameDurationMilliseconds = RenderCadenceCompositor::FrameDurationMillisecondsFromRateString(appConfig.outputFrameRate);
|
||||||
|
renderConfig.pboDepth = 6;
|
||||||
|
|
||||||
|
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();
|
||||||
|
}
|
||||||
237
apps/RenderCadenceCompositor/app/AppConfigProvider.cpp
Normal file
237
apps/RenderCadenceCompositor/app/AppConfigProvider.cpp
Normal file
@@ -0,0 +1,237 @@
|
|||||||
|
#include "AppConfigProvider.h"
|
||||||
|
|
||||||
|
#include "RuntimeJson.h"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cctype>
|
||||||
|
#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;
|
||||||
|
}
|
||||||
|
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
33
apps/RenderCadenceCompositor/app/AppConfigProvider.h
Normal file
33
apps/RenderCadenceCompositor/app/AppConfigProvider.h
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "AppConfig.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);
|
||||||
|
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);
|
||||||
|
}
|
||||||
274
apps/RenderCadenceCompositor/app/RenderCadenceApp.h
Normal file
274
apps/RenderCadenceCompositor/app/RenderCadenceApp.h
Normal file
@@ -0,0 +1,274 @@
|
|||||||
|
#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);
|
||||||
|
|
||||||
|
Log("app", "Waiting for rendered warmup frames.");
|
||||||
|
if (!mFrameExchange.WaitForCompletedDepth(mConfig.warmupCompletedFrames, mConfig.warmupTimeout))
|
||||||
|
{
|
||||||
|
error = "Timed out waiting for rendered warmup frames.";
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
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.";
|
||||||
|
};
|
||||||
|
}
|
||||||
361
apps/RenderCadenceCompositor/app/RuntimeLayerController.cpp
Normal file
361
apps/RenderCadenceCompositor/app/RuntimeLayerController.cpp
Normal file
@@ -0,0 +1,361 @@
|
|||||||
|
#include "RuntimeLayerController.h"
|
||||||
|
|
||||||
|
#include "AppConfigProvider.h"
|
||||||
|
#include "RuntimeJson.h"
|
||||||
|
#include "../logging/Logger.h"
|
||||||
|
|
||||||
|
#include <filesystem>
|
||||||
|
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
|
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." };
|
||||||
|
}
|
||||||
|
|
||||||
|
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::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();
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string RuntimeLayerController::FirstRuntimeLayerId() const
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(mRuntimeLayerMutex);
|
||||||
|
return mRuntimeLayerModel.FirstLayerId();
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
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;
|
||||||
|
};
|
||||||
|
}
|
||||||
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.KeyNull("renderMs");
|
||||||
|
writer.KeyNull("smoothedRenderMs");
|
||||||
|
writer.KeyNull("budgetUsedPercent");
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
486
apps/RenderCadenceCompositor/control/http/HttpControlServer.cpp
Normal file
486
apps/RenderCadenceCompositor/control/http/HttpControlServer.cpp
Normal file
@@ -0,0 +1,486 @@
|
|||||||
|
#include "HttpControlServer.h"
|
||||||
|
|
||||||
|
#include "../json/JsonWriter.h"
|
||||||
|
#include "../logging/Logger.h"
|
||||||
|
|
||||||
|
#include <ws2tcpip.h>
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cctype>
|
||||||
|
#include <fstream>
|
||||||
|
#include <sstream>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
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";
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
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");
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
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,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());
|
||||||
|
}
|
||||||
|
}
|
||||||
252
apps/RenderCadenceCompositor/frames/InputFrameMailbox.cpp
Normal file
252
apps/RenderCadenceCompositor/frames/InputFrameMailbox.cpp
Normal file
@@ -0,0 +1,252 @@
|
|||||||
|
#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);
|
||||||
|
++mCounters.submittedFrames;
|
||||||
|
mCounters.latestFrameIndex = frameIndex;
|
||||||
|
mCounters.hasSubmittedFrame = true;
|
||||||
|
mLatestSubmitTime = std::chrono::steady_clock::now();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool InputFrameMailbox::TryAcquireLatest(InputFrame& frame)
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(mMutex);
|
||||||
|
while (!mReadyIndices.empty())
|
||||||
|
{
|
||||||
|
const std::size_t index = mReadyIndices.back();
|
||||||
|
mReadyIndices.pop_back();
|
||||||
|
if (index >= mSlots.size() || mSlots[index].state != InputFrameSlotState::Ready)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
while (!mReadyIndices.empty())
|
||||||
|
{
|
||||||
|
const std::size_t olderIndex = mReadyIndices.front();
|
||||||
|
mReadyIndices.pop_front();
|
||||||
|
if (olderIndex >= mSlots.size() || mSlots[olderIndex].state != InputFrameSlotState::Ready)
|
||||||
|
continue;
|
||||||
|
mSlots[olderIndex].state = InputFrameSlotState::Free;
|
||||||
|
++mSlots[olderIndex].generation;
|
||||||
|
++mCounters.droppedReadyFrames;
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::size_t InputFrameMailbox::FrameByteCount() const
|
||||||
|
{
|
||||||
|
return static_cast<std::size_t>(mConfig.rowBytes) * static_cast<std::size_t>(mConfig.height);
|
||||||
|
}
|
||||||
91
apps/RenderCadenceCompositor/frames/InputFrameMailbox.h
Normal file
91
apps/RenderCadenceCompositor/frames/InputFrameMailbox.h
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
#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;
|
||||||
|
};
|
||||||
|
|
||||||
|
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 TryAcquireLatest(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();
|
||||||
|
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;
|
||||||
|
};
|
||||||
245
apps/RenderCadenceCompositor/frames/SystemFrameExchange.cpp
Normal file
245
apps/RenderCadenceCompositor/frames/SystemFrameExchange.cpp
Normal file
@@ -0,0 +1,245 @@
|
|||||||
|
#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))
|
||||||
|
{
|
||||||
|
if (!DropOldestCompletedLocked() || !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);
|
||||||
|
++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;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
51
apps/RenderCadenceCompositor/frames/SystemFrameExchange.h
Normal file
51
apps/RenderCadenceCompositor/frames/SystemFrameExchange.h
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
#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);
|
||||||
|
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();
|
||||||
|
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;
|
||||||
|
};
|
||||||
51
apps/RenderCadenceCompositor/frames/SystemFrameTypes.h
Normal file
51
apps/RenderCadenceCompositor/frames/SystemFrameTypes.h
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
#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;
|
||||||
|
};
|
||||||
|
|
||||||
|
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->TryAcquireLatest(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;
|
||||||
|
};
|
||||||
45
apps/RenderCadenceCompositor/render/RenderCadenceClock.cpp
Normal file
45
apps/RenderCadenceCompositor/render/RenderCadenceClock.cpp
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
#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;
|
||||||
|
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;
|
||||||
|
const Duration lateBy = now - mNextRenderTime;
|
||||||
|
if (lateBy > mFrameDuration)
|
||||||
|
{
|
||||||
|
tick.skippedFrames = static_cast<uint64_t>(lateBy / mFrameDuration);
|
||||||
|
++mOverrunCount;
|
||||||
|
mSkippedFrameCount += tick.skippedFrames;
|
||||||
|
}
|
||||||
|
return tick;
|
||||||
|
}
|
||||||
|
|
||||||
|
void RenderCadenceClock::MarkRendered(TimePoint now)
|
||||||
|
{
|
||||||
|
mNextRenderTime += mFrameDuration;
|
||||||
|
if (now - mNextRenderTime > mFrameDuration * 4)
|
||||||
|
mNextRenderTime = now + mFrameDuration;
|
||||||
|
}
|
||||||
36
apps/RenderCadenceCompositor/render/RenderCadenceClock.h
Normal file
36
apps/RenderCadenceCompositor/render/RenderCadenceClock.h
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
#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 mOverrunCount = 0;
|
||||||
|
uint64_t mSkippedFrameCount = 0;
|
||||||
|
};
|
||||||
347
apps/RenderCadenceCompositor/render/RenderThread.cpp
Normal file
347
apps/RenderCadenceCompositor/render/RenderThread.cpp
Normal file
@@ -0,0 +1,347 @@
|
|||||||
|
#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.inputFramesReceived = mInputFramesReceived.load(std::memory_order_relaxed);
|
||||||
|
metrics.inputFramesDropped = mInputFramesDropped.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(); });
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
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(); });
|
||||||
|
}
|
||||||
|
|
||||||
|
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::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);
|
||||||
|
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);
|
||||||
|
mInputLatestAgeMilliseconds.store(0.0, std::memory_order_relaxed);
|
||||||
|
mInputSignalPresent.store(false, 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);
|
||||||
|
}
|
||||||
109
apps/RenderCadenceCompositor/render/RenderThread.h
Normal file
109
apps/RenderCadenceCompositor/render/RenderThread.h
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
#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 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;
|
||||||
|
uint64_t inputFramesReceived = 0;
|
||||||
|
uint64_t inputFramesDropped = 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 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<uint64_t> mInputFramesReceived{ 0 };
|
||||||
|
std::atomic<uint64_t> mInputFramesDropped{ 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,142 @@
|
|||||||
|
#include "Bgra8ReadbackPipeline.h"
|
||||||
|
|
||||||
|
#include "../frames/SystemFrameTypes.h"
|
||||||
|
|
||||||
|
#include <cstring>
|
||||||
|
|
||||||
|
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);
|
||||||
|
renderFrame(frameIndex);
|
||||||
|
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||||
|
|
||||||
|
return mPboRing.QueueReadback(mFramebuffer, mWidth, mHeight, frameIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
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))
|
||||||
|
{
|
||||||
|
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);
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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,52 @@
|
|||||||
|
#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(); }
|
||||||
|
|
||||||
|
private:
|
||||||
|
bool CreateRenderTarget();
|
||||||
|
void DestroyRenderTarget();
|
||||||
|
|
||||||
|
unsigned mWidth = 0;
|
||||||
|
unsigned mHeight = 0;
|
||||||
|
unsigned mRowBytes = 0;
|
||||||
|
GLuint mFramebuffer = 0;
|
||||||
|
GLuint mTexture = 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,482 @@
|
|||||||
|
#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;
|
||||||
|
}
|
||||||
|
|
||||||
|
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 latest 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
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,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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "ShaderTypes.h"
|
||||||
|
|
||||||
|
#include <filesystem>
|
||||||
|
#include <map>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
namespace RenderCadenceCompositor
|
||||||
|
{
|
||||||
|
struct SupportedShaderSummary
|
||||||
|
{
|
||||||
|
std::string id;
|
||||||
|
std::string name;
|
||||||
|
std::string description;
|
||||||
|
std::string category;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct ShaderSupportResult
|
||||||
|
{
|
||||||
|
bool supported = false;
|
||||||
|
std::string reason;
|
||||||
|
};
|
||||||
|
|
||||||
|
ShaderSupportResult CheckStatelessSinglePassShaderSupport(const ShaderPackage& shaderPackage);
|
||||||
|
|
||||||
|
class SupportedShaderCatalog
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
bool Load(const std::filesystem::path& shaderRoot, unsigned maxTemporalHistoryFrames, std::string& error);
|
||||||
|
const std::vector<SupportedShaderSummary>& Shaders() const { return mShaders; }
|
||||||
|
const ShaderPackage* FindPackage(const std::string& shaderId) const;
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::vector<SupportedShaderSummary> mShaders;
|
||||||
|
std::map<std::string, ShaderPackage> mPackagesById;
|
||||||
|
};
|
||||||
|
}
|
||||||
145
apps/RenderCadenceCompositor/telemetry/CadenceTelemetry.h
Normal file
145
apps/RenderCadenceCompositor/telemetry/CadenceTelemetry.h
Normal file
@@ -0,0 +1,145 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <chrono>
|
||||||
|
#include <cstddef>
|
||||||
|
#include <cstdint>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
namespace RenderCadenceCompositor
|
||||||
|
{
|
||||||
|
struct CadenceTelemetrySnapshot
|
||||||
|
{
|
||||||
|
double sampleSeconds = 0.0;
|
||||||
|
double renderFps = 0.0;
|
||||||
|
double scheduleFps = 0.0;
|
||||||
|
std::size_t freeFrames = 0;
|
||||||
|
std::size_t completedFrames = 0;
|
||||||
|
std::size_t scheduledFrames = 0;
|
||||||
|
uint64_t renderedTotal = 0;
|
||||||
|
uint64_t scheduledTotal = 0;
|
||||||
|
uint64_t completedPollMisses = 0;
|
||||||
|
uint64_t scheduleFailures = 0;
|
||||||
|
uint64_t completions = 0;
|
||||||
|
uint64_t displayedLate = 0;
|
||||||
|
uint64_t dropped = 0;
|
||||||
|
uint64_t shaderBuildsCommitted = 0;
|
||||||
|
uint64_t shaderBuildFailures = 0;
|
||||||
|
uint64_t inputFramesReceived = 0;
|
||||||
|
uint64_t inputFramesDropped = 0;
|
||||||
|
double inputLatestAgeMilliseconds = 0.0;
|
||||||
|
double inputUploadMilliseconds = 0.0;
|
||||||
|
bool inputFormatSupported = true;
|
||||||
|
bool inputSignalPresent = false;
|
||||||
|
double inputCaptureFps = 0.0;
|
||||||
|
double inputConvertMilliseconds = 0.0;
|
||||||
|
double inputSubmitMilliseconds = 0.0;
|
||||||
|
uint64_t inputNoSignalFrames = 0;
|
||||||
|
uint64_t inputUnsupportedFrames = 0;
|
||||||
|
uint64_t inputSubmitMisses = 0;
|
||||||
|
std::string inputCaptureFormat = "none";
|
||||||
|
bool deckLinkBufferedAvailable = false;
|
||||||
|
uint64_t deckLinkBuffered = 0;
|
||||||
|
double deckLinkScheduleCallMilliseconds = 0.0;
|
||||||
|
};
|
||||||
|
|
||||||
|
class CadenceTelemetry
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
template <typename SystemFrameExchange, typename Output, typename OutputThread>
|
||||||
|
CadenceTelemetrySnapshot Sample(
|
||||||
|
const SystemFrameExchange& exchange,
|
||||||
|
const Output& output,
|
||||||
|
const OutputThread& outputThread)
|
||||||
|
{
|
||||||
|
const auto now = Clock::now();
|
||||||
|
const double seconds = mHasLastSample
|
||||||
|
? std::chrono::duration_cast<std::chrono::duration<double>>(now - mLastSampleTime).count()
|
||||||
|
: 0.0;
|
||||||
|
|
||||||
|
const auto exchangeMetrics = exchange.Metrics();
|
||||||
|
const auto outputMetrics = output.Metrics();
|
||||||
|
const auto threadMetrics = outputThread.Metrics();
|
||||||
|
|
||||||
|
CadenceTelemetrySnapshot snapshot;
|
||||||
|
snapshot.sampleSeconds = seconds;
|
||||||
|
snapshot.renderedTotal = exchangeMetrics.completedFrames;
|
||||||
|
snapshot.scheduledTotal = exchangeMetrics.scheduledFrames;
|
||||||
|
snapshot.freeFrames = exchangeMetrics.freeCount;
|
||||||
|
snapshot.completedFrames = exchangeMetrics.completedCount;
|
||||||
|
snapshot.scheduledFrames = exchangeMetrics.scheduledCount;
|
||||||
|
snapshot.completedPollMisses = threadMetrics.completedPollMisses;
|
||||||
|
snapshot.scheduleFailures = outputMetrics.scheduleFailures > threadMetrics.scheduleFailures
|
||||||
|
? outputMetrics.scheduleFailures
|
||||||
|
: threadMetrics.scheduleFailures;
|
||||||
|
snapshot.completions = outputMetrics.completions;
|
||||||
|
snapshot.displayedLate = outputMetrics.displayedLate;
|
||||||
|
snapshot.dropped = outputMetrics.dropped;
|
||||||
|
snapshot.deckLinkBufferedAvailable = outputMetrics.actualBufferedFramesAvailable;
|
||||||
|
snapshot.deckLinkBuffered = outputMetrics.actualBufferedFrames;
|
||||||
|
snapshot.deckLinkScheduleCallMilliseconds = outputMetrics.scheduleCallMilliseconds;
|
||||||
|
|
||||||
|
if (mHasLastSample && seconds > 0.0)
|
||||||
|
{
|
||||||
|
snapshot.renderFps = static_cast<double>(snapshot.renderedTotal - mLastRenderedFrames) / seconds;
|
||||||
|
snapshot.scheduleFps = static_cast<double>(snapshot.scheduledTotal - mLastScheduledFrames) / seconds;
|
||||||
|
}
|
||||||
|
|
||||||
|
mLastSampleTime = now;
|
||||||
|
mLastRenderedFrames = snapshot.renderedTotal;
|
||||||
|
mLastScheduledFrames = snapshot.scheduledTotal;
|
||||||
|
mHasLastSample = true;
|
||||||
|
return snapshot;
|
||||||
|
}
|
||||||
|
|
||||||
|
template <typename SystemFrameExchange, typename Output, typename OutputThread, typename RenderThread>
|
||||||
|
CadenceTelemetrySnapshot Sample(
|
||||||
|
const SystemFrameExchange& exchange,
|
||||||
|
const Output& output,
|
||||||
|
const OutputThread& outputThread,
|
||||||
|
const RenderThread& renderThread)
|
||||||
|
{
|
||||||
|
CadenceTelemetrySnapshot snapshot = Sample(exchange, output, outputThread);
|
||||||
|
const auto renderMetrics = renderThread.GetMetrics();
|
||||||
|
snapshot.shaderBuildsCommitted = renderMetrics.shaderBuildsCommitted;
|
||||||
|
snapshot.shaderBuildFailures = renderMetrics.shaderBuildFailures;
|
||||||
|
snapshot.inputFramesReceived = renderMetrics.inputFramesReceived;
|
||||||
|
snapshot.inputFramesDropped = renderMetrics.inputFramesDropped;
|
||||||
|
snapshot.inputLatestAgeMilliseconds = renderMetrics.inputLatestAgeMilliseconds;
|
||||||
|
snapshot.inputUploadMilliseconds = renderMetrics.inputUploadMilliseconds;
|
||||||
|
snapshot.inputFormatSupported = renderMetrics.inputFormatSupported;
|
||||||
|
snapshot.inputSignalPresent = renderMetrics.inputSignalPresent;
|
||||||
|
return snapshot;
|
||||||
|
}
|
||||||
|
|
||||||
|
template <typename SystemFrameExchange, typename Output, typename OutputThread, typename RenderThread, typename InputEdge>
|
||||||
|
CadenceTelemetrySnapshot Sample(
|
||||||
|
const SystemFrameExchange& exchange,
|
||||||
|
const Output& output,
|
||||||
|
const OutputThread& outputThread,
|
||||||
|
const RenderThread& renderThread,
|
||||||
|
const InputEdge& inputEdge)
|
||||||
|
{
|
||||||
|
CadenceTelemetrySnapshot snapshot = Sample(exchange, output, outputThread, renderThread);
|
||||||
|
const auto inputMetrics = inputEdge.Metrics();
|
||||||
|
snapshot.inputConvertMilliseconds = inputMetrics.convertMilliseconds;
|
||||||
|
snapshot.inputSubmitMilliseconds = inputMetrics.submitMilliseconds;
|
||||||
|
snapshot.inputNoSignalFrames = inputMetrics.noInputSourceFrames;
|
||||||
|
snapshot.inputUnsupportedFrames = inputMetrics.unsupportedFrames;
|
||||||
|
snapshot.inputSubmitMisses = inputMetrics.submitMisses;
|
||||||
|
snapshot.inputCaptureFormat = inputMetrics.captureFormat ? inputMetrics.captureFormat : "none";
|
||||||
|
if (snapshot.sampleSeconds > 0.0)
|
||||||
|
snapshot.inputCaptureFps = static_cast<double>(inputMetrics.capturedFrames - mLastInputCapturedFrames) / snapshot.sampleSeconds;
|
||||||
|
mLastInputCapturedFrames = inputMetrics.capturedFrames;
|
||||||
|
return snapshot;
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
using Clock = std::chrono::steady_clock;
|
||||||
|
|
||||||
|
Clock::time_point mLastSampleTime = Clock::now();
|
||||||
|
uint64_t mLastRenderedFrames = 0;
|
||||||
|
uint64_t mLastScheduledFrames = 0;
|
||||||
|
uint64_t mLastInputCapturedFrames = 0;
|
||||||
|
bool mHasLastSample = false;
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "CadenceTelemetry.h"
|
||||||
|
#include "../json/JsonWriter.h"
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
namespace RenderCadenceCompositor
|
||||||
|
{
|
||||||
|
inline void WriteCadenceTelemetryJson(JsonWriter& writer, const CadenceTelemetrySnapshot& snapshot)
|
||||||
|
{
|
||||||
|
writer.BeginObject();
|
||||||
|
writer.KeyDouble("sampleSeconds", snapshot.sampleSeconds);
|
||||||
|
writer.KeyDouble("renderFps", snapshot.renderFps);
|
||||||
|
writer.KeyDouble("scheduleFps", snapshot.scheduleFps);
|
||||||
|
writer.KeyUInt("free", static_cast<uint64_t>(snapshot.freeFrames));
|
||||||
|
writer.KeyUInt("completed", static_cast<uint64_t>(snapshot.completedFrames));
|
||||||
|
writer.KeyUInt("scheduled", static_cast<uint64_t>(snapshot.scheduledFrames));
|
||||||
|
writer.KeyUInt("renderedTotal", snapshot.renderedTotal);
|
||||||
|
writer.KeyUInt("scheduledTotal", snapshot.scheduledTotal);
|
||||||
|
writer.KeyUInt("completedPollMisses", snapshot.completedPollMisses);
|
||||||
|
writer.KeyUInt("scheduleFailures", snapshot.scheduleFailures);
|
||||||
|
writer.KeyUInt("completions", snapshot.completions);
|
||||||
|
writer.KeyUInt("late", snapshot.displayedLate);
|
||||||
|
writer.KeyUInt("dropped", snapshot.dropped);
|
||||||
|
writer.KeyUInt("shaderCommitted", snapshot.shaderBuildsCommitted);
|
||||||
|
writer.KeyUInt("shaderFailures", snapshot.shaderBuildFailures);
|
||||||
|
writer.KeyUInt("inputFramesReceived", snapshot.inputFramesReceived);
|
||||||
|
writer.KeyUInt("inputFramesDropped", snapshot.inputFramesDropped);
|
||||||
|
writer.KeyDouble("inputLatestAgeMs", snapshot.inputLatestAgeMilliseconds);
|
||||||
|
writer.KeyDouble("inputUploadMs", snapshot.inputUploadMilliseconds);
|
||||||
|
writer.KeyBool("inputFormatSupported", snapshot.inputFormatSupported);
|
||||||
|
writer.KeyBool("inputSignalPresent", snapshot.inputSignalPresent);
|
||||||
|
writer.KeyDouble("inputCaptureFps", snapshot.inputCaptureFps);
|
||||||
|
writer.KeyDouble("inputConvertMs", snapshot.inputConvertMilliseconds);
|
||||||
|
writer.KeyDouble("inputSubmitMs", snapshot.inputSubmitMilliseconds);
|
||||||
|
writer.KeyUInt("inputNoSignalFrames", snapshot.inputNoSignalFrames);
|
||||||
|
writer.KeyUInt("inputUnsupportedFrames", snapshot.inputUnsupportedFrames);
|
||||||
|
writer.KeyUInt("inputSubmitMisses", snapshot.inputSubmitMisses);
|
||||||
|
writer.KeyString("inputCaptureFormat", snapshot.inputCaptureFormat);
|
||||||
|
writer.KeyBool("deckLinkBufferedAvailable", snapshot.deckLinkBufferedAvailable);
|
||||||
|
writer.Key("deckLinkBuffered");
|
||||||
|
if (snapshot.deckLinkBufferedAvailable)
|
||||||
|
writer.UInt(snapshot.deckLinkBuffered);
|
||||||
|
else
|
||||||
|
writer.Null();
|
||||||
|
writer.KeyDouble("scheduleCallMs", snapshot.deckLinkScheduleCallMilliseconds);
|
||||||
|
writer.EndObject();
|
||||||
|
}
|
||||||
|
|
||||||
|
inline std::string CadenceTelemetryToJson(const CadenceTelemetrySnapshot& snapshot)
|
||||||
|
{
|
||||||
|
JsonWriter writer;
|
||||||
|
WriteCadenceTelemetryJson(writer, snapshot);
|
||||||
|
return writer.StringValue();
|
||||||
|
}
|
||||||
|
}
|
||||||
116
apps/RenderCadenceCompositor/telemetry/TelemetryHealthMonitor.h
Normal file
116
apps/RenderCadenceCompositor/telemetry/TelemetryHealthMonitor.h
Normal file
@@ -0,0 +1,116 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "CadenceTelemetry.h"
|
||||||
|
#include "../logging/Logger.h"
|
||||||
|
|
||||||
|
#include <atomic>
|
||||||
|
#include <chrono>
|
||||||
|
#include <sstream>
|
||||||
|
#include <thread>
|
||||||
|
|
||||||
|
namespace RenderCadenceCompositor
|
||||||
|
{
|
||||||
|
struct TelemetryHealthMonitorConfig
|
||||||
|
{
|
||||||
|
std::chrono::milliseconds interval = std::chrono::seconds(1);
|
||||||
|
std::size_t scheduledStarvationThreshold = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
class TelemetryHealthMonitor
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
explicit TelemetryHealthMonitor(TelemetryHealthMonitorConfig config = TelemetryHealthMonitorConfig()) :
|
||||||
|
mConfig(config)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
TelemetryHealthMonitor(const TelemetryHealthMonitor&) = delete;
|
||||||
|
TelemetryHealthMonitor& operator=(const TelemetryHealthMonitor&) = delete;
|
||||||
|
|
||||||
|
~TelemetryHealthMonitor()
|
||||||
|
{
|
||||||
|
Stop();
|
||||||
|
}
|
||||||
|
|
||||||
|
template <typename SystemFrameExchange, typename Output, typename OutputThread, typename RenderThread>
|
||||||
|
void Start(const SystemFrameExchange& exchange, const Output& output, const OutputThread& outputThread, const RenderThread& renderThread)
|
||||||
|
{
|
||||||
|
if (mRunning)
|
||||||
|
return;
|
||||||
|
mStopping = false;
|
||||||
|
mThread = std::thread([this, &exchange, &output, &outputThread, &renderThread]() {
|
||||||
|
CadenceTelemetry telemetry;
|
||||||
|
CadenceTelemetrySnapshot previous;
|
||||||
|
bool hasPrevious = false;
|
||||||
|
while (!mStopping)
|
||||||
|
{
|
||||||
|
std::this_thread::sleep_for(mConfig.interval);
|
||||||
|
const CadenceTelemetrySnapshot snapshot = telemetry.Sample(exchange, output, outputThread, renderThread);
|
||||||
|
ReportHealth(snapshot, hasPrevious ? &previous : nullptr);
|
||||||
|
previous = snapshot;
|
||||||
|
hasPrevious = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
mRunning = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Stop()
|
||||||
|
{
|
||||||
|
mStopping = true;
|
||||||
|
if (mThread.joinable())
|
||||||
|
mThread.join();
|
||||||
|
mRunning = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
void ReportHealth(const CadenceTelemetrySnapshot& snapshot, const CadenceTelemetrySnapshot* previous) const
|
||||||
|
{
|
||||||
|
if (!previous)
|
||||||
|
return;
|
||||||
|
|
||||||
|
const uint64_t lateDelta = snapshot.displayedLate - previous->displayedLate;
|
||||||
|
const uint64_t droppedDelta = snapshot.dropped - previous->dropped;
|
||||||
|
const uint64_t scheduleFailureDelta = snapshot.scheduleFailures - previous->scheduleFailures;
|
||||||
|
|
||||||
|
if (droppedDelta > 0 || lateDelta > 0)
|
||||||
|
{
|
||||||
|
std::ostringstream message;
|
||||||
|
message << "DeckLink reported frame timing issue: lateDelta=" << lateDelta
|
||||||
|
<< " droppedDelta=" << droppedDelta
|
||||||
|
<< " totalLate=" << snapshot.displayedLate
|
||||||
|
<< " totalDropped=" << snapshot.dropped;
|
||||||
|
LogWarning("telemetry", message.str());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (scheduleFailureDelta > 0)
|
||||||
|
{
|
||||||
|
std::ostringstream message;
|
||||||
|
message << "DeckLink schedule failures increased: delta=" << scheduleFailureDelta
|
||||||
|
<< " total=" << snapshot.scheduleFailures;
|
||||||
|
LogWarning("telemetry", message.str());
|
||||||
|
}
|
||||||
|
|
||||||
|
const bool appScheduledStarved = snapshot.scheduledFrames <= mConfig.scheduledStarvationThreshold
|
||||||
|
&& snapshot.scheduledTotal > 0;
|
||||||
|
const bool deckLinkStarved = snapshot.deckLinkBufferedAvailable && snapshot.deckLinkBuffered == 0;
|
||||||
|
if (appScheduledStarved || deckLinkStarved)
|
||||||
|
{
|
||||||
|
std::ostringstream message;
|
||||||
|
message << "Output buffer starvation detected: scheduled=" << snapshot.scheduledFrames
|
||||||
|
<< " decklinkBuffered=";
|
||||||
|
if (snapshot.deckLinkBufferedAvailable)
|
||||||
|
message << snapshot.deckLinkBuffered;
|
||||||
|
else
|
||||||
|
message << "n/a";
|
||||||
|
message << " renderFps=" << snapshot.renderFps
|
||||||
|
<< " scheduleFps=" << snapshot.scheduleFps;
|
||||||
|
LogError("telemetry", message.str());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TelemetryHealthMonitorConfig mConfig;
|
||||||
|
std::thread mThread;
|
||||||
|
std::atomic<bool> mStopping{ false };
|
||||||
|
std::atomic<bool> mRunning{ false };
|
||||||
|
};
|
||||||
|
}
|
||||||
367
apps/RenderCadenceCompositor/video/DeckLinkInput.cpp
Normal file
367
apps/RenderCadenceCompositor/video/DeckLinkInput.cpp
Normal file
@@ -0,0 +1,367 @@
|
|||||||
|
#include "DeckLinkInput.h"
|
||||||
|
|
||||||
|
#include "DeckLinkVideoIOFormat.h"
|
||||||
|
#include "../logging/Logger.h"
|
||||||
|
|
||||||
|
#include <chrono>
|
||||||
|
#include <new>
|
||||||
|
|
||||||
|
namespace RenderCadenceCompositor
|
||||||
|
{
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
bool FindInputDisplayMode(IDeckLinkInput* input, BMDDisplayMode targetMode, IDeckLinkDisplayMode** foundMode)
|
||||||
|
{
|
||||||
|
if (input == nullptr || foundMode == nullptr)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
*foundMode = nullptr;
|
||||||
|
CComPtr<IDeckLinkDisplayModeIterator> iterator;
|
||||||
|
if (input->GetDisplayModeIterator(&iterator) != S_OK)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
return FindDeckLinkDisplayMode(iterator, targetMode, foundMode);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
DeckLinkInputCallback::DeckLinkInputCallback(DeckLinkInput& owner) :
|
||||||
|
mOwner(owner)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
HRESULT STDMETHODCALLTYPE DeckLinkInputCallback::QueryInterface(REFIID iid, LPVOID* ppv)
|
||||||
|
{
|
||||||
|
if (ppv == nullptr)
|
||||||
|
return E_POINTER;
|
||||||
|
if (iid == IID_IUnknown || iid == IID_IDeckLinkInputCallback)
|
||||||
|
{
|
||||||
|
*ppv = static_cast<IDeckLinkInputCallback*>(this);
|
||||||
|
AddRef();
|
||||||
|
return S_OK;
|
||||||
|
}
|
||||||
|
*ppv = nullptr;
|
||||||
|
return E_NOINTERFACE;
|
||||||
|
}
|
||||||
|
|
||||||
|
ULONG STDMETHODCALLTYPE DeckLinkInputCallback::AddRef()
|
||||||
|
{
|
||||||
|
return ++mRefCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
ULONG STDMETHODCALLTYPE DeckLinkInputCallback::Release()
|
||||||
|
{
|
||||||
|
const ULONG refCount = --mRefCount;
|
||||||
|
if (refCount == 0)
|
||||||
|
delete this;
|
||||||
|
return refCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
HRESULT STDMETHODCALLTYPE DeckLinkInputCallback::VideoInputFrameArrived(IDeckLinkVideoInputFrame* videoFrame, IDeckLinkAudioInputPacket*)
|
||||||
|
{
|
||||||
|
if (videoFrame != nullptr)
|
||||||
|
mOwner.HandleFrameArrived(videoFrame);
|
||||||
|
return S_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
HRESULT STDMETHODCALLTYPE DeckLinkInputCallback::VideoInputFormatChanged(BMDVideoInputFormatChangedEvents, IDeckLinkDisplayMode*, BMDDetectedVideoInputFormatFlags)
|
||||||
|
{
|
||||||
|
mOwner.HandleFormatChanged();
|
||||||
|
return S_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
DeckLinkInput::DeckLinkInput(InputFrameMailbox& mailbox) :
|
||||||
|
mMailbox(mailbox)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
DeckLinkInput::~DeckLinkInput()
|
||||||
|
{
|
||||||
|
ReleaseResources();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool DeckLinkInput::Initialize(const DeckLinkInputConfig& config, std::string& error)
|
||||||
|
{
|
||||||
|
ReleaseResources();
|
||||||
|
mConfig = config;
|
||||||
|
Log("decklink-input", "Initializing DeckLink input for " + config.videoFormat.displayName + ".");
|
||||||
|
|
||||||
|
if (!DiscoverInput(config, error))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if (mInput->EnableVideoInput(config.videoFormat.displayMode, mCapturePixelFormat, bmdVideoInputFlagDefault) != S_OK)
|
||||||
|
{
|
||||||
|
error = "DeckLink input setup failed while enabling " +
|
||||||
|
std::string(mCapturePixelFormat == bmdFormat8BitBGRA ? "BGRA8" : "UYVY8") +
|
||||||
|
" input for " + config.videoFormat.displayName + ".";
|
||||||
|
ReleaseResources();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
Log(
|
||||||
|
"decklink-input",
|
||||||
|
std::string("DeckLink input enabled in ") + (mCapturePixelFormat == bmdFormat8BitBGRA ? "BGRA8" : "UYVY8 raw capture") + " mode.");
|
||||||
|
|
||||||
|
mCallback.Attach(new (std::nothrow) DeckLinkInputCallback(*this));
|
||||||
|
if (mCallback == nullptr)
|
||||||
|
{
|
||||||
|
error = "DeckLink input setup failed while creating the capture callback.";
|
||||||
|
ReleaseResources();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mInput->SetCallback(mCallback) != S_OK)
|
||||||
|
{
|
||||||
|
error = "DeckLink input setup failed while installing the capture callback.";
|
||||||
|
ReleaseResources();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
Log("decklink-input", "DeckLink input callback installed.");
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool DeckLinkInput::Start(std::string& error)
|
||||||
|
{
|
||||||
|
if (mInput == nullptr)
|
||||||
|
{
|
||||||
|
error = "DeckLink input has not been initialized.";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (mRunning.load(std::memory_order_acquire))
|
||||||
|
return true;
|
||||||
|
|
||||||
|
if (mInput->StartStreams() != S_OK)
|
||||||
|
{
|
||||||
|
error = "DeckLink input stream failed to start.";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
mRunning.store(true, std::memory_order_release);
|
||||||
|
Log("decklink-input", "DeckLink input stream started.");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void DeckLinkInput::Stop()
|
||||||
|
{
|
||||||
|
if (mInput != nullptr && mRunning.exchange(false, std::memory_order_acq_rel))
|
||||||
|
{
|
||||||
|
mInput->StopStreams();
|
||||||
|
Log("decklink-input", "DeckLink input stream stopped.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void DeckLinkInput::ReleaseResources()
|
||||||
|
{
|
||||||
|
Stop();
|
||||||
|
if (mInput != nullptr)
|
||||||
|
{
|
||||||
|
mInput->SetCallback(nullptr);
|
||||||
|
mInput->DisableVideoInput();
|
||||||
|
}
|
||||||
|
mCallback.Release();
|
||||||
|
mInput.Release();
|
||||||
|
}
|
||||||
|
|
||||||
|
DeckLinkInputMetrics DeckLinkInput::Metrics() const
|
||||||
|
{
|
||||||
|
DeckLinkInputMetrics metrics;
|
||||||
|
metrics.capturedFrames = mCapturedFrames.load(std::memory_order_relaxed);
|
||||||
|
metrics.noInputSourceFrames = mNoInputSourceFrames.load(std::memory_order_relaxed);
|
||||||
|
metrics.unsupportedFrames = mUnsupportedFrames.load(std::memory_order_relaxed);
|
||||||
|
metrics.submitMisses = mSubmitMisses.load(std::memory_order_relaxed);
|
||||||
|
metrics.convertMilliseconds = mConvertMilliseconds.load(std::memory_order_relaxed);
|
||||||
|
metrics.submitMilliseconds = mSubmitMilliseconds.load(std::memory_order_relaxed);
|
||||||
|
metrics.captureFormat = CaptureFormatName();
|
||||||
|
return metrics;
|
||||||
|
}
|
||||||
|
|
||||||
|
VideoIOPixelFormat DeckLinkInput::CapturePixelFormat() const
|
||||||
|
{
|
||||||
|
return mCapturePixelFormat == bmdFormat8BitYUV ? VideoIOPixelFormat::Uyvy8 : VideoIOPixelFormat::Bgra8;
|
||||||
|
}
|
||||||
|
|
||||||
|
void DeckLinkInput::HandleFrameArrived(IDeckLinkVideoInputFrame* inputFrame)
|
||||||
|
{
|
||||||
|
if (inputFrame == nullptr)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if ((inputFrame->GetFlags() & bmdFrameHasNoInputSource) == bmdFrameHasNoInputSource)
|
||||||
|
{
|
||||||
|
mNoInputSourceFrames.fetch_add(1, std::memory_order_relaxed);
|
||||||
|
bool expected = false;
|
||||||
|
if (mLoggedNoInputSource.compare_exchange_strong(expected, true, std::memory_order_relaxed))
|
||||||
|
TryLog(LogLevel::Warning, "decklink-input", "DeckLink input callback reports no input source.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (inputFrame->GetWidth() != static_cast<long>(mMailbox.Config().width) ||
|
||||||
|
inputFrame->GetHeight() != static_cast<long>(mMailbox.Config().height))
|
||||||
|
{
|
||||||
|
mUnsupportedFrames.fetch_add(1, std::memory_order_relaxed);
|
||||||
|
bool expected = false;
|
||||||
|
if (mLoggedUnsupportedFrame.compare_exchange_strong(expected, true, std::memory_order_relaxed))
|
||||||
|
TryLog(LogLevel::Warning, "decklink-input", "DeckLink input frame dimensions do not match the configured mailbox.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
CComPtr<IDeckLinkVideoBuffer> inputFrameBuffer;
|
||||||
|
if (inputFrame->QueryInterface(IID_IDeckLinkVideoBuffer, reinterpret_cast<void**>(&inputFrameBuffer)) != S_OK)
|
||||||
|
{
|
||||||
|
mUnsupportedFrames.fetch_add(1, std::memory_order_relaxed);
|
||||||
|
bool expected = false;
|
||||||
|
if (mLoggedUnsupportedFrame.compare_exchange_strong(expected, true, std::memory_order_relaxed))
|
||||||
|
TryLog(LogLevel::Warning, "decklink-input", "DeckLink input frame does not expose IDeckLinkVideoBuffer.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (inputFrameBuffer->StartAccess(bmdBufferAccessRead) != S_OK)
|
||||||
|
{
|
||||||
|
mUnsupportedFrames.fetch_add(1, std::memory_order_relaxed);
|
||||||
|
bool expected = false;
|
||||||
|
if (mLoggedUnsupportedFrame.compare_exchange_strong(expected, true, std::memory_order_relaxed))
|
||||||
|
TryLog(LogLevel::Warning, "decklink-input", "DeckLink input frame buffer could not be opened for read access.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
void* bytes = nullptr;
|
||||||
|
inputFrameBuffer->GetBytes(&bytes);
|
||||||
|
bool submitted = false;
|
||||||
|
if (mCapturePixelFormat == bmdFormat8BitBGRA)
|
||||||
|
submitted = SubmitBgra8Frame(inputFrame, bytes);
|
||||||
|
else if (mCapturePixelFormat == bmdFormat8BitYUV)
|
||||||
|
submitted = SubmitUyvy8Frame(inputFrame, bytes);
|
||||||
|
else
|
||||||
|
mUnsupportedFrames.fetch_add(1, std::memory_order_relaxed);
|
||||||
|
|
||||||
|
if (!submitted)
|
||||||
|
{
|
||||||
|
mSubmitMisses.fetch_add(1, std::memory_order_relaxed);
|
||||||
|
bool expected = false;
|
||||||
|
if (mLoggedSubmitMiss.compare_exchange_strong(expected, true, std::memory_order_relaxed))
|
||||||
|
TryLog(LogLevel::Warning, "decklink-input", "DeckLink input frame could not be submitted to InputFrameMailbox.");
|
||||||
|
}
|
||||||
|
mCapturedFrames.fetch_add(1, std::memory_order_relaxed);
|
||||||
|
bool expectedFirstFrame = false;
|
||||||
|
if (mLoggedFirstFrame.compare_exchange_strong(expectedFirstFrame, true, std::memory_order_relaxed))
|
||||||
|
{
|
||||||
|
TryLog(
|
||||||
|
LogLevel::Log,
|
||||||
|
"decklink-input",
|
||||||
|
std::string("First DeckLink ") + (mCapturePixelFormat == bmdFormat8BitBGRA ? "BGRA8" : "UYVY8 raw") + " input frame submitted to InputFrameMailbox.");
|
||||||
|
}
|
||||||
|
|
||||||
|
inputFrameBuffer->EndAccess(bmdBufferAccessRead);
|
||||||
|
}
|
||||||
|
|
||||||
|
void DeckLinkInput::HandleFormatChanged()
|
||||||
|
{
|
||||||
|
mUnsupportedFrames.fetch_add(1, std::memory_order_relaxed);
|
||||||
|
TryLog(LogLevel::Warning, "decklink-input", "DeckLink input format changed; input edge does not auto-switch formats yet.");
|
||||||
|
}
|
||||||
|
|
||||||
|
bool DeckLinkInput::DiscoverInput(const DeckLinkInputConfig& config, std::string& error)
|
||||||
|
{
|
||||||
|
CComPtr<IDeckLinkIterator> iterator;
|
||||||
|
HRESULT result = CoCreateInstance(CLSID_CDeckLinkIterator, nullptr, CLSCTX_ALL, IID_IDeckLinkIterator, reinterpret_cast<void**>(&iterator));
|
||||||
|
if (FAILED(result))
|
||||||
|
{
|
||||||
|
error = "DeckLink input discovery failed. Blackmagic DeckLink drivers may not be installed.";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
CComPtr<IDeckLink> deckLink;
|
||||||
|
while (iterator->Next(&deckLink) == S_OK)
|
||||||
|
{
|
||||||
|
CComPtr<IDeckLinkInput> candidateInput;
|
||||||
|
if (deckLink->QueryInterface(IID_IDeckLinkInput, reinterpret_cast<void**>(&candidateInput)) == S_OK && candidateInput != nullptr)
|
||||||
|
{
|
||||||
|
CComPtr<IDeckLinkDisplayMode> displayMode;
|
||||||
|
if (FindInputDisplayMode(candidateInput, config.videoFormat.displayMode, &displayMode) &&
|
||||||
|
SupportsInputFormat(candidateInput, config.videoFormat.displayMode, bmdFormat8BitBGRA))
|
||||||
|
{
|
||||||
|
mInput = candidateInput;
|
||||||
|
mCapturePixelFormat = bmdFormat8BitBGRA;
|
||||||
|
Log("decklink-input", "DeckLink input device selected for BGRA8 capture.");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (displayMode != nullptr &&
|
||||||
|
SupportsInputFormat(candidateInput, config.videoFormat.displayMode, bmdFormat8BitYUV))
|
||||||
|
{
|
||||||
|
mInput = candidateInput;
|
||||||
|
mCapturePixelFormat = bmdFormat8BitYUV;
|
||||||
|
Log("decklink-input", "DeckLink input device selected for UYVY8 raw capture with render-thread GPU decode.");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
deckLink.Release();
|
||||||
|
}
|
||||||
|
|
||||||
|
error = "No DeckLink input device supports BGRA8 or UYVY8 capture for " + config.videoFormat.displayName + ".";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool DeckLinkInput::SupportsInputFormat(IDeckLinkInput* input, BMDDisplayMode displayMode, BMDPixelFormat pixelFormat) const
|
||||||
|
{
|
||||||
|
if (input == nullptr)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
BOOL supported = FALSE;
|
||||||
|
BMDDisplayMode actualMode = bmdModeUnknown;
|
||||||
|
const HRESULT result = input->DoesSupportVideoMode(
|
||||||
|
bmdVideoConnectionUnspecified,
|
||||||
|
displayMode,
|
||||||
|
pixelFormat,
|
||||||
|
bmdNoVideoInputConversion,
|
||||||
|
bmdSupportedVideoModeDefault,
|
||||||
|
&actualMode,
|
||||||
|
&supported);
|
||||||
|
return result == S_OK && supported != FALSE;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool DeckLinkInput::SubmitBgra8Frame(IDeckLinkVideoInputFrame* inputFrame, const void* bytes)
|
||||||
|
{
|
||||||
|
if (inputFrame == nullptr || bytes == nullptr)
|
||||||
|
return false;
|
||||||
|
const uint64_t frameIndex = mCapturedFrames.load(std::memory_order_relaxed);
|
||||||
|
mConvertMilliseconds.store(0.0, std::memory_order_relaxed);
|
||||||
|
const auto submitStart = std::chrono::steady_clock::now();
|
||||||
|
const bool submitted = mMailbox.SubmitFrame(bytes, static_cast<unsigned>(inputFrame->GetRowBytes()), frameIndex);
|
||||||
|
const auto submitEnd = std::chrono::steady_clock::now();
|
||||||
|
mSubmitMilliseconds.store(
|
||||||
|
std::chrono::duration_cast<std::chrono::duration<double, std::milli>>(submitEnd - submitStart).count(),
|
||||||
|
std::memory_order_relaxed);
|
||||||
|
return submitted;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool DeckLinkInput::SubmitUyvy8Frame(IDeckLinkVideoInputFrame* inputFrame, const void* bytes)
|
||||||
|
{
|
||||||
|
if (inputFrame == nullptr || bytes == nullptr)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
const unsigned width = static_cast<unsigned>(inputFrame->GetWidth());
|
||||||
|
const unsigned height = static_cast<unsigned>(inputFrame->GetHeight());
|
||||||
|
const long sourceRowBytes = inputFrame->GetRowBytes();
|
||||||
|
if (width == 0 || height == 0 || sourceRowBytes < static_cast<long>(width * 2u))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
mConvertMilliseconds.store(0.0, std::memory_order_relaxed);
|
||||||
|
const uint64_t frameIndex = mCapturedFrames.load(std::memory_order_relaxed);
|
||||||
|
const auto submitStart = std::chrono::steady_clock::now();
|
||||||
|
const bool submitted = mMailbox.SubmitFrame(bytes, static_cast<unsigned>(sourceRowBytes), frameIndex);
|
||||||
|
const auto submitEnd = std::chrono::steady_clock::now();
|
||||||
|
mSubmitMilliseconds.store(
|
||||||
|
std::chrono::duration_cast<std::chrono::duration<double, std::milli>>(submitEnd - submitStart).count(),
|
||||||
|
std::memory_order_relaxed);
|
||||||
|
return submitted;
|
||||||
|
}
|
||||||
|
|
||||||
|
const char* DeckLinkInput::CaptureFormatName() const
|
||||||
|
{
|
||||||
|
if (mInput == nullptr)
|
||||||
|
return "none";
|
||||||
|
if (mCapturePixelFormat == bmdFormat8BitBGRA)
|
||||||
|
return "BGRA8";
|
||||||
|
if (mCapturePixelFormat == bmdFormat8BitYUV)
|
||||||
|
return "UYVY8";
|
||||||
|
return "unsupported";
|
||||||
|
}
|
||||||
|
}
|
||||||
96
apps/RenderCadenceCompositor/video/DeckLinkInput.h
Normal file
96
apps/RenderCadenceCompositor/video/DeckLinkInput.h
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "../frames/InputFrameMailbox.h"
|
||||||
|
#include "DeckLinkAPI_h.h"
|
||||||
|
#include "DeckLinkDisplayMode.h"
|
||||||
|
|
||||||
|
#include <atlbase.h>
|
||||||
|
|
||||||
|
#include <atomic>
|
||||||
|
#include <chrono>
|
||||||
|
#include <cstdint>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
namespace RenderCadenceCompositor
|
||||||
|
{
|
||||||
|
struct DeckLinkInputConfig
|
||||||
|
{
|
||||||
|
VideoFormat videoFormat;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct DeckLinkInputMetrics
|
||||||
|
{
|
||||||
|
uint64_t capturedFrames = 0;
|
||||||
|
uint64_t noInputSourceFrames = 0;
|
||||||
|
uint64_t unsupportedFrames = 0;
|
||||||
|
uint64_t submitMisses = 0;
|
||||||
|
double convertMilliseconds = 0.0;
|
||||||
|
double submitMilliseconds = 0.0;
|
||||||
|
const char* captureFormat = "none";
|
||||||
|
};
|
||||||
|
|
||||||
|
class DeckLinkInput;
|
||||||
|
|
||||||
|
class DeckLinkInputCallback final : public IDeckLinkInputCallback
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
explicit DeckLinkInputCallback(DeckLinkInput& owner);
|
||||||
|
|
||||||
|
HRESULT STDMETHODCALLTYPE QueryInterface(REFIID iid, LPVOID* ppv) override;
|
||||||
|
ULONG STDMETHODCALLTYPE AddRef() override;
|
||||||
|
ULONG STDMETHODCALLTYPE Release() override;
|
||||||
|
HRESULT STDMETHODCALLTYPE VideoInputFrameArrived(IDeckLinkVideoInputFrame* videoFrame, IDeckLinkAudioInputPacket* audioPacket) override;
|
||||||
|
HRESULT STDMETHODCALLTYPE VideoInputFormatChanged(BMDVideoInputFormatChangedEvents notificationEvents, IDeckLinkDisplayMode* newDisplayMode, BMDDetectedVideoInputFormatFlags detectedSignalFlags) override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
DeckLinkInput& mOwner;
|
||||||
|
std::atomic<ULONG> mRefCount{ 1 };
|
||||||
|
};
|
||||||
|
|
||||||
|
class DeckLinkInput
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
DeckLinkInput(InputFrameMailbox& mailbox);
|
||||||
|
DeckLinkInput(const DeckLinkInput&) = delete;
|
||||||
|
DeckLinkInput& operator=(const DeckLinkInput&) = delete;
|
||||||
|
~DeckLinkInput();
|
||||||
|
|
||||||
|
bool Initialize(const DeckLinkInputConfig& config, std::string& error);
|
||||||
|
bool Start(std::string& error);
|
||||||
|
void Stop();
|
||||||
|
void ReleaseResources();
|
||||||
|
|
||||||
|
bool IsInitialized() const { return mInput != nullptr; }
|
||||||
|
bool IsRunning() const { return mRunning.load(std::memory_order_acquire); }
|
||||||
|
VideoIOPixelFormat CapturePixelFormat() const;
|
||||||
|
DeckLinkInputMetrics Metrics() const;
|
||||||
|
|
||||||
|
void HandleFrameArrived(IDeckLinkVideoInputFrame* inputFrame);
|
||||||
|
void HandleFormatChanged();
|
||||||
|
|
||||||
|
private:
|
||||||
|
bool DiscoverInput(const DeckLinkInputConfig& config, std::string& error);
|
||||||
|
bool SupportsInputFormat(IDeckLinkInput* input, BMDDisplayMode displayMode, BMDPixelFormat pixelFormat) const;
|
||||||
|
bool SubmitBgra8Frame(IDeckLinkVideoInputFrame* inputFrame, const void* bytes);
|
||||||
|
bool SubmitUyvy8Frame(IDeckLinkVideoInputFrame* inputFrame, const void* bytes);
|
||||||
|
const char* CaptureFormatName() const;
|
||||||
|
|
||||||
|
InputFrameMailbox& mMailbox;
|
||||||
|
DeckLinkInputConfig mConfig;
|
||||||
|
BMDPixelFormat mCapturePixelFormat = bmdFormat8BitBGRA;
|
||||||
|
CComPtr<IDeckLinkInput> mInput;
|
||||||
|
CComPtr<DeckLinkInputCallback> mCallback;
|
||||||
|
std::atomic<bool> mRunning{ false };
|
||||||
|
std::atomic<uint64_t> mCapturedFrames{ 0 };
|
||||||
|
std::atomic<uint64_t> mNoInputSourceFrames{ 0 };
|
||||||
|
std::atomic<uint64_t> mUnsupportedFrames{ 0 };
|
||||||
|
std::atomic<uint64_t> mSubmitMisses{ 0 };
|
||||||
|
std::atomic<double> mConvertMilliseconds{ 0.0 };
|
||||||
|
std::atomic<double> mSubmitMilliseconds{ 0.0 };
|
||||||
|
std::atomic<bool> mLoggedFirstFrame{ false };
|
||||||
|
std::atomic<bool> mLoggedNoInputSource{ false };
|
||||||
|
std::atomic<bool> mLoggedUnsupportedFrame{ false };
|
||||||
|
std::atomic<bool> mLoggedSubmitMiss{ false };
|
||||||
|
};
|
||||||
|
}
|
||||||
87
apps/RenderCadenceCompositor/video/DeckLinkInputThread.h
Normal file
87
apps/RenderCadenceCompositor/video/DeckLinkInputThread.h
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "DeckLinkInput.h"
|
||||||
|
|
||||||
|
#include <atomic>
|
||||||
|
#include <chrono>
|
||||||
|
#include <string>
|
||||||
|
#include <thread>
|
||||||
|
|
||||||
|
namespace RenderCadenceCompositor
|
||||||
|
{
|
||||||
|
struct DeckLinkInputThreadConfig
|
||||||
|
{
|
||||||
|
std::chrono::milliseconds idleSleep = std::chrono::milliseconds(100);
|
||||||
|
};
|
||||||
|
|
||||||
|
class DeckLinkInputThread
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
DeckLinkInputThread(DeckLinkInput& input, DeckLinkInputThreadConfig config = DeckLinkInputThreadConfig()) :
|
||||||
|
mInput(input),
|
||||||
|
mConfig(config)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
DeckLinkInputThread(const DeckLinkInputThread&) = delete;
|
||||||
|
DeckLinkInputThread& operator=(const DeckLinkInputThread&) = delete;
|
||||||
|
|
||||||
|
~DeckLinkInputThread()
|
||||||
|
{
|
||||||
|
Stop();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Start(std::string& error)
|
||||||
|
{
|
||||||
|
if (mThread.joinable())
|
||||||
|
return true;
|
||||||
|
mStartSucceeded.store(false, std::memory_order_release);
|
||||||
|
mStartCompleted.store(false, std::memory_order_release);
|
||||||
|
mStopping.store(false, std::memory_order_release);
|
||||||
|
mThread = std::thread([this]() { ThreadMain(); });
|
||||||
|
|
||||||
|
while (!mStartCompleted.load(std::memory_order_acquire))
|
||||||
|
std::this_thread::sleep_for(std::chrono::milliseconds(1));
|
||||||
|
|
||||||
|
if (mStartSucceeded.load(std::memory_order_acquire))
|
||||||
|
return true;
|
||||||
|
|
||||||
|
error = mStartError;
|
||||||
|
Stop();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Stop()
|
||||||
|
{
|
||||||
|
mStopping.store(true, std::memory_order_release);
|
||||||
|
if (mThread.joinable())
|
||||||
|
mThread.join();
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
void ThreadMain()
|
||||||
|
{
|
||||||
|
std::string error;
|
||||||
|
if (!mInput.Start(error))
|
||||||
|
{
|
||||||
|
mStartError = error;
|
||||||
|
mStartCompleted.store(true, std::memory_order_release);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
mStartSucceeded.store(true, std::memory_order_release);
|
||||||
|
mStartCompleted.store(true, std::memory_order_release);
|
||||||
|
while (!mStopping.load(std::memory_order_acquire))
|
||||||
|
std::this_thread::sleep_for(mConfig.idleSleep);
|
||||||
|
mInput.Stop();
|
||||||
|
}
|
||||||
|
|
||||||
|
DeckLinkInput& mInput;
|
||||||
|
DeckLinkInputThreadConfig mConfig;
|
||||||
|
std::thread mThread;
|
||||||
|
std::atomic<bool> mStopping{ false };
|
||||||
|
std::atomic<bool> mStartCompleted{ false };
|
||||||
|
std::atomic<bool> mStartSucceeded{ false };
|
||||||
|
std::string mStartError;
|
||||||
|
};
|
||||||
|
}
|
||||||
105
apps/RenderCadenceCompositor/video/DeckLinkOutput.cpp
Normal file
105
apps/RenderCadenceCompositor/video/DeckLinkOutput.cpp
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
#include "DeckLinkOutput.h"
|
||||||
|
|
||||||
|
#include "VideoIOFormat.h"
|
||||||
|
|
||||||
|
namespace RenderCadenceCompositor
|
||||||
|
{
|
||||||
|
DeckLinkOutput::~DeckLinkOutput()
|
||||||
|
{
|
||||||
|
ReleaseResources();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool DeckLinkOutput::Initialize(const DeckLinkOutputConfig& config, CompletionCallback completionCallback, std::string& error)
|
||||||
|
{
|
||||||
|
mConfig = config;
|
||||||
|
mCompletionCallback = completionCallback;
|
||||||
|
|
||||||
|
VideoFormatSelection formats;
|
||||||
|
if (!mSession.DiscoverDevicesAndModes(formats, error))
|
||||||
|
return false;
|
||||||
|
if (!mSession.SelectPreferredFormats(formats, config.outputAlphaRequired, error))
|
||||||
|
return false;
|
||||||
|
if (!mSession.ConfigureOutput(
|
||||||
|
[this](const VideoIOCompletion& completion) { HandleCompletion(completion); },
|
||||||
|
formats.output,
|
||||||
|
config.externalKeyingEnabled,
|
||||||
|
error))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!mSession.PrepareOutputSchedule())
|
||||||
|
{
|
||||||
|
error = "DeckLink output schedule preparation failed.";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool DeckLinkOutput::StartScheduledPlayback(std::string& error)
|
||||||
|
{
|
||||||
|
if (mSession.StartScheduledPlayback())
|
||||||
|
return true;
|
||||||
|
error = "DeckLink scheduled playback failed to start.";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool DeckLinkOutput::ScheduleFrame(const VideoIOOutputFrame& frame)
|
||||||
|
{
|
||||||
|
return mSession.ScheduleOutputFrame(frame);
|
||||||
|
}
|
||||||
|
|
||||||
|
void DeckLinkOutput::Stop()
|
||||||
|
{
|
||||||
|
mSession.Stop();
|
||||||
|
}
|
||||||
|
|
||||||
|
void DeckLinkOutput::ReleaseResources()
|
||||||
|
{
|
||||||
|
mSession.ReleaseResources();
|
||||||
|
}
|
||||||
|
|
||||||
|
const VideoIOState& DeckLinkOutput::State() const
|
||||||
|
{
|
||||||
|
return mSession.State();
|
||||||
|
}
|
||||||
|
|
||||||
|
DeckLinkOutputMetrics DeckLinkOutput::Metrics() const
|
||||||
|
{
|
||||||
|
DeckLinkOutputMetrics metrics;
|
||||||
|
metrics.completions = mCompletions.load();
|
||||||
|
metrics.displayedLate = mDisplayedLate.load();
|
||||||
|
metrics.dropped = mDropped.load();
|
||||||
|
metrics.flushed = mFlushed.load();
|
||||||
|
|
||||||
|
const VideoIOState& state = mSession.State();
|
||||||
|
metrics.scheduleFailures = state.deckLinkScheduleFailureCount;
|
||||||
|
metrics.actualBufferedFramesAvailable = state.actualDeckLinkBufferedFramesAvailable;
|
||||||
|
metrics.actualBufferedFrames = state.actualDeckLinkBufferedFrames;
|
||||||
|
metrics.scheduleCallMilliseconds = state.deckLinkScheduleCallMilliseconds;
|
||||||
|
return metrics;
|
||||||
|
}
|
||||||
|
|
||||||
|
void DeckLinkOutput::HandleCompletion(const VideoIOCompletion& completion)
|
||||||
|
{
|
||||||
|
++mCompletions;
|
||||||
|
switch (completion.result)
|
||||||
|
{
|
||||||
|
case VideoIOCompletionResult::DisplayedLate:
|
||||||
|
++mDisplayedLate;
|
||||||
|
break;
|
||||||
|
case VideoIOCompletionResult::Dropped:
|
||||||
|
++mDropped;
|
||||||
|
break;
|
||||||
|
case VideoIOCompletionResult::Flushed:
|
||||||
|
++mFlushed;
|
||||||
|
break;
|
||||||
|
case VideoIOCompletionResult::Completed:
|
||||||
|
case VideoIOCompletionResult::Unknown:
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mCompletionCallback)
|
||||||
|
mCompletionCallback(completion);
|
||||||
|
}
|
||||||
|
}
|
||||||
61
apps/RenderCadenceCompositor/video/DeckLinkOutput.h
Normal file
61
apps/RenderCadenceCompositor/video/DeckLinkOutput.h
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "DeckLinkSession.h"
|
||||||
|
#include "VideoIOTypes.h"
|
||||||
|
|
||||||
|
#include <atomic>
|
||||||
|
#include <cstdint>
|
||||||
|
#include <functional>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
namespace RenderCadenceCompositor
|
||||||
|
{
|
||||||
|
struct DeckLinkOutputConfig
|
||||||
|
{
|
||||||
|
bool externalKeyingEnabled = false;
|
||||||
|
bool outputAlphaRequired = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct DeckLinkOutputMetrics
|
||||||
|
{
|
||||||
|
uint64_t completions = 0;
|
||||||
|
uint64_t displayedLate = 0;
|
||||||
|
uint64_t dropped = 0;
|
||||||
|
uint64_t flushed = 0;
|
||||||
|
uint64_t scheduleFailures = 0;
|
||||||
|
bool actualBufferedFramesAvailable = false;
|
||||||
|
uint64_t actualBufferedFrames = 0;
|
||||||
|
double scheduleCallMilliseconds = 0.0;
|
||||||
|
};
|
||||||
|
|
||||||
|
class DeckLinkOutput
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
using CompletionCallback = std::function<void(const VideoIOCompletion&)>;
|
||||||
|
|
||||||
|
DeckLinkOutput() = default;
|
||||||
|
DeckLinkOutput(const DeckLinkOutput&) = delete;
|
||||||
|
DeckLinkOutput& operator=(const DeckLinkOutput&) = delete;
|
||||||
|
~DeckLinkOutput();
|
||||||
|
|
||||||
|
bool Initialize(const DeckLinkOutputConfig& config, CompletionCallback completionCallback, std::string& error);
|
||||||
|
bool StartScheduledPlayback(std::string& error);
|
||||||
|
bool ScheduleFrame(const VideoIOOutputFrame& frame);
|
||||||
|
void Stop();
|
||||||
|
void ReleaseResources();
|
||||||
|
|
||||||
|
const VideoIOState& State() const;
|
||||||
|
DeckLinkOutputMetrics Metrics() const;
|
||||||
|
|
||||||
|
private:
|
||||||
|
void HandleCompletion(const VideoIOCompletion& completion);
|
||||||
|
|
||||||
|
DeckLinkSession mSession;
|
||||||
|
DeckLinkOutputConfig mConfig;
|
||||||
|
CompletionCallback mCompletionCallback;
|
||||||
|
std::atomic<uint64_t> mCompletions{ 0 };
|
||||||
|
std::atomic<uint64_t> mDisplayedLate{ 0 };
|
||||||
|
std::atomic<uint64_t> mDropped{ 0 };
|
||||||
|
std::atomic<uint64_t> mFlushed{ 0 };
|
||||||
|
};
|
||||||
|
}
|
||||||
124
apps/RenderCadenceCompositor/video/DeckLinkOutputThread.h
Normal file
124
apps/RenderCadenceCompositor/video/DeckLinkOutputThread.h
Normal file
@@ -0,0 +1,124 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "../frames/SystemFrameTypes.h"
|
||||||
|
#include "DeckLinkOutput.h"
|
||||||
|
#include "VideoIOTypes.h"
|
||||||
|
|
||||||
|
#include <atomic>
|
||||||
|
#include <chrono>
|
||||||
|
#include <cstddef>
|
||||||
|
#include <cstdint>
|
||||||
|
#include <thread>
|
||||||
|
|
||||||
|
namespace RenderCadenceCompositor
|
||||||
|
{
|
||||||
|
struct DeckLinkOutputThreadConfig
|
||||||
|
{
|
||||||
|
std::size_t targetBufferedFrames = 4;
|
||||||
|
std::chrono::milliseconds idleSleep = std::chrono::milliseconds(1);
|
||||||
|
};
|
||||||
|
|
||||||
|
struct DeckLinkOutputThreadMetrics
|
||||||
|
{
|
||||||
|
uint64_t scheduledFrames = 0;
|
||||||
|
uint64_t completedPollMisses = 0;
|
||||||
|
uint64_t scheduleFailures = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
template <typename SystemFrameExchange>
|
||||||
|
class DeckLinkOutputThread
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
DeckLinkOutputThread(DeckLinkOutput& output, SystemFrameExchange& exchange, DeckLinkOutputThreadConfig config = DeckLinkOutputThreadConfig()) :
|
||||||
|
mOutput(output),
|
||||||
|
mExchange(exchange),
|
||||||
|
mConfig(config)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
DeckLinkOutputThread(const DeckLinkOutputThread&) = delete;
|
||||||
|
DeckLinkOutputThread& operator=(const DeckLinkOutputThread&) = delete;
|
||||||
|
|
||||||
|
~DeckLinkOutputThread()
|
||||||
|
{
|
||||||
|
Stop();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Start()
|
||||||
|
{
|
||||||
|
if (mRunning)
|
||||||
|
return true;
|
||||||
|
mStopping = false;
|
||||||
|
mThread = std::thread([this]() { ThreadMain(); });
|
||||||
|
mRunning = true;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Stop()
|
||||||
|
{
|
||||||
|
mStopping = true;
|
||||||
|
if (mThread.joinable())
|
||||||
|
mThread.join();
|
||||||
|
mRunning = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
DeckLinkOutputThreadMetrics Metrics() const
|
||||||
|
{
|
||||||
|
DeckLinkOutputThreadMetrics metrics;
|
||||||
|
metrics.scheduledFrames = mScheduledFrames.load();
|
||||||
|
metrics.completedPollMisses = mCompletedPollMisses.load();
|
||||||
|
metrics.scheduleFailures = mScheduleFailures.load();
|
||||||
|
return metrics;
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
void ThreadMain()
|
||||||
|
{
|
||||||
|
while (!mStopping)
|
||||||
|
{
|
||||||
|
const auto exchangeMetrics = mExchange.Metrics();
|
||||||
|
if (exchangeMetrics.scheduledCount >= mConfig.targetBufferedFrames)
|
||||||
|
{
|
||||||
|
std::this_thread::sleep_for(mConfig.idleSleep);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
SystemFrame frame;
|
||||||
|
if (!mExchange.ConsumeCompletedForSchedule(frame))
|
||||||
|
{
|
||||||
|
++mCompletedPollMisses;
|
||||||
|
std::this_thread::sleep_for(mConfig.idleSleep);
|
||||||
|
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 (!mOutput.ScheduleFrame(outputFrame))
|
||||||
|
{
|
||||||
|
++mScheduleFailures;
|
||||||
|
mExchange.ReleaseScheduledByBytes(frame.bytes);
|
||||||
|
std::this_thread::sleep_for(mConfig.idleSleep);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
++mScheduledFrames;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DeckLinkOutput& mOutput;
|
||||||
|
SystemFrameExchange& mExchange;
|
||||||
|
DeckLinkOutputThreadConfig mConfig;
|
||||||
|
std::thread mThread;
|
||||||
|
std::atomic<bool> mStopping{ false };
|
||||||
|
std::atomic<bool> mRunning{ false };
|
||||||
|
std::atomic<uint64_t> mScheduledFrames{ 0 };
|
||||||
|
std::atomic<uint64_t> mCompletedPollMisses{ 0 };
|
||||||
|
std::atomic<uint64_t> mScheduleFailures{ 0 };
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -4,6 +4,14 @@ This document describes how the application currently works.
|
|||||||
|
|
||||||
It replaces the phase-by-phase design trail as the best entry point for understanding the repo. The older phase documents remain useful history, but they mix implementation notes, experiments, and target designs. This document is organized by current runtime behavior and subsystem ownership instead.
|
It replaces the phase-by-phase design trail as the best entry point for understanding the repo. The older phase documents remain useful history, but they mix implementation notes, experiments, and target designs. This document is organized by current runtime behavior and subsystem ownership instead.
|
||||||
|
|
||||||
|
The active plan for tightening render-thread ownership is:
|
||||||
|
|
||||||
|
- [Render Thread Ownership Plan](RENDER_THREAD_OWNERSHIP_PLAN.md)
|
||||||
|
|
||||||
|
The plan for building a fresh modular app around the proven probe architecture is:
|
||||||
|
|
||||||
|
- [New Render Cadence App Plan](NEW_RENDER_CADENCE_APP_PLAN.md)
|
||||||
|
|
||||||
## Application Shape
|
## Application Shape
|
||||||
|
|
||||||
The app is a live OpenGL compositor with DeckLink input/output, runtime control services, persistent layer-stack state, live state overlays, health telemetry, and a small internal event model.
|
The app is a live OpenGL compositor with DeckLink input/output, runtime control services, persistent layer-stack state, live state overlays, health telemetry, and a small internal event model.
|
||||||
|
|||||||
@@ -282,6 +282,24 @@ Lesson:
|
|||||||
- test policies such as `one_before_output` or `skip_before_output`
|
- test policies such as `one_before_output` or `skip_before_output`
|
||||||
- prefer latest-input semantics over draining every pending upload
|
- prefer latest-input semantics over draining every pending upload
|
||||||
|
|
||||||
|
### CPU Input Conversion Can Be Worse Than Input Copy
|
||||||
|
|
||||||
|
When DeckLink input only exposed UYVY8 on the test machine, an initial CPU UYVY-to-BGRA conversion in the input callback measured around a full-frame budget on sampled runs and reduced input cadence dramatically.
|
||||||
|
|
||||||
|
Moving the input edge to raw UYVY8 capture changed the ownership:
|
||||||
|
|
||||||
|
- DeckLink callback copies raw supported input bytes into `InputFrameMailbox`
|
||||||
|
- the mailbox keeps latest-frame semantics and uses a contiguous copy when row strides match
|
||||||
|
- the render thread uploads/decodes UYVY8 into the shader-visible `gVideoInput` texture
|
||||||
|
- runtime shaders continue to see decoded input, not packed capture bytes
|
||||||
|
|
||||||
|
Lesson:
|
||||||
|
|
||||||
|
- keep input callbacks as capture/copy edges
|
||||||
|
- keep GL decode/upload in the render-owned path
|
||||||
|
- measure input copy, upload, and decode separately
|
||||||
|
- do not hide expensive format conversion inside the DeckLink callback
|
||||||
|
|
||||||
### Preview And Screenshot Must Stay Secondary
|
### Preview And Screenshot Must Stay Secondary
|
||||||
|
|
||||||
Preview is useful, but DeckLink output is the real-time path.
|
Preview is useful, but DeckLink output is the real-time path.
|
||||||
|
|||||||
578
docs/NEW_RENDER_CADENCE_APP_PLAN.md
Normal file
578
docs/NEW_RENDER_CADENCE_APP_PLAN.md
Normal file
@@ -0,0 +1,578 @@
|
|||||||
|
# New Render Cadence App Plan
|
||||||
|
|
||||||
|
This plan describes a new application folder that rebuilds the output path from the proven `DeckLinkRenderCadenceProbe` architecture, but as a maintainable app foundation rather than a monolithic probe file.
|
||||||
|
|
||||||
|
The first goal is not to port the current compositor feature set. The first goal is to reproduce the probe's smooth 59.94/60 fps DeckLink output with clean module boundaries, tests where possible, and a structure that can later accept the shader/runtime/control systems without compromising timing.
|
||||||
|
|
||||||
|
## Working Name
|
||||||
|
|
||||||
|
Suggested folder:
|
||||||
|
|
||||||
|
```text
|
||||||
|
apps/RenderCadenceCompositor
|
||||||
|
```
|
||||||
|
|
||||||
|
Suggested executable:
|
||||||
|
|
||||||
|
```text
|
||||||
|
RenderCadenceCompositor
|
||||||
|
```
|
||||||
|
|
||||||
|
The existing app remains intact:
|
||||||
|
|
||||||
|
```text
|
||||||
|
apps/LoopThroughWithOpenGLCompositing
|
||||||
|
```
|
||||||
|
|
||||||
|
The probe remains the control sample:
|
||||||
|
|
||||||
|
```text
|
||||||
|
apps/DeckLinkRenderCadenceProbe
|
||||||
|
```
|
||||||
|
|
||||||
|
## Design Principle
|
||||||
|
|
||||||
|
The app is built around one spine:
|
||||||
|
|
||||||
|
```text
|
||||||
|
Render cadence thread
|
||||||
|
-> owns GL context
|
||||||
|
-> renders at selected frame cadence
|
||||||
|
-> performs async BGRA8 readback
|
||||||
|
-> publishes completed system-memory frames
|
||||||
|
|
||||||
|
System frame exchange
|
||||||
|
-> owns Free / Rendering / Completed / Scheduled slots
|
||||||
|
-> latest-N semantics for completed unscheduled frames
|
||||||
|
-> protects scheduled frames until DeckLink completion
|
||||||
|
|
||||||
|
DeckLink output thread
|
||||||
|
-> consumes completed frames
|
||||||
|
-> schedules to target buffer depth
|
||||||
|
-> releases scheduled frames on completion
|
||||||
|
-> never renders
|
||||||
|
```
|
||||||
|
|
||||||
|
Everything else must fit around that spine.
|
||||||
|
|
||||||
|
## Non-Negotiable Rules
|
||||||
|
|
||||||
|
- The render thread owns its GL context from initialization to shutdown.
|
||||||
|
- The render thread is driven by selected render cadence, not DeckLink demand.
|
||||||
|
- DeckLink scheduling never calls render code.
|
||||||
|
- Completion callbacks never render.
|
||||||
|
- No synchronous render request exists in the output path.
|
||||||
|
- Preview, screenshot, input upload, shader rebuild, and runtime control cannot run ahead of a due output frame.
|
||||||
|
- Completed unscheduled frames are latest-N and disposable.
|
||||||
|
- Scheduled frames are protected until DeckLink completion.
|
||||||
|
- Startup warms up real rendered frames before scheduled playback starts.
|
||||||
|
|
||||||
|
## Borrow From The Probe
|
||||||
|
|
||||||
|
Keep these behaviors from `DeckLinkRenderCadenceProbe`:
|
||||||
|
|
||||||
|
- hidden OpenGL context owned by the render thread
|
||||||
|
- simple render loop with `nextRenderTime`
|
||||||
|
- BGRA8 render target
|
||||||
|
- PBO ring readback
|
||||||
|
- non-blocking fence polling with zero timeout
|
||||||
|
- system-memory slots with `Free`, `Rendering`, `Completed`, `Scheduled`
|
||||||
|
- drop oldest completed unscheduled frame if render needs space
|
||||||
|
- DeckLink playout thread only schedules completed frames
|
||||||
|
- warmup completed frames before `StartScheduledPlayback()`
|
||||||
|
- one-line-per-second timing telemetry
|
||||||
|
|
||||||
|
## Do Not Borrow Directly
|
||||||
|
|
||||||
|
The probe is deliberately compact. Do not carry over these probe limitations into the new app:
|
||||||
|
|
||||||
|
- one huge `.cpp` file
|
||||||
|
- hard-coded output mode as permanent behavior
|
||||||
|
- render pattern, frame store, PBO logic, DeckLink playout, COM setup, and telemetry mixed together
|
||||||
|
- no reusable interfaces
|
||||||
|
- no unit-testable non-GL core
|
||||||
|
|
||||||
|
## Proposed Folder Structure
|
||||||
|
|
||||||
|
```text
|
||||||
|
apps/RenderCadenceCompositor/
|
||||||
|
README.md
|
||||||
|
RenderCadenceCompositor.cpp
|
||||||
|
|
||||||
|
app/
|
||||||
|
RenderCadenceApp.cpp
|
||||||
|
RenderCadenceApp.h
|
||||||
|
AppConfig.cpp
|
||||||
|
AppConfig.h
|
||||||
|
AppConfigProvider.cpp
|
||||||
|
AppConfigProvider.h
|
||||||
|
|
||||||
|
control/
|
||||||
|
HttpControlServer.cpp
|
||||||
|
HttpControlServer.h
|
||||||
|
RuntimeStateJson.h
|
||||||
|
|
||||||
|
platform/
|
||||||
|
ComInit.cpp
|
||||||
|
ComInit.h
|
||||||
|
HiddenGlWindow.cpp
|
||||||
|
HiddenGlWindow.h
|
||||||
|
Win32Console.cpp
|
||||||
|
Win32Console.h
|
||||||
|
|
||||||
|
render/
|
||||||
|
RenderThread.cpp
|
||||||
|
RenderThread.h
|
||||||
|
RenderCadenceClock.cpp
|
||||||
|
RenderCadenceClock.h
|
||||||
|
SimpleMotionRenderer.cpp
|
||||||
|
SimpleMotionRenderer.h
|
||||||
|
Bgra8ReadbackPipeline.cpp
|
||||||
|
Bgra8ReadbackPipeline.h
|
||||||
|
PboReadbackRing.cpp
|
||||||
|
PboReadbackRing.h
|
||||||
|
|
||||||
|
frames/
|
||||||
|
SystemFrameExchange.cpp
|
||||||
|
SystemFrameExchange.h
|
||||||
|
SystemFrameTypes.h
|
||||||
|
|
||||||
|
video/
|
||||||
|
DeckLinkOutput.cpp
|
||||||
|
DeckLinkOutput.h
|
||||||
|
DeckLinkOutputThread.cpp
|
||||||
|
DeckLinkOutputThread.h
|
||||||
|
|
||||||
|
telemetry/
|
||||||
|
CadenceTelemetry.cpp
|
||||||
|
CadenceTelemetry.h
|
||||||
|
CadenceTelemetryJson.h
|
||||||
|
TelemetryHealthMonitor.h
|
||||||
|
|
||||||
|
logging/
|
||||||
|
Logger.cpp
|
||||||
|
Logger.h
|
||||||
|
|
||||||
|
json/
|
||||||
|
JsonWriter.cpp
|
||||||
|
JsonWriter.h
|
||||||
|
```
|
||||||
|
|
||||||
|
The new app can reuse selected existing source files from the current app at first:
|
||||||
|
|
||||||
|
- `videoio/decklink/DeckLinkSession.*`
|
||||||
|
- `videoio/decklink/DeckLinkDisplayMode.*`
|
||||||
|
- `videoio/decklink/DeckLinkVideoIOFormat.*`
|
||||||
|
- `videoio/decklink/DeckLinkFrameTransfer.*`
|
||||||
|
- `videoio/VideoIOFormat.*`
|
||||||
|
- `videoio/VideoIOTypes.h`
|
||||||
|
- `videoio/VideoPlayoutScheduler.*`
|
||||||
|
- `gl/renderer/GLExtensions.*`
|
||||||
|
|
||||||
|
Longer term, shared code should move into common libraries, but the first version can link these files directly to avoid a big build-system refactor.
|
||||||
|
|
||||||
|
## Module Responsibilities
|
||||||
|
|
||||||
|
### `RenderCadenceApp`
|
||||||
|
|
||||||
|
Owns top-level startup/shutdown sequencing.
|
||||||
|
|
||||||
|
Responsibilities:
|
||||||
|
|
||||||
|
- initialize COM
|
||||||
|
- discover/select DeckLink output
|
||||||
|
- create frame exchange
|
||||||
|
- start render thread
|
||||||
|
- wait for completed-frame warmup
|
||||||
|
- start DeckLink output thread
|
||||||
|
- wait for scheduled buffer warmup
|
||||||
|
- start DeckLink scheduled playback
|
||||||
|
- start telemetry printer
|
||||||
|
- stop in reverse order
|
||||||
|
|
||||||
|
It should not contain OpenGL drawing code, frame slot policy, or DeckLink scheduling loops.
|
||||||
|
|
||||||
|
### `AppConfig`
|
||||||
|
|
||||||
|
Owns runtime settings for the initial app.
|
||||||
|
|
||||||
|
Initial settings:
|
||||||
|
|
||||||
|
- output mode preference
|
||||||
|
- output width/height validation
|
||||||
|
- frame buffer capacity
|
||||||
|
- PBO depth
|
||||||
|
- warmup completed-frame count
|
||||||
|
- target DeckLink scheduled depth
|
||||||
|
- telemetry interval
|
||||||
|
|
||||||
|
Initial values should match the successful probe:
|
||||||
|
|
||||||
|
```text
|
||||||
|
systemFrameSlots = 12
|
||||||
|
pboDepth = 6
|
||||||
|
warmupFrames = 4
|
||||||
|
targetDeckLinkBufferedFrames = 4
|
||||||
|
pixelFormat = BGRA8
|
||||||
|
```
|
||||||
|
|
||||||
|
### `HiddenGlWindow`
|
||||||
|
|
||||||
|
Owns hidden Win32 window, device context, and OpenGL context creation.
|
||||||
|
|
||||||
|
Responsibilities:
|
||||||
|
|
||||||
|
- create hidden window with `CS_OWNDC`
|
||||||
|
- choose/set pixel format
|
||||||
|
- create `HGLRC`
|
||||||
|
- expose `MakeCurrent()` and `ClearCurrent()`
|
||||||
|
- destroy context/window safely
|
||||||
|
|
||||||
|
Only `RenderThread` should call `MakeCurrent()` after startup.
|
||||||
|
|
||||||
|
### `RenderThread`
|
||||||
|
|
||||||
|
Owns the render loop and GL context for its full lifetime.
|
||||||
|
|
||||||
|
Responsibilities:
|
||||||
|
|
||||||
|
- create/bind hidden GL context
|
||||||
|
- resolve GL extensions
|
||||||
|
- initialize renderer/readback pipeline
|
||||||
|
- run cadence loop
|
||||||
|
- render one frame when due
|
||||||
|
- queue PBO readback
|
||||||
|
- consume completed PBOs into `SystemFrameExchange`
|
||||||
|
- record telemetry
|
||||||
|
- destroy GL resources on the render thread
|
||||||
|
|
||||||
|
It must not:
|
||||||
|
|
||||||
|
- wait for DeckLink
|
||||||
|
- schedule DeckLink frames
|
||||||
|
- block on a system frame slot if only completed unscheduled frames can be dropped
|
||||||
|
- accept arbitrary GL tasks ahead of output frames
|
||||||
|
|
||||||
|
### `RenderCadenceClock`
|
||||||
|
|
||||||
|
Small, testable cadence helper.
|
||||||
|
|
||||||
|
Responsibilities:
|
||||||
|
|
||||||
|
- track target frame duration
|
||||||
|
- return whether a render is due
|
||||||
|
- compute sleep duration
|
||||||
|
- detect overrun/skipped ticks
|
||||||
|
- never speed up to fill buffers
|
||||||
|
|
||||||
|
This should be unit tested without GL.
|
||||||
|
|
||||||
|
### `SimpleMotionRenderer`
|
||||||
|
|
||||||
|
First renderer only.
|
||||||
|
|
||||||
|
Responsibilities:
|
||||||
|
|
||||||
|
- render obvious smooth motion and color changes
|
||||||
|
- produce BGRA8-compatible framebuffer content
|
||||||
|
- make dropped/repeated frames visually obvious
|
||||||
|
|
||||||
|
This intentionally avoids shader-package/runtime complexity.
|
||||||
|
|
||||||
|
### `Bgra8ReadbackPipeline`
|
||||||
|
|
||||||
|
Owns output framebuffer and BGRA8 readback orchestration.
|
||||||
|
|
||||||
|
Responsibilities:
|
||||||
|
|
||||||
|
- configure render target dimensions
|
||||||
|
- render into an RGBA8/BGRA-compatible texture
|
||||||
|
- coordinate `PboReadbackRing`
|
||||||
|
- publish completed frames into `SystemFrameExchange`
|
||||||
|
|
||||||
|
### `PboReadbackRing`
|
||||||
|
|
||||||
|
Owns PBO/fence state.
|
||||||
|
|
||||||
|
Responsibilities:
|
||||||
|
|
||||||
|
- queue readback into the next free PBO slot
|
||||||
|
- poll completed fences with zero timeout
|
||||||
|
- map/copy completed PBOs into provided system-memory slots
|
||||||
|
- count PBO misses
|
||||||
|
- clean up fences/PBOs on render thread
|
||||||
|
|
||||||
|
This is GL-backed, but the state model should be small and easy to reason about.
|
||||||
|
|
||||||
|
### `SystemFrameExchange`
|
||||||
|
|
||||||
|
The central handoff between render and video.
|
||||||
|
|
||||||
|
Responsibilities:
|
||||||
|
|
||||||
|
- own system-memory frame buffers
|
||||||
|
- track slot states: `Free`, `Rendering`, `Completed`, `Scheduled`
|
||||||
|
- provide `AcquireForRender()`
|
||||||
|
- provide `PublishCompleted()`
|
||||||
|
- provide `ConsumeCompletedForSchedule()`
|
||||||
|
- provide `ReleaseScheduledByBytes()`
|
||||||
|
- drop oldest completed unscheduled frame when render needs a slot
|
||||||
|
- expose metrics
|
||||||
|
|
||||||
|
This should be unit tested heavily.
|
||||||
|
|
||||||
|
### `DeckLinkOutput`
|
||||||
|
|
||||||
|
Thin wrapper around `DeckLinkSession` for output-only use.
|
||||||
|
|
||||||
|
Responsibilities:
|
||||||
|
|
||||||
|
- discover/select output mode
|
||||||
|
- configure output callback
|
||||||
|
- prepare output schedule
|
||||||
|
- schedule app-owned system-memory frames
|
||||||
|
- start scheduled playback
|
||||||
|
- stop/release resources
|
||||||
|
- expose actual DeckLink buffered count
|
||||||
|
|
||||||
|
No input support in the first version.
|
||||||
|
|
||||||
|
### `DeckLinkOutputThread`
|
||||||
|
|
||||||
|
Owns playout scheduling loop.
|
||||||
|
|
||||||
|
Responsibilities:
|
||||||
|
|
||||||
|
- keep scheduled depth near target
|
||||||
|
- consume completed frames from `SystemFrameExchange`
|
||||||
|
- schedule them through `DeckLinkOutput`
|
||||||
|
- release frame if scheduling fails
|
||||||
|
- sleep briefly when scheduled buffer is full or no completed frame exists
|
||||||
|
|
||||||
|
It must not render.
|
||||||
|
|
||||||
|
### `CadenceTelemetry`
|
||||||
|
|
||||||
|
Owns counters, not policy.
|
||||||
|
|
||||||
|
Initial counters:
|
||||||
|
|
||||||
|
- rendered frames
|
||||||
|
- completed readback frames
|
||||||
|
- scheduled frames
|
||||||
|
- completion count
|
||||||
|
- completed-frame drops
|
||||||
|
- acquire misses
|
||||||
|
- schedule underruns
|
||||||
|
- PBO queue misses
|
||||||
|
- DeckLink late count
|
||||||
|
- DeckLink dropped count
|
||||||
|
- free/rendering/completed/scheduled slot counts
|
||||||
|
- actual DeckLink buffered frames
|
||||||
|
|
||||||
|
### `TelemetryHealthMonitor`
|
||||||
|
|
||||||
|
Samples cadence telemetry once per interval and logs only health events.
|
||||||
|
|
||||||
|
Normal telemetry is available through the HTTP state endpoint. The console should not receive a healthy once-per-second cadence line.
|
||||||
|
|
||||||
|
Health events:
|
||||||
|
|
||||||
|
- warning when DeckLink late/dropped-frame counters increase
|
||||||
|
- warning when schedule failures increase
|
||||||
|
- error when app/DeckLink output buffering is starved
|
||||||
|
|
||||||
|
## Startup Sequence
|
||||||
|
|
||||||
|
Target first-version startup:
|
||||||
|
|
||||||
|
```text
|
||||||
|
main
|
||||||
|
-> load AppConfig through AppConfigProvider
|
||||||
|
-> initialize COM
|
||||||
|
-> create SystemFrameExchange
|
||||||
|
-> start RenderThread
|
||||||
|
-> wait for completed frame warmup
|
||||||
|
-> optionally discover/select/configure DeckLink output
|
||||||
|
-> if DeckLink is available:
|
||||||
|
-> start DeckLinkOutputThread
|
||||||
|
-> wait for scheduled depth warmup
|
||||||
|
-> DeckLinkOutput start scheduled playback
|
||||||
|
-> if DeckLink is unavailable:
|
||||||
|
-> continue without video output
|
||||||
|
-> start TelemetryHealthMonitor
|
||||||
|
-> start HttpControlServer
|
||||||
|
-> wait for Enter
|
||||||
|
```
|
||||||
|
|
||||||
|
Shutdown:
|
||||||
|
|
||||||
|
```text
|
||||||
|
stop HttpControlServer
|
||||||
|
stop TelemetryHealthMonitor
|
||||||
|
stop DeckLinkOutputThread
|
||||||
|
DeckLinkOutput stop playback
|
||||||
|
stop RenderThread
|
||||||
|
DeckLinkOutput release resources
|
||||||
|
release COM
|
||||||
|
```
|
||||||
|
|
||||||
|
## First Milestone: Modular Probe Equivalent
|
||||||
|
|
||||||
|
This is the only goal for the initial implementation.
|
||||||
|
|
||||||
|
Feature set:
|
||||||
|
|
||||||
|
- console app
|
||||||
|
- output-only DeckLink
|
||||||
|
- no input
|
||||||
|
- hidden GL context
|
||||||
|
- simple motion renderer
|
||||||
|
- BGRA8 only
|
||||||
|
- PBO async readback
|
||||||
|
- latest-N system-memory frame exchange
|
||||||
|
- warmup before playback
|
||||||
|
- one-line telemetry
|
||||||
|
|
||||||
|
Acceptance:
|
||||||
|
|
||||||
|
- visible DeckLink output is smooth
|
||||||
|
- `renderFps` near selected cadence
|
||||||
|
- `scheduleFps` near selected cadence
|
||||||
|
- scheduled count/decklink buffered count stable around 4
|
||||||
|
- no continuous late/drop count
|
||||||
|
- no continuous PBO misses
|
||||||
|
- behavior matches or exceeds `DeckLinkRenderCadenceProbe`
|
||||||
|
|
||||||
|
## Second Milestone: Testable Core
|
||||||
|
|
||||||
|
Before porting compositor features, add tests for non-GL/non-DeckLink pieces.
|
||||||
|
|
||||||
|
Test targets:
|
||||||
|
|
||||||
|
- `SystemFrameExchangeTests`
|
||||||
|
- `RenderCadenceClockTests`
|
||||||
|
- `CadenceTelemetryTests`
|
||||||
|
|
||||||
|
Important cases:
|
||||||
|
|
||||||
|
- slot lifecycle transitions
|
||||||
|
- scheduled slots are protected
|
||||||
|
- completed unscheduled frames can be dropped
|
||||||
|
- stale handles/generations are rejected
|
||||||
|
- cadence does not speed up to refill buffers
|
||||||
|
- cadence records overrun/skipped ticks
|
||||||
|
|
||||||
|
## Third Milestone: Replace Simple Renderer With Render Interface
|
||||||
|
|
||||||
|
Add an interface around frame rendering:
|
||||||
|
|
||||||
|
```text
|
||||||
|
IRenderScene
|
||||||
|
-> InitializeGl()
|
||||||
|
-> RenderFrame(frameIndex, time)
|
||||||
|
-> ShutdownGl()
|
||||||
|
```
|
||||||
|
|
||||||
|
The first implementation remains `SimpleMotionRenderer`.
|
||||||
|
|
||||||
|
This creates the insertion point for shader-package rendering later without changing timing/scheduling.
|
||||||
|
|
||||||
|
## Fourth Milestone: Begin Porting Current App Features
|
||||||
|
|
||||||
|
Port only after the modular probe equivalent is stable.
|
||||||
|
|
||||||
|
Suggested order:
|
||||||
|
|
||||||
|
1. shader package compile/load
|
||||||
|
2. render pass/layer stack drawing
|
||||||
|
3. runtime snapshot input to renderer
|
||||||
|
4. live state overlays
|
||||||
|
5. control services
|
||||||
|
6. persistence/runtime store
|
||||||
|
7. preview from system-memory frames
|
||||||
|
8. screenshot from system-memory frames
|
||||||
|
9. input capture via CPU latest-frame mailbox
|
||||||
|
|
||||||
|
Each port must preserve the rule that the render thread cadence is primary.
|
||||||
|
|
||||||
|
## What Not To Port Early
|
||||||
|
|
||||||
|
Do not port these until the output spine is proven:
|
||||||
|
|
||||||
|
- DeckLink input
|
||||||
|
- preview GL presentation
|
||||||
|
- screenshot GL readback
|
||||||
|
- HTTP/OSC control services
|
||||||
|
- shader hot reload
|
||||||
|
- persistence
|
||||||
|
- runtime state JSON/open API
|
||||||
|
- complex telemetry/event dispatch
|
||||||
|
|
||||||
|
These are useful, but they are exactly the kinds of features that can accidentally reintroduce timing coupling.
|
||||||
|
|
||||||
|
## Build Plan
|
||||||
|
|
||||||
|
Initial CMake can follow the probe pattern:
|
||||||
|
|
||||||
|
```cmake
|
||||||
|
set(RENDER_CADENCE_APP_DIR "${CMAKE_CURRENT_SOURCE_DIR}/apps/RenderCadenceCompositor")
|
||||||
|
|
||||||
|
add_executable(RenderCadenceCompositor
|
||||||
|
# selected shared DeckLink/video/gl support files
|
||||||
|
# new modular app files
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
Later, shared source should be split into libraries:
|
||||||
|
|
||||||
|
```text
|
||||||
|
video_shader_decklink
|
||||||
|
video_shader_videoio
|
||||||
|
video_shader_gl_support
|
||||||
|
render_cadence_core
|
||||||
|
```
|
||||||
|
|
||||||
|
Avoid doing that library split before the first modular app works.
|
||||||
|
|
||||||
|
## VS Code Launch
|
||||||
|
|
||||||
|
Add a separate launch profile:
|
||||||
|
|
||||||
|
```text
|
||||||
|
Debug RenderCadenceCompositor
|
||||||
|
```
|
||||||
|
|
||||||
|
Run it as a console app so telemetry remains visible.
|
||||||
|
|
||||||
|
## Documentation
|
||||||
|
|
||||||
|
Add:
|
||||||
|
|
||||||
|
```text
|
||||||
|
apps/RenderCadenceCompositor/README.md
|
||||||
|
```
|
||||||
|
|
||||||
|
The README should record:
|
||||||
|
|
||||||
|
- intended architecture
|
||||||
|
- build/run instructions
|
||||||
|
- expected telemetry
|
||||||
|
- test result notes
|
||||||
|
- differences from the old app
|
||||||
|
- differences from the probe
|
||||||
|
|
||||||
|
## Success Criteria Before Porting More Features
|
||||||
|
|
||||||
|
Do not start feature porting until the new app can run with:
|
||||||
|
|
||||||
|
- stable smooth DeckLink output
|
||||||
|
- stable target scheduled depth
|
||||||
|
- stable actual DeckLink buffered count
|
||||||
|
- no regular visible freezes
|
||||||
|
- no steady PBO misses
|
||||||
|
- no steadily increasing late/dropped completions
|
||||||
|
- focus/minimize changes do not affect output cadence
|
||||||
|
- clean shutdown without hangs
|
||||||
|
|
||||||
|
This gives us a clean foundation. Once this is true, every feature added later has to prove it does not damage the spine.
|
||||||
154
docs/RENDER_CADENCE_GOLDEN_RULES.md
Normal file
154
docs/RENDER_CADENCE_GOLDEN_RULES.md
Normal file
@@ -0,0 +1,154 @@
|
|||||||
|
# Render Cadence Golden Rules
|
||||||
|
|
||||||
|
These are the non-negotiable rules for the new render-cadence architecture.
|
||||||
|
|
||||||
|
They exist because the old app drifted into a place where DeckLink timing, render work, shader build work, state coordination, readback, and recovery behavior all influenced each other. The new app should stay boring, explicit, and easy to reason about.
|
||||||
|
|
||||||
|
## 1. The Render Thread Owns Its GL Context
|
||||||
|
|
||||||
|
Only the render thread may bind and use its primary OpenGL context.
|
||||||
|
|
||||||
|
Allowed on the render thread:
|
||||||
|
|
||||||
|
- GL resource creation and destruction for resources it owns
|
||||||
|
- GL shader/program swap from an already-prepared GL program
|
||||||
|
- drawing the next frame
|
||||||
|
- async readback queueing and completion polling
|
||||||
|
- publishing completed system-memory frames
|
||||||
|
|
||||||
|
Not allowed on the render thread:
|
||||||
|
|
||||||
|
- Slang compiler invocation
|
||||||
|
- manifest scanning/parsing
|
||||||
|
- filesystem discovery
|
||||||
|
- image/font/LUT decoding
|
||||||
|
- persistence
|
||||||
|
- network/API/OSC handling
|
||||||
|
- DeckLink scheduling
|
||||||
|
- blocking console logging
|
||||||
|
- config file discovery or parsing
|
||||||
|
|
||||||
|
If GL preparation happens off-thread, use an explicit shared-context GL prepare thread. Do not smuggle non-render work back into the cadence loop.
|
||||||
|
|
||||||
|
## 2. Render Cadence Does Not Chase Buffers
|
||||||
|
|
||||||
|
The render thread runs at the selected render cadence.
|
||||||
|
|
||||||
|
It must not speed up to fill a DeckLink/system-memory buffer, and it must not slow down because a consumer is late. If the GPU is genuinely overloaded, record that as render overrun telemetry.
|
||||||
|
|
||||||
|
Buffers absorb timing differences. They do not control render cadence.
|
||||||
|
|
||||||
|
## 3. Video I/O Never Renders
|
||||||
|
|
||||||
|
DeckLink output consumes already-rendered system-memory frames.
|
||||||
|
|
||||||
|
The output/scheduling side may:
|
||||||
|
|
||||||
|
- schedule completed frames
|
||||||
|
- release frames after DeckLink completion
|
||||||
|
- report late/dropped/schedule telemetry
|
||||||
|
- record app-side poll misses
|
||||||
|
|
||||||
|
It must not:
|
||||||
|
|
||||||
|
- render fallback frames
|
||||||
|
- invoke GL
|
||||||
|
- compile shaders
|
||||||
|
- block the render cadence waiting for DeckLink
|
||||||
|
|
||||||
|
If no completed frame is available, record the miss and keep the ownership boundary intact.
|
||||||
|
|
||||||
|
DeckLink input is also an edge, not a renderer. It may capture/copy the latest supported CPU frame into an input mailbox and update input telemetry, but it must not call GL, schedule output, compile shaders, or drive render cadence. Format decode that requires GL belongs to the render-owned input upload path.
|
||||||
|
|
||||||
|
## 4. Runtime Build Work Produces Artifacts
|
||||||
|
|
||||||
|
Runtime shader work is split into two phases:
|
||||||
|
|
||||||
|
1. CPU/build phase outside the render thread
|
||||||
|
2. shared-context GL preparation outside the render thread where practical
|
||||||
|
3. GL program swap on the render thread
|
||||||
|
|
||||||
|
The CPU/build phase may parse manifests, invoke Slang, validate package shape, and prepare CPU-side data.
|
||||||
|
|
||||||
|
The render thread receives completed render-layer artifacts, asks the shared-context prepare worker to compile/link changed GL programs, and only swaps in prepared programs at a frame boundary. A failed artifact or failed GL preparation must not disturb the current renderer.
|
||||||
|
|
||||||
|
The display/render layer model is app-owned. It may track requested shaders, build state, display metadata, and render-ready artifacts, but it must not perform GL work or drive render cadence directly.
|
||||||
|
|
||||||
|
## 5. No Hidden Blocking In The Cadence Path
|
||||||
|
|
||||||
|
The render loop must not do work with unbounded or OS-dependent latency.
|
||||||
|
|
||||||
|
Examples to avoid:
|
||||||
|
|
||||||
|
- file reads
|
||||||
|
- directory scans
|
||||||
|
- image decoding
|
||||||
|
- process launches
|
||||||
|
- waits on worker threads
|
||||||
|
- blocking locks around slow code
|
||||||
|
- synchronous GPU readback waits
|
||||||
|
- console I/O
|
||||||
|
|
||||||
|
Short mutex use for exchanging small already-prepared objects is acceptable. Holding a lock while doing heavy work is not.
|
||||||
|
|
||||||
|
## 6. System Memory Frames Are A Handoff, Not A Render Driver
|
||||||
|
|
||||||
|
The system-memory frame exchange stores the latest rendered frames and protects frames scheduled to DeckLink.
|
||||||
|
|
||||||
|
It may drop old completed, unscheduled frames when the render thread needs a free slot. It must never force the render thread to wait for the output side to consume a frame.
|
||||||
|
|
||||||
|
## 7. Startup Uses Warmup, Not Burst Rendering
|
||||||
|
|
||||||
|
DeckLink playback starts only after the render thread has produced enough real frames for preroll.
|
||||||
|
|
||||||
|
Warmup should happen at normal render cadence. Do not temporarily accelerate the renderer to fill buffers.
|
||||||
|
|
||||||
|
## 8. Telemetry Must Name Ownership Clearly
|
||||||
|
|
||||||
|
Counters should say which subsystem had the event.
|
||||||
|
|
||||||
|
Good examples:
|
||||||
|
|
||||||
|
- `renderFps`
|
||||||
|
- `scheduleFps`
|
||||||
|
- `completedPollMisses`
|
||||||
|
- `scheduleFailures`
|
||||||
|
- `decklinkBuffered`
|
||||||
|
- `inputCaptureFps`
|
||||||
|
- `inputSubmitMs`
|
||||||
|
- `inputUploadMs`
|
||||||
|
- `inputConvertMs`
|
||||||
|
- `shaderCommitted`
|
||||||
|
- `shaderFailures`
|
||||||
|
|
||||||
|
Avoid ambiguous names like `underrun` unless it is clear whether it means app-ready underrun, DeckLink buffered-frame underrun, render overrun, or schedule failure.
|
||||||
|
|
||||||
|
## 9. Keep Files Small And Role-Based
|
||||||
|
|
||||||
|
A file should have one clear reason to change.
|
||||||
|
|
||||||
|
Preferred boundaries:
|
||||||
|
|
||||||
|
- app orchestration
|
||||||
|
- render cadence/thread ownership
|
||||||
|
- GL rendering
|
||||||
|
- runtime artifact build/bridge
|
||||||
|
- app-owned display/render layer model
|
||||||
|
- parameter packing
|
||||||
|
- system-memory frame exchange
|
||||||
|
- DeckLink output scheduling
|
||||||
|
- telemetry
|
||||||
|
- local control/API edge
|
||||||
|
- config loading
|
||||||
|
- JSON presentation/serialization
|
||||||
|
- logging
|
||||||
|
|
||||||
|
If a file starts coordinating multiple subsystems and doing detailed work for each of them, split it before it becomes the new old app.
|
||||||
|
|
||||||
|
## 10. Prefer Explicit Unsupported States
|
||||||
|
|
||||||
|
If a feature needs storage, timing behavior, or ownership we have not designed yet, reject it clearly.
|
||||||
|
|
||||||
|
For example, in the current new app it is better to reject texture/LUT/text/temporal/feedback shaders than to quietly load files or allocate history state on the render thread.
|
||||||
|
|
||||||
|
Unsupported is healthy when it protects the architecture.
|
||||||
448
docs/RENDER_THREAD_OWNERSHIP_PLAN.md
Normal file
448
docs/RENDER_THREAD_OWNERSHIP_PLAN.md
Normal file
@@ -0,0 +1,448 @@
|
|||||||
|
# Render Thread Ownership Plan
|
||||||
|
|
||||||
|
This plan describes how to make the main compositor behave like the successful `DeckLinkRenderCadenceProbe`: one render cadence owner, one GL context owner, no unrelated work able to interrupt output frame production.
|
||||||
|
|
||||||
|
The goal is not just "all GL calls happen on one thread". The current app mostly does that during runtime already. The real goal is:
|
||||||
|
|
||||||
|
- the output render thread owns its GL context for its whole lifetime
|
||||||
|
- output cadence is driven by the render thread, not by DeckLink completion timing
|
||||||
|
- non-output GL work cannot sit ahead of output frames
|
||||||
|
- callers cannot block the render thread while waiting for synchronous answers
|
||||||
|
- DeckLink scheduling consumes completed system-memory frames and never causes rendering
|
||||||
|
|
||||||
|
## Current Risk Points
|
||||||
|
|
||||||
|
The current main app still has several ways to interrupt output cadence.
|
||||||
|
|
||||||
|
### Shared GL Executor
|
||||||
|
|
||||||
|
`RenderEngine` owns the GL context during runtime, but it acts as a general task executor.
|
||||||
|
|
||||||
|
The same queue/path can run:
|
||||||
|
|
||||||
|
- output frame render
|
||||||
|
- input upload
|
||||||
|
- preview present
|
||||||
|
- screenshot capture
|
||||||
|
- render resets
|
||||||
|
- shader/program commits
|
||||||
|
- resource resize
|
||||||
|
- state clearing
|
||||||
|
|
||||||
|
That means output frames are not guaranteed to be the next GL work item at the selected frame time.
|
||||||
|
|
||||||
|
### Synchronous Output Render Request
|
||||||
|
|
||||||
|
`VideoBackend` drives output production from its output producer thread, then calls:
|
||||||
|
|
||||||
|
```text
|
||||||
|
VideoBackend
|
||||||
|
-> OpenGLVideoIOBridge::RenderScheduledFrame
|
||||||
|
-> RenderEngine::RequestOutputFrame
|
||||||
|
-> TryInvokeOnRenderThread
|
||||||
|
```
|
||||||
|
|
||||||
|
That makes output production a request/response interaction. The producer waits for the render thread, and the render thread is still shared with other work.
|
||||||
|
|
||||||
|
### Input Upload Shares Output Context
|
||||||
|
|
||||||
|
DeckLink input capture currently flows into:
|
||||||
|
|
||||||
|
```text
|
||||||
|
VideoBackend::HandleInputFrame
|
||||||
|
-> OpenGLVideoIOBridge::UploadInputFrame
|
||||||
|
-> RenderEngine::QueueInputFrame
|
||||||
|
-> render thread upload
|
||||||
|
```
|
||||||
|
|
||||||
|
Even with coalescing, input upload can consume render-thread time and GPU bandwidth directly before output rendering.
|
||||||
|
|
||||||
|
### Preview And Screenshot Share Output Context
|
||||||
|
|
||||||
|
Preview and screenshot are lower-priority features, but today they still execute on the render thread.
|
||||||
|
|
||||||
|
Preview is best-effort at the caller side, but once queued it can still occupy the same context. Screenshot capture can be more expensive because it performs readback and CPU-side image preparation.
|
||||||
|
|
||||||
|
### Startup Context Ownership Is Transitional
|
||||||
|
|
||||||
|
The Win32 startup path creates and binds the GL context before `RenderEngine::StartRenderThread()`.
|
||||||
|
|
||||||
|
That is acceptable as a transitional state, but the final model should make context ownership explicit:
|
||||||
|
|
||||||
|
- bootstrap thread creates the window/context
|
||||||
|
- bootstrap thread releases it
|
||||||
|
- render thread binds it
|
||||||
|
- only render thread initializes GL resources
|
||||||
|
- only render thread destroys GL resources
|
||||||
|
|
||||||
|
### Render Callback Re-enters App State
|
||||||
|
|
||||||
|
`OpenGLRenderPipeline::RenderFrame()` calls a callback into `OpenGLComposite::renderEffect()`.
|
||||||
|
|
||||||
|
That callback builds `RenderFrameInput`, resolves frame state, drains runtime live state, and then calls back into `RenderEngine` to draw the prepared frame.
|
||||||
|
|
||||||
|
This works, but it means the output render path still reaches up into app/runtime code at frame time.
|
||||||
|
|
||||||
|
## Target Runtime Shape
|
||||||
|
|
||||||
|
The main app should match this ownership model:
|
||||||
|
|
||||||
|
```text
|
||||||
|
runtime/control threads
|
||||||
|
-> publish snapshots, live overlays, reset requests, shader-build results
|
||||||
|
-> never call GL
|
||||||
|
|
||||||
|
render cadence thread
|
||||||
|
-> sole owner of output GL context
|
||||||
|
-> wakes at selected render cadence
|
||||||
|
-> samples latest render input/state
|
||||||
|
-> renders one frame
|
||||||
|
-> queues async readback/copies completed readback into system-memory slot
|
||||||
|
-> publishes completed frame to latest-N output buffer
|
||||||
|
|
||||||
|
video output thread
|
||||||
|
-> consumes completed system-memory frames
|
||||||
|
-> schedules DeckLink frames to target buffer depth
|
||||||
|
-> processes completion results
|
||||||
|
-> never calls GL
|
||||||
|
|
||||||
|
optional input upload path
|
||||||
|
-> writes latest input frame into CPU-side latest-frame buffer
|
||||||
|
-> render thread imports/uploads at a controlled point in its frame
|
||||||
|
|
||||||
|
preview/screenshot path
|
||||||
|
-> consumes already-rendered output/system-memory frame when possible
|
||||||
|
-> never interrupts output render cadence
|
||||||
|
```
|
||||||
|
|
||||||
|
## Non-Negotiable Rules
|
||||||
|
|
||||||
|
- The render thread never waits for DeckLink.
|
||||||
|
- DeckLink callbacks never render.
|
||||||
|
- Runtime/control threads never directly execute GL.
|
||||||
|
- Preview and screenshot never execute ahead of output frames.
|
||||||
|
- Input upload is never a separate urgent GL task ahead of output render.
|
||||||
|
- Shader/resource commits are applied only at a frame boundary.
|
||||||
|
- Telemetry on the hot path must be lock-light or try-lock only.
|
||||||
|
- The render thread cadence does not speed up to refill buffers.
|
||||||
|
- If output work overruns, the render thread records the overrun and resumes the selected cadence policy.
|
||||||
|
|
||||||
|
## Implementation Plan
|
||||||
|
|
||||||
|
### 1. Add Thread/Context Ownership Guards
|
||||||
|
|
||||||
|
Add explicit render-thread ownership checks around all GL entry points.
|
||||||
|
|
||||||
|
Deliverables:
|
||||||
|
|
||||||
|
- `RenderEngine` exposes `IsOnRenderThread()` for assertions/tests.
|
||||||
|
- GL-facing classes get debug-only owner checks where practical.
|
||||||
|
- wrong-thread GL access becomes a counted telemetry warning, not just `OutputDebugStringA`.
|
||||||
|
- tests cover that public request methods do not execute GL directly.
|
||||||
|
|
||||||
|
Acceptance:
|
||||||
|
|
||||||
|
- every `RenderEngine` public method is classified as either request-only, lifecycle-only, or render-thread-only.
|
||||||
|
- render-thread-only methods are private or guarded.
|
||||||
|
- no normal runtime caller can accidentally invoke GL work inline.
|
||||||
|
|
||||||
|
### 2. Move GL Initialization Fully Onto The Render Thread
|
||||||
|
|
||||||
|
Start the render thread before compiling shaders and initializing GL resources.
|
||||||
|
|
||||||
|
Current startup does:
|
||||||
|
|
||||||
|
```text
|
||||||
|
InitOpenGLState()
|
||||||
|
-> CompileDecodeShader
|
||||||
|
-> CompileOutputPackShader
|
||||||
|
-> InitializeResources
|
||||||
|
-> CompileLayerPrograms
|
||||||
|
StartRenderThread()
|
||||||
|
```
|
||||||
|
|
||||||
|
Move toward:
|
||||||
|
|
||||||
|
```text
|
||||||
|
create context on Win32 thread
|
||||||
|
release context on Win32 thread
|
||||||
|
StartRenderThread()
|
||||||
|
render thread binds context
|
||||||
|
render thread initializes extensions, shaders, resources
|
||||||
|
```
|
||||||
|
|
||||||
|
Deliverables:
|
||||||
|
|
||||||
|
- a single `RenderEngine::StartAndInitialize(RenderInitializationConfig)` path.
|
||||||
|
- GL extension resolution happens on the render thread.
|
||||||
|
- shader/resource initialization is a render-thread startup phase.
|
||||||
|
- `RenderEngine` destructor only destroys resources on the render thread.
|
||||||
|
|
||||||
|
Acceptance:
|
||||||
|
|
||||||
|
- after `StartRenderThread()`, no non-render thread binds or uses the app GL context.
|
||||||
|
- shutdown order is deterministic: stop video output, stop render cadence, destroy GL resources, release context.
|
||||||
|
|
||||||
|
### 3. Replace Synchronous Output Render Requests With Render-Owned Cadence
|
||||||
|
|
||||||
|
Move output cadence out of `VideoBackend` and into the render system.
|
||||||
|
|
||||||
|
Current:
|
||||||
|
|
||||||
|
```text
|
||||||
|
VideoBackend output producer
|
||||||
|
-> cadence tick
|
||||||
|
-> acquire output slot
|
||||||
|
-> synchronous render-thread request
|
||||||
|
```
|
||||||
|
|
||||||
|
Target:
|
||||||
|
|
||||||
|
```text
|
||||||
|
RenderEngine output cadence loop
|
||||||
|
-> cadence tick
|
||||||
|
-> acquire/free output slot through a non-blocking frame-sink interface
|
||||||
|
-> render frame
|
||||||
|
-> publish completed frame
|
||||||
|
```
|
||||||
|
|
||||||
|
Deliverables:
|
||||||
|
|
||||||
|
- introduce `RenderedFrameSink` or similar interface owned by video output.
|
||||||
|
- render thread pulls/claims a free system-memory slot without waiting.
|
||||||
|
- if no free slot exists, render thread drops/recycles the oldest unscheduled completed frame or records backpressure without blocking.
|
||||||
|
- remove `RenderEngine::RequestOutputFrame()` from the steady-state output path.
|
||||||
|
|
||||||
|
Acceptance:
|
||||||
|
|
||||||
|
- output rendering continues even if DeckLink completion is delayed.
|
||||||
|
- no `std::future` wait exists in the output cadence path.
|
||||||
|
- `VideoBackend` no longer owns the producer render loop; it owns scheduling/completion only.
|
||||||
|
|
||||||
|
### 4. Make The Render Thread A Frame Loop, Not A Task Queue
|
||||||
|
|
||||||
|
Keep a command mailbox, but process it only at safe frame-boundary points.
|
||||||
|
|
||||||
|
Frame loop:
|
||||||
|
|
||||||
|
```text
|
||||||
|
while running:
|
||||||
|
wait until next render timestamp
|
||||||
|
apply bounded frame-boundary commands
|
||||||
|
sample latest frame input/state
|
||||||
|
upload latest input frame if enabled and budget allows
|
||||||
|
render output frame
|
||||||
|
queue/consume readback
|
||||||
|
publish completed frame
|
||||||
|
record timings
|
||||||
|
```
|
||||||
|
|
||||||
|
Command classes:
|
||||||
|
|
||||||
|
- frame-boundary commands: reset temporal history, reset shader feedback, commit prepared shader programs
|
||||||
|
- background/low-priority commands: preview, screenshot, diagnostic readback
|
||||||
|
- non-GL commands: state publication, telemetry, persistence
|
||||||
|
|
||||||
|
Deliverables:
|
||||||
|
|
||||||
|
- replace FIFO render task queue with a priority/mailbox model.
|
||||||
|
- output cadence is the loop's main clock.
|
||||||
|
- commands have budget classes and max work per frame.
|
||||||
|
- long commands are deferred rather than blocking the current output tick.
|
||||||
|
|
||||||
|
Acceptance:
|
||||||
|
|
||||||
|
- preview/screenshot cannot run immediately before a due output frame.
|
||||||
|
- reset/shader work is applied between frames and measured.
|
||||||
|
- output render starts within a small jitter window when the GPU is not overrun.
|
||||||
|
|
||||||
|
### 5. Move Input Capture To A CPU Latest-Frame Buffer
|
||||||
|
|
||||||
|
Input capture should not enqueue independent GL upload tasks.
|
||||||
|
|
||||||
|
Target:
|
||||||
|
|
||||||
|
```text
|
||||||
|
DeckLink input callback
|
||||||
|
-> copy/coalesce latest CPU input frame
|
||||||
|
-> return quickly
|
||||||
|
|
||||||
|
render thread frame boundary
|
||||||
|
-> if input version changed, upload latest frame
|
||||||
|
-> render using last successfully uploaded input texture
|
||||||
|
```
|
||||||
|
|
||||||
|
Deliverables:
|
||||||
|
|
||||||
|
- introduce `InputFrameMailbox` with latest-frame semantics.
|
||||||
|
- remove `RenderEngine::QueueInputFrame()` from the callback path.
|
||||||
|
- render thread owns the upload moment.
|
||||||
|
- if upload would exceed budget, render thread can reuse the previous input texture and record an input-upload skip.
|
||||||
|
|
||||||
|
Acceptance:
|
||||||
|
|
||||||
|
- input capture enabled does not create arbitrary render-thread tasks.
|
||||||
|
- output cadence remains stable when input frames arrive.
|
||||||
|
- telemetry separates input-frame arrival, upload count, upload skips, and upload cost.
|
||||||
|
|
||||||
|
### 6. Move Preview To A Consumer Path
|
||||||
|
|
||||||
|
Preview should consume the latest completed output image instead of asking the output GL context to present.
|
||||||
|
|
||||||
|
Options:
|
||||||
|
|
||||||
|
- CPU preview from latest system-memory output frame.
|
||||||
|
- a separate preview GL context fed asynchronously from completed frames.
|
||||||
|
- a low-priority render-thread blit only when output has measurable slack.
|
||||||
|
|
||||||
|
Recommended first step:
|
||||||
|
|
||||||
|
- use latest system-memory BGRA8 output for the window preview.
|
||||||
|
|
||||||
|
Deliverables:
|
||||||
|
|
||||||
|
- preview reads from latest completed/scheduled output frame copy.
|
||||||
|
- `TryPresentPreview()` no longer queues GL work on the output render thread.
|
||||||
|
- preview FPS throttling remains caller-side.
|
||||||
|
|
||||||
|
Acceptance:
|
||||||
|
|
||||||
|
- forcing preview cannot delay output rendering.
|
||||||
|
- minimizing/focusing the window does not affect output cadence.
|
||||||
|
|
||||||
|
### 7. Move Screenshot To Completed Frame Capture
|
||||||
|
|
||||||
|
Screenshot should capture from the latest completed output frame unless an explicit "exact render capture" mode is requested.
|
||||||
|
|
||||||
|
Deliverables:
|
||||||
|
|
||||||
|
- screenshot request reads the latest system-memory output frame.
|
||||||
|
- PNG write remains async.
|
||||||
|
- optional diagnostic exact-GL screenshot is disabled during live output or explicitly marked disruptive.
|
||||||
|
|
||||||
|
Acceptance:
|
||||||
|
|
||||||
|
- screenshot request does not call `glReadPixels` on the output render context during steady-state playout.
|
||||||
|
|
||||||
|
### 8. Make Shader Commits Frame-Boundary Work
|
||||||
|
|
||||||
|
Prepared shader builds are CPU/background work; GL program commit is still GL work.
|
||||||
|
|
||||||
|
Deliverables:
|
||||||
|
|
||||||
|
- shader build queue produces `PreparedShaderBuild`.
|
||||||
|
- render thread sees latest pending prepared build at a frame boundary.
|
||||||
|
- commit is applied only between frames.
|
||||||
|
- expensive commits can temporarily enter a measured "render reconfigure" state.
|
||||||
|
|
||||||
|
Acceptance:
|
||||||
|
|
||||||
|
- shader commits do not interleave midway through output render.
|
||||||
|
- output timing telemetry records commit duration separately from normal render duration.
|
||||||
|
|
||||||
|
### 9. Split Output Scheduling From Rendering Completely
|
||||||
|
|
||||||
|
`VideoBackend` should become a playout/scheduling owner, not a render producer.
|
||||||
|
|
||||||
|
Target:
|
||||||
|
|
||||||
|
```text
|
||||||
|
RenderEngine
|
||||||
|
-> produces completed frames at render cadence
|
||||||
|
|
||||||
|
VideoBackend
|
||||||
|
-> schedules completed frames up to target DeckLink depth
|
||||||
|
-> processes completions
|
||||||
|
-> releases scheduled slots
|
||||||
|
```
|
||||||
|
|
||||||
|
Deliverables:
|
||||||
|
|
||||||
|
- `VideoBackend` owns `SystemOutputFramePool`, or a new `SystemFrameExchange` owns it between render/video.
|
||||||
|
- render thread publishes completed frames into the exchange.
|
||||||
|
- video output thread schedules from the exchange.
|
||||||
|
- no render calls exist in completion handling or scheduling paths.
|
||||||
|
|
||||||
|
Acceptance:
|
||||||
|
|
||||||
|
- DeckLink buffer depth changes cannot directly cause render-thread wakeups except through non-blocking availability signals.
|
||||||
|
- render cadence can be tested without DeckLink by using a fake frame sink.
|
||||||
|
- video scheduling can be tested without GL by using synthetic frames.
|
||||||
|
|
||||||
|
### 10. Preserve The Probe As The Reference Contract
|
||||||
|
|
||||||
|
The `DeckLinkRenderCadenceProbe` is now the control sample.
|
||||||
|
|
||||||
|
Deliverables:
|
||||||
|
|
||||||
|
- document which main-app components correspond to the probe components.
|
||||||
|
- add a small regression checklist:
|
||||||
|
- render FPS near target
|
||||||
|
- schedule FPS near target
|
||||||
|
- DeckLink buffered frames stable
|
||||||
|
- no late/drop frames
|
||||||
|
- no PBO misses or readback stalls
|
||||||
|
- focus/minimize does not change output cadence
|
||||||
|
|
||||||
|
Acceptance:
|
||||||
|
|
||||||
|
- after each migration step, compare the main app telemetry against the probe's known-good behavior.
|
||||||
|
|
||||||
|
## Suggested Order Of Work
|
||||||
|
|
||||||
|
1. Add ownership guards and classify render methods.
|
||||||
|
2. Move GL initialization/destruction fully onto the render thread.
|
||||||
|
3. Introduce a render-owned cadence loop behind a feature flag.
|
||||||
|
4. Add a frame-sink/exchange interface between render and video.
|
||||||
|
5. Move output production from `VideoBackend` to the render cadence loop.
|
||||||
|
6. Convert input upload to latest-frame mailbox semantics.
|
||||||
|
7. Move preview to completed-frame consumption.
|
||||||
|
8. Move screenshot to completed-frame capture.
|
||||||
|
9. Convert shader commits/resets to frame-boundary mailbox commands.
|
||||||
|
10. Remove old synchronous output render request path.
|
||||||
|
|
||||||
|
## Feature Flags During Migration
|
||||||
|
|
||||||
|
Use flags only to keep testing safe, not as long-term compatibility layers.
|
||||||
|
|
||||||
|
Suggested flags:
|
||||||
|
|
||||||
|
```text
|
||||||
|
VST_RENDER_CADENCE_OWNER=render_thread
|
||||||
|
VST_DISABLE_INPUT_CAPTURE=1
|
||||||
|
VST_PREVIEW_SOURCE=system_frame
|
||||||
|
VST_SCREENSHOT_SOURCE=system_frame
|
||||||
|
```
|
||||||
|
|
||||||
|
Remove each flag once the new behavior is proven and becomes the only supported path.
|
||||||
|
|
||||||
|
## Telemetry Needed
|
||||||
|
|
||||||
|
Add or preserve counters for:
|
||||||
|
|
||||||
|
- render tick jitter
|
||||||
|
- render tick overrun
|
||||||
|
- output render duration
|
||||||
|
- GL command mailbox depth by class
|
||||||
|
- frame-boundary command duration
|
||||||
|
- input upload duration and skips
|
||||||
|
- readback queue/consume duration
|
||||||
|
- completed system-memory frame depth
|
||||||
|
- scheduled DeckLink frame depth
|
||||||
|
- DeckLink actual buffered frames
|
||||||
|
- preview frames consumed
|
||||||
|
- screenshot requests served from system memory
|
||||||
|
|
||||||
|
The key metric is whether output render starts on time. Buffer depth alone is not enough; a full buffer can still contain stale or repeated frames.
|
||||||
|
|
||||||
|
## Completion Definition
|
||||||
|
|
||||||
|
This work is complete when:
|
||||||
|
|
||||||
|
- the output render thread owns the app GL context from initialization through shutdown
|
||||||
|
- output rendering is driven by the render thread's selected frame cadence
|
||||||
|
- no non-output task can run ahead of a due output frame
|
||||||
|
- `VideoBackend` never asks the render thread to render synchronously
|
||||||
|
- DeckLink scheduling consumes already completed system-memory frames
|
||||||
|
- input upload, preview, screenshot, shader commits, and resets are all frame-boundary, mailbox, or consumer-side operations
|
||||||
|
- main-app telemetry approaches the cadence probe behavior under the same output mode
|
||||||
@@ -6,17 +6,20 @@ info:
|
|||||||
REST API exposed by the local Video Shader Toys control server.
|
REST API exposed by the local Video Shader Toys control server.
|
||||||
|
|
||||||
The API is intended for local control tools and the bundled React UI. All mutating
|
The API is intended for local control tools and the bundled React UI. All mutating
|
||||||
endpoints return a small action result object. Successful mutating requests also
|
endpoints return a small action result object.
|
||||||
broadcast the latest runtime state over the `/ws` WebSocket.
|
|
||||||
|
|
||||||
WebSocket state streaming is not described by OpenAPI; connect to `ws://127.0.0.1:{port}/ws`
|
RenderCadenceCompositor serves `/api/state` for snapshots and `/ws` for local
|
||||||
to receive full runtime state JSON messages whenever state changes.
|
WebSocket state updates consumed by the bundled control UI.
|
||||||
servers:
|
servers:
|
||||||
- url: http://127.0.0.1:8080
|
- url: http://127.0.0.1:8080
|
||||||
description: Default local control server
|
description: Default local control server
|
||||||
tags:
|
tags:
|
||||||
- name: State
|
- name: State
|
||||||
description: Runtime state and status.
|
description: Runtime state and status.
|
||||||
|
- name: Static
|
||||||
|
description: Bundled control UI and static assets served by the local host.
|
||||||
|
- name: Docs
|
||||||
|
description: OpenAPI and Swagger UI documentation served by the local host.
|
||||||
- name: Layers
|
- name: Layers
|
||||||
description: Layer stack control.
|
description: Layer stack control.
|
||||||
- name: Stack Presets
|
- name: Stack Presets
|
||||||
@@ -24,6 +27,146 @@ tags:
|
|||||||
- name: Runtime
|
- name: Runtime
|
||||||
description: Runtime actions.
|
description: Runtime actions.
|
||||||
paths:
|
paths:
|
||||||
|
/:
|
||||||
|
get:
|
||||||
|
tags: [Static]
|
||||||
|
summary: Serve the bundled control UI
|
||||||
|
description: Returns the built React control UI `index.html` from `ui/dist`.
|
||||||
|
operationId: getControlUiRoot
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: Control UI HTML.
|
||||||
|
content:
|
||||||
|
text/html:
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
"404":
|
||||||
|
description: UI bundle was not found.
|
||||||
|
content:
|
||||||
|
text/plain:
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
/index.html:
|
||||||
|
get:
|
||||||
|
tags: [Static]
|
||||||
|
summary: Serve the bundled control UI index file
|
||||||
|
description: Returns the built React control UI `index.html` from `ui/dist`.
|
||||||
|
operationId: getControlUiIndex
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: Control UI HTML.
|
||||||
|
content:
|
||||||
|
text/html:
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
"404":
|
||||||
|
description: UI bundle was not found.
|
||||||
|
content:
|
||||||
|
text/plain:
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
/assets/{assetPath}:
|
||||||
|
get:
|
||||||
|
tags: [Static]
|
||||||
|
summary: Serve a bundled control UI asset
|
||||||
|
description: Serves files from `ui/dist/assets`. The server rejects unsafe relative paths and guesses the content type from the file extension.
|
||||||
|
operationId: getControlUiAsset
|
||||||
|
parameters:
|
||||||
|
- name: assetPath
|
||||||
|
in: path
|
||||||
|
required: true
|
||||||
|
description: Relative asset path below `ui/dist/assets`.
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: Static asset.
|
||||||
|
content:
|
||||||
|
text/javascript:
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
text/css:
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
image/svg+xml:
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
image/png:
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
format: binary
|
||||||
|
text/plain:
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
"404":
|
||||||
|
description: Asset was not found or the path was unsafe.
|
||||||
|
content:
|
||||||
|
text/plain:
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
/docs:
|
||||||
|
get:
|
||||||
|
tags: [Docs]
|
||||||
|
summary: Serve Swagger UI
|
||||||
|
description: Returns a small Swagger UI page pointed at `/docs/openapi.yaml`.
|
||||||
|
operationId: getSwaggerUi
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: Swagger UI HTML.
|
||||||
|
content:
|
||||||
|
text/html:
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
/docs/:
|
||||||
|
get:
|
||||||
|
tags: [Docs]
|
||||||
|
summary: Serve Swagger UI
|
||||||
|
description: Alias for `/docs`.
|
||||||
|
operationId: getSwaggerUiWithTrailingSlash
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: Swagger UI HTML.
|
||||||
|
content:
|
||||||
|
text/html:
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
/docs/openapi.yaml:
|
||||||
|
get:
|
||||||
|
tags: [Docs]
|
||||||
|
summary: Serve the OpenAPI document
|
||||||
|
operationId: getOpenApiDocumentFromDocs
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: OpenAPI YAML document.
|
||||||
|
content:
|
||||||
|
application/yaml:
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
"404":
|
||||||
|
description: OpenAPI document was not found.
|
||||||
|
content:
|
||||||
|
text/plain:
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
/openapi.yaml:
|
||||||
|
get:
|
||||||
|
tags: [Docs]
|
||||||
|
summary: Serve the OpenAPI document
|
||||||
|
description: Alias for `/docs/openapi.yaml`.
|
||||||
|
operationId: getOpenApiDocument
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: OpenAPI YAML document.
|
||||||
|
content:
|
||||||
|
application/yaml:
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
"404":
|
||||||
|
description: OpenAPI document was not found.
|
||||||
|
content:
|
||||||
|
text/plain:
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
/api/state:
|
/api/state:
|
||||||
get:
|
get:
|
||||||
tags: [State]
|
tags: [State]
|
||||||
@@ -36,6 +179,24 @@ paths:
|
|||||||
application/json:
|
application/json:
|
||||||
schema:
|
schema:
|
||||||
$ref: "#/components/schemas/RuntimeState"
|
$ref: "#/components/schemas/RuntimeState"
|
||||||
|
/ws:
|
||||||
|
get:
|
||||||
|
tags: [State]
|
||||||
|
summary: Stream runtime state over WebSocket
|
||||||
|
description: |
|
||||||
|
Upgrades to a WebSocket connection. The server sends JSON runtime-state
|
||||||
|
snapshots using the same shape as `GET /api/state` whenever the serialized
|
||||||
|
state changes.
|
||||||
|
operationId: streamRuntimeState
|
||||||
|
responses:
|
||||||
|
"101":
|
||||||
|
description: WebSocket protocol upgrade accepted.
|
||||||
|
"400":
|
||||||
|
description: The request was not a valid WebSocket upgrade.
|
||||||
|
content:
|
||||||
|
text/plain:
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
/api/layers/add:
|
/api/layers/add:
|
||||||
post:
|
post:
|
||||||
tags: [Layers]
|
tags: [Layers]
|
||||||
|
|||||||
@@ -40,12 +40,12 @@ float4 sampleWarped(float2 uv, float2 resolution, out bool insideSource)
|
|||||||
insideSource = uv.x >= 0.0 && uv.x <= 1.0 && uv.y >= 0.0 && uv.y <= 1.0;
|
insideSource = uv.x >= 0.0 && uv.x <= 1.0 && uv.y >= 0.0 && uv.y <= 1.0;
|
||||||
|
|
||||||
if (edgeMode == 1)
|
if (edgeMode == 1)
|
||||||
return sampleVideo(clamp(uv, 0.0, 1.0));
|
return sampleLayerInput(clamp(uv, 0.0, 1.0));
|
||||||
if (edgeMode == 2)
|
if (edgeMode == 2)
|
||||||
return sampleVideo(float2(mirroredCoordinate(uv.x), mirroredCoordinate(uv.y)));
|
return sampleLayerInput(float2(mirroredCoordinate(uv.x), mirroredCoordinate(uv.y)));
|
||||||
|
|
||||||
float edgeMask = sourceBoundsMask(uv, resolution);
|
float edgeMask = sourceBoundsMask(uv, resolution);
|
||||||
float4 color = sampleVideo(clamp(uv, 0.0, 1.0));
|
float4 color = sampleLayerInput(clamp(uv, 0.0, 1.0));
|
||||||
return lerp(outsideColor, color, edgeMask);
|
return lerp(outsideColor, color, edgeMask);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
128
tests/RenderCadenceCompositorAppConfigProviderTests.cpp
Normal file
128
tests/RenderCadenceCompositorAppConfigProviderTests.cpp
Normal file
@@ -0,0 +1,128 @@
|
|||||||
|
#include "AppConfigProvider.h"
|
||||||
|
|
||||||
|
#include <filesystem>
|
||||||
|
#include <fstream>
|
||||||
|
#include <iostream>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
int gFailures = 0;
|
||||||
|
|
||||||
|
void Expect(bool condition, const std::string& message)
|
||||||
|
{
|
||||||
|
if (condition)
|
||||||
|
return;
|
||||||
|
|
||||||
|
++gFailures;
|
||||||
|
std::cerr << "FAILED: " << message << "\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
std::filesystem::path WriteConfigFixture()
|
||||||
|
{
|
||||||
|
const std::filesystem::path path = std::filesystem::temp_directory_path() / "render-cadence-compositor-config-test.json";
|
||||||
|
std::ofstream output(path, std::ios::binary);
|
||||||
|
output
|
||||||
|
<< "{\n"
|
||||||
|
<< " \"shaderLibrary\": \"test-shaders\",\n"
|
||||||
|
<< " \"serverPort\": 8181,\n"
|
||||||
|
<< " \"oscBindAddress\": \"127.0.0.1\",\n"
|
||||||
|
<< " \"oscPort\": 9100,\n"
|
||||||
|
<< " \"oscSmoothing\": 0.25,\n"
|
||||||
|
<< " \"inputVideoFormat\": \"720p\",\n"
|
||||||
|
<< " \"inputFrameRate\": \"50\",\n"
|
||||||
|
<< " \"outputVideoFormat\": \"2160p\",\n"
|
||||||
|
<< " \"outputFrameRate\": \"60\",\n"
|
||||||
|
<< " \"autoReload\": false,\n"
|
||||||
|
<< " \"maxTemporalHistoryFrames\": 8,\n"
|
||||||
|
<< " \"previewFps\": 24,\n"
|
||||||
|
<< " \"enableExternalKeying\": true\n"
|
||||||
|
<< "}\n";
|
||||||
|
return path;
|
||||||
|
}
|
||||||
|
|
||||||
|
void TestLoadsRuntimeHostConfig()
|
||||||
|
{
|
||||||
|
using namespace RenderCadenceCompositor;
|
||||||
|
|
||||||
|
const std::filesystem::path path = WriteConfigFixture();
|
||||||
|
AppConfigProvider provider;
|
||||||
|
std::string error;
|
||||||
|
const bool loaded = provider.Load(path, error);
|
||||||
|
const AppConfig& config = provider.Config();
|
||||||
|
|
||||||
|
Expect(loaded, "config loads");
|
||||||
|
Expect(error.empty(), "config load has no error");
|
||||||
|
Expect(provider.LoadedFromFile(), "provider records file load");
|
||||||
|
Expect(config.shaderLibrary == "test-shaders", "shader library loads");
|
||||||
|
Expect(config.http.preferredPort == 8181, "server port loads");
|
||||||
|
Expect(config.oscBindAddress == "127.0.0.1", "OSC bind address loads");
|
||||||
|
Expect(config.oscPort == 9100, "OSC port loads");
|
||||||
|
Expect(config.oscSmoothing == 0.25, "OSC smoothing loads");
|
||||||
|
Expect(config.inputVideoFormat == "720p", "input format loads");
|
||||||
|
Expect(config.inputFrameRate == "50", "input frame rate loads");
|
||||||
|
Expect(config.outputVideoFormat == "2160p", "output format loads");
|
||||||
|
Expect(config.outputFrameRate == "60", "output frame rate loads");
|
||||||
|
Expect(!config.autoReload, "auto reload loads");
|
||||||
|
Expect(config.maxTemporalHistoryFrames == 8, "history length loads");
|
||||||
|
Expect(config.previewFps == 24.0, "preview fps loads");
|
||||||
|
Expect(config.deckLink.externalKeyingEnabled, "external keying loads");
|
||||||
|
|
||||||
|
std::filesystem::remove(path);
|
||||||
|
}
|
||||||
|
|
||||||
|
void TestCommandLineOverrides()
|
||||||
|
{
|
||||||
|
using namespace RenderCadenceCompositor;
|
||||||
|
|
||||||
|
AppConfigProvider provider;
|
||||||
|
const char* argv[] = {
|
||||||
|
"app.exe",
|
||||||
|
"--shader",
|
||||||
|
"solid-color",
|
||||||
|
"--port",
|
||||||
|
"8282"
|
||||||
|
};
|
||||||
|
provider.ApplyCommandLine(5, const_cast<char**>(argv));
|
||||||
|
|
||||||
|
const AppConfig& config = provider.Config();
|
||||||
|
Expect(config.runtimeShaderId == "solid-color", "shader CLI override applies");
|
||||||
|
Expect(config.http.preferredPort == 8282, "port CLI override applies");
|
||||||
|
}
|
||||||
|
|
||||||
|
void TestHelpers()
|
||||||
|
{
|
||||||
|
using namespace RenderCadenceCompositor;
|
||||||
|
|
||||||
|
unsigned width = 0;
|
||||||
|
unsigned height = 0;
|
||||||
|
VideoFormatDimensions("720p", width, height);
|
||||||
|
Expect(width == 1280 && height == 720, "720p dimensions resolve");
|
||||||
|
|
||||||
|
VideoFormatDimensions("2160p", width, height);
|
||||||
|
Expect(width == 3840 && height == 2160, "2160p dimensions resolve");
|
||||||
|
|
||||||
|
const double duration = FrameDurationMillisecondsFromRateString("50");
|
||||||
|
Expect(duration > 19.9 && duration < 20.1, "frame duration parses numeric rate");
|
||||||
|
|
||||||
|
const std::filesystem::path configPath = FindConfigFile();
|
||||||
|
Expect(!configPath.empty(), "default config is discoverable from test working directory");
|
||||||
|
Expect(configPath.filename() == "runtime-host.json", "default config discovery returns runtime-host.json");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int main()
|
||||||
|
{
|
||||||
|
TestLoadsRuntimeHostConfig();
|
||||||
|
TestCommandLineOverrides();
|
||||||
|
TestHelpers();
|
||||||
|
|
||||||
|
if (gFailures != 0)
|
||||||
|
{
|
||||||
|
std::cerr << gFailures << " RenderCadenceCompositorAppConfigProvider test failure(s).\n";
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::cout << "RenderCadenceCompositorAppConfigProvider tests passed.\n";
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
94
tests/RenderCadenceCompositorClockTests.cpp
Normal file
94
tests/RenderCadenceCompositorClockTests.cpp
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
#include "RenderCadenceClock.h"
|
||||||
|
|
||||||
|
#include <chrono>
|
||||||
|
#include <iostream>
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
int gFailures = 0;
|
||||||
|
|
||||||
|
void Expect(bool condition, const char* message)
|
||||||
|
{
|
||||||
|
if (condition)
|
||||||
|
return;
|
||||||
|
|
||||||
|
std::cerr << "FAIL: " << message << "\n";
|
||||||
|
++gFailures;
|
||||||
|
}
|
||||||
|
|
||||||
|
void TestEarlyPollWaitsWithoutAdvancing()
|
||||||
|
{
|
||||||
|
using Clock = RenderCadenceClock::Clock;
|
||||||
|
RenderCadenceClock cadence(16.0);
|
||||||
|
const auto start = Clock::now();
|
||||||
|
cadence.Reset(start);
|
||||||
|
|
||||||
|
const auto tick = cadence.Poll(start - std::chrono::milliseconds(1));
|
||||||
|
Expect(!tick.due, "early poll is not due");
|
||||||
|
Expect(tick.sleepFor > RenderCadenceClock::Duration::zero(), "early poll returns a sleep duration");
|
||||||
|
Expect(cadence.OverrunCount() == 0, "early poll does not count overrun");
|
||||||
|
Expect(cadence.SkippedFrameCount() == 0, "early poll does not skip frames");
|
||||||
|
}
|
||||||
|
|
||||||
|
void TestDuePollRendersWithoutSkipping()
|
||||||
|
{
|
||||||
|
using Clock = RenderCadenceClock::Clock;
|
||||||
|
RenderCadenceClock cadence(16.0);
|
||||||
|
const auto start = Clock::now();
|
||||||
|
cadence.Reset(start);
|
||||||
|
|
||||||
|
const auto tick = cadence.Poll(start);
|
||||||
|
Expect(tick.due, "exact cadence time is due");
|
||||||
|
Expect(tick.skippedFrames == 0, "exact cadence time skips no frames");
|
||||||
|
Expect(cadence.OverrunCount() == 0, "exact cadence time is not an overrun");
|
||||||
|
|
||||||
|
cadence.MarkRendered(start);
|
||||||
|
Expect(cadence.NextRenderTime() > start, "mark rendered advances next render time");
|
||||||
|
}
|
||||||
|
|
||||||
|
void TestLatePollRecordsSkippedFrames()
|
||||||
|
{
|
||||||
|
using Clock = RenderCadenceClock::Clock;
|
||||||
|
RenderCadenceClock cadence(10.0);
|
||||||
|
const auto start = Clock::now();
|
||||||
|
cadence.Reset(start);
|
||||||
|
|
||||||
|
const auto tick = cadence.Poll(start + std::chrono::milliseconds(35));
|
||||||
|
Expect(tick.due, "late poll is due");
|
||||||
|
Expect(tick.skippedFrames == 3, "late poll records skipped frame intervals");
|
||||||
|
Expect(cadence.OverrunCount() == 1, "late poll records overrun");
|
||||||
|
Expect(cadence.SkippedFrameCount() == 3, "late poll accumulates skipped frames");
|
||||||
|
}
|
||||||
|
|
||||||
|
void TestMarkRenderedRebasesAfterLargeStall()
|
||||||
|
{
|
||||||
|
using Clock = RenderCadenceClock::Clock;
|
||||||
|
RenderCadenceClock cadence(10.0);
|
||||||
|
const auto start = Clock::now();
|
||||||
|
cadence.Reset(start);
|
||||||
|
|
||||||
|
const auto stalled = start + std::chrono::milliseconds(100);
|
||||||
|
cadence.MarkRendered(stalled);
|
||||||
|
|
||||||
|
const auto next = cadence.NextRenderTime();
|
||||||
|
Expect(next > stalled, "large stall rebases next render time after now");
|
||||||
|
Expect(next - stalled <= std::chrono::milliseconds(11), "large stall rebases to roughly one frame ahead");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int main()
|
||||||
|
{
|
||||||
|
TestEarlyPollWaitsWithoutAdvancing();
|
||||||
|
TestDuePollRendersWithoutSkipping();
|
||||||
|
TestLatePollRecordsSkippedFrames();
|
||||||
|
TestMarkRenderedRebasesAfterLargeStall();
|
||||||
|
|
||||||
|
if (gFailures != 0)
|
||||||
|
{
|
||||||
|
std::cerr << gFailures << " cadence clock test failure(s).\n";
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::cout << "RenderCadenceCompositor cadence clock tests passed.\n";
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
176
tests/RenderCadenceCompositorFrameExchangeTests.cpp
Normal file
176
tests/RenderCadenceCompositorFrameExchangeTests.cpp
Normal file
@@ -0,0 +1,176 @@
|
|||||||
|
#include "SystemFrameExchange.h"
|
||||||
|
|
||||||
|
#include <chrono>
|
||||||
|
#include <cstdint>
|
||||||
|
#include <iostream>
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
int gFailures = 0;
|
||||||
|
|
||||||
|
void Expect(bool condition, const char* message)
|
||||||
|
{
|
||||||
|
if (condition)
|
||||||
|
return;
|
||||||
|
|
||||||
|
std::cerr << "FAIL: " << message << "\n";
|
||||||
|
++gFailures;
|
||||||
|
}
|
||||||
|
|
||||||
|
SystemFrameExchangeConfig MakeConfig(std::size_t capacity = 2)
|
||||||
|
{
|
||||||
|
SystemFrameExchangeConfig config;
|
||||||
|
config.width = 4;
|
||||||
|
config.height = 3;
|
||||||
|
config.pixelFormat = VideoIOPixelFormat::Bgra8;
|
||||||
|
config.capacity = capacity;
|
||||||
|
return config;
|
||||||
|
}
|
||||||
|
|
||||||
|
void TestAcquirePublishesAndSchedules()
|
||||||
|
{
|
||||||
|
SystemFrameExchange exchange(MakeConfig(1));
|
||||||
|
|
||||||
|
SystemFrame frame;
|
||||||
|
Expect(exchange.AcquireForRender(frame), "frame can be acquired for render");
|
||||||
|
Expect(frame.bytes != nullptr, "acquired frame has storage");
|
||||||
|
Expect(frame.width == 4, "frame width is configured");
|
||||||
|
Expect(frame.height == 3, "frame height is configured");
|
||||||
|
Expect(frame.rowBytes == 16, "BGRA8 row bytes are inferred");
|
||||||
|
Expect(frame.pixelFormat == VideoIOPixelFormat::Bgra8, "pixel format is configured");
|
||||||
|
|
||||||
|
frame.frameIndex = 42;
|
||||||
|
Expect(exchange.PublishCompleted(frame), "rendering frame can be completed");
|
||||||
|
Expect(exchange.WaitForCompletedDepth(1, std::chrono::milliseconds(0)), "completed depth can be observed");
|
||||||
|
|
||||||
|
SystemFrame scheduled;
|
||||||
|
Expect(exchange.ConsumeCompletedForSchedule(scheduled), "completed frame can be scheduled");
|
||||||
|
Expect(scheduled.index == frame.index, "scheduled frame uses completed slot");
|
||||||
|
Expect(scheduled.generation == frame.generation, "scheduled frame keeps generation");
|
||||||
|
Expect(scheduled.frameIndex == 42, "frame index is preserved");
|
||||||
|
|
||||||
|
Expect(exchange.ReleaseScheduledByBytes(scheduled.bytes), "scheduled frame can be released by bytes");
|
||||||
|
|
||||||
|
SystemFrameExchangeMetrics metrics = exchange.Metrics();
|
||||||
|
Expect(metrics.freeCount == 1, "released slot returns to free");
|
||||||
|
Expect(metrics.completedFrames == 1, "completed metric is counted");
|
||||||
|
Expect(metrics.scheduledFrames == 1, "scheduled metric is counted");
|
||||||
|
}
|
||||||
|
|
||||||
|
void TestAcquireDropsOldestCompletedUnscheduled()
|
||||||
|
{
|
||||||
|
SystemFrameExchange exchange(MakeConfig(2));
|
||||||
|
|
||||||
|
SystemFrame first;
|
||||||
|
SystemFrame second;
|
||||||
|
SystemFrame third;
|
||||||
|
Expect(exchange.AcquireForRender(first), "first frame can be acquired");
|
||||||
|
first.frameIndex = 1;
|
||||||
|
Expect(exchange.PublishCompleted(first), "first frame can be completed");
|
||||||
|
Expect(exchange.AcquireForRender(second), "second frame can be acquired");
|
||||||
|
second.frameIndex = 2;
|
||||||
|
Expect(exchange.PublishCompleted(second), "second frame can be completed");
|
||||||
|
|
||||||
|
Expect(exchange.AcquireForRender(third), "third acquire drops the oldest completed frame");
|
||||||
|
Expect(third.index == first.index, "oldest completed slot is reused");
|
||||||
|
|
||||||
|
SystemFrame scheduled;
|
||||||
|
Expect(exchange.ConsumeCompletedForSchedule(scheduled), "remaining completed frame can be scheduled");
|
||||||
|
Expect(scheduled.index == second.index, "newer completed frame survives drop");
|
||||||
|
Expect(scheduled.frameIndex == 2, "newer frame index survives drop");
|
||||||
|
|
||||||
|
SystemFrameExchangeMetrics metrics = exchange.Metrics();
|
||||||
|
Expect(metrics.completedDrops == 1, "drop metric is counted");
|
||||||
|
Expect(metrics.renderingCount == 1, "reused slot is rendering");
|
||||||
|
Expect(metrics.scheduledCount == 1, "consumed slot is scheduled");
|
||||||
|
}
|
||||||
|
|
||||||
|
void TestScheduledFramesAreNotDropped()
|
||||||
|
{
|
||||||
|
SystemFrameExchange exchange(MakeConfig(1));
|
||||||
|
|
||||||
|
SystemFrame frame;
|
||||||
|
Expect(exchange.AcquireForRender(frame), "single frame can be acquired");
|
||||||
|
Expect(exchange.PublishCompleted(frame), "single frame can be completed");
|
||||||
|
|
||||||
|
SystemFrame scheduled;
|
||||||
|
Expect(exchange.ConsumeCompletedForSchedule(scheduled), "single frame can be scheduled");
|
||||||
|
|
||||||
|
SystemFrame extra;
|
||||||
|
Expect(!exchange.AcquireForRender(extra), "scheduled frame is not dropped for render acquire");
|
||||||
|
|
||||||
|
SystemFrameExchangeMetrics metrics = exchange.Metrics();
|
||||||
|
Expect(metrics.acquireMisses == 1, "blocked acquire miss is counted");
|
||||||
|
Expect(metrics.completedDrops == 0, "scheduled frame is not counted as a completed drop");
|
||||||
|
}
|
||||||
|
|
||||||
|
void TestGenerationValidationRejectsStaleFrames()
|
||||||
|
{
|
||||||
|
SystemFrameExchange exchange(MakeConfig(1));
|
||||||
|
|
||||||
|
SystemFrame first;
|
||||||
|
Expect(exchange.AcquireForRender(first), "frame can be acquired");
|
||||||
|
Expect(exchange.PublishCompleted(first), "frame can be completed");
|
||||||
|
|
||||||
|
SystemFrame scheduled;
|
||||||
|
Expect(exchange.ConsumeCompletedForSchedule(scheduled), "frame can be scheduled");
|
||||||
|
Expect(exchange.ReleaseScheduledByBytes(scheduled.bytes), "frame can be released");
|
||||||
|
|
||||||
|
SystemFrame second;
|
||||||
|
Expect(exchange.AcquireForRender(second), "slot can be reacquired");
|
||||||
|
Expect(second.index == first.index, "same slot is reused");
|
||||||
|
Expect(second.generation != first.generation, "reacquire invalidates stale generation");
|
||||||
|
Expect(!exchange.PublishCompleted(first), "stale frame cannot be completed");
|
||||||
|
}
|
||||||
|
|
||||||
|
void TestPixelFormatAwareSizing()
|
||||||
|
{
|
||||||
|
SystemFrameExchangeConfig config;
|
||||||
|
config.width = 7;
|
||||||
|
config.height = 2;
|
||||||
|
config.pixelFormat = VideoIOPixelFormat::V210;
|
||||||
|
config.capacity = 1;
|
||||||
|
|
||||||
|
SystemFrameExchange exchange(config);
|
||||||
|
SystemFrame frame;
|
||||||
|
Expect(exchange.AcquireForRender(frame), "v210 frame can be acquired");
|
||||||
|
Expect(frame.pixelFormat == VideoIOPixelFormat::V210, "v210 pixel format is preserved");
|
||||||
|
Expect(frame.rowBytes == static_cast<long>(VideoIORowBytes(VideoIOPixelFormat::V210, 7)), "v210 row bytes are inferred");
|
||||||
|
|
||||||
|
config.pixelFormat = VideoIOPixelFormat::Uyvy8;
|
||||||
|
config.rowBytes = 64;
|
||||||
|
exchange.Configure(config);
|
||||||
|
Expect(exchange.AcquireForRender(frame), "explicit row-byte frame can be acquired");
|
||||||
|
Expect(frame.pixelFormat == VideoIOPixelFormat::Uyvy8, "reconfigured pixel format is preserved");
|
||||||
|
Expect(frame.rowBytes == 64, "explicit row bytes are preserved");
|
||||||
|
}
|
||||||
|
|
||||||
|
void TestCompletedPollMissIsCounted()
|
||||||
|
{
|
||||||
|
SystemFrameExchange exchange(MakeConfig(1));
|
||||||
|
SystemFrame frame;
|
||||||
|
Expect(!exchange.ConsumeCompletedForSchedule(frame), "empty completed queue cannot be consumed");
|
||||||
|
|
||||||
|
SystemFrameExchangeMetrics metrics = exchange.Metrics();
|
||||||
|
Expect(metrics.completedPollMisses == 1, "completed poll miss is counted");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int main()
|
||||||
|
{
|
||||||
|
TestAcquirePublishesAndSchedules();
|
||||||
|
TestAcquireDropsOldestCompletedUnscheduled();
|
||||||
|
TestScheduledFramesAreNotDropped();
|
||||||
|
TestGenerationValidationRejectsStaleFrames();
|
||||||
|
TestPixelFormatAwareSizing();
|
||||||
|
TestCompletedPollMissIsCounted();
|
||||||
|
|
||||||
|
if (gFailures != 0)
|
||||||
|
{
|
||||||
|
std::cerr << gFailures << " frame exchange test failure(s).\n";
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::cout << "RenderCadenceCompositor frame exchange tests passed.\n";
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
206
tests/RenderCadenceCompositorHttpControlServerTests.cpp
Normal file
206
tests/RenderCadenceCompositorHttpControlServerTests.cpp
Normal file
@@ -0,0 +1,206 @@
|
|||||||
|
#include "HttpControlServer.h"
|
||||||
|
|
||||||
|
#include <filesystem>
|
||||||
|
#include <fstream>
|
||||||
|
#include <iostream>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
int gFailures = 0;
|
||||||
|
|
||||||
|
void Expect(bool condition, const std::string& message)
|
||||||
|
{
|
||||||
|
if (condition)
|
||||||
|
return;
|
||||||
|
|
||||||
|
++gFailures;
|
||||||
|
std::cerr << "FAILED: " << message << "\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
void ExpectEquals(const std::string& actual, const std::string& expected, const std::string& message)
|
||||||
|
{
|
||||||
|
if (actual == expected)
|
||||||
|
return;
|
||||||
|
|
||||||
|
++gFailures;
|
||||||
|
std::cerr << "FAILED: " << message << "\n"
|
||||||
|
<< "expected: " << expected << "\n"
|
||||||
|
<< "actual: " << actual << "\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
void TestParsesHttpRequest()
|
||||||
|
{
|
||||||
|
using namespace RenderCadenceCompositor;
|
||||||
|
|
||||||
|
HttpControlServer::HttpRequest request;
|
||||||
|
const bool parsed = HttpControlServer::ParseHttpRequest(
|
||||||
|
"GET /api/state?cacheBust=1 HTTP/1.1\r\nHost: 127.0.0.1\r\n\r\n",
|
||||||
|
request);
|
||||||
|
|
||||||
|
Expect(parsed, "request parses");
|
||||||
|
ExpectEquals(request.method, "GET", "method is parsed");
|
||||||
|
ExpectEquals(request.path, "/api/state", "query string is stripped from path");
|
||||||
|
ExpectEquals(request.headers["host"], "127.0.0.1", "headers are lower-cased and trimmed");
|
||||||
|
}
|
||||||
|
|
||||||
|
void TestStateEndpointUsesCallback()
|
||||||
|
{
|
||||||
|
using namespace RenderCadenceCompositor;
|
||||||
|
|
||||||
|
HttpControlServer server;
|
||||||
|
HttpControlServerCallbacks callbacks;
|
||||||
|
callbacks.getStateJson = []() { return std::string("{\"ok\":true}"); };
|
||||||
|
server.SetCallbacksForTest(callbacks);
|
||||||
|
|
||||||
|
HttpControlServer::HttpRequest request;
|
||||||
|
request.method = "GET";
|
||||||
|
request.path = "/api/state";
|
||||||
|
|
||||||
|
const HttpControlServer::HttpResponse response = server.RouteRequestForTest(request);
|
||||||
|
ExpectEquals(response.status, "200 OK", "state endpoint succeeds");
|
||||||
|
ExpectEquals(response.contentType, "application/json", "state endpoint is JSON");
|
||||||
|
ExpectEquals(response.body, "{\"ok\":true}", "state endpoint returns callback JSON");
|
||||||
|
}
|
||||||
|
|
||||||
|
void TestWebSocketAcceptKey()
|
||||||
|
{
|
||||||
|
using namespace RenderCadenceCompositor;
|
||||||
|
|
||||||
|
const std::string acceptKey = HttpControlServer::WebSocketAcceptKey("dGhlIHNhbXBsZSBub25jZQ==");
|
||||||
|
ExpectEquals(acceptKey, "s3pPLMBiTxaQ9kYGzzhZRbK+xOo=", "WebSocket accept key matches RFC example");
|
||||||
|
}
|
||||||
|
|
||||||
|
void TestRootServesUiIndex()
|
||||||
|
{
|
||||||
|
using namespace RenderCadenceCompositor;
|
||||||
|
|
||||||
|
const std::filesystem::path root = std::filesystem::temp_directory_path() / "render-cadence-compositor-ui-test";
|
||||||
|
std::filesystem::create_directories(root);
|
||||||
|
{
|
||||||
|
std::ofstream output(root / "index.html", std::ios::binary);
|
||||||
|
output << "<!doctype html><div id=\"root\"></div>";
|
||||||
|
}
|
||||||
|
|
||||||
|
HttpControlServer server;
|
||||||
|
HttpControlServer::HttpRequest request;
|
||||||
|
request.method = "GET";
|
||||||
|
request.path = "/";
|
||||||
|
|
||||||
|
server.SetRootsForTest(root, std::filesystem::path());
|
||||||
|
const HttpControlServer::HttpResponse response = server.RouteRequestForTest(request);
|
||||||
|
|
||||||
|
ExpectEquals(response.status, "200 OK", "root endpoint serves UI index");
|
||||||
|
ExpectEquals(response.contentType, "text/html", "UI index content type is html");
|
||||||
|
Expect(response.body.find("root") != std::string::npos, "UI index body is returned");
|
||||||
|
|
||||||
|
std::filesystem::remove_all(root);
|
||||||
|
}
|
||||||
|
|
||||||
|
void TestKnownPostEndpointReturnsActionError()
|
||||||
|
{
|
||||||
|
using namespace RenderCadenceCompositor;
|
||||||
|
|
||||||
|
HttpControlServer server;
|
||||||
|
HttpControlServer::HttpRequest request;
|
||||||
|
request.method = "POST";
|
||||||
|
request.path = "/api/layers/add";
|
||||||
|
request.body = "{\"shaderId\":\"happy-accident\"}";
|
||||||
|
|
||||||
|
const HttpControlServer::HttpResponse response = server.RouteRequestForTest(request);
|
||||||
|
ExpectEquals(response.status, "400 Bad Request", "unimplemented post returns OpenAPI action error status");
|
||||||
|
ExpectEquals(response.contentType, "application/json", "unimplemented post returns JSON");
|
||||||
|
Expect(response.body.find("\"ok\":false") != std::string::npos, "unimplemented post reports ok false");
|
||||||
|
Expect(response.body.find("not implemented") != std::string::npos, "unimplemented post reports diagnostic");
|
||||||
|
}
|
||||||
|
|
||||||
|
void TestLayerPostEndpointsUseCallbacks()
|
||||||
|
{
|
||||||
|
using namespace RenderCadenceCompositor;
|
||||||
|
|
||||||
|
HttpControlServer server;
|
||||||
|
HttpControlServerCallbacks callbacks;
|
||||||
|
callbacks.addLayer = [](const std::string& body) {
|
||||||
|
Expect(body.find("solid") != std::string::npos, "add callback receives request body");
|
||||||
|
return ControlActionResult{ true, std::string() };
|
||||||
|
};
|
||||||
|
callbacks.removeLayer = [](const std::string& body) {
|
||||||
|
Expect(body.find("runtime-layer-1") != std::string::npos, "remove callback receives request body");
|
||||||
|
return ControlActionResult{ false, "Unknown layer id." };
|
||||||
|
};
|
||||||
|
server.SetCallbacksForTest(callbacks);
|
||||||
|
|
||||||
|
HttpControlServer::HttpRequest addRequest;
|
||||||
|
addRequest.method = "POST";
|
||||||
|
addRequest.path = "/api/layers/add";
|
||||||
|
addRequest.body = "{\"shaderId\":\"solid\"}";
|
||||||
|
const HttpControlServer::HttpResponse addResponse = server.RouteRequestForTest(addRequest);
|
||||||
|
ExpectEquals(addResponse.status, "200 OK", "add layer callback success returns 200");
|
||||||
|
Expect(addResponse.body.find("\"ok\":true") != std::string::npos, "add layer callback returns action success");
|
||||||
|
|
||||||
|
HttpControlServer::HttpRequest removeRequest;
|
||||||
|
removeRequest.method = "POST";
|
||||||
|
removeRequest.path = "/api/layers/remove";
|
||||||
|
removeRequest.body = "{\"layerId\":\"runtime-layer-1\"}";
|
||||||
|
const HttpControlServer::HttpResponse removeResponse = server.RouteRequestForTest(removeRequest);
|
||||||
|
ExpectEquals(removeResponse.status, "400 Bad Request", "remove layer callback failure returns 400");
|
||||||
|
Expect(removeResponse.body.find("Unknown layer id.") != std::string::npos, "remove layer callback returns diagnostic");
|
||||||
|
}
|
||||||
|
|
||||||
|
void TestGenericPostCallbackHandlesControlRoutes()
|
||||||
|
{
|
||||||
|
using namespace RenderCadenceCompositor;
|
||||||
|
|
||||||
|
HttpControlServer server;
|
||||||
|
HttpControlServerCallbacks callbacks;
|
||||||
|
callbacks.executePost = [](const std::string& path, const std::string& body) {
|
||||||
|
ExpectEquals(path, "/api/layers/set-bypass", "generic callback receives route path");
|
||||||
|
Expect(body.find("runtime-layer-1") != std::string::npos, "generic callback receives request body");
|
||||||
|
return ControlActionResult{ true, std::string() };
|
||||||
|
};
|
||||||
|
server.SetCallbacksForTest(callbacks);
|
||||||
|
|
||||||
|
HttpControlServer::HttpRequest request;
|
||||||
|
request.method = "POST";
|
||||||
|
request.path = "/api/layers/set-bypass";
|
||||||
|
request.body = "{\"layerId\":\"runtime-layer-1\",\"bypass\":true}";
|
||||||
|
|
||||||
|
const HttpControlServer::HttpResponse response = server.RouteRequestForTest(request);
|
||||||
|
ExpectEquals(response.status, "200 OK", "generic control callback success returns 200");
|
||||||
|
Expect(response.body.find("\"ok\":true") != std::string::npos, "generic control callback returns action success");
|
||||||
|
}
|
||||||
|
|
||||||
|
void TestUnknownEndpointReturns404()
|
||||||
|
{
|
||||||
|
using namespace RenderCadenceCompositor;
|
||||||
|
|
||||||
|
HttpControlServer server;
|
||||||
|
HttpControlServer::HttpRequest request;
|
||||||
|
request.method = "GET";
|
||||||
|
request.path = "/api/nope";
|
||||||
|
|
||||||
|
const HttpControlServer::HttpResponse response = server.RouteRequestForTest(request);
|
||||||
|
ExpectEquals(response.status, "404 Not Found", "unknown endpoint returns 404");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int main()
|
||||||
|
{
|
||||||
|
TestParsesHttpRequest();
|
||||||
|
TestStateEndpointUsesCallback();
|
||||||
|
TestWebSocketAcceptKey();
|
||||||
|
TestRootServesUiIndex();
|
||||||
|
TestKnownPostEndpointReturnsActionError();
|
||||||
|
TestLayerPostEndpointsUseCallbacks();
|
||||||
|
TestGenericPostCallbackHandlesControlRoutes();
|
||||||
|
TestUnknownEndpointReturns404();
|
||||||
|
|
||||||
|
if (gFailures != 0)
|
||||||
|
{
|
||||||
|
std::cerr << gFailures << " RenderCadenceCompositorHttpControlServer test failure(s).\n";
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::cout << "RenderCadenceCompositorHttpControlServer tests passed.\n";
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
109
tests/RenderCadenceCompositorInputFrameMailboxTests.cpp
Normal file
109
tests/RenderCadenceCompositorInputFrameMailboxTests.cpp
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
#include "InputFrameMailbox.h"
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
#include <cstring>
|
||||||
|
#include <iostream>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
int gFailures = 0;
|
||||||
|
|
||||||
|
void Expect(bool condition, const char* message)
|
||||||
|
{
|
||||||
|
if (condition)
|
||||||
|
return;
|
||||||
|
|
||||||
|
++gFailures;
|
||||||
|
std::cerr << "FAIL: " << message << "\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
InputFrameMailboxConfig MakeConfig(std::size_t capacity = 2)
|
||||||
|
{
|
||||||
|
InputFrameMailboxConfig config;
|
||||||
|
config.width = 2;
|
||||||
|
config.height = 2;
|
||||||
|
config.pixelFormat = VideoIOPixelFormat::Bgra8;
|
||||||
|
config.rowBytes = VideoIORowBytes(config.pixelFormat, config.width);
|
||||||
|
config.capacity = capacity;
|
||||||
|
return config;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<unsigned char> MakeFrame(unsigned char value)
|
||||||
|
{
|
||||||
|
return std::vector<unsigned char>(16, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
void TestAcquireLatestDropsOlderReadyFrames()
|
||||||
|
{
|
||||||
|
InputFrameMailbox mailbox(MakeConfig(3));
|
||||||
|
const std::vector<unsigned char> frame1 = MakeFrame(1);
|
||||||
|
const std::vector<unsigned char> frame2 = MakeFrame(2);
|
||||||
|
const std::vector<unsigned char> frame3 = MakeFrame(3);
|
||||||
|
|
||||||
|
Expect(mailbox.SubmitFrame(frame1.data(), 8, 1), "first input frame submits");
|
||||||
|
Expect(mailbox.SubmitFrame(frame2.data(), 8, 2), "second input frame submits");
|
||||||
|
Expect(mailbox.SubmitFrame(frame3.data(), 8, 3), "third input frame submits");
|
||||||
|
|
||||||
|
InputFrame latest;
|
||||||
|
Expect(mailbox.TryAcquireLatest(latest), "latest input frame can be acquired");
|
||||||
|
Expect(latest.frameIndex == 3, "mailbox returns newest frame");
|
||||||
|
Expect(latest.bytes != nullptr && static_cast<const unsigned char*>(latest.bytes)[0] == 3, "latest frame bytes match newest frame");
|
||||||
|
Expect(mailbox.Release(latest), "latest input frame can be released");
|
||||||
|
|
||||||
|
const InputFrameMailboxMetrics metrics = mailbox.Metrics();
|
||||||
|
Expect(metrics.droppedReadyFrames == 2, "older ready input frames are dropped after latest acquire");
|
||||||
|
Expect(metrics.freeCount == 3, "all slots are free after release");
|
||||||
|
}
|
||||||
|
|
||||||
|
void TestSubmitDropsOldestWhenFull()
|
||||||
|
{
|
||||||
|
InputFrameMailbox mailbox(MakeConfig(2));
|
||||||
|
const std::vector<unsigned char> frame1 = MakeFrame(1);
|
||||||
|
const std::vector<unsigned char> frame2 = MakeFrame(2);
|
||||||
|
const std::vector<unsigned char> frame3 = MakeFrame(3);
|
||||||
|
|
||||||
|
Expect(mailbox.SubmitFrame(frame1.data(), 8, 1), "first frame submits into full test");
|
||||||
|
Expect(mailbox.SubmitFrame(frame2.data(), 8, 2), "second frame submits into full test");
|
||||||
|
Expect(mailbox.SubmitFrame(frame3.data(), 8, 3), "third frame submits by dropping oldest ready frame");
|
||||||
|
|
||||||
|
InputFrame latest;
|
||||||
|
Expect(mailbox.TryAcquireLatest(latest), "latest frame available after drop");
|
||||||
|
Expect(latest.frameIndex == 3, "newest frame survived full mailbox");
|
||||||
|
Expect(mailbox.Release(latest), "newest frame releases");
|
||||||
|
|
||||||
|
const InputFrameMailboxMetrics metrics = mailbox.Metrics();
|
||||||
|
Expect(metrics.droppedReadyFrames >= 1, "full mailbox records ready drop");
|
||||||
|
Expect(metrics.submitMisses == 0, "full mailbox did not block producer when ready slots were disposable");
|
||||||
|
}
|
||||||
|
|
||||||
|
void TestReadingFrameIsProtected()
|
||||||
|
{
|
||||||
|
InputFrameMailbox mailbox(MakeConfig(1));
|
||||||
|
const std::vector<unsigned char> frame1 = MakeFrame(1);
|
||||||
|
const std::vector<unsigned char> frame2 = MakeFrame(2);
|
||||||
|
|
||||||
|
Expect(mailbox.SubmitFrame(frame1.data(), 8, 1), "protected frame submits");
|
||||||
|
InputFrame acquired;
|
||||||
|
Expect(mailbox.TryAcquireLatest(acquired), "protected frame acquired");
|
||||||
|
Expect(!mailbox.SubmitFrame(frame2.data(), 8, 2), "producer cannot overwrite frame currently being read");
|
||||||
|
Expect(mailbox.Release(acquired), "protected frame releases");
|
||||||
|
Expect(mailbox.SubmitFrame(frame2.data(), 8, 2), "producer can submit after release");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int main()
|
||||||
|
{
|
||||||
|
TestAcquireLatestDropsOlderReadyFrames();
|
||||||
|
TestSubmitDropsOldestWhenFull();
|
||||||
|
TestReadingFrameIsProtected();
|
||||||
|
|
||||||
|
if (gFailures != 0)
|
||||||
|
{
|
||||||
|
std::cerr << gFailures << " RenderCadenceCompositorInputFrameMailbox test failure(s).\n";
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::cout << "RenderCadenceCompositorInputFrameMailbox tests passed.\n";
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
123
tests/RenderCadenceCompositorJsonWriterTests.cpp
Normal file
123
tests/RenderCadenceCompositorJsonWriterTests.cpp
Normal file
@@ -0,0 +1,123 @@
|
|||||||
|
#include "JsonWriter.h"
|
||||||
|
|
||||||
|
#include <iostream>
|
||||||
|
#include <stdexcept>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
int gFailures = 0;
|
||||||
|
|
||||||
|
void Expect(bool condition, const std::string& message)
|
||||||
|
{
|
||||||
|
if (condition)
|
||||||
|
return;
|
||||||
|
|
||||||
|
++gFailures;
|
||||||
|
std::cerr << "FAILED: " << message << "\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
void ExpectEquals(const std::string& actual, const std::string& expected, const std::string& message)
|
||||||
|
{
|
||||||
|
if (actual == expected)
|
||||||
|
return;
|
||||||
|
|
||||||
|
++gFailures;
|
||||||
|
std::cerr << "FAILED: " << message << "\n"
|
||||||
|
<< "expected: " << expected << "\n"
|
||||||
|
<< "actual: " << actual << "\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
void TestEscapesStrings()
|
||||||
|
{
|
||||||
|
using RenderCadenceCompositor::JsonWriter;
|
||||||
|
|
||||||
|
ExpectEquals(
|
||||||
|
JsonWriter::EscapeString("quote\" slash\\ newline\n tab\t"),
|
||||||
|
"quote\\\" slash\\\\ newline\\n tab\\t",
|
||||||
|
"string escape handles common escaped characters");
|
||||||
|
|
||||||
|
std::string control;
|
||||||
|
control.push_back(static_cast<char>(0x01));
|
||||||
|
ExpectEquals(JsonWriter::EscapeString(control), "\\u0001", "string escape handles control characters");
|
||||||
|
}
|
||||||
|
|
||||||
|
void TestObjectSerialization()
|
||||||
|
{
|
||||||
|
using RenderCadenceCompositor::JsonWriter;
|
||||||
|
|
||||||
|
JsonWriter writer;
|
||||||
|
writer.BeginObject();
|
||||||
|
writer.KeyString("name", "cadence");
|
||||||
|
writer.KeyDouble("renderFps", 59.94);
|
||||||
|
writer.KeyBool("healthy", true);
|
||||||
|
writer.KeyNull("error");
|
||||||
|
writer.EndObject();
|
||||||
|
|
||||||
|
ExpectEquals(
|
||||||
|
writer.StringValue(),
|
||||||
|
"{\"name\":\"cadence\",\"renderFps\":59.94,\"healthy\":true,\"error\":null}",
|
||||||
|
"object serialization is compact and ordered");
|
||||||
|
}
|
||||||
|
|
||||||
|
void TestNestedArrays()
|
||||||
|
{
|
||||||
|
using RenderCadenceCompositor::JsonWriter;
|
||||||
|
|
||||||
|
JsonWriter writer;
|
||||||
|
writer.BeginObject();
|
||||||
|
writer.Key("levels");
|
||||||
|
writer.BeginArray();
|
||||||
|
writer.String("log");
|
||||||
|
writer.String("warning");
|
||||||
|
writer.String("error");
|
||||||
|
writer.EndArray();
|
||||||
|
writer.Key("counts");
|
||||||
|
writer.BeginObject();
|
||||||
|
writer.KeyUInt("queued", 3);
|
||||||
|
writer.KeyInt("delta", -1);
|
||||||
|
writer.EndObject();
|
||||||
|
writer.EndObject();
|
||||||
|
|
||||||
|
ExpectEquals(
|
||||||
|
writer.StringValue(),
|
||||||
|
"{\"levels\":[\"log\",\"warning\",\"error\"],\"counts\":{\"queued\":3,\"delta\":-1}}",
|
||||||
|
"nested arrays and objects serialize correctly");
|
||||||
|
}
|
||||||
|
|
||||||
|
void TestMisuseThrows()
|
||||||
|
{
|
||||||
|
using RenderCadenceCompositor::JsonWriter;
|
||||||
|
|
||||||
|
JsonWriter writer;
|
||||||
|
bool threw = false;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
writer.BeginObject();
|
||||||
|
writer.Key("missing");
|
||||||
|
writer.EndObject();
|
||||||
|
}
|
||||||
|
catch (const std::logic_error&)
|
||||||
|
{
|
||||||
|
threw = true;
|
||||||
|
}
|
||||||
|
Expect(threw, "ending an object with a key missing a value throws");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int main()
|
||||||
|
{
|
||||||
|
TestEscapesStrings();
|
||||||
|
TestObjectSerialization();
|
||||||
|
TestNestedArrays();
|
||||||
|
TestMisuseThrows();
|
||||||
|
|
||||||
|
if (gFailures != 0)
|
||||||
|
{
|
||||||
|
std::cerr << gFailures << " RenderCadenceCompositorJsonWriter test failure(s).\n";
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::cout << "RenderCadenceCompositorJsonWriter tests passed.\n";
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
88
tests/RenderCadenceCompositorLoggerTests.cpp
Normal file
88
tests/RenderCadenceCompositorLoggerTests.cpp
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
#include "Logger.h"
|
||||||
|
|
||||||
|
#include <chrono>
|
||||||
|
#include <iostream>
|
||||||
|
#include <string>
|
||||||
|
#include <thread>
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
int gFailures = 0;
|
||||||
|
|
||||||
|
void Expect(bool condition, const std::string& message)
|
||||||
|
{
|
||||||
|
if (condition)
|
||||||
|
return;
|
||||||
|
|
||||||
|
++gFailures;
|
||||||
|
std::cerr << "FAILED: " << message << "\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
void TestLevelNames()
|
||||||
|
{
|
||||||
|
using namespace RenderCadenceCompositor;
|
||||||
|
Expect(std::string(LogLevelName(LogLevel::Log)) == "log", "log level name");
|
||||||
|
Expect(std::string(LogLevelName(LogLevel::Warning)) == "warning", "warning level name");
|
||||||
|
Expect(std::string(LogLevelName(LogLevel::Error)) == "error", "error level name");
|
||||||
|
}
|
||||||
|
|
||||||
|
void TestLevelFiltering()
|
||||||
|
{
|
||||||
|
using namespace RenderCadenceCompositor;
|
||||||
|
|
||||||
|
LoggerConfig config;
|
||||||
|
config.minimumLevel = LogLevel::Warning;
|
||||||
|
config.writeToConsole = false;
|
||||||
|
config.writeToDebugOutput = false;
|
||||||
|
config.writeToFile = false;
|
||||||
|
config.maxQueuedMessages = 16;
|
||||||
|
|
||||||
|
Logger& logger = Logger::Instance();
|
||||||
|
logger.Start(config);
|
||||||
|
logger.Write(LogLevel::Log, "test", "filtered");
|
||||||
|
logger.Write(LogLevel::Warning, "test", "kept");
|
||||||
|
logger.Write(LogLevel::Error, "test", "kept");
|
||||||
|
logger.Stop();
|
||||||
|
|
||||||
|
const LoggerCounters counters = logger.Counters();
|
||||||
|
Expect(counters.queued == 2, "logger queues only messages at or above minimum level");
|
||||||
|
Expect(counters.written == 2, "logger writes queued messages before stop returns");
|
||||||
|
Expect(counters.dropped == 0, "logger does not drop under queue capacity");
|
||||||
|
}
|
||||||
|
|
||||||
|
void TestTryWriteDropsWhenQueueIsFull()
|
||||||
|
{
|
||||||
|
using namespace RenderCadenceCompositor;
|
||||||
|
|
||||||
|
LoggerConfig config;
|
||||||
|
config.minimumLevel = LogLevel::Log;
|
||||||
|
config.writeToConsole = false;
|
||||||
|
config.writeToDebugOutput = false;
|
||||||
|
config.writeToFile = false;
|
||||||
|
config.maxQueuedMessages = 0;
|
||||||
|
|
||||||
|
Logger& logger = Logger::Instance();
|
||||||
|
logger.Start(config);
|
||||||
|
|
||||||
|
const bool sawDrop = !logger.TryWrite(LogLevel::Log, "test", "message");
|
||||||
|
logger.Stop();
|
||||||
|
|
||||||
|
Expect(sawDrop || logger.Counters().dropped > 0, "try-write reports or counts queue pressure");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int main()
|
||||||
|
{
|
||||||
|
TestLevelNames();
|
||||||
|
TestLevelFiltering();
|
||||||
|
TestTryWriteDropsWhenQueueIsFull();
|
||||||
|
|
||||||
|
if (gFailures != 0)
|
||||||
|
{
|
||||||
|
std::cerr << gFailures << " RenderCadenceCompositorLogger test failure(s).\n";
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::cout << "RenderCadenceCompositorLogger tests passed.\n";
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
217
tests/RenderCadenceCompositorRuntimeLayerModelTests.cpp
Normal file
217
tests/RenderCadenceCompositorRuntimeLayerModelTests.cpp
Normal file
@@ -0,0 +1,217 @@
|
|||||||
|
#include "RuntimeLayerModel.h"
|
||||||
|
|
||||||
|
#include <chrono>
|
||||||
|
#include <filesystem>
|
||||||
|
#include <fstream>
|
||||||
|
#include <iostream>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
int gFailures = 0;
|
||||||
|
|
||||||
|
void Expect(bool condition, const std::string& message)
|
||||||
|
{
|
||||||
|
if (condition)
|
||||||
|
return;
|
||||||
|
|
||||||
|
++gFailures;
|
||||||
|
std::cerr << "FAIL: " << message << "\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
std::filesystem::path MakeTestRoot()
|
||||||
|
{
|
||||||
|
const auto stamp = std::chrono::steady_clock::now().time_since_epoch().count();
|
||||||
|
const std::filesystem::path root = std::filesystem::temp_directory_path() / ("render-cadence-layer-model-tests-" + std::to_string(stamp));
|
||||||
|
std::filesystem::create_directories(root);
|
||||||
|
return root;
|
||||||
|
}
|
||||||
|
|
||||||
|
void WriteFile(const std::filesystem::path& path, const std::string& contents)
|
||||||
|
{
|
||||||
|
std::filesystem::create_directories(path.parent_path());
|
||||||
|
std::ofstream output(path, std::ios::binary);
|
||||||
|
output << contents;
|
||||||
|
}
|
||||||
|
|
||||||
|
RenderCadenceCompositor::SupportedShaderCatalog MakeCatalog(std::filesystem::path& root)
|
||||||
|
{
|
||||||
|
root = MakeTestRoot();
|
||||||
|
WriteFile(root / "solid" / "shader.slang", "float4 shadeVideo(float2 uv) { return float4(uv, 0.0, 1.0); }\n");
|
||||||
|
WriteFile(root / "solid" / "shader.json", R"({
|
||||||
|
"id": "solid",
|
||||||
|
"name": "Solid",
|
||||||
|
"description": "Solid test shader",
|
||||||
|
"category": "Tests",
|
||||||
|
"entryPoint": "shadeVideo",
|
||||||
|
"parameters": [
|
||||||
|
{ "id": "gain", "label": "Gain", "type": "float", "default": 0.5 },
|
||||||
|
{ "id": "drop", "label": "Drop", "type": "trigger" }
|
||||||
|
]
|
||||||
|
})");
|
||||||
|
|
||||||
|
RenderCadenceCompositor::SupportedShaderCatalog catalog;
|
||||||
|
std::string error;
|
||||||
|
Expect(catalog.Load(root, 4, error), error.empty() ? "catalog loads test shader" : error);
|
||||||
|
return catalog;
|
||||||
|
}
|
||||||
|
|
||||||
|
void TestSingleLayerLifecycle()
|
||||||
|
{
|
||||||
|
std::filesystem::path root;
|
||||||
|
RenderCadenceCompositor::SupportedShaderCatalog catalog = MakeCatalog(root);
|
||||||
|
|
||||||
|
RenderCadenceCompositor::RuntimeLayerModel model;
|
||||||
|
std::string error;
|
||||||
|
Expect(model.InitializeSingleLayer(catalog, "solid", error), "model initializes a supported startup shader");
|
||||||
|
Expect(model.FirstLayerId() == "runtime-layer-1", "startup layer id is stable");
|
||||||
|
|
||||||
|
Expect(model.MarkBuildStarted(model.FirstLayerId(), "build started", error), "build start updates the layer");
|
||||||
|
RenderCadenceCompositor::RuntimeLayerModelSnapshot snapshot = model.Snapshot();
|
||||||
|
Expect(snapshot.displayLayers.size() == 1, "snapshot exposes the display layer");
|
||||||
|
Expect(snapshot.displayLayers[0].buildState == RenderCadenceCompositor::RuntimeLayerBuildState::Pending, "started layer is pending");
|
||||||
|
Expect(!snapshot.displayLayers[0].renderReady, "started layer is not render-ready yet");
|
||||||
|
|
||||||
|
RuntimeShaderArtifact artifact;
|
||||||
|
artifact.shaderId = "solid";
|
||||||
|
artifact.displayName = "Solid";
|
||||||
|
artifact.fragmentShaderSource = "void main(){}";
|
||||||
|
artifact.message = "build ready";
|
||||||
|
Expect(model.MarkBuildReady(artifact, error), "ready artifact updates the matching layer");
|
||||||
|
|
||||||
|
snapshot = model.Snapshot();
|
||||||
|
Expect(snapshot.compileSucceeded, "ready layer reports compile success");
|
||||||
|
Expect(snapshot.displayLayers[0].buildState == RenderCadenceCompositor::RuntimeLayerBuildState::Ready, "ready layer is marked ready");
|
||||||
|
Expect(snapshot.displayLayers[0].renderReady, "ready layer exposes render readiness");
|
||||||
|
Expect(snapshot.renderLayers.size() == 1, "ready layer produces one render layer artifact");
|
||||||
|
Expect(snapshot.renderLayers[0].artifact.shaderId == "solid", "render layer carries the artifact");
|
||||||
|
|
||||||
|
std::filesystem::remove_all(root);
|
||||||
|
}
|
||||||
|
|
||||||
|
void TestRejectsUnsupportedStartupShader()
|
||||||
|
{
|
||||||
|
std::filesystem::path root;
|
||||||
|
RenderCadenceCompositor::SupportedShaderCatalog catalog = MakeCatalog(root);
|
||||||
|
|
||||||
|
RenderCadenceCompositor::RuntimeLayerModel model;
|
||||||
|
std::string error;
|
||||||
|
Expect(!model.InitializeSingleLayer(catalog, "missing", error), "model rejects unsupported shader ids");
|
||||||
|
Expect(!error.empty(), "unsupported shader rejection explains the problem");
|
||||||
|
Expect(model.Snapshot().displayLayers.empty(), "rejected startup shader leaves no display layer");
|
||||||
|
|
||||||
|
std::filesystem::remove_all(root);
|
||||||
|
}
|
||||||
|
|
||||||
|
void TestBuildFailureStaysDisplaySide()
|
||||||
|
{
|
||||||
|
std::filesystem::path root;
|
||||||
|
RenderCadenceCompositor::SupportedShaderCatalog catalog = MakeCatalog(root);
|
||||||
|
|
||||||
|
RenderCadenceCompositor::RuntimeLayerModel model;
|
||||||
|
std::string error;
|
||||||
|
Expect(model.InitializeSingleLayer(catalog, "solid", error), "model initializes for failure test");
|
||||||
|
Expect(model.MarkBuildFailed(model.FirstLayerId(), "compile failed", error), "build failure updates the layer");
|
||||||
|
|
||||||
|
const RenderCadenceCompositor::RuntimeLayerModelSnapshot snapshot = model.Snapshot();
|
||||||
|
Expect(!snapshot.compileSucceeded, "failed layer reports compile failure");
|
||||||
|
Expect(snapshot.displayLayers[0].buildState == RenderCadenceCompositor::RuntimeLayerBuildState::Failed, "failed layer is marked failed");
|
||||||
|
Expect(!snapshot.displayLayers[0].renderReady, "failed layer is not render-ready");
|
||||||
|
Expect(snapshot.renderLayers.empty(), "failed layer does not produce a render artifact");
|
||||||
|
|
||||||
|
std::filesystem::remove_all(root);
|
||||||
|
}
|
||||||
|
|
||||||
|
void TestAddAndRemoveLayers()
|
||||||
|
{
|
||||||
|
std::filesystem::path root;
|
||||||
|
RenderCadenceCompositor::SupportedShaderCatalog catalog = MakeCatalog(root);
|
||||||
|
|
||||||
|
RenderCadenceCompositor::RuntimeLayerModel model;
|
||||||
|
std::string error;
|
||||||
|
std::string firstLayerId;
|
||||||
|
std::string secondLayerId;
|
||||||
|
Expect(model.AddLayer(catalog, "solid", firstLayerId, error), "first layer can be added");
|
||||||
|
Expect(model.AddLayer(catalog, "solid", secondLayerId, error), "second layer can be added");
|
||||||
|
Expect(firstLayerId != secondLayerId, "added layers receive distinct ids");
|
||||||
|
Expect(model.Snapshot().displayLayers.size() == 2, "added layers appear in display snapshot");
|
||||||
|
|
||||||
|
Expect(model.RemoveLayer(firstLayerId, error), "existing layer can be removed");
|
||||||
|
RenderCadenceCompositor::RuntimeLayerModelSnapshot snapshot = model.Snapshot();
|
||||||
|
Expect(snapshot.displayLayers.size() == 1, "removed layer leaves snapshot");
|
||||||
|
Expect(snapshot.displayLayers[0].id == secondLayerId, "remaining layer identity is preserved");
|
||||||
|
Expect(!model.RemoveLayer(firstLayerId, error), "removed layer cannot be removed twice");
|
||||||
|
|
||||||
|
std::filesystem::remove_all(root);
|
||||||
|
}
|
||||||
|
|
||||||
|
void TestLayerControlsUpdateDisplayAndRenderModels()
|
||||||
|
{
|
||||||
|
std::filesystem::path root;
|
||||||
|
RenderCadenceCompositor::SupportedShaderCatalog catalog = MakeCatalog(root);
|
||||||
|
|
||||||
|
RenderCadenceCompositor::RuntimeLayerModel model;
|
||||||
|
std::string error;
|
||||||
|
std::string firstLayerId;
|
||||||
|
std::string secondLayerId;
|
||||||
|
Expect(model.AddLayer(catalog, "solid", firstLayerId, error), "first control layer can be added");
|
||||||
|
Expect(model.AddLayer(catalog, "solid", secondLayerId, error), "second control layer can be added");
|
||||||
|
|
||||||
|
Expect(model.SetLayerBypass(firstLayerId, true, error), "bypass can be set");
|
||||||
|
Expect(model.ReorderLayer(firstLayerId, 1, error), "layer can be reordered");
|
||||||
|
RenderCadenceCompositor::RuntimeLayerModelSnapshot snapshot = model.Snapshot();
|
||||||
|
Expect(snapshot.displayLayers[1].id == firstLayerId, "reordered layer moves to requested index");
|
||||||
|
Expect(snapshot.displayLayers[1].bypass, "bypass state is visible in read model");
|
||||||
|
|
||||||
|
JsonValue gainValue(0.75);
|
||||||
|
Expect(model.UpdateParameter(firstLayerId, "gain", gainValue, error), "parameter value can be updated");
|
||||||
|
snapshot = model.Snapshot();
|
||||||
|
Expect(snapshot.displayLayers[1].parameterValues.at("gain").numberValues.front() == 0.75, "updated parameter value is visible");
|
||||||
|
JsonValue dropPulse(true);
|
||||||
|
Expect(model.UpdateParameter(firstLayerId, "drop", dropPulse, error), "trigger parameter can be pulsed");
|
||||||
|
snapshot = model.Snapshot();
|
||||||
|
const std::vector<double> firstTrigger = snapshot.displayLayers[1].parameterValues.at("drop").numberValues;
|
||||||
|
Expect(firstTrigger.size() == 2 && firstTrigger[0] == 1.0 && firstTrigger[1] >= 0.0, "trigger pulse increments count and records runtime time");
|
||||||
|
Expect(model.UpdateParameter(firstLayerId, "drop", dropPulse, error), "trigger parameter can be pulsed again");
|
||||||
|
snapshot = model.Snapshot();
|
||||||
|
const std::vector<double> secondTrigger = snapshot.displayLayers[1].parameterValues.at("drop").numberValues;
|
||||||
|
Expect(secondTrigger.size() == 2 && secondTrigger[0] == 2.0 && secondTrigger[1] >= firstTrigger[1], "second trigger pulse increments count again");
|
||||||
|
|
||||||
|
RuntimeShaderArtifact artifact;
|
||||||
|
artifact.layerId = firstLayerId;
|
||||||
|
artifact.shaderId = "solid";
|
||||||
|
artifact.displayName = "Solid";
|
||||||
|
artifact.fragmentShaderSource = "void main(){}";
|
||||||
|
artifact.parameterDefinitions = snapshot.displayLayers[1].parameterDefinitions;
|
||||||
|
artifact.message = "build ready";
|
||||||
|
Expect(model.MarkBuildReady(artifact, error), "ready artifact keeps layer parameter state");
|
||||||
|
snapshot = model.Snapshot();
|
||||||
|
Expect(snapshot.renderLayers.size() == 1, "ready layer produces render model");
|
||||||
|
Expect(snapshot.renderLayers[0].bypass, "render model carries bypass state");
|
||||||
|
Expect(snapshot.renderLayers[0].artifact.parameterValues.at("gain").numberValues.front() == 0.75, "render artifact carries updated parameter value");
|
||||||
|
|
||||||
|
Expect(model.ResetParameters(firstLayerId, error), "parameters can reset to defaults");
|
||||||
|
snapshot = model.Snapshot();
|
||||||
|
Expect(snapshot.displayLayers[1].parameterValues.at("gain").numberValues.front() == 0.5, "reset restores default value");
|
||||||
|
|
||||||
|
std::filesystem::remove_all(root);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int main()
|
||||||
|
{
|
||||||
|
TestSingleLayerLifecycle();
|
||||||
|
TestRejectsUnsupportedStartupShader();
|
||||||
|
TestBuildFailureStaysDisplaySide();
|
||||||
|
TestAddAndRemoveLayers();
|
||||||
|
TestLayerControlsUpdateDisplayAndRenderModels();
|
||||||
|
|
||||||
|
if (gFailures != 0)
|
||||||
|
{
|
||||||
|
std::cerr << gFailures << " RenderCadenceCompositorRuntimeLayerModel test failure(s).\n";
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::cout << "RenderCadenceCompositorRuntimeLayerModel tests passed.\n";
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
126
tests/RenderCadenceCompositorRuntimeShaderParamsTests.cpp
Normal file
126
tests/RenderCadenceCompositorRuntimeShaderParamsTests.cpp
Normal file
@@ -0,0 +1,126 @@
|
|||||||
|
#include "RuntimeShaderParams.h"
|
||||||
|
|
||||||
|
#include <cstring>
|
||||||
|
#include <iostream>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
void Expect(bool condition, const char* message)
|
||||||
|
{
|
||||||
|
if (!condition)
|
||||||
|
{
|
||||||
|
std::cerr << "FAILED: " << message << "\n";
|
||||||
|
std::exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
float ReadFloat(const std::vector<unsigned char>& buffer, std::size_t offset)
|
||||||
|
{
|
||||||
|
float value = 0.0f;
|
||||||
|
std::memcpy(&value, buffer.data() + offset, sizeof(value));
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
int ReadInt(const std::vector<unsigned char>& buffer, std::size_t offset)
|
||||||
|
{
|
||||||
|
int value = 0;
|
||||||
|
std::memcpy(&value, buffer.data() + offset, sizeof(value));
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
ShaderParameterDefinition FloatParam(const std::string& id, double defaultValue)
|
||||||
|
{
|
||||||
|
ShaderParameterDefinition definition;
|
||||||
|
definition.id = id;
|
||||||
|
definition.label = id;
|
||||||
|
definition.type = ShaderParameterType::Float;
|
||||||
|
definition.defaultNumbers = { defaultValue };
|
||||||
|
return definition;
|
||||||
|
}
|
||||||
|
|
||||||
|
ShaderParameterDefinition Vec2Param()
|
||||||
|
{
|
||||||
|
ShaderParameterDefinition definition;
|
||||||
|
definition.id = "offset";
|
||||||
|
definition.label = "Offset";
|
||||||
|
definition.type = ShaderParameterType::Vec2;
|
||||||
|
definition.defaultNumbers = { 2.0, 3.0 };
|
||||||
|
return definition;
|
||||||
|
}
|
||||||
|
|
||||||
|
ShaderParameterDefinition ColorParam()
|
||||||
|
{
|
||||||
|
ShaderParameterDefinition definition;
|
||||||
|
definition.id = "tint";
|
||||||
|
definition.label = "Tint";
|
||||||
|
definition.type = ShaderParameterType::Color;
|
||||||
|
definition.defaultNumbers = { 0.25, 0.5, 0.75, 1.0 };
|
||||||
|
return definition;
|
||||||
|
}
|
||||||
|
|
||||||
|
ShaderParameterDefinition BoolParam()
|
||||||
|
{
|
||||||
|
ShaderParameterDefinition definition;
|
||||||
|
definition.id = "enabled";
|
||||||
|
definition.label = "Enabled";
|
||||||
|
definition.type = ShaderParameterType::Boolean;
|
||||||
|
definition.defaultBoolean = true;
|
||||||
|
return definition;
|
||||||
|
}
|
||||||
|
|
||||||
|
ShaderParameterDefinition EnumParam()
|
||||||
|
{
|
||||||
|
ShaderParameterDefinition definition;
|
||||||
|
definition.id = "mode";
|
||||||
|
definition.label = "Mode";
|
||||||
|
definition.type = ShaderParameterType::Enum;
|
||||||
|
definition.defaultEnumValue = "hard";
|
||||||
|
definition.enumOptions = { { "soft", "Soft" }, { "hard", "Hard" } };
|
||||||
|
return definition;
|
||||||
|
}
|
||||||
|
|
||||||
|
ShaderParameterDefinition TriggerParam()
|
||||||
|
{
|
||||||
|
ShaderParameterDefinition definition;
|
||||||
|
definition.id = "drop";
|
||||||
|
definition.label = "Drop";
|
||||||
|
definition.type = ShaderParameterType::Trigger;
|
||||||
|
return definition;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int main()
|
||||||
|
{
|
||||||
|
RuntimeShaderArtifact artifact;
|
||||||
|
artifact.parameterDefinitions.push_back(FloatParam("gain", 0.5));
|
||||||
|
artifact.parameterDefinitions.push_back(Vec2Param());
|
||||||
|
artifact.parameterDefinitions.push_back(ColorParam());
|
||||||
|
artifact.parameterDefinitions.push_back(BoolParam());
|
||||||
|
artifact.parameterDefinitions.push_back(EnumParam());
|
||||||
|
artifact.parameterDefinitions.push_back(TriggerParam());
|
||||||
|
|
||||||
|
ShaderParameterValue triggerValue;
|
||||||
|
triggerValue.numberValues = { 3.0, 1.25 };
|
||||||
|
artifact.parameterValues["drop"] = triggerValue;
|
||||||
|
|
||||||
|
const std::vector<unsigned char> buffer = BuildRuntimeShaderGlobalParamsStd140(artifact, 120, 1920, 1080);
|
||||||
|
|
||||||
|
Expect(buffer.size() == 112, "runtime shader params block keeps expected std140 size");
|
||||||
|
Expect(ReadFloat(buffer, 0) == 2.0f, "time is derived from frame index at 60 fps");
|
||||||
|
Expect(ReadFloat(buffer, 8) == 1920.0f, "input width is packed after std140 vec2 alignment");
|
||||||
|
Expect(ReadFloat(buffer, 12) == 1080.0f, "input height is packed after std140 vec2 alignment");
|
||||||
|
Expect(ReadFloat(buffer, 60) == 0.5f, "float default parameter is packed");
|
||||||
|
Expect(ReadFloat(buffer, 64) == 2.0f, "vec2 default x is packed");
|
||||||
|
Expect(ReadFloat(buffer, 68) == 3.0f, "vec2 default y is packed");
|
||||||
|
Expect(ReadFloat(buffer, 80) == 0.25f, "color default red is packed on vec4 alignment");
|
||||||
|
Expect(ReadFloat(buffer, 92) == 1.0f, "color default alpha is packed");
|
||||||
|
Expect(ReadInt(buffer, 96) == 1, "bool default is packed as int");
|
||||||
|
Expect(ReadInt(buffer, 100) == 1, "enum default is packed as selected option index");
|
||||||
|
Expect(ReadInt(buffer, 104) == 3, "trigger count is packed as int");
|
||||||
|
Expect(ReadFloat(buffer, 108) == 1.25f, "trigger time is packed after trigger count");
|
||||||
|
|
||||||
|
std::cout << "RenderCadenceCompositorRuntimeShaderParams tests passed.\n";
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
112
tests/RenderCadenceCompositorRuntimeStateJsonTests.cpp
Normal file
112
tests/RenderCadenceCompositorRuntimeStateJsonTests.cpp
Normal file
@@ -0,0 +1,112 @@
|
|||||||
|
#include "RuntimeStateJson.h"
|
||||||
|
|
||||||
|
#include <chrono>
|
||||||
|
#include <filesystem>
|
||||||
|
#include <fstream>
|
||||||
|
#include <iostream>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
int gFailures = 0;
|
||||||
|
|
||||||
|
void ExpectContains(const std::string& text, const std::string& fragment, const std::string& message)
|
||||||
|
{
|
||||||
|
if (text.find(fragment) != std::string::npos)
|
||||||
|
return;
|
||||||
|
|
||||||
|
++gFailures;
|
||||||
|
std::cerr << "FAIL: " << message << "\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
std::filesystem::path MakeTestRoot()
|
||||||
|
{
|
||||||
|
const auto stamp = std::chrono::steady_clock::now().time_since_epoch().count();
|
||||||
|
const std::filesystem::path root = std::filesystem::temp_directory_path() / ("render-cadence-state-json-tests-" + std::to_string(stamp));
|
||||||
|
std::filesystem::create_directories(root);
|
||||||
|
return root;
|
||||||
|
}
|
||||||
|
|
||||||
|
void WriteFile(const std::filesystem::path& path, const std::string& contents)
|
||||||
|
{
|
||||||
|
std::filesystem::create_directories(path.parent_path());
|
||||||
|
std::ofstream output(path, std::ios::binary);
|
||||||
|
output << contents;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int main()
|
||||||
|
{
|
||||||
|
RenderCadenceCompositor::AppConfig config = RenderCadenceCompositor::DefaultAppConfig();
|
||||||
|
config.outputVideoFormat = "1080p";
|
||||||
|
config.outputFrameRate = "59.94";
|
||||||
|
|
||||||
|
RenderCadenceCompositor::CadenceTelemetrySnapshot telemetry;
|
||||||
|
telemetry.renderFps = 59.94;
|
||||||
|
telemetry.shaderBuildsCommitted = 1;
|
||||||
|
|
||||||
|
const std::filesystem::path root = MakeTestRoot();
|
||||||
|
WriteFile(root / "solid-color" / "shader.slang", "float4 shadeVideo(float2 uv) { return float4(uv, 0.0, 1.0); }\n");
|
||||||
|
WriteFile(root / "solid-color" / "shader.json", R"({
|
||||||
|
"id": "solid-color",
|
||||||
|
"name": "Solid Color",
|
||||||
|
"description": "A single color shader.",
|
||||||
|
"category": "Generator",
|
||||||
|
"entryPoint": "shadeVideo",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"id": "color",
|
||||||
|
"label": "Color",
|
||||||
|
"description": "Output color.",
|
||||||
|
"type": "color",
|
||||||
|
"default": [1.0, 0.25, 0.5, 1.0],
|
||||||
|
"min": [0.0, 0.0, 0.0, 0.0],
|
||||||
|
"max": [1.0, 1.0, 1.0, 1.0],
|
||||||
|
"step": [0.01, 0.01, 0.01, 0.01]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
})");
|
||||||
|
|
||||||
|
RenderCadenceCompositor::SupportedShaderCatalog shaderCatalog;
|
||||||
|
std::string error;
|
||||||
|
ExpectContains(shaderCatalog.Load(root, 4, error) ? "loaded" : error, "loaded", "test shader catalog should load");
|
||||||
|
|
||||||
|
RenderCadenceCompositor::RuntimeLayerModel layerModel;
|
||||||
|
layerModel.InitializeSingleLayer(shaderCatalog, "solid-color", error);
|
||||||
|
RuntimeShaderArtifact artifact;
|
||||||
|
artifact.shaderId = "solid-color";
|
||||||
|
artifact.displayName = "Solid Color";
|
||||||
|
artifact.fragmentShaderSource = "void main(){}";
|
||||||
|
artifact.message = "Runtime shader committed.";
|
||||||
|
layerModel.MarkBuildReady(artifact, error);
|
||||||
|
const RenderCadenceCompositor::RuntimeLayerModelSnapshot layerSnapshot = layerModel.Snapshot();
|
||||||
|
|
||||||
|
const std::string json = RenderCadenceCompositor::RuntimeStateToJson(RenderCadenceCompositor::RuntimeStateJsonInput{
|
||||||
|
config,
|
||||||
|
telemetry,
|
||||||
|
8080,
|
||||||
|
true,
|
||||||
|
"DeckLink scheduled output running.",
|
||||||
|
shaderCatalog,
|
||||||
|
layerSnapshot
|
||||||
|
});
|
||||||
|
|
||||||
|
ExpectContains(json, "\"shaders\":[{\"id\":\"solid-color\"", "state JSON should include supported shaders");
|
||||||
|
ExpectContains(json, "\"layerCount\":1", "state JSON should expose the display layer count");
|
||||||
|
ExpectContains(json, "\"layers\":[{\"id\":\"runtime-layer-1\"", "state JSON should expose the active display layer");
|
||||||
|
ExpectContains(json, "\"parameters\":[{\"id\":\"color\"", "state JSON should expose active shader parameters");
|
||||||
|
ExpectContains(json, "\"type\":\"color\"", "state JSON should serialize parameter types for the UI");
|
||||||
|
ExpectContains(json, "\"width\":1920", "state JSON should expose output width");
|
||||||
|
ExpectContains(json, "\"height\":1080", "state JSON should expose output height");
|
||||||
|
|
||||||
|
std::filesystem::remove_all(root);
|
||||||
|
|
||||||
|
if (gFailures != 0)
|
||||||
|
{
|
||||||
|
std::cerr << gFailures << " RenderCadenceCompositorRuntimeStateJson test failure(s).\n";
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::cout << "RenderCadenceCompositorRuntimeStateJson tests passed.\n";
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
132
tests/RenderCadenceCompositorSupportedShaderCatalogTests.cpp
Normal file
132
tests/RenderCadenceCompositorSupportedShaderCatalogTests.cpp
Normal file
@@ -0,0 +1,132 @@
|
|||||||
|
#include "SupportedShaderCatalog.h"
|
||||||
|
|
||||||
|
#include <iostream>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
int gFailures = 0;
|
||||||
|
|
||||||
|
void Expect(bool condition, const std::string& message)
|
||||||
|
{
|
||||||
|
if (condition)
|
||||||
|
return;
|
||||||
|
|
||||||
|
++gFailures;
|
||||||
|
std::cerr << "FAIL: " << message << "\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
ShaderPackage MakeSinglePassPackage()
|
||||||
|
{
|
||||||
|
ShaderPackage shaderPackage;
|
||||||
|
shaderPackage.id = "supported";
|
||||||
|
ShaderPassDefinition pass;
|
||||||
|
pass.id = "main";
|
||||||
|
pass.entryPoint = "mainImage";
|
||||||
|
pass.sourcePath = "shader.slang";
|
||||||
|
pass.outputName = "layerOutput";
|
||||||
|
shaderPackage.passes.push_back(pass);
|
||||||
|
return shaderPackage;
|
||||||
|
}
|
||||||
|
|
||||||
|
void SupportsSinglePassStatelessPackage()
|
||||||
|
{
|
||||||
|
const ShaderPackage shaderPackage = MakeSinglePassPackage();
|
||||||
|
const RenderCadenceCompositor::ShaderSupportResult result =
|
||||||
|
RenderCadenceCompositor::CheckStatelessSinglePassShaderSupport(shaderPackage);
|
||||||
|
|
||||||
|
Expect(result.supported, "single-pass stateless packages should be supported");
|
||||||
|
Expect(result.reason.empty(), "supported packages should not report a rejection reason");
|
||||||
|
}
|
||||||
|
|
||||||
|
void SupportsStatelessNamedPassPackage()
|
||||||
|
{
|
||||||
|
ShaderPackage shaderPackage = MakeSinglePassPackage();
|
||||||
|
shaderPackage.passes.front().outputName = "generatedMask";
|
||||||
|
ShaderPassDefinition secondPass;
|
||||||
|
secondPass.id = "second";
|
||||||
|
secondPass.entryPoint = "mainImage";
|
||||||
|
secondPass.sourcePath = "shader.slang";
|
||||||
|
secondPass.inputNames.push_back("generatedMask");
|
||||||
|
secondPass.outputName = "layerOutput";
|
||||||
|
shaderPackage.passes.push_back(secondPass);
|
||||||
|
|
||||||
|
const RenderCadenceCompositor::ShaderSupportResult result =
|
||||||
|
RenderCadenceCompositor::CheckStatelessSinglePassShaderSupport(shaderPackage);
|
||||||
|
|
||||||
|
Expect(result.supported, "stateless named-pass packages should be supported");
|
||||||
|
Expect(result.reason.empty(), "supported named-pass packages should not report a rejection reason");
|
||||||
|
}
|
||||||
|
|
||||||
|
void RejectsUnknownPassInput()
|
||||||
|
{
|
||||||
|
ShaderPackage shaderPackage = MakeSinglePassPackage();
|
||||||
|
shaderPackage.passes.front().inputNames.push_back("missingIntermediate");
|
||||||
|
|
||||||
|
const RenderCadenceCompositor::ShaderSupportResult result =
|
||||||
|
RenderCadenceCompositor::CheckStatelessSinglePassShaderSupport(shaderPackage);
|
||||||
|
|
||||||
|
Expect(!result.supported, "packages with unknown pass inputs should be rejected");
|
||||||
|
Expect(result.reason.find("unknown input") != std::string::npos, "unknown input rejection should explain the missing named output");
|
||||||
|
}
|
||||||
|
|
||||||
|
void RejectsTemporalPackage()
|
||||||
|
{
|
||||||
|
ShaderPackage shaderPackage = MakeSinglePassPackage();
|
||||||
|
shaderPackage.temporal.enabled = true;
|
||||||
|
|
||||||
|
const RenderCadenceCompositor::ShaderSupportResult result =
|
||||||
|
RenderCadenceCompositor::CheckStatelessSinglePassShaderSupport(shaderPackage);
|
||||||
|
|
||||||
|
Expect(!result.supported, "temporal packages should be rejected");
|
||||||
|
Expect(result.reason.find("temporal") != std::string::npos, "temporal rejection should mention temporal storage");
|
||||||
|
}
|
||||||
|
|
||||||
|
void RejectsTextureAssets()
|
||||||
|
{
|
||||||
|
ShaderPackage shaderPackage = MakeSinglePassPackage();
|
||||||
|
ShaderTextureAsset asset;
|
||||||
|
asset.id = "lut";
|
||||||
|
shaderPackage.textureAssets.push_back(asset);
|
||||||
|
|
||||||
|
const RenderCadenceCompositor::ShaderSupportResult result =
|
||||||
|
RenderCadenceCompositor::CheckStatelessSinglePassShaderSupport(shaderPackage);
|
||||||
|
|
||||||
|
Expect(!result.supported, "texture-backed packages should be rejected for now");
|
||||||
|
Expect(result.reason.find("texture") != std::string::npos, "texture rejection should mention texture assets");
|
||||||
|
}
|
||||||
|
|
||||||
|
void RejectsTextParameters()
|
||||||
|
{
|
||||||
|
ShaderPackage shaderPackage = MakeSinglePassPackage();
|
||||||
|
ShaderParameterDefinition parameter;
|
||||||
|
parameter.id = "caption";
|
||||||
|
parameter.type = ShaderParameterType::Text;
|
||||||
|
shaderPackage.parameters.push_back(parameter);
|
||||||
|
|
||||||
|
const RenderCadenceCompositor::ShaderSupportResult result =
|
||||||
|
RenderCadenceCompositor::CheckStatelessSinglePassShaderSupport(shaderPackage);
|
||||||
|
|
||||||
|
Expect(!result.supported, "text-parameter packages should be rejected for now");
|
||||||
|
Expect(result.reason.find("text") != std::string::npos, "text rejection should mention text parameters");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int main()
|
||||||
|
{
|
||||||
|
SupportsSinglePassStatelessPackage();
|
||||||
|
SupportsStatelessNamedPassPackage();
|
||||||
|
RejectsUnknownPassInput();
|
||||||
|
RejectsTemporalPackage();
|
||||||
|
RejectsTextureAssets();
|
||||||
|
RejectsTextParameters();
|
||||||
|
|
||||||
|
if (gFailures != 0)
|
||||||
|
{
|
||||||
|
std::cerr << gFailures << " RenderCadenceCompositorSupportedShaderCatalog test failure(s).\n";
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::cout << "RenderCadenceCompositorSupportedShaderCatalog tests passed.\n";
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
233
tests/RenderCadenceCompositorTelemetryTests.cpp
Normal file
233
tests/RenderCadenceCompositorTelemetryTests.cpp
Normal file
@@ -0,0 +1,233 @@
|
|||||||
|
#include "CadenceTelemetry.h"
|
||||||
|
#include "CadenceTelemetryJson.h"
|
||||||
|
|
||||||
|
#include <chrono>
|
||||||
|
#include <cstdint>
|
||||||
|
#include <iostream>
|
||||||
|
#include <thread>
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
int gFailures = 0;
|
||||||
|
|
||||||
|
void Expect(bool condition, const char* message)
|
||||||
|
{
|
||||||
|
if (condition)
|
||||||
|
return;
|
||||||
|
|
||||||
|
std::cerr << "FAIL: " << message << "\n";
|
||||||
|
++gFailures;
|
||||||
|
}
|
||||||
|
|
||||||
|
struct FakeExchangeMetrics
|
||||||
|
{
|
||||||
|
std::size_t freeCount = 0;
|
||||||
|
std::size_t completedCount = 0;
|
||||||
|
std::size_t scheduledCount = 0;
|
||||||
|
uint64_t completedFrames = 0;
|
||||||
|
uint64_t scheduledFrames = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct FakeExchange
|
||||||
|
{
|
||||||
|
FakeExchangeMetrics metrics;
|
||||||
|
FakeExchangeMetrics Metrics() const { return metrics; }
|
||||||
|
};
|
||||||
|
|
||||||
|
struct FakeOutputThreadMetrics
|
||||||
|
{
|
||||||
|
uint64_t completedPollMisses = 0;
|
||||||
|
uint64_t scheduleFailures = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct FakeOutputThread
|
||||||
|
{
|
||||||
|
FakeOutputThreadMetrics metrics;
|
||||||
|
FakeOutputThreadMetrics Metrics() const { return metrics; }
|
||||||
|
};
|
||||||
|
|
||||||
|
struct FakeOutputMetrics
|
||||||
|
{
|
||||||
|
uint64_t completions = 0;
|
||||||
|
uint64_t displayedLate = 0;
|
||||||
|
uint64_t dropped = 0;
|
||||||
|
uint64_t scheduleFailures = 0;
|
||||||
|
bool actualBufferedFramesAvailable = false;
|
||||||
|
uint64_t actualBufferedFrames = 0;
|
||||||
|
double scheduleCallMilliseconds = 0.0;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct FakeOutput
|
||||||
|
{
|
||||||
|
FakeOutputMetrics metrics;
|
||||||
|
FakeOutputMetrics Metrics() const { return metrics; }
|
||||||
|
};
|
||||||
|
|
||||||
|
struct FakeRenderThreadMetrics
|
||||||
|
{
|
||||||
|
uint64_t shaderBuildsCommitted = 0;
|
||||||
|
uint64_t shaderBuildFailures = 0;
|
||||||
|
uint64_t inputFramesReceived = 0;
|
||||||
|
uint64_t inputFramesDropped = 0;
|
||||||
|
double inputLatestAgeMilliseconds = 0.0;
|
||||||
|
double inputUploadMilliseconds = 0.0;
|
||||||
|
bool inputFormatSupported = true;
|
||||||
|
bool inputSignalPresent = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct FakeRenderThread
|
||||||
|
{
|
||||||
|
FakeRenderThreadMetrics metrics;
|
||||||
|
FakeRenderThreadMetrics GetMetrics() const { return metrics; }
|
||||||
|
};
|
||||||
|
|
||||||
|
void TestTelemetrySamplesCompletedPollMissesAndShaderCounts()
|
||||||
|
{
|
||||||
|
RenderCadenceCompositor::CadenceTelemetry telemetry;
|
||||||
|
FakeExchange exchange;
|
||||||
|
exchange.metrics.freeCount = 7;
|
||||||
|
exchange.metrics.completedCount = 1;
|
||||||
|
exchange.metrics.scheduledCount = 4;
|
||||||
|
exchange.metrics.completedFrames = 100;
|
||||||
|
exchange.metrics.scheduledFrames = 96;
|
||||||
|
|
||||||
|
FakeOutput output;
|
||||||
|
output.metrics.actualBufferedFramesAvailable = true;
|
||||||
|
output.metrics.actualBufferedFrames = 4;
|
||||||
|
|
||||||
|
FakeOutputThread outputThread;
|
||||||
|
outputThread.metrics.completedPollMisses = 12;
|
||||||
|
outputThread.metrics.scheduleFailures = 0;
|
||||||
|
|
||||||
|
FakeRenderThread renderThread;
|
||||||
|
renderThread.metrics.shaderBuildsCommitted = 1;
|
||||||
|
renderThread.metrics.shaderBuildFailures = 0;
|
||||||
|
renderThread.metrics.inputFramesReceived = 9;
|
||||||
|
renderThread.metrics.inputFramesDropped = 2;
|
||||||
|
renderThread.metrics.inputLatestAgeMilliseconds = 4.5;
|
||||||
|
renderThread.metrics.inputUploadMilliseconds = 0.25;
|
||||||
|
renderThread.metrics.inputFormatSupported = true;
|
||||||
|
renderThread.metrics.inputSignalPresent = true;
|
||||||
|
|
||||||
|
const auto snapshot = telemetry.Sample(exchange, output, outputThread, renderThread);
|
||||||
|
Expect(snapshot.freeFrames == 7, "free frame count is sampled");
|
||||||
|
Expect(snapshot.completedFrames == 1, "completed frame count is sampled");
|
||||||
|
Expect(snapshot.scheduledFrames == 4, "scheduled frame count is sampled");
|
||||||
|
Expect(snapshot.completedPollMisses == 12, "completed poll misses are sampled");
|
||||||
|
Expect(snapshot.shaderBuildsCommitted == 1, "shader committed count is sampled");
|
||||||
|
Expect(snapshot.shaderBuildFailures == 0, "shader failure count is sampled");
|
||||||
|
Expect(snapshot.inputFramesReceived == 9, "input received count is sampled");
|
||||||
|
Expect(snapshot.inputFramesDropped == 2, "input dropped count is sampled");
|
||||||
|
Expect(snapshot.inputLatestAgeMilliseconds == 4.5, "input latest age is sampled");
|
||||||
|
Expect(snapshot.inputUploadMilliseconds == 0.25, "input upload timing is sampled");
|
||||||
|
Expect(snapshot.inputFormatSupported, "input format support is sampled");
|
||||||
|
Expect(snapshot.inputSignalPresent, "input signal present is sampled");
|
||||||
|
Expect(snapshot.deckLinkBufferedAvailable, "buffer telemetry availability is sampled");
|
||||||
|
Expect(snapshot.deckLinkBuffered == 4, "buffer depth is sampled");
|
||||||
|
}
|
||||||
|
|
||||||
|
void TestTelemetryComputesRatesFromDeltas()
|
||||||
|
{
|
||||||
|
RenderCadenceCompositor::CadenceTelemetry telemetry;
|
||||||
|
FakeExchange exchange;
|
||||||
|
FakeOutput output;
|
||||||
|
FakeOutputThread outputThread;
|
||||||
|
FakeRenderThread renderThread;
|
||||||
|
|
||||||
|
exchange.metrics.completedFrames = 10;
|
||||||
|
exchange.metrics.scheduledFrames = 10;
|
||||||
|
(void)telemetry.Sample(exchange, output, outputThread, renderThread);
|
||||||
|
|
||||||
|
std::this_thread::sleep_for(std::chrono::milliseconds(5));
|
||||||
|
|
||||||
|
exchange.metrics.completedFrames = 20;
|
||||||
|
exchange.metrics.scheduledFrames = 19;
|
||||||
|
const auto snapshot = telemetry.Sample(exchange, output, outputThread, renderThread);
|
||||||
|
Expect(snapshot.sampleSeconds > 0.0, "second telemetry sample has elapsed time");
|
||||||
|
Expect(snapshot.renderFps > 0.0, "render fps is computed from completed frame delta");
|
||||||
|
Expect(snapshot.scheduleFps > 0.0, "schedule fps is computed from scheduled frame delta");
|
||||||
|
}
|
||||||
|
|
||||||
|
void TestTelemetrySerializesToJson()
|
||||||
|
{
|
||||||
|
RenderCadenceCompositor::CadenceTelemetrySnapshot snapshot;
|
||||||
|
snapshot.sampleSeconds = 1.0;
|
||||||
|
snapshot.renderFps = 59.94;
|
||||||
|
snapshot.scheduleFps = 60.0;
|
||||||
|
snapshot.freeFrames = 7;
|
||||||
|
snapshot.completedFrames = 1;
|
||||||
|
snapshot.scheduledFrames = 4;
|
||||||
|
snapshot.renderedTotal = 120;
|
||||||
|
snapshot.scheduledTotal = 118;
|
||||||
|
snapshot.completedPollMisses = 3;
|
||||||
|
snapshot.scheduleFailures = 0;
|
||||||
|
snapshot.completions = 117;
|
||||||
|
snapshot.displayedLate = 1;
|
||||||
|
snapshot.dropped = 2;
|
||||||
|
snapshot.shaderBuildsCommitted = 1;
|
||||||
|
snapshot.shaderBuildFailures = 0;
|
||||||
|
snapshot.inputFramesReceived = 10;
|
||||||
|
snapshot.inputFramesDropped = 1;
|
||||||
|
snapshot.inputLatestAgeMilliseconds = 3.5;
|
||||||
|
snapshot.inputUploadMilliseconds = 0.75;
|
||||||
|
snapshot.inputFormatSupported = true;
|
||||||
|
snapshot.inputSignalPresent = true;
|
||||||
|
snapshot.inputCaptureFps = 59.94;
|
||||||
|
snapshot.inputConvertMilliseconds = 4.25;
|
||||||
|
snapshot.inputSubmitMilliseconds = 0.35;
|
||||||
|
snapshot.inputNoSignalFrames = 2;
|
||||||
|
snapshot.inputUnsupportedFrames = 3;
|
||||||
|
snapshot.inputSubmitMisses = 4;
|
||||||
|
snapshot.inputCaptureFormat = "UYVY8";
|
||||||
|
snapshot.deckLinkBufferedAvailable = true;
|
||||||
|
snapshot.deckLinkBuffered = 4;
|
||||||
|
snapshot.deckLinkScheduleCallMilliseconds = 1.25;
|
||||||
|
|
||||||
|
const std::string json = RenderCadenceCompositor::CadenceTelemetryToJson(snapshot);
|
||||||
|
const std::string expected =
|
||||||
|
"{\"sampleSeconds\":1,\"renderFps\":59.94,\"scheduleFps\":60,"
|
||||||
|
"\"free\":7,\"completed\":1,\"scheduled\":4,"
|
||||||
|
"\"renderedTotal\":120,\"scheduledTotal\":118,"
|
||||||
|
"\"completedPollMisses\":3,\"scheduleFailures\":0,"
|
||||||
|
"\"completions\":117,\"late\":1,\"dropped\":2,"
|
||||||
|
"\"shaderCommitted\":1,\"shaderFailures\":0,"
|
||||||
|
"\"inputFramesReceived\":10,\"inputFramesDropped\":1,"
|
||||||
|
"\"inputLatestAgeMs\":3.5,\"inputUploadMs\":0.75,"
|
||||||
|
"\"inputFormatSupported\":true,\"inputSignalPresent\":true,"
|
||||||
|
"\"inputCaptureFps\":59.94,\"inputConvertMs\":4.25,"
|
||||||
|
"\"inputSubmitMs\":0.35,\"inputNoSignalFrames\":2,"
|
||||||
|
"\"inputUnsupportedFrames\":3,\"inputSubmitMisses\":4,"
|
||||||
|
"\"inputCaptureFormat\":\"UYVY8\","
|
||||||
|
"\"deckLinkBufferedAvailable\":true,\"deckLinkBuffered\":4,"
|
||||||
|
"\"scheduleCallMs\":1.25}";
|
||||||
|
Expect(json == expected, "telemetry snapshot serializes to stable JSON");
|
||||||
|
}
|
||||||
|
|
||||||
|
void TestUnavailableDeckLinkBufferSerializesAsNull()
|
||||||
|
{
|
||||||
|
RenderCadenceCompositor::CadenceTelemetrySnapshot snapshot;
|
||||||
|
snapshot.deckLinkBufferedAvailable = false;
|
||||||
|
|
||||||
|
const std::string json = RenderCadenceCompositor::CadenceTelemetryToJson(snapshot);
|
||||||
|
Expect(
|
||||||
|
json.find("\"deckLinkBufferedAvailable\":false,\"deckLinkBuffered\":null") != std::string::npos,
|
||||||
|
"unavailable DeckLink buffer depth serializes as null");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int main()
|
||||||
|
{
|
||||||
|
TestTelemetrySamplesCompletedPollMissesAndShaderCounts();
|
||||||
|
TestTelemetryComputesRatesFromDeltas();
|
||||||
|
TestTelemetrySerializesToJson();
|
||||||
|
TestUnavailableDeckLinkBufferSerializesAsNull();
|
||||||
|
|
||||||
|
if (gFailures != 0)
|
||||||
|
{
|
||||||
|
std::cerr << gFailures << " telemetry test failure(s).\n";
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::cout << "RenderCadenceCompositor telemetry tests passed.\n";
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
@@ -30,7 +30,7 @@ function App() {
|
|||||||
const [dropTargetLayerId, setDropTargetLayerId] = useState(null);
|
const [dropTargetLayerId, setDropTargetLayerId] = useState(null);
|
||||||
|
|
||||||
const layers = appState?.layers ?? [];
|
const layers = appState?.layers ?? [];
|
||||||
const shaders = appState?.shaders ?? [];
|
const shaders = (appState?.shaders ?? []).filter((shader) => shader.available !== false);
|
||||||
const performance = appState?.performance ?? {};
|
const performance = appState?.performance ?? {};
|
||||||
const runtime = appState?.runtime ?? {};
|
const runtime = appState?.runtime ?? {};
|
||||||
const video = appState?.video ?? {};
|
const video = appState?.video ?? {};
|
||||||
|
|||||||
Reference in New Issue
Block a user