Compare commits
25 Commits
6a33bd02ab
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3ffb562ff7 | ||
|
|
c2d548499c | ||
|
|
6a0340d1b4 | ||
|
|
5c1fc2a6cf | ||
|
|
d411453f80 | ||
|
|
4a049a557a | ||
|
|
13586c611a | ||
|
|
3a83d9617f | ||
|
|
5c66cfdc64 | ||
|
|
d72272b5a8 | ||
|
|
c25ae7b25b | ||
|
|
a39be6fb20 | ||
|
|
0a1fe440d9 | ||
|
|
3e45bba54b | ||
|
|
fd4b70ec9c | ||
|
|
ce28904891 | ||
|
|
2c5e925b97 | ||
|
|
957c0be05a | ||
|
|
0a8b335048 | ||
|
|
6e32941675 | ||
|
|
5fb4607d8c | ||
|
|
f43b6f6519 | ||
|
|
dfd49fd0e3 | ||
|
|
1429b2e660 | ||
|
|
02b221f481 |
@@ -290,6 +290,8 @@ set(RENDER_CADENCE_APP_SOURCES
|
|||||||
"${APP_DIR}/gl/shader/Std140Buffer.h"
|
"${APP_DIR}/gl/shader/Std140Buffer.h"
|
||||||
"${APP_DIR}/runtime/support/RuntimeJson.cpp"
|
"${APP_DIR}/runtime/support/RuntimeJson.cpp"
|
||||||
"${APP_DIR}/runtime/support/RuntimeJson.h"
|
"${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.cpp"
|
||||||
"${APP_DIR}/shader/ShaderCompiler.h"
|
"${APP_DIR}/shader/ShaderCompiler.h"
|
||||||
"${APP_DIR}/shader/ShaderPackageRegistry.cpp"
|
"${APP_DIR}/shader/ShaderPackageRegistry.cpp"
|
||||||
@@ -308,12 +310,19 @@ set(RENDER_CADENCE_APP_SOURCES
|
|||||||
"${RENDER_CADENCE_APP_DIR}/app/AppConfigProvider.h"
|
"${RENDER_CADENCE_APP_DIR}/app/AppConfigProvider.h"
|
||||||
"${RENDER_CADENCE_APP_DIR}/app/RenderCadenceApp.h"
|
"${RENDER_CADENCE_APP_DIR}/app/RenderCadenceApp.h"
|
||||||
"${RENDER_CADENCE_APP_DIR}/app/RuntimeLayerController.cpp"
|
"${RENDER_CADENCE_APP_DIR}/app/RuntimeLayerController.cpp"
|
||||||
|
"${RENDER_CADENCE_APP_DIR}/app/RuntimeLayerControllerBuild.cpp"
|
||||||
|
"${RENDER_CADENCE_APP_DIR}/app/RuntimeLayerControllerControls.cpp"
|
||||||
"${RENDER_CADENCE_APP_DIR}/app/RuntimeLayerController.h"
|
"${RENDER_CADENCE_APP_DIR}/app/RuntimeLayerController.h"
|
||||||
"${RENDER_CADENCE_APP_DIR}/control/ControlActionResult.h"
|
"${RENDER_CADENCE_APP_DIR}/control/ControlActionResult.h"
|
||||||
"${RENDER_CADENCE_APP_DIR}/control/HttpControlServer.cpp"
|
"${RENDER_CADENCE_APP_DIR}/control/RuntimeControlCommand.cpp"
|
||||||
"${RENDER_CADENCE_APP_DIR}/control/HttpControlServer.h"
|
"${RENDER_CADENCE_APP_DIR}/control/RuntimeControlCommand.h"
|
||||||
"${RENDER_CADENCE_APP_DIR}/control/HttpControlServerWebSocket.cpp"
|
"${RENDER_CADENCE_APP_DIR}/control/http/HttpControlServer.cpp"
|
||||||
|
"${RENDER_CADENCE_APP_DIR}/control/http/HttpControlServer.h"
|
||||||
|
"${RENDER_CADENCE_APP_DIR}/control/http/HttpControlServerRoutes.cpp"
|
||||||
|
"${RENDER_CADENCE_APP_DIR}/control/http/HttpControlServerWebSocket.cpp"
|
||||||
"${RENDER_CADENCE_APP_DIR}/control/RuntimeStateJson.h"
|
"${RENDER_CADENCE_APP_DIR}/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.cpp"
|
||||||
"${RENDER_CADENCE_APP_DIR}/frames/SystemFrameExchange.h"
|
"${RENDER_CADENCE_APP_DIR}/frames/SystemFrameExchange.h"
|
||||||
"${RENDER_CADENCE_APP_DIR}/frames/SystemFrameTypes.h"
|
"${RENDER_CADENCE_APP_DIR}/frames/SystemFrameTypes.h"
|
||||||
@@ -323,23 +332,26 @@ set(RENDER_CADENCE_APP_SOURCES
|
|||||||
"${RENDER_CADENCE_APP_DIR}/logging/Logger.h"
|
"${RENDER_CADENCE_APP_DIR}/logging/Logger.h"
|
||||||
"${RENDER_CADENCE_APP_DIR}/platform/HiddenGlWindow.cpp"
|
"${RENDER_CADENCE_APP_DIR}/platform/HiddenGlWindow.cpp"
|
||||||
"${RENDER_CADENCE_APP_DIR}/platform/HiddenGlWindow.h"
|
"${RENDER_CADENCE_APP_DIR}/platform/HiddenGlWindow.h"
|
||||||
"${RENDER_CADENCE_APP_DIR}/render/Bgra8ReadbackPipeline.cpp"
|
"${RENDER_CADENCE_APP_DIR}/render/InputFrameTexture.cpp"
|
||||||
"${RENDER_CADENCE_APP_DIR}/render/Bgra8ReadbackPipeline.h"
|
"${RENDER_CADENCE_APP_DIR}/render/InputFrameTexture.h"
|
||||||
"${RENDER_CADENCE_APP_DIR}/render/PboReadbackRing.cpp"
|
"${RENDER_CADENCE_APP_DIR}/render/readback/Bgra8ReadbackPipeline.cpp"
|
||||||
"${RENDER_CADENCE_APP_DIR}/render/PboReadbackRing.h"
|
"${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.cpp"
|
||||||
"${RENDER_CADENCE_APP_DIR}/render/RenderCadenceClock.h"
|
"${RENDER_CADENCE_APP_DIR}/render/RenderCadenceClock.h"
|
||||||
"${RENDER_CADENCE_APP_DIR}/render/RenderThread.cpp"
|
"${RENDER_CADENCE_APP_DIR}/render/RenderThread.cpp"
|
||||||
"${RENDER_CADENCE_APP_DIR}/render/RenderThread.h"
|
"${RENDER_CADENCE_APP_DIR}/render/RenderThread.h"
|
||||||
"${RENDER_CADENCE_APP_DIR}/render/RuntimeShaderRenderer.cpp"
|
"${RENDER_CADENCE_APP_DIR}/render/runtime/RuntimeShaderRenderer.cpp"
|
||||||
"${RENDER_CADENCE_APP_DIR}/render/RuntimeShaderRenderer.h"
|
"${RENDER_CADENCE_APP_DIR}/render/runtime/RuntimeShaderRenderer.h"
|
||||||
"${RENDER_CADENCE_APP_DIR}/render/RuntimeShaderParams.cpp"
|
"${RENDER_CADENCE_APP_DIR}/render/runtime/RuntimeShaderParams.cpp"
|
||||||
"${RENDER_CADENCE_APP_DIR}/render/RuntimeShaderParams.h"
|
"${RENDER_CADENCE_APP_DIR}/render/runtime/RuntimeShaderParams.h"
|
||||||
"${RENDER_CADENCE_APP_DIR}/render/RuntimeRenderScene.cpp"
|
"${RENDER_CADENCE_APP_DIR}/render/runtime/RuntimeRenderScene.cpp"
|
||||||
"${RENDER_CADENCE_APP_DIR}/render/RuntimeRenderScene.h"
|
"${RENDER_CADENCE_APP_DIR}/render/runtime/RuntimeRenderScene.h"
|
||||||
"${RENDER_CADENCE_APP_DIR}/render/RuntimeShaderPrepareWorker.cpp"
|
"${RENDER_CADENCE_APP_DIR}/render/runtime/RuntimeRenderSceneRender.cpp"
|
||||||
"${RENDER_CADENCE_APP_DIR}/render/RuntimeShaderPrepareWorker.h"
|
"${RENDER_CADENCE_APP_DIR}/render/runtime/RuntimeShaderPrepareWorker.cpp"
|
||||||
"${RENDER_CADENCE_APP_DIR}/render/RuntimeShaderProgram.h"
|
"${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.cpp"
|
||||||
"${RENDER_CADENCE_APP_DIR}/render/SimpleMotionRenderer.h"
|
"${RENDER_CADENCE_APP_DIR}/render/SimpleMotionRenderer.h"
|
||||||
"${RENDER_CADENCE_APP_DIR}/runtime/RuntimeLayerModel.cpp"
|
"${RENDER_CADENCE_APP_DIR}/runtime/RuntimeLayerModel.cpp"
|
||||||
@@ -353,6 +365,9 @@ set(RENDER_CADENCE_APP_SOURCES
|
|||||||
"${RENDER_CADENCE_APP_DIR}/telemetry/CadenceTelemetryJson.h"
|
"${RENDER_CADENCE_APP_DIR}/telemetry/CadenceTelemetryJson.h"
|
||||||
"${RENDER_CADENCE_APP_DIR}/telemetry/CadenceTelemetry.h"
|
"${RENDER_CADENCE_APP_DIR}/telemetry/CadenceTelemetry.h"
|
||||||
"${RENDER_CADENCE_APP_DIR}/telemetry/TelemetryHealthMonitor.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.cpp"
|
||||||
"${RENDER_CADENCE_APP_DIR}/video/DeckLinkOutput.h"
|
"${RENDER_CADENCE_APP_DIR}/video/DeckLinkOutput.h"
|
||||||
"${RENDER_CADENCE_APP_DIR}/video/DeckLinkOutputThread.h"
|
"${RENDER_CADENCE_APP_DIR}/video/DeckLinkOutputThread.h"
|
||||||
@@ -372,11 +387,14 @@ target_include_directories(RenderCadenceCompositor PRIVATE
|
|||||||
"${RENDER_CADENCE_APP_DIR}"
|
"${RENDER_CADENCE_APP_DIR}"
|
||||||
"${RENDER_CADENCE_APP_DIR}/app"
|
"${RENDER_CADENCE_APP_DIR}/app"
|
||||||
"${RENDER_CADENCE_APP_DIR}/control"
|
"${RENDER_CADENCE_APP_DIR}/control"
|
||||||
|
"${RENDER_CADENCE_APP_DIR}/control/http"
|
||||||
"${RENDER_CADENCE_APP_DIR}/frames"
|
"${RENDER_CADENCE_APP_DIR}/frames"
|
||||||
"${RENDER_CADENCE_APP_DIR}/json"
|
"${RENDER_CADENCE_APP_DIR}/json"
|
||||||
"${RENDER_CADENCE_APP_DIR}/logging"
|
"${RENDER_CADENCE_APP_DIR}/logging"
|
||||||
"${RENDER_CADENCE_APP_DIR}/platform"
|
"${RENDER_CADENCE_APP_DIR}/platform"
|
||||||
"${RENDER_CADENCE_APP_DIR}/render"
|
"${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}/runtime"
|
||||||
"${RENDER_CADENCE_APP_DIR}/telemetry"
|
"${RENDER_CADENCE_APP_DIR}/telemetry"
|
||||||
"${RENDER_CADENCE_APP_DIR}/video"
|
"${RENDER_CADENCE_APP_DIR}/video"
|
||||||
@@ -783,6 +801,23 @@ endif()
|
|||||||
|
|
||||||
add_test(NAME RenderCadenceCompositorFrameExchangeTests COMMAND RenderCadenceCompositorFrameExchangeTests)
|
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
|
add_executable(RenderCadenceCompositorClockTests
|
||||||
"${RENDER_CADENCE_APP_DIR}/render/RenderCadenceClock.cpp"
|
"${RENDER_CADENCE_APP_DIR}/render/RenderCadenceClock.cpp"
|
||||||
"${CMAKE_CURRENT_SOURCE_DIR}/tests/RenderCadenceCompositorClockTests.cpp"
|
"${CMAKE_CURRENT_SOURCE_DIR}/tests/RenderCadenceCompositorClockTests.cpp"
|
||||||
@@ -815,7 +850,7 @@ endif()
|
|||||||
add_test(NAME RenderCadenceCompositorTelemetryTests COMMAND RenderCadenceCompositorTelemetryTests)
|
add_test(NAME RenderCadenceCompositorTelemetryTests COMMAND RenderCadenceCompositorTelemetryTests)
|
||||||
|
|
||||||
add_executable(RenderCadenceCompositorRuntimeShaderParamsTests
|
add_executable(RenderCadenceCompositorRuntimeShaderParamsTests
|
||||||
"${RENDER_CADENCE_APP_DIR}/render/RuntimeShaderParams.cpp"
|
"${RENDER_CADENCE_APP_DIR}/render/runtime/RuntimeShaderParams.cpp"
|
||||||
"${CMAKE_CURRENT_SOURCE_DIR}/tests/RenderCadenceCompositorRuntimeShaderParamsTests.cpp"
|
"${CMAKE_CURRENT_SOURCE_DIR}/tests/RenderCadenceCompositorRuntimeShaderParamsTests.cpp"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -823,6 +858,7 @@ target_include_directories(RenderCadenceCompositorRuntimeShaderParamsTests PRIVA
|
|||||||
"${APP_DIR}/gl/shader"
|
"${APP_DIR}/gl/shader"
|
||||||
"${APP_DIR}/shader"
|
"${APP_DIR}/shader"
|
||||||
"${RENDER_CADENCE_APP_DIR}/render"
|
"${RENDER_CADENCE_APP_DIR}/render"
|
||||||
|
"${RENDER_CADENCE_APP_DIR}/render/runtime"
|
||||||
"${RENDER_CADENCE_APP_DIR}/runtime"
|
"${RENDER_CADENCE_APP_DIR}/runtime"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -837,6 +873,7 @@ add_executable(RenderCadenceCompositorRuntimeLayerModelTests
|
|||||||
"${RENDER_CADENCE_APP_DIR}/runtime/SupportedShaderCatalog.cpp"
|
"${RENDER_CADENCE_APP_DIR}/runtime/SupportedShaderCatalog.cpp"
|
||||||
"${APP_DIR}/shader/ShaderPackageRegistry.cpp"
|
"${APP_DIR}/shader/ShaderPackageRegistry.cpp"
|
||||||
"${APP_DIR}/runtime/support/RuntimeJson.cpp"
|
"${APP_DIR}/runtime/support/RuntimeJson.cpp"
|
||||||
|
"${APP_DIR}/runtime/support/RuntimeParameterUtils.cpp"
|
||||||
"${CMAKE_CURRENT_SOURCE_DIR}/tests/RenderCadenceCompositorRuntimeLayerModelTests.cpp"
|
"${CMAKE_CURRENT_SOURCE_DIR}/tests/RenderCadenceCompositorRuntimeLayerModelTests.cpp"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -905,6 +942,7 @@ add_test(NAME RenderCadenceCompositorJsonWriterTests COMMAND RenderCadenceCompos
|
|||||||
|
|
||||||
add_executable(RenderCadenceCompositorRuntimeStateJsonTests
|
add_executable(RenderCadenceCompositorRuntimeStateJsonTests
|
||||||
"${APP_DIR}/runtime/support/RuntimeJson.cpp"
|
"${APP_DIR}/runtime/support/RuntimeJson.cpp"
|
||||||
|
"${APP_DIR}/runtime/support/RuntimeParameterUtils.cpp"
|
||||||
"${APP_DIR}/shader/ShaderPackageRegistry.cpp"
|
"${APP_DIR}/shader/ShaderPackageRegistry.cpp"
|
||||||
"${RENDER_CADENCE_APP_DIR}/app/AppConfig.cpp"
|
"${RENDER_CADENCE_APP_DIR}/app/AppConfig.cpp"
|
||||||
"${RENDER_CADENCE_APP_DIR}/app/AppConfigProvider.cpp"
|
"${RENDER_CADENCE_APP_DIR}/app/AppConfigProvider.cpp"
|
||||||
@@ -936,15 +974,21 @@ endif()
|
|||||||
add_test(NAME RenderCadenceCompositorRuntimeStateJsonTests COMMAND RenderCadenceCompositorRuntimeStateJsonTests)
|
add_test(NAME RenderCadenceCompositorRuntimeStateJsonTests COMMAND RenderCadenceCompositorRuntimeStateJsonTests)
|
||||||
|
|
||||||
add_executable(RenderCadenceCompositorHttpControlServerTests
|
add_executable(RenderCadenceCompositorHttpControlServerTests
|
||||||
"${RENDER_CADENCE_APP_DIR}/control/HttpControlServer.cpp"
|
"${APP_DIR}/runtime/support/RuntimeJson.cpp"
|
||||||
"${RENDER_CADENCE_APP_DIR}/control/HttpControlServerWebSocket.cpp"
|
"${RENDER_CADENCE_APP_DIR}/control/RuntimeControlCommand.cpp"
|
||||||
|
"${RENDER_CADENCE_APP_DIR}/control/http/HttpControlServer.cpp"
|
||||||
|
"${RENDER_CADENCE_APP_DIR}/control/http/HttpControlServerRoutes.cpp"
|
||||||
|
"${RENDER_CADENCE_APP_DIR}/control/http/HttpControlServerWebSocket.cpp"
|
||||||
"${RENDER_CADENCE_APP_DIR}/json/JsonWriter.cpp"
|
"${RENDER_CADENCE_APP_DIR}/json/JsonWriter.cpp"
|
||||||
"${RENDER_CADENCE_APP_DIR}/logging/Logger.cpp"
|
"${RENDER_CADENCE_APP_DIR}/logging/Logger.cpp"
|
||||||
"${CMAKE_CURRENT_SOURCE_DIR}/tests/RenderCadenceCompositorHttpControlServerTests.cpp"
|
"${CMAKE_CURRENT_SOURCE_DIR}/tests/RenderCadenceCompositorHttpControlServerTests.cpp"
|
||||||
)
|
)
|
||||||
|
|
||||||
target_include_directories(RenderCadenceCompositorHttpControlServerTests PRIVATE
|
target_include_directories(RenderCadenceCompositorHttpControlServerTests PRIVATE
|
||||||
|
"${APP_DIR}"
|
||||||
|
"${APP_DIR}/runtime/support"
|
||||||
"${RENDER_CADENCE_APP_DIR}/control"
|
"${RENDER_CADENCE_APP_DIR}/control"
|
||||||
|
"${RENDER_CADENCE_APP_DIR}/control/http"
|
||||||
"${RENDER_CADENCE_APP_DIR}/json"
|
"${RENDER_CADENCE_APP_DIR}/json"
|
||||||
"${RENDER_CADENCE_APP_DIR}/logging"
|
"${RENDER_CADENCE_APP_DIR}/logging"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -54,6 +54,12 @@ struct VideoIOState
|
|||||||
uint64_t actualDeckLinkBufferedFrames = 0;
|
uint64_t actualDeckLinkBufferedFrames = 0;
|
||||||
double deckLinkScheduleCallMilliseconds = 0.0;
|
double deckLinkScheduleCallMilliseconds = 0.0;
|
||||||
uint64_t deckLinkScheduleFailureCount = 0;
|
uint64_t deckLinkScheduleFailureCount = 0;
|
||||||
|
bool deckLinkScheduleLeadAvailable = false;
|
||||||
|
int64_t deckLinkPlaybackStreamTime = 0;
|
||||||
|
uint64_t deckLinkPlaybackFrameIndex = 0;
|
||||||
|
uint64_t deckLinkNextScheduleFrameIndex = 0;
|
||||||
|
int64_t deckLinkScheduleLeadFrames = 0;
|
||||||
|
uint64_t deckLinkScheduleRealignmentCount = 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
struct VideoIOFrame
|
struct VideoIOFrame
|
||||||
|
|||||||
@@ -32,6 +32,17 @@ VideoIOScheduleTime VideoPlayoutScheduler::NextScheduleTime()
|
|||||||
return time;
|
return time;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void VideoPlayoutScheduler::AlignNextScheduleTimeToPlayback(int64_t streamTime, uint64_t leadFrames)
|
||||||
|
{
|
||||||
|
if (mFrameDuration <= 0 || streamTime < 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
const uint64_t playbackFrameIndex = static_cast<uint64_t>(streamTime / mFrameDuration);
|
||||||
|
const uint64_t minimumScheduleIndex = playbackFrameIndex + leadFrames;
|
||||||
|
if (minimumScheduleIndex > mScheduledFrameIndex)
|
||||||
|
mScheduledFrameIndex = minimumScheduleIndex;
|
||||||
|
}
|
||||||
|
|
||||||
VideoPlayoutRecoveryDecision VideoPlayoutScheduler::AccountForCompletionResult(VideoIOCompletionResult result, uint64_t readyQueueDepth)
|
VideoPlayoutRecoveryDecision VideoPlayoutScheduler::AccountForCompletionResult(VideoIOCompletionResult result, uint64_t readyQueueDepth)
|
||||||
{
|
{
|
||||||
++mCompletedFrameIndex;
|
++mCompletedFrameIndex;
|
||||||
|
|||||||
@@ -12,10 +12,12 @@ public:
|
|||||||
void Configure(int64_t frameDuration, int64_t timeScale, const VideoPlayoutPolicy& policy);
|
void Configure(int64_t frameDuration, int64_t timeScale, const VideoPlayoutPolicy& policy);
|
||||||
void Reset();
|
void Reset();
|
||||||
VideoIOScheduleTime NextScheduleTime();
|
VideoIOScheduleTime NextScheduleTime();
|
||||||
|
void AlignNextScheduleTimeToPlayback(int64_t streamTime, uint64_t leadFrames);
|
||||||
VideoPlayoutRecoveryDecision AccountForCompletionResult(VideoIOCompletionResult result, uint64_t readyQueueDepth = 0);
|
VideoPlayoutRecoveryDecision AccountForCompletionResult(VideoIOCompletionResult result, uint64_t readyQueueDepth = 0);
|
||||||
double FrameBudgetMilliseconds() const;
|
double FrameBudgetMilliseconds() const;
|
||||||
uint64_t ScheduledFrameIndex() const { return mScheduledFrameIndex; }
|
uint64_t ScheduledFrameIndex() const { return mScheduledFrameIndex; }
|
||||||
uint64_t CompletedFrameIndex() const { return mCompletedFrameIndex; }
|
uint64_t CompletedFrameIndex() const { return mCompletedFrameIndex; }
|
||||||
|
int64_t FrameDuration() const { return mFrameDuration; }
|
||||||
uint64_t LateStreak() const { return mLateStreak; }
|
uint64_t LateStreak() const { return mLateStreak; }
|
||||||
uint64_t DropStreak() const { return mDropStreak; }
|
uint64_t DropStreak() const { return mDropStreak; }
|
||||||
int64_t TimeScale() const { return mTimeScale; }
|
int64_t TimeScale() const { return mTimeScale; }
|
||||||
|
|||||||
@@ -12,6 +12,9 @@
|
|||||||
|
|
||||||
namespace
|
namespace
|
||||||
{
|
{
|
||||||
|
constexpr int64_t kMinimumHealthyScheduleLeadFrames = 4;
|
||||||
|
constexpr int64_t kProactiveScheduleLeadFloorFrames = 1;
|
||||||
|
|
||||||
class SystemMemoryDeckLinkVideoBuffer : public IDeckLinkVideoBuffer
|
class SystemMemoryDeckLinkVideoBuffer : public IDeckLinkVideoBuffer
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
@@ -526,13 +529,21 @@ bool DeckLinkSession::PopulateOutputFrame(IDeckLinkMutableVideoFrame* outputVide
|
|||||||
|
|
||||||
bool DeckLinkSession::ScheduleFrame(IDeckLinkMutableVideoFrame* outputVideoFrame)
|
bool DeckLinkSession::ScheduleFrame(IDeckLinkMutableVideoFrame* outputVideoFrame)
|
||||||
{
|
{
|
||||||
const VideoIOScheduleTime scheduleTime = mScheduler.NextScheduleTime();
|
|
||||||
if (outputVideoFrame == nullptr || output == nullptr)
|
if (outputVideoFrame == nullptr || output == nullptr)
|
||||||
{
|
{
|
||||||
++mState.deckLinkScheduleFailureCount;
|
++mState.deckLinkScheduleFailureCount;
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (mScheduleRealignmentPending)
|
||||||
|
{
|
||||||
|
RealignScheduleCursorToPlayback();
|
||||||
|
mScheduleRealignmentPending = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
UpdateScheduleLeadTelemetry();
|
||||||
|
MaybeRealignScheduleCursorForLowLead();
|
||||||
|
const VideoIOScheduleTime scheduleTime = mScheduler.NextScheduleTime();
|
||||||
const auto scheduleStart = std::chrono::steady_clock::now();
|
const auto scheduleStart = std::chrono::steady_clock::now();
|
||||||
const HRESULT result = output->ScheduleVideoFrame(outputVideoFrame, scheduleTime.streamTime, scheduleTime.duration, scheduleTime.timeScale);
|
const HRESULT result = output->ScheduleVideoFrame(outputVideoFrame, scheduleTime.streamTime, scheduleTime.duration, scheduleTime.timeScale);
|
||||||
const auto scheduleEnd = std::chrono::steady_clock::now();
|
const auto scheduleEnd = std::chrono::steady_clock::now();
|
||||||
@@ -543,6 +554,67 @@ bool DeckLinkSession::ScheduleFrame(IDeckLinkMutableVideoFrame* outputVideoFrame
|
|||||||
return result == S_OK;
|
return result == S_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void DeckLinkSession::UpdateScheduleLeadTelemetry()
|
||||||
|
{
|
||||||
|
if (output == nullptr)
|
||||||
|
{
|
||||||
|
mState.deckLinkScheduleLeadAvailable = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
BMDTimeValue streamTime = 0;
|
||||||
|
double playbackSpeed = 0.0;
|
||||||
|
if (output->GetScheduledStreamTime(mScheduler.TimeScale(), &streamTime, &playbackSpeed) != S_OK || playbackSpeed <= 0.0)
|
||||||
|
{
|
||||||
|
mState.deckLinkScheduleLeadAvailable = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const uint64_t playbackFrameIndex = streamTime >= 0 && mScheduler.FrameDuration() > 0
|
||||||
|
? static_cast<uint64_t>(streamTime / mScheduler.FrameDuration())
|
||||||
|
: 0;
|
||||||
|
const uint64_t nextScheduleFrameIndex = mScheduler.ScheduledFrameIndex();
|
||||||
|
mState.deckLinkScheduleLeadAvailable = true;
|
||||||
|
mState.deckLinkPlaybackStreamTime = streamTime;
|
||||||
|
mState.deckLinkPlaybackFrameIndex = playbackFrameIndex;
|
||||||
|
mState.deckLinkNextScheduleFrameIndex = nextScheduleFrameIndex;
|
||||||
|
mState.deckLinkScheduleLeadFrames = static_cast<int64_t>(nextScheduleFrameIndex) - static_cast<int64_t>(playbackFrameIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
void DeckLinkSession::MaybeRealignScheduleCursorForLowLead()
|
||||||
|
{
|
||||||
|
if (!mState.deckLinkScheduleLeadAvailable)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (mState.deckLinkScheduleLeadFrames >= kMinimumHealthyScheduleLeadFrames)
|
||||||
|
{
|
||||||
|
mProactiveScheduleRealignmentArmed = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!mProactiveScheduleRealignmentArmed || mState.deckLinkScheduleLeadFrames > kProactiveScheduleLeadFloorFrames)
|
||||||
|
return;
|
||||||
|
|
||||||
|
RealignScheduleCursorToPlayback();
|
||||||
|
mProactiveScheduleRealignmentArmed = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void DeckLinkSession::RealignScheduleCursorToPlayback()
|
||||||
|
{
|
||||||
|
if (output == nullptr)
|
||||||
|
return;
|
||||||
|
|
||||||
|
BMDTimeValue streamTime = 0;
|
||||||
|
double playbackSpeed = 0.0;
|
||||||
|
if (output->GetScheduledStreamTime(mScheduler.TimeScale(), &streamTime, &playbackSpeed) != S_OK || playbackSpeed <= 0.0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
const VideoPlayoutPolicy policy = NormalizeVideoPlayoutPolicy(mPlayoutPolicy);
|
||||||
|
mScheduler.AlignNextScheduleTimeToPlayback(streamTime, policy.targetPrerollFrames);
|
||||||
|
++mState.deckLinkScheduleRealignmentCount;
|
||||||
|
UpdateScheduleLeadTelemetry();
|
||||||
|
}
|
||||||
|
|
||||||
bool DeckLinkSession::ScheduleSystemMemoryFrame(const VideoIOOutputFrame& frame)
|
bool DeckLinkSession::ScheduleSystemMemoryFrame(const VideoIOOutputFrame& frame)
|
||||||
{
|
{
|
||||||
if (output == nullptr || frame.bytes == nullptr || frame.rowBytes <= 0 || frame.height == 0)
|
if (output == nullptr || frame.bytes == nullptr || frame.rowBytes <= 0 || frame.height == 0)
|
||||||
@@ -827,6 +899,18 @@ void DeckLinkSession::HandlePlayoutFrameCompleted(IDeckLinkVideoFrame* completed
|
|||||||
|
|
||||||
VideoIOCompletion completion;
|
VideoIOCompletion completion;
|
||||||
completion.result = TranslateCompletionResult(completionResult);
|
completion.result = TranslateCompletionResult(completionResult);
|
||||||
|
if (completion.result == VideoIOCompletionResult::DisplayedLate || completion.result == VideoIOCompletionResult::Dropped)
|
||||||
|
{
|
||||||
|
if (mScheduleRealignmentArmed)
|
||||||
|
{
|
||||||
|
mScheduleRealignmentPending = true;
|
||||||
|
mScheduleRealignmentArmed = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (completion.result == VideoIOCompletionResult::Completed)
|
||||||
|
{
|
||||||
|
mScheduleRealignmentArmed = true;
|
||||||
|
}
|
||||||
completion.outputFrameBuffer = completedSystemBuffer;
|
completion.outputFrameBuffer = completedSystemBuffer;
|
||||||
mOutputFrameCallback(completion);
|
mOutputFrameCallback(completion);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -75,6 +75,9 @@ private:
|
|||||||
bool AcquireNextOutputVideoFrame(CComPtr<IDeckLinkMutableVideoFrame>& outputVideoFrame);
|
bool AcquireNextOutputVideoFrame(CComPtr<IDeckLinkMutableVideoFrame>& outputVideoFrame);
|
||||||
bool PopulateOutputFrame(IDeckLinkMutableVideoFrame* outputVideoFrame, VideoIOOutputFrame& frame);
|
bool PopulateOutputFrame(IDeckLinkMutableVideoFrame* outputVideoFrame, VideoIOOutputFrame& frame);
|
||||||
bool ScheduleFrame(IDeckLinkMutableVideoFrame* outputVideoFrame);
|
bool ScheduleFrame(IDeckLinkMutableVideoFrame* outputVideoFrame);
|
||||||
|
void UpdateScheduleLeadTelemetry();
|
||||||
|
void MaybeRealignScheduleCursorForLowLead();
|
||||||
|
void RealignScheduleCursorToPlayback();
|
||||||
bool ScheduleSystemMemoryFrame(const VideoIOOutputFrame& frame);
|
bool ScheduleSystemMemoryFrame(const VideoIOOutputFrame& frame);
|
||||||
bool ScheduleBlackFrame(IDeckLinkMutableVideoFrame* outputVideoFrame);
|
bool ScheduleBlackFrame(IDeckLinkMutableVideoFrame* outputVideoFrame);
|
||||||
void RefreshBufferedVideoFrameCount();
|
void RefreshBufferedVideoFrameCount();
|
||||||
@@ -91,6 +94,9 @@ private:
|
|||||||
VideoIOState mState;
|
VideoIOState mState;
|
||||||
VideoPlayoutPolicy mPlayoutPolicy;
|
VideoPlayoutPolicy mPlayoutPolicy;
|
||||||
VideoPlayoutScheduler mScheduler;
|
VideoPlayoutScheduler mScheduler;
|
||||||
|
bool mScheduleRealignmentPending = false;
|
||||||
|
bool mScheduleRealignmentArmed = true;
|
||||||
|
bool mProactiveScheduleRealignmentArmed = true;
|
||||||
InputFrameCallback mInputFrameCallback;
|
InputFrameCallback mInputFrameCallback;
|
||||||
OutputFrameCallback mOutputFrameCallback;
|
OutputFrameCallback mOutputFrameCallback;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -11,13 +11,22 @@ Before adding features here, read the guardrails in [Render Cadence Golden Rules
|
|||||||
```text
|
```text
|
||||||
RenderThread
|
RenderThread
|
||||||
owns a hidden OpenGL context
|
owns a hidden OpenGL context
|
||||||
|
polls the oldest ready input frame without waiting
|
||||||
|
uploads input frames into a render-owned GL texture
|
||||||
renders simple BGRA8 motion at selected cadence
|
renders simple BGRA8 motion at selected cadence
|
||||||
queues async PBO readback
|
queues async PBO readback
|
||||||
publishes completed frames into SystemFrameExchange
|
publishes completed frames into SystemFrameExchange
|
||||||
|
|
||||||
|
InputFrameMailbox
|
||||||
|
owns bounded FIFO CPU input slots
|
||||||
|
keeps a bounded three-ready-frame input buffer for render
|
||||||
|
trims frames beyond that bound to avoid runaway input latency
|
||||||
|
protects the one frame currently being uploaded by render
|
||||||
|
uses a single contiguous copy when capture row stride matches mailbox row stride
|
||||||
|
|
||||||
SystemFrameExchange
|
SystemFrameExchange
|
||||||
owns Free / Rendering / Completed / Scheduled slots
|
owns Free / Rendering / Completed / Scheduled slots
|
||||||
drops old completed unscheduled frames when render needs space
|
preserves completed output frames as a bounded FIFO reserve once they are waiting for playout
|
||||||
protects scheduled frames until DeckLink completion
|
protects scheduled frames until DeckLink completion
|
||||||
|
|
||||||
DeckLinkOutputThread
|
DeckLinkOutputThread
|
||||||
@@ -26,20 +35,26 @@ DeckLinkOutputThread
|
|||||||
never renders
|
never renders
|
||||||
```
|
```
|
||||||
|
|
||||||
Startup warms up real rendered frames before DeckLink scheduled playback starts.
|
Startup builds a small output preroll reserve before DeckLink scheduled playback starts. When DeckLink input is available, startup also waits briefly for three ready input frames before the render thread starts so the first render ticks are deliberate rather than lucky.
|
||||||
|
|
||||||
## Current Scope
|
## Current Scope
|
||||||
|
|
||||||
Included now:
|
Included now:
|
||||||
|
|
||||||
- output-only DeckLink
|
- 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
|
- non-blocking startup when DeckLink output is unavailable
|
||||||
- hidden render-thread-owned OpenGL context
|
- hidden render-thread-owned OpenGL context
|
||||||
- simple smooth-motion renderer
|
- simple smooth-motion renderer
|
||||||
- BGRA8-only output
|
- BGRA8-only output
|
||||||
|
- non-blocking three-frame FIFO input mailbox for render
|
||||||
|
- fast contiguous mailbox copy path for matching input row strides
|
||||||
|
- bounded three-frame input warmup before render cadence starts
|
||||||
|
- render-thread-owned input texture upload
|
||||||
- async PBO readback
|
- async PBO readback
|
||||||
- latest-N system-memory frame exchange
|
- bounded FIFO system-memory frame exchange
|
||||||
- rendered-frame warmup
|
- bounded completed-frame output preroll reserve before DeckLink playback, with DeckLink scheduled depth still targeted at four
|
||||||
|
- conservative DeckLink schedule-lead telemetry and recovery
|
||||||
- background Slang compile of `shaders/happy-accident`
|
- background Slang compile of `shaders/happy-accident`
|
||||||
- app-owned display/render layer model for shader build readiness
|
- app-owned display/render layer model for shader build readiness
|
||||||
- app-owned submission of a completed shader artifact
|
- app-owned submission of a completed shader artifact
|
||||||
@@ -47,31 +62,89 @@ Included now:
|
|||||||
- shared-context GL prepare worker for runtime shader program compile/link
|
- shared-context GL prepare worker for runtime shader program compile/link
|
||||||
- render-thread-only GL program swap once a prepared program is ready
|
- render-thread-only GL program swap once a prepared program is ready
|
||||||
- manifest-driven stateless single-pass shader packages
|
- manifest-driven stateless single-pass shader packages
|
||||||
- HTTP shader list populated from supported 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
|
- default float, vec2, color, boolean, enum, and trigger parameters
|
||||||
- small JSON writer for future HTTP/WebSocket payloads
|
- small JSON writer for future HTTP/WebSocket payloads
|
||||||
- JSON serialization for cadence telemetry snapshots
|
- JSON serialization for cadence telemetry snapshots
|
||||||
- background logging with `log`, `warning`, and `error` levels
|
- background logging with `log`, `warning`, and `error` levels
|
||||||
- local HTTP control server matching the OpenAPI route surface
|
- 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`
|
- startup config provider for `config/runtime-host.json`
|
||||||
- quiet telemetry health monitor
|
- quiet telemetry health monitor
|
||||||
- non-GL frame-exchange tests
|
- non-GL frame-exchange tests
|
||||||
|
- non-GL input-mailbox tests
|
||||||
|
|
||||||
Intentionally not included yet:
|
Intentionally not included yet:
|
||||||
|
|
||||||
- DeckLink input
|
- additional input format conversion/scaling
|
||||||
- multipass shader rendering
|
|
||||||
- temporal/history/feedback shader storage
|
- temporal/history/feedback shader storage
|
||||||
- texture/LUT asset upload
|
- texture/LUT asset upload
|
||||||
- text-parameter rasterization
|
- text-parameter rasterization
|
||||||
- runtime state
|
- runtime state
|
||||||
- OSC/API control
|
- OSC control
|
||||||
|
- persistent control/state writes
|
||||||
|
- trigger event history for stacked repeated pulses
|
||||||
- preview
|
- preview
|
||||||
- screenshots
|
- screenshots
|
||||||
- persistence
|
- persistence
|
||||||
|
|
||||||
Those features should be ported only after the cadence spine is stable.
|
Those features should be ported only after the cadence spine is stable.
|
||||||
|
|
||||||
|
## V1 Feature Parity Checklist
|
||||||
|
|
||||||
|
This tracks parity with `apps/LoopThroughWithOpenGLCompositing`.
|
||||||
|
|
||||||
|
- [x] Stable DeckLink output cadence
|
||||||
|
- [x] BGRA8 system-memory output path
|
||||||
|
- [x] Render thread owns its primary GL context
|
||||||
|
- [x] Output startup warmup before scheduled playback
|
||||||
|
- [x] Non-blocking startup when DeckLink output is unavailable
|
||||||
|
- [x] Runtime shader package discovery
|
||||||
|
- [x] Background Slang shader compile
|
||||||
|
- [x] Shared-context GL shader/program preparation
|
||||||
|
- [x] Render-thread program swap at a frame boundary
|
||||||
|
- [x] Stateless single-pass shader rendering
|
||||||
|
- [x] Stateless named-pass shader rendering
|
||||||
|
- [x] Atomic multipass render-plan commit
|
||||||
|
- [x] Shader add/remove control path
|
||||||
|
- [x] Previous-layer texture handoff for stacked shaders
|
||||||
|
- [x] Supported shader list in HTTP/UI state
|
||||||
|
- [x] Local HTTP server
|
||||||
|
- [x] WebSocket state updates for the UI
|
||||||
|
- [x] OpenAPI document serving
|
||||||
|
- [x] Static control UI serving
|
||||||
|
- [x] Startup config loading from `config/runtime-host.json`
|
||||||
|
- [x] Cadence telemetry JSON
|
||||||
|
- [x] Health logging for schedule/drop/starvation events
|
||||||
|
- [x] Runtime parameter updates from HTTP controls
|
||||||
|
- [x] Layer reorder/bypass/set-shader/update-parameter/reset-parameter HTTP controls
|
||||||
|
- [x] Trigger parameter pulse count/time for latest trigger events
|
||||||
|
- [x] Optional DeckLink input capture
|
||||||
|
- [x] UYVY8 input capture with render-thread GPU decode to shader input texture
|
||||||
|
- [x] Three-frame FIFO CPU input mailbox for render
|
||||||
|
- [x] Fast contiguous input mailbox copy when source/destination stride matches
|
||||||
|
- [x] Bounded three-frame input warmup before render cadence starts
|
||||||
|
- [x] Render-owned input texture upload
|
||||||
|
- [x] Runtime shaders receive input through `gVideoInput`
|
||||||
|
- [x] Live DeckLink input bound to `gVideoInput`
|
||||||
|
- [ ] Input format conversion/scaling
|
||||||
|
- [ ] Temporal history buffers
|
||||||
|
- [ ] Feedback buffers
|
||||||
|
- [ ] Texture asset loading and upload
|
||||||
|
- [ ] LUT asset loading and upload
|
||||||
|
- [ ] Text parameter rasterization
|
||||||
|
- [ ] Trigger history/event buffers for overlapping repeated trigger effects
|
||||||
|
- [ ] Full runtime state store/read model
|
||||||
|
- [ ] Persistent layer stack/config writes
|
||||||
|
- [ ] OSC ingress
|
||||||
|
- [ ] Preview output
|
||||||
|
- [ ] Screenshot capture
|
||||||
|
- [ ] External keying support
|
||||||
|
- [ ] Full V1 health/runtime presentation model
|
||||||
|
|
||||||
## Build
|
## Build
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
@@ -153,10 +226,10 @@ Current endpoints:
|
|||||||
- `GET /ws`: upgrades to a WebSocket and streams state snapshots when they change
|
- `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/openapi.yaml` and `GET /openapi.yaml`: serves the OpenAPI document
|
||||||
- `GET /docs`: serves Swagger UI
|
- `GET /docs`: serves Swagger UI
|
||||||
- `POST /api/layers/add` and `POST /api/layers/remove` mutate the app-owned display layer model only
|
- `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." }`
|
- 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 does not call render work or DeckLink scheduling.
|
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
|
## Optional DeckLink Output
|
||||||
|
|
||||||
@@ -165,7 +238,7 @@ DeckLink output is an optional edge service in this app.
|
|||||||
Startup order is:
|
Startup order is:
|
||||||
|
|
||||||
1. start render thread
|
1. start render thread
|
||||||
2. warm up rendered system-memory frames
|
2. build a bounded completed-frame output preroll reserve at normal render cadence
|
||||||
3. try to attach DeckLink output
|
3. try to attach DeckLink output
|
||||||
4. start telemetry and HTTP either way
|
4. start telemetry and HTTP either way
|
||||||
|
|
||||||
@@ -173,14 +246,72 @@ If DeckLink discovery or output setup fails, the app logs a warning and continue
|
|||||||
|
|
||||||
`/api/state` reports the output status in `videoIO.statusMessage`.
|
`/api/state` reports the output status in `videoIO.statusMessage`.
|
||||||
|
|
||||||
|
## Optional DeckLink Input
|
||||||
|
|
||||||
|
DeckLink input is an optional edge service in this app.
|
||||||
|
|
||||||
|
Startup order is:
|
||||||
|
|
||||||
|
1. create `InputFrameMailbox`
|
||||||
|
2. try to attach DeckLink input for the configured input mode
|
||||||
|
3. prefer BGRA8 capture, otherwise accept raw UYVY8 capture and configure the mailbox for UYVY8 bytes
|
||||||
|
4. start `DeckLinkInputThread`
|
||||||
|
5. wait briefly for three ready input warmup frames before starting render cadence
|
||||||
|
6. leave input absent if discovery, setup, format support, or stream startup fails
|
||||||
|
|
||||||
|
`DeckLinkInput` and `DeckLinkInputThread` are deliberately narrow. They capture BGRA8 frames directly or raw UYVY8 frames into `InputFrameMailbox`; they do not call GL, render, preview, screenshot, shader, or output scheduling code. UYVY8-to-RGBA decode happens later inside the render-thread-owned input texture upload path, so the DeckLink callback stays a capture/copy edge only. The render upload path consumes the oldest ready input frame from a bounded three-ready-frame queue, so the input behaves like a small jitter buffer instead of a latest-frame preview mailbox. The mailbox trims older frames beyond that bound to avoid runaway latency, uses one contiguous copy when the capture row stride matches the configured mailbox row stride, and falls back to row-by-row copy only for padded or mismatched frames. Unsupported input modes or formats outside BGRA8/UYVY8 are reported explicitly and treated as an unavailable edge rather than silently converted.
|
||||||
|
|
||||||
|
Input warmup is startup-only and bounded. It may delay render-thread startup for a short window, but it does not add waits to the steady-state render cadence loop.
|
||||||
|
|
||||||
The app samples telemetry once per second.
|
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:
|
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 DeckLink late/dropped-frame counters increase, including schedule lead and recovery count
|
||||||
- warning when schedule failures increase
|
- warning when schedule failures increase
|
||||||
- error when the app/DeckLink output buffer is starved
|
- error when the app/DeckLink output buffer is starved
|
||||||
|
|
||||||
|
Render cadence telemetry:
|
||||||
|
|
||||||
|
- `clockOverruns`: render cadence overruns where missed time was detected
|
||||||
|
- `clockSkippedFrames`: selected-cadence frame intervals skipped instead of catch-up rendering
|
||||||
|
- `clockOveruns` / `clockSkipped`: compatibility aliases for quick polling scripts
|
||||||
|
|
||||||
|
Input telemetry:
|
||||||
|
|
||||||
|
- `inputFramesReceived`: frames accepted into `InputFrameMailbox`
|
||||||
|
- `inputFramesDropped`: ready input frames dropped or missed because the mailbox was full
|
||||||
|
- `renderFrameMs`: most recent render-thread draw duration, excluding completed-readback copy and readback queue work
|
||||||
|
- `renderFrameBudgetUsedPercent`: most recent render draw time as a percentage of the selected frame budget
|
||||||
|
- `renderFrameMaxMs`: maximum observed render-thread draw duration for this process
|
||||||
|
- `readbackQueueMs`: time spent queueing the most recent async BGRA8 PBO readback
|
||||||
|
- `completedReadbackCopyMs`: time spent mapping/copying the most recent completed readback into system-memory frame storage
|
||||||
|
- `completedDrops`: oldest completed unscheduled system-memory frames dropped because the bounded completed reserve overflowed; this is an app-side reserve drop, not a DeckLink dropped-frame report
|
||||||
|
- `acquireMisses`: times render/readback could not acquire a writable system-memory frame slot; completed frames waiting for playout are preserved instead of being displaced
|
||||||
|
- `deckLinkScheduleLeadAvailable`: whether DeckLink schedule-lead telemetry was available for the latest sample
|
||||||
|
- `deckLinkScheduleLeadFrames`: estimated distance between the DeckLink playback cursor and the next scheduled stream time
|
||||||
|
- `deckLinkPlaybackFrameIndex`: latest sampled DeckLink playback frame index
|
||||||
|
- `deckLinkNextScheduleFrameIndex`: next stream frame index the app intends to schedule
|
||||||
|
- `deckLinkPlaybackStreamTime`: latest sampled DeckLink playback stream time in DeckLink time units
|
||||||
|
- `deckLinkScheduleRealignments`: conservative schedule-cursor recoveries triggered by late/drop pressure or dangerously low lead
|
||||||
|
- `inputConsumeMisses`: render ticks where no ready input frame was available to upload
|
||||||
|
- `inputUploadMisses`: input texture upload attempts that reused the previous GL input texture
|
||||||
|
- `inputReadyFrames`: ready input frames currently queued in `InputFrameMailbox`
|
||||||
|
- `inputReadingFrames`: input frames currently protected while render uploads them
|
||||||
|
- `inputLatestAgeMs`: age of the newest submitted input frame
|
||||||
|
- `inputUploadMs`: render-thread GL upload/decode submission time for the latest uploaded input frame
|
||||||
|
- `inputFormatSupported`: whether the latest frame reaching the render upload path was BGRA8 or UYVY8 compatible
|
||||||
|
- `inputSignalPresent`: whether any input frame has reached the mailbox
|
||||||
|
- `inputCaptureFps`: DeckLink input callback capture rate
|
||||||
|
- `inputConvertMs`: input-edge CPU conversion time; expected to remain `0` for BGRA8 and raw UYVY8 capture because UYVY8 decode is render-thread GPU work
|
||||||
|
- `inputSubmitMs`: time spent copying/submitting the latest captured input frame to `InputFrameMailbox`
|
||||||
|
- `inputCaptureFormat`: selected DeckLink input format (`BGRA8`, `UYVY8`, or `none`)
|
||||||
|
- `inputNoSignalFrames`: DeckLink callbacks reporting no input source
|
||||||
|
- `inputUnsupportedFrames`: input frames rejected before mailbox submission
|
||||||
|
- `inputSubmitMisses`: input frames that could not be submitted to the mailbox
|
||||||
|
|
||||||
|
Runtime shaders continue rendering when input is missing. If no mailbox frame has been uploaded yet, shader samplers use the runtime fallback source texture; once DeckLink input is flowing, shaders such as CRT and trigger-ripple sample the current input through `gVideoInput`.
|
||||||
|
|
||||||
Healthy first-run signs:
|
Healthy first-run signs:
|
||||||
|
|
||||||
- visible DeckLink output is smooth
|
- visible DeckLink output is smooth
|
||||||
@@ -188,6 +319,8 @@ Healthy first-run signs:
|
|||||||
- `scheduleFps` is close to the selected cadence after warmup
|
- `scheduleFps` is close to the selected cadence after warmup
|
||||||
- `scheduled` stays near 4
|
- `scheduled` stays near 4
|
||||||
- `decklinkBuffered` stays near 4 when available
|
- `decklinkBuffered` stays near 4 when available
|
||||||
|
- `deckLinkScheduleLeadFrames` remains positive and stable when available
|
||||||
|
- `deckLinkScheduleRealignments` does not increase continuously
|
||||||
- `late` and `dropped` do not increase continuously
|
- `late` and `dropped` do not increase continuously
|
||||||
- `scheduleFailures` does not increase
|
- `scheduleFailures` does not increase
|
||||||
- `shaderCommitted` becomes `1` after the background Happy Accident compile completes
|
- `shaderCommitted` becomes `1` after the background Happy Accident compile completes
|
||||||
@@ -201,21 +334,37 @@ On startup the app begins compiling the selected shader package on a background
|
|||||||
|
|
||||||
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.
|
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 single-pass packages:
|
Current runtime shader support is deliberately limited to stateless full-frame packages:
|
||||||
|
|
||||||
- one pass only
|
- 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 temporal history
|
||||||
- no feedback storage
|
- no feedback storage
|
||||||
- no texture/LUT assets yet
|
- no texture/LUT assets yet
|
||||||
- no text parameters yet
|
- no text parameters yet
|
||||||
- manifest defaults are used for parameters
|
- manifest defaults initialize parameters
|
||||||
- `gVideoInput` and `gLayerInput` are bound to a small fallback source texture until DeckLink input is added
|
- 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`
|
||||||
|
|
||||||
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 multipass, temporal, feedback, texture-backed, font-backed, or text-parameter shaders are hidden from the control UI for now.
|
Shader source semantics:
|
||||||
|
|
||||||
Runtime shaders are exposed through `RuntimeLayerModel` as display layers with manifest parameter defaults. The model also records whether each layer has a render-ready artifact. Add/remove POST controls mutate this app-owned model and may start background shader builds.
|
- `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`.
|
||||||
|
|
||||||
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 programs to the shared-context prepare worker, swaps in prepared programs when available, removes obsolete GL programs, and renders ready layers in order. Current layer rendering is still deliberately simple: each stateless full-frame shader draws to the output target using fallback source textures until proper layer-input texture handoff is designed.
|
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:
|
Successful handoff signs:
|
||||||
|
|
||||||
@@ -246,6 +395,7 @@ Read:
|
|||||||
- render cadence and DeckLink schedule cadence both held roughly 60 fps
|
- render cadence and DeckLink schedule cadence both held roughly 60 fps
|
||||||
- app scheduled depth stayed at 4
|
- app scheduled depth stayed at 4
|
||||||
- actual DeckLink buffered depth stayed at 4
|
- actual DeckLink buffered depth stayed at 4
|
||||||
|
- DeckLink schedule lead remained positive during healthy playback
|
||||||
- no late frames, dropped frames, or schedule failures were observed
|
- no late frames, dropped frames, or schedule failures were observed
|
||||||
- completed poll misses were benign because playout remained fully fed
|
- completed poll misses were benign because playout remained fully fed
|
||||||
|
|
||||||
@@ -264,11 +414,15 @@ This app keeps the same core behavior but splits it into modules that can grow:
|
|||||||
|
|
||||||
- `frames/`: system-memory handoff
|
- `frames/`: system-memory handoff
|
||||||
- `platform/`: COM/Win32/hidden GL context support
|
- `platform/`: COM/Win32/hidden GL context support
|
||||||
- `render/`: cadence, simple rendering, PBO readback
|
- `render/`: cadence thread, clock, and simple renderer
|
||||||
- `render/RuntimeRenderScene`: render-thread-owned GL scene for ready runtime shader layers
|
- `frames/InputFrameMailbox`: non-blocking bounded FIFO CPU input handoff with contiguous-copy fast path for matching row strides
|
||||||
- `render/RuntimeShaderPrepareWorker`: shared-context runtime shader program compile/link worker
|
- `render/InputFrameTexture`: render-thread-owned upload of the currently acquired CPU input frame into GL, including raw UYVY8 decode into the shader-visible input texture
|
||||||
|
- `render/readback/`: PBO-backed BGRA8 readback and completed-frame publication
|
||||||
|
- `render/runtime/RuntimeRenderScene`: render-thread-owned GL scene for ready runtime shader layers
|
||||||
|
- `render/runtime/RuntimeShaderPrepareWorker`: shared-context runtime shader program compile/link worker
|
||||||
- `runtime/`: app-owned shader layer readiness model, runtime Slang build bridge, and completed artifact handoff
|
- `runtime/`: app-owned shader layer readiness model, runtime Slang build bridge, and completed artifact handoff
|
||||||
- `control/`: local HTTP API edge and runtime-state JSON presentation
|
- `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
|
- `json/`: compact JSON serialization helpers
|
||||||
- `video/`: DeckLink output wrapper and scheduling thread
|
- `video/`: DeckLink output wrapper and scheduling thread
|
||||||
- `telemetry/`: cadence telemetry
|
- `telemetry/`: cadence telemetry
|
||||||
@@ -285,4 +439,4 @@ Only after this app matches the probe's smooth output:
|
|||||||
3. port runtime snapshots/live state
|
3. port runtime snapshots/live state
|
||||||
4. add control services
|
4. add control services
|
||||||
5. add preview/screenshot from system-memory frames
|
5. add preview/screenshot from system-memory frames
|
||||||
6. add DeckLink input as a CPU latest-frame mailbox
|
6. add scaling and additional input format support after the BGRA8/raw-UYVY8 input edge is stable
|
||||||
|
|||||||
@@ -1,19 +1,29 @@
|
|||||||
#include "app/AppConfig.h"
|
#include "app/AppConfig.h"
|
||||||
#include "app/AppConfigProvider.h"
|
#include "app/AppConfigProvider.h"
|
||||||
#include "app/RenderCadenceApp.h"
|
#include "app/RenderCadenceApp.h"
|
||||||
|
#include "frames/InputFrameMailbox.h"
|
||||||
#include "frames/SystemFrameExchange.h"
|
#include "frames/SystemFrameExchange.h"
|
||||||
#include "logging/Logger.h"
|
#include "logging/Logger.h"
|
||||||
#include "render/RenderThread.h"
|
#include "render/RenderThread.h"
|
||||||
|
#include "video/DeckLinkInput.h"
|
||||||
|
#include "video/DeckLinkInputThread.h"
|
||||||
|
#include "DeckLinkDisplayMode.h"
|
||||||
#include "VideoIOFormat.h"
|
#include "VideoIOFormat.h"
|
||||||
|
|
||||||
#include <windows.h>
|
#include <windows.h>
|
||||||
|
|
||||||
|
#include <chrono>
|
||||||
#include <iostream>
|
#include <iostream>
|
||||||
#include <sstream>
|
#include <sstream>
|
||||||
#include <string>
|
#include <string>
|
||||||
|
#include <thread>
|
||||||
|
|
||||||
namespace
|
namespace
|
||||||
{
|
{
|
||||||
|
constexpr std::size_t kDeckLinkTargetBufferedFrames = 4;
|
||||||
|
constexpr std::size_t kReadbackDepth = 6;
|
||||||
|
constexpr std::size_t kWritableOutputReserveFrames = kReadbackDepth + 2;
|
||||||
|
|
||||||
class ComInitGuard
|
class ComInitGuard
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
@@ -37,6 +47,19 @@ private:
|
|||||||
bool mInitialized = false;
|
bool mInitialized = false;
|
||||||
HRESULT mResult = S_OK;
|
HRESULT mResult = S_OK;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
bool WaitForInputWarmup(InputFrameMailbox& mailbox, std::size_t targetReadyFrames, std::chrono::milliseconds timeout)
|
||||||
|
{
|
||||||
|
const auto start = std::chrono::steady_clock::now();
|
||||||
|
while (std::chrono::steady_clock::now() - start < timeout)
|
||||||
|
{
|
||||||
|
const InputFrameMailboxMetrics metrics = mailbox.Metrics();
|
||||||
|
if (metrics.readyCount >= targetReadyFrames)
|
||||||
|
return true;
|
||||||
|
std::this_thread::sleep_for(std::chrono::milliseconds(2));
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
int main(int argc, char** argv)
|
int main(int argc, char** argv)
|
||||||
@@ -76,24 +99,120 @@ int main(int argc, char** argv)
|
|||||||
frameExchangeConfig.height);
|
frameExchangeConfig.height);
|
||||||
frameExchangeConfig.pixelFormat = VideoIOPixelFormat::Bgra8;
|
frameExchangeConfig.pixelFormat = VideoIOPixelFormat::Bgra8;
|
||||||
frameExchangeConfig.rowBytes = VideoIORowBytes(frameExchangeConfig.pixelFormat, frameExchangeConfig.width);
|
frameExchangeConfig.rowBytes = VideoIORowBytes(frameExchangeConfig.pixelFormat, frameExchangeConfig.width);
|
||||||
frameExchangeConfig.capacity = 12;
|
frameExchangeConfig.capacity =
|
||||||
|
appConfig.warmupCompletedFrames +
|
||||||
|
kDeckLinkTargetBufferedFrames +
|
||||||
|
kWritableOutputReserveFrames;
|
||||||
|
frameExchangeConfig.maxCompletedFrames = appConfig.warmupCompletedFrames;
|
||||||
|
|
||||||
SystemFrameExchange frameExchange(frameExchangeConfig);
|
SystemFrameExchange frameExchange(frameExchangeConfig);
|
||||||
|
|
||||||
|
InputFrameMailboxConfig inputMailboxConfig;
|
||||||
|
RenderCadenceCompositor::VideoFormatDimensions(
|
||||||
|
appConfig.inputVideoFormat,
|
||||||
|
inputMailboxConfig.width,
|
||||||
|
inputMailboxConfig.height);
|
||||||
|
inputMailboxConfig.pixelFormat = VideoIOPixelFormat::Bgra8;
|
||||||
|
inputMailboxConfig.rowBytes = VideoIORowBytes(inputMailboxConfig.pixelFormat, inputMailboxConfig.width);
|
||||||
|
inputMailboxConfig.capacity = 4;
|
||||||
|
inputMailboxConfig.maxReadyFrames = 3;
|
||||||
|
InputFrameMailbox inputMailbox(inputMailboxConfig);
|
||||||
|
|
||||||
|
VideoFormat inputVideoMode;
|
||||||
|
VideoFormat outputVideoMode;
|
||||||
|
std::string inputVideoModeError;
|
||||||
|
const bool inputVideoModeResolved = ResolveConfiguredVideoFormat(appConfig.inputVideoFormat, appConfig.inputFrameRate, inputVideoMode);
|
||||||
|
const bool outputVideoModeResolved = ResolveConfiguredVideoFormat(appConfig.outputVideoFormat, appConfig.outputFrameRate, outputVideoMode);
|
||||||
|
if (!inputVideoModeResolved)
|
||||||
|
{
|
||||||
|
inputVideoModeError = "Unsupported DeckLink inputVideoFormat/inputFrameRate in config/runtime-host.json: " +
|
||||||
|
appConfig.inputVideoFormat + " / " + appConfig.inputFrameRate;
|
||||||
|
RenderCadenceCompositor::LogWarning("app", inputVideoModeError);
|
||||||
|
}
|
||||||
|
if (!outputVideoModeResolved)
|
||||||
|
{
|
||||||
|
RenderCadenceCompositor::LogWarning(
|
||||||
|
"app",
|
||||||
|
"Unsupported DeckLink outputVideoFormat/outputFrameRate in config/runtime-host.json; render cadence will use parsed frame-rate fallback: " +
|
||||||
|
appConfig.outputVideoFormat + " / " + appConfig.outputFrameRate);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
appConfig.deckLink.outputVideoMode = outputVideoMode;
|
||||||
|
}
|
||||||
|
|
||||||
|
RenderCadenceCompositor::DeckLinkInput deckLinkInput(inputMailbox);
|
||||||
|
RenderCadenceCompositor::DeckLinkInputThread deckLinkInputThread(deckLinkInput);
|
||||||
|
bool deckLinkInputStarted = false;
|
||||||
|
if (inputVideoModeResolved)
|
||||||
|
{
|
||||||
|
RenderCadenceCompositor::DeckLinkInputConfig deckLinkInputConfig;
|
||||||
|
deckLinkInputConfig.videoFormat = inputVideoMode;
|
||||||
|
std::string deckLinkInputError;
|
||||||
|
if (deckLinkInput.Initialize(deckLinkInputConfig, deckLinkInputError))
|
||||||
|
{
|
||||||
|
inputMailboxConfig.pixelFormat = deckLinkInput.CapturePixelFormat();
|
||||||
|
inputMailboxConfig.rowBytes = VideoIORowBytes(inputMailboxConfig.pixelFormat, inputMailboxConfig.width);
|
||||||
|
inputMailbox.Configure(inputMailboxConfig);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (deckLinkInput.IsInitialized() && deckLinkInputThread.Start(deckLinkInputError))
|
||||||
|
{
|
||||||
|
deckLinkInputStarted = true;
|
||||||
|
RenderCadenceCompositor::Log("app", "DeckLink input edge started for " + inputVideoMode.displayName + ".");
|
||||||
|
RenderCadenceCompositor::Log("app", "Waiting for DeckLink input warmup frames.");
|
||||||
|
constexpr std::size_t kInputStartupBufferedFrames = 3;
|
||||||
|
constexpr std::chrono::milliseconds kInputWarmupTimeout(1000);
|
||||||
|
if (WaitForInputWarmup(inputMailbox, kInputStartupBufferedFrames, kInputWarmupTimeout))
|
||||||
|
{
|
||||||
|
const InputFrameMailboxMetrics metrics = inputMailbox.Metrics();
|
||||||
|
RenderCadenceCompositor::Log(
|
||||||
|
"app",
|
||||||
|
"DeckLink input warmup complete. ready=" + std::to_string(metrics.readyCount) +
|
||||||
|
" submitted=" + std::to_string(metrics.submittedFrames) + ".");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
const InputFrameMailboxMetrics metrics = inputMailbox.Metrics();
|
||||||
|
RenderCadenceCompositor::LogWarning(
|
||||||
|
"app",
|
||||||
|
"DeckLink input warmup timed out; starting render cadence with current input buffer. ready=" +
|
||||||
|
std::to_string(metrics.readyCount) + " submitted=" + std::to_string(metrics.submittedFrames) + ".");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
RenderCadenceCompositor::LogWarning("app", "DeckLink input edge unavailable; runtime shaders will use fallback input until a real input edge is available. " + deckLinkInputError);
|
||||||
|
deckLinkInput.ReleaseResources();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
RenderCadenceCompositor::LogWarning("app", "DeckLink input mode was not resolved; runtime shaders will use fallback input until a real input edge is available.");
|
||||||
|
}
|
||||||
|
|
||||||
RenderThread::Config renderConfig;
|
RenderThread::Config renderConfig;
|
||||||
renderConfig.width = frameExchangeConfig.width;
|
renderConfig.width = frameExchangeConfig.width;
|
||||||
renderConfig.height = frameExchangeConfig.height;
|
renderConfig.height = frameExchangeConfig.height;
|
||||||
renderConfig.frameDurationMilliseconds = RenderCadenceCompositor::FrameDurationMillisecondsFromRateString(appConfig.outputFrameRate);
|
const double fallbackFrameDurationMilliseconds = RenderCadenceCompositor::FrameDurationMillisecondsFromRateString(appConfig.outputFrameRate);
|
||||||
renderConfig.pboDepth = 6;
|
renderConfig.frameDurationMilliseconds = outputVideoModeResolved
|
||||||
|
? RenderCadenceCompositor::FrameDurationMillisecondsFromDisplayMode(outputVideoMode.displayMode, fallbackFrameDurationMilliseconds)
|
||||||
|
: fallbackFrameDurationMilliseconds;
|
||||||
|
renderConfig.pboDepth = kReadbackDepth;
|
||||||
|
|
||||||
RenderThread renderThread(frameExchange, renderConfig);
|
RenderThread renderThread(frameExchange, &inputMailbox, renderConfig);
|
||||||
|
|
||||||
RenderCadenceCompositor::RenderCadenceApp<RenderThread, SystemFrameExchange> app(renderThread, frameExchange, appConfig);
|
RenderCadenceCompositor::RenderCadenceApp<RenderThread, SystemFrameExchange> app(renderThread, frameExchange, appConfig);
|
||||||
|
app.SetDeckLinkInputMetricsProvider([&deckLinkInput]() {
|
||||||
|
return deckLinkInput.Metrics();
|
||||||
|
});
|
||||||
|
|
||||||
std::string error;
|
std::string error;
|
||||||
if (!app.Start(error))
|
if (!app.Start(error))
|
||||||
{
|
{
|
||||||
RenderCadenceCompositor::LogError("app", "RenderCadenceCompositor start failed: " + error);
|
RenderCadenceCompositor::LogError("app", "RenderCadenceCompositor start failed: " + error);
|
||||||
|
if (deckLinkInputStarted)
|
||||||
|
deckLinkInputThread.Stop();
|
||||||
RenderCadenceCompositor::Logger::Instance().Stop();
|
RenderCadenceCompositor::Logger::Instance().Stop();
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
@@ -101,6 +220,8 @@ int main(int argc, char** argv)
|
|||||||
std::string line;
|
std::string line;
|
||||||
std::getline(std::cin, line);
|
std::getline(std::cin, line);
|
||||||
app.Stop();
|
app.Stop();
|
||||||
|
if (deckLinkInputStarted)
|
||||||
|
deckLinkInputThread.Stop();
|
||||||
RenderCadenceCompositor::Log("app", "RenderCadenceCompositor stopped.");
|
RenderCadenceCompositor::Log("app", "RenderCadenceCompositor stopped.");
|
||||||
RenderCadenceCompositor::Logger::Instance().Stop();
|
RenderCadenceCompositor::Logger::Instance().Stop();
|
||||||
return 0;
|
return 0;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include "../control/HttpControlServer.h"
|
#include "../control/http/HttpControlServer.h"
|
||||||
#include "../logging/Logger.h"
|
#include "../logging/Logger.h"
|
||||||
#include "../telemetry/TelemetryHealthMonitor.h"
|
#include "../telemetry/TelemetryHealthMonitor.h"
|
||||||
#include "../video/DeckLinkOutput.h"
|
#include "../video/DeckLinkOutput.h"
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <cctype>
|
#include <cctype>
|
||||||
|
#include <cstdint>
|
||||||
#include <cstdlib>
|
#include <cstdlib>
|
||||||
#include <fstream>
|
#include <fstream>
|
||||||
#include <sstream>
|
#include <sstream>
|
||||||
@@ -181,6 +182,50 @@ double FrameDurationMillisecondsFromRateString(const std::string& rateText, doub
|
|||||||
return 1000.0 / rate;
|
return 1000.0 / rate;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
double FrameDurationMillisecondsFromDisplayMode(BMDDisplayMode displayMode, double fallbackMilliseconds)
|
||||||
|
{
|
||||||
|
struct ModeRate
|
||||||
|
{
|
||||||
|
BMDDisplayMode mode;
|
||||||
|
int64_t frameDuration;
|
||||||
|
int64_t timeScale;
|
||||||
|
};
|
||||||
|
|
||||||
|
static const ModeRate rates[] =
|
||||||
|
{
|
||||||
|
{ bmdModeHD720p50, 1, 50 },
|
||||||
|
{ bmdModeHD720p5994, 1001, 60000 },
|
||||||
|
{ bmdModeHD720p60, 1, 60 },
|
||||||
|
{ bmdModeHD1080i50, 1, 25 },
|
||||||
|
{ bmdModeHD1080i5994, 1001, 30000 },
|
||||||
|
{ bmdModeHD1080i6000, 1, 30 },
|
||||||
|
{ bmdModeHD1080p2398, 1001, 24000 },
|
||||||
|
{ bmdModeHD1080p24, 1, 24 },
|
||||||
|
{ bmdModeHD1080p25, 1, 25 },
|
||||||
|
{ bmdModeHD1080p2997, 1001, 30000 },
|
||||||
|
{ bmdModeHD1080p30, 1, 30 },
|
||||||
|
{ bmdModeHD1080p50, 1, 50 },
|
||||||
|
{ bmdModeHD1080p5994, 1001, 60000 },
|
||||||
|
{ bmdModeHD1080p6000, 1, 60 },
|
||||||
|
{ bmdMode4K2160p2398, 1001, 24000 },
|
||||||
|
{ bmdMode4K2160p24, 1, 24 },
|
||||||
|
{ bmdMode4K2160p25, 1, 25 },
|
||||||
|
{ bmdMode4K2160p2997, 1001, 30000 },
|
||||||
|
{ bmdMode4K2160p30, 1, 30 },
|
||||||
|
{ bmdMode4K2160p50, 1, 50 },
|
||||||
|
{ bmdMode4K2160p5994, 1001, 60000 },
|
||||||
|
{ bmdMode4K2160p60, 1, 60 }
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const ModeRate& rate : rates)
|
||||||
|
{
|
||||||
|
if (rate.mode == displayMode && rate.timeScale > 0)
|
||||||
|
return (static_cast<double>(rate.frameDuration) * 1000.0) / static_cast<double>(rate.timeScale);
|
||||||
|
}
|
||||||
|
|
||||||
|
return fallbackMilliseconds;
|
||||||
|
}
|
||||||
|
|
||||||
void VideoFormatDimensions(const std::string& formatName, unsigned& width, unsigned& height)
|
void VideoFormatDimensions(const std::string& formatName, unsigned& width, unsigned& height)
|
||||||
{
|
{
|
||||||
std::string normalized = formatName;
|
std::string normalized = formatName;
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include "AppConfig.h"
|
#include "AppConfig.h"
|
||||||
|
#include "DeckLinkDisplayMode.h"
|
||||||
|
|
||||||
#include <filesystem>
|
#include <filesystem>
|
||||||
#include <string>
|
#include <string>
|
||||||
@@ -27,6 +28,7 @@ private:
|
|||||||
};
|
};
|
||||||
|
|
||||||
double FrameDurationMillisecondsFromRateString(const std::string& rateText, double fallbackRate = 59.94);
|
double FrameDurationMillisecondsFromRateString(const std::string& rateText, double fallbackRate = 59.94);
|
||||||
|
double FrameDurationMillisecondsFromDisplayMode(BMDDisplayMode displayMode, double fallbackMilliseconds);
|
||||||
void VideoFormatDimensions(const std::string& formatName, unsigned& width, unsigned& height);
|
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 FindConfigFile(const std::filesystem::path& relativePath = "config/runtime-host.json");
|
||||||
std::filesystem::path FindRepoPath(const std::filesystem::path& relativePath);
|
std::filesystem::path FindRepoPath(const std::filesystem::path& relativePath);
|
||||||
|
|||||||
@@ -6,11 +6,13 @@
|
|||||||
#include "../logging/Logger.h"
|
#include "../logging/Logger.h"
|
||||||
#include "../control/RuntimeStateJson.h"
|
#include "../control/RuntimeStateJson.h"
|
||||||
#include "../telemetry/TelemetryHealthMonitor.h"
|
#include "../telemetry/TelemetryHealthMonitor.h"
|
||||||
|
#include "../video/DeckLinkInput.h"
|
||||||
#include "../video/DeckLinkOutput.h"
|
#include "../video/DeckLinkOutput.h"
|
||||||
#include "../video/DeckLinkOutputThread.h"
|
#include "../video/DeckLinkOutputThread.h"
|
||||||
|
|
||||||
#include <chrono>
|
#include <chrono>
|
||||||
#include <filesystem>
|
#include <filesystem>
|
||||||
|
#include <functional>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <thread>
|
#include <thread>
|
||||||
#include <type_traits>
|
#include <type_traits>
|
||||||
@@ -85,10 +87,8 @@ public:
|
|||||||
}
|
}
|
||||||
mRuntimeLayers.StartStartupBuild(mConfig.runtimeShaderId);
|
mRuntimeLayers.StartStartupBuild(mConfig.runtimeShaderId);
|
||||||
|
|
||||||
Log("app", "Waiting for rendered warmup frames.");
|
if (!BuildSettledOutputReserve(error))
|
||||||
if (!mFrameExchange.WaitForCompletedDepth(mConfig.warmupCompletedFrames, mConfig.warmupTimeout))
|
|
||||||
{
|
{
|
||||||
error = "Timed out waiting for rendered warmup frames.";
|
|
||||||
LogError("app", error);
|
LogError("app", error);
|
||||||
Stop();
|
Stop();
|
||||||
return false;
|
return false;
|
||||||
@@ -118,6 +118,10 @@ public:
|
|||||||
|
|
||||||
bool Started() const { return mStarted; }
|
bool Started() const { return mStarted; }
|
||||||
const DeckLinkOutput& Output() const { return mOutput; }
|
const DeckLinkOutput& Output() const { return mOutput; }
|
||||||
|
void SetDeckLinkInputMetricsProvider(std::function<DeckLinkInputMetrics()> provider)
|
||||||
|
{
|
||||||
|
mDeckLinkInputMetricsProvider = std::move(provider);
|
||||||
|
}
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void StartOptionalVideoOutput()
|
void StartOptionalVideoOutput()
|
||||||
@@ -159,6 +163,25 @@ private:
|
|||||||
mVideoOutputEnabled = true;
|
mVideoOutputEnabled = true;
|
||||||
mVideoOutputStatus = "DeckLink scheduled output running.";
|
mVideoOutputStatus = "DeckLink scheduled output running.";
|
||||||
Log("app", mVideoOutputStatus);
|
Log("app", mVideoOutputStatus);
|
||||||
|
Log(
|
||||||
|
"app",
|
||||||
|
"DeckLink output mode: " + mOutput.State().outputDisplayModeName +
|
||||||
|
", frame budget " + std::to_string(mOutput.State().frameBudgetMilliseconds) + " ms.");
|
||||||
|
}
|
||||||
|
|
||||||
|
bool BuildSettledOutputReserve(std::string& error)
|
||||||
|
{
|
||||||
|
const auto reserveTimeout = mConfig.warmupTimeout;
|
||||||
|
Log("app",
|
||||||
|
"Building output preroll reserve: waiting for " + std::to_string(mConfig.warmupCompletedFrames) +
|
||||||
|
" completed frame(s).");
|
||||||
|
if (mFrameExchange.WaitForCompletedDepth(mConfig.warmupCompletedFrames, reserveTimeout))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
error = "Timed out waiting for output preroll reserve.";
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
void DisableVideoOutput(const std::string& reason)
|
void DisableVideoOutput(const std::string& reason)
|
||||||
@@ -184,6 +207,13 @@ private:
|
|||||||
callbacks.removeLayer = [this](const std::string& body) {
|
callbacks.removeLayer = [this](const std::string& body) {
|
||||||
return mRuntimeLayers.HandleRemoveLayer(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;
|
std::string error;
|
||||||
if (!mHttpServer.Start(
|
if (!mHttpServer.Start(
|
||||||
@@ -201,6 +231,7 @@ private:
|
|||||||
std::string BuildStateJson()
|
std::string BuildStateJson()
|
||||||
{
|
{
|
||||||
CadenceTelemetrySnapshot telemetry = mHttpTelemetry.Sample(mFrameExchange, mOutput, mOutputThread, mRenderThread);
|
CadenceTelemetrySnapshot telemetry = mHttpTelemetry.Sample(mFrameExchange, mOutput, mOutputThread, mRenderThread);
|
||||||
|
ApplyDeckLinkInputMetrics(telemetry);
|
||||||
RuntimeLayerModelSnapshot layerSnapshot = mRuntimeLayers.Snapshot(telemetry);
|
RuntimeLayerModelSnapshot layerSnapshot = mRuntimeLayers.Snapshot(telemetry);
|
||||||
return RuntimeStateToJson(RuntimeStateJsonInput{
|
return RuntimeStateToJson(RuntimeStateJsonInput{
|
||||||
mConfig,
|
mConfig,
|
||||||
@@ -213,6 +244,23 @@ private:
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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
|
bool WaitForPreroll() const
|
||||||
{
|
{
|
||||||
const auto deadline = std::chrono::steady_clock::now() + mConfig.prerollTimeout;
|
const auto deadline = std::chrono::steady_clock::now() + mConfig.prerollTimeout;
|
||||||
@@ -234,6 +282,8 @@ private:
|
|||||||
CadenceTelemetry mHttpTelemetry;
|
CadenceTelemetry mHttpTelemetry;
|
||||||
HttpControlServer mHttpServer;
|
HttpControlServer mHttpServer;
|
||||||
RuntimeLayerController mRuntimeLayers;
|
RuntimeLayerController mRuntimeLayers;
|
||||||
|
std::function<DeckLinkInputMetrics()> mDeckLinkInputMetricsProvider;
|
||||||
|
uint64_t mLastInputCapturedFrames = 0;
|
||||||
bool mStarted = false;
|
bool mStarted = false;
|
||||||
bool mVideoOutputEnabled = false;
|
bool mVideoOutputEnabled = false;
|
||||||
std::string mVideoOutputStatus = "DeckLink output not started.";
|
std::string mVideoOutputStatus = "DeckLink output not started.";
|
||||||
|
|||||||
@@ -1,11 +1,7 @@
|
|||||||
#include "RuntimeLayerController.h"
|
#include "RuntimeLayerController.h"
|
||||||
|
|
||||||
#include "AppConfigProvider.h"
|
|
||||||
#include "RuntimeJson.h"
|
|
||||||
#include "../logging/Logger.h"
|
#include "../logging/Logger.h"
|
||||||
|
|
||||||
#include <filesystem>
|
|
||||||
|
|
||||||
namespace RenderCadenceCompositor
|
namespace RenderCadenceCompositor
|
||||||
{
|
{
|
||||||
RuntimeLayerController::RuntimeLayerController(RenderLayerPublisher publisher) :
|
RuntimeLayerController::RuntimeLayerController(RenderLayerPublisher publisher) :
|
||||||
@@ -48,48 +44,6 @@ void RuntimeLayerController::Stop()
|
|||||||
StopAllRuntimeShaderBuilds();
|
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() };
|
|
||||||
}
|
|
||||||
|
|
||||||
RuntimeLayerModelSnapshot RuntimeLayerController::Snapshot(const CadenceTelemetrySnapshot& telemetry) const
|
RuntimeLayerModelSnapshot RuntimeLayerController::Snapshot(const CadenceTelemetrySnapshot& telemetry) const
|
||||||
{
|
{
|
||||||
std::lock_guard<std::mutex> lock(mRuntimeLayerMutex);
|
std::lock_guard<std::mutex> lock(mRuntimeLayerMutex);
|
||||||
@@ -102,113 +56,6 @@ RuntimeLayerModelSnapshot RuntimeLayerController::Snapshot(const CadenceTelemetr
|
|||||||
return snapshot;
|
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()
|
void RuntimeLayerController::PublishRuntimeRenderLayers()
|
||||||
{
|
{
|
||||||
if (!mPublisher)
|
if (!mPublisher)
|
||||||
@@ -242,31 +89,4 @@ void RuntimeLayerController::MarkRuntimeBuildFailedForLayer(const std::string& l
|
|||||||
LogWarning("runtime-shader", 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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include "../control/ControlActionResult.h"
|
#include "../control/ControlActionResult.h"
|
||||||
|
#include "../control/RuntimeControlCommand.h"
|
||||||
#include "../runtime/RuntimeLayerModel.h"
|
#include "../runtime/RuntimeLayerModel.h"
|
||||||
#include "../runtime/RuntimeShaderBridge.h"
|
#include "../runtime/RuntimeShaderBridge.h"
|
||||||
#include "../runtime/SupportedShaderCatalog.h"
|
#include "../runtime/SupportedShaderCatalog.h"
|
||||||
@@ -32,6 +33,7 @@ public:
|
|||||||
|
|
||||||
ControlActionResult HandleAddLayer(const std::string& body);
|
ControlActionResult HandleAddLayer(const std::string& body);
|
||||||
ControlActionResult HandleRemoveLayer(const std::string& body);
|
ControlActionResult HandleRemoveLayer(const std::string& body);
|
||||||
|
ControlActionResult HandleControlCommand(const RuntimeControlCommand& command);
|
||||||
|
|
||||||
RuntimeLayerModelSnapshot Snapshot(const CadenceTelemetrySnapshot& telemetry) const;
|
RuntimeLayerModelSnapshot Snapshot(const CadenceTelemetrySnapshot& telemetry) const;
|
||||||
const SupportedShaderCatalog& ShaderCatalog() const { return mShaderCatalog; }
|
const SupportedShaderCatalog& ShaderCatalog() const { return mShaderCatalog; }
|
||||||
|
|||||||
122
apps/RenderCadenceCompositor/app/RuntimeLayerControllerBuild.cpp
Normal file
122
apps/RenderCadenceCompositor/app/RuntimeLayerControllerBuild.cpp
Normal file
@@ -0,0 +1,122 @@
|
|||||||
|
#include "RuntimeLayerController.h"
|
||||||
|
|
||||||
|
#include "AppConfigProvider.h"
|
||||||
|
#include "../logging/Logger.h"
|
||||||
|
|
||||||
|
#include <filesystem>
|
||||||
|
|
||||||
|
namespace RenderCadenceCompositor
|
||||||
|
{
|
||||||
|
void RuntimeLayerController::LoadSupportedShaderCatalog(const std::string& shaderLibrary, unsigned maxTemporalHistoryFrames)
|
||||||
|
{
|
||||||
|
const std::filesystem::path shaderRoot = FindRepoPath(shaderLibrary);
|
||||||
|
std::string error;
|
||||||
|
if (!mShaderCatalog.Load(shaderRoot, maxTemporalHistoryFrames, error))
|
||||||
|
{
|
||||||
|
LogWarning("runtime-shader", "Supported shader catalog is empty: " + error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Log("runtime-shader", "Supported shader catalog loaded with " + std::to_string(mShaderCatalog.Shaders().size()) + " shader(s).");
|
||||||
|
}
|
||||||
|
|
||||||
|
void RuntimeLayerController::InitializeLayerModel(std::string& runtimeShaderId)
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(mRuntimeLayerMutex);
|
||||||
|
std::string error;
|
||||||
|
if (!mRuntimeLayerModel.InitializeSingleLayer(mShaderCatalog, runtimeShaderId, error))
|
||||||
|
{
|
||||||
|
LogWarning("runtime-shader", error + " Runtime shader build disabled.");
|
||||||
|
runtimeShaderId.clear();
|
||||||
|
mRuntimeLayerModel.Clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void RuntimeLayerController::StartLayerShaderBuild(const std::string& layerId, const std::string& shaderId)
|
||||||
|
{
|
||||||
|
CleanupRetiredShaderBuilds();
|
||||||
|
RetireLayerShaderBuild(layerId);
|
||||||
|
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(mRuntimeLayerMutex);
|
||||||
|
std::string error;
|
||||||
|
mRuntimeLayerModel.MarkBuildStarted(layerId, "Runtime Slang build started for shader '" + shaderId + "'.", error);
|
||||||
|
}
|
||||||
|
|
||||||
|
auto bridge = std::make_unique<RuntimeShaderBridge>();
|
||||||
|
RuntimeShaderBridge* bridgePtr = bridge.get();
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(mShaderBuildMutex);
|
||||||
|
mShaderBuilds[layerId] = std::move(bridge);
|
||||||
|
}
|
||||||
|
|
||||||
|
bridgePtr->Start(
|
||||||
|
layerId,
|
||||||
|
shaderId,
|
||||||
|
[this](const RuntimeShaderArtifact& artifact) {
|
||||||
|
if (MarkRuntimeBuildReady(artifact))
|
||||||
|
PublishRuntimeRenderLayers();
|
||||||
|
},
|
||||||
|
[this, layerId](const std::string& message) {
|
||||||
|
MarkRuntimeBuildFailedForLayer(layerId, message);
|
||||||
|
LogError("runtime-shader", "Runtime Slang build failed: " + message);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void RuntimeLayerController::RetireLayerShaderBuild(const std::string& layerId)
|
||||||
|
{
|
||||||
|
std::unique_ptr<RuntimeShaderBridge> bridge;
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(mShaderBuildMutex);
|
||||||
|
auto bridgeIt = mShaderBuilds.find(layerId);
|
||||||
|
if (bridgeIt == mShaderBuilds.end())
|
||||||
|
return;
|
||||||
|
bridge = std::move(bridgeIt->second);
|
||||||
|
mShaderBuilds.erase(bridgeIt);
|
||||||
|
bridge->RequestStop();
|
||||||
|
mRetiredShaderBuilds.push_back(std::move(bridge));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void RuntimeLayerController::CleanupRetiredShaderBuilds()
|
||||||
|
{
|
||||||
|
std::vector<std::unique_ptr<RuntimeShaderBridge>> readyToStop;
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(mShaderBuildMutex);
|
||||||
|
for (auto it = mRetiredShaderBuilds.begin(); it != mRetiredShaderBuilds.end();)
|
||||||
|
{
|
||||||
|
if ((*it)->CanStopWithoutWaiting())
|
||||||
|
{
|
||||||
|
readyToStop.push_back(std::move(*it));
|
||||||
|
it = mRetiredShaderBuilds.erase(it);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
++it;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (std::unique_ptr<RuntimeShaderBridge>& bridge : readyToStop)
|
||||||
|
bridge->Stop();
|
||||||
|
}
|
||||||
|
|
||||||
|
void RuntimeLayerController::StopAllRuntimeShaderBuilds()
|
||||||
|
{
|
||||||
|
std::map<std::string, std::unique_ptr<RuntimeShaderBridge>> builds;
|
||||||
|
std::vector<std::unique_ptr<RuntimeShaderBridge>> retiredBuilds;
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(mShaderBuildMutex);
|
||||||
|
builds.swap(mShaderBuilds);
|
||||||
|
retiredBuilds.swap(mRetiredShaderBuilds);
|
||||||
|
}
|
||||||
|
for (auto& entry : builds)
|
||||||
|
entry.second->Stop();
|
||||||
|
for (auto& bridge : retiredBuilds)
|
||||||
|
bridge->Stop();
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string RuntimeLayerController::FirstRuntimeLayerId() const
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(mRuntimeLayerMutex);
|
||||||
|
return mRuntimeLayerModel.FirstLayerId();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,160 @@
|
|||||||
|
#include "RuntimeLayerController.h"
|
||||||
|
|
||||||
|
#include "RuntimeJson.h"
|
||||||
|
#include "../logging/Logger.h"
|
||||||
|
|
||||||
|
namespace RenderCadenceCompositor
|
||||||
|
{
|
||||||
|
ControlActionResult RuntimeLayerController::HandleAddLayer(const std::string& body)
|
||||||
|
{
|
||||||
|
CleanupRetiredShaderBuilds();
|
||||||
|
|
||||||
|
std::string shaderId;
|
||||||
|
std::string error;
|
||||||
|
if (!ExtractStringField(body, "shaderId", shaderId, error))
|
||||||
|
return { false, error };
|
||||||
|
|
||||||
|
std::string layerId;
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(mRuntimeLayerMutex);
|
||||||
|
if (!mRuntimeLayerModel.AddLayer(mShaderCatalog, shaderId, layerId, error))
|
||||||
|
return { false, error };
|
||||||
|
}
|
||||||
|
|
||||||
|
Log("runtime-shader", "Layer added: " + layerId + " shader=" + shaderId);
|
||||||
|
StartLayerShaderBuild(layerId, shaderId);
|
||||||
|
return { true, std::string() };
|
||||||
|
}
|
||||||
|
|
||||||
|
ControlActionResult RuntimeLayerController::HandleRemoveLayer(const std::string& body)
|
||||||
|
{
|
||||||
|
CleanupRetiredShaderBuilds();
|
||||||
|
|
||||||
|
std::string layerId;
|
||||||
|
std::string error;
|
||||||
|
if (!ExtractStringField(body, "layerId", layerId, error))
|
||||||
|
return { false, error };
|
||||||
|
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(mRuntimeLayerMutex);
|
||||||
|
if (!mRuntimeLayerModel.RemoveLayer(layerId, error))
|
||||||
|
return { false, error };
|
||||||
|
}
|
||||||
|
|
||||||
|
Log("runtime-shader", "Layer removed: " + layerId);
|
||||||
|
RetireLayerShaderBuild(layerId);
|
||||||
|
PublishRuntimeRenderLayers();
|
||||||
|
return { true, std::string() };
|
||||||
|
}
|
||||||
|
|
||||||
|
ControlActionResult RuntimeLayerController::HandleControlCommand(const RuntimeControlCommand& command)
|
||||||
|
{
|
||||||
|
CleanupRetiredShaderBuilds();
|
||||||
|
|
||||||
|
std::string error;
|
||||||
|
switch (command.type)
|
||||||
|
{
|
||||||
|
case RuntimeControlCommandType::AddLayer:
|
||||||
|
{
|
||||||
|
std::string layerId;
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(mRuntimeLayerMutex);
|
||||||
|
if (!mRuntimeLayerModel.AddLayer(mShaderCatalog, command.shaderId, layerId, error))
|
||||||
|
return { false, error };
|
||||||
|
}
|
||||||
|
Log("runtime-shader", "Layer added: " + layerId + " shader=" + command.shaderId);
|
||||||
|
StartLayerShaderBuild(layerId, command.shaderId);
|
||||||
|
return { true, std::string() };
|
||||||
|
}
|
||||||
|
case RuntimeControlCommandType::RemoveLayer:
|
||||||
|
{
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(mRuntimeLayerMutex);
|
||||||
|
if (!mRuntimeLayerModel.RemoveLayer(command.layerId, error))
|
||||||
|
return { false, error };
|
||||||
|
}
|
||||||
|
Log("runtime-shader", "Layer removed: " + command.layerId);
|
||||||
|
RetireLayerShaderBuild(command.layerId);
|
||||||
|
PublishRuntimeRenderLayers();
|
||||||
|
return { true, std::string() };
|
||||||
|
}
|
||||||
|
case RuntimeControlCommandType::ReorderLayer:
|
||||||
|
{
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(mRuntimeLayerMutex);
|
||||||
|
if (!mRuntimeLayerModel.ReorderLayer(command.layerId, command.targetIndex, error))
|
||||||
|
return { false, error };
|
||||||
|
}
|
||||||
|
PublishRuntimeRenderLayers();
|
||||||
|
return { true, std::string() };
|
||||||
|
}
|
||||||
|
case RuntimeControlCommandType::SetLayerBypass:
|
||||||
|
{
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(mRuntimeLayerMutex);
|
||||||
|
if (!mRuntimeLayerModel.SetLayerBypass(command.layerId, command.bypass, error))
|
||||||
|
return { false, error };
|
||||||
|
}
|
||||||
|
PublishRuntimeRenderLayers();
|
||||||
|
return { true, std::string() };
|
||||||
|
}
|
||||||
|
case RuntimeControlCommandType::SetLayerShader:
|
||||||
|
{
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(mRuntimeLayerMutex);
|
||||||
|
if (!mRuntimeLayerModel.SetLayerShader(mShaderCatalog, command.layerId, command.shaderId, error))
|
||||||
|
return { false, error };
|
||||||
|
}
|
||||||
|
Log("runtime-shader", "Layer shader changed: " + command.layerId + " shader=" + command.shaderId);
|
||||||
|
StartLayerShaderBuild(command.layerId, command.shaderId);
|
||||||
|
return { true, std::string() };
|
||||||
|
}
|
||||||
|
case RuntimeControlCommandType::UpdateLayerParameter:
|
||||||
|
{
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(mRuntimeLayerMutex);
|
||||||
|
if (!mRuntimeLayerModel.UpdateParameter(command.layerId, command.parameterId, command.value, error))
|
||||||
|
return { false, error };
|
||||||
|
}
|
||||||
|
PublishRuntimeRenderLayers();
|
||||||
|
return { true, std::string() };
|
||||||
|
}
|
||||||
|
case RuntimeControlCommandType::ResetLayerParameters:
|
||||||
|
{
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(mRuntimeLayerMutex);
|
||||||
|
if (!mRuntimeLayerModel.ResetParameters(command.layerId, error))
|
||||||
|
return { false, error };
|
||||||
|
}
|
||||||
|
PublishRuntimeRenderLayers();
|
||||||
|
return { true, std::string() };
|
||||||
|
}
|
||||||
|
case RuntimeControlCommandType::Unsupported:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return { false, "Unsupported runtime control command." };
|
||||||
|
}
|
||||||
|
|
||||||
|
bool RuntimeLayerController::ExtractStringField(const std::string& body, const char* fieldName, std::string& value, std::string& error)
|
||||||
|
{
|
||||||
|
JsonValue root;
|
||||||
|
std::string parseError;
|
||||||
|
if (!ParseJson(body.empty() ? "{}" : body, root, parseError) || !root.isObject())
|
||||||
|
{
|
||||||
|
error = parseError.empty() ? "Request body must be a JSON object." : parseError;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const JsonValue* field = root.find(fieldName);
|
||||||
|
if (!field || !field->isString() || field->asString().empty())
|
||||||
|
{
|
||||||
|
error = std::string("Request field '") + fieldName + "' must be a non-empty string.";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
value = field->asString();
|
||||||
|
error.clear();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
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);
|
||||||
|
}
|
||||||
@@ -93,6 +93,31 @@ inline void WriteDefaultParameterValue(JsonWriter& writer, const ShaderParameter
|
|||||||
writer.Null();
|
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)
|
inline void WriteTemporalJson(JsonWriter& writer, const TemporalSettings& temporal)
|
||||||
{
|
{
|
||||||
writer.BeginObject();
|
writer.BeginObject();
|
||||||
@@ -122,7 +147,7 @@ inline const char* RuntimeLayerBuildStateName(RuntimeLayerBuildState state)
|
|||||||
return "unknown";
|
return "unknown";
|
||||||
}
|
}
|
||||||
|
|
||||||
inline void WriteParameterDefinitionJson(JsonWriter& writer, const ShaderParameterDefinition& parameter)
|
inline void WriteParameterDefinitionJson(JsonWriter& writer, const ShaderParameterDefinition& parameter, const ShaderParameterValue* value)
|
||||||
{
|
{
|
||||||
writer.BeginObject();
|
writer.BeginObject();
|
||||||
writer.KeyString("id", parameter.id);
|
writer.KeyString("id", parameter.id);
|
||||||
@@ -132,7 +157,10 @@ inline void WriteParameterDefinitionJson(JsonWriter& writer, const ShaderParamet
|
|||||||
writer.Key("defaultValue");
|
writer.Key("defaultValue");
|
||||||
WriteDefaultParameterValue(writer, parameter);
|
WriteDefaultParameterValue(writer, parameter);
|
||||||
writer.Key("value");
|
writer.Key("value");
|
||||||
WriteDefaultParameterValue(writer, parameter);
|
if (value)
|
||||||
|
WriteParameterValue(writer, parameter, *value);
|
||||||
|
else
|
||||||
|
WriteDefaultParameterValue(writer, parameter);
|
||||||
|
|
||||||
if (!parameter.minNumbers.empty())
|
if (!parameter.minNumbers.empty())
|
||||||
{
|
{
|
||||||
@@ -197,10 +225,10 @@ inline void WriteLayersJson(JsonWriter& writer, const RuntimeStateJsonInput& inp
|
|||||||
WriteFeedbackJson(writer, FeedbackSettings());
|
WriteFeedbackJson(writer, FeedbackSettings());
|
||||||
writer.Key("parameters");
|
writer.Key("parameters");
|
||||||
writer.BeginArray();
|
writer.BeginArray();
|
||||||
if (shaderPackage)
|
for (const ShaderParameterDefinition& parameter : layer.parameterDefinitions)
|
||||||
{
|
{
|
||||||
for (const ShaderParameterDefinition& parameter : shaderPackage->parameters)
|
const auto valueIt = layer.parameterValues.find(parameter.id);
|
||||||
WriteParameterDefinitionJson(writer, parameter);
|
WriteParameterDefinitionJson(writer, parameter, valueIt == layer.parameterValues.end() ? nullptr : &valueIt->second);
|
||||||
}
|
}
|
||||||
writer.EndArray();
|
writer.EndArray();
|
||||||
writer.EndObject();
|
writer.EndObject();
|
||||||
@@ -255,9 +283,9 @@ inline std::string RuntimeStateToJson(const RuntimeStateJsonInput& input)
|
|||||||
writer.Key("performance");
|
writer.Key("performance");
|
||||||
writer.BeginObject();
|
writer.BeginObject();
|
||||||
writer.KeyDouble("frameBudgetMs", FrameDurationMillisecondsFromRateString(input.config.outputFrameRate));
|
writer.KeyDouble("frameBudgetMs", FrameDurationMillisecondsFromRateString(input.config.outputFrameRate));
|
||||||
writer.KeyNull("renderMs");
|
writer.KeyDouble("renderMs", input.telemetry.renderFrameMilliseconds);
|
||||||
writer.KeyNull("smoothedRenderMs");
|
writer.KeyNull("smoothedRenderMs");
|
||||||
writer.KeyNull("budgetUsedPercent");
|
writer.KeyDouble("budgetUsedPercent", input.telemetry.renderFrameBudgetUsedPercent);
|
||||||
writer.KeyNull("completionIntervalMs");
|
writer.KeyNull("completionIntervalMs");
|
||||||
writer.KeyNull("smoothedCompletionIntervalMs");
|
writer.KeyNull("smoothedCompletionIntervalMs");
|
||||||
writer.KeyNull("maxCompletionIntervalMs");
|
writer.KeyNull("maxCompletionIntervalMs");
|
||||||
|
|||||||
@@ -1,15 +1,11 @@
|
|||||||
#include "HttpControlServer.h"
|
#include "HttpControlServer.h"
|
||||||
|
|
||||||
#include "../json/JsonWriter.h"
|
|
||||||
#include "../logging/Logger.h"
|
#include "../logging/Logger.h"
|
||||||
|
|
||||||
#include <ws2tcpip.h>
|
#include <ws2tcpip.h>
|
||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <cctype>
|
|
||||||
#include <fstream>
|
|
||||||
#include <sstream>
|
#include <sstream>
|
||||||
#include <vector>
|
|
||||||
|
|
||||||
namespace RenderCadenceCompositor
|
namespace RenderCadenceCompositor
|
||||||
{
|
{
|
||||||
@@ -27,22 +23,6 @@ bool InitializeWinsock(std::string& error)
|
|||||||
return true;
|
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) :
|
UniqueSocket::UniqueSocket(SOCKET socket) :
|
||||||
@@ -260,173 +240,6 @@ HttpControlServer::HttpResponse HttpControlServer::RouteRequest(const HttpReques
|
|||||||
return TextResponse("404 Not Found", "Not Found");
|
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 (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)
|
bool HttpControlServer::ParseHttpRequest(const std::string& rawRequest, HttpRequest& request)
|
||||||
{
|
{
|
||||||
const std::size_t requestLineEnd = rawRequest.find("\r\n");
|
const std::size_t requestLineEnd = rawRequest.find("\r\n");
|
||||||
@@ -28,6 +28,7 @@ struct HttpControlServerCallbacks
|
|||||||
std::function<std::string()> getStateJson;
|
std::function<std::string()> getStateJson;
|
||||||
std::function<ControlActionResult(const std::string&)> addLayer;
|
std::function<ControlActionResult(const std::string&)> addLayer;
|
||||||
std::function<ControlActionResult(const std::string&)> removeLayer;
|
std::function<ControlActionResult(const std::string&)> removeLayer;
|
||||||
|
std::function<ControlActionResult(const std::string&, const std::string&)> executePost;
|
||||||
};
|
};
|
||||||
|
|
||||||
class UniqueSocket
|
class UniqueSocket
|
||||||
@@ -0,0 +1,203 @@
|
|||||||
|
#include "HttpControlServer.h"
|
||||||
|
|
||||||
|
#include "../json/JsonWriter.h"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cctype>
|
||||||
|
#include <fstream>
|
||||||
|
#include <sstream>
|
||||||
|
|
||||||
|
namespace RenderCadenceCompositor
|
||||||
|
{
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
bool IsKnownPostEndpoint(const std::string& path)
|
||||||
|
{
|
||||||
|
return path == "/api/layers/add"
|
||||||
|
|| path == "/api/layers/remove"
|
||||||
|
|| path == "/api/layers/move"
|
||||||
|
|| path == "/api/layers/reorder"
|
||||||
|
|| path == "/api/layers/set-bypass"
|
||||||
|
|| path == "/api/layers/set-shader"
|
||||||
|
|| path == "/api/layers/update-parameter"
|
||||||
|
|| path == "/api/layers/reset-parameters"
|
||||||
|
|| path == "/api/stack-presets/save"
|
||||||
|
|| path == "/api/stack-presets/load"
|
||||||
|
|| path == "/api/reload"
|
||||||
|
|| path == "/api/screenshot";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
HttpControlServer::HttpResponse HttpControlServer::ServeGet(const HttpRequest& request) const
|
||||||
|
{
|
||||||
|
if (request.path == "/api/state")
|
||||||
|
return JsonResponse("200 OK", mCallbacks.getStateJson ? mCallbacks.getStateJson() : "{}");
|
||||||
|
if (request.path == "/openapi.yaml" || request.path == "/docs/openapi.yaml")
|
||||||
|
return ServeOpenApiSpec();
|
||||||
|
if (request.path == "/docs" || request.path == "/docs/")
|
||||||
|
return ServeSwaggerDocs();
|
||||||
|
if (request.path == "/" || request.path == "/index.html")
|
||||||
|
return ServeUiAsset("index.html");
|
||||||
|
if (request.path.rfind("/assets/", 0) == 0)
|
||||||
|
return ServeUiAsset(request.path.substr(1));
|
||||||
|
if (request.path.size() > 1)
|
||||||
|
{
|
||||||
|
const HttpResponse asset = ServeUiAsset(request.path.substr(1));
|
||||||
|
if (asset.status != "404 Not Found")
|
||||||
|
return asset;
|
||||||
|
}
|
||||||
|
return ServeUiAsset("index.html");
|
||||||
|
}
|
||||||
|
|
||||||
|
HttpControlServer::HttpResponse HttpControlServer::ServePost(const HttpRequest& request) const
|
||||||
|
{
|
||||||
|
if (!IsKnownPostEndpoint(request.path))
|
||||||
|
return TextResponse("404 Not Found", "Not Found");
|
||||||
|
|
||||||
|
if (mCallbacks.executePost)
|
||||||
|
{
|
||||||
|
const ControlActionResult result = mCallbacks.executePost(request.path, request.body);
|
||||||
|
return JsonResponse(result.ok ? "200 OK" : "400 Bad Request", ActionResponse(result.ok, result.error));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (request.path == "/api/layers/add" && mCallbacks.addLayer)
|
||||||
|
{
|
||||||
|
const ControlActionResult result = mCallbacks.addLayer(request.body);
|
||||||
|
return JsonResponse(result.ok ? "200 OK" : "400 Bad Request", ActionResponse(result.ok, result.error));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (request.path == "/api/layers/remove" && mCallbacks.removeLayer)
|
||||||
|
{
|
||||||
|
const ControlActionResult result = mCallbacks.removeLayer(request.body);
|
||||||
|
return JsonResponse(result.ok ? "200 OK" : "400 Bad Request", ActionResponse(result.ok, result.error));
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
"400 Bad Request",
|
||||||
|
"application/json",
|
||||||
|
ActionResponse(false, "Endpoint is not implemented in RenderCadenceCompositor yet.")
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
HttpControlServer::HttpResponse HttpControlServer::ServeOpenApiSpec() const
|
||||||
|
{
|
||||||
|
const std::filesystem::path path = mDocsRoot / "openapi.yaml";
|
||||||
|
const std::string body = LoadTextFile(path);
|
||||||
|
return body.empty()
|
||||||
|
? TextResponse("404 Not Found", "OpenAPI spec not found")
|
||||||
|
: HttpResponse{ "200 OK", GuessContentType(path), body };
|
||||||
|
}
|
||||||
|
|
||||||
|
HttpControlServer::HttpResponse HttpControlServer::ServeSwaggerDocs() const
|
||||||
|
{
|
||||||
|
std::ostringstream html;
|
||||||
|
html << "<!doctype html>\n"
|
||||||
|
<< "<html><head><meta charset=\"utf-8\"><title>Video Shader Toys API Docs</title>\n"
|
||||||
|
<< "<link rel=\"stylesheet\" href=\"https://unpkg.com/swagger-ui-dist@5/swagger-ui.css\"></head>\n"
|
||||||
|
<< "<body><div id=\"swagger-ui\"></div>\n"
|
||||||
|
<< "<script src=\"https://unpkg.com/swagger-ui-dist@5/swagger-ui-bundle.js\"></script>\n"
|
||||||
|
<< "<script>SwaggerUIBundle({url:'/docs/openapi.yaml',dom_id:'#swagger-ui'});</script>\n"
|
||||||
|
<< "</body></html>\n";
|
||||||
|
return { "200 OK", "text/html", html.str() };
|
||||||
|
}
|
||||||
|
|
||||||
|
HttpControlServer::HttpResponse HttpControlServer::ServeUiAsset(const std::string& relativePath) const
|
||||||
|
{
|
||||||
|
if (mUiRoot.empty())
|
||||||
|
return TextResponse("404 Not Found", "UI root is not configured");
|
||||||
|
|
||||||
|
const std::filesystem::path sanitizedPath = std::filesystem::path(relativePath).lexically_normal();
|
||||||
|
if (!IsSafeRelativePath(sanitizedPath))
|
||||||
|
return TextResponse("404 Not Found", "Not Found");
|
||||||
|
|
||||||
|
const std::filesystem::path path = mUiRoot / sanitizedPath;
|
||||||
|
const std::string body = LoadTextFile(path);
|
||||||
|
if (body.empty())
|
||||||
|
return TextResponse("404 Not Found", "Not Found");
|
||||||
|
return { "200 OK", GuessContentType(path), body };
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string HttpControlServer::LoadTextFile(const std::filesystem::path& path) const
|
||||||
|
{
|
||||||
|
std::ifstream input(path, std::ios::binary);
|
||||||
|
if (!input)
|
||||||
|
return std::string();
|
||||||
|
|
||||||
|
std::ostringstream buffer;
|
||||||
|
buffer << input.rdbuf();
|
||||||
|
return buffer.str();
|
||||||
|
}
|
||||||
|
|
||||||
|
HttpControlServer::HttpResponse HttpControlServer::JsonResponse(const std::string& status, const std::string& body)
|
||||||
|
{
|
||||||
|
return { status, "application/json", body };
|
||||||
|
}
|
||||||
|
|
||||||
|
HttpControlServer::HttpResponse HttpControlServer::TextResponse(const std::string& status, const std::string& body)
|
||||||
|
{
|
||||||
|
return { status, "text/plain", body };
|
||||||
|
}
|
||||||
|
|
||||||
|
HttpControlServer::HttpResponse HttpControlServer::HtmlResponse(const std::string& status, const std::string& body)
|
||||||
|
{
|
||||||
|
return { status, "text/html", body };
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string HttpControlServer::ActionResponse(bool ok, const std::string& error)
|
||||||
|
{
|
||||||
|
JsonWriter writer;
|
||||||
|
writer.BeginObject();
|
||||||
|
writer.KeyBool("ok", ok);
|
||||||
|
if (!error.empty())
|
||||||
|
writer.KeyString("error", error);
|
||||||
|
writer.EndObject();
|
||||||
|
return writer.StringValue();
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string HttpControlServer::GuessContentType(const std::filesystem::path& path)
|
||||||
|
{
|
||||||
|
const std::string extension = ToLower(path.extension().string());
|
||||||
|
if (extension == ".yaml" || extension == ".yml")
|
||||||
|
return "application/yaml";
|
||||||
|
if (extension == ".json")
|
||||||
|
return "application/json";
|
||||||
|
if (extension == ".js" || extension == ".mjs")
|
||||||
|
return "text/javascript";
|
||||||
|
if (extension == ".css")
|
||||||
|
return "text/css";
|
||||||
|
if (extension == ".html" || extension == ".htm")
|
||||||
|
return "text/html";
|
||||||
|
if (extension == ".svg")
|
||||||
|
return "image/svg+xml";
|
||||||
|
if (extension == ".png")
|
||||||
|
return "image/png";
|
||||||
|
if (extension == ".jpg" || extension == ".jpeg")
|
||||||
|
return "image/jpeg";
|
||||||
|
if (extension == ".ico")
|
||||||
|
return "image/x-icon";
|
||||||
|
if (extension == ".map")
|
||||||
|
return "application/json";
|
||||||
|
return "text/plain";
|
||||||
|
}
|
||||||
|
|
||||||
|
bool HttpControlServer::IsSafeRelativePath(const std::filesystem::path& path)
|
||||||
|
{
|
||||||
|
if (path.empty() || path.is_absolute())
|
||||||
|
return false;
|
||||||
|
|
||||||
|
for (const std::filesystem::path& part : path)
|
||||||
|
{
|
||||||
|
if (part == "..")
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string HttpControlServer::ToLower(std::string text)
|
||||||
|
{
|
||||||
|
std::transform(text.begin(), text.end(), text.begin(), [](unsigned char character) {
|
||||||
|
return static_cast<char>(std::tolower(character));
|
||||||
|
});
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
}
|
||||||
250
apps/RenderCadenceCompositor/frames/InputFrameMailbox.cpp
Normal file
250
apps/RenderCadenceCompositor/frames/InputFrameMailbox.cpp
Normal file
@@ -0,0 +1,250 @@
|
|||||||
|
#include "InputFrameMailbox.h"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <chrono>
|
||||||
|
#include <cstring>
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
InputFrameMailboxConfig NormalizeConfig(InputFrameMailboxConfig config)
|
||||||
|
{
|
||||||
|
if (config.rowBytes == 0)
|
||||||
|
config.rowBytes = VideoIORowBytes(config.pixelFormat, config.width);
|
||||||
|
return config;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
InputFrameMailbox::InputFrameMailbox(const InputFrameMailboxConfig& config)
|
||||||
|
{
|
||||||
|
Configure(config);
|
||||||
|
}
|
||||||
|
|
||||||
|
void InputFrameMailbox::Configure(const InputFrameMailboxConfig& config)
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(mMutex);
|
||||||
|
mConfig = NormalizeConfig(config);
|
||||||
|
mReadyIndices.clear();
|
||||||
|
mSlots.clear();
|
||||||
|
mSlots.resize(mConfig.capacity);
|
||||||
|
|
||||||
|
const std::size_t byteCount = FrameByteCount();
|
||||||
|
for (Slot& slot : mSlots)
|
||||||
|
{
|
||||||
|
slot.bytes.resize(byteCount);
|
||||||
|
slot.state = InputFrameSlotState::Free;
|
||||||
|
slot.frameIndex = 0;
|
||||||
|
++slot.generation;
|
||||||
|
}
|
||||||
|
|
||||||
|
mCounters = InputFrameMailboxMetrics();
|
||||||
|
}
|
||||||
|
|
||||||
|
InputFrameMailboxConfig InputFrameMailbox::Config() const
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(mMutex);
|
||||||
|
return mConfig;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool InputFrameMailbox::SubmitFrame(const void* bytes, unsigned rowBytes, uint64_t frameIndex)
|
||||||
|
{
|
||||||
|
if (bytes == nullptr || rowBytes == 0)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
std::lock_guard<std::mutex> lock(mMutex);
|
||||||
|
if (mSlots.empty() || mConfig.width == 0 || mConfig.height == 0)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
std::size_t slotIndex = mSlots.size();
|
||||||
|
for (std::size_t index = 0; index < mSlots.size(); ++index)
|
||||||
|
{
|
||||||
|
if (mSlots[index].state == InputFrameSlotState::Free)
|
||||||
|
{
|
||||||
|
slotIndex = index;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (slotIndex == mSlots.size())
|
||||||
|
{
|
||||||
|
if (!DropOldestReadyLocked())
|
||||||
|
{
|
||||||
|
++mCounters.submitMisses;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (std::size_t index = 0; index < mSlots.size(); ++index)
|
||||||
|
{
|
||||||
|
if (mSlots[index].state == InputFrameSlotState::Free)
|
||||||
|
{
|
||||||
|
slotIndex = index;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (slotIndex == mSlots.size())
|
||||||
|
{
|
||||||
|
++mCounters.submitMisses;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
Slot& slot = mSlots[slotIndex];
|
||||||
|
const std::size_t destinationRowBytes = mConfig.rowBytes;
|
||||||
|
const std::size_t sourceRowBytes = static_cast<std::size_t>(rowBytes);
|
||||||
|
const unsigned char* source = static_cast<const unsigned char*>(bytes);
|
||||||
|
if (sourceRowBytes == destinationRowBytes)
|
||||||
|
{
|
||||||
|
std::memcpy(slot.bytes.data(), source, destinationRowBytes * static_cast<std::size_t>(mConfig.height));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
const std::size_t copyRowBytes = (std::min)(sourceRowBytes, destinationRowBytes);
|
||||||
|
for (unsigned y = 0; y < mConfig.height; ++y)
|
||||||
|
{
|
||||||
|
std::memcpy(
|
||||||
|
slot.bytes.data() + static_cast<std::size_t>(y) * destinationRowBytes,
|
||||||
|
source + static_cast<std::size_t>(y) * sourceRowBytes,
|
||||||
|
copyRowBytes);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
slot.state = InputFrameSlotState::Ready;
|
||||||
|
slot.frameIndex = frameIndex;
|
||||||
|
++slot.generation;
|
||||||
|
mReadyIndices.push_back(slotIndex);
|
||||||
|
TrimReadyFramesLocked();
|
||||||
|
++mCounters.submittedFrames;
|
||||||
|
mCounters.latestFrameIndex = frameIndex;
|
||||||
|
mCounters.hasSubmittedFrame = true;
|
||||||
|
mLatestSubmitTime = std::chrono::steady_clock::now();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool InputFrameMailbox::TryAcquireOldest(InputFrame& frame)
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(mMutex);
|
||||||
|
while (!mReadyIndices.empty())
|
||||||
|
{
|
||||||
|
const std::size_t index = mReadyIndices.front();
|
||||||
|
mReadyIndices.pop_front();
|
||||||
|
if (index >= mSlots.size() || mSlots[index].state != InputFrameSlotState::Ready)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
mSlots[index].state = InputFrameSlotState::Reading;
|
||||||
|
FillFrameLocked(index, frame);
|
||||||
|
++mCounters.consumedFrames;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
frame = InputFrame();
|
||||||
|
++mCounters.consumeMisses;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool InputFrameMailbox::Release(const InputFrame& frame)
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(mMutex);
|
||||||
|
if (!IsValidLocked(frame))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
Slot& slot = mSlots[frame.index];
|
||||||
|
if (slot.state != InputFrameSlotState::Reading)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
slot.state = InputFrameSlotState::Free;
|
||||||
|
slot.frameIndex = 0;
|
||||||
|
++slot.generation;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void InputFrameMailbox::Clear()
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(mMutex);
|
||||||
|
mReadyIndices.clear();
|
||||||
|
for (Slot& slot : mSlots)
|
||||||
|
{
|
||||||
|
slot.state = InputFrameSlotState::Free;
|
||||||
|
slot.frameIndex = 0;
|
||||||
|
++slot.generation;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
InputFrameMailboxMetrics InputFrameMailbox::Metrics() const
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(mMutex);
|
||||||
|
InputFrameMailboxMetrics metrics = mCounters;
|
||||||
|
metrics.capacity = mSlots.size();
|
||||||
|
if (metrics.hasSubmittedFrame)
|
||||||
|
{
|
||||||
|
metrics.latestFrameAgeMilliseconds = std::chrono::duration_cast<std::chrono::duration<double, std::milli>>(
|
||||||
|
std::chrono::steady_clock::now() - mLatestSubmitTime).count();
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const Slot& slot : mSlots)
|
||||||
|
{
|
||||||
|
switch (slot.state)
|
||||||
|
{
|
||||||
|
case InputFrameSlotState::Free:
|
||||||
|
++metrics.freeCount;
|
||||||
|
break;
|
||||||
|
case InputFrameSlotState::Ready:
|
||||||
|
++metrics.readyCount;
|
||||||
|
break;
|
||||||
|
case InputFrameSlotState::Reading:
|
||||||
|
++metrics.readingCount;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return metrics;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool InputFrameMailbox::IsValidLocked(const InputFrame& frame) const
|
||||||
|
{
|
||||||
|
return frame.index < mSlots.size() && mSlots[frame.index].generation == frame.generation;
|
||||||
|
}
|
||||||
|
|
||||||
|
void InputFrameMailbox::FillFrameLocked(std::size_t index, InputFrame& frame) const
|
||||||
|
{
|
||||||
|
const Slot& slot = mSlots[index];
|
||||||
|
frame.bytes = slot.bytes.empty() ? nullptr : slot.bytes.data();
|
||||||
|
frame.rowBytes = static_cast<long>(mConfig.rowBytes);
|
||||||
|
frame.width = mConfig.width;
|
||||||
|
frame.height = mConfig.height;
|
||||||
|
frame.pixelFormat = mConfig.pixelFormat;
|
||||||
|
frame.index = index;
|
||||||
|
frame.generation = slot.generation;
|
||||||
|
frame.frameIndex = slot.frameIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool InputFrameMailbox::DropOldestReadyLocked()
|
||||||
|
{
|
||||||
|
while (!mReadyIndices.empty())
|
||||||
|
{
|
||||||
|
const std::size_t index = mReadyIndices.front();
|
||||||
|
mReadyIndices.pop_front();
|
||||||
|
if (index >= mSlots.size() || mSlots[index].state != InputFrameSlotState::Ready)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
mSlots[index].state = InputFrameSlotState::Free;
|
||||||
|
mSlots[index].frameIndex = 0;
|
||||||
|
++mSlots[index].generation;
|
||||||
|
++mCounters.droppedReadyFrames;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void InputFrameMailbox::TrimReadyFramesLocked()
|
||||||
|
{
|
||||||
|
if (mConfig.maxReadyFrames == 0)
|
||||||
|
return;
|
||||||
|
while (mReadyIndices.size() > mConfig.maxReadyFrames)
|
||||||
|
DropOldestReadyLocked();
|
||||||
|
}
|
||||||
|
|
||||||
|
std::size_t InputFrameMailbox::FrameByteCount() const
|
||||||
|
{
|
||||||
|
return static_cast<std::size_t>(mConfig.rowBytes) * static_cast<std::size_t>(mConfig.height);
|
||||||
|
}
|
||||||
93
apps/RenderCadenceCompositor/frames/InputFrameMailbox.h
Normal file
93
apps/RenderCadenceCompositor/frames/InputFrameMailbox.h
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "VideoIOFormat.h"
|
||||||
|
|
||||||
|
#include <cstddef>
|
||||||
|
#include <cstdint>
|
||||||
|
#include <chrono>
|
||||||
|
#include <deque>
|
||||||
|
#include <mutex>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
enum class InputFrameSlotState
|
||||||
|
{
|
||||||
|
Free,
|
||||||
|
Ready,
|
||||||
|
Reading
|
||||||
|
};
|
||||||
|
|
||||||
|
struct InputFrameMailboxConfig
|
||||||
|
{
|
||||||
|
unsigned width = 0;
|
||||||
|
unsigned height = 0;
|
||||||
|
VideoIOPixelFormat pixelFormat = VideoIOPixelFormat::Bgra8;
|
||||||
|
unsigned rowBytes = 0;
|
||||||
|
std::size_t capacity = 0;
|
||||||
|
std::size_t maxReadyFrames = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct InputFrame
|
||||||
|
{
|
||||||
|
const void* bytes = nullptr;
|
||||||
|
long rowBytes = 0;
|
||||||
|
unsigned width = 0;
|
||||||
|
unsigned height = 0;
|
||||||
|
VideoIOPixelFormat pixelFormat = VideoIOPixelFormat::Bgra8;
|
||||||
|
std::size_t index = 0;
|
||||||
|
uint64_t generation = 0;
|
||||||
|
uint64_t frameIndex = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct InputFrameMailboxMetrics
|
||||||
|
{
|
||||||
|
std::size_t capacity = 0;
|
||||||
|
std::size_t freeCount = 0;
|
||||||
|
std::size_t readyCount = 0;
|
||||||
|
std::size_t readingCount = 0;
|
||||||
|
uint64_t submittedFrames = 0;
|
||||||
|
uint64_t consumedFrames = 0;
|
||||||
|
uint64_t droppedReadyFrames = 0;
|
||||||
|
uint64_t submitMisses = 0;
|
||||||
|
uint64_t consumeMisses = 0;
|
||||||
|
uint64_t latestFrameIndex = 0;
|
||||||
|
bool hasSubmittedFrame = false;
|
||||||
|
double latestFrameAgeMilliseconds = 0.0;
|
||||||
|
};
|
||||||
|
|
||||||
|
class InputFrameMailbox
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
InputFrameMailbox() = default;
|
||||||
|
explicit InputFrameMailbox(const InputFrameMailboxConfig& config);
|
||||||
|
|
||||||
|
void Configure(const InputFrameMailboxConfig& config);
|
||||||
|
InputFrameMailboxConfig Config() const;
|
||||||
|
|
||||||
|
bool SubmitFrame(const void* bytes, unsigned rowBytes, uint64_t frameIndex);
|
||||||
|
bool TryAcquireOldest(InputFrame& frame);
|
||||||
|
bool Release(const InputFrame& frame);
|
||||||
|
void Clear();
|
||||||
|
InputFrameMailboxMetrics Metrics() const;
|
||||||
|
|
||||||
|
private:
|
||||||
|
struct Slot
|
||||||
|
{
|
||||||
|
std::vector<unsigned char> bytes;
|
||||||
|
InputFrameSlotState state = InputFrameSlotState::Free;
|
||||||
|
uint64_t frameIndex = 0;
|
||||||
|
uint64_t generation = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
bool IsValidLocked(const InputFrame& frame) const;
|
||||||
|
void FillFrameLocked(std::size_t index, InputFrame& frame) const;
|
||||||
|
bool DropOldestReadyLocked();
|
||||||
|
void TrimReadyFramesLocked();
|
||||||
|
std::size_t FrameByteCount() const;
|
||||||
|
|
||||||
|
mutable std::mutex mMutex;
|
||||||
|
InputFrameMailboxConfig mConfig;
|
||||||
|
std::vector<Slot> mSlots;
|
||||||
|
std::deque<std::size_t> mReadyIndices;
|
||||||
|
InputFrameMailboxMetrics mCounters;
|
||||||
|
std::chrono::steady_clock::time_point mLatestSubmitTime;
|
||||||
|
};
|
||||||
@@ -47,12 +47,9 @@ bool SystemFrameExchange::AcquireForRender(SystemFrame& frame)
|
|||||||
std::lock_guard<std::mutex> lock(mMutex);
|
std::lock_guard<std::mutex> lock(mMutex);
|
||||||
if (!AcquireFreeLocked(frame))
|
if (!AcquireFreeLocked(frame))
|
||||||
{
|
{
|
||||||
if (!DropOldestCompletedLocked() || !AcquireFreeLocked(frame))
|
frame = SystemFrame();
|
||||||
{
|
++mCounters.acquireMisses;
|
||||||
frame = SystemFrame();
|
return false;
|
||||||
++mCounters.acquireMisses;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
++mCounters.acquiredFrames;
|
++mCounters.acquiredFrames;
|
||||||
@@ -72,6 +69,7 @@ bool SystemFrameExchange::PublishCompleted(const SystemFrame& frame)
|
|||||||
slot.state = SystemFrameSlotState::Completed;
|
slot.state = SystemFrameSlotState::Completed;
|
||||||
slot.frameIndex = frame.frameIndex;
|
slot.frameIndex = frame.frameIndex;
|
||||||
mCompletedIndices.push_back(frame.index);
|
mCompletedIndices.push_back(frame.index);
|
||||||
|
TrimCompletedLocked();
|
||||||
++mCounters.completedFrames;
|
++mCounters.completedFrames;
|
||||||
mCondition.notify_all();
|
mCondition.notify_all();
|
||||||
return true;
|
return true;
|
||||||
@@ -130,6 +128,51 @@ bool SystemFrameExchange::WaitForCompletedDepth(std::size_t targetDepth, std::ch
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool SystemFrameExchange::WaitForStableCompletedDepth(
|
||||||
|
std::size_t targetDepth,
|
||||||
|
std::chrono::milliseconds stableDuration,
|
||||||
|
std::chrono::milliseconds timeout)
|
||||||
|
{
|
||||||
|
if (targetDepth == 0)
|
||||||
|
return true;
|
||||||
|
|
||||||
|
const auto deadline = std::chrono::steady_clock::now() + timeout;
|
||||||
|
std::unique_lock<std::mutex> lock(mMutex);
|
||||||
|
bool stableWindowStarted = false;
|
||||||
|
std::chrono::steady_clock::time_point stableSince;
|
||||||
|
|
||||||
|
while (true)
|
||||||
|
{
|
||||||
|
const auto now = std::chrono::steady_clock::now();
|
||||||
|
if (now >= deadline)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if (CompletedCountLocked() >= targetDepth)
|
||||||
|
{
|
||||||
|
if (stableDuration <= std::chrono::milliseconds::zero())
|
||||||
|
return true;
|
||||||
|
|
||||||
|
if (!stableWindowStarted)
|
||||||
|
{
|
||||||
|
stableSince = now;
|
||||||
|
stableWindowStarted = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto stableDeadline = stableSince + stableDuration;
|
||||||
|
if (now >= stableDeadline)
|
||||||
|
return true;
|
||||||
|
|
||||||
|
mCondition.wait_until(lock, stableDeadline < deadline ? stableDeadline : deadline);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
stableWindowStarted = false;
|
||||||
|
mCondition.wait_until(lock, deadline, [&]() {
|
||||||
|
return CompletedCountLocked() >= targetDepth;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void SystemFrameExchange::Clear()
|
void SystemFrameExchange::Clear()
|
||||||
{
|
{
|
||||||
std::lock_guard<std::mutex> lock(mMutex);
|
std::lock_guard<std::mutex> lock(mMutex);
|
||||||
@@ -210,6 +253,17 @@ bool SystemFrameExchange::DropOldestCompletedLocked()
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void SystemFrameExchange::TrimCompletedLocked()
|
||||||
|
{
|
||||||
|
if (mConfig.maxCompletedFrames == 0)
|
||||||
|
return;
|
||||||
|
while (CompletedCountLocked() > mConfig.maxCompletedFrames)
|
||||||
|
{
|
||||||
|
if (!DropOldestCompletedLocked())
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
bool SystemFrameExchange::IsValidLocked(const SystemFrame& frame) const
|
bool SystemFrameExchange::IsValidLocked(const SystemFrame& frame) const
|
||||||
{
|
{
|
||||||
return frame.index < mSlots.size() && mSlots[frame.index].generation == frame.generation;
|
return frame.index < mSlots.size() && mSlots[frame.index].generation == frame.generation;
|
||||||
|
|||||||
@@ -22,6 +22,10 @@ public:
|
|||||||
bool ConsumeCompletedForSchedule(SystemFrame& frame);
|
bool ConsumeCompletedForSchedule(SystemFrame& frame);
|
||||||
bool ReleaseScheduledByBytes(void* bytes);
|
bool ReleaseScheduledByBytes(void* bytes);
|
||||||
bool WaitForCompletedDepth(std::size_t targetDepth, std::chrono::milliseconds timeout);
|
bool WaitForCompletedDepth(std::size_t targetDepth, std::chrono::milliseconds timeout);
|
||||||
|
bool WaitForStableCompletedDepth(
|
||||||
|
std::size_t targetDepth,
|
||||||
|
std::chrono::milliseconds stableDuration,
|
||||||
|
std::chrono::milliseconds timeout);
|
||||||
void Clear();
|
void Clear();
|
||||||
|
|
||||||
SystemFrameExchangeMetrics Metrics() const;
|
SystemFrameExchangeMetrics Metrics() const;
|
||||||
@@ -37,6 +41,7 @@ private:
|
|||||||
|
|
||||||
bool AcquireFreeLocked(SystemFrame& frame);
|
bool AcquireFreeLocked(SystemFrame& frame);
|
||||||
bool DropOldestCompletedLocked();
|
bool DropOldestCompletedLocked();
|
||||||
|
void TrimCompletedLocked();
|
||||||
bool IsValidLocked(const SystemFrame& frame) const;
|
bool IsValidLocked(const SystemFrame& frame) const;
|
||||||
void FillFrameLocked(std::size_t index, SystemFrame& frame);
|
void FillFrameLocked(std::size_t index, SystemFrame& frame);
|
||||||
std::size_t CompletedCountLocked() const;
|
std::size_t CompletedCountLocked() const;
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ struct SystemFrameExchangeConfig
|
|||||||
VideoIOPixelFormat pixelFormat = VideoIOPixelFormat::Bgra8;
|
VideoIOPixelFormat pixelFormat = VideoIOPixelFormat::Bgra8;
|
||||||
unsigned rowBytes = 0;
|
unsigned rowBytes = 0;
|
||||||
std::size_t capacity = 0;
|
std::size_t capacity = 0;
|
||||||
|
std::size_t maxCompletedFrames = 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
struct SystemFrame
|
struct SystemFrame
|
||||||
|
|||||||
341
apps/RenderCadenceCompositor/render/InputFrameTexture.cpp
Normal file
341
apps/RenderCadenceCompositor/render/InputFrameTexture.cpp
Normal file
@@ -0,0 +1,341 @@
|
|||||||
|
#include "InputFrameTexture.h"
|
||||||
|
|
||||||
|
#include <chrono>
|
||||||
|
|
||||||
|
#ifndef GL_FRAMEBUFFER_BINDING
|
||||||
|
#define GL_FRAMEBUFFER_BINDING 0x8CA6
|
||||||
|
#endif
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
constexpr GLuint kUyvyTextureUnit = 0;
|
||||||
|
|
||||||
|
const char* kDecodeVertexShader = R"GLSL(
|
||||||
|
#version 430 core
|
||||||
|
out vec2 vTexCoord;
|
||||||
|
void main()
|
||||||
|
{
|
||||||
|
vec2 positions[3] = vec2[3](
|
||||||
|
vec2(-1.0, -1.0),
|
||||||
|
vec2( 3.0, -1.0),
|
||||||
|
vec2(-1.0, 3.0));
|
||||||
|
vec2 texCoords[3] = vec2[3](
|
||||||
|
vec2(0.0, 0.0),
|
||||||
|
vec2(2.0, 0.0),
|
||||||
|
vec2(0.0, 2.0));
|
||||||
|
gl_Position = vec4(positions[gl_VertexID], 0.0, 1.0);
|
||||||
|
vTexCoord = texCoords[gl_VertexID];
|
||||||
|
}
|
||||||
|
)GLSL";
|
||||||
|
|
||||||
|
const char* kUyvyDecodeFragmentShader = R"GLSL(
|
||||||
|
#version 430 core
|
||||||
|
layout(binding = 0) uniform sampler2D uPackedUyvy;
|
||||||
|
uniform vec2 uDecodedSize;
|
||||||
|
in vec2 vTexCoord;
|
||||||
|
out vec4 fragColor;
|
||||||
|
|
||||||
|
vec4 rec709YCbCr2rgba(float yByte, float cbByte, float crByte)
|
||||||
|
{
|
||||||
|
float y = (yByte - 16.0) / 219.0;
|
||||||
|
float cb = (cbByte - 16.0) / 224.0 - 0.5;
|
||||||
|
float cr = (crByte - 16.0) / 224.0 - 0.5;
|
||||||
|
return vec4(
|
||||||
|
y + 1.5748 * cr,
|
||||||
|
y - 0.1873 * cb - 0.4681 * cr,
|
||||||
|
y + 1.8556 * cb,
|
||||||
|
1.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
void main()
|
||||||
|
{
|
||||||
|
ivec2 decodedSize = ivec2(uDecodedSize);
|
||||||
|
ivec2 outputCoord = ivec2(clamp(gl_FragCoord.xy, vec2(0.0), vec2(decodedSize - ivec2(1))));
|
||||||
|
int sourceY = decodedSize.y - 1 - outputCoord.y;
|
||||||
|
ivec2 packedCoord = ivec2(clamp(outputCoord.x / 2, 0, max(decodedSize.x / 2 - 1, 0)), sourceY);
|
||||||
|
vec4 macroPixel = texelFetch(uPackedUyvy, packedCoord, 0) * 255.0;
|
||||||
|
float ySample = (outputCoord.x & 1) != 0 ? macroPixel.a : macroPixel.g;
|
||||||
|
fragColor = clamp(rec709YCbCr2rgba(ySample, macroPixel.r, macroPixel.b), vec4(0.0), vec4(1.0));
|
||||||
|
}
|
||||||
|
)GLSL";
|
||||||
|
}
|
||||||
|
|
||||||
|
InputFrameTexture::~InputFrameTexture()
|
||||||
|
{
|
||||||
|
ShutdownGl();
|
||||||
|
}
|
||||||
|
|
||||||
|
GLuint InputFrameTexture::PollAndUpload(InputFrameMailbox* mailbox)
|
||||||
|
{
|
||||||
|
if (mailbox == nullptr)
|
||||||
|
return mTexture;
|
||||||
|
|
||||||
|
InputFrame frame;
|
||||||
|
if (!mailbox->TryAcquireOldest(frame))
|
||||||
|
{
|
||||||
|
++mUploadMisses;
|
||||||
|
mLastUploadMilliseconds = 0.0;
|
||||||
|
return mTexture;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (frame.bytes != nullptr && frame.pixelFormat == VideoIOPixelFormat::Bgra8 && EnsureTexture(frame))
|
||||||
|
{
|
||||||
|
mLastFrameFormatSupported = true;
|
||||||
|
const auto uploadStart = std::chrono::steady_clock::now();
|
||||||
|
UploadBgra8FrameFlippedVertically(frame);
|
||||||
|
const auto uploadEnd = std::chrono::steady_clock::now();
|
||||||
|
mLastUploadMilliseconds = std::chrono::duration_cast<std::chrono::duration<double, std::milli>>(uploadEnd - uploadStart).count();
|
||||||
|
++mUploadedFrames;
|
||||||
|
}
|
||||||
|
else if (frame.bytes != nullptr && frame.pixelFormat == VideoIOPixelFormat::Uyvy8 && EnsureTexture(frame) && EnsureRawUyvyTexture(frame) && EnsureDecodeProgram())
|
||||||
|
{
|
||||||
|
mLastFrameFormatSupported = true;
|
||||||
|
const auto uploadStart = std::chrono::steady_clock::now();
|
||||||
|
UploadUyvy8Frame(frame);
|
||||||
|
DecodeUyvy8Frame(frame);
|
||||||
|
const auto uploadEnd = std::chrono::steady_clock::now();
|
||||||
|
mLastUploadMilliseconds = std::chrono::duration_cast<std::chrono::duration<double, std::milli>>(uploadEnd - uploadStart).count();
|
||||||
|
++mUploadedFrames;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
mLastFrameFormatSupported = frame.pixelFormat == VideoIOPixelFormat::Bgra8 || frame.pixelFormat == VideoIOPixelFormat::Uyvy8;
|
||||||
|
mLastUploadMilliseconds = 0.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
mailbox->Release(frame);
|
||||||
|
return mTexture;
|
||||||
|
}
|
||||||
|
|
||||||
|
void InputFrameTexture::ShutdownGl()
|
||||||
|
{
|
||||||
|
if (mTexture != 0)
|
||||||
|
glDeleteTextures(1, &mTexture);
|
||||||
|
if (mRawTexture != 0)
|
||||||
|
glDeleteTextures(1, &mRawTexture);
|
||||||
|
mTexture = 0;
|
||||||
|
mRawTexture = 0;
|
||||||
|
mWidth = 0;
|
||||||
|
mHeight = 0;
|
||||||
|
mRawWidth = 0;
|
||||||
|
mRawHeight = 0;
|
||||||
|
DestroyDecodeResources();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool InputFrameTexture::EnsureTexture(const InputFrame& frame)
|
||||||
|
{
|
||||||
|
if (frame.width == 0 || frame.height == 0)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if (mTexture != 0 && mWidth == frame.width && mHeight == frame.height)
|
||||||
|
return true;
|
||||||
|
|
||||||
|
ShutdownGl();
|
||||||
|
glGenTextures(1, &mTexture);
|
||||||
|
glBindTexture(GL_TEXTURE_2D, mTexture);
|
||||||
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
|
||||||
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
|
||||||
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
|
||||||
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
|
||||||
|
glTexImage2D(
|
||||||
|
GL_TEXTURE_2D,
|
||||||
|
0,
|
||||||
|
GL_RGBA8,
|
||||||
|
static_cast<GLsizei>(frame.width),
|
||||||
|
static_cast<GLsizei>(frame.height),
|
||||||
|
0,
|
||||||
|
GL_BGRA,
|
||||||
|
GL_UNSIGNED_INT_8_8_8_8_REV,
|
||||||
|
nullptr);
|
||||||
|
glBindTexture(GL_TEXTURE_2D, 0);
|
||||||
|
|
||||||
|
mWidth = frame.width;
|
||||||
|
mHeight = frame.height;
|
||||||
|
return mTexture != 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool InputFrameTexture::EnsureRawUyvyTexture(const InputFrame& frame)
|
||||||
|
{
|
||||||
|
if (frame.width == 0 || frame.height == 0)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
const unsigned rawWidth = (frame.width + 1u) / 2u;
|
||||||
|
if (mRawTexture != 0 && mRawWidth == rawWidth && mRawHeight == frame.height)
|
||||||
|
return true;
|
||||||
|
|
||||||
|
if (mRawTexture != 0)
|
||||||
|
glDeleteTextures(1, &mRawTexture);
|
||||||
|
mRawTexture = 0;
|
||||||
|
glGenTextures(1, &mRawTexture);
|
||||||
|
glBindTexture(GL_TEXTURE_2D, mRawTexture);
|
||||||
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
|
||||||
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
|
||||||
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
|
||||||
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
|
||||||
|
glTexImage2D(
|
||||||
|
GL_TEXTURE_2D,
|
||||||
|
0,
|
||||||
|
GL_RGBA8,
|
||||||
|
static_cast<GLsizei>(rawWidth),
|
||||||
|
static_cast<GLsizei>(frame.height),
|
||||||
|
0,
|
||||||
|
GL_RGBA,
|
||||||
|
GL_UNSIGNED_BYTE,
|
||||||
|
nullptr);
|
||||||
|
glBindTexture(GL_TEXTURE_2D, 0);
|
||||||
|
|
||||||
|
mRawWidth = rawWidth;
|
||||||
|
mRawHeight = frame.height;
|
||||||
|
return mRawTexture != 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
void InputFrameTexture::UploadBgra8FrameFlippedVertically(const InputFrame& frame)
|
||||||
|
{
|
||||||
|
glBindTexture(GL_TEXTURE_2D, mTexture);
|
||||||
|
glPixelStorei(GL_UNPACK_ALIGNMENT, 4);
|
||||||
|
glPixelStorei(GL_UNPACK_ROW_LENGTH, frame.rowBytes > 0 ? static_cast<GLint>(frame.rowBytes / 4) : 0);
|
||||||
|
|
||||||
|
const unsigned char* sourceBytes = static_cast<const unsigned char*>(frame.bytes);
|
||||||
|
for (unsigned destinationY = 0; destinationY < frame.height; ++destinationY)
|
||||||
|
{
|
||||||
|
const unsigned sourceY = frame.height - 1u - destinationY;
|
||||||
|
const unsigned char* sourceRow = sourceBytes + static_cast<std::size_t>(sourceY) * static_cast<std::size_t>(frame.rowBytes);
|
||||||
|
glTexSubImage2D(
|
||||||
|
GL_TEXTURE_2D,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
static_cast<GLint>(destinationY),
|
||||||
|
static_cast<GLsizei>(frame.width),
|
||||||
|
1,
|
||||||
|
GL_BGRA,
|
||||||
|
GL_UNSIGNED_INT_8_8_8_8_REV,
|
||||||
|
sourceRow);
|
||||||
|
}
|
||||||
|
|
||||||
|
glPixelStorei(GL_UNPACK_ROW_LENGTH, 0);
|
||||||
|
glBindTexture(GL_TEXTURE_2D, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
void InputFrameTexture::UploadUyvy8Frame(const InputFrame& frame)
|
||||||
|
{
|
||||||
|
glBindTexture(GL_TEXTURE_2D, mRawTexture);
|
||||||
|
glPixelStorei(GL_UNPACK_ALIGNMENT, 4);
|
||||||
|
glPixelStorei(GL_UNPACK_ROW_LENGTH, frame.rowBytes > 0 ? static_cast<GLint>(frame.rowBytes / 4) : 0);
|
||||||
|
glTexSubImage2D(
|
||||||
|
GL_TEXTURE_2D,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
static_cast<GLsizei>((frame.width + 1u) / 2u),
|
||||||
|
static_cast<GLsizei>(frame.height),
|
||||||
|
GL_RGBA,
|
||||||
|
GL_UNSIGNED_BYTE,
|
||||||
|
frame.bytes);
|
||||||
|
glPixelStorei(GL_UNPACK_ROW_LENGTH, 0);
|
||||||
|
glBindTexture(GL_TEXTURE_2D, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
void InputFrameTexture::DecodeUyvy8Frame(const InputFrame& frame)
|
||||||
|
{
|
||||||
|
GLint previousFramebuffer = 0;
|
||||||
|
glGetIntegerv(GL_FRAMEBUFFER_BINDING, &previousFramebuffer);
|
||||||
|
|
||||||
|
if (mDecodeFramebuffer == 0)
|
||||||
|
glGenFramebuffers(1, &mDecodeFramebuffer);
|
||||||
|
glBindFramebuffer(GL_FRAMEBUFFER, mDecodeFramebuffer);
|
||||||
|
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, mTexture, 0);
|
||||||
|
if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE)
|
||||||
|
{
|
||||||
|
glBindFramebuffer(GL_FRAMEBUFFER, static_cast<GLuint>(previousFramebuffer));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
glViewport(0, 0, static_cast<GLsizei>(frame.width), static_cast<GLsizei>(frame.height));
|
||||||
|
glDisable(GL_SCISSOR_TEST);
|
||||||
|
glDisable(GL_DEPTH_TEST);
|
||||||
|
glDisable(GL_BLEND);
|
||||||
|
glUseProgram(mDecodeProgram);
|
||||||
|
const GLint decodedSizeLocation = glGetUniformLocation(mDecodeProgram, "uDecodedSize");
|
||||||
|
if (decodedSizeLocation >= 0)
|
||||||
|
glUniform2f(decodedSizeLocation, static_cast<GLfloat>(frame.width), static_cast<GLfloat>(frame.height));
|
||||||
|
glActiveTexture(GL_TEXTURE0 + kUyvyTextureUnit);
|
||||||
|
glBindTexture(GL_TEXTURE_2D, mRawTexture);
|
||||||
|
glBindVertexArray(mDecodeVertexArray);
|
||||||
|
glDrawArrays(GL_TRIANGLES, 0, 3);
|
||||||
|
glBindVertexArray(0);
|
||||||
|
glBindTexture(GL_TEXTURE_2D, 0);
|
||||||
|
glActiveTexture(GL_TEXTURE0);
|
||||||
|
glUseProgram(0);
|
||||||
|
glBindFramebuffer(GL_FRAMEBUFFER, static_cast<GLuint>(previousFramebuffer));
|
||||||
|
}
|
||||||
|
|
||||||
|
bool InputFrameTexture::EnsureDecodeProgram()
|
||||||
|
{
|
||||||
|
if (mDecodeProgram != 0)
|
||||||
|
return true;
|
||||||
|
|
||||||
|
if (!CompileShader(GL_VERTEX_SHADER, kDecodeVertexShader, mDecodeVertexShader))
|
||||||
|
return false;
|
||||||
|
if (!CompileShader(GL_FRAGMENT_SHADER, kUyvyDecodeFragmentShader, mDecodeFragmentShader))
|
||||||
|
return false;
|
||||||
|
if (!LinkProgram(mDecodeVertexShader, mDecodeFragmentShader, mDecodeProgram))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
glUseProgram(mDecodeProgram);
|
||||||
|
const GLint samplerLocation = glGetUniformLocation(mDecodeProgram, "uPackedUyvy");
|
||||||
|
if (samplerLocation >= 0)
|
||||||
|
glUniform1i(samplerLocation, static_cast<GLint>(kUyvyTextureUnit));
|
||||||
|
glUseProgram(0);
|
||||||
|
|
||||||
|
if (mDecodeVertexArray == 0)
|
||||||
|
glGenVertexArrays(1, &mDecodeVertexArray);
|
||||||
|
return mDecodeProgram != 0 && mDecodeVertexArray != 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
void InputFrameTexture::DestroyDecodeResources()
|
||||||
|
{
|
||||||
|
if (mDecodeFramebuffer != 0)
|
||||||
|
glDeleteFramebuffers(1, &mDecodeFramebuffer);
|
||||||
|
if (mDecodeVertexArray != 0)
|
||||||
|
glDeleteVertexArrays(1, &mDecodeVertexArray);
|
||||||
|
if (mDecodeProgram != 0)
|
||||||
|
glDeleteProgram(mDecodeProgram);
|
||||||
|
if (mDecodeVertexShader != 0)
|
||||||
|
glDeleteShader(mDecodeVertexShader);
|
||||||
|
if (mDecodeFragmentShader != 0)
|
||||||
|
glDeleteShader(mDecodeFragmentShader);
|
||||||
|
mDecodeFramebuffer = 0;
|
||||||
|
mDecodeVertexArray = 0;
|
||||||
|
mDecodeProgram = 0;
|
||||||
|
mDecodeVertexShader = 0;
|
||||||
|
mDecodeFragmentShader = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool InputFrameTexture::CompileShader(GLenum shaderType, const char* source, GLuint& shader)
|
||||||
|
{
|
||||||
|
shader = glCreateShader(shaderType);
|
||||||
|
glShaderSource(shader, 1, &source, nullptr);
|
||||||
|
glCompileShader(shader);
|
||||||
|
GLint compileResult = GL_FALSE;
|
||||||
|
glGetShaderiv(shader, GL_COMPILE_STATUS, &compileResult);
|
||||||
|
if (compileResult != GL_FALSE)
|
||||||
|
return true;
|
||||||
|
glDeleteShader(shader);
|
||||||
|
shader = 0;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool InputFrameTexture::LinkProgram(GLuint vertexShader, GLuint fragmentShader, GLuint& program)
|
||||||
|
{
|
||||||
|
program = glCreateProgram();
|
||||||
|
glAttachShader(program, vertexShader);
|
||||||
|
glAttachShader(program, fragmentShader);
|
||||||
|
glLinkProgram(program);
|
||||||
|
GLint linkResult = GL_FALSE;
|
||||||
|
glGetProgramiv(program, GL_LINK_STATUS, &linkResult);
|
||||||
|
if (linkResult != GL_FALSE)
|
||||||
|
return true;
|
||||||
|
glDeleteProgram(program);
|
||||||
|
program = 0;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
50
apps/RenderCadenceCompositor/render/InputFrameTexture.h
Normal file
50
apps/RenderCadenceCompositor/render/InputFrameTexture.h
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "../frames/InputFrameMailbox.h"
|
||||||
|
#include "GLExtensions.h"
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
|
||||||
|
class InputFrameTexture
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
InputFrameTexture() = default;
|
||||||
|
InputFrameTexture(const InputFrameTexture&) = delete;
|
||||||
|
InputFrameTexture& operator=(const InputFrameTexture&) = delete;
|
||||||
|
~InputFrameTexture();
|
||||||
|
|
||||||
|
GLuint PollAndUpload(InputFrameMailbox* mailbox);
|
||||||
|
GLuint Texture() const { return mTexture; }
|
||||||
|
uint64_t UploadedFrames() const { return mUploadedFrames; }
|
||||||
|
uint64_t UploadMisses() const { return mUploadMisses; }
|
||||||
|
double LastUploadMilliseconds() const { return mLastUploadMilliseconds; }
|
||||||
|
bool LastFrameFormatSupported() const { return mLastFrameFormatSupported; }
|
||||||
|
void ShutdownGl();
|
||||||
|
|
||||||
|
private:
|
||||||
|
bool EnsureTexture(const InputFrame& frame);
|
||||||
|
bool EnsureRawUyvyTexture(const InputFrame& frame);
|
||||||
|
bool EnsureDecodeProgram();
|
||||||
|
void UploadBgra8FrameFlippedVertically(const InputFrame& frame);
|
||||||
|
void UploadUyvy8Frame(const InputFrame& frame);
|
||||||
|
void DecodeUyvy8Frame(const InputFrame& frame);
|
||||||
|
void DestroyDecodeResources();
|
||||||
|
static bool CompileShader(GLenum shaderType, const char* source, GLuint& shader);
|
||||||
|
static bool LinkProgram(GLuint vertexShader, GLuint fragmentShader, GLuint& program);
|
||||||
|
|
||||||
|
GLuint mTexture = 0;
|
||||||
|
GLuint mRawTexture = 0;
|
||||||
|
GLuint mDecodeFramebuffer = 0;
|
||||||
|
GLuint mDecodeVertexArray = 0;
|
||||||
|
GLuint mDecodeProgram = 0;
|
||||||
|
GLuint mDecodeVertexShader = 0;
|
||||||
|
GLuint mDecodeFragmentShader = 0;
|
||||||
|
unsigned mWidth = 0;
|
||||||
|
unsigned mHeight = 0;
|
||||||
|
unsigned mRawWidth = 0;
|
||||||
|
unsigned mRawHeight = 0;
|
||||||
|
uint64_t mUploadedFrames = 0;
|
||||||
|
uint64_t mUploadMisses = 0;
|
||||||
|
double mLastUploadMilliseconds = 0.0;
|
||||||
|
bool mLastFrameFormatSupported = true;
|
||||||
|
};
|
||||||
@@ -13,6 +13,7 @@ RenderCadenceClock::RenderCadenceClock(double frameDurationMilliseconds)
|
|||||||
void RenderCadenceClock::Reset(TimePoint now)
|
void RenderCadenceClock::Reset(TimePoint now)
|
||||||
{
|
{
|
||||||
mNextRenderTime = now;
|
mNextRenderTime = now;
|
||||||
|
mPendingFrameAdvance = 1;
|
||||||
mOverrunCount = 0;
|
mOverrunCount = 0;
|
||||||
mSkippedFrameCount = 0;
|
mSkippedFrameCount = 0;
|
||||||
}
|
}
|
||||||
@@ -27,10 +28,12 @@ RenderCadenceClock::Tick RenderCadenceClock::Poll(TimePoint now)
|
|||||||
}
|
}
|
||||||
|
|
||||||
tick.due = true;
|
tick.due = true;
|
||||||
|
mPendingFrameAdvance = 1;
|
||||||
const Duration lateBy = now - mNextRenderTime;
|
const Duration lateBy = now - mNextRenderTime;
|
||||||
if (lateBy > mFrameDuration)
|
if (lateBy > mFrameDuration)
|
||||||
{
|
{
|
||||||
tick.skippedFrames = static_cast<uint64_t>(lateBy / mFrameDuration);
|
tick.skippedFrames = static_cast<uint64_t>(lateBy / mFrameDuration);
|
||||||
|
mPendingFrameAdvance += tick.skippedFrames;
|
||||||
++mOverrunCount;
|
++mOverrunCount;
|
||||||
mSkippedFrameCount += tick.skippedFrames;
|
mSkippedFrameCount += tick.skippedFrames;
|
||||||
}
|
}
|
||||||
@@ -39,7 +42,8 @@ RenderCadenceClock::Tick RenderCadenceClock::Poll(TimePoint now)
|
|||||||
|
|
||||||
void RenderCadenceClock::MarkRendered(TimePoint now)
|
void RenderCadenceClock::MarkRendered(TimePoint now)
|
||||||
{
|
{
|
||||||
mNextRenderTime += mFrameDuration;
|
mNextRenderTime += mFrameDuration * mPendingFrameAdvance;
|
||||||
|
mPendingFrameAdvance = 1;
|
||||||
if (now - mNextRenderTime > mFrameDuration * 4)
|
if (now - mNextRenderTime > mFrameDuration * 4)
|
||||||
mNextRenderTime = now + mFrameDuration;
|
mNextRenderTime = now + mFrameDuration;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ public:
|
|||||||
private:
|
private:
|
||||||
Duration mFrameDuration;
|
Duration mFrameDuration;
|
||||||
TimePoint mNextRenderTime = Clock::now();
|
TimePoint mNextRenderTime = Clock::now();
|
||||||
|
uint64_t mPendingFrameAdvance = 1;
|
||||||
uint64_t mOverrunCount = 0;
|
uint64_t mOverrunCount = 0;
|
||||||
uint64_t mSkippedFrameCount = 0;
|
uint64_t mSkippedFrameCount = 0;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,13 +1,15 @@
|
|||||||
#include "RenderThread.h"
|
#include "RenderThread.h"
|
||||||
|
|
||||||
|
#include "../frames/InputFrameMailbox.h"
|
||||||
#include "../frames/SystemFrameExchange.h"
|
#include "../frames/SystemFrameExchange.h"
|
||||||
#include "../frames/SystemFrameTypes.h"
|
#include "../frames/SystemFrameTypes.h"
|
||||||
#include "../logging/Logger.h"
|
#include "../logging/Logger.h"
|
||||||
#include "../platform/HiddenGlWindow.h"
|
#include "../platform/HiddenGlWindow.h"
|
||||||
#include "Bgra8ReadbackPipeline.h"
|
#include "InputFrameTexture.h"
|
||||||
|
#include "readback/Bgra8ReadbackPipeline.h"
|
||||||
#include "GLExtensions.h"
|
#include "GLExtensions.h"
|
||||||
#include "RuntimeRenderScene.h"
|
#include "runtime/RuntimeRenderScene.h"
|
||||||
#include "RuntimeShaderRenderer.h"
|
#include "runtime/RuntimeShaderRenderer.h"
|
||||||
#include "SimpleMotionRenderer.h"
|
#include "SimpleMotionRenderer.h"
|
||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
@@ -20,6 +22,13 @@ RenderThread::RenderThread(SystemFrameExchange& frameExchange, Config config) :
|
|||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
|
RenderThread::RenderThread(SystemFrameExchange& frameExchange, InputFrameMailbox* inputMailbox, Config config) :
|
||||||
|
mFrameExchange(frameExchange),
|
||||||
|
mInputMailbox(inputMailbox),
|
||||||
|
mConfig(config)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
RenderThread::~RenderThread()
|
RenderThread::~RenderThread()
|
||||||
{
|
{
|
||||||
Stop();
|
Stop();
|
||||||
@@ -76,6 +85,21 @@ RenderThread::Metrics RenderThread::GetMetrics() const
|
|||||||
metrics.skippedFrames = mSkippedFrames.load(std::memory_order_relaxed);
|
metrics.skippedFrames = mSkippedFrames.load(std::memory_order_relaxed);
|
||||||
metrics.shaderBuildsCommitted = mShaderBuildsCommitted.load(std::memory_order_relaxed);
|
metrics.shaderBuildsCommitted = mShaderBuildsCommitted.load(std::memory_order_relaxed);
|
||||||
metrics.shaderBuildFailures = mShaderBuildFailures.load(std::memory_order_relaxed);
|
metrics.shaderBuildFailures = mShaderBuildFailures.load(std::memory_order_relaxed);
|
||||||
|
metrics.renderFrameMilliseconds = mRenderFrameMilliseconds.load(std::memory_order_relaxed);
|
||||||
|
metrics.renderFrameBudgetUsedPercent = mRenderFrameBudgetUsedPercent.load(std::memory_order_relaxed);
|
||||||
|
metrics.renderFrameMaxMilliseconds = mRenderFrameMaxMilliseconds.load(std::memory_order_relaxed);
|
||||||
|
metrics.readbackQueueMilliseconds = mReadbackQueueMilliseconds.load(std::memory_order_relaxed);
|
||||||
|
metrics.completedReadbackCopyMilliseconds = mCompletedReadbackCopyMilliseconds.load(std::memory_order_relaxed);
|
||||||
|
metrics.inputFramesReceived = mInputFramesReceived.load(std::memory_order_relaxed);
|
||||||
|
metrics.inputFramesDropped = mInputFramesDropped.load(std::memory_order_relaxed);
|
||||||
|
metrics.inputConsumeMisses = mInputConsumeMisses.load(std::memory_order_relaxed);
|
||||||
|
metrics.inputUploadMisses = mInputUploadMisses.load(std::memory_order_relaxed);
|
||||||
|
metrics.inputReadyFrames = mInputReadyFrames.load(std::memory_order_relaxed);
|
||||||
|
metrics.inputReadingFrames = mInputReadingFrames.load(std::memory_order_relaxed);
|
||||||
|
metrics.inputLatestAgeMilliseconds = mInputLatestAgeMilliseconds.load(std::memory_order_relaxed);
|
||||||
|
metrics.inputUploadMilliseconds = mInputUploadMilliseconds.load(std::memory_order_relaxed);
|
||||||
|
metrics.inputFormatSupported = mInputFormatSupported.load(std::memory_order_relaxed);
|
||||||
|
metrics.inputSignalPresent = mInputSignalPresent.load(std::memory_order_relaxed);
|
||||||
return metrics;
|
return metrics;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -109,6 +133,7 @@ void RenderThread::ThreadMain()
|
|||||||
SimpleMotionRenderer renderer;
|
SimpleMotionRenderer renderer;
|
||||||
RuntimeRenderScene runtimeRenderScene;
|
RuntimeRenderScene runtimeRenderScene;
|
||||||
Bgra8ReadbackPipeline readback;
|
Bgra8ReadbackPipeline readback;
|
||||||
|
InputFrameTexture inputTexture;
|
||||||
if (!runtimeRenderScene.StartPrepareWorker(std::move(prepareWindow), error))
|
if (!runtimeRenderScene.StartPrepareWorker(std::move(prepareWindow), error))
|
||||||
{
|
{
|
||||||
SignalStartupFailure(error.empty() ? "Runtime shader prepare worker initialization failed." : error);
|
SignalStartupFailure(error.empty() ? "Runtime shader prepare worker initialization failed." : error);
|
||||||
@@ -134,6 +159,7 @@ void RenderThread::ThreadMain()
|
|||||||
CountAcquireMiss();
|
CountAcquireMiss();
|
||||||
},
|
},
|
||||||
[this]() { CountCompleted(); });
|
[this]() { CountCompleted(); });
|
||||||
|
PublishReadbackMetrics(readback);
|
||||||
|
|
||||||
const auto now = RenderCadenceClock::Clock::now();
|
const auto now = RenderCadenceClock::Clock::now();
|
||||||
const RenderCadenceClock::Tick tick = clock.Poll(now);
|
const RenderCadenceClock::Tick tick = clock.Poll(now);
|
||||||
@@ -145,15 +171,20 @@ void RenderThread::ThreadMain()
|
|||||||
}
|
}
|
||||||
|
|
||||||
TryCommitReadyRuntimeShader(runtimeRenderScene);
|
TryCommitReadyRuntimeShader(runtimeRenderScene);
|
||||||
if (!readback.RenderAndQueue(frameIndex, [this, &renderer, &runtimeRenderScene](uint64_t index) {
|
const GLuint videoInputTexture = inputTexture.PollAndUpload(mInputMailbox);
|
||||||
|
PublishInputMetrics(inputTexture);
|
||||||
|
if (!readback.RenderAndQueue(frameIndex, [this, &renderer, &runtimeRenderScene, videoInputTexture](uint64_t index) {
|
||||||
if (runtimeRenderScene.HasLayers())
|
if (runtimeRenderScene.HasLayers())
|
||||||
runtimeRenderScene.RenderFrame(index, mConfig.width, mConfig.height);
|
runtimeRenderScene.RenderFrame(index, mConfig.width, mConfig.height, videoInputTexture);
|
||||||
|
else if (videoInputTexture != 0)
|
||||||
|
renderer.RenderTexture(videoInputTexture);
|
||||||
else
|
else
|
||||||
renderer.RenderFrame(index);
|
renderer.RenderFrame(index);
|
||||||
}))
|
}))
|
||||||
{
|
{
|
||||||
mPboQueueMisses.fetch_add(1, std::memory_order_relaxed);
|
mPboQueueMisses.fetch_add(1, std::memory_order_relaxed);
|
||||||
}
|
}
|
||||||
|
PublishReadbackMetrics(readback);
|
||||||
|
|
||||||
CountRendered();
|
CountRendered();
|
||||||
++frameIndex;
|
++frameIndex;
|
||||||
@@ -172,9 +203,11 @@ void RenderThread::ThreadMain()
|
|||||||
CountAcquireMiss();
|
CountAcquireMiss();
|
||||||
},
|
},
|
||||||
[this]() { CountCompleted(); });
|
[this]() { CountCompleted(); });
|
||||||
|
PublishReadbackMetrics(readback);
|
||||||
}
|
}
|
||||||
|
|
||||||
readback.Shutdown();
|
readback.Shutdown();
|
||||||
|
inputTexture.ShutdownGl();
|
||||||
runtimeRenderScene.ShutdownGl();
|
runtimeRenderScene.ShutdownGl();
|
||||||
renderer.ShutdownGl();
|
renderer.ShutdownGl();
|
||||||
window.ClearCurrent();
|
window.ClearCurrent();
|
||||||
@@ -212,6 +245,58 @@ void RenderThread::CountAcquireMiss()
|
|||||||
mAcquireMisses.fetch_add(1, std::memory_order_relaxed);
|
mAcquireMisses.fetch_add(1, std::memory_order_relaxed);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void RenderThread::PublishReadbackMetrics(const Bgra8ReadbackPipeline& readback)
|
||||||
|
{
|
||||||
|
const double renderMilliseconds = readback.LastRenderFrameMilliseconds();
|
||||||
|
mRenderFrameMilliseconds.store(renderMilliseconds, std::memory_order_relaxed);
|
||||||
|
if (mConfig.frameDurationMilliseconds > 0.0)
|
||||||
|
{
|
||||||
|
mRenderFrameBudgetUsedPercent.store(
|
||||||
|
(renderMilliseconds / mConfig.frameDurationMilliseconds) * 100.0,
|
||||||
|
std::memory_order_relaxed);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
mRenderFrameBudgetUsedPercent.store(0.0, std::memory_order_relaxed);
|
||||||
|
}
|
||||||
|
|
||||||
|
const double previousMax = mRenderFrameMaxMilliseconds.load(std::memory_order_relaxed);
|
||||||
|
if (renderMilliseconds > previousMax)
|
||||||
|
mRenderFrameMaxMilliseconds.store(renderMilliseconds, std::memory_order_relaxed);
|
||||||
|
|
||||||
|
mReadbackQueueMilliseconds.store(readback.LastReadbackQueueMilliseconds(), std::memory_order_relaxed);
|
||||||
|
mCompletedReadbackCopyMilliseconds.store(readback.LastCompletedReadbackCopyMilliseconds(), std::memory_order_relaxed);
|
||||||
|
}
|
||||||
|
|
||||||
|
void RenderThread::PublishInputMetrics(const InputFrameTexture& inputTexture)
|
||||||
|
{
|
||||||
|
if (mInputMailbox != nullptr)
|
||||||
|
{
|
||||||
|
const InputFrameMailboxMetrics mailboxMetrics = mInputMailbox->Metrics();
|
||||||
|
mInputFramesReceived.store(mailboxMetrics.submittedFrames, std::memory_order_relaxed);
|
||||||
|
mInputFramesDropped.store(mailboxMetrics.droppedReadyFrames + mailboxMetrics.submitMisses, std::memory_order_relaxed);
|
||||||
|
mInputConsumeMisses.store(mailboxMetrics.consumeMisses, std::memory_order_relaxed);
|
||||||
|
mInputReadyFrames.store(mailboxMetrics.readyCount, std::memory_order_relaxed);
|
||||||
|
mInputReadingFrames.store(mailboxMetrics.readingCount, std::memory_order_relaxed);
|
||||||
|
mInputLatestAgeMilliseconds.store(mailboxMetrics.latestFrameAgeMilliseconds, std::memory_order_relaxed);
|
||||||
|
mInputSignalPresent.store(mailboxMetrics.hasSubmittedFrame, std::memory_order_relaxed);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
mInputFramesReceived.store(0, std::memory_order_relaxed);
|
||||||
|
mInputFramesDropped.store(0, std::memory_order_relaxed);
|
||||||
|
mInputConsumeMisses.store(0, std::memory_order_relaxed);
|
||||||
|
mInputReadyFrames.store(0, std::memory_order_relaxed);
|
||||||
|
mInputReadingFrames.store(0, std::memory_order_relaxed);
|
||||||
|
mInputLatestAgeMilliseconds.store(0.0, std::memory_order_relaxed);
|
||||||
|
mInputSignalPresent.store(false, std::memory_order_relaxed);
|
||||||
|
}
|
||||||
|
|
||||||
|
mInputUploadMisses.store(inputTexture.UploadMisses(), std::memory_order_relaxed);
|
||||||
|
mInputUploadMilliseconds.store(inputTexture.LastUploadMilliseconds(), std::memory_order_relaxed);
|
||||||
|
mInputFormatSupported.store(inputTexture.LastFrameFormatSupported(), std::memory_order_relaxed);
|
||||||
|
}
|
||||||
|
|
||||||
void RenderThread::SubmitRuntimeShaderArtifact(const RuntimeShaderArtifact& artifact)
|
void RenderThread::SubmitRuntimeShaderArtifact(const RuntimeShaderArtifact& artifact)
|
||||||
{
|
{
|
||||||
if (artifact.fragmentShaderSource.empty())
|
if (artifact.fragmentShaderSource.empty())
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
#include "RenderCadenceClock.h"
|
#include "RenderCadenceClock.h"
|
||||||
#include "../runtime/RuntimeLayerModel.h"
|
#include "../runtime/RuntimeLayerModel.h"
|
||||||
#include "../runtime/RuntimeShaderArtifact.h"
|
#include "../runtime/RuntimeShaderArtifact.h"
|
||||||
#include "RuntimeRenderScene.h"
|
#include "runtime/RuntimeRenderScene.h"
|
||||||
|
|
||||||
#include <atomic>
|
#include <atomic>
|
||||||
#include <condition_variable>
|
#include <condition_variable>
|
||||||
@@ -14,6 +14,9 @@
|
|||||||
#include <thread>
|
#include <thread>
|
||||||
|
|
||||||
class SystemFrameExchange;
|
class SystemFrameExchange;
|
||||||
|
class InputFrameMailbox;
|
||||||
|
class InputFrameTexture;
|
||||||
|
class Bgra8ReadbackPipeline;
|
||||||
|
|
||||||
class RenderThread
|
class RenderThread
|
||||||
{
|
{
|
||||||
@@ -36,9 +39,25 @@ public:
|
|||||||
uint64_t skippedFrames = 0;
|
uint64_t skippedFrames = 0;
|
||||||
uint64_t shaderBuildsCommitted = 0;
|
uint64_t shaderBuildsCommitted = 0;
|
||||||
uint64_t shaderBuildFailures = 0;
|
uint64_t shaderBuildFailures = 0;
|
||||||
|
double renderFrameMilliseconds = 0.0;
|
||||||
|
double renderFrameBudgetUsedPercent = 0.0;
|
||||||
|
double renderFrameMaxMilliseconds = 0.0;
|
||||||
|
double readbackQueueMilliseconds = 0.0;
|
||||||
|
double completedReadbackCopyMilliseconds = 0.0;
|
||||||
|
uint64_t inputFramesReceived = 0;
|
||||||
|
uint64_t inputFramesDropped = 0;
|
||||||
|
uint64_t inputConsumeMisses = 0;
|
||||||
|
uint64_t inputUploadMisses = 0;
|
||||||
|
std::size_t inputReadyFrames = 0;
|
||||||
|
std::size_t inputReadingFrames = 0;
|
||||||
|
double inputLatestAgeMilliseconds = 0.0;
|
||||||
|
double inputUploadMilliseconds = 0.0;
|
||||||
|
bool inputFormatSupported = true;
|
||||||
|
bool inputSignalPresent = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
RenderThread(SystemFrameExchange& frameExchange, Config config);
|
RenderThread(SystemFrameExchange& frameExchange, Config config);
|
||||||
|
RenderThread(SystemFrameExchange& frameExchange, InputFrameMailbox* inputMailbox, Config config);
|
||||||
RenderThread(const RenderThread&) = delete;
|
RenderThread(const RenderThread&) = delete;
|
||||||
RenderThread& operator=(const RenderThread&) = delete;
|
RenderThread& operator=(const RenderThread&) = delete;
|
||||||
~RenderThread();
|
~RenderThread();
|
||||||
@@ -58,11 +77,14 @@ private:
|
|||||||
void CountRendered();
|
void CountRendered();
|
||||||
void CountCompleted();
|
void CountCompleted();
|
||||||
void CountAcquireMiss();
|
void CountAcquireMiss();
|
||||||
|
void PublishReadbackMetrics(const Bgra8ReadbackPipeline& readback);
|
||||||
|
void PublishInputMetrics(const InputFrameTexture& inputTexture);
|
||||||
void TryCommitReadyRuntimeShader(RuntimeRenderScene& runtimeRenderScene);
|
void TryCommitReadyRuntimeShader(RuntimeRenderScene& runtimeRenderScene);
|
||||||
bool TryTakePendingRuntimeShaderArtifact(RuntimeShaderArtifact& artifact);
|
bool TryTakePendingRuntimeShaderArtifact(RuntimeShaderArtifact& artifact);
|
||||||
bool TryTakePendingRenderLayers(std::vector<RenderCadenceCompositor::RuntimeRenderLayerModel>& layers);
|
bool TryTakePendingRenderLayers(std::vector<RenderCadenceCompositor::RuntimeRenderLayerModel>& layers);
|
||||||
|
|
||||||
SystemFrameExchange& mFrameExchange;
|
SystemFrameExchange& mFrameExchange;
|
||||||
|
InputFrameMailbox* mInputMailbox = nullptr;
|
||||||
Config mConfig;
|
Config mConfig;
|
||||||
std::thread mThread;
|
std::thread mThread;
|
||||||
std::atomic<bool> mStopping{ false };
|
std::atomic<bool> mStopping{ false };
|
||||||
@@ -81,6 +103,21 @@ private:
|
|||||||
std::atomic<uint64_t> mSkippedFrames{ 0 };
|
std::atomic<uint64_t> mSkippedFrames{ 0 };
|
||||||
std::atomic<uint64_t> mShaderBuildsCommitted{ 0 };
|
std::atomic<uint64_t> mShaderBuildsCommitted{ 0 };
|
||||||
std::atomic<uint64_t> mShaderBuildFailures{ 0 };
|
std::atomic<uint64_t> mShaderBuildFailures{ 0 };
|
||||||
|
std::atomic<double> mRenderFrameMilliseconds{ 0.0 };
|
||||||
|
std::atomic<double> mRenderFrameBudgetUsedPercent{ 0.0 };
|
||||||
|
std::atomic<double> mRenderFrameMaxMilliseconds{ 0.0 };
|
||||||
|
std::atomic<double> mReadbackQueueMilliseconds{ 0.0 };
|
||||||
|
std::atomic<double> mCompletedReadbackCopyMilliseconds{ 0.0 };
|
||||||
|
std::atomic<uint64_t> mInputFramesReceived{ 0 };
|
||||||
|
std::atomic<uint64_t> mInputFramesDropped{ 0 };
|
||||||
|
std::atomic<uint64_t> mInputConsumeMisses{ 0 };
|
||||||
|
std::atomic<uint64_t> mInputUploadMisses{ 0 };
|
||||||
|
std::atomic<std::size_t> mInputReadyFrames{ 0 };
|
||||||
|
std::atomic<std::size_t> mInputReadingFrames{ 0 };
|
||||||
|
std::atomic<double> mInputLatestAgeMilliseconds{ 0.0 };
|
||||||
|
std::atomic<double> mInputUploadMilliseconds{ 0.0 };
|
||||||
|
std::atomic<bool> mInputFormatSupported{ true };
|
||||||
|
std::atomic<bool> mInputSignalPresent{ false };
|
||||||
|
|
||||||
std::mutex mShaderArtifactMutex;
|
std::mutex mShaderArtifactMutex;
|
||||||
bool mHasPendingShaderArtifact = false;
|
bool mHasPendingShaderArtifact = false;
|
||||||
|
|||||||
@@ -1,173 +0,0 @@
|
|||||||
#include "RuntimeRenderScene.h"
|
|
||||||
|
|
||||||
#include "../platform/HiddenGlWindow.h"
|
|
||||||
|
|
||||||
#include <algorithm>
|
|
||||||
#include <functional>
|
|
||||||
#include <utility>
|
|
||||||
|
|
||||||
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)
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (layerIt->renderer)
|
|
||||||
layerIt->renderer->ShutdownGl();
|
|
||||||
layerIt = mLayers.erase(layerIt);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const RenderCadenceCompositor::RuntimeRenderLayerModel& layer : layers)
|
|
||||||
{
|
|
||||||
if (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;
|
|
||||||
next.renderer = std::make_unique<RuntimeShaderRenderer>();
|
|
||||||
mLayers.push_back(std::move(next));
|
|
||||||
program = &mLayers.back();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (program->shaderId == layer.shaderId && program->sourceFingerprint == fingerprint && program->renderer && program->renderer->HasProgram())
|
|
||||||
continue;
|
|
||||||
if (program->pendingFingerprint == fingerprint)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
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 && layer->renderer && layer->renderer->HasProgram())
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
void RuntimeRenderScene::RenderFrame(uint64_t frameIndex, unsigned width, unsigned height)
|
|
||||||
{
|
|
||||||
ConsumePreparedPrograms();
|
|
||||||
|
|
||||||
for (const std::string& layerId : mLayerOrder)
|
|
||||||
{
|
|
||||||
LayerProgram* layer = FindLayer(layerId);
|
|
||||||
if (!layer || !layer->renderer || !layer->renderer->HasProgram())
|
|
||||||
continue;
|
|
||||||
layer->renderer->RenderFrame(frameIndex, width, height);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void RuntimeRenderScene::ShutdownGl()
|
|
||||||
{
|
|
||||||
mPrepareWorker.Stop();
|
|
||||||
for (LayerProgram& layer : mLayers)
|
|
||||||
{
|
|
||||||
if (layer.renderer)
|
|
||||||
layer.renderer->ShutdownGl();
|
|
||||||
}
|
|
||||||
mLayers.clear();
|
|
||||||
mLayerOrder.clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
std::unique_ptr<RuntimeShaderRenderer> nextRenderer = std::make_unique<RuntimeShaderRenderer>();
|
|
||||||
std::string error;
|
|
||||||
if (!nextRenderer->CommitPreparedProgram(preparedProgram, error))
|
|
||||||
{
|
|
||||||
preparedProgram.ReleaseGl();
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (layer->renderer)
|
|
||||||
layer->renderer->ShutdownGl();
|
|
||||||
layer->renderer = std::move(nextRenderer);
|
|
||||||
layer->shaderId = preparedProgram.shaderId;
|
|
||||||
layer->sourceFingerprint = preparedProgram.sourceFingerprint;
|
|
||||||
layer->pendingFingerprint.clear();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
RuntimeRenderScene::LayerProgram* RuntimeRenderScene::FindLayer(const std::string& layerId)
|
|
||||||
{
|
|
||||||
for (LayerProgram& layer : mLayers)
|
|
||||||
{
|
|
||||||
if (layer.layerId == layerId)
|
|
||||||
return &layer;
|
|
||||||
}
|
|
||||||
return nullptr;
|
|
||||||
}
|
|
||||||
|
|
||||||
const RuntimeRenderScene::LayerProgram* RuntimeRenderScene::FindLayer(const std::string& layerId) const
|
|
||||||
{
|
|
||||||
for (const LayerProgram& layer : mLayers)
|
|
||||||
{
|
|
||||||
if (layer.layerId == layerId)
|
|
||||||
return &layer;
|
|
||||||
}
|
|
||||||
return nullptr;
|
|
||||||
}
|
|
||||||
|
|
||||||
std::string RuntimeRenderScene::Fingerprint(const RuntimeShaderArtifact& artifact)
|
|
||||||
{
|
|
||||||
const std::hash<std::string> hasher;
|
|
||||||
return artifact.shaderId + ":" + std::to_string(artifact.fragmentShaderSource.size()) + ":" + std::to_string(hasher(artifact.fragmentShaderSource));
|
|
||||||
}
|
|
||||||
@@ -3,8 +3,135 @@
|
|||||||
#include "GLExtensions.h"
|
#include "GLExtensions.h"
|
||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
|
#include <array>
|
||||||
#include <cmath>
|
#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)
|
bool SimpleMotionRenderer::InitializeGl(unsigned width, unsigned height)
|
||||||
{
|
{
|
||||||
mWidth = width;
|
mWidth = width;
|
||||||
@@ -14,37 +141,173 @@ bool SimpleMotionRenderer::InitializeGl(unsigned width, unsigned height)
|
|||||||
|
|
||||||
void SimpleMotionRenderer::RenderFrame(uint64_t frameIndex)
|
void SimpleMotionRenderer::RenderFrame(uint64_t frameIndex)
|
||||||
{
|
{
|
||||||
const float t = static_cast<float>(frameIndex) / 60.0f;
|
if (!EnsurePatternProgram())
|
||||||
const float red = 0.1f + 0.4f * (0.5f + 0.5f * std::sin(t));
|
{
|
||||||
const float green = 0.1f + 0.4f * (0.5f + 0.5f * std::sin(t * 0.73f + 1.0f));
|
glViewport(0, 0, static_cast<GLsizei>(mWidth), static_cast<GLsizei>(mHeight));
|
||||||
const float blue = 0.15f + 0.3f * (0.5f + 0.5f * std::sin(t * 0.41f + 2.0f));
|
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));
|
glViewport(0, 0, static_cast<GLsizei>(mWidth), static_cast<GLsizei>(mHeight));
|
||||||
glDisable(GL_SCISSOR_TEST);
|
glDisable(GL_SCISSOR_TEST);
|
||||||
glClearColor(red, green, blue, 1.0f);
|
glDisable(GL_DEPTH_TEST);
|
||||||
glClear(GL_COLOR_BUFFER_BIT);
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
const int boxWidth = (std::max)(1, static_cast<int>(mWidth / 6));
|
void SimpleMotionRenderer::RenderTexture(GLuint texture)
|
||||||
const int boxHeight = (std::max)(1, static_cast<int>(mHeight / 5));
|
{
|
||||||
const float phase = 0.5f + 0.5f * std::sin(t * 1.7f);
|
if (texture == 0 || !EnsureTextureProgram())
|
||||||
const int x = static_cast<int>(phase * static_cast<float>(mWidth - static_cast<unsigned>(boxWidth)));
|
{
|
||||||
const int y = static_cast<int>((0.5f + 0.5f * std::sin(t * 1.1f + 0.8f)) * static_cast<float>(mHeight - static_cast<unsigned>(boxHeight)));
|
RenderFrame(0);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
glEnable(GL_SCISSOR_TEST);
|
glViewport(0, 0, static_cast<GLsizei>(mWidth), static_cast<GLsizei>(mHeight));
|
||||||
glScissor(x, y, boxWidth, boxHeight);
|
|
||||||
glClearColor(1.0f - red, 0.85f, 0.15f + blue, 1.0f);
|
|
||||||
glClear(GL_COLOR_BUFFER_BIT);
|
|
||||||
|
|
||||||
const int stripeWidth = (std::max)(1, static_cast<int>(mWidth / 80));
|
|
||||||
const int stripeX = static_cast<int>((frameIndex % 120) * (mWidth - static_cast<unsigned>(stripeWidth)) / 119);
|
|
||||||
glScissor(stripeX, 0, stripeWidth, static_cast<GLsizei>(mHeight));
|
|
||||||
glClearColor(1.0f, 1.0f, 1.0f, 1.0f);
|
|
||||||
glClear(GL_COLOR_BUFFER_BIT);
|
|
||||||
glDisable(GL_SCISSOR_TEST);
|
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()
|
void SimpleMotionRenderer::ShutdownGl()
|
||||||
{
|
{
|
||||||
|
DestroyPatternProgram();
|
||||||
|
DestroyTextureProgram();
|
||||||
mWidth = 0;
|
mWidth = 0;
|
||||||
mHeight = 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;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
#include "GLExtensions.h"
|
||||||
|
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
|
|
||||||
class SimpleMotionRenderer
|
class SimpleMotionRenderer
|
||||||
@@ -9,12 +11,27 @@ public:
|
|||||||
|
|
||||||
bool InitializeGl(unsigned width, unsigned height);
|
bool InitializeGl(unsigned width, unsigned height);
|
||||||
void RenderFrame(uint64_t frameIndex);
|
void RenderFrame(uint64_t frameIndex);
|
||||||
|
void RenderTexture(GLuint texture);
|
||||||
void ShutdownGl();
|
void ShutdownGl();
|
||||||
|
|
||||||
unsigned Width() const { return mWidth; }
|
unsigned Width() const { return mWidth; }
|
||||||
unsigned Height() const { return mHeight; }
|
unsigned Height() const { return mHeight; }
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
bool EnsureTextureProgram();
|
||||||
|
bool EnsurePatternProgram();
|
||||||
|
void DestroyTextureProgram();
|
||||||
|
void DestroyPatternProgram();
|
||||||
|
static bool CompileShader(GLenum shaderType, const char* source, GLuint& shader);
|
||||||
|
|
||||||
unsigned mWidth = 0;
|
unsigned mWidth = 0;
|
||||||
unsigned mHeight = 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;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,8 +2,18 @@
|
|||||||
|
|
||||||
#include "../frames/SystemFrameTypes.h"
|
#include "../frames/SystemFrameTypes.h"
|
||||||
|
|
||||||
|
#include <chrono>
|
||||||
#include <cstring>
|
#include <cstring>
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
double MillisecondsSince(std::chrono::steady_clock::time_point start)
|
||||||
|
{
|
||||||
|
return std::chrono::duration_cast<std::chrono::duration<double, std::milli>>(
|
||||||
|
std::chrono::steady_clock::now() - start).count();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Bgra8ReadbackPipeline::~Bgra8ReadbackPipeline()
|
Bgra8ReadbackPipeline::~Bgra8ReadbackPipeline()
|
||||||
{
|
{
|
||||||
Shutdown();
|
Shutdown();
|
||||||
@@ -50,10 +60,15 @@ bool Bgra8ReadbackPipeline::RenderAndQueue(uint64_t frameIndex, const RenderCall
|
|||||||
return false;
|
return false;
|
||||||
|
|
||||||
glBindFramebuffer(GL_FRAMEBUFFER, mFramebuffer);
|
glBindFramebuffer(GL_FRAMEBUFFER, mFramebuffer);
|
||||||
|
const auto renderStart = std::chrono::steady_clock::now();
|
||||||
renderFrame(frameIndex);
|
renderFrame(frameIndex);
|
||||||
|
mLastRenderFrameMilliseconds = MillisecondsSince(renderStart);
|
||||||
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||||
|
|
||||||
return mPboRing.QueueReadback(mFramebuffer, mWidth, mHeight, frameIndex);
|
const auto queueStart = std::chrono::steady_clock::now();
|
||||||
|
const bool queued = mPboRing.QueueReadback(mFramebuffer, mWidth, mHeight, frameIndex);
|
||||||
|
mLastReadbackQueueMilliseconds = MillisecondsSince(queueStart);
|
||||||
|
return queued;
|
||||||
}
|
}
|
||||||
|
|
||||||
void Bgra8ReadbackPipeline::ConsumeCompleted(
|
void Bgra8ReadbackPipeline::ConsumeCompleted(
|
||||||
@@ -68,12 +83,14 @@ void Bgra8ReadbackPipeline::ConsumeCompleted(
|
|||||||
PboReadbackRing::CompletedReadback readback;
|
PboReadbackRing::CompletedReadback readback;
|
||||||
while (mPboRing.TryAcquireCompleted(readback))
|
while (mPboRing.TryAcquireCompleted(readback))
|
||||||
{
|
{
|
||||||
|
const auto copyStart = std::chrono::steady_clock::now();
|
||||||
glBindBuffer(GL_PIXEL_PACK_BUFFER, readback.pbo);
|
glBindBuffer(GL_PIXEL_PACK_BUFFER, readback.pbo);
|
||||||
void* mapped = glMapBuffer(GL_PIXEL_PACK_BUFFER, GL_READ_ONLY);
|
void* mapped = glMapBuffer(GL_PIXEL_PACK_BUFFER, GL_READ_ONLY);
|
||||||
if (!mapped)
|
if (!mapped)
|
||||||
{
|
{
|
||||||
glBindBuffer(GL_PIXEL_PACK_BUFFER, 0);
|
glBindBuffer(GL_PIXEL_PACK_BUFFER, 0);
|
||||||
mPboRing.ReleaseCompleted(readback);
|
mPboRing.ReleaseCompleted(readback);
|
||||||
|
mLastCompletedReadbackCopyMilliseconds = MillisecondsSince(copyStart);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -99,6 +116,7 @@ void Bgra8ReadbackPipeline::ConsumeCompleted(
|
|||||||
glUnmapBuffer(GL_PIXEL_PACK_BUFFER);
|
glUnmapBuffer(GL_PIXEL_PACK_BUFFER);
|
||||||
glBindBuffer(GL_PIXEL_PACK_BUFFER, 0);
|
glBindBuffer(GL_PIXEL_PACK_BUFFER, 0);
|
||||||
mPboRing.ReleaseCompleted(readback);
|
mPboRing.ReleaseCompleted(readback);
|
||||||
|
mLastCompletedReadbackCopyMilliseconds = MillisecondsSince(copyStart);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -38,6 +38,9 @@ public:
|
|||||||
unsigned RowBytes() const { return mRowBytes; }
|
unsigned RowBytes() const { return mRowBytes; }
|
||||||
VideoIOPixelFormat PixelFormat() const { return VideoIOPixelFormat::Bgra8; }
|
VideoIOPixelFormat PixelFormat() const { return VideoIOPixelFormat::Bgra8; }
|
||||||
uint64_t PboQueueMisses() const { return mPboRing.QueueMisses(); }
|
uint64_t PboQueueMisses() const { return mPboRing.QueueMisses(); }
|
||||||
|
double LastRenderFrameMilliseconds() const { return mLastRenderFrameMilliseconds; }
|
||||||
|
double LastReadbackQueueMilliseconds() const { return mLastReadbackQueueMilliseconds; }
|
||||||
|
double LastCompletedReadbackCopyMilliseconds() const { return mLastCompletedReadbackCopyMilliseconds; }
|
||||||
|
|
||||||
private:
|
private:
|
||||||
bool CreateRenderTarget();
|
bool CreateRenderTarget();
|
||||||
@@ -48,5 +51,8 @@ private:
|
|||||||
unsigned mRowBytes = 0;
|
unsigned mRowBytes = 0;
|
||||||
GLuint mFramebuffer = 0;
|
GLuint mFramebuffer = 0;
|
||||||
GLuint mTexture = 0;
|
GLuint mTexture = 0;
|
||||||
|
double mLastRenderFrameMilliseconds = 0.0;
|
||||||
|
double mLastReadbackQueueMilliseconds = 0.0;
|
||||||
|
double mLastCompletedReadbackCopyMilliseconds = 0.0;
|
||||||
PboReadbackRing mPboRing;
|
PboReadbackRing mPboRing;
|
||||||
};
|
};
|
||||||
@@ -0,0 +1,271 @@
|
|||||||
|
#include "RuntimeRenderScene.h"
|
||||||
|
|
||||||
|
#include "../../platform/HiddenGlWindow.h"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <functional>
|
||||||
|
#include <utility>
|
||||||
|
|
||||||
|
#ifndef GL_FRAMEBUFFER_BINDING
|
||||||
|
#define GL_FRAMEBUFFER_BINDING 0x8CA6
|
||||||
|
#endif
|
||||||
|
|
||||||
|
RuntimeRenderScene::~RuntimeRenderScene()
|
||||||
|
{
|
||||||
|
ShutdownGl();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool RuntimeRenderScene::StartPrepareWorker(std::unique_ptr<HiddenGlWindow> sharedWindow, std::string& error)
|
||||||
|
{
|
||||||
|
return mPrepareWorker.Start(std::move(sharedWindow), error);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool RuntimeRenderScene::CommitRenderLayers(const std::vector<RenderCadenceCompositor::RuntimeRenderLayerModel>& layers, std::string& error)
|
||||||
|
{
|
||||||
|
ConsumePreparedPrograms();
|
||||||
|
|
||||||
|
std::vector<std::string> nextOrder;
|
||||||
|
std::vector<RenderCadenceCompositor::RuntimeRenderLayerModel> layersToPrepare;
|
||||||
|
nextOrder.reserve(layers.size());
|
||||||
|
for (const RenderCadenceCompositor::RuntimeRenderLayerModel& layer : layers)
|
||||||
|
{
|
||||||
|
if (!layer.bypass)
|
||||||
|
nextOrder.push_back(layer.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (auto layerIt = mLayers.begin(); layerIt != mLayers.end();)
|
||||||
|
{
|
||||||
|
const bool stillPresent = std::find(nextOrder.begin(), nextOrder.end(), layerIt->layerId) != nextOrder.end();
|
||||||
|
if (stillPresent)
|
||||||
|
{
|
||||||
|
++layerIt;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (LayerProgram::PassProgram& pass : layerIt->passes)
|
||||||
|
{
|
||||||
|
if (pass.renderer)
|
||||||
|
pass.renderer->ShutdownGl();
|
||||||
|
}
|
||||||
|
ReleasePendingPrograms(*layerIt);
|
||||||
|
layerIt = mLayers.erase(layerIt);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const RenderCadenceCompositor::RuntimeRenderLayerModel& layer : layers)
|
||||||
|
{
|
||||||
|
if (layer.bypass)
|
||||||
|
continue;
|
||||||
|
if (layer.artifact.passes.empty() && layer.artifact.fragmentShaderSource.empty())
|
||||||
|
continue;
|
||||||
|
|
||||||
|
const std::string fingerprint = Fingerprint(layer.artifact);
|
||||||
|
LayerProgram* program = FindLayer(layer.id);
|
||||||
|
if (!program)
|
||||||
|
{
|
||||||
|
LayerProgram next;
|
||||||
|
next.layerId = layer.id;
|
||||||
|
mLayers.push_back(std::move(next));
|
||||||
|
program = &mLayers.back();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool hasReadyPass = false;
|
||||||
|
for (const LayerProgram::PassProgram& pass : program->passes)
|
||||||
|
{
|
||||||
|
if (pass.renderer && pass.renderer->HasProgram())
|
||||||
|
{
|
||||||
|
hasReadyPass = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (program->shaderId == layer.shaderId && program->sourceFingerprint == fingerprint && hasReadyPass)
|
||||||
|
{
|
||||||
|
for (LayerProgram::PassProgram& pass : program->passes)
|
||||||
|
{
|
||||||
|
if (pass.renderer)
|
||||||
|
pass.renderer->UpdateArtifactState(layer.artifact);
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (program->pendingFingerprint == fingerprint)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
ReleasePendingPrograms(*program);
|
||||||
|
program->shaderId = layer.shaderId;
|
||||||
|
program->pendingFingerprint = fingerprint;
|
||||||
|
layersToPrepare.push_back(layer);
|
||||||
|
}
|
||||||
|
|
||||||
|
mLayerOrder = std::move(nextOrder);
|
||||||
|
if (!layersToPrepare.empty())
|
||||||
|
mPrepareWorker.Submit(layersToPrepare);
|
||||||
|
error.clear();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void RuntimeRenderScene::ShutdownGl()
|
||||||
|
{
|
||||||
|
mPrepareWorker.Stop();
|
||||||
|
for (LayerProgram& layer : mLayers)
|
||||||
|
{
|
||||||
|
for (LayerProgram::PassProgram& pass : layer.passes)
|
||||||
|
{
|
||||||
|
if (pass.renderer)
|
||||||
|
pass.renderer->ShutdownGl();
|
||||||
|
}
|
||||||
|
ReleasePendingPrograms(layer);
|
||||||
|
}
|
||||||
|
mLayers.clear();
|
||||||
|
mLayerOrder.clear();
|
||||||
|
DestroyLayerTargets();
|
||||||
|
}
|
||||||
|
|
||||||
|
void RuntimeRenderScene::ConsumePreparedPrograms()
|
||||||
|
{
|
||||||
|
RuntimePreparedShaderProgram preparedProgram;
|
||||||
|
while (mPrepareWorker.TryConsume(preparedProgram))
|
||||||
|
{
|
||||||
|
if (!preparedProgram.succeeded)
|
||||||
|
{
|
||||||
|
preparedProgram.ReleaseGl();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
LayerProgram* layer = FindLayer(preparedProgram.layerId);
|
||||||
|
if (!layer || layer->pendingFingerprint != preparedProgram.sourceFingerprint)
|
||||||
|
{
|
||||||
|
preparedProgram.ReleaseGl();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool replacesExistingPendingPass = false;
|
||||||
|
for (RuntimePreparedShaderProgram& existing : layer->pendingPreparedPrograms)
|
||||||
|
{
|
||||||
|
if (existing.passId != preparedProgram.passId)
|
||||||
|
continue;
|
||||||
|
existing.ReleaseGl();
|
||||||
|
existing = std::move(preparedProgram);
|
||||||
|
replacesExistingPendingPass = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (!replacesExistingPendingPass)
|
||||||
|
layer->pendingPreparedPrograms.push_back(std::move(preparedProgram));
|
||||||
|
TryCommitPendingPrograms(*layer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void RuntimeRenderScene::ReleasePendingPrograms(LayerProgram& layer)
|
||||||
|
{
|
||||||
|
for (RuntimePreparedShaderProgram& program : layer.pendingPreparedPrograms)
|
||||||
|
program.ReleaseGl();
|
||||||
|
layer.pendingPreparedPrograms.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
void RuntimeRenderScene::TryCommitPendingPrograms(LayerProgram& layer)
|
||||||
|
{
|
||||||
|
if (layer.pendingPreparedPrograms.empty())
|
||||||
|
return;
|
||||||
|
|
||||||
|
const RuntimeShaderArtifact& artifact = layer.pendingPreparedPrograms.front().artifact;
|
||||||
|
const std::size_t expectedPassCount = artifact.passes.empty() ? 1 : artifact.passes.size();
|
||||||
|
if (layer.pendingPreparedPrograms.size() < expectedPassCount)
|
||||||
|
return;
|
||||||
|
|
||||||
|
std::vector<LayerProgram::PassProgram> nextPasses;
|
||||||
|
nextPasses.reserve(expectedPassCount);
|
||||||
|
for (const RuntimeShaderPassArtifact& passArtifact : artifact.passes)
|
||||||
|
{
|
||||||
|
auto preparedIt = std::find_if(
|
||||||
|
layer.pendingPreparedPrograms.begin(),
|
||||||
|
layer.pendingPreparedPrograms.end(),
|
||||||
|
[&passArtifact](const RuntimePreparedShaderProgram& prepared) {
|
||||||
|
return prepared.passId == passArtifact.passId;
|
||||||
|
});
|
||||||
|
if (preparedIt == layer.pendingPreparedPrograms.end())
|
||||||
|
return;
|
||||||
|
|
||||||
|
std::unique_ptr<RuntimeShaderRenderer> nextRenderer = std::make_unique<RuntimeShaderRenderer>();
|
||||||
|
std::string error;
|
||||||
|
if (!nextRenderer->CommitPreparedProgram(*preparedIt, error))
|
||||||
|
{
|
||||||
|
ReleasePendingPrograms(layer);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
LayerProgram::PassProgram nextPass;
|
||||||
|
nextPass.passId = preparedIt->passId;
|
||||||
|
nextPass.inputNames = preparedIt->inputNames;
|
||||||
|
nextPass.outputName = preparedIt->outputName.empty() ? preparedIt->passId : preparedIt->outputName;
|
||||||
|
nextPass.renderer = std::move(nextRenderer);
|
||||||
|
nextPasses.push_back(std::move(nextPass));
|
||||||
|
}
|
||||||
|
if (artifact.passes.empty())
|
||||||
|
{
|
||||||
|
RuntimePreparedShaderProgram& prepared = layer.pendingPreparedPrograms.front();
|
||||||
|
std::unique_ptr<RuntimeShaderRenderer> nextRenderer = std::make_unique<RuntimeShaderRenderer>();
|
||||||
|
std::string error;
|
||||||
|
if (!nextRenderer->CommitPreparedProgram(prepared, error))
|
||||||
|
{
|
||||||
|
ReleasePendingPrograms(layer);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
LayerProgram::PassProgram nextPass;
|
||||||
|
nextPass.passId = prepared.passId;
|
||||||
|
nextPass.inputNames = prepared.inputNames;
|
||||||
|
nextPass.outputName = prepared.outputName.empty() ? prepared.passId : prepared.outputName;
|
||||||
|
nextPass.renderer = std::move(nextRenderer);
|
||||||
|
nextPasses.push_back(std::move(nextPass));
|
||||||
|
}
|
||||||
|
|
||||||
|
for (LayerProgram::PassProgram& pass : layer.passes)
|
||||||
|
{
|
||||||
|
if (pass.renderer)
|
||||||
|
pass.renderer->ShutdownGl();
|
||||||
|
}
|
||||||
|
layer.passes = std::move(nextPasses);
|
||||||
|
layer.shaderId = artifact.shaderId;
|
||||||
|
layer.sourceFingerprint = layer.pendingPreparedPrograms.front().sourceFingerprint;
|
||||||
|
layer.pendingFingerprint.clear();
|
||||||
|
layer.pendingPreparedPrograms.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
RuntimeRenderScene::LayerProgram* RuntimeRenderScene::FindLayer(const std::string& layerId)
|
||||||
|
{
|
||||||
|
for (LayerProgram& layer : mLayers)
|
||||||
|
{
|
||||||
|
if (layer.layerId == layerId)
|
||||||
|
return &layer;
|
||||||
|
}
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
const RuntimeRenderScene::LayerProgram* RuntimeRenderScene::FindLayer(const std::string& layerId) const
|
||||||
|
{
|
||||||
|
for (const LayerProgram& layer : mLayers)
|
||||||
|
{
|
||||||
|
if (layer.layerId == layerId)
|
||||||
|
return &layer;
|
||||||
|
}
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
RuntimeRenderScene::LayerProgram::PassProgram* RuntimeRenderScene::FindPass(LayerProgram& layer, const std::string& passId)
|
||||||
|
{
|
||||||
|
for (LayerProgram::PassProgram& pass : layer.passes)
|
||||||
|
{
|
||||||
|
if (pass.passId == passId)
|
||||||
|
return &pass;
|
||||||
|
}
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string RuntimeRenderScene::Fingerprint(const RuntimeShaderArtifact& artifact)
|
||||||
|
{
|
||||||
|
const std::hash<std::string> hasher;
|
||||||
|
std::string source;
|
||||||
|
for (const RuntimeShaderPassArtifact& pass : artifact.passes)
|
||||||
|
source += pass.passId + ":" + pass.outputName + ":" + pass.fragmentShaderSource + "\n";
|
||||||
|
if (source.empty())
|
||||||
|
source = artifact.fragmentShaderSource;
|
||||||
|
return artifact.shaderId + ":" + std::to_string(source.size()) + ":" + std::to_string(hasher(source));
|
||||||
|
}
|
||||||
@@ -22,7 +22,7 @@ public:
|
|||||||
bool StartPrepareWorker(std::unique_ptr<HiddenGlWindow> sharedWindow, std::string& error);
|
bool StartPrepareWorker(std::unique_ptr<HiddenGlWindow> sharedWindow, std::string& error);
|
||||||
bool CommitRenderLayers(const std::vector<RenderCadenceCompositor::RuntimeRenderLayerModel>& layers, std::string& error);
|
bool CommitRenderLayers(const std::vector<RenderCadenceCompositor::RuntimeRenderLayerModel>& layers, std::string& error);
|
||||||
bool HasLayers();
|
bool HasLayers();
|
||||||
void RenderFrame(uint64_t frameIndex, unsigned width, unsigned height);
|
void RenderFrame(uint64_t frameIndex, unsigned width, unsigned height, GLuint videoInputTexture = 0);
|
||||||
void ShutdownGl();
|
void ShutdownGl();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
@@ -32,15 +32,33 @@ private:
|
|||||||
std::string shaderId;
|
std::string shaderId;
|
||||||
std::string sourceFingerprint;
|
std::string sourceFingerprint;
|
||||||
std::string pendingFingerprint;
|
std::string pendingFingerprint;
|
||||||
std::unique_ptr<RuntimeShaderRenderer> renderer;
|
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 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);
|
LayerProgram* FindLayer(const std::string& layerId);
|
||||||
const LayerProgram* FindLayer(const std::string& layerId) const;
|
const LayerProgram* FindLayer(const std::string& layerId) const;
|
||||||
|
LayerProgram::PassProgram* FindPass(LayerProgram& layer, const std::string& passId);
|
||||||
static std::string Fingerprint(const RuntimeShaderArtifact& artifact);
|
static std::string Fingerprint(const RuntimeShaderArtifact& artifact);
|
||||||
|
|
||||||
RuntimeShaderPrepareWorker mPrepareWorker;
|
RuntimeShaderPrepareWorker mPrepareWorker;
|
||||||
std::vector<LayerProgram> mLayers;
|
std::vector<LayerProgram> mLayers;
|
||||||
std::vector<std::string> mLayerOrder;
|
std::vector<std::string> mLayerOrder;
|
||||||
|
GLuint mLayerFramebuffers[4] = {};
|
||||||
|
GLuint mLayerTextures[4] = {};
|
||||||
|
unsigned mLayerTargetWidth = 0;
|
||||||
|
unsigned mLayerTargetHeight = 0;
|
||||||
};
|
};
|
||||||
@@ -0,0 +1,219 @@
|
|||||||
|
#include "RuntimeRenderScene.h"
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#ifndef GL_FRAMEBUFFER_BINDING
|
||||||
|
#define GL_FRAMEBUFFER_BINDING 0x8CA6
|
||||||
|
#endif
|
||||||
|
|
||||||
|
bool RuntimeRenderScene::HasLayers()
|
||||||
|
{
|
||||||
|
ConsumePreparedPrograms();
|
||||||
|
|
||||||
|
for (const std::string& layerId : mLayerOrder)
|
||||||
|
{
|
||||||
|
const LayerProgram* layer = FindLayer(layerId);
|
||||||
|
if (!layer)
|
||||||
|
continue;
|
||||||
|
for (const LayerProgram::PassProgram& pass : layer->passes)
|
||||||
|
{
|
||||||
|
if (pass.renderer && pass.renderer->HasProgram())
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void RuntimeRenderScene::RenderFrame(uint64_t frameIndex, unsigned width, unsigned height, GLuint videoInputTexture)
|
||||||
|
{
|
||||||
|
ConsumePreparedPrograms();
|
||||||
|
|
||||||
|
std::vector<LayerProgram*> readyLayers;
|
||||||
|
for (const std::string& layerId : mLayerOrder)
|
||||||
|
{
|
||||||
|
LayerProgram* layer = FindLayer(layerId);
|
||||||
|
if (!layer)
|
||||||
|
continue;
|
||||||
|
for (const LayerProgram::PassProgram& pass : layer->passes)
|
||||||
|
{
|
||||||
|
if (pass.renderer && pass.renderer->HasProgram())
|
||||||
|
{
|
||||||
|
readyLayers.push_back(layer);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (readyLayers.empty())
|
||||||
|
return;
|
||||||
|
|
||||||
|
GLint outputFramebuffer = 0;
|
||||||
|
glGetIntegerv(GL_FRAMEBUFFER_BINDING, &outputFramebuffer);
|
||||||
|
|
||||||
|
if (readyLayers.size() == 1)
|
||||||
|
{
|
||||||
|
RenderLayer(*readyLayers.front(), frameIndex, width, height, videoInputTexture, videoInputTexture, static_cast<GLuint>(outputFramebuffer), true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!EnsureLayerTargets(width, height))
|
||||||
|
{
|
||||||
|
glBindFramebuffer(GL_FRAMEBUFFER, static_cast<GLuint>(outputFramebuffer));
|
||||||
|
RenderLayer(*readyLayers.back(), frameIndex, width, height, videoInputTexture, videoInputTexture, static_cast<GLuint>(outputFramebuffer), true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Shader source contract:
|
||||||
|
// - gVideoInput is the decoded current input texture for every layer in the stack.
|
||||||
|
// - gLayerInput starts as gVideoInput for the first layer, then becomes the previous layer output.
|
||||||
|
GLuint layerInputTexture = videoInputTexture;
|
||||||
|
std::size_t nextTargetIndex = 0;
|
||||||
|
for (std::size_t layerIndex = 0; layerIndex < readyLayers.size(); ++layerIndex)
|
||||||
|
{
|
||||||
|
const bool isFinalLayer = layerIndex == readyLayers.size() - 1;
|
||||||
|
if (isFinalLayer)
|
||||||
|
{
|
||||||
|
glBindFramebuffer(GL_FRAMEBUFFER, static_cast<GLuint>(outputFramebuffer));
|
||||||
|
RenderLayer(*readyLayers[layerIndex], frameIndex, width, height, videoInputTexture, layerInputTexture, static_cast<GLuint>(outputFramebuffer), true);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
RenderLayer(*readyLayers[layerIndex], frameIndex, width, height, videoInputTexture, layerInputTexture, mLayerFramebuffers[nextTargetIndex], true);
|
||||||
|
layerInputTexture = mLayerTextures[nextTargetIndex];
|
||||||
|
nextTargetIndex = 1 - nextTargetIndex;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
GLuint RuntimeRenderScene::RenderLayer(
|
||||||
|
LayerProgram& layer,
|
||||||
|
uint64_t frameIndex,
|
||||||
|
unsigned width,
|
||||||
|
unsigned height,
|
||||||
|
GLuint videoInputTexture,
|
||||||
|
GLuint layerInputTexture,
|
||||||
|
GLuint outputFramebuffer,
|
||||||
|
bool renderToOutput)
|
||||||
|
{
|
||||||
|
GLuint namedOutputs[2] = {};
|
||||||
|
std::string namedOutputNames[2];
|
||||||
|
std::size_t nextTargetIndex = 2;
|
||||||
|
GLuint lastOutputTexture = layerInputTexture;
|
||||||
|
|
||||||
|
for (LayerProgram::PassProgram& pass : layer.passes)
|
||||||
|
{
|
||||||
|
if (!pass.renderer || !pass.renderer->HasProgram())
|
||||||
|
continue;
|
||||||
|
|
||||||
|
GLuint sourceTexture = videoInputTexture;
|
||||||
|
if (!pass.inputNames.empty())
|
||||||
|
{
|
||||||
|
const std::string& inputName = pass.inputNames.front();
|
||||||
|
if (inputName == "videoInput")
|
||||||
|
{
|
||||||
|
sourceTexture = videoInputTexture;
|
||||||
|
}
|
||||||
|
else if (inputName != "layerInput")
|
||||||
|
{
|
||||||
|
// Named intermediate pass inputs currently use the gVideoInput binding slot as the
|
||||||
|
// selected pass source. Layer stack shaders should use gLayerInput for previous-layer
|
||||||
|
// sampling and gVideoInput for the original input frame.
|
||||||
|
for (std::size_t index = 0; index < 2; ++index)
|
||||||
|
{
|
||||||
|
if (namedOutputNames[index] == inputName)
|
||||||
|
{
|
||||||
|
sourceTexture = namedOutputs[index];
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const bool writesLayerOutput = pass.outputName == "layerOutput";
|
||||||
|
if (writesLayerOutput && renderToOutput)
|
||||||
|
{
|
||||||
|
glBindFramebuffer(GL_FRAMEBUFFER, outputFramebuffer);
|
||||||
|
pass.renderer->RenderFrame(frameIndex, width, height, sourceTexture, layerInputTexture);
|
||||||
|
lastOutputTexture = 0;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!EnsureLayerTargets(width, height))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
const std::size_t targetIndex = nextTargetIndex;
|
||||||
|
nextTargetIndex = nextTargetIndex == 2 ? 3 : 2;
|
||||||
|
glBindFramebuffer(GL_FRAMEBUFFER, mLayerFramebuffers[targetIndex]);
|
||||||
|
pass.renderer->RenderFrame(frameIndex, width, height, sourceTexture, layerInputTexture);
|
||||||
|
const std::size_t namedIndex = targetIndex - 2;
|
||||||
|
namedOutputs[namedIndex] = mLayerTextures[targetIndex];
|
||||||
|
namedOutputNames[namedIndex] = pass.outputName;
|
||||||
|
lastOutputTexture = mLayerTextures[targetIndex];
|
||||||
|
}
|
||||||
|
|
||||||
|
return lastOutputTexture;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool RuntimeRenderScene::EnsureLayerTargets(unsigned width, unsigned height)
|
||||||
|
{
|
||||||
|
if (width == 0 || height == 0)
|
||||||
|
return false;
|
||||||
|
if (mLayerFramebuffers[0] != 0 && mLayerFramebuffers[1] != 0 && mLayerFramebuffers[2] != 0 && mLayerFramebuffers[3] != 0
|
||||||
|
&& mLayerTextures[0] != 0 && mLayerTextures[1] != 0 && mLayerTextures[2] != 0 && mLayerTextures[3] != 0
|
||||||
|
&& mLayerTargetWidth == width && mLayerTargetHeight == height)
|
||||||
|
return true;
|
||||||
|
|
||||||
|
DestroyLayerTargets();
|
||||||
|
mLayerTargetWidth = width;
|
||||||
|
mLayerTargetHeight = height;
|
||||||
|
|
||||||
|
glGenFramebuffers(4, mLayerFramebuffers);
|
||||||
|
glGenTextures(4, mLayerTextures);
|
||||||
|
for (int index = 0; index < 4; ++index)
|
||||||
|
{
|
||||||
|
glBindTexture(GL_TEXTURE_2D, mLayerTextures[index]);
|
||||||
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
|
||||||
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
|
||||||
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
|
||||||
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
|
||||||
|
glTexImage2D(
|
||||||
|
GL_TEXTURE_2D,
|
||||||
|
0,
|
||||||
|
GL_RGBA8,
|
||||||
|
static_cast<GLsizei>(width),
|
||||||
|
static_cast<GLsizei>(height),
|
||||||
|
0,
|
||||||
|
GL_BGRA,
|
||||||
|
GL_UNSIGNED_INT_8_8_8_8_REV,
|
||||||
|
nullptr);
|
||||||
|
|
||||||
|
glBindFramebuffer(GL_FRAMEBUFFER, mLayerFramebuffers[index]);
|
||||||
|
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, mLayerTextures[index], 0);
|
||||||
|
if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE)
|
||||||
|
{
|
||||||
|
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||||
|
glBindTexture(GL_TEXTURE_2D, 0);
|
||||||
|
DestroyLayerTargets();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||||
|
glBindTexture(GL_TEXTURE_2D, 0);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void RuntimeRenderScene::DestroyLayerTargets()
|
||||||
|
{
|
||||||
|
if (mLayerFramebuffers[0] != 0 || mLayerFramebuffers[1] != 0 || mLayerFramebuffers[2] != 0 || mLayerFramebuffers[3] != 0)
|
||||||
|
glDeleteFramebuffers(4, mLayerFramebuffers);
|
||||||
|
if (mLayerTextures[0] != 0 || mLayerTextures[1] != 0 || mLayerTextures[2] != 0 || mLayerTextures[3] != 0)
|
||||||
|
glDeleteTextures(4, mLayerTextures);
|
||||||
|
for (int index = 0; index < 4; ++index)
|
||||||
|
{
|
||||||
|
mLayerFramebuffers[index] = 0;
|
||||||
|
mLayerTextures[index] = 0;
|
||||||
|
}
|
||||||
|
mLayerTargetWidth = 0;
|
||||||
|
mLayerTargetHeight = 0;
|
||||||
|
}
|
||||||
@@ -82,7 +82,10 @@ std::vector<unsigned char> BuildRuntimeShaderGlobalParamsStd140(
|
|||||||
|
|
||||||
for (const ShaderParameterDefinition& definition : artifact.parameterDefinitions)
|
for (const ShaderParameterDefinition& definition : artifact.parameterDefinitions)
|
||||||
{
|
{
|
||||||
const ShaderParameterValue value = DefaultValueForDefinition(definition);
|
const auto valueIt = artifact.parameterValues.find(definition.id);
|
||||||
|
const ShaderParameterValue value = valueIt == artifact.parameterValues.end()
|
||||||
|
? DefaultValueForDefinition(definition)
|
||||||
|
: valueIt->second;
|
||||||
switch (definition.type)
|
switch (definition.type)
|
||||||
{
|
{
|
||||||
case ShaderParameterType::Float:
|
case ShaderParameterType::Float:
|
||||||
@@ -109,8 +112,8 @@ std::vector<unsigned char> BuildRuntimeShaderGlobalParamsStd140(
|
|||||||
case ShaderParameterType::Text:
|
case ShaderParameterType::Text:
|
||||||
break;
|
break;
|
||||||
case ShaderParameterType::Trigger:
|
case ShaderParameterType::Trigger:
|
||||||
AppendStd140Int(buffer, 0);
|
AppendStd140Int(buffer, value.numberValues.empty() ? 0 : static_cast<int>(value.numberValues[0]));
|
||||||
AppendStd140Float(buffer, -1000000.0f);
|
AppendStd140Float(buffer, value.numberValues.size() > 1 ? static_cast<float>(value.numberValues[1]) : -1000000.0f);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
#include "RuntimeShaderPrepareWorker.h"
|
#include "RuntimeShaderPrepareWorker.h"
|
||||||
|
|
||||||
#include "../platform/HiddenGlWindow.h"
|
#include "../../platform/HiddenGlWindow.h"
|
||||||
#include "RuntimeShaderRenderer.h"
|
#include "RuntimeShaderRenderer.h"
|
||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
@@ -77,20 +77,35 @@ void RuntimeShaderPrepareWorker::Submit(const std::vector<RenderCadenceComposito
|
|||||||
std::lock_guard<std::mutex> lock(mMutex);
|
std::lock_guard<std::mutex> lock(mMutex);
|
||||||
for (const RenderCadenceCompositor::RuntimeRenderLayerModel& layer : layers)
|
for (const RenderCadenceCompositor::RuntimeRenderLayerModel& layer : layers)
|
||||||
{
|
{
|
||||||
if (layer.artifact.fragmentShaderSource.empty())
|
if (layer.artifact.passes.empty() && layer.artifact.fragmentShaderSource.empty())
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
PrepareRequest request;
|
std::vector<RuntimeShaderPassArtifact> passes = layer.artifact.passes;
|
||||||
request.layerId = layer.id;
|
if (passes.empty())
|
||||||
request.shaderId = layer.shaderId;
|
{
|
||||||
request.sourceFingerprint = Fingerprint(layer.artifact);
|
RuntimeShaderPassArtifact pass;
|
||||||
request.artifact = layer.artifact;
|
pass.passId = "main";
|
||||||
|
pass.fragmentShaderSource = layer.artifact.fragmentShaderSource;
|
||||||
|
pass.outputName = "layerOutput";
|
||||||
|
passes.push_back(std::move(pass));
|
||||||
|
}
|
||||||
|
|
||||||
auto sameLayer = [&request](const PrepareRequest& existing) {
|
auto sameLayer = [&layer](const PrepareRequest& existing) {
|
||||||
return existing.layerId == request.layerId;
|
return existing.layerId == layer.id;
|
||||||
};
|
};
|
||||||
mRequests.erase(std::remove_if(mRequests.begin(), mRequests.end(), sameLayer), mRequests.end());
|
mRequests.erase(std::remove_if(mRequests.begin(), mRequests.end(), sameLayer), mRequests.end());
|
||||||
mRequests.push_back(std::move(request));
|
|
||||||
|
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();
|
mCondition.notify_one();
|
||||||
}
|
}
|
||||||
@@ -137,10 +152,11 @@ void RuntimeShaderPrepareWorker::ThreadMain()
|
|||||||
}
|
}
|
||||||
|
|
||||||
RuntimePreparedShaderProgram preparedProgram;
|
RuntimePreparedShaderProgram preparedProgram;
|
||||||
RuntimeShaderRenderer::BuildPreparedProgram(
|
RuntimeShaderRenderer::BuildPreparedPassProgram(
|
||||||
request.layerId,
|
request.layerId,
|
||||||
request.sourceFingerprint,
|
request.sourceFingerprint,
|
||||||
request.artifact,
|
request.artifact,
|
||||||
|
request.passArtifact,
|
||||||
preparedProgram);
|
preparedProgram);
|
||||||
glFlush();
|
glFlush();
|
||||||
|
|
||||||
@@ -154,5 +170,10 @@ void RuntimeShaderPrepareWorker::ThreadMain()
|
|||||||
std::string RuntimeShaderPrepareWorker::Fingerprint(const RuntimeShaderArtifact& artifact)
|
std::string RuntimeShaderPrepareWorker::Fingerprint(const RuntimeShaderArtifact& artifact)
|
||||||
{
|
{
|
||||||
const std::hash<std::string> hasher;
|
const std::hash<std::string> hasher;
|
||||||
return artifact.shaderId + ":" + std::to_string(artifact.fragmentShaderSource.size()) + ":" + std::to_string(hasher(artifact.fragmentShaderSource));
|
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));
|
||||||
}
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include "RuntimeShaderProgram.h"
|
#include "RuntimeShaderProgram.h"
|
||||||
#include "../runtime/RuntimeLayerModel.h"
|
#include "../../runtime/RuntimeLayerModel.h"
|
||||||
|
|
||||||
#include <windows.h>
|
#include <windows.h>
|
||||||
|
|
||||||
@@ -35,8 +35,10 @@ private:
|
|||||||
{
|
{
|
||||||
std::string layerId;
|
std::string layerId;
|
||||||
std::string shaderId;
|
std::string shaderId;
|
||||||
|
std::string passId;
|
||||||
std::string sourceFingerprint;
|
std::string sourceFingerprint;
|
||||||
RuntimeShaderArtifact artifact;
|
RuntimeShaderArtifact artifact;
|
||||||
|
RuntimeShaderPassArtifact passArtifact;
|
||||||
};
|
};
|
||||||
|
|
||||||
void ThreadMain();
|
void ThreadMain();
|
||||||
@@ -1,16 +1,21 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include "GLExtensions.h"
|
#include "GLExtensions.h"
|
||||||
#include "../runtime/RuntimeShaderArtifact.h"
|
#include "../../runtime/RuntimeShaderArtifact.h"
|
||||||
|
|
||||||
#include <string>
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
struct RuntimePreparedShaderProgram
|
struct RuntimePreparedShaderProgram
|
||||||
{
|
{
|
||||||
std::string layerId;
|
std::string layerId;
|
||||||
std::string shaderId;
|
std::string shaderId;
|
||||||
|
std::string passId;
|
||||||
std::string sourceFingerprint;
|
std::string sourceFingerprint;
|
||||||
RuntimeShaderArtifact artifact;
|
RuntimeShaderArtifact artifact;
|
||||||
|
RuntimeShaderPassArtifact passArtifact;
|
||||||
|
std::vector<std::string> inputNames;
|
||||||
|
std::string outputName;
|
||||||
GLuint program = 0;
|
GLuint program = 0;
|
||||||
GLuint vertexShader = 0;
|
GLuint vertexShader = 0;
|
||||||
GLuint fragmentShader = 0;
|
GLuint fragmentShader = 0;
|
||||||
@@ -9,7 +9,8 @@
|
|||||||
namespace
|
namespace
|
||||||
{
|
{
|
||||||
constexpr GLuint kGlobalParamsBindingPoint = 0;
|
constexpr GLuint kGlobalParamsBindingPoint = 0;
|
||||||
constexpr GLuint kSourceTextureUnit = 0;
|
constexpr GLuint kVideoInputTextureUnit = 0;
|
||||||
|
constexpr GLuint kLayerInputTextureUnit = 1;
|
||||||
|
|
||||||
const char* kVertexShaderSource = R"GLSL(
|
const char* kVertexShaderSource = R"GLSL(
|
||||||
#version 430 core
|
#version 430 core
|
||||||
@@ -36,41 +37,6 @@ RuntimeShaderRenderer::~RuntimeShaderRenderer()
|
|||||||
ShutdownGl();
|
ShutdownGl();
|
||||||
}
|
}
|
||||||
|
|
||||||
bool RuntimeShaderRenderer::CommitFragmentShader(const std::string& fragmentShaderSource, std::string& error)
|
|
||||||
{
|
|
||||||
RuntimeShaderArtifact artifact;
|
|
||||||
artifact.shaderId = "runtime-fragment";
|
|
||||||
artifact.displayName = "Runtime Fragment";
|
|
||||||
artifact.fragmentShaderSource = fragmentShaderSource;
|
|
||||||
return CommitShaderArtifact(artifact, error);
|
|
||||||
}
|
|
||||||
|
|
||||||
bool RuntimeShaderRenderer::CommitShaderArtifact(const RuntimeShaderArtifact& artifact, std::string& error)
|
|
||||||
{
|
|
||||||
if (artifact.fragmentShaderSource.empty())
|
|
||||||
{
|
|
||||||
error = "Cannot commit an empty fragment shader.";
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!EnsureStaticGlResources(error))
|
|
||||||
return false;
|
|
||||||
|
|
||||||
GLuint vertexShader = 0;
|
|
||||||
GLuint fragmentShader = 0;
|
|
||||||
GLuint program = 0;
|
|
||||||
if (!BuildProgram(artifact.fragmentShaderSource, program, vertexShader, fragmentShader, error))
|
|
||||||
return false;
|
|
||||||
|
|
||||||
DestroyProgram();
|
|
||||||
mProgram = program;
|
|
||||||
mVertexShader = vertexShader;
|
|
||||||
mFragmentShader = fragmentShader;
|
|
||||||
mArtifact = artifact;
|
|
||||||
AssignSamplerUniforms(mProgram);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool RuntimeShaderRenderer::CommitPreparedProgram(RuntimePreparedShaderProgram& preparedProgram, std::string& error)
|
bool RuntimeShaderRenderer::CommitPreparedProgram(RuntimePreparedShaderProgram& preparedProgram, std::string& error)
|
||||||
{
|
{
|
||||||
if (!preparedProgram.succeeded || preparedProgram.program == 0)
|
if (!preparedProgram.succeeded || preparedProgram.program == 0)
|
||||||
@@ -93,26 +59,53 @@ bool RuntimeShaderRenderer::CommitPreparedProgram(RuntimePreparedShaderProgram&
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void RuntimeShaderRenderer::UpdateArtifactState(const RuntimeShaderArtifact& artifact)
|
||||||
|
{
|
||||||
|
mArtifact.parameterDefinitions = artifact.parameterDefinitions;
|
||||||
|
mArtifact.parameterValues = artifact.parameterValues;
|
||||||
|
mArtifact.message = artifact.message;
|
||||||
|
}
|
||||||
|
|
||||||
bool RuntimeShaderRenderer::BuildPreparedProgram(
|
bool RuntimeShaderRenderer::BuildPreparedProgram(
|
||||||
const std::string& layerId,
|
const std::string& layerId,
|
||||||
const std::string& sourceFingerprint,
|
const std::string& sourceFingerprint,
|
||||||
const RuntimeShaderArtifact& artifact,
|
const RuntimeShaderArtifact& artifact,
|
||||||
RuntimePreparedShaderProgram& preparedProgram)
|
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 = RuntimePreparedShaderProgram();
|
||||||
preparedProgram.layerId = layerId;
|
preparedProgram.layerId = layerId;
|
||||||
preparedProgram.shaderId = artifact.shaderId;
|
preparedProgram.shaderId = artifact.shaderId;
|
||||||
|
preparedProgram.passId = passArtifact.passId;
|
||||||
preparedProgram.sourceFingerprint = sourceFingerprint;
|
preparedProgram.sourceFingerprint = sourceFingerprint;
|
||||||
preparedProgram.artifact = artifact;
|
preparedProgram.artifact = artifact;
|
||||||
|
preparedProgram.passArtifact = passArtifact;
|
||||||
|
preparedProgram.inputNames = passArtifact.inputNames;
|
||||||
|
preparedProgram.outputName = passArtifact.outputName.empty() ? passArtifact.passId : passArtifact.outputName;
|
||||||
|
|
||||||
if (artifact.fragmentShaderSource.empty())
|
if (passArtifact.fragmentShaderSource.empty())
|
||||||
{
|
{
|
||||||
preparedProgram.error = "Cannot prepare an empty fragment shader.";
|
preparedProgram.error = "Cannot prepare an empty fragment shader.";
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!BuildProgram(
|
if (!BuildProgram(
|
||||||
artifact.fragmentShaderSource,
|
passArtifact.fragmentShaderSource,
|
||||||
preparedProgram.program,
|
preparedProgram.program,
|
||||||
preparedProgram.vertexShader,
|
preparedProgram.vertexShader,
|
||||||
preparedProgram.fragmentShader,
|
preparedProgram.fragmentShader,
|
||||||
@@ -127,7 +120,7 @@ bool RuntimeShaderRenderer::BuildPreparedProgram(
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
void RuntimeShaderRenderer::RenderFrame(uint64_t frameIndex, unsigned width, unsigned height)
|
void RuntimeShaderRenderer::RenderFrame(uint64_t frameIndex, unsigned width, unsigned height, GLuint sourceTexture, GLuint layerInputTexture)
|
||||||
{
|
{
|
||||||
if (mProgram == 0)
|
if (mProgram == 0)
|
||||||
return;
|
return;
|
||||||
@@ -137,7 +130,7 @@ void RuntimeShaderRenderer::RenderFrame(uint64_t frameIndex, unsigned width, uns
|
|||||||
glDisable(GL_DEPTH_TEST);
|
glDisable(GL_DEPTH_TEST);
|
||||||
glDisable(GL_BLEND);
|
glDisable(GL_BLEND);
|
||||||
UpdateGlobalParams(frameIndex, width, height);
|
UpdateGlobalParams(frameIndex, width, height);
|
||||||
BindRuntimeTextures();
|
BindRuntimeTextures(sourceTexture, layerInputTexture);
|
||||||
glBindVertexArray(mVertexArray);
|
glBindVertexArray(mVertexArray);
|
||||||
glUseProgram(mProgram);
|
glUseProgram(mProgram);
|
||||||
glDrawArrays(GL_TRIANGLES, 0, 3);
|
glDrawArrays(GL_TRIANGLES, 0, 3);
|
||||||
@@ -236,16 +229,16 @@ void RuntimeShaderRenderer::AssignSamplerUniforms(GLuint program)
|
|||||||
glUseProgram(program);
|
glUseProgram(program);
|
||||||
const GLint videoInputLocation = glGetUniformLocation(program, "gVideoInput");
|
const GLint videoInputLocation = glGetUniformLocation(program, "gVideoInput");
|
||||||
if (videoInputLocation >= 0)
|
if (videoInputLocation >= 0)
|
||||||
glUniform1i(videoInputLocation, static_cast<GLint>(kSourceTextureUnit));
|
glUniform1i(videoInputLocation, static_cast<GLint>(kVideoInputTextureUnit));
|
||||||
const GLint videoInputArrayLocation = glGetUniformLocation(program, "gVideoInput_0");
|
const GLint videoInputArrayLocation = glGetUniformLocation(program, "gVideoInput_0");
|
||||||
if (videoInputArrayLocation >= 0)
|
if (videoInputArrayLocation >= 0)
|
||||||
glUniform1i(videoInputArrayLocation, static_cast<GLint>(kSourceTextureUnit));
|
glUniform1i(videoInputArrayLocation, static_cast<GLint>(kVideoInputTextureUnit));
|
||||||
const GLint layerInputLocation = glGetUniformLocation(program, "gLayerInput");
|
const GLint layerInputLocation = glGetUniformLocation(program, "gLayerInput");
|
||||||
if (layerInputLocation >= 0)
|
if (layerInputLocation >= 0)
|
||||||
glUniform1i(layerInputLocation, static_cast<GLint>(kSourceTextureUnit));
|
glUniform1i(layerInputLocation, static_cast<GLint>(kLayerInputTextureUnit));
|
||||||
const GLint layerInputArrayLocation = glGetUniformLocation(program, "gLayerInput_0");
|
const GLint layerInputArrayLocation = glGetUniformLocation(program, "gLayerInput_0");
|
||||||
if (layerInputArrayLocation >= 0)
|
if (layerInputArrayLocation >= 0)
|
||||||
glUniform1i(layerInputArrayLocation, static_cast<GLint>(kSourceTextureUnit));
|
glUniform1i(layerInputArrayLocation, static_cast<GLint>(kLayerInputTextureUnit));
|
||||||
glUseProgram(0);
|
glUseProgram(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -268,10 +261,14 @@ void RuntimeShaderRenderer::UpdateGlobalParams(uint64_t frameIndex, unsigned wid
|
|||||||
glBindBuffer(GL_UNIFORM_BUFFER, 0);
|
glBindBuffer(GL_UNIFORM_BUFFER, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
void RuntimeShaderRenderer::BindRuntimeTextures()
|
void RuntimeShaderRenderer::BindRuntimeTextures(GLuint sourceTexture, GLuint layerInputTexture)
|
||||||
{
|
{
|
||||||
glActiveTexture(GL_TEXTURE0 + kSourceTextureUnit);
|
const GLuint resolvedSourceTexture = sourceTexture != 0 ? sourceTexture : mFallbackSourceTexture;
|
||||||
glBindTexture(GL_TEXTURE_2D, 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);
|
glActiveTexture(GL_TEXTURE0);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
#include "GLExtensions.h"
|
#include "GLExtensions.h"
|
||||||
#include "RuntimeShaderProgram.h"
|
#include "RuntimeShaderProgram.h"
|
||||||
#include "../runtime/RuntimeShaderArtifact.h"
|
#include "../../runtime/RuntimeShaderArtifact.h"
|
||||||
|
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
#include <string>
|
#include <string>
|
||||||
@@ -16,11 +16,10 @@ public:
|
|||||||
RuntimeShaderRenderer& operator=(const RuntimeShaderRenderer&) = delete;
|
RuntimeShaderRenderer& operator=(const RuntimeShaderRenderer&) = delete;
|
||||||
~RuntimeShaderRenderer();
|
~RuntimeShaderRenderer();
|
||||||
|
|
||||||
bool CommitFragmentShader(const std::string& fragmentShaderSource, std::string& error);
|
|
||||||
bool CommitShaderArtifact(const RuntimeShaderArtifact& artifact, std::string& error);
|
|
||||||
bool CommitPreparedProgram(RuntimePreparedShaderProgram& preparedProgram, std::string& error);
|
bool CommitPreparedProgram(RuntimePreparedShaderProgram& preparedProgram, std::string& error);
|
||||||
bool HasProgram() const { return mProgram != 0; }
|
bool HasProgram() const { return mProgram != 0; }
|
||||||
void RenderFrame(uint64_t frameIndex, unsigned width, unsigned height);
|
void UpdateArtifactState(const RuntimeShaderArtifact& artifact);
|
||||||
|
void RenderFrame(uint64_t frameIndex, unsigned width, unsigned height, GLuint sourceTexture = 0, GLuint layerInputTexture = 0);
|
||||||
void ShutdownGl();
|
void ShutdownGl();
|
||||||
|
|
||||||
static bool BuildPreparedProgram(
|
static bool BuildPreparedProgram(
|
||||||
@@ -28,6 +27,12 @@ public:
|
|||||||
const std::string& sourceFingerprint,
|
const std::string& sourceFingerprint,
|
||||||
const RuntimeShaderArtifact& artifact,
|
const RuntimeShaderArtifact& artifact,
|
||||||
RuntimePreparedShaderProgram& preparedProgram);
|
RuntimePreparedShaderProgram& preparedProgram);
|
||||||
|
static bool BuildPreparedPassProgram(
|
||||||
|
const std::string& layerId,
|
||||||
|
const std::string& sourceFingerprint,
|
||||||
|
const RuntimeShaderArtifact& artifact,
|
||||||
|
const RuntimeShaderPassArtifact& passArtifact,
|
||||||
|
RuntimePreparedShaderProgram& preparedProgram);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
bool EnsureStaticGlResources(std::string& error);
|
bool EnsureStaticGlResources(std::string& error);
|
||||||
@@ -35,7 +40,7 @@ private:
|
|||||||
static bool BuildProgram(const std::string& fragmentShaderSource, GLuint& program, GLuint& vertexShader, GLuint& fragmentShader, std::string& error);
|
static bool BuildProgram(const std::string& fragmentShaderSource, GLuint& program, GLuint& vertexShader, GLuint& fragmentShader, std::string& error);
|
||||||
static void AssignSamplerUniforms(GLuint program);
|
static void AssignSamplerUniforms(GLuint program);
|
||||||
void UpdateGlobalParams(uint64_t frameIndex, unsigned width, unsigned height);
|
void UpdateGlobalParams(uint64_t frameIndex, unsigned width, unsigned height);
|
||||||
void BindRuntimeTextures();
|
void BindRuntimeTextures(GLuint sourceTexture, GLuint layerInputTexture);
|
||||||
void DestroyProgram();
|
void DestroyProgram();
|
||||||
void DestroyStaticGlResources();
|
void DestroyStaticGlResources();
|
||||||
|
|
||||||
@@ -1,5 +1,9 @@
|
|||||||
#include "RuntimeLayerModel.h"
|
#include "RuntimeLayerModel.h"
|
||||||
|
|
||||||
|
#include "RuntimeParameterUtils.h"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <chrono>
|
||||||
#include <utility>
|
#include <utility>
|
||||||
|
|
||||||
namespace RenderCadenceCompositor
|
namespace RenderCadenceCompositor
|
||||||
@@ -26,6 +30,7 @@ bool RuntimeLayerModel::InitializeSingleLayer(const SupportedShaderCatalog& shad
|
|||||||
layer.shaderName = shaderPackage->displayName.empty() ? shaderPackage->id : shaderPackage->displayName;
|
layer.shaderName = shaderPackage->displayName.empty() ? shaderPackage->id : shaderPackage->displayName;
|
||||||
layer.buildState = RuntimeLayerBuildState::Pending;
|
layer.buildState = RuntimeLayerBuildState::Pending;
|
||||||
layer.message = "Runtime Slang build is waiting to start.";
|
layer.message = "Runtime Slang build is waiting to start.";
|
||||||
|
InitializeDefaultParameterValues(layer, *shaderPackage);
|
||||||
mLayers.push_back(std::move(layer));
|
mLayers.push_back(std::move(layer));
|
||||||
error.clear();
|
error.clear();
|
||||||
return true;
|
return true;
|
||||||
@@ -46,6 +51,7 @@ bool RuntimeLayerModel::AddLayer(const SupportedShaderCatalog& shaderCatalog, co
|
|||||||
layer.shaderName = shaderPackage->displayName.empty() ? shaderPackage->id : shaderPackage->displayName;
|
layer.shaderName = shaderPackage->displayName.empty() ? shaderPackage->id : shaderPackage->displayName;
|
||||||
layer.buildState = RuntimeLayerBuildState::Pending;
|
layer.buildState = RuntimeLayerBuildState::Pending;
|
||||||
layer.message = "Runtime Slang build is waiting to start.";
|
layer.message = "Runtime Slang build is waiting to start.";
|
||||||
|
InitializeDefaultParameterValues(layer, *shaderPackage);
|
||||||
layerId = layer.id;
|
layerId = layer.id;
|
||||||
mLayers.push_back(std::move(layer));
|
mLayers.push_back(std::move(layer));
|
||||||
error.clear();
|
error.clear();
|
||||||
@@ -68,6 +74,127 @@ bool RuntimeLayerModel::RemoveLayer(const std::string& layerId, std::string& err
|
|||||||
return false;
|
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()
|
void RuntimeLayerModel::Clear()
|
||||||
{
|
{
|
||||||
mLayers.clear();
|
mLayers.clear();
|
||||||
@@ -106,6 +233,7 @@ bool RuntimeLayerModel::MarkBuildReady(const RuntimeShaderArtifact& artifact, st
|
|||||||
layer->message = artifact.message;
|
layer->message = artifact.message;
|
||||||
layer->renderReady = true;
|
layer->renderReady = true;
|
||||||
layer->artifact = artifact;
|
layer->artifact = artifact;
|
||||||
|
layer->artifact.parameterValues = layer->parameterValues;
|
||||||
error.clear();
|
error.clear();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -159,7 +287,9 @@ RuntimeLayerModelSnapshot RuntimeLayerModel::Snapshot() const
|
|||||||
RuntimeRenderLayerModel renderLayer;
|
RuntimeRenderLayerModel renderLayer;
|
||||||
renderLayer.id = layer.id;
|
renderLayer.id = layer.id;
|
||||||
renderLayer.shaderId = layer.shaderId;
|
renderLayer.shaderId = layer.shaderId;
|
||||||
|
renderLayer.bypass = layer.bypass;
|
||||||
renderLayer.artifact = layer.artifact;
|
renderLayer.artifact = layer.artifact;
|
||||||
|
renderLayer.artifact.parameterValues = layer.parameterValues;
|
||||||
snapshot.renderLayers.push_back(std::move(renderLayer));
|
snapshot.renderLayers.push_back(std::move(renderLayer));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -204,6 +334,24 @@ RuntimeLayerModel::Layer* RuntimeLayerModel::FindFirstLayerForShader(const std::
|
|||||||
return nullptr;
|
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()
|
std::string RuntimeLayerModel::AllocateLayerId()
|
||||||
{
|
{
|
||||||
return "runtime-layer-" + std::to_string(mNextLayerNumber++);
|
return "runtime-layer-" + std::to_string(mNextLayerNumber++);
|
||||||
@@ -219,6 +367,13 @@ RuntimeLayerReadModel RuntimeLayerModel::ToReadModel(const Layer& layer)
|
|||||||
readModel.buildState = layer.buildState;
|
readModel.buildState = layer.buildState;
|
||||||
readModel.message = layer.message;
|
readModel.message = layer.message;
|
||||||
readModel.renderReady = layer.renderReady;
|
readModel.renderReady = layer.renderReady;
|
||||||
|
readModel.parameterDefinitions = layer.parameterDefinitions;
|
||||||
|
readModel.parameterValues = layer.parameterValues;
|
||||||
return readModel;
|
return readModel;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
double RuntimeLayerModel::RuntimeElapsedSeconds() const
|
||||||
|
{
|
||||||
|
return std::chrono::duration_cast<std::chrono::duration<double>>(std::chrono::steady_clock::now() - mStartTime).count();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,12 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
#include "RuntimeJson.h"
|
||||||
#include "RuntimeShaderArtifact.h"
|
#include "RuntimeShaderArtifact.h"
|
||||||
#include "SupportedShaderCatalog.h"
|
#include "SupportedShaderCatalog.h"
|
||||||
|
|
||||||
|
#include <chrono>
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
|
#include <map>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
@@ -25,12 +28,15 @@ struct RuntimeLayerReadModel
|
|||||||
RuntimeLayerBuildState buildState = RuntimeLayerBuildState::Pending;
|
RuntimeLayerBuildState buildState = RuntimeLayerBuildState::Pending;
|
||||||
std::string message;
|
std::string message;
|
||||||
bool renderReady = false;
|
bool renderReady = false;
|
||||||
|
std::vector<ShaderParameterDefinition> parameterDefinitions;
|
||||||
|
std::map<std::string, ShaderParameterValue> parameterValues;
|
||||||
};
|
};
|
||||||
|
|
||||||
struct RuntimeRenderLayerModel
|
struct RuntimeRenderLayerModel
|
||||||
{
|
{
|
||||||
std::string id;
|
std::string id;
|
||||||
std::string shaderId;
|
std::string shaderId;
|
||||||
|
bool bypass = false;
|
||||||
RuntimeShaderArtifact artifact;
|
RuntimeShaderArtifact artifact;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -50,6 +56,11 @@ public:
|
|||||||
|
|
||||||
bool AddLayer(const SupportedShaderCatalog& shaderCatalog, const std::string& shaderId, std::string& layerId, std::string& error);
|
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 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 MarkBuildStarted(const std::string& layerId, const std::string& message, std::string& error);
|
||||||
bool MarkBuildReady(const RuntimeShaderArtifact& artifact, std::string& error);
|
bool MarkBuildReady(const RuntimeShaderArtifact& artifact, std::string& error);
|
||||||
bool MarkBuildFailedForShader(const std::string& shaderId, const std::string& message);
|
bool MarkBuildFailedForShader(const std::string& shaderId, const std::string& message);
|
||||||
@@ -69,16 +80,22 @@ private:
|
|||||||
RuntimeLayerBuildState buildState = RuntimeLayerBuildState::Pending;
|
RuntimeLayerBuildState buildState = RuntimeLayerBuildState::Pending;
|
||||||
std::string message;
|
std::string message;
|
||||||
bool renderReady = false;
|
bool renderReady = false;
|
||||||
|
std::vector<ShaderParameterDefinition> parameterDefinitions;
|
||||||
|
std::map<std::string, ShaderParameterValue> parameterValues;
|
||||||
RuntimeShaderArtifact artifact;
|
RuntimeShaderArtifact artifact;
|
||||||
};
|
};
|
||||||
|
|
||||||
Layer* FindLayer(const std::string& layerId);
|
Layer* FindLayer(const std::string& layerId);
|
||||||
const Layer* FindLayer(const std::string& layerId) const;
|
const Layer* FindLayer(const std::string& layerId) const;
|
||||||
Layer* FindFirstLayerForShader(const std::string& shaderId);
|
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();
|
std::string AllocateLayerId();
|
||||||
static RuntimeLayerReadModel ToReadModel(const Layer& layer);
|
static RuntimeLayerReadModel ToReadModel(const Layer& layer);
|
||||||
|
double RuntimeElapsedSeconds() const;
|
||||||
|
|
||||||
std::vector<Layer> mLayers;
|
std::vector<Layer> mLayers;
|
||||||
uint64_t mNextLayerNumber = 1;
|
uint64_t mNextLayerNumber = 1;
|
||||||
|
std::chrono::steady_clock::time_point mStartTime = std::chrono::steady_clock::now();
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,15 +2,26 @@
|
|||||||
|
|
||||||
#include "ShaderTypes.h"
|
#include "ShaderTypes.h"
|
||||||
|
|
||||||
|
#include <map>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
|
struct RuntimeShaderPassArtifact
|
||||||
|
{
|
||||||
|
std::string passId;
|
||||||
|
std::string fragmentShaderSource;
|
||||||
|
std::vector<std::string> inputNames;
|
||||||
|
std::string outputName;
|
||||||
|
};
|
||||||
|
|
||||||
struct RuntimeShaderArtifact
|
struct RuntimeShaderArtifact
|
||||||
{
|
{
|
||||||
std::string layerId;
|
std::string layerId;
|
||||||
std::string shaderId;
|
std::string shaderId;
|
||||||
std::string displayName;
|
std::string displayName;
|
||||||
std::string fragmentShaderSource;
|
std::string fragmentShaderSource;
|
||||||
|
std::vector<RuntimeShaderPassArtifact> passes;
|
||||||
std::string message;
|
std::string message;
|
||||||
std::vector<ShaderParameterDefinition> parameterDefinitions;
|
std::vector<ShaderParameterDefinition> parameterDefinitions;
|
||||||
|
std::map<std::string, ShaderParameterValue> parameterValues;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -112,8 +112,6 @@ RuntimeSlangShaderBuild RuntimeSlangShaderCompiler::BuildShader(const std::strin
|
|||||||
return build;
|
return build;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ShaderPassDefinition& pass = shaderPackage.passes.front();
|
|
||||||
|
|
||||||
ShaderCompiler compiler(
|
ShaderCompiler compiler(
|
||||||
repoRoot,
|
repoRoot,
|
||||||
runtimeBuildDir / (shaderId + ".wrapper.slang"),
|
runtimeBuildDir / (shaderId + ".wrapper.slang"),
|
||||||
@@ -122,11 +120,22 @@ RuntimeSlangShaderBuild RuntimeSlangShaderCompiler::BuildShader(const std::strin
|
|||||||
0);
|
0);
|
||||||
|
|
||||||
const auto start = std::chrono::steady_clock::now();
|
const auto start = std::chrono::steady_clock::now();
|
||||||
if (!compiler.BuildPassFragmentShaderSource(shaderPackage, pass, build.artifact.fragmentShaderSource, error))
|
for (const ShaderPassDefinition& pass : shaderPackage.passes)
|
||||||
{
|
{
|
||||||
build.succeeded = false;
|
std::string fragmentShaderSource;
|
||||||
build.message = error.empty() ? "Slang compile failed." : error;
|
if (!compiler.BuildPassFragmentShaderSource(shaderPackage, pass, fragmentShaderSource, error))
|
||||||
return build;
|
{
|
||||||
|
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 auto end = std::chrono::steady_clock::now();
|
||||||
@@ -135,6 +144,8 @@ RuntimeSlangShaderBuild RuntimeSlangShaderCompiler::BuildShader(const std::strin
|
|||||||
build.artifact.shaderId = shaderPackage.id;
|
build.artifact.shaderId = shaderPackage.id;
|
||||||
build.artifact.displayName = shaderPackage.displayName;
|
build.artifact.displayName = shaderPackage.displayName;
|
||||||
build.artifact.parameterDefinitions = shaderPackage.parameters;
|
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.artifact.message = shaderPackage.displayName + " Slang compile completed in " + std::to_string(milliseconds) + " ms.";
|
||||||
build.message = build.artifact.message;
|
build.message = build.artifact.message;
|
||||||
return build;
|
return build;
|
||||||
|
|||||||
@@ -9,8 +9,8 @@ namespace RenderCadenceCompositor
|
|||||||
{
|
{
|
||||||
ShaderSupportResult CheckStatelessSinglePassShaderSupport(const ShaderPackage& shaderPackage)
|
ShaderSupportResult CheckStatelessSinglePassShaderSupport(const ShaderPackage& shaderPackage)
|
||||||
{
|
{
|
||||||
if (shaderPackage.passes.size() != 1)
|
if (shaderPackage.passes.empty())
|
||||||
return { false, "RenderCadenceCompositor currently supports only single-pass runtime shaders." };
|
return { false, "Shader package has no render passes." };
|
||||||
|
|
||||||
if (shaderPackage.temporal.enabled)
|
if (shaderPackage.temporal.enabled)
|
||||||
return { false, "RenderCadenceCompositor currently supports only stateless shaders; temporal history is not enabled in this app." };
|
return { false, "RenderCadenceCompositor currently supports only stateless shaders; temporal history is not enabled in this app." };
|
||||||
@@ -30,6 +30,35 @@ ShaderSupportResult CheckStatelessSinglePassShaderSupport(const ShaderPackage& s
|
|||||||
return { false, "RenderCadenceCompositor currently skips text parameters because they require per-shader text texture storage." };
|
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() };
|
return { true, std::string() };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
#include <chrono>
|
#include <chrono>
|
||||||
#include <cstddef>
|
#include <cstddef>
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
namespace RenderCadenceCompositor
|
namespace RenderCadenceCompositor
|
||||||
{
|
{
|
||||||
@@ -18,14 +19,46 @@ struct CadenceTelemetrySnapshot
|
|||||||
uint64_t scheduledTotal = 0;
|
uint64_t scheduledTotal = 0;
|
||||||
uint64_t completedPollMisses = 0;
|
uint64_t completedPollMisses = 0;
|
||||||
uint64_t scheduleFailures = 0;
|
uint64_t scheduleFailures = 0;
|
||||||
|
uint64_t completedDrops = 0;
|
||||||
|
uint64_t acquireMisses = 0;
|
||||||
uint64_t completions = 0;
|
uint64_t completions = 0;
|
||||||
uint64_t displayedLate = 0;
|
uint64_t displayedLate = 0;
|
||||||
uint64_t dropped = 0;
|
uint64_t dropped = 0;
|
||||||
|
uint64_t clockOverruns = 0;
|
||||||
|
uint64_t clockSkippedFrames = 0;
|
||||||
uint64_t shaderBuildsCommitted = 0;
|
uint64_t shaderBuildsCommitted = 0;
|
||||||
uint64_t shaderBuildFailures = 0;
|
uint64_t shaderBuildFailures = 0;
|
||||||
|
double renderFrameMilliseconds = 0.0;
|
||||||
|
double renderFrameBudgetUsedPercent = 0.0;
|
||||||
|
double renderFrameMaxMilliseconds = 0.0;
|
||||||
|
double readbackQueueMilliseconds = 0.0;
|
||||||
|
double completedReadbackCopyMilliseconds = 0.0;
|
||||||
|
uint64_t inputFramesReceived = 0;
|
||||||
|
uint64_t inputFramesDropped = 0;
|
||||||
|
uint64_t inputConsumeMisses = 0;
|
||||||
|
uint64_t inputUploadMisses = 0;
|
||||||
|
std::size_t inputReadyFrames = 0;
|
||||||
|
std::size_t inputReadingFrames = 0;
|
||||||
|
double inputLatestAgeMilliseconds = 0.0;
|
||||||
|
double inputUploadMilliseconds = 0.0;
|
||||||
|
bool inputFormatSupported = true;
|
||||||
|
bool inputSignalPresent = false;
|
||||||
|
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;
|
bool deckLinkBufferedAvailable = false;
|
||||||
uint64_t deckLinkBuffered = 0;
|
uint64_t deckLinkBuffered = 0;
|
||||||
double deckLinkScheduleCallMilliseconds = 0.0;
|
double deckLinkScheduleCallMilliseconds = 0.0;
|
||||||
|
bool deckLinkScheduleLeadAvailable = false;
|
||||||
|
int64_t deckLinkPlaybackStreamTime = 0;
|
||||||
|
uint64_t deckLinkPlaybackFrameIndex = 0;
|
||||||
|
uint64_t deckLinkNextScheduleFrameIndex = 0;
|
||||||
|
int64_t deckLinkScheduleLeadFrames = 0;
|
||||||
|
uint64_t deckLinkScheduleRealignments = 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
class CadenceTelemetry
|
class CadenceTelemetry
|
||||||
@@ -57,12 +90,20 @@ public:
|
|||||||
snapshot.scheduleFailures = outputMetrics.scheduleFailures > threadMetrics.scheduleFailures
|
snapshot.scheduleFailures = outputMetrics.scheduleFailures > threadMetrics.scheduleFailures
|
||||||
? outputMetrics.scheduleFailures
|
? outputMetrics.scheduleFailures
|
||||||
: threadMetrics.scheduleFailures;
|
: threadMetrics.scheduleFailures;
|
||||||
|
snapshot.completedDrops = exchangeMetrics.completedDrops;
|
||||||
|
snapshot.acquireMisses = exchangeMetrics.acquireMisses;
|
||||||
snapshot.completions = outputMetrics.completions;
|
snapshot.completions = outputMetrics.completions;
|
||||||
snapshot.displayedLate = outputMetrics.displayedLate;
|
snapshot.displayedLate = outputMetrics.displayedLate;
|
||||||
snapshot.dropped = outputMetrics.dropped;
|
snapshot.dropped = outputMetrics.dropped;
|
||||||
snapshot.deckLinkBufferedAvailable = outputMetrics.actualBufferedFramesAvailable;
|
snapshot.deckLinkBufferedAvailable = outputMetrics.actualBufferedFramesAvailable;
|
||||||
snapshot.deckLinkBuffered = outputMetrics.actualBufferedFrames;
|
snapshot.deckLinkBuffered = outputMetrics.actualBufferedFrames;
|
||||||
snapshot.deckLinkScheduleCallMilliseconds = outputMetrics.scheduleCallMilliseconds;
|
snapshot.deckLinkScheduleCallMilliseconds = outputMetrics.scheduleCallMilliseconds;
|
||||||
|
snapshot.deckLinkScheduleLeadAvailable = outputMetrics.scheduleLeadAvailable;
|
||||||
|
snapshot.deckLinkPlaybackStreamTime = outputMetrics.playbackStreamTime;
|
||||||
|
snapshot.deckLinkPlaybackFrameIndex = outputMetrics.playbackFrameIndex;
|
||||||
|
snapshot.deckLinkNextScheduleFrameIndex = outputMetrics.nextScheduleFrameIndex;
|
||||||
|
snapshot.deckLinkScheduleLeadFrames = outputMetrics.scheduleLeadFrames;
|
||||||
|
snapshot.deckLinkScheduleRealignments = outputMetrics.scheduleRealignmentCount;
|
||||||
|
|
||||||
if (mHasLastSample && seconds > 0.0)
|
if (mHasLastSample && seconds > 0.0)
|
||||||
{
|
{
|
||||||
@@ -86,8 +127,47 @@ public:
|
|||||||
{
|
{
|
||||||
CadenceTelemetrySnapshot snapshot = Sample(exchange, output, outputThread);
|
CadenceTelemetrySnapshot snapshot = Sample(exchange, output, outputThread);
|
||||||
const auto renderMetrics = renderThread.GetMetrics();
|
const auto renderMetrics = renderThread.GetMetrics();
|
||||||
|
snapshot.clockOverruns = renderMetrics.clockOverruns;
|
||||||
|
snapshot.clockSkippedFrames = renderMetrics.skippedFrames;
|
||||||
snapshot.shaderBuildsCommitted = renderMetrics.shaderBuildsCommitted;
|
snapshot.shaderBuildsCommitted = renderMetrics.shaderBuildsCommitted;
|
||||||
snapshot.shaderBuildFailures = renderMetrics.shaderBuildFailures;
|
snapshot.shaderBuildFailures = renderMetrics.shaderBuildFailures;
|
||||||
|
snapshot.renderFrameMilliseconds = renderMetrics.renderFrameMilliseconds;
|
||||||
|
snapshot.renderFrameBudgetUsedPercent = renderMetrics.renderFrameBudgetUsedPercent;
|
||||||
|
snapshot.renderFrameMaxMilliseconds = renderMetrics.renderFrameMaxMilliseconds;
|
||||||
|
snapshot.readbackQueueMilliseconds = renderMetrics.readbackQueueMilliseconds;
|
||||||
|
snapshot.completedReadbackCopyMilliseconds = renderMetrics.completedReadbackCopyMilliseconds;
|
||||||
|
snapshot.inputFramesReceived = renderMetrics.inputFramesReceived;
|
||||||
|
snapshot.inputFramesDropped = renderMetrics.inputFramesDropped;
|
||||||
|
snapshot.inputConsumeMisses = renderMetrics.inputConsumeMisses;
|
||||||
|
snapshot.inputUploadMisses = renderMetrics.inputUploadMisses;
|
||||||
|
snapshot.inputReadyFrames = renderMetrics.inputReadyFrames;
|
||||||
|
snapshot.inputReadingFrames = renderMetrics.inputReadingFrames;
|
||||||
|
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;
|
return snapshot;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -97,6 +177,7 @@ private:
|
|||||||
Clock::time_point mLastSampleTime = Clock::now();
|
Clock::time_point mLastSampleTime = Clock::now();
|
||||||
uint64_t mLastRenderedFrames = 0;
|
uint64_t mLastRenderedFrames = 0;
|
||||||
uint64_t mLastScheduledFrames = 0;
|
uint64_t mLastScheduledFrames = 0;
|
||||||
|
uint64_t mLastInputCapturedFrames = 0;
|
||||||
bool mHasLastSample = false;
|
bool mHasLastSample = false;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,11 +21,39 @@ inline void WriteCadenceTelemetryJson(JsonWriter& writer, const CadenceTelemetry
|
|||||||
writer.KeyUInt("scheduledTotal", snapshot.scheduledTotal);
|
writer.KeyUInt("scheduledTotal", snapshot.scheduledTotal);
|
||||||
writer.KeyUInt("completedPollMisses", snapshot.completedPollMisses);
|
writer.KeyUInt("completedPollMisses", snapshot.completedPollMisses);
|
||||||
writer.KeyUInt("scheduleFailures", snapshot.scheduleFailures);
|
writer.KeyUInt("scheduleFailures", snapshot.scheduleFailures);
|
||||||
|
writer.KeyUInt("completedDrops", snapshot.completedDrops);
|
||||||
|
writer.KeyUInt("acquireMisses", snapshot.acquireMisses);
|
||||||
writer.KeyUInt("completions", snapshot.completions);
|
writer.KeyUInt("completions", snapshot.completions);
|
||||||
writer.KeyUInt("late", snapshot.displayedLate);
|
writer.KeyUInt("late", snapshot.displayedLate);
|
||||||
writer.KeyUInt("dropped", snapshot.dropped);
|
writer.KeyUInt("dropped", snapshot.dropped);
|
||||||
|
writer.KeyUInt("clockOverruns", snapshot.clockOverruns);
|
||||||
|
writer.KeyUInt("clockSkippedFrames", snapshot.clockSkippedFrames);
|
||||||
|
writer.KeyUInt("clockOveruns", snapshot.clockOverruns);
|
||||||
|
writer.KeyUInt("clockSkipped", snapshot.clockSkippedFrames);
|
||||||
writer.KeyUInt("shaderCommitted", snapshot.shaderBuildsCommitted);
|
writer.KeyUInt("shaderCommitted", snapshot.shaderBuildsCommitted);
|
||||||
writer.KeyUInt("shaderFailures", snapshot.shaderBuildFailures);
|
writer.KeyUInt("shaderFailures", snapshot.shaderBuildFailures);
|
||||||
|
writer.KeyDouble("renderFrameMs", snapshot.renderFrameMilliseconds);
|
||||||
|
writer.KeyDouble("renderFrameBudgetUsedPercent", snapshot.renderFrameBudgetUsedPercent);
|
||||||
|
writer.KeyDouble("renderFrameMaxMs", snapshot.renderFrameMaxMilliseconds);
|
||||||
|
writer.KeyDouble("readbackQueueMs", snapshot.readbackQueueMilliseconds);
|
||||||
|
writer.KeyDouble("completedReadbackCopyMs", snapshot.completedReadbackCopyMilliseconds);
|
||||||
|
writer.KeyUInt("inputFramesReceived", snapshot.inputFramesReceived);
|
||||||
|
writer.KeyUInt("inputFramesDropped", snapshot.inputFramesDropped);
|
||||||
|
writer.KeyUInt("inputConsumeMisses", snapshot.inputConsumeMisses);
|
||||||
|
writer.KeyUInt("inputUploadMisses", snapshot.inputUploadMisses);
|
||||||
|
writer.KeyUInt("inputReadyFrames", static_cast<uint64_t>(snapshot.inputReadyFrames));
|
||||||
|
writer.KeyUInt("inputReadingFrames", static_cast<uint64_t>(snapshot.inputReadingFrames));
|
||||||
|
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.KeyBool("deckLinkBufferedAvailable", snapshot.deckLinkBufferedAvailable);
|
||||||
writer.Key("deckLinkBuffered");
|
writer.Key("deckLinkBuffered");
|
||||||
if (snapshot.deckLinkBufferedAvailable)
|
if (snapshot.deckLinkBufferedAvailable)
|
||||||
@@ -33,6 +61,16 @@ inline void WriteCadenceTelemetryJson(JsonWriter& writer, const CadenceTelemetry
|
|||||||
else
|
else
|
||||||
writer.Null();
|
writer.Null();
|
||||||
writer.KeyDouble("scheduleCallMs", snapshot.deckLinkScheduleCallMilliseconds);
|
writer.KeyDouble("scheduleCallMs", snapshot.deckLinkScheduleCallMilliseconds);
|
||||||
|
writer.KeyBool("deckLinkScheduleLeadAvailable", snapshot.deckLinkScheduleLeadAvailable);
|
||||||
|
writer.Key("deckLinkScheduleLeadFrames");
|
||||||
|
if (snapshot.deckLinkScheduleLeadAvailable)
|
||||||
|
writer.Int(snapshot.deckLinkScheduleLeadFrames);
|
||||||
|
else
|
||||||
|
writer.Null();
|
||||||
|
writer.KeyUInt("deckLinkPlaybackFrameIndex", snapshot.deckLinkPlaybackFrameIndex);
|
||||||
|
writer.KeyUInt("deckLinkNextScheduleFrameIndex", snapshot.deckLinkNextScheduleFrameIndex);
|
||||||
|
writer.KeyInt("deckLinkPlaybackStreamTime", snapshot.deckLinkPlaybackStreamTime);
|
||||||
|
writer.KeyUInt("deckLinkScheduleRealignments", snapshot.deckLinkScheduleRealignments);
|
||||||
writer.EndObject();
|
writer.EndObject();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -78,7 +78,13 @@ private:
|
|||||||
message << "DeckLink reported frame timing issue: lateDelta=" << lateDelta
|
message << "DeckLink reported frame timing issue: lateDelta=" << lateDelta
|
||||||
<< " droppedDelta=" << droppedDelta
|
<< " droppedDelta=" << droppedDelta
|
||||||
<< " totalLate=" << snapshot.displayedLate
|
<< " totalLate=" << snapshot.displayedLate
|
||||||
<< " totalDropped=" << snapshot.dropped;
|
<< " totalDropped=" << snapshot.dropped
|
||||||
|
<< " scheduleLead=";
|
||||||
|
if (snapshot.deckLinkScheduleLeadAvailable)
|
||||||
|
message << snapshot.deckLinkScheduleLeadFrames;
|
||||||
|
else
|
||||||
|
message << "n/a";
|
||||||
|
message << " realignments=" << snapshot.deckLinkScheduleRealignments;
|
||||||
LogWarning("telemetry", message.str());
|
LogWarning("telemetry", message.str());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
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;
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -15,6 +15,7 @@ bool DeckLinkOutput::Initialize(const DeckLinkOutputConfig& config, CompletionCa
|
|||||||
mCompletionCallback = completionCallback;
|
mCompletionCallback = completionCallback;
|
||||||
|
|
||||||
VideoFormatSelection formats;
|
VideoFormatSelection formats;
|
||||||
|
formats.output = config.outputVideoMode;
|
||||||
if (!mSession.DiscoverDevicesAndModes(formats, error))
|
if (!mSession.DiscoverDevicesAndModes(formats, error))
|
||||||
return false;
|
return false;
|
||||||
if (!mSession.SelectPreferredFormats(formats, config.outputAlphaRequired, error))
|
if (!mSession.SelectPreferredFormats(formats, config.outputAlphaRequired, error))
|
||||||
@@ -76,6 +77,12 @@ DeckLinkOutputMetrics DeckLinkOutput::Metrics() const
|
|||||||
metrics.actualBufferedFramesAvailable = state.actualDeckLinkBufferedFramesAvailable;
|
metrics.actualBufferedFramesAvailable = state.actualDeckLinkBufferedFramesAvailable;
|
||||||
metrics.actualBufferedFrames = state.actualDeckLinkBufferedFrames;
|
metrics.actualBufferedFrames = state.actualDeckLinkBufferedFrames;
|
||||||
metrics.scheduleCallMilliseconds = state.deckLinkScheduleCallMilliseconds;
|
metrics.scheduleCallMilliseconds = state.deckLinkScheduleCallMilliseconds;
|
||||||
|
metrics.scheduleLeadAvailable = state.deckLinkScheduleLeadAvailable;
|
||||||
|
metrics.playbackStreamTime = state.deckLinkPlaybackStreamTime;
|
||||||
|
metrics.playbackFrameIndex = state.deckLinkPlaybackFrameIndex;
|
||||||
|
metrics.nextScheduleFrameIndex = state.deckLinkNextScheduleFrameIndex;
|
||||||
|
metrics.scheduleLeadFrames = state.deckLinkScheduleLeadFrames;
|
||||||
|
metrics.scheduleRealignmentCount = state.deckLinkScheduleRealignmentCount;
|
||||||
return metrics;
|
return metrics;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
#include "DeckLinkDisplayMode.h"
|
||||||
#include "DeckLinkSession.h"
|
#include "DeckLinkSession.h"
|
||||||
#include "VideoIOTypes.h"
|
#include "VideoIOTypes.h"
|
||||||
|
|
||||||
@@ -12,6 +13,7 @@ namespace RenderCadenceCompositor
|
|||||||
{
|
{
|
||||||
struct DeckLinkOutputConfig
|
struct DeckLinkOutputConfig
|
||||||
{
|
{
|
||||||
|
VideoFormat outputVideoMode;
|
||||||
bool externalKeyingEnabled = false;
|
bool externalKeyingEnabled = false;
|
||||||
bool outputAlphaRequired = false;
|
bool outputAlphaRequired = false;
|
||||||
};
|
};
|
||||||
@@ -26,6 +28,12 @@ struct DeckLinkOutputMetrics
|
|||||||
bool actualBufferedFramesAvailable = false;
|
bool actualBufferedFramesAvailable = false;
|
||||||
uint64_t actualBufferedFrames = 0;
|
uint64_t actualBufferedFrames = 0;
|
||||||
double scheduleCallMilliseconds = 0.0;
|
double scheduleCallMilliseconds = 0.0;
|
||||||
|
bool scheduleLeadAvailable = false;
|
||||||
|
int64_t playbackStreamTime = 0;
|
||||||
|
uint64_t playbackFrameIndex = 0;
|
||||||
|
uint64_t nextScheduleFrameIndex = 0;
|
||||||
|
int64_t scheduleLeadFrames = 0;
|
||||||
|
uint64_t scheduleRealignmentCount = 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
class DeckLinkOutput
|
class DeckLinkOutput
|
||||||
|
|||||||
@@ -77,12 +77,15 @@ private:
|
|||||||
while (!mStopping)
|
while (!mStopping)
|
||||||
{
|
{
|
||||||
const auto exchangeMetrics = mExchange.Metrics();
|
const auto exchangeMetrics = mExchange.Metrics();
|
||||||
if (exchangeMetrics.scheduledCount >= mConfig.targetBufferedFrames)
|
const auto outputMetrics = mOutput.Metrics();
|
||||||
|
const std::size_t bufferedFrames = outputMetrics.actualBufferedFramesAvailable
|
||||||
|
? static_cast<std::size_t>(outputMetrics.actualBufferedFrames)
|
||||||
|
: exchangeMetrics.scheduledCount;
|
||||||
|
if (bufferedFrames >= mConfig.targetBufferedFrames)
|
||||||
{
|
{
|
||||||
std::this_thread::sleep_for(mConfig.idleSleep);
|
std::this_thread::sleep_for(mConfig.idleSleep);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
SystemFrame frame;
|
SystemFrame frame;
|
||||||
if (!mExchange.ConsumeCompletedForSchedule(frame))
|
if (!mExchange.ConsumeCompletedForSchedule(frame))
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -10,7 +10,10 @@ The active plan for tightening render-thread ownership is:
|
|||||||
|
|
||||||
The plan for building a fresh modular app around the proven probe architecture is:
|
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)
|
- [RenderCadenceCompositor README](../apps/RenderCadenceCompositor/README.md)
|
||||||
|
- [Render Cadence Golden Rules](RENDER_CADENCE_GOLDEN_RULES.md)
|
||||||
|
|
||||||
|
`NEW_RENDER_CADENCE_APP_PLAN.md` remains as historical planning context, but the README and golden rules are the current contract for the new cadence-first app.
|
||||||
|
|
||||||
## Application Shape
|
## Application Shape
|
||||||
|
|
||||||
@@ -287,7 +290,7 @@ Slots have four states:
|
|||||||
- `Completed`
|
- `Completed`
|
||||||
- `Scheduled`
|
- `Scheduled`
|
||||||
|
|
||||||
Completed-but-unscheduled frames are treated as a latest-N cache. If render cadence needs space and old completed frames have not been scheduled, the oldest unscheduled completed frame can be recycled.
|
In the current legacy app, completed-but-unscheduled frames are treated as a latest-N cache. The newer `RenderCadenceCompositor` uses a bounded FIFO completed reserve instead; see its README for the cadence-first contract.
|
||||||
|
|
||||||
Scheduled frames are protected until DeckLink reports completion.
|
Scheduled frames are protected until DeckLink reports completion.
|
||||||
|
|
||||||
@@ -295,7 +298,7 @@ Scheduled frames are protected until DeckLink reports completion.
|
|||||||
|
|
||||||
`RenderOutputQueue` holds completed unscheduled output frames waiting to be scheduled.
|
`RenderOutputQueue` holds completed unscheduled output frames waiting to be scheduled.
|
||||||
|
|
||||||
It is bounded and latest-N:
|
In the legacy app it is bounded and latest-N:
|
||||||
|
|
||||||
- pushing beyond capacity releases/drops the oldest ready frame
|
- pushing beyond capacity releases/drops the oldest ready frame
|
||||||
- `DropOldestFrame()` is used when the frame pool needs to recycle old completed work
|
- `DropOldestFrame()` is used when the frame pool needs to recycle old completed work
|
||||||
@@ -363,7 +366,7 @@ The probe does not use the main runtime, shader system, preview path, input uplo
|
|||||||
- one OpenGL render thread with its own hidden GL context
|
- one OpenGL render thread with its own hidden GL context
|
||||||
- simple BGRA8 motion rendering
|
- simple BGRA8 motion rendering
|
||||||
- async PBO readback
|
- async PBO readback
|
||||||
- latest-N system-memory frame slots
|
- legacy latest-N system-memory frame slots; bounded FIFO completed reserve in `RenderCadenceCompositor`
|
||||||
- a playout thread that feeds DeckLink
|
- a playout thread that feeds DeckLink
|
||||||
- real rendered warmup before scheduled playback
|
- real rendered warmup before scheduled playback
|
||||||
|
|
||||||
@@ -531,7 +534,7 @@ When `VST_DISABLE_INPUT_CAPTURE=1`, this flow is skipped.
|
|||||||
- Keep one owner for each kind of state.
|
- Keep one owner for each kind of state.
|
||||||
- Keep GL work on the render thread.
|
- Keep GL work on the render thread.
|
||||||
- Keep DeckLink completion callbacks passive.
|
- Keep DeckLink completion callbacks passive.
|
||||||
- Treat completed unscheduled output frames as latest-N cache entries.
|
- In the legacy app, treat completed unscheduled output frames as latest-N cache entries; in `RenderCadenceCompositor`, preserve completed frames as a bounded FIFO reserve.
|
||||||
- Protect scheduled output frames until DeckLink completion.
|
- Protect scheduled output frames until DeckLink completion.
|
||||||
- Keep output timing more important than preview/screenshot.
|
- Keep output timing more important than preview/screenshot.
|
||||||
- Measure timing by domain instead of adding fallback branches blindly.
|
- Measure timing by domain instead of adding fallback branches blindly.
|
||||||
|
|||||||
@@ -115,6 +115,24 @@ Lesson:
|
|||||||
- keep synthetic counters only as diagnostics
|
- keep synthetic counters only as diagnostics
|
||||||
- do not infer device health from internal stream indexes alone
|
- do not infer device health from internal stream indexes alone
|
||||||
|
|
||||||
|
### Schedule Cursor Recovery Must Be Conservative
|
||||||
|
|
||||||
|
The DeckLink schedule cursor should normally advance as a continuous stream timeline. Continuously realigning the next scheduled stream time to the sampled playback cursor can create its own timing fault: output may look like low FPS even when render and scheduling counters average 59.94/60 fps.
|
||||||
|
|
||||||
|
What worked better:
|
||||||
|
|
||||||
|
- use the exact DeckLink frame duration for the render cadence
|
||||||
|
- keep healthy scheduling on a continuous stream cursor
|
||||||
|
- measure schedule lead from DeckLink playback time versus the next schedule time
|
||||||
|
- realign only after real pressure, such as a late/drop report or dangerously low measured lead
|
||||||
|
- re-arm proactive realignment only after lead has recovered
|
||||||
|
|
||||||
|
Lesson:
|
||||||
|
|
||||||
|
- schedule recovery is an output-edge safety valve, not a per-frame timing policy
|
||||||
|
- if recovery increments continuously, the recovery path has become the problem
|
||||||
|
- include schedule lead and realignment count in telemetry/logs so drift is visible before guessing
|
||||||
|
|
||||||
### More Buffer Is Not Automatically Smoother
|
### More Buffer Is Not Automatically Smoother
|
||||||
|
|
||||||
Increasing DeckLink scheduled frames sometimes made the reported device buffer look healthier while visible motion still stuttered.
|
Increasing DeckLink scheduled frames sometimes made the reported device buffer look healthier while visible motion still stuttered.
|
||||||
@@ -196,7 +214,7 @@ Lesson:
|
|||||||
|
|
||||||
- system-memory slots are the contract between render and playout
|
- system-memory slots are the contract between render and playout
|
||||||
- scheduled slots must not be recycled early
|
- scheduled slots must not be recycled early
|
||||||
- completed-but-unscheduled slots can be latest-N cache entries
|
- completed-but-unscheduled slots should form a bounded FIFO reserve for playout
|
||||||
|
|
||||||
### Startup Needs Real Preroll
|
### Startup Needs Real Preroll
|
||||||
|
|
||||||
@@ -222,18 +240,18 @@ Lesson:
|
|||||||
|
|
||||||
The app has at least two important frame stores:
|
The app has at least two important frame stores:
|
||||||
|
|
||||||
- system-memory completed/latest-N frames
|
- system-memory completed FIFO reserve frames
|
||||||
- DeckLink scheduled/device buffer
|
- DeckLink scheduled/device buffer
|
||||||
|
|
||||||
They have different ownership rules.
|
They have different ownership rules.
|
||||||
|
|
||||||
Completed-but-unscheduled frames are disposable if a newer frame is available and cadence needs the slot.
|
Completed-but-unscheduled frames should be a bounded FIFO reserve for playout. If that reserve overflows, dropping the oldest completed frame is an app-side reserve policy and should be counted separately from DeckLink dropped frames.
|
||||||
|
|
||||||
Scheduled frames are not disposable because DeckLink may still read them.
|
Scheduled frames are not disposable because DeckLink may still read them.
|
||||||
|
|
||||||
Lesson:
|
Lesson:
|
||||||
|
|
||||||
- latest-N completed frames are a cache
|
- completed frames waiting for playout are a bounded FIFO reserve
|
||||||
- scheduled frames are owned by DeckLink until completion
|
- scheduled frames are owned by DeckLink until completion
|
||||||
- keep metrics for both
|
- keep metrics for both
|
||||||
|
|
||||||
@@ -246,7 +264,8 @@ That couples the clocks again.
|
|||||||
Lesson:
|
Lesson:
|
||||||
|
|
||||||
- render cadence should keep rendering at selected cadence
|
- render cadence should keep rendering at selected cadence
|
||||||
- if completed cache is full, recycle/drop the oldest unscheduled completed frame
|
- render acquire should not evict completed frames that are waiting for playout
|
||||||
|
- if the completed reserve overflows, drop/count the oldest unscheduled completed frame
|
||||||
- only scheduled/in-flight saturation should prevent rendering to a safe slot
|
- only scheduled/in-flight saturation should prevent rendering to a safe slot
|
||||||
|
|
||||||
## Render Thread Lessons
|
## Render Thread Lessons
|
||||||
@@ -282,6 +301,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.
|
||||||
@@ -322,7 +359,7 @@ The current direction is still sound:
|
|||||||
```text
|
```text
|
||||||
Render cadence loop
|
Render cadence loop
|
||||||
renders at selected output cadence
|
renders at selected output cadence
|
||||||
writes latest-N completed system-memory frames
|
writes completed system-memory frames into a bounded FIFO reserve
|
||||||
never sprints to refill DeckLink
|
never sprints to refill DeckLink
|
||||||
|
|
||||||
Frame store
|
Frame store
|
||||||
@@ -369,7 +406,7 @@ A full rewrite becomes attractive only if the current GL ownership model cannot
|
|||||||
- Render cadence is time-driven, not completion-driven.
|
- Render cadence is time-driven, not completion-driven.
|
||||||
- DeckLink scheduling is device-buffer-driven, not render-driven.
|
- DeckLink scheduling is device-buffer-driven, not render-driven.
|
||||||
- Completion callbacks release and report; they do not render.
|
- Completion callbacks release and report; they do not render.
|
||||||
- System-memory completed frames are latest-N cache entries.
|
- System-memory completed frames are a bounded FIFO reserve.
|
||||||
- Scheduled frames are protected until DeckLink completion.
|
- Scheduled frames are protected until DeckLink completion.
|
||||||
- Startup uses real rendered warmup/preroll.
|
- Startup uses real rendered warmup/preroll.
|
||||||
- Black fallback is degraded/error behavior, not steady-state behavior.
|
- Black fallback is degraded/error behavior, not steady-state behavior.
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
# New Render Cadence App Plan
|
# New Render Cadence App Plan
|
||||||
|
|
||||||
|
Status: historical implementation plan. `apps/RenderCadenceCompositor` now exists; use [apps/RenderCadenceCompositor/README.md](../apps/RenderCadenceCompositor/README.md) and [Render Cadence Golden Rules](RENDER_CADENCE_GOLDEN_RULES.md) as the current implementation contract.
|
||||||
|
|
||||||
This plan describes a new application folder that rebuilds the output path from the proven `DeckLinkRenderCadenceProbe` architecture, but as a maintainable app foundation rather than a monolithic probe file.
|
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.
|
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.
|
||||||
@@ -43,7 +45,7 @@ Render cadence thread
|
|||||||
|
|
||||||
System frame exchange
|
System frame exchange
|
||||||
-> owns Free / Rendering / Completed / Scheduled slots
|
-> owns Free / Rendering / Completed / Scheduled slots
|
||||||
-> latest-N semantics for completed unscheduled frames
|
-> bounded FIFO reserve for completed unscheduled frames
|
||||||
-> protects scheduled frames until DeckLink completion
|
-> protects scheduled frames until DeckLink completion
|
||||||
|
|
||||||
DeckLink output thread
|
DeckLink output thread
|
||||||
@@ -63,7 +65,7 @@ Everything else must fit around that spine.
|
|||||||
- Completion callbacks never render.
|
- Completion callbacks never render.
|
||||||
- No synchronous render request exists in the output path.
|
- 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.
|
- 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.
|
- Completed unscheduled frames are a bounded FIFO reserve; overflow drops are counted separately from DeckLink drops.
|
||||||
- Scheduled frames are protected until DeckLink completion.
|
- Scheduled frames are protected until DeckLink completion.
|
||||||
- Startup warms up real rendered frames before scheduled playback starts.
|
- Startup warms up real rendered frames before scheduled playback starts.
|
||||||
|
|
||||||
@@ -77,7 +79,7 @@ Keep these behaviors from `DeckLinkRenderCadenceProbe`:
|
|||||||
- PBO ring readback
|
- PBO ring readback
|
||||||
- non-blocking fence polling with zero timeout
|
- non-blocking fence polling with zero timeout
|
||||||
- system-memory slots with `Free`, `Rendering`, `Completed`, `Scheduled`
|
- system-memory slots with `Free`, `Rendering`, `Completed`, `Scheduled`
|
||||||
- drop oldest completed unscheduled frame if render needs space
|
- preserve completed frames waiting for playout; drop/count the oldest completed frame only if the bounded reserve overflows
|
||||||
- DeckLink playout thread only schedules completed frames
|
- DeckLink playout thread only schedules completed frames
|
||||||
- warmup completed frames before `StartScheduledPlayback()`
|
- warmup completed frames before `StartScheduledPlayback()`
|
||||||
- one-line-per-second timing telemetry
|
- one-line-per-second timing telemetry
|
||||||
@@ -430,7 +432,7 @@ Feature set:
|
|||||||
- simple motion renderer
|
- simple motion renderer
|
||||||
- BGRA8 only
|
- BGRA8 only
|
||||||
- PBO async readback
|
- PBO async readback
|
||||||
- latest-N system-memory frame exchange
|
- bounded FIFO system-memory frame exchange
|
||||||
- warmup before playback
|
- warmup before playback
|
||||||
- one-line telemetry
|
- one-line telemetry
|
||||||
|
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ The output/scheduling side may:
|
|||||||
- release frames after DeckLink completion
|
- release frames after DeckLink completion
|
||||||
- report late/dropped/schedule telemetry
|
- report late/dropped/schedule telemetry
|
||||||
- record app-side poll misses
|
- record app-side poll misses
|
||||||
|
- conservatively realign the DeckLink schedule cursor after measured timing pressure
|
||||||
|
|
||||||
It must not:
|
It must not:
|
||||||
|
|
||||||
@@ -55,9 +56,12 @@ It must not:
|
|||||||
- invoke GL
|
- invoke GL
|
||||||
- compile shaders
|
- compile shaders
|
||||||
- block the render cadence waiting for DeckLink
|
- block the render cadence waiting for DeckLink
|
||||||
|
- continuously rewrite healthy scheduled timestamps
|
||||||
|
|
||||||
If no completed frame is available, record the miss and keep the ownership boundary intact.
|
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
|
## 4. Runtime Build Work Produces Artifacts
|
||||||
|
|
||||||
Runtime shader work is split into two phases:
|
Runtime shader work is split into two phases:
|
||||||
@@ -91,9 +95,11 @@ Short mutex use for exchanging small already-prepared objects is acceptable. Hol
|
|||||||
|
|
||||||
## 6. System Memory Frames Are A Handoff, Not A Render Driver
|
## 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.
|
The system-memory frame exchange stores completed frames as a bounded FIFO reserve 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.
|
Render acquire must not evict completed frames that are waiting for playout, and it must never force the render thread to wait for the output side to consume a frame.
|
||||||
|
|
||||||
|
If the completed reserve overflows, the exchange may drop the oldest completed, unscheduled frame and record `completedDrops`. That is an app-side reserve drop, not a DeckLink dropped frame.
|
||||||
|
|
||||||
## 7. Startup Uses Warmup, Not Burst Rendering
|
## 7. Startup Uses Warmup, Not Burst Rendering
|
||||||
|
|
||||||
@@ -112,6 +118,12 @@ Good examples:
|
|||||||
- `completedPollMisses`
|
- `completedPollMisses`
|
||||||
- `scheduleFailures`
|
- `scheduleFailures`
|
||||||
- `decklinkBuffered`
|
- `decklinkBuffered`
|
||||||
|
- `deckLinkScheduleLeadFrames`
|
||||||
|
- `deckLinkScheduleRealignments`
|
||||||
|
- `inputCaptureFps`
|
||||||
|
- `inputSubmitMs`
|
||||||
|
- `inputUploadMs`
|
||||||
|
- `inputConvertMs`
|
||||||
- `shaderCommitted`
|
- `shaderCommitted`
|
||||||
- `shaderFailures`
|
- `shaderFailures`
|
||||||
|
|
||||||
|
|||||||
@@ -98,7 +98,7 @@ render cadence thread
|
|||||||
-> samples latest render input/state
|
-> samples latest render input/state
|
||||||
-> renders one frame
|
-> renders one frame
|
||||||
-> queues async readback/copies completed readback into system-memory slot
|
-> queues async readback/copies completed readback into system-memory slot
|
||||||
-> publishes completed frame to latest-N output buffer
|
-> publishes completed frame to bounded FIFO output reserve
|
||||||
|
|
||||||
video output thread
|
video output thread
|
||||||
-> consumes completed system-memory frames
|
-> consumes completed system-memory frames
|
||||||
|
|||||||
@@ -633,10 +633,12 @@ components:
|
|||||||
type: number
|
type: number
|
||||||
renderMs:
|
renderMs:
|
||||||
type: number
|
type: number
|
||||||
|
description: Alias of cadence.renderFrameMs for clients that read the top-level performance object.
|
||||||
smoothedRenderMs:
|
smoothedRenderMs:
|
||||||
type: number
|
type: number
|
||||||
budgetUsedPercent:
|
budgetUsedPercent:
|
||||||
type: number
|
type: number
|
||||||
|
description: Alias of cadence.renderFrameBudgetUsedPercent for clients that read the top-level performance object.
|
||||||
completionIntervalMs:
|
completionIntervalMs:
|
||||||
type: number
|
type: number
|
||||||
smoothedCompletionIntervalMs:
|
smoothedCompletionIntervalMs:
|
||||||
@@ -649,6 +651,93 @@ components:
|
|||||||
type: number
|
type: number
|
||||||
flushedFrameCount:
|
flushedFrameCount:
|
||||||
type: number
|
type: number
|
||||||
|
cadence:
|
||||||
|
$ref: "#/components/schemas/CadenceTelemetry"
|
||||||
|
CadenceTelemetry:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
clockOverruns:
|
||||||
|
type: number
|
||||||
|
description: Render cadence overruns where the render thread was late enough to skip one or more frame intervals.
|
||||||
|
clockSkippedFrames:
|
||||||
|
type: number
|
||||||
|
description: Total render cadence frame intervals skipped instead of catch-up rendering.
|
||||||
|
clockOveruns:
|
||||||
|
type: number
|
||||||
|
deprecated: true
|
||||||
|
description: Deprecated misspelled alias for clockOverruns.
|
||||||
|
clockSkipped:
|
||||||
|
type: number
|
||||||
|
deprecated: true
|
||||||
|
description: Deprecated alias for clockSkippedFrames.
|
||||||
|
renderFrameMs:
|
||||||
|
type: number
|
||||||
|
description: Most recent render-thread frame draw duration in milliseconds, excluding completed-readback copy and readback queue work.
|
||||||
|
renderFrameBudgetUsedPercent:
|
||||||
|
type: number
|
||||||
|
description: Most recent render-thread frame draw duration as a percentage of the selected frame budget.
|
||||||
|
renderFrameMaxMs:
|
||||||
|
type: number
|
||||||
|
description: Maximum observed render-thread frame draw duration in milliseconds for this process.
|
||||||
|
readbackQueueMs:
|
||||||
|
type: number
|
||||||
|
description: Most recent duration spent queueing BGRA8 async PBO readback after rendering.
|
||||||
|
completedReadbackCopyMs:
|
||||||
|
type: number
|
||||||
|
description: Most recent duration spent mapping and copying a completed BGRA8 readback into system-memory frame storage.
|
||||||
|
completedDrops:
|
||||||
|
type: number
|
||||||
|
description: Number of completed unscheduled system-memory frames dropped so render could reuse the slot.
|
||||||
|
acquireMisses:
|
||||||
|
type: number
|
||||||
|
description: Number of times render/readback could not acquire a writable system-memory frame slot.
|
||||||
|
inputFramesReceived:
|
||||||
|
type: number
|
||||||
|
inputFramesDropped:
|
||||||
|
type: number
|
||||||
|
inputConsumeMisses:
|
||||||
|
type: number
|
||||||
|
description: Render ticks where no ready input frame was available to upload.
|
||||||
|
inputUploadMisses:
|
||||||
|
type: number
|
||||||
|
description: Input texture upload attempts that reused the previous GL input texture.
|
||||||
|
inputReadyFrames:
|
||||||
|
type: number
|
||||||
|
description: Ready input frames currently queued in the input mailbox.
|
||||||
|
inputReadingFrames:
|
||||||
|
type: number
|
||||||
|
description: Input frames currently protected while render uploads them.
|
||||||
|
inputLatestAgeMs:
|
||||||
|
type: number
|
||||||
|
inputUploadMs:
|
||||||
|
type: number
|
||||||
|
inputCaptureFps:
|
||||||
|
type: number
|
||||||
|
inputConvertMs:
|
||||||
|
type: number
|
||||||
|
inputSubmitMs:
|
||||||
|
type: number
|
||||||
|
inputCaptureFormat:
|
||||||
|
type: string
|
||||||
|
deckLinkScheduleLeadAvailable:
|
||||||
|
type: boolean
|
||||||
|
description: Whether DeckLink playback stream-time lead telemetry is currently available.
|
||||||
|
deckLinkScheduleLeadFrames:
|
||||||
|
type: number
|
||||||
|
nullable: true
|
||||||
|
description: Estimated number of frame intervals between the next app schedule timestamp and the DeckLink playback frame index.
|
||||||
|
deckLinkPlaybackFrameIndex:
|
||||||
|
type: number
|
||||||
|
description: DeckLink playback stream time converted to frame index at the configured output cadence.
|
||||||
|
deckLinkNextScheduleFrameIndex:
|
||||||
|
type: number
|
||||||
|
description: Next frame index the app scheduler will assign to a DeckLink output frame.
|
||||||
|
deckLinkPlaybackStreamTime:
|
||||||
|
type: number
|
||||||
|
description: Raw DeckLink scheduled playback stream time in the output mode time scale.
|
||||||
|
deckLinkScheduleRealignments:
|
||||||
|
type: number
|
||||||
|
description: Count of schedule-cursor recovery realignments triggered by DeckLink late/drop pressure.
|
||||||
BackendPlayoutStatus:
|
BackendPlayoutStatus:
|
||||||
type: object
|
type: object
|
||||||
properties:
|
properties:
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
#include "AppConfigProvider.h"
|
#include "AppConfigProvider.h"
|
||||||
|
|
||||||
|
#include <chrono>
|
||||||
#include <filesystem>
|
#include <filesystem>
|
||||||
#include <fstream>
|
#include <fstream>
|
||||||
#include <iostream>
|
#include <iostream>
|
||||||
@@ -104,6 +105,8 @@ void TestHelpers()
|
|||||||
|
|
||||||
const double duration = FrameDurationMillisecondsFromRateString("50");
|
const double duration = FrameDurationMillisecondsFromRateString("50");
|
||||||
Expect(duration > 19.9 && duration < 20.1, "frame duration parses numeric rate");
|
Expect(duration > 19.9 && duration < 20.1, "frame duration parses numeric rate");
|
||||||
|
const double deckLinkDuration = FrameDurationMillisecondsFromDisplayMode(bmdModeHD1080p5994, 0.0);
|
||||||
|
Expect(deckLinkDuration > 16.6833 && deckLinkDuration < 16.6834, "DeckLink 59.94 display mode duration is exact");
|
||||||
|
|
||||||
const std::filesystem::path configPath = FindConfigFile();
|
const std::filesystem::path configPath = FindConfigFile();
|
||||||
Expect(!configPath.empty(), "default config is discoverable from test working directory");
|
Expect(!configPath.empty(), "default config is discoverable from test working directory");
|
||||||
|
|||||||
@@ -60,6 +60,27 @@ void TestLatePollRecordsSkippedFrames()
|
|||||||
Expect(cadence.SkippedFrameCount() == 3, "late poll accumulates skipped frames");
|
Expect(cadence.SkippedFrameCount() == 3, "late poll accumulates skipped frames");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void TestLatePollSkipsMissedIntervalsInsteadOfCatchingUp()
|
||||||
|
{
|
||||||
|
using Clock = RenderCadenceClock::Clock;
|
||||||
|
RenderCadenceClock cadence(10.0);
|
||||||
|
const auto start = Clock::now();
|
||||||
|
cadence.Reset(start);
|
||||||
|
|
||||||
|
const auto late = start + std::chrono::milliseconds(35);
|
||||||
|
const auto tick = cadence.Poll(late);
|
||||||
|
Expect(tick.due, "late skipped-interval poll is due");
|
||||||
|
Expect(tick.skippedFrames == 3, "late skipped-interval poll counts missed frames");
|
||||||
|
|
||||||
|
cadence.MarkRendered(late);
|
||||||
|
Expect(cadence.NextRenderTime() > late, "late render schedules the next tick in the future");
|
||||||
|
Expect(cadence.NextRenderTime() - late <= std::chrono::milliseconds(6), "late render does not leave catch-up frames due immediately");
|
||||||
|
|
||||||
|
const auto immediateFollowup = cadence.Poll(late);
|
||||||
|
Expect(!immediateFollowup.due, "cadence does not allow an immediate catch-up render after a late frame");
|
||||||
|
Expect(immediateFollowup.sleepFor > RenderCadenceClock::Duration::zero(), "cadence reports wait time after skipping missed intervals");
|
||||||
|
}
|
||||||
|
|
||||||
void TestMarkRenderedRebasesAfterLargeStall()
|
void TestMarkRenderedRebasesAfterLargeStall()
|
||||||
{
|
{
|
||||||
using Clock = RenderCadenceClock::Clock;
|
using Clock = RenderCadenceClock::Clock;
|
||||||
@@ -81,6 +102,7 @@ int main()
|
|||||||
TestEarlyPollWaitsWithoutAdvancing();
|
TestEarlyPollWaitsWithoutAdvancing();
|
||||||
TestDuePollRendersWithoutSkipping();
|
TestDuePollRendersWithoutSkipping();
|
||||||
TestLatePollRecordsSkippedFrames();
|
TestLatePollRecordsSkippedFrames();
|
||||||
|
TestLatePollSkipsMissedIntervalsInsteadOfCatchingUp();
|
||||||
TestMarkRenderedRebasesAfterLargeStall();
|
TestMarkRenderedRebasesAfterLargeStall();
|
||||||
|
|
||||||
if (gFailures != 0)
|
if (gFailures != 0)
|
||||||
|
|||||||
@@ -27,6 +27,13 @@ SystemFrameExchangeConfig MakeConfig(std::size_t capacity = 2)
|
|||||||
return config;
|
return config;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
SystemFrameExchangeConfig MakeBoundedCompletedConfig(std::size_t capacity = 4, std::size_t maxCompletedFrames = 2)
|
||||||
|
{
|
||||||
|
SystemFrameExchangeConfig config = MakeConfig(capacity);
|
||||||
|
config.maxCompletedFrames = maxCompletedFrames;
|
||||||
|
return config;
|
||||||
|
}
|
||||||
|
|
||||||
void TestAcquirePublishesAndSchedules()
|
void TestAcquirePublishesAndSchedules()
|
||||||
{
|
{
|
||||||
SystemFrameExchange exchange(MakeConfig(1));
|
SystemFrameExchange exchange(MakeConfig(1));
|
||||||
@@ -57,32 +64,54 @@ void TestAcquirePublishesAndSchedules()
|
|||||||
Expect(metrics.scheduledFrames == 1, "scheduled metric is counted");
|
Expect(metrics.scheduledFrames == 1, "scheduled metric is counted");
|
||||||
}
|
}
|
||||||
|
|
||||||
void TestAcquireDropsOldestCompletedUnscheduled()
|
void TestAcquirePreservesCompletedFrames()
|
||||||
{
|
{
|
||||||
SystemFrameExchange exchange(MakeConfig(2));
|
SystemFrameExchange exchange(MakeConfig(2));
|
||||||
|
|
||||||
SystemFrame first;
|
SystemFrame first;
|
||||||
SystemFrame second;
|
SystemFrame second;
|
||||||
SystemFrame third;
|
SystemFrame third;
|
||||||
Expect(exchange.AcquireForRender(first), "first frame can be acquired");
|
Expect(exchange.AcquireForRender(first), "first preserving frame can be acquired");
|
||||||
first.frameIndex = 1;
|
first.frameIndex = 1;
|
||||||
Expect(exchange.PublishCompleted(first), "first frame can be completed");
|
Expect(exchange.PublishCompleted(first), "first preserving frame can be completed");
|
||||||
Expect(exchange.AcquireForRender(second), "second frame can be acquired");
|
Expect(exchange.AcquireForRender(second), "second preserving frame can be acquired");
|
||||||
second.frameIndex = 2;
|
second.frameIndex = 2;
|
||||||
Expect(exchange.PublishCompleted(second), "second frame can be completed");
|
Expect(exchange.PublishCompleted(second), "second preserving frame can be completed");
|
||||||
|
|
||||||
Expect(exchange.AcquireForRender(third), "third acquire drops the oldest completed frame");
|
Expect(!exchange.AcquireForRender(third), "render acquire refuses to drop completed frames");
|
||||||
Expect(third.index == first.index, "oldest completed slot is reused");
|
|
||||||
|
|
||||||
SystemFrame scheduled;
|
SystemFrame scheduled;
|
||||||
Expect(exchange.ConsumeCompletedForSchedule(scheduled), "remaining completed frame can be scheduled");
|
Expect(exchange.ConsumeCompletedForSchedule(scheduled), "oldest completed frame survives acquire miss");
|
||||||
Expect(scheduled.index == second.index, "newer completed frame survives drop");
|
Expect(scheduled.frameIndex == 1, "preserving acquire keeps FIFO output continuity");
|
||||||
Expect(scheduled.frameIndex == 2, "newer frame index survives drop");
|
|
||||||
|
|
||||||
SystemFrameExchangeMetrics metrics = exchange.Metrics();
|
SystemFrameExchangeMetrics metrics = exchange.Metrics();
|
||||||
Expect(metrics.completedDrops == 1, "drop metric is counted");
|
Expect(metrics.completedDrops == 0, "preserving acquire does not count completed drops");
|
||||||
Expect(metrics.renderingCount == 1, "reused slot is rendering");
|
Expect(metrics.acquireMisses == 1, "preserving acquire miss is counted");
|
||||||
Expect(metrics.scheduledCount == 1, "consumed slot is scheduled");
|
}
|
||||||
|
|
||||||
|
void TestCompletedReserveIsBoundedFifo()
|
||||||
|
{
|
||||||
|
SystemFrameExchange exchange(MakeBoundedCompletedConfig(4, 2));
|
||||||
|
|
||||||
|
for (uint64_t frameIndex = 1; frameIndex <= 3; ++frameIndex)
|
||||||
|
{
|
||||||
|
SystemFrame frame;
|
||||||
|
Expect(exchange.AcquireForRender(frame), "bounded reserve frame can be acquired");
|
||||||
|
frame.frameIndex = frameIndex;
|
||||||
|
Expect(exchange.PublishCompleted(frame), "bounded reserve frame can be completed");
|
||||||
|
}
|
||||||
|
|
||||||
|
SystemFrame firstScheduled;
|
||||||
|
Expect(exchange.ConsumeCompletedForSchedule(firstScheduled), "bounded reserve oldest retained frame can be scheduled");
|
||||||
|
Expect(firstScheduled.frameIndex == 2, "bounded reserve drops oldest overflow and keeps FIFO order");
|
||||||
|
|
||||||
|
SystemFrame secondScheduled;
|
||||||
|
Expect(exchange.ConsumeCompletedForSchedule(secondScheduled), "bounded reserve second retained frame can be scheduled");
|
||||||
|
Expect(secondScheduled.frameIndex == 3, "bounded reserve schedules next retained frame");
|
||||||
|
|
||||||
|
SystemFrameExchangeMetrics metrics = exchange.Metrics();
|
||||||
|
Expect(metrics.completedDrops == 1, "bounded completed reserve records oldest overflow drop");
|
||||||
|
Expect(metrics.scheduledFrames == 2, "bounded reserve schedules retained frames");
|
||||||
}
|
}
|
||||||
|
|
||||||
void TestScheduledFramesAreNotDropped()
|
void TestScheduledFramesAreNotDropped()
|
||||||
@@ -154,16 +183,39 @@ void TestCompletedPollMissIsCounted()
|
|||||||
SystemFrameExchangeMetrics metrics = exchange.Metrics();
|
SystemFrameExchangeMetrics metrics = exchange.Metrics();
|
||||||
Expect(metrics.completedPollMisses == 1, "completed poll miss is counted");
|
Expect(metrics.completedPollMisses == 1, "completed poll miss is counted");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void TestStableCompletedDepthCanBeObserved()
|
||||||
|
{
|
||||||
|
SystemFrameExchange exchange(MakeConfig(1));
|
||||||
|
SystemFrame frame;
|
||||||
|
Expect(exchange.AcquireForRender(frame), "stable-depth frame can be acquired");
|
||||||
|
Expect(exchange.PublishCompleted(frame), "stable-depth frame can be completed");
|
||||||
|
|
||||||
|
Expect(
|
||||||
|
exchange.WaitForStableCompletedDepth(1, std::chrono::milliseconds(1), std::chrono::milliseconds(50)),
|
||||||
|
"stable completed depth can be observed");
|
||||||
|
}
|
||||||
|
|
||||||
|
void TestStableCompletedDepthTimesOut()
|
||||||
|
{
|
||||||
|
SystemFrameExchange exchange(MakeConfig(1));
|
||||||
|
Expect(
|
||||||
|
!exchange.WaitForStableCompletedDepth(1, std::chrono::milliseconds(1), std::chrono::milliseconds(1)),
|
||||||
|
"missing stable completed depth times out");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
int main()
|
int main()
|
||||||
{
|
{
|
||||||
TestAcquirePublishesAndSchedules();
|
TestAcquirePublishesAndSchedules();
|
||||||
TestAcquireDropsOldestCompletedUnscheduled();
|
TestAcquirePreservesCompletedFrames();
|
||||||
|
TestCompletedReserveIsBoundedFifo();
|
||||||
TestScheduledFramesAreNotDropped();
|
TestScheduledFramesAreNotDropped();
|
||||||
TestGenerationValidationRejectsStaleFrames();
|
TestGenerationValidationRejectsStaleFrames();
|
||||||
TestPixelFormatAwareSizing();
|
TestPixelFormatAwareSizing();
|
||||||
TestCompletedPollMissIsCounted();
|
TestCompletedPollMissIsCounted();
|
||||||
|
TestStableCompletedDepthCanBeObserved();
|
||||||
|
TestStableCompletedDepthTimesOut();
|
||||||
|
|
||||||
if (gFailures != 0)
|
if (gFailures != 0)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -147,6 +147,29 @@ void TestLayerPostEndpointsUseCallbacks()
|
|||||||
Expect(removeResponse.body.find("Unknown layer id.") != std::string::npos, "remove layer callback returns diagnostic");
|
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()
|
void TestUnknownEndpointReturns404()
|
||||||
{
|
{
|
||||||
using namespace RenderCadenceCompositor;
|
using namespace RenderCadenceCompositor;
|
||||||
@@ -169,6 +192,7 @@ int main()
|
|||||||
TestRootServesUiIndex();
|
TestRootServesUiIndex();
|
||||||
TestKnownPostEndpointReturnsActionError();
|
TestKnownPostEndpointReturnsActionError();
|
||||||
TestLayerPostEndpointsUseCallbacks();
|
TestLayerPostEndpointsUseCallbacks();
|
||||||
|
TestGenericPostCallbackHandlesControlRoutes();
|
||||||
TestUnknownEndpointReturns404();
|
TestUnknownEndpointReturns404();
|
||||||
|
|
||||||
if (gFailures != 0)
|
if (gFailures != 0)
|
||||||
|
|||||||
137
tests/RenderCadenceCompositorInputFrameMailboxTests.cpp
Normal file
137
tests/RenderCadenceCompositorInputFrameMailboxTests.cpp
Normal file
@@ -0,0 +1,137 @@
|
|||||||
|
#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;
|
||||||
|
}
|
||||||
|
|
||||||
|
InputFrameMailboxConfig MakeBufferedConfig(std::size_t capacity = 4, std::size_t maxReadyFrames = 2)
|
||||||
|
{
|
||||||
|
InputFrameMailboxConfig config = MakeConfig(capacity);
|
||||||
|
config.maxReadyFrames = maxReadyFrames;
|
||||||
|
return config;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<unsigned char> MakeFrame(unsigned char value)
|
||||||
|
{
|
||||||
|
return std::vector<unsigned char>(16, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
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 oldest;
|
||||||
|
Expect(mailbox.TryAcquireOldest(oldest), "oldest frame available after drop");
|
||||||
|
Expect(oldest.frameIndex == 2, "bounded mailbox keeps FIFO order after trimming oldest overflow");
|
||||||
|
Expect(mailbox.Release(oldest), "oldest 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.TryAcquireOldest(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");
|
||||||
|
}
|
||||||
|
|
||||||
|
void TestAcquireOldestConsumesFifoWithoutDroppingReadyFrames()
|
||||||
|
{
|
||||||
|
InputFrameMailbox mailbox(MakeConfig(3));
|
||||||
|
const std::vector<unsigned char> frame1 = MakeFrame(1);
|
||||||
|
const std::vector<unsigned char> frame2 = MakeFrame(2);
|
||||||
|
|
||||||
|
Expect(mailbox.SubmitFrame(frame1.data(), 8, 1), "fifo first frame submits");
|
||||||
|
Expect(mailbox.SubmitFrame(frame2.data(), 8, 2), "fifo second frame submits");
|
||||||
|
|
||||||
|
InputFrame acquired;
|
||||||
|
Expect(mailbox.TryAcquireOldest(acquired), "fifo oldest frame acquired");
|
||||||
|
Expect(acquired.frameIndex == 1, "fifo acquire returns oldest frame");
|
||||||
|
Expect(mailbox.Release(acquired), "fifo acquired frame releases");
|
||||||
|
|
||||||
|
const InputFrameMailboxMetrics metrics = mailbox.Metrics();
|
||||||
|
Expect(metrics.readyCount == 1, "fifo acquire leaves newer frame ready");
|
||||||
|
Expect(metrics.droppedReadyFrames == 0, "fifo acquire does not drop newer ready frame");
|
||||||
|
}
|
||||||
|
|
||||||
|
void TestMaxReadyFramesKeepsConfiguredInputBuffer()
|
||||||
|
{
|
||||||
|
InputFrameMailbox mailbox(MakeBufferedConfig(4, 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);
|
||||||
|
const std::vector<unsigned char> frame4 = MakeFrame(4);
|
||||||
|
|
||||||
|
Expect(mailbox.SubmitFrame(frame1.data(), 8, 1), "bounded first frame submits");
|
||||||
|
Expect(mailbox.SubmitFrame(frame2.data(), 8, 2), "bounded second frame submits");
|
||||||
|
Expect(mailbox.SubmitFrame(frame3.data(), 8, 3), "bounded third frame submits");
|
||||||
|
Expect(mailbox.SubmitFrame(frame4.data(), 8, 4), "bounded fourth frame submits");
|
||||||
|
|
||||||
|
InputFrame acquired;
|
||||||
|
Expect(mailbox.TryAcquireOldest(acquired), "bounded oldest available frame acquired");
|
||||||
|
Expect(acquired.frameIndex == 2, "bounded buffer trims oldest beyond configured ready frame limit");
|
||||||
|
Expect(mailbox.Release(acquired), "bounded acquired frame releases");
|
||||||
|
|
||||||
|
const InputFrameMailboxMetrics metrics = mailbox.Metrics();
|
||||||
|
Expect(metrics.readyCount == 2, "bounded acquire leaves remaining configured ready frames");
|
||||||
|
Expect(metrics.droppedReadyFrames == 1, "bounded buffer records trimmed frame");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int main()
|
||||||
|
{
|
||||||
|
TestSubmitDropsOldestWhenFull();
|
||||||
|
TestReadingFrameIsProtected();
|
||||||
|
TestAcquireOldestConsumesFifoWithoutDroppingReadyFrames();
|
||||||
|
TestMaxReadyFramesKeepsConfiguredInputBuffer();
|
||||||
|
|
||||||
|
if (gFailures != 0)
|
||||||
|
{
|
||||||
|
std::cerr << gFailures << " RenderCadenceCompositorInputFrameMailbox test failure(s).\n";
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::cout << "RenderCadenceCompositorInputFrameMailbox tests passed.\n";
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
@@ -45,7 +45,8 @@ RenderCadenceCompositor::SupportedShaderCatalog MakeCatalog(std::filesystem::pat
|
|||||||
"category": "Tests",
|
"category": "Tests",
|
||||||
"entryPoint": "shadeVideo",
|
"entryPoint": "shadeVideo",
|
||||||
"parameters": [
|
"parameters": [
|
||||||
{ "id": "gain", "label": "Gain", "type": "float", "default": 0.5 }
|
{ "id": "gain", "label": "Gain", "type": "float", "default": 0.5 },
|
||||||
|
{ "id": "drop", "label": "Drop", "type": "trigger" }
|
||||||
]
|
]
|
||||||
})");
|
})");
|
||||||
|
|
||||||
@@ -143,6 +144,58 @@ void TestAddAndRemoveLayers()
|
|||||||
|
|
||||||
std::filesystem::remove_all(root);
|
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()
|
int main()
|
||||||
@@ -151,6 +204,7 @@ int main()
|
|||||||
TestRejectsUnsupportedStartupShader();
|
TestRejectsUnsupportedStartupShader();
|
||||||
TestBuildFailureStaysDisplaySide();
|
TestBuildFailureStaysDisplaySide();
|
||||||
TestAddAndRemoveLayers();
|
TestAddAndRemoveLayers();
|
||||||
|
TestLayerControlsUpdateDisplayAndRenderModels();
|
||||||
|
|
||||||
if (gFailures != 0)
|
if (gFailures != 0)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -80,6 +80,15 @@ ShaderParameterDefinition EnumParam()
|
|||||||
definition.enumOptions = { { "soft", "Soft" }, { "hard", "Hard" } };
|
definition.enumOptions = { { "soft", "Soft" }, { "hard", "Hard" } };
|
||||||
return definition;
|
return definition;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ShaderParameterDefinition TriggerParam()
|
||||||
|
{
|
||||||
|
ShaderParameterDefinition definition;
|
||||||
|
definition.id = "drop";
|
||||||
|
definition.label = "Drop";
|
||||||
|
definition.type = ShaderParameterType::Trigger;
|
||||||
|
return definition;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
int main()
|
int main()
|
||||||
@@ -90,6 +99,11 @@ int main()
|
|||||||
artifact.parameterDefinitions.push_back(ColorParam());
|
artifact.parameterDefinitions.push_back(ColorParam());
|
||||||
artifact.parameterDefinitions.push_back(BoolParam());
|
artifact.parameterDefinitions.push_back(BoolParam());
|
||||||
artifact.parameterDefinitions.push_back(EnumParam());
|
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);
|
const std::vector<unsigned char> buffer = BuildRuntimeShaderGlobalParamsStd140(artifact, 120, 1920, 1080);
|
||||||
|
|
||||||
@@ -104,6 +118,8 @@ int main()
|
|||||||
Expect(ReadFloat(buffer, 92) == 1.0f, "color default alpha is packed");
|
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, 96) == 1, "bool default is packed as int");
|
||||||
Expect(ReadInt(buffer, 100) == 1, "enum default is packed as selected option index");
|
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";
|
std::cout << "RenderCadenceCompositorRuntimeShaderParams tests passed.\n";
|
||||||
return 0;
|
return 0;
|
||||||
|
|||||||
@@ -43,6 +43,13 @@ int main()
|
|||||||
|
|
||||||
RenderCadenceCompositor::CadenceTelemetrySnapshot telemetry;
|
RenderCadenceCompositor::CadenceTelemetrySnapshot telemetry;
|
||||||
telemetry.renderFps = 59.94;
|
telemetry.renderFps = 59.94;
|
||||||
|
telemetry.renderFrameMilliseconds = 2.5;
|
||||||
|
telemetry.renderFrameBudgetUsedPercent = 15.0;
|
||||||
|
telemetry.renderFrameMaxMilliseconds = 4.0;
|
||||||
|
telemetry.readbackQueueMilliseconds = 0.6;
|
||||||
|
telemetry.completedReadbackCopyMilliseconds = 1.2;
|
||||||
|
telemetry.completedDrops = 3;
|
||||||
|
telemetry.acquireMisses = 4;
|
||||||
telemetry.shaderBuildsCommitted = 1;
|
telemetry.shaderBuildsCommitted = 1;
|
||||||
|
|
||||||
const std::filesystem::path root = MakeTestRoot();
|
const std::filesystem::path root = MakeTestRoot();
|
||||||
@@ -98,6 +105,13 @@ int main()
|
|||||||
ExpectContains(json, "\"type\":\"color\"", "state JSON should serialize parameter types for the UI");
|
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, "\"width\":1920", "state JSON should expose output width");
|
||||||
ExpectContains(json, "\"height\":1080", "state JSON should expose output height");
|
ExpectContains(json, "\"height\":1080", "state JSON should expose output height");
|
||||||
|
ExpectContains(json, "\"renderMs\":2.5", "state JSON should expose top-level render timing");
|
||||||
|
ExpectContains(json, "\"budgetUsedPercent\":15", "state JSON should expose top-level render budget percentage");
|
||||||
|
ExpectContains(json, "\"renderFrameMs\":2.5", "state JSON should expose cadence render timing");
|
||||||
|
ExpectContains(json, "\"readbackQueueMs\":0.6", "state JSON should expose readback queue timing");
|
||||||
|
ExpectContains(json, "\"completedReadbackCopyMs\":1.2", "state JSON should expose completed readback copy timing");
|
||||||
|
ExpectContains(json, "\"completedDrops\":3", "state JSON should expose completed drop count");
|
||||||
|
ExpectContains(json, "\"acquireMisses\":4", "state JSON should expose acquire miss count");
|
||||||
|
|
||||||
std::filesystem::remove_all(root);
|
std::filesystem::remove_all(root);
|
||||||
|
|
||||||
|
|||||||
@@ -23,6 +23,8 @@ ShaderPackage MakeSinglePassPackage()
|
|||||||
ShaderPassDefinition pass;
|
ShaderPassDefinition pass;
|
||||||
pass.id = "main";
|
pass.id = "main";
|
||||||
pass.entryPoint = "mainImage";
|
pass.entryPoint = "mainImage";
|
||||||
|
pass.sourcePath = "shader.slang";
|
||||||
|
pass.outputName = "layerOutput";
|
||||||
shaderPackage.passes.push_back(pass);
|
shaderPackage.passes.push_back(pass);
|
||||||
return shaderPackage;
|
return shaderPackage;
|
||||||
}
|
}
|
||||||
@@ -37,19 +39,35 @@ void SupportsSinglePassStatelessPackage()
|
|||||||
Expect(result.reason.empty(), "supported packages should not report a rejection reason");
|
Expect(result.reason.empty(), "supported packages should not report a rejection reason");
|
||||||
}
|
}
|
||||||
|
|
||||||
void RejectsMultipassPackage()
|
void SupportsStatelessNamedPassPackage()
|
||||||
{
|
{
|
||||||
ShaderPackage shaderPackage = MakeSinglePassPackage();
|
ShaderPackage shaderPackage = MakeSinglePassPackage();
|
||||||
|
shaderPackage.passes.front().outputName = "generatedMask";
|
||||||
ShaderPassDefinition secondPass;
|
ShaderPassDefinition secondPass;
|
||||||
secondPass.id = "second";
|
secondPass.id = "second";
|
||||||
secondPass.entryPoint = "mainImage";
|
secondPass.entryPoint = "mainImage";
|
||||||
|
secondPass.sourcePath = "shader.slang";
|
||||||
|
secondPass.inputNames.push_back("generatedMask");
|
||||||
|
secondPass.outputName = "layerOutput";
|
||||||
shaderPackage.passes.push_back(secondPass);
|
shaderPackage.passes.push_back(secondPass);
|
||||||
|
|
||||||
const RenderCadenceCompositor::ShaderSupportResult result =
|
const RenderCadenceCompositor::ShaderSupportResult result =
|
||||||
RenderCadenceCompositor::CheckStatelessSinglePassShaderSupport(shaderPackage);
|
RenderCadenceCompositor::CheckStatelessSinglePassShaderSupport(shaderPackage);
|
||||||
|
|
||||||
Expect(!result.supported, "multipass packages should be rejected");
|
Expect(result.supported, "stateless named-pass packages should be supported");
|
||||||
Expect(result.reason.find("single-pass") != std::string::npos, "multipass rejection should explain the single-pass limit");
|
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()
|
void RejectsTemporalPackage()
|
||||||
@@ -97,7 +115,8 @@ void RejectsTextParameters()
|
|||||||
int main()
|
int main()
|
||||||
{
|
{
|
||||||
SupportsSinglePassStatelessPackage();
|
SupportsSinglePassStatelessPackage();
|
||||||
RejectsMultipassPackage();
|
SupportsStatelessNamedPassPackage();
|
||||||
|
RejectsUnknownPassInput();
|
||||||
RejectsTemporalPackage();
|
RejectsTemporalPackage();
|
||||||
RejectsTextureAssets();
|
RejectsTextureAssets();
|
||||||
RejectsTextParameters();
|
RejectsTextParameters();
|
||||||
|
|||||||
@@ -26,6 +26,8 @@ struct FakeExchangeMetrics
|
|||||||
std::size_t scheduledCount = 0;
|
std::size_t scheduledCount = 0;
|
||||||
uint64_t completedFrames = 0;
|
uint64_t completedFrames = 0;
|
||||||
uint64_t scheduledFrames = 0;
|
uint64_t scheduledFrames = 0;
|
||||||
|
uint64_t completedDrops = 0;
|
||||||
|
uint64_t acquireMisses = 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
struct FakeExchange
|
struct FakeExchange
|
||||||
@@ -55,6 +57,12 @@ struct FakeOutputMetrics
|
|||||||
bool actualBufferedFramesAvailable = false;
|
bool actualBufferedFramesAvailable = false;
|
||||||
uint64_t actualBufferedFrames = 0;
|
uint64_t actualBufferedFrames = 0;
|
||||||
double scheduleCallMilliseconds = 0.0;
|
double scheduleCallMilliseconds = 0.0;
|
||||||
|
bool scheduleLeadAvailable = false;
|
||||||
|
int64_t playbackStreamTime = 0;
|
||||||
|
uint64_t playbackFrameIndex = 0;
|
||||||
|
uint64_t nextScheduleFrameIndex = 0;
|
||||||
|
int64_t scheduleLeadFrames = 0;
|
||||||
|
uint64_t scheduleRealignmentCount = 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
struct FakeOutput
|
struct FakeOutput
|
||||||
@@ -65,8 +73,25 @@ struct FakeOutput
|
|||||||
|
|
||||||
struct FakeRenderThreadMetrics
|
struct FakeRenderThreadMetrics
|
||||||
{
|
{
|
||||||
|
uint64_t clockOverruns = 0;
|
||||||
|
uint64_t skippedFrames = 0;
|
||||||
uint64_t shaderBuildsCommitted = 0;
|
uint64_t shaderBuildsCommitted = 0;
|
||||||
uint64_t shaderBuildFailures = 0;
|
uint64_t shaderBuildFailures = 0;
|
||||||
|
double renderFrameMilliseconds = 0.0;
|
||||||
|
double renderFrameBudgetUsedPercent = 0.0;
|
||||||
|
double renderFrameMaxMilliseconds = 0.0;
|
||||||
|
double readbackQueueMilliseconds = 0.0;
|
||||||
|
double completedReadbackCopyMilliseconds = 0.0;
|
||||||
|
uint64_t inputFramesReceived = 0;
|
||||||
|
uint64_t inputFramesDropped = 0;
|
||||||
|
uint64_t inputConsumeMisses = 0;
|
||||||
|
uint64_t inputUploadMisses = 0;
|
||||||
|
std::size_t inputReadyFrames = 0;
|
||||||
|
std::size_t inputReadingFrames = 0;
|
||||||
|
double inputLatestAgeMilliseconds = 0.0;
|
||||||
|
double inputUploadMilliseconds = 0.0;
|
||||||
|
bool inputFormatSupported = true;
|
||||||
|
bool inputSignalPresent = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
struct FakeRenderThread
|
struct FakeRenderThread
|
||||||
@@ -84,28 +109,78 @@ void TestTelemetrySamplesCompletedPollMissesAndShaderCounts()
|
|||||||
exchange.metrics.scheduledCount = 4;
|
exchange.metrics.scheduledCount = 4;
|
||||||
exchange.metrics.completedFrames = 100;
|
exchange.metrics.completedFrames = 100;
|
||||||
exchange.metrics.scheduledFrames = 96;
|
exchange.metrics.scheduledFrames = 96;
|
||||||
|
exchange.metrics.completedDrops = 2;
|
||||||
|
exchange.metrics.acquireMisses = 3;
|
||||||
|
|
||||||
FakeOutput output;
|
FakeOutput output;
|
||||||
output.metrics.actualBufferedFramesAvailable = true;
|
output.metrics.actualBufferedFramesAvailable = true;
|
||||||
output.metrics.actualBufferedFrames = 4;
|
output.metrics.actualBufferedFrames = 4;
|
||||||
|
output.metrics.scheduleLeadAvailable = true;
|
||||||
|
output.metrics.playbackStreamTime = 10010;
|
||||||
|
output.metrics.playbackFrameIndex = 10;
|
||||||
|
output.metrics.nextScheduleFrameIndex = 14;
|
||||||
|
output.metrics.scheduleLeadFrames = 4;
|
||||||
|
output.metrics.scheduleRealignmentCount = 1;
|
||||||
|
|
||||||
FakeOutputThread outputThread;
|
FakeOutputThread outputThread;
|
||||||
outputThread.metrics.completedPollMisses = 12;
|
outputThread.metrics.completedPollMisses = 12;
|
||||||
outputThread.metrics.scheduleFailures = 0;
|
outputThread.metrics.scheduleFailures = 0;
|
||||||
|
|
||||||
FakeRenderThread renderThread;
|
FakeRenderThread renderThread;
|
||||||
|
renderThread.metrics.clockOverruns = 5;
|
||||||
|
renderThread.metrics.skippedFrames = 8;
|
||||||
renderThread.metrics.shaderBuildsCommitted = 1;
|
renderThread.metrics.shaderBuildsCommitted = 1;
|
||||||
renderThread.metrics.shaderBuildFailures = 0;
|
renderThread.metrics.shaderBuildFailures = 0;
|
||||||
|
renderThread.metrics.renderFrameMilliseconds = 2.5;
|
||||||
|
renderThread.metrics.renderFrameBudgetUsedPercent = 15.0;
|
||||||
|
renderThread.metrics.renderFrameMaxMilliseconds = 4.0;
|
||||||
|
renderThread.metrics.readbackQueueMilliseconds = 0.6;
|
||||||
|
renderThread.metrics.completedReadbackCopyMilliseconds = 1.2;
|
||||||
|
renderThread.metrics.inputFramesReceived = 9;
|
||||||
|
renderThread.metrics.inputFramesDropped = 2;
|
||||||
|
renderThread.metrics.inputConsumeMisses = 3;
|
||||||
|
renderThread.metrics.inputUploadMisses = 4;
|
||||||
|
renderThread.metrics.inputReadyFrames = 1;
|
||||||
|
renderThread.metrics.inputReadingFrames = 0;
|
||||||
|
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);
|
const auto snapshot = telemetry.Sample(exchange, output, outputThread, renderThread);
|
||||||
Expect(snapshot.freeFrames == 7, "free frame count is sampled");
|
Expect(snapshot.freeFrames == 7, "free frame count is sampled");
|
||||||
Expect(snapshot.completedFrames == 1, "completed frame count is sampled");
|
Expect(snapshot.completedFrames == 1, "completed frame count is sampled");
|
||||||
Expect(snapshot.scheduledFrames == 4, "scheduled frame count is sampled");
|
Expect(snapshot.scheduledFrames == 4, "scheduled frame count is sampled");
|
||||||
Expect(snapshot.completedPollMisses == 12, "completed poll misses are sampled");
|
Expect(snapshot.completedPollMisses == 12, "completed poll misses are sampled");
|
||||||
|
Expect(snapshot.completedDrops == 2, "completed drops are sampled");
|
||||||
|
Expect(snapshot.acquireMisses == 3, "acquire misses are sampled");
|
||||||
|
Expect(snapshot.clockOverruns == 5, "clock overrun count is sampled");
|
||||||
|
Expect(snapshot.clockSkippedFrames == 8, "clock skipped frame count is sampled");
|
||||||
Expect(snapshot.shaderBuildsCommitted == 1, "shader committed count is sampled");
|
Expect(snapshot.shaderBuildsCommitted == 1, "shader committed count is sampled");
|
||||||
Expect(snapshot.shaderBuildFailures == 0, "shader failure count is sampled");
|
Expect(snapshot.shaderBuildFailures == 0, "shader failure count is sampled");
|
||||||
|
Expect(snapshot.renderFrameMilliseconds == 2.5, "render frame timing is sampled");
|
||||||
|
Expect(snapshot.renderFrameBudgetUsedPercent == 15.0, "render budget percentage is sampled");
|
||||||
|
Expect(snapshot.renderFrameMaxMilliseconds == 4.0, "render frame max timing is sampled");
|
||||||
|
Expect(snapshot.readbackQueueMilliseconds == 0.6, "readback queue timing is sampled");
|
||||||
|
Expect(snapshot.completedReadbackCopyMilliseconds == 1.2, "completed readback copy timing is sampled");
|
||||||
|
Expect(snapshot.inputFramesReceived == 9, "input received count is sampled");
|
||||||
|
Expect(snapshot.inputFramesDropped == 2, "input dropped count is sampled");
|
||||||
|
Expect(snapshot.inputConsumeMisses == 3, "input consume miss count is sampled");
|
||||||
|
Expect(snapshot.inputUploadMisses == 4, "input upload miss count is sampled");
|
||||||
|
Expect(snapshot.inputReadyFrames == 1, "input ready frame count is sampled");
|
||||||
|
Expect(snapshot.inputReadingFrames == 0, "input reading frame 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.deckLinkBufferedAvailable, "buffer telemetry availability is sampled");
|
||||||
Expect(snapshot.deckLinkBuffered == 4, "buffer depth is sampled");
|
Expect(snapshot.deckLinkBuffered == 4, "buffer depth is sampled");
|
||||||
|
Expect(snapshot.deckLinkScheduleLeadAvailable, "schedule lead availability is sampled");
|
||||||
|
Expect(snapshot.deckLinkPlaybackStreamTime == 10010, "playback stream time is sampled");
|
||||||
|
Expect(snapshot.deckLinkPlaybackFrameIndex == 10, "playback frame index is sampled");
|
||||||
|
Expect(snapshot.deckLinkNextScheduleFrameIndex == 14, "next schedule frame index is sampled");
|
||||||
|
Expect(snapshot.deckLinkScheduleLeadFrames == 4, "schedule lead frames are sampled");
|
||||||
|
Expect(snapshot.deckLinkScheduleRealignments == 1, "schedule realignment count is sampled");
|
||||||
}
|
}
|
||||||
|
|
||||||
void TestTelemetryComputesRatesFromDeltas()
|
void TestTelemetryComputesRatesFromDeltas()
|
||||||
@@ -143,14 +218,46 @@ void TestTelemetrySerializesToJson()
|
|||||||
snapshot.scheduledTotal = 118;
|
snapshot.scheduledTotal = 118;
|
||||||
snapshot.completedPollMisses = 3;
|
snapshot.completedPollMisses = 3;
|
||||||
snapshot.scheduleFailures = 0;
|
snapshot.scheduleFailures = 0;
|
||||||
|
snapshot.completedDrops = 4;
|
||||||
|
snapshot.acquireMisses = 5;
|
||||||
snapshot.completions = 117;
|
snapshot.completions = 117;
|
||||||
snapshot.displayedLate = 1;
|
snapshot.displayedLate = 1;
|
||||||
snapshot.dropped = 2;
|
snapshot.dropped = 2;
|
||||||
|
snapshot.clockOverruns = 3;
|
||||||
|
snapshot.clockSkippedFrames = 5;
|
||||||
snapshot.shaderBuildsCommitted = 1;
|
snapshot.shaderBuildsCommitted = 1;
|
||||||
snapshot.shaderBuildFailures = 0;
|
snapshot.shaderBuildFailures = 0;
|
||||||
|
snapshot.renderFrameMilliseconds = 2.5;
|
||||||
|
snapshot.renderFrameBudgetUsedPercent = 15.0;
|
||||||
|
snapshot.renderFrameMaxMilliseconds = 4.0;
|
||||||
|
snapshot.readbackQueueMilliseconds = 0.6;
|
||||||
|
snapshot.completedReadbackCopyMilliseconds = 1.2;
|
||||||
|
snapshot.inputFramesReceived = 10;
|
||||||
|
snapshot.inputFramesDropped = 1;
|
||||||
|
snapshot.inputConsumeMisses = 2;
|
||||||
|
snapshot.inputUploadMisses = 3;
|
||||||
|
snapshot.inputReadyFrames = 1;
|
||||||
|
snapshot.inputReadingFrames = 0;
|
||||||
|
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.deckLinkBufferedAvailable = true;
|
||||||
snapshot.deckLinkBuffered = 4;
|
snapshot.deckLinkBuffered = 4;
|
||||||
snapshot.deckLinkScheduleCallMilliseconds = 1.25;
|
snapshot.deckLinkScheduleCallMilliseconds = 1.25;
|
||||||
|
snapshot.deckLinkScheduleLeadAvailable = true;
|
||||||
|
snapshot.deckLinkScheduleLeadFrames = 4;
|
||||||
|
snapshot.deckLinkPlaybackFrameIndex = 10;
|
||||||
|
snapshot.deckLinkNextScheduleFrameIndex = 14;
|
||||||
|
snapshot.deckLinkPlaybackStreamTime = 10010;
|
||||||
|
snapshot.deckLinkScheduleRealignments = 1;
|
||||||
|
|
||||||
const std::string json = RenderCadenceCompositor::CadenceTelemetryToJson(snapshot);
|
const std::string json = RenderCadenceCompositor::CadenceTelemetryToJson(snapshot);
|
||||||
const std::string expected =
|
const std::string expected =
|
||||||
@@ -158,10 +265,31 @@ void TestTelemetrySerializesToJson()
|
|||||||
"\"free\":7,\"completed\":1,\"scheduled\":4,"
|
"\"free\":7,\"completed\":1,\"scheduled\":4,"
|
||||||
"\"renderedTotal\":120,\"scheduledTotal\":118,"
|
"\"renderedTotal\":120,\"scheduledTotal\":118,"
|
||||||
"\"completedPollMisses\":3,\"scheduleFailures\":0,"
|
"\"completedPollMisses\":3,\"scheduleFailures\":0,"
|
||||||
|
"\"completedDrops\":4,\"acquireMisses\":5,"
|
||||||
"\"completions\":117,\"late\":1,\"dropped\":2,"
|
"\"completions\":117,\"late\":1,\"dropped\":2,"
|
||||||
|
"\"clockOverruns\":3,\"clockSkippedFrames\":5,"
|
||||||
|
"\"clockOveruns\":3,\"clockSkipped\":5,"
|
||||||
"\"shaderCommitted\":1,\"shaderFailures\":0,"
|
"\"shaderCommitted\":1,\"shaderFailures\":0,"
|
||||||
|
"\"renderFrameMs\":2.5,\"renderFrameBudgetUsedPercent\":15,"
|
||||||
|
"\"renderFrameMaxMs\":4,\"readbackQueueMs\":0.6,"
|
||||||
|
"\"completedReadbackCopyMs\":1.2,"
|
||||||
|
"\"inputFramesReceived\":10,\"inputFramesDropped\":1,"
|
||||||
|
"\"inputConsumeMisses\":2,\"inputUploadMisses\":3,"
|
||||||
|
"\"inputReadyFrames\":1,\"inputReadingFrames\":0,"
|
||||||
|
"\"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,"
|
"\"deckLinkBufferedAvailable\":true,\"deckLinkBuffered\":4,"
|
||||||
"\"scheduleCallMs\":1.25}";
|
"\"scheduleCallMs\":1.25,"
|
||||||
|
"\"deckLinkScheduleLeadAvailable\":true,"
|
||||||
|
"\"deckLinkScheduleLeadFrames\":4,"
|
||||||
|
"\"deckLinkPlaybackFrameIndex\":10,"
|
||||||
|
"\"deckLinkNextScheduleFrameIndex\":14,"
|
||||||
|
"\"deckLinkPlaybackStreamTime\":10010,"
|
||||||
|
"\"deckLinkScheduleRealignments\":1}";
|
||||||
Expect(json == expected, "telemetry snapshot serializes to stable JSON");
|
Expect(json == expected, "telemetry snapshot serializes to stable JSON");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -70,6 +70,19 @@ void TestDefaultPolicyReportsLagWithoutSkippingScheduleTime()
|
|||||||
Expect(scheduler.NextScheduleTime().streamTime == 1000, "default recovery keeps stream time continuous");
|
Expect(scheduler.NextScheduleTime().streamTime == 1000, "default recovery keeps stream time continuous");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void TestScheduleCursorCanAlignToPlaybackClock()
|
||||||
|
{
|
||||||
|
VideoPlayoutScheduler scheduler;
|
||||||
|
scheduler.Configure(1000, 50000);
|
||||||
|
|
||||||
|
(void)scheduler.NextScheduleTime();
|
||||||
|
scheduler.AlignNextScheduleTimeToPlayback(10000, 4);
|
||||||
|
Expect(scheduler.NextScheduleTime().streamTime == 14000, "schedule cursor skips stale stream time after underfeed");
|
||||||
|
|
||||||
|
scheduler.AlignNextScheduleTimeToPlayback(11000, 1);
|
||||||
|
Expect(scheduler.NextScheduleTime().streamTime == 15000, "schedule cursor does not move backward");
|
||||||
|
}
|
||||||
|
|
||||||
void TestMeasuredRecoveryIsCappedByPolicy()
|
void TestMeasuredRecoveryIsCappedByPolicy()
|
||||||
{
|
{
|
||||||
VideoPlayoutPolicy policy;
|
VideoPlayoutPolicy policy;
|
||||||
@@ -133,6 +146,7 @@ int main()
|
|||||||
TestScheduleAdvancesFromZero();
|
TestScheduleAdvancesFromZero();
|
||||||
TestLateAndDroppedRecoveryUsesMeasuredPressure();
|
TestLateAndDroppedRecoveryUsesMeasuredPressure();
|
||||||
TestDefaultPolicyReportsLagWithoutSkippingScheduleTime();
|
TestDefaultPolicyReportsLagWithoutSkippingScheduleTime();
|
||||||
|
TestScheduleCursorCanAlignToPlaybackClock();
|
||||||
TestMeasuredRecoveryIsCappedByPolicy();
|
TestMeasuredRecoveryIsCappedByPolicy();
|
||||||
TestCleanCompletionTracksCompletedIndexAndClearsStreaks();
|
TestCleanCompletionTracksCompletedIndexAndClearsStreaks();
|
||||||
TestPolicyNormalization();
|
TestPolicyNormalization();
|
||||||
|
|||||||
Reference in New Issue
Block a user