30 Commits

Author SHA1 Message Date
Aiden
3ffb562ff7 docs update
All checks were successful
CI / React UI Build (push) Successful in 49s
CI / Native Windows Build And Tests (push) Successful in 3m27s
CI / Windows Release Package (push) Successful in 3m39s
2026-05-13 01:06:20 +10:00
Aiden
c2d548499c Timing is finally good
All checks were successful
CI / React UI Build (push) Successful in 11s
CI / Native Windows Build And Tests (push) Successful in 2m57s
CI / Windows Release Package (push) Has been skipped
2026-05-13 00:58:32 +10:00
Aiden
6a0340d1b4 proactive realignment 2026-05-13 00:28:11 +10:00
Aiden
5c1fc2a6cf telemetry and timing updates
All checks were successful
CI / React UI Build (push) Successful in 10s
CI / Native Windows Build And Tests (push) Successful in 2m58s
CI / Windows Release Package (push) Has been skipped
2026-05-13 00:21:28 +10:00
Aiden
d411453f80 timing refactor
All checks were successful
CI / React UI Build (push) Successful in 10s
CI / Native Windows Build And Tests (push) Successful in 3m1s
CI / Windows Release Package (push) Successful in 3m20s
2026-05-12 23:39:57 +10:00
Aiden
4a049a557a Render timing 2026-05-12 22:18:27 +10:00
Aiden
13586c611a Start up settle 2026-05-12 22:04:46 +10:00
Aiden
3a83d9617f Clock updates 2026-05-12 21:44:26 +10:00
Aiden
5c66cfdc64 Input telemetry
All checks were successful
CI / React UI Build (push) Successful in 11s
CI / Native Windows Build And Tests (push) Successful in 2m56s
CI / Windows Release Package (push) Has been skipped
2026-05-12 21:13:22 +10:00
Aiden
d72272b5a8 2 frame buffer
All checks were successful
CI / React UI Build (push) Successful in 11s
CI / Native Windows Build And Tests (push) Successful in 2m59s
CI / Windows Release Package (push) Has been skipped
2026-05-12 21:08:02 +10:00
Aiden
c25ae7b25b input buffer
Some checks failed
CI / React UI Build (push) Successful in 11s
CI / Windows Release Package (push) Has been cancelled
CI / Native Windows Build And Tests (push) Has been cancelled
2026-05-12 21:05:42 +10:00
Aiden
a39be6fb20 Alignment
All checks were successful
CI / React UI Build (push) Successful in 10s
CI / Native Windows Build And Tests (push) Successful in 2m59s
CI / Windows Release Package (push) Has been skipped
2026-05-12 20:38:26 +10:00
Aiden
0a1fe440d9 docs pass 2026-05-12 20:32:32 +10:00
Aiden
3e45bba54b Update InputFrameMailbox.cpp
All checks were successful
CI / React UI Build (push) Successful in 11s
CI / Native Windows Build And Tests (push) Successful in 2m59s
CI / Windows Release Package (push) Has been skipped
2026-05-12 20:30:19 +10:00
Aiden
fd4b70ec9c Input GPU decoding
All checks were successful
CI / React UI Build (push) Successful in 11s
CI / Native Windows Build And Tests (push) Successful in 3m4s
CI / Windows Release Package (push) Has been skipped
2026-05-12 20:26:03 +10:00
Aiden
ce28904891 input testing
All checks were successful
CI / React UI Build (push) Successful in 11s
CI / Native Windows Build And Tests (push) Successful in 3m2s
CI / Windows Release Package (push) Has been skipped
2026-05-12 20:06:23 +10:00
Aiden
2c5e925b97 Video input fallback
All checks were successful
CI / React UI Build (push) Successful in 11s
CI / Native Windows Build And Tests (push) Successful in 3m7s
CI / Windows Release Package (push) Has been skipped
2026-05-12 18:53:46 +10:00
Aiden
957c0be05a Draw video if everything bypassed 2026-05-12 18:41:48 +10:00
Aiden
0a8b335048 INput
All checks were successful
CI / React UI Build (push) Successful in 10s
CI / Native Windows Build And Tests (push) Successful in 2m59s
CI / Windows Release Package (push) Has been skipped
2026-05-12 18:39:08 +10:00
Aiden
6e32941675 Fixed trigger
All checks were successful
CI / React UI Build (push) Successful in 10s
CI / Native Windows Build And Tests (push) Successful in 2m57s
CI / Windows Release Package (push) Has been skipped
2026-05-12 18:11:43 +10:00
Aiden
5fb4607d8c Clean up
All checks were successful
CI / React UI Build (push) Successful in 10s
CI / Native Windows Build And Tests (push) Successful in 2m55s
CI / Windows Release Package (push) Has been skipped
2026-05-12 18:03:54 +10:00
Aiden
f43b6f6519 shader control
All checks were successful
CI / React UI Build (push) Successful in 11s
CI / Native Windows Build And Tests (push) Successful in 3m3s
CI / Windows Release Package (push) Has been skipped
2026-05-12 17:52:55 +10:00
Aiden
dfd49fd0e3 Multipass shaders 2026-05-12 17:08:35 +10:00
Aiden
1429b2e660 Update shader
All checks were successful
CI / React UI Build (push) Successful in 11s
CI / Native Windows Build And Tests (push) Successful in 3m0s
CI / Windows Release Package (push) Has been skipped
2026-05-12 16:52:15 +10:00
Aiden
02b221f481 restructure 2026-05-12 15:47:59 +10:00
Aiden
6a33bd02ab Websocket split
All checks were successful
CI / React UI Build (push) Successful in 10s
CI / Native Windows Build And Tests (push) Successful in 3m0s
CI / Windows Release Package (push) Has been skipped
2026-05-12 15:36:02 +10:00
Aiden
da7e1a93f6 Websockets
All checks were successful
CI / React UI Build (push) Successful in 11s
CI / Native Windows Build And Tests (push) Successful in 2m58s
CI / Windows Release Package (push) Has been skipped
2026-05-12 15:32:01 +10:00
Aiden
334693f28c Render udpates 2026-05-12 15:26:02 +10:00
Aiden
c5fd8e72b4 non-blocking http
All checks were successful
CI / React UI Build (push) Successful in 11s
CI / Native Windows Build And Tests (push) Successful in 2m58s
CI / Windows Release Package (push) Has been skipped
2026-05-12 15:05:54 +10:00
Aiden
95b4a54326 Seperation 2026-05-12 14:57:18 +10:00
90 changed files with 5649 additions and 822 deletions

View File

@@ -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"
@@ -307,9 +309,20 @@ set(RENDER_CADENCE_APP_SOURCES
"${RENDER_CADENCE_APP_DIR}/app/AppConfigProvider.cpp" "${RENDER_CADENCE_APP_DIR}/app/AppConfigProvider.cpp"
"${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}/control/HttpControlServer.cpp" "${RENDER_CADENCE_APP_DIR}/app/RuntimeLayerController.cpp"
"${RENDER_CADENCE_APP_DIR}/control/HttpControlServer.h" "${RENDER_CADENCE_APP_DIR}/app/RuntimeLayerControllerBuild.cpp"
"${RENDER_CADENCE_APP_DIR}/app/RuntimeLayerControllerControls.cpp"
"${RENDER_CADENCE_APP_DIR}/app/RuntimeLayerController.h"
"${RENDER_CADENCE_APP_DIR}/control/ControlActionResult.h"
"${RENDER_CADENCE_APP_DIR}/control/RuntimeControlCommand.cpp"
"${RENDER_CADENCE_APP_DIR}/control/RuntimeControlCommand.h"
"${RENDER_CADENCE_APP_DIR}/control/http/HttpControlServer.cpp"
"${RENDER_CADENCE_APP_DIR}/control/http/HttpControlServer.h"
"${RENDER_CADENCE_APP_DIR}/control/http/HttpControlServerRoutes.cpp"
"${RENDER_CADENCE_APP_DIR}/control/http/HttpControlServerWebSocket.cpp"
"${RENDER_CADENCE_APP_DIR}/control/RuntimeStateJson.h" "${RENDER_CADENCE_APP_DIR}/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"
@@ -319,20 +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/runtime/RuntimeRenderSceneRender.cpp"
"${RENDER_CADENCE_APP_DIR}/render/runtime/RuntimeShaderPrepareWorker.cpp"
"${RENDER_CADENCE_APP_DIR}/render/runtime/RuntimeShaderPrepareWorker.h"
"${RENDER_CADENCE_APP_DIR}/render/runtime/RuntimeShaderProgram.h"
"${RENDER_CADENCE_APP_DIR}/render/SimpleMotionRenderer.cpp" "${RENDER_CADENCE_APP_DIR}/render/SimpleMotionRenderer.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"
@@ -346,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"
@@ -365,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"
@@ -776,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"
@@ -808,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"
) )
@@ -816,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"
) )
@@ -830,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"
) )
@@ -898,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"
@@ -929,14 +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/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"
) )

View File

@@ -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

View File

@@ -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;

View File

@@ -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; }

View File

@@ -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);
} }

View File

@@ -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;
}; };

View File

@@ -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,51 +35,116 @@ 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
- render-thread-owned runtime render scene for ready shader layers - render-thread-owned runtime render scene for ready shader layers
- render-thread-only GL commit once the artifact is ready - shared-context GL prepare worker for runtime shader program compile/link
- render-thread-only GL program swap once a prepared program is ready
- manifest-driven stateless single-pass shader packages - manifest-driven stateless 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
@@ -149,12 +223,13 @@ Current endpoints:
- `GET /` and UI asset paths: serve the bundled control UI from `ui/dist` - `GET /` and UI asset paths: serve the bundled control UI from `ui/dist`
- `GET /api/state`: returns OpenAPI-shaped display data with cadence telemetry, supported shaders, output status, and a read-only current runtime layer - `GET /api/state`: returns OpenAPI-shaped display data with cadence telemetry, supported shaders, output status, and a read-only current runtime layer
- `GET /ws`: upgrades to a WebSocket and streams state snapshots when they change
- `GET /docs/openapi.yaml` and `GET /openapi.yaml`: serves the OpenAPI document - `GET /docs/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
@@ -163,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
@@ -171,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
@@ -186,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
@@ -197,23 +332,39 @@ Healthy first-run signs:
On startup the app begins compiling the selected shader package on a background thread owned by the app orchestration layer. The default is `shaders/happy-accident`. On startup the app begins compiling the selected shader package on a background thread owned by the app orchestration layer. The default is `shaders/happy-accident`.
The render thread keeps drawing the simple motion renderer while Slang compiles. It does not choose packages, launch Slang, or track build lifecycle. It only receives a completed shader artifact and attempts the OpenGL shader compile/link at a frame boundary. If either the Slang build or GL commit fails, the app keeps rendering the 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, commits/removes GL programs, and renders the 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:
@@ -244,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
@@ -262,10 +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/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
@@ -282,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

View File

@@ -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;

View File

@@ -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"

View File

@@ -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;

View File

@@ -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);

View File

@@ -2,21 +2,17 @@
#include "AppConfig.h" #include "AppConfig.h"
#include "AppConfigProvider.h" #include "AppConfigProvider.h"
#include "RuntimeLayerController.h"
#include "../logging/Logger.h" #include "../logging/Logger.h"
#include "../runtime/RuntimeLayerModel.h"
#include "../runtime/RuntimeShaderBridge.h"
#include "../runtime/SupportedShaderCatalog.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 "RuntimeJson.h"
#include <chrono> #include <chrono>
#include <filesystem> #include <filesystem>
#include <map> #include <functional>
#include <memory>
#include <mutex>
#include <string> #include <string>
#include <thread> #include <thread>
#include <type_traits> #include <type_traits>
@@ -60,7 +56,10 @@ public:
mFrameExchange(frameExchange), mFrameExchange(frameExchange),
mConfig(config), mConfig(config),
mOutputThread(mOutput, mFrameExchange, mConfig.outputThread), mOutputThread(mOutput, mFrameExchange, mConfig.outputThread),
mTelemetryHealth(mConfig.telemetry) mTelemetryHealth(mConfig.telemetry),
mRuntimeLayers([this](const std::vector<RuntimeRenderLayerModel>& layers) {
mRenderThread.SubmitRuntimeRenderLayers(layers);
})
{ {
} }
@@ -74,8 +73,10 @@ public:
bool Start(std::string& error) bool Start(std::string& error)
{ {
LoadSupportedShaderCatalog(); mRuntimeLayers.Initialize(
InitializeRuntimeLayerModel(); mConfig.shaderLibrary,
static_cast<unsigned>(mConfig.maxTemporalHistoryFrames),
mConfig.runtimeShaderId);
Log("app", "Starting render thread."); Log("app", "Starting render thread.");
if (!detail::StartRenderThread(mRenderThread, error, 0)) if (!detail::StartRenderThread(mRenderThread, error, 0))
@@ -84,12 +85,10 @@ public:
Stop(); Stop();
return false; return false;
} }
StartRuntimeShaderBuild(); 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;
@@ -109,7 +108,7 @@ public:
mTelemetryHealth.Stop(); mTelemetryHealth.Stop();
mOutputThread.Stop(); mOutputThread.Stop();
mOutput.Stop(); mOutput.Stop();
StopRuntimeShaderBuild(); mRuntimeLayers.Stop();
mRenderThread.Stop(); mRenderThread.Stop();
mOutput.ReleaseResources(); mOutput.ReleaseResources();
if (mStarted) if (mStarted)
@@ -119,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()
@@ -160,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)
@@ -180,10 +202,17 @@ private:
return BuildStateJson(); return BuildStateJson();
}; };
callbacks.addLayer = [this](const std::string& body) { callbacks.addLayer = [this](const std::string& body) {
return HandleAddLayer(body); return mRuntimeLayers.HandleAddLayer(body);
}; };
callbacks.removeLayer = [this](const std::string& body) { callbacks.removeLayer = [this](const std::string& body) {
return 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;
@@ -202,18 +231,36 @@ private:
std::string BuildStateJson() std::string BuildStateJson()
{ {
CadenceTelemetrySnapshot telemetry = mHttpTelemetry.Sample(mFrameExchange, mOutput, mOutputThread, mRenderThread); CadenceTelemetrySnapshot telemetry = mHttpTelemetry.Sample(mFrameExchange, mOutput, mOutputThread, mRenderThread);
RuntimeLayerModelSnapshot layerSnapshot = CopyRuntimeLayerSnapshot(telemetry); ApplyDeckLinkInputMetrics(telemetry);
RuntimeLayerModelSnapshot layerSnapshot = mRuntimeLayers.Snapshot(telemetry);
return RuntimeStateToJson(RuntimeStateJsonInput{ return RuntimeStateToJson(RuntimeStateJsonInput{
mConfig, mConfig,
telemetry, telemetry,
mHttpServer.Port(), mHttpServer.Port(),
mVideoOutputEnabled, mVideoOutputEnabled,
mVideoOutputStatus, mVideoOutputStatus,
mShaderCatalog, mRuntimeLayers.ShaderCatalog(),
layerSnapshot layerSnapshot
}); });
} }
void ApplyDeckLinkInputMetrics(CadenceTelemetrySnapshot& telemetry)
{
if (!mDeckLinkInputMetricsProvider)
return;
const DeckLinkInputMetrics inputMetrics = mDeckLinkInputMetricsProvider();
telemetry.inputConvertMilliseconds = inputMetrics.convertMilliseconds;
telemetry.inputSubmitMilliseconds = inputMetrics.submitMilliseconds;
telemetry.inputNoSignalFrames = inputMetrics.noInputSourceFrames;
telemetry.inputUnsupportedFrames = inputMetrics.unsupportedFrames;
telemetry.inputSubmitMisses = inputMetrics.submitMisses;
telemetry.inputCaptureFormat = inputMetrics.captureFormat ? inputMetrics.captureFormat : "none";
if (telemetry.sampleSeconds > 0.0)
telemetry.inputCaptureFps = static_cast<double>(inputMetrics.capturedFrames - mLastInputCapturedFrames) / telemetry.sampleSeconds;
mLastInputCapturedFrames = inputMetrics.capturedFrames;
}
bool WaitForPreroll() const bool WaitForPreroll() const
{ {
const auto deadline = std::chrono::steady_clock::now() + mConfig.prerollTimeout; const auto deadline = std::chrono::steady_clock::now() + mConfig.prerollTimeout;
@@ -226,230 +273,6 @@ private:
return false; return false;
} }
void StartRuntimeShaderBuild()
{
if (mConfig.runtimeShaderId.empty())
{
Log("runtime-shader", "Runtime shader build disabled.");
return;
}
Log("runtime-shader", "Starting background Slang build for shader '" + mConfig.runtimeShaderId + "'.");
const std::string layerId = FirstRuntimeLayerId();
if (!layerId.empty())
StartLayerShaderBuild(layerId, mConfig.runtimeShaderId);
}
void LoadSupportedShaderCatalog()
{
const std::filesystem::path shaderRoot = FindRepoPath(mConfig.shaderLibrary);
std::string error;
if (!mShaderCatalog.Load(shaderRoot, static_cast<unsigned>(mConfig.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 InitializeRuntimeLayerModel()
{
std::lock_guard<std::mutex> lock(mRuntimeLayerMutex);
std::string error;
if (!mRuntimeLayerModel.InitializeSingleLayer(mShaderCatalog, mConfig.runtimeShaderId, error))
{
LogWarning("runtime-shader", error + " Runtime shader build disabled.");
mConfig.runtimeShaderId.clear();
mRuntimeLayerModel.Clear();
}
}
void StopRuntimeShaderBuild()
{
StopAllRuntimeShaderBuilds();
}
void MarkRuntimeBuildStarted(const std::string& message)
{
std::lock_guard<std::mutex> lock(mRuntimeLayerMutex);
std::string error;
const std::string layerId = mRuntimeLayerModel.FirstLayerId();
if (!layerId.empty())
mRuntimeLayerModel.MarkBuildStarted(layerId, message, error);
}
bool MarkRuntimeBuildReady(const RuntimeShaderArtifact& artifact)
{
std::lock_guard<std::mutex> lock(mRuntimeLayerMutex);
std::string error;
if (!mRuntimeLayerModel.MarkBuildReady(artifact, error))
{
LogWarning("runtime-shader", error);
return false;
}
return true;
}
void MarkRuntimeBuildFailed(const std::string& message)
{
std::lock_guard<std::mutex> lock(mRuntimeLayerMutex);
if (!mRuntimeLayerModel.MarkBuildFailedForShader(mConfig.runtimeShaderId, message))
LogWarning("runtime-shader", "Runtime shader failed without a matching display layer: " + message);
}
void MarkRuntimeBuildFailedForLayer(const std::string& layerId, const std::string& message)
{
std::lock_guard<std::mutex> lock(mRuntimeLayerMutex);
std::string error;
if (!mRuntimeLayerModel.MarkBuildFailed(layerId, message, error))
LogWarning("runtime-shader", error);
}
std::string FirstRuntimeLayerId() const
{
std::lock_guard<std::mutex> lock(mRuntimeLayerMutex);
return mRuntimeLayerModel.FirstLayerId();
}
void StartLayerShaderBuild(const std::string& layerId, const std::string& shaderId)
{
{
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);
auto existingIt = mShaderBuilds.find(layerId);
if (existingIt != mShaderBuilds.end())
existingIt->second->Stop();
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 StopLayerShaderBuild(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->Stop();
}
void StopAllRuntimeShaderBuilds()
{
std::map<std::string, std::unique_ptr<RuntimeShaderBridge>> builds;
{
std::lock_guard<std::mutex> lock(mShaderBuildMutex);
builds.swap(mShaderBuilds);
}
for (auto& entry : builds)
entry.second->Stop();
}
ControlActionResult HandleAddLayer(const std::string& body)
{
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 HandleRemoveLayer(const std::string& body)
{
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);
StopLayerShaderBuild(layerId);
PublishRuntimeRenderLayers();
return { true, std::string() };
}
void PublishRuntimeRenderLayers()
{
std::vector<RuntimeRenderLayerModel> renderLayers;
{
std::lock_guard<std::mutex> lock(mRuntimeLayerMutex);
renderLayers = mRuntimeLayerModel.Snapshot().renderLayers;
}
mRenderThread.SubmitRuntimeRenderLayers(renderLayers);
}
static bool 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;
}
RuntimeLayerModelSnapshot CopyRuntimeLayerSnapshot(const CadenceTelemetrySnapshot& telemetry) const
{
std::lock_guard<std::mutex> lock(mRuntimeLayerMutex);
RuntimeLayerModelSnapshot snapshot = mRuntimeLayerModel.Snapshot();
if (telemetry.shaderBuildFailures > 0)
{
snapshot.compileSucceeded = false;
snapshot.compileMessage = "Runtime shader GL commit failed; see logs for details.";
}
return snapshot;
}
RenderThread& mRenderThread; RenderThread& mRenderThread;
SystemFrameExchange& mFrameExchange; SystemFrameExchange& mFrameExchange;
AppConfig mConfig; AppConfig mConfig;
@@ -458,11 +281,9 @@ private:
TelemetryHealthMonitor mTelemetryHealth; TelemetryHealthMonitor mTelemetryHealth;
CadenceTelemetry mHttpTelemetry; CadenceTelemetry mHttpTelemetry;
HttpControlServer mHttpServer; HttpControlServer mHttpServer;
SupportedShaderCatalog mShaderCatalog; RuntimeLayerController mRuntimeLayers;
mutable std::mutex mRuntimeLayerMutex; std::function<DeckLinkInputMetrics()> mDeckLinkInputMetricsProvider;
RuntimeLayerModel mRuntimeLayerModel; uint64_t mLastInputCapturedFrames = 0;
std::mutex mShaderBuildMutex;
std::map<std::string, std::unique_ptr<RuntimeShaderBridge>> mShaderBuilds;
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.";

View File

@@ -0,0 +1,92 @@
#include "RuntimeLayerController.h"
#include "../logging/Logger.h"
namespace RenderCadenceCompositor
{
RuntimeLayerController::RuntimeLayerController(RenderLayerPublisher publisher) :
mPublisher(std::move(publisher))
{
}
RuntimeLayerController::~RuntimeLayerController()
{
Stop();
}
void RuntimeLayerController::SetPublisher(RenderLayerPublisher publisher)
{
mPublisher = std::move(publisher);
}
void RuntimeLayerController::Initialize(const std::string& shaderLibrary, unsigned maxTemporalHistoryFrames, std::string& runtimeShaderId)
{
LoadSupportedShaderCatalog(shaderLibrary, maxTemporalHistoryFrames);
InitializeLayerModel(runtimeShaderId);
}
void RuntimeLayerController::StartStartupBuild(const std::string& runtimeShaderId)
{
if (runtimeShaderId.empty())
{
Log("runtime-shader", "Runtime shader build disabled.");
return;
}
Log("runtime-shader", "Starting background Slang build for shader '" + runtimeShaderId + "'.");
const std::string layerId = FirstRuntimeLayerId();
if (!layerId.empty())
StartLayerShaderBuild(layerId, runtimeShaderId);
}
void RuntimeLayerController::Stop()
{
StopAllRuntimeShaderBuilds();
}
RuntimeLayerModelSnapshot RuntimeLayerController::Snapshot(const CadenceTelemetrySnapshot& telemetry) const
{
std::lock_guard<std::mutex> lock(mRuntimeLayerMutex);
RuntimeLayerModelSnapshot snapshot = mRuntimeLayerModel.Snapshot();
if (telemetry.shaderBuildFailures > 0)
{
snapshot.compileSucceeded = false;
snapshot.compileMessage = "Runtime shader GL commit failed; see logs for details.";
}
return snapshot;
}
void RuntimeLayerController::PublishRuntimeRenderLayers()
{
if (!mPublisher)
return;
std::vector<RuntimeRenderLayerModel> renderLayers;
{
std::lock_guard<std::mutex> lock(mRuntimeLayerMutex);
renderLayers = mRuntimeLayerModel.Snapshot().renderLayers;
}
mPublisher(renderLayers);
}
bool RuntimeLayerController::MarkRuntimeBuildReady(const RuntimeShaderArtifact& artifact)
{
std::lock_guard<std::mutex> lock(mRuntimeLayerMutex);
std::string error;
if (!mRuntimeLayerModel.MarkBuildReady(artifact, error))
{
LogWarning("runtime-shader", error);
return false;
}
return true;
}
void RuntimeLayerController::MarkRuntimeBuildFailedForLayer(const std::string& layerId, const std::string& message)
{
std::lock_guard<std::mutex> lock(mRuntimeLayerMutex);
std::string error;
if (!mRuntimeLayerModel.MarkBuildFailed(layerId, message, error))
LogWarning("runtime-shader", error);
}
}

View File

@@ -0,0 +1,63 @@
#pragma once
#include "../control/ControlActionResult.h"
#include "../control/RuntimeControlCommand.h"
#include "../runtime/RuntimeLayerModel.h"
#include "../runtime/RuntimeShaderBridge.h"
#include "../runtime/SupportedShaderCatalog.h"
#include "../telemetry/CadenceTelemetry.h"
#include <functional>
#include <map>
#include <memory>
#include <mutex>
#include <string>
#include <vector>
namespace RenderCadenceCompositor
{
class RuntimeLayerController
{
public:
using RenderLayerPublisher = std::function<void(const std::vector<RuntimeRenderLayerModel>&)>;
explicit RuntimeLayerController(RenderLayerPublisher publisher = RenderLayerPublisher());
RuntimeLayerController(const RuntimeLayerController&) = delete;
RuntimeLayerController& operator=(const RuntimeLayerController&) = delete;
~RuntimeLayerController();
void SetPublisher(RenderLayerPublisher publisher);
void Initialize(const std::string& shaderLibrary, unsigned maxTemporalHistoryFrames, std::string& runtimeShaderId);
void StartStartupBuild(const std::string& runtimeShaderId);
void Stop();
ControlActionResult HandleAddLayer(const std::string& body);
ControlActionResult HandleRemoveLayer(const std::string& body);
ControlActionResult HandleControlCommand(const RuntimeControlCommand& command);
RuntimeLayerModelSnapshot Snapshot(const CadenceTelemetrySnapshot& telemetry) const;
const SupportedShaderCatalog& ShaderCatalog() const { return mShaderCatalog; }
private:
void LoadSupportedShaderCatalog(const std::string& shaderLibrary, unsigned maxTemporalHistoryFrames);
void InitializeLayerModel(std::string& runtimeShaderId);
void StartLayerShaderBuild(const std::string& layerId, const std::string& shaderId);
void RetireLayerShaderBuild(const std::string& layerId);
void CleanupRetiredShaderBuilds();
void StopAllRuntimeShaderBuilds();
void PublishRuntimeRenderLayers();
bool MarkRuntimeBuildReady(const RuntimeShaderArtifact& artifact);
void MarkRuntimeBuildFailedForLayer(const std::string& layerId, const std::string& message);
std::string FirstRuntimeLayerId() const;
static bool ExtractStringField(const std::string& body, const char* fieldName, std::string& value, std::string& error);
RenderLayerPublisher mPublisher;
SupportedShaderCatalog mShaderCatalog;
mutable std::mutex mRuntimeLayerMutex;
RuntimeLayerModel mRuntimeLayerModel;
std::mutex mShaderBuildMutex;
std::map<std::string, std::unique_ptr<RuntimeShaderBridge>> mShaderBuilds;
std::vector<std::unique_ptr<RuntimeShaderBridge>> mRetiredShaderBuilds;
};
}

View 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();
}
}

View File

@@ -0,0 +1,160 @@
#include "RuntimeLayerController.h"
#include "RuntimeJson.h"
#include "../logging/Logger.h"
namespace RenderCadenceCompositor
{
ControlActionResult RuntimeLayerController::HandleAddLayer(const std::string& body)
{
CleanupRetiredShaderBuilds();
std::string shaderId;
std::string error;
if (!ExtractStringField(body, "shaderId", shaderId, error))
return { false, error };
std::string layerId;
{
std::lock_guard<std::mutex> lock(mRuntimeLayerMutex);
if (!mRuntimeLayerModel.AddLayer(mShaderCatalog, shaderId, layerId, error))
return { false, error };
}
Log("runtime-shader", "Layer added: " + layerId + " shader=" + shaderId);
StartLayerShaderBuild(layerId, shaderId);
return { true, std::string() };
}
ControlActionResult RuntimeLayerController::HandleRemoveLayer(const std::string& body)
{
CleanupRetiredShaderBuilds();
std::string layerId;
std::string error;
if (!ExtractStringField(body, "layerId", layerId, error))
return { false, error };
{
std::lock_guard<std::mutex> lock(mRuntimeLayerMutex);
if (!mRuntimeLayerModel.RemoveLayer(layerId, error))
return { false, error };
}
Log("runtime-shader", "Layer removed: " + layerId);
RetireLayerShaderBuild(layerId);
PublishRuntimeRenderLayers();
return { true, std::string() };
}
ControlActionResult RuntimeLayerController::HandleControlCommand(const RuntimeControlCommand& command)
{
CleanupRetiredShaderBuilds();
std::string error;
switch (command.type)
{
case RuntimeControlCommandType::AddLayer:
{
std::string layerId;
{
std::lock_guard<std::mutex> lock(mRuntimeLayerMutex);
if (!mRuntimeLayerModel.AddLayer(mShaderCatalog, command.shaderId, layerId, error))
return { false, error };
}
Log("runtime-shader", "Layer added: " + layerId + " shader=" + command.shaderId);
StartLayerShaderBuild(layerId, command.shaderId);
return { true, std::string() };
}
case RuntimeControlCommandType::RemoveLayer:
{
{
std::lock_guard<std::mutex> lock(mRuntimeLayerMutex);
if (!mRuntimeLayerModel.RemoveLayer(command.layerId, error))
return { false, error };
}
Log("runtime-shader", "Layer removed: " + command.layerId);
RetireLayerShaderBuild(command.layerId);
PublishRuntimeRenderLayers();
return { true, std::string() };
}
case RuntimeControlCommandType::ReorderLayer:
{
{
std::lock_guard<std::mutex> lock(mRuntimeLayerMutex);
if (!mRuntimeLayerModel.ReorderLayer(command.layerId, command.targetIndex, error))
return { false, error };
}
PublishRuntimeRenderLayers();
return { true, std::string() };
}
case RuntimeControlCommandType::SetLayerBypass:
{
{
std::lock_guard<std::mutex> lock(mRuntimeLayerMutex);
if (!mRuntimeLayerModel.SetLayerBypass(command.layerId, command.bypass, error))
return { false, error };
}
PublishRuntimeRenderLayers();
return { true, std::string() };
}
case RuntimeControlCommandType::SetLayerShader:
{
{
std::lock_guard<std::mutex> lock(mRuntimeLayerMutex);
if (!mRuntimeLayerModel.SetLayerShader(mShaderCatalog, command.layerId, command.shaderId, error))
return { false, error };
}
Log("runtime-shader", "Layer shader changed: " + command.layerId + " shader=" + command.shaderId);
StartLayerShaderBuild(command.layerId, command.shaderId);
return { true, std::string() };
}
case RuntimeControlCommandType::UpdateLayerParameter:
{
{
std::lock_guard<std::mutex> lock(mRuntimeLayerMutex);
if (!mRuntimeLayerModel.UpdateParameter(command.layerId, command.parameterId, command.value, error))
return { false, error };
}
PublishRuntimeRenderLayers();
return { true, std::string() };
}
case RuntimeControlCommandType::ResetLayerParameters:
{
{
std::lock_guard<std::mutex> lock(mRuntimeLayerMutex);
if (!mRuntimeLayerModel.ResetParameters(command.layerId, error))
return { false, error };
}
PublishRuntimeRenderLayers();
return { true, std::string() };
}
case RuntimeControlCommandType::Unsupported:
break;
}
return { false, "Unsupported runtime control command." };
}
bool RuntimeLayerController::ExtractStringField(const std::string& body, const char* fieldName, std::string& value, std::string& error)
{
JsonValue root;
std::string parseError;
if (!ParseJson(body.empty() ? "{}" : body, root, parseError) || !root.isObject())
{
error = parseError.empty() ? "Request body must be a JSON object." : parseError;
return false;
}
const JsonValue* field = root.find(fieldName);
if (!field || !field->isString() || field->asString().empty())
{
error = std::string("Request field '") + fieldName + "' must be a non-empty string.";
return false;
}
value = field->asString();
error.clear();
return true;
}
}

View File

@@ -0,0 +1,12 @@
#pragma once
#include <string>
namespace RenderCadenceCompositor
{
struct ControlActionResult
{
bool ok = false;
std::string error;
};
}

View 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;
}
}

View 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);
}

View File

@@ -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,6 +157,9 @@ 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");
if (value)
WriteParameterValue(writer, parameter, *value);
else
WriteDefaultParameterValue(writer, parameter); 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");

View File

@@ -1,13 +1,10 @@
#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>
namespace RenderCadenceCompositor namespace RenderCadenceCompositor
@@ -26,21 +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) :
@@ -157,6 +139,20 @@ void HttpControlServer::Stop()
if (mThread.joinable()) if (mThread.joinable())
mThread.join(); mThread.join();
std::vector<std::thread> clientThreads;
{
std::lock_guard<std::mutex> lock(mClientThreadsMutex);
clientThreads.swap(mClientThreads);
for (std::thread& thread : mFinishedClientThreads)
clientThreads.push_back(std::move(thread));
mFinishedClientThreads.clear();
}
for (std::thread& thread : clientThreads)
{
if (thread.joinable())
thread.join();
}
if (mWinsockStarted) if (mWinsockStarted)
{ {
WSACleanup(); WSACleanup();
@@ -185,6 +181,7 @@ void HttpControlServer::ThreadMain()
{ {
while (mRunning.load(std::memory_order_acquire)) while (mRunning.load(std::memory_order_acquire))
{ {
JoinFinishedClientThreads();
TryAcceptClient(); TryAcceptClient();
std::this_thread::sleep_for(mConfig.idleSleep); std::this_thread::sleep_for(mConfig.idleSleep);
} }
@@ -212,6 +209,9 @@ bool HttpControlServer::HandleClient(UniqueSocket clientSocket)
if (!ParseHttpRequest(std::string(buffer, buffer + received), request)) if (!ParseHttpRequest(std::string(buffer, buffer + received), request))
return SendResponse(clientSocket.get(), TextResponse("400 Bad Request", "Bad Request")); return SendResponse(clientSocket.get(), TextResponse("400 Bad Request", "Bad Request"));
if (request.path == "/ws")
return HandleWebSocketClient(std::move(clientSocket), request);
return SendResponse(clientSocket.get(), RouteRequest(request)); return SendResponse(clientSocket.get(), RouteRequest(request));
} }
@@ -240,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");

View File

@@ -1,5 +1,7 @@
#pragma once #pragma once
#include "ControlActionResult.h"
#include <winsock2.h> #include <winsock2.h>
#include <atomic> #include <atomic>
@@ -7,8 +9,10 @@
#include <filesystem> #include <filesystem>
#include <functional> #include <functional>
#include <map> #include <map>
#include <mutex>
#include <string> #include <string>
#include <thread> #include <thread>
#include <vector>
namespace RenderCadenceCompositor namespace RenderCadenceCompositor
{ {
@@ -19,17 +23,12 @@ struct HttpControlServerConfig
std::chrono::milliseconds idleSleep = std::chrono::milliseconds(10); std::chrono::milliseconds idleSleep = std::chrono::milliseconds(10);
}; };
struct ControlActionResult
{
bool ok = false;
std::string error;
};
struct HttpControlServerCallbacks 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
@@ -93,11 +92,15 @@ public:
HttpResponse RouteRequestForTest(const HttpRequest& request) const; HttpResponse RouteRequestForTest(const HttpRequest& request) const;
static bool ParseHttpRequest(const std::string& rawRequest, HttpRequest& request); static bool ParseHttpRequest(const std::string& rawRequest, HttpRequest& request);
static std::string WebSocketAcceptKey(const std::string& clientKey);
private: private:
void ThreadMain(); void ThreadMain();
bool TryAcceptClient(); bool TryAcceptClient();
bool HandleClient(UniqueSocket clientSocket); bool HandleClient(UniqueSocket clientSocket);
bool HandleWebSocketClient(UniqueSocket clientSocket, const HttpRequest& request);
void WebSocketClientMain(UniqueSocket clientSocket);
void JoinFinishedClientThreads();
bool SendResponse(SOCKET clientSocket, const HttpResponse& response) const; bool SendResponse(SOCKET clientSocket, const HttpResponse& response) const;
HttpResponse RouteRequest(const HttpRequest& request) const; HttpResponse RouteRequest(const HttpRequest& request) const;
HttpResponse ServeGet(const HttpRequest& request) const; HttpResponse ServeGet(const HttpRequest& request) const;
@@ -111,6 +114,7 @@ private:
static HttpResponse TextResponse(const std::string& status, const std::string& body); static HttpResponse TextResponse(const std::string& status, const std::string& body);
static HttpResponse HtmlResponse(const std::string& status, const std::string& body); static HttpResponse HtmlResponse(const std::string& status, const std::string& body);
static std::string ActionResponse(bool ok, const std::string& error = std::string()); static std::string ActionResponse(bool ok, const std::string& error = std::string());
static bool SendWebSocketText(SOCKET clientSocket, const std::string& text);
static std::string GuessContentType(const std::filesystem::path& path); static std::string GuessContentType(const std::filesystem::path& path);
static bool IsSafeRelativePath(const std::filesystem::path& path); static bool IsSafeRelativePath(const std::filesystem::path& path);
static std::string ToLower(std::string text); static std::string ToLower(std::string text);
@@ -121,6 +125,9 @@ private:
HttpControlServerCallbacks mCallbacks; HttpControlServerCallbacks mCallbacks;
UniqueSocket mListenSocket; UniqueSocket mListenSocket;
std::thread mThread; std::thread mThread;
std::mutex mClientThreadsMutex;
std::vector<std::thread> mClientThreads;
std::vector<std::thread> mFinishedClientThreads;
std::atomic<bool> mRunning{ false }; std::atomic<bool> mRunning{ false };
unsigned short mPort = 0; unsigned short mPort = 0;
bool mWinsockStarted = false; bool mWinsockStarted = false;

View File

@@ -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;
}
}

View File

@@ -0,0 +1,248 @@
#include "HttpControlServer.h"
#include <array>
#include <cstdint>
#include <sstream>
#include <utility>
#include <vector>
namespace RenderCadenceCompositor
{
namespace
{
std::array<uint8_t, 20> Sha1(const std::string& input)
{
auto leftRotate = [](uint32_t value, uint32_t bits) {
return (value << bits) | (value >> (32U - bits));
};
std::vector<uint8_t> data(input.begin(), input.end());
const uint64_t bitLength = static_cast<uint64_t>(data.size()) * 8ULL;
data.push_back(0x80);
while ((data.size() % 64) != 56)
data.push_back(0);
for (int shift = 56; shift >= 0; shift -= 8)
data.push_back(static_cast<uint8_t>((bitLength >> shift) & 0xff));
uint32_t h0 = 0x67452301;
uint32_t h1 = 0xefcdab89;
uint32_t h2 = 0x98badcfe;
uint32_t h3 = 0x10325476;
uint32_t h4 = 0xc3d2e1f0;
for (std::size_t offset = 0; offset < data.size(); offset += 64)
{
uint32_t words[80] = {};
for (std::size_t i = 0; i < 16; ++i)
{
const std::size_t index = offset + i * 4;
words[i] = (static_cast<uint32_t>(data[index]) << 24)
| (static_cast<uint32_t>(data[index + 1]) << 16)
| (static_cast<uint32_t>(data[index + 2]) << 8)
| static_cast<uint32_t>(data[index + 3]);
}
for (std::size_t i = 16; i < 80; ++i)
words[i] = leftRotate(words[i - 3] ^ words[i - 8] ^ words[i - 14] ^ words[i - 16], 1);
uint32_t a = h0;
uint32_t b = h1;
uint32_t c = h2;
uint32_t d = h3;
uint32_t e = h4;
for (std::size_t i = 0; i < 80; ++i)
{
uint32_t f = 0;
uint32_t k = 0;
if (i < 20)
{
f = (b & c) | ((~b) & d);
k = 0x5a827999;
}
else if (i < 40)
{
f = b ^ c ^ d;
k = 0x6ed9eba1;
}
else if (i < 60)
{
f = (b & c) | (b & d) | (c & d);
k = 0x8f1bbcdc;
}
else
{
f = b ^ c ^ d;
k = 0xca62c1d6;
}
const uint32_t temp = leftRotate(a, 5) + f + e + k + words[i];
e = d;
d = c;
c = leftRotate(b, 30);
b = a;
a = temp;
}
h0 += a;
h1 += b;
h2 += c;
h3 += d;
h4 += e;
}
std::array<uint8_t, 20> digest = {};
const uint32_t parts[] = { h0, h1, h2, h3, h4 };
for (std::size_t i = 0; i < 5; ++i)
{
digest[i * 4] = static_cast<uint8_t>((parts[i] >> 24) & 0xff);
digest[i * 4 + 1] = static_cast<uint8_t>((parts[i] >> 16) & 0xff);
digest[i * 4 + 2] = static_cast<uint8_t>((parts[i] >> 8) & 0xff);
digest[i * 4 + 3] = static_cast<uint8_t>(parts[i] & 0xff);
}
return digest;
}
std::string Base64Encode(const uint8_t* data, std::size_t size)
{
static constexpr char kAlphabet[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
std::string output;
output.reserve(((size + 2) / 3) * 4);
for (std::size_t i = 0; i < size; i += 3)
{
const uint32_t a = data[i];
const uint32_t b = i + 1 < size ? data[i + 1] : 0;
const uint32_t c = i + 2 < size ? data[i + 2] : 0;
const uint32_t triple = (a << 16) | (b << 8) | c;
output.push_back(kAlphabet[(triple >> 18) & 0x3f]);
output.push_back(kAlphabet[(triple >> 12) & 0x3f]);
output.push_back(i + 1 < size ? kAlphabet[(triple >> 6) & 0x3f] : '=');
output.push_back(i + 2 < size ? kAlphabet[triple & 0x3f] : '=');
}
return output;
}
}
bool HttpControlServer::HandleWebSocketClient(UniqueSocket clientSocket, const HttpRequest& request)
{
const auto keyIt = request.headers.find("sec-websocket-key");
if (keyIt == request.headers.end() || keyIt->second.empty())
return SendResponse(clientSocket.get(), TextResponse("400 Bad Request", "Missing WebSocket key"));
std::ostringstream stream;
stream << "HTTP/1.1 101 Switching Protocols\r\n"
<< "Upgrade: websocket\r\n"
<< "Connection: Upgrade\r\n"
<< "Sec-WebSocket-Accept: " << WebSocketAcceptKey(keyIt->second) << "\r\n\r\n";
const std::string response = stream.str();
if (send(clientSocket.get(), response.c_str(), static_cast<int>(response.size()), 0) != static_cast<int>(response.size()))
return false;
u_long nonBlocking = 1;
ioctlsocket(clientSocket.get(), FIONBIO, &nonBlocking);
std::thread thread([this, socket = std::move(clientSocket)]() mutable {
WebSocketClientMain(std::move(socket));
});
{
std::lock_guard<std::mutex> lock(mClientThreadsMutex);
mClientThreads.push_back(std::move(thread));
}
return true;
}
void HttpControlServer::WebSocketClientMain(UniqueSocket clientSocket)
{
std::string previousState;
while (mRunning.load(std::memory_order_acquire))
{
const std::string state = mCallbacks.getStateJson ? mCallbacks.getStateJson() : "{}";
if (state != previousState)
{
if (!SendWebSocketText(clientSocket.get(), state))
break;
previousState = state;
}
std::this_thread::sleep_for(std::chrono::milliseconds(250));
}
std::lock_guard<std::mutex> lock(mClientThreadsMutex);
const std::thread::id currentId = std::this_thread::get_id();
for (auto it = mClientThreads.begin(); it != mClientThreads.end(); ++it)
{
if (it->get_id() != currentId)
continue;
mFinishedClientThreads.push_back(std::move(*it));
mClientThreads.erase(it);
break;
}
}
void HttpControlServer::JoinFinishedClientThreads()
{
std::vector<std::thread> finished;
{
std::lock_guard<std::mutex> lock(mClientThreadsMutex);
finished.swap(mFinishedClientThreads);
}
for (std::thread& thread : finished)
{
if (thread.joinable())
thread.join();
}
}
bool HttpControlServer::SendWebSocketText(SOCKET clientSocket, const std::string& text)
{
if (clientSocket == INVALID_SOCKET)
return false;
std::vector<unsigned char> frame;
frame.reserve(text.size() + 16);
frame.push_back(0x81);
if (text.size() <= 125)
{
frame.push_back(static_cast<unsigned char>(text.size()));
}
else if (text.size() <= 0xffff)
{
frame.push_back(126);
frame.push_back(static_cast<unsigned char>((text.size() >> 8) & 0xff));
frame.push_back(static_cast<unsigned char>(text.size() & 0xff));
}
else
{
frame.push_back(127);
const uint64_t length = static_cast<uint64_t>(text.size());
for (int shift = 56; shift >= 0; shift -= 8)
frame.push_back(static_cast<unsigned char>((length >> shift) & 0xff));
}
frame.insert(frame.end(), text.begin(), text.end());
const char* data = reinterpret_cast<const char*>(frame.data());
int remaining = static_cast<int>(frame.size());
while (remaining > 0)
{
const int sent = send(clientSocket, data, remaining, 0);
if (sent <= 0)
{
const int error = WSAGetLastError();
if (error == WSAEWOULDBLOCK)
{
std::this_thread::sleep_for(std::chrono::milliseconds(2));
continue;
}
return false;
}
data += sent;
remaining -= sent;
}
return true;
}
std::string HttpControlServer::WebSocketAcceptKey(const std::string& clientKey)
{
static constexpr const char* kWebSocketGuid = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
const std::array<uint8_t, 20> digest = Sha1(clientKey + kWebSocketGuid);
return Base64Encode(digest.data(), digest.size());
}
}

View 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);
}

View 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;
};

View File

@@ -46,14 +46,11 @@ 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(); frame = SystemFrame();
++mCounters.acquireMisses; ++mCounters.acquireMisses;
return false; return false;
} }
}
++mCounters.acquiredFrames; ++mCounters.acquiredFrames;
return true; return true;
@@ -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;

View File

@@ -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;

View File

@@ -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

View File

@@ -11,6 +11,11 @@ HiddenGlWindow::~HiddenGlWindow()
} }
bool HiddenGlWindow::Create(unsigned width, unsigned height, std::string& error) bool HiddenGlWindow::Create(unsigned width, unsigned height, std::string& error)
{
return CreateShared(width, height, nullptr, nullptr, error);
}
bool HiddenGlWindow::CreateShared(unsigned width, unsigned height, HDC sharedDeviceContext, HGLRC sharedContext, std::string& error)
{ {
Destroy(); Destroy();
@@ -63,7 +68,11 @@ bool HiddenGlWindow::Create(unsigned width, unsigned height, std::string& error)
pfd.cDepthBits = 0; pfd.cDepthBits = 0;
pfd.iLayerType = PFD_MAIN_PLANE; pfd.iLayerType = PFD_MAIN_PLANE;
const int pixelFormat = ChoosePixelFormat(mDc, &pfd); int pixelFormat = 0;
if (sharedDeviceContext != nullptr)
pixelFormat = GetPixelFormat(sharedDeviceContext);
if (pixelFormat == 0)
pixelFormat = ChoosePixelFormat(mDc, &pfd);
if (pixelFormat == 0 || !SetPixelFormat(mDc, pixelFormat, &pfd)) if (pixelFormat == 0 || !SetPixelFormat(mDc, pixelFormat, &pfd))
{ {
error = "Could not choose/set pixel format for hidden OpenGL window."; error = "Could not choose/set pixel format for hidden OpenGL window.";
@@ -76,6 +85,11 @@ bool HiddenGlWindow::Create(unsigned width, unsigned height, std::string& error)
error = "wglCreateContext failed for hidden OpenGL window."; error = "wglCreateContext failed for hidden OpenGL window.";
return false; return false;
} }
if (sharedContext != nullptr && wglShareLists(sharedContext, mGlrc) != TRUE)
{
error = "wglShareLists failed for hidden OpenGL shared context.";
return false;
}
return true; return true;
} }

View File

@@ -13,6 +13,7 @@ public:
~HiddenGlWindow(); ~HiddenGlWindow();
bool Create(unsigned width, unsigned height, std::string& error); bool Create(unsigned width, unsigned height, std::string& error);
bool CreateShared(unsigned width, unsigned height, HDC sharedDeviceContext, HGLRC sharedContext, std::string& error);
bool MakeCurrent() const; bool MakeCurrent() const;
void ClearCurrent() const; void ClearCurrent() const;
void Destroy(); void Destroy();

View 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;
}

View 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;
};

View File

@@ -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;
} }

View File

@@ -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;
}; };

View File

@@ -1,16 +1,19 @@
#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>
#include <memory>
#include <thread> #include <thread>
RenderThread::RenderThread(SystemFrameExchange& frameExchange, Config config) : RenderThread::RenderThread(SystemFrameExchange& frameExchange, Config config) :
@@ -19,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();
@@ -75,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;
} }
@@ -83,11 +108,22 @@ void RenderThread::ThreadMain()
RenderCadenceCompositor::TryLog(RenderCadenceCompositor::LogLevel::Log, "render-thread", "Render thread starting."); RenderCadenceCompositor::TryLog(RenderCadenceCompositor::LogLevel::Log, "render-thread", "Render thread starting.");
HiddenGlWindow window; HiddenGlWindow window;
std::string error; std::string error;
if (!window.Create(mConfig.width, mConfig.height, error) || !window.MakeCurrent()) if (!window.Create(mConfig.width, mConfig.height, error))
{ {
SignalStartupFailure(error.empty() ? "OpenGL context creation failed." : error); SignalStartupFailure(error.empty() ? "OpenGL context creation failed." : error);
return; return;
} }
std::unique_ptr<HiddenGlWindow> prepareWindow = std::make_unique<HiddenGlWindow>();
if (!prepareWindow->CreateShared(mConfig.width, mConfig.height, window.DeviceContext(), window.Context(), error))
{
SignalStartupFailure(error.empty() ? "Runtime shader prepare shared context creation failed." : error);
return;
}
if (!window.MakeCurrent())
{
SignalStartupFailure("OpenGL context creation failed.");
return;
}
if (!ResolveGLExtensions()) if (!ResolveGLExtensions())
{ {
SignalStartupFailure("OpenGL extension resolution failed."); SignalStartupFailure("OpenGL extension resolution failed.");
@@ -97,6 +133,12 @@ void RenderThread::ThreadMain()
SimpleMotionRenderer renderer; SimpleMotionRenderer renderer;
RuntimeRenderScene runtimeRenderScene; RuntimeRenderScene runtimeRenderScene;
Bgra8ReadbackPipeline readback; Bgra8ReadbackPipeline readback;
InputFrameTexture inputTexture;
if (!runtimeRenderScene.StartPrepareWorker(std::move(prepareWindow), error))
{
SignalStartupFailure(error.empty() ? "Runtime shader prepare worker initialization failed." : error);
return;
}
if (!renderer.InitializeGl(mConfig.width, mConfig.height) || !readback.Initialize(mConfig.width, mConfig.height, mConfig.pboDepth)) if (!renderer.InitializeGl(mConfig.width, mConfig.height) || !readback.Initialize(mConfig.width, mConfig.height, mConfig.pboDepth))
{ {
SignalStartupFailure("Render pipeline initialization failed."); SignalStartupFailure("Render pipeline initialization failed.");
@@ -117,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);
@@ -128,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;
@@ -155,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();
@@ -195,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())

View File

@@ -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;

View File

@@ -1,114 +0,0 @@
#include "RuntimeRenderScene.h"
#include <algorithm>
#include <functional>
#include <utility>
RuntimeRenderScene::~RuntimeRenderScene()
{
ShutdownGl();
}
bool RuntimeRenderScene::CommitRenderLayers(const std::vector<RenderCadenceCompositor::RuntimeRenderLayerModel>& layers, std::string& error)
{
std::vector<std::string> nextOrder;
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;
std::unique_ptr<RuntimeShaderRenderer> nextRenderer = std::make_unique<RuntimeShaderRenderer>();
if (!nextRenderer->CommitShaderArtifact(layer.artifact, error))
return false;
if (program->renderer)
program->renderer->ShutdownGl();
program->shaderId = layer.shaderId;
program->sourceFingerprint = fingerprint;
program->renderer = std::move(nextRenderer);
}
mLayerOrder = std::move(nextOrder);
error.clear();
return true;
}
void RuntimeRenderScene::RenderFrame(uint64_t frameIndex, unsigned width, unsigned height)
{
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()
{
for (LayerProgram& layer : mLayers)
{
if (layer.renderer)
layer.renderer->ShutdownGl();
}
mLayers.clear();
mLayerOrder.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));
}

View File

@@ -1,39 +0,0 @@
#pragma once
#include "../runtime/RuntimeLayerModel.h"
#include "RuntimeShaderRenderer.h"
#include <cstdint>
#include <memory>
#include <string>
#include <vector>
class RuntimeRenderScene
{
public:
RuntimeRenderScene() = default;
RuntimeRenderScene(const RuntimeRenderScene&) = delete;
RuntimeRenderScene& operator=(const RuntimeRenderScene&) = delete;
~RuntimeRenderScene();
bool CommitRenderLayers(const std::vector<RenderCadenceCompositor::RuntimeRenderLayerModel>& layers, std::string& error);
bool HasLayers() const { return !mLayerOrder.empty(); }
void RenderFrame(uint64_t frameIndex, unsigned width, unsigned height);
void ShutdownGl();
private:
struct LayerProgram
{
std::string layerId;
std::string shaderId;
std::string sourceFingerprint;
std::unique_ptr<RuntimeShaderRenderer> renderer;
};
LayerProgram* FindLayer(const std::string& layerId);
const LayerProgram* FindLayer(const std::string& layerId) const;
static std::string Fingerprint(const RuntimeShaderArtifact& artifact);
std::vector<LayerProgram> mLayers;
std::vector<std::string> mLayerOrder;
};

View File

@@ -1,43 +0,0 @@
#pragma once
#include "GLExtensions.h"
#include "../runtime/RuntimeShaderArtifact.h"
#include <cstdint>
#include <string>
#include <vector>
class RuntimeShaderRenderer
{
public:
RuntimeShaderRenderer() = default;
RuntimeShaderRenderer(const RuntimeShaderRenderer&) = delete;
RuntimeShaderRenderer& operator=(const RuntimeShaderRenderer&) = delete;
~RuntimeShaderRenderer();
bool CommitFragmentShader(const std::string& fragmentShaderSource, std::string& error);
bool CommitShaderArtifact(const RuntimeShaderArtifact& artifact, std::string& error);
bool HasProgram() const { return mProgram != 0; }
void RenderFrame(uint64_t frameIndex, unsigned width, unsigned height);
void ShutdownGl();
private:
bool EnsureStaticGlResources(std::string& error);
bool CompileShader(GLenum shaderType, const char* source, GLuint& shader, std::string& error) const;
bool BuildProgram(const std::string& fragmentShaderSource, GLuint& program, GLuint& vertexShader, GLuint& fragmentShader, std::string& error);
void AssignSamplerUniforms(GLuint program) const;
void UpdateGlobalParams(uint64_t frameIndex, unsigned width, unsigned height);
void BindRuntimeTextures();
void DestroyProgram();
void DestroyStaticGlResources();
RuntimeShaderArtifact mArtifact;
GLuint mProgram = 0;
GLuint mVertexShader = 0;
GLuint mFragmentShader = 0;
GLuint mVertexArray = 0;
GLuint mGlobalParamsBuffer = 0;
GLsizeiptr mGlobalParamsBufferSize = 0;
GLuint mFallbackSourceTexture = 0;
std::vector<unsigned char> mGlobalParamsScratch;
};

View File

@@ -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;
}

View File

@@ -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;
}; };

View File

@@ -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);
} }
} }

View File

@@ -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;
}; };

View File

@@ -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));
}

View File

@@ -0,0 +1,64 @@
#pragma once
#include "../runtime/RuntimeLayerModel.h"
#include "RuntimeShaderPrepareWorker.h"
#include "RuntimeShaderRenderer.h"
#include <windows.h>
#include <cstdint>
#include <memory>
#include <string>
#include <vector>
class RuntimeRenderScene
{
public:
RuntimeRenderScene() = default;
RuntimeRenderScene(const RuntimeRenderScene&) = delete;
RuntimeRenderScene& operator=(const RuntimeRenderScene&) = delete;
~RuntimeRenderScene();
bool StartPrepareWorker(std::unique_ptr<HiddenGlWindow> sharedWindow, std::string& error);
bool CommitRenderLayers(const std::vector<RenderCadenceCompositor::RuntimeRenderLayerModel>& layers, std::string& error);
bool HasLayers();
void RenderFrame(uint64_t frameIndex, unsigned width, unsigned height, GLuint videoInputTexture = 0);
void ShutdownGl();
private:
struct LayerProgram
{
std::string layerId;
std::string shaderId;
std::string sourceFingerprint;
std::string pendingFingerprint;
std::vector<RuntimePreparedShaderProgram> pendingPreparedPrograms;
struct PassProgram
{
std::string passId;
std::vector<std::string> inputNames;
std::string outputName;
std::unique_ptr<RuntimeShaderRenderer> renderer;
};
std::vector<PassProgram> passes;
};
void ConsumePreparedPrograms();
void ReleasePendingPrograms(LayerProgram& layer);
void TryCommitPendingPrograms(LayerProgram& layer);
bool EnsureLayerTargets(unsigned width, unsigned height);
void DestroyLayerTargets();
GLuint RenderLayer(LayerProgram& layer, uint64_t frameIndex, unsigned width, unsigned height, GLuint videoInputTexture, GLuint layerInputTexture, GLuint outputFramebuffer, bool renderToOutput);
LayerProgram* FindLayer(const std::string& layerId);
const LayerProgram* FindLayer(const std::string& layerId) const;
LayerProgram::PassProgram* FindPass(LayerProgram& layer, const std::string& passId);
static std::string Fingerprint(const RuntimeShaderArtifact& artifact);
RuntimeShaderPrepareWorker mPrepareWorker;
std::vector<LayerProgram> mLayers;
std::vector<std::string> mLayerOrder;
GLuint mLayerFramebuffers[4] = {};
GLuint mLayerTextures[4] = {};
unsigned mLayerTargetWidth = 0;
unsigned mLayerTargetHeight = 0;
};

View File

@@ -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;
}

View File

@@ -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;
} }
} }

View File

@@ -0,0 +1,179 @@
#include "RuntimeShaderPrepareWorker.h"
#include "../../platform/HiddenGlWindow.h"
#include "RuntimeShaderRenderer.h"
#include <algorithm>
#include <chrono>
#include <functional>
#include <utility>
RuntimeShaderPrepareWorker::~RuntimeShaderPrepareWorker()
{
Stop();
}
bool RuntimeShaderPrepareWorker::Start(std::unique_ptr<HiddenGlWindow> sharedWindow, std::string& error)
{
if (mThread.joinable())
return true;
if (!sharedWindow || sharedWindow->DeviceContext() == nullptr || sharedWindow->Context() == nullptr)
{
error = "Runtime shader prepare worker needs an existing shared GL context.";
return false;
}
mWindow = std::move(sharedWindow);
mStopping.store(false, std::memory_order_release);
mStarted.store(false, std::memory_order_release);
{
std::lock_guard<std::mutex> lock(mMutex);
mStartupReady = false;
mStartupError.clear();
}
mThread = std::thread([this]() { ThreadMain(); });
std::unique_lock<std::mutex> lock(mMutex);
if (!mStartupCondition.wait_for(lock, std::chrono::seconds(3), [this]() {
return mStartupReady || !mStartupError.empty();
}))
{
error = "Timed out starting runtime shader prepare worker.";
lock.unlock();
Stop();
return false;
}
if (!mStartupError.empty())
{
error = mStartupError;
lock.unlock();
Stop();
return false;
}
return true;
}
void RuntimeShaderPrepareWorker::Stop()
{
mStopping.store(true, std::memory_order_release);
mCondition.notify_all();
if (mThread.joinable())
mThread.join();
std::deque<RuntimePreparedShaderProgram> completed;
{
std::lock_guard<std::mutex> lock(mMutex);
mRequests.clear();
completed.swap(mCompleted);
}
for (RuntimePreparedShaderProgram& program : completed)
program.ReleaseGl();
mWindow.reset();
mStarted.store(false, std::memory_order_release);
}
void RuntimeShaderPrepareWorker::Submit(const std::vector<RenderCadenceCompositor::RuntimeRenderLayerModel>& layers)
{
std::lock_guard<std::mutex> lock(mMutex);
for (const RenderCadenceCompositor::RuntimeRenderLayerModel& layer : layers)
{
if (layer.artifact.passes.empty() && layer.artifact.fragmentShaderSource.empty())
continue;
std::vector<RuntimeShaderPassArtifact> passes = layer.artifact.passes;
if (passes.empty())
{
RuntimeShaderPassArtifact pass;
pass.passId = "main";
pass.fragmentShaderSource = layer.artifact.fragmentShaderSource;
pass.outputName = "layerOutput";
passes.push_back(std::move(pass));
}
auto sameLayer = [&layer](const PrepareRequest& existing) {
return existing.layerId == layer.id;
};
mRequests.erase(std::remove_if(mRequests.begin(), mRequests.end(), sameLayer), mRequests.end());
for (const RuntimeShaderPassArtifact& pass : passes)
{
PrepareRequest request;
request.layerId = layer.id;
request.shaderId = layer.shaderId;
request.passId = pass.passId;
request.sourceFingerprint = Fingerprint(layer.artifact);
request.artifact = layer.artifact;
request.passArtifact = pass;
mRequests.push_back(std::move(request));
}
}
mCondition.notify_one();
}
bool RuntimeShaderPrepareWorker::TryConsume(RuntimePreparedShaderProgram& preparedProgram)
{
std::lock_guard<std::mutex> lock(mMutex);
if (mCompleted.empty())
return false;
preparedProgram = std::move(mCompleted.front());
mCompleted.pop_front();
return true;
}
void RuntimeShaderPrepareWorker::ThreadMain()
{
if (!mWindow || !mWindow->MakeCurrent())
{
std::lock_guard<std::mutex> lock(mMutex);
mStartupError = "Runtime shader prepare worker could not make shared GL context current.";
mStartupCondition.notify_all();
return;
}
{
std::lock_guard<std::mutex> lock(mMutex);
mStartupReady = true;
}
mStarted.store(true, std::memory_order_release);
mStartupCondition.notify_all();
while (!mStopping.load(std::memory_order_acquire))
{
PrepareRequest request;
{
std::unique_lock<std::mutex> lock(mMutex);
mCondition.wait(lock, [this]() {
return mStopping.load(std::memory_order_acquire) || !mRequests.empty();
});
if (mStopping.load(std::memory_order_acquire))
break;
request = std::move(mRequests.front());
mRequests.pop_front();
}
RuntimePreparedShaderProgram preparedProgram;
RuntimeShaderRenderer::BuildPreparedPassProgram(
request.layerId,
request.sourceFingerprint,
request.artifact,
request.passArtifact,
preparedProgram);
glFlush();
std::lock_guard<std::mutex> lock(mMutex);
mCompleted.push_back(std::move(preparedProgram));
}
mWindow->ClearCurrent();
}
std::string RuntimeShaderPrepareWorker::Fingerprint(const RuntimeShaderArtifact& artifact)
{
const std::hash<std::string> hasher;
std::string source;
for (const RuntimeShaderPassArtifact& pass : artifact.passes)
source += pass.passId + ":" + pass.outputName + ":" + pass.fragmentShaderSource + "\n";
if (source.empty())
source = artifact.fragmentShaderSource;
return artifact.shaderId + ":" + std::to_string(source.size()) + ":" + std::to_string(hasher(source));
}

View File

@@ -0,0 +1,58 @@
#pragma once
#include "RuntimeShaderProgram.h"
#include "../../runtime/RuntimeLayerModel.h"
#include <windows.h>
#include <atomic>
#include <condition_variable>
#include <deque>
#include <memory>
#include <mutex>
#include <string>
#include <thread>
#include <vector>
class HiddenGlWindow;
class RuntimeShaderPrepareWorker
{
public:
RuntimeShaderPrepareWorker() = default;
RuntimeShaderPrepareWorker(const RuntimeShaderPrepareWorker&) = delete;
RuntimeShaderPrepareWorker& operator=(const RuntimeShaderPrepareWorker&) = delete;
~RuntimeShaderPrepareWorker();
bool Start(std::unique_ptr<HiddenGlWindow> sharedWindow, std::string& error);
void Stop();
void Submit(const std::vector<RenderCadenceCompositor::RuntimeRenderLayerModel>& layers);
bool TryConsume(RuntimePreparedShaderProgram& preparedProgram);
private:
struct PrepareRequest
{
std::string layerId;
std::string shaderId;
std::string passId;
std::string sourceFingerprint;
RuntimeShaderArtifact artifact;
RuntimeShaderPassArtifact passArtifact;
};
void ThreadMain();
static std::string Fingerprint(const RuntimeShaderArtifact& artifact);
std::unique_ptr<HiddenGlWindow> mWindow;
std::mutex mMutex;
std::condition_variable mCondition;
std::deque<PrepareRequest> mRequests;
std::deque<RuntimePreparedShaderProgram> mCompleted;
std::condition_variable mStartupCondition;
std::thread mThread;
std::atomic<bool> mStopping{ false };
std::atomic<bool> mStarted{ false };
bool mStartupReady = false;
std::string mStartupError;
};

View File

@@ -0,0 +1,37 @@
#pragma once
#include "GLExtensions.h"
#include "../../runtime/RuntimeShaderArtifact.h"
#include <string>
#include <vector>
struct RuntimePreparedShaderProgram
{
std::string layerId;
std::string shaderId;
std::string passId;
std::string sourceFingerprint;
RuntimeShaderArtifact artifact;
RuntimeShaderPassArtifact passArtifact;
std::vector<std::string> inputNames;
std::string outputName;
GLuint program = 0;
GLuint vertexShader = 0;
GLuint fragmentShader = 0;
bool succeeded = false;
std::string error;
void ReleaseGl()
{
if (program != 0)
glDeleteProgram(program);
if (vertexShader != 0)
glDeleteShader(vertexShader);
if (fragmentShader != 0)
glDeleteShader(fragmentShader);
program = 0;
vertexShader = 0;
fragmentShader = 0;
}
};

View File

@@ -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,42 +37,90 @@ RuntimeShaderRenderer::~RuntimeShaderRenderer()
ShutdownGl(); ShutdownGl();
} }
bool RuntimeShaderRenderer::CommitFragmentShader(const std::string& fragmentShaderSource, std::string& error) bool RuntimeShaderRenderer::CommitPreparedProgram(RuntimePreparedShaderProgram& preparedProgram, std::string& error)
{ {
RuntimeShaderArtifact artifact; if (!preparedProgram.succeeded || preparedProgram.program == 0)
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 = preparedProgram.error.empty() ? "Prepared runtime shader program is not valid." : preparedProgram.error;
{
error = "Cannot commit an empty fragment shader.";
return false; return false;
} }
if (!EnsureStaticGlResources(error)) if (!EnsureStaticGlResources(error))
return false; return false;
GLuint vertexShader = 0;
GLuint fragmentShader = 0;
GLuint program = 0;
if (!BuildProgram(artifact.fragmentShaderSource, program, vertexShader, fragmentShader, error))
return false;
DestroyProgram(); DestroyProgram();
mProgram = program; mProgram = preparedProgram.program;
mVertexShader = vertexShader; mVertexShader = preparedProgram.vertexShader;
mFragmentShader = fragmentShader; mFragmentShader = preparedProgram.fragmentShader;
mArtifact = artifact; mArtifact = preparedProgram.artifact;
AssignSamplerUniforms(mProgram); preparedProgram.program = 0;
preparedProgram.vertexShader = 0;
preparedProgram.fragmentShader = 0;
return true; return true;
} }
void RuntimeShaderRenderer::RenderFrame(uint64_t frameIndex, unsigned width, unsigned height) void RuntimeShaderRenderer::UpdateArtifactState(const RuntimeShaderArtifact& artifact)
{
mArtifact.parameterDefinitions = artifact.parameterDefinitions;
mArtifact.parameterValues = artifact.parameterValues;
mArtifact.message = artifact.message;
}
bool RuntimeShaderRenderer::BuildPreparedProgram(
const std::string& layerId,
const std::string& sourceFingerprint,
const RuntimeShaderArtifact& artifact,
RuntimePreparedShaderProgram& preparedProgram)
{
RuntimeShaderPassArtifact passArtifact;
passArtifact.passId = "main";
passArtifact.fragmentShaderSource = artifact.fragmentShaderSource;
passArtifact.outputName = "layerOutput";
if (!artifact.passes.empty())
passArtifact = artifact.passes.front();
return BuildPreparedPassProgram(layerId, sourceFingerprint, artifact, passArtifact, preparedProgram);
}
bool RuntimeShaderRenderer::BuildPreparedPassProgram(
const std::string& layerId,
const std::string& sourceFingerprint,
const RuntimeShaderArtifact& artifact,
const RuntimeShaderPassArtifact& passArtifact,
RuntimePreparedShaderProgram& preparedProgram)
{
preparedProgram = RuntimePreparedShaderProgram();
preparedProgram.layerId = layerId;
preparedProgram.shaderId = artifact.shaderId;
preparedProgram.passId = passArtifact.passId;
preparedProgram.sourceFingerprint = sourceFingerprint;
preparedProgram.artifact = artifact;
preparedProgram.passArtifact = passArtifact;
preparedProgram.inputNames = passArtifact.inputNames;
preparedProgram.outputName = passArtifact.outputName.empty() ? passArtifact.passId : passArtifact.outputName;
if (passArtifact.fragmentShaderSource.empty())
{
preparedProgram.error = "Cannot prepare an empty fragment shader.";
return false;
}
if (!BuildProgram(
passArtifact.fragmentShaderSource,
preparedProgram.program,
preparedProgram.vertexShader,
preparedProgram.fragmentShader,
preparedProgram.error))
{
preparedProgram.ReleaseGl();
return false;
}
preparedProgram.succeeded = true;
AssignSamplerUniforms(preparedProgram.program);
return true;
}
void RuntimeShaderRenderer::RenderFrame(uint64_t frameIndex, unsigned width, unsigned height, GLuint sourceTexture, GLuint layerInputTexture)
{ {
if (mProgram == 0) if (mProgram == 0)
return; return;
@@ -81,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);
@@ -175,21 +224,21 @@ bool RuntimeShaderRenderer::BuildProgram(const std::string& fragmentShaderSource
return true; return true;
} }
void RuntimeShaderRenderer::AssignSamplerUniforms(GLuint program) const 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);
} }
@@ -212,14 +261,18 @@ 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);
} }
bool RuntimeShaderRenderer::CompileShader(GLenum shaderType, const char* source, GLuint& shader, std::string& error) const bool RuntimeShaderRenderer::CompileShader(GLenum shaderType, const char* source, GLuint& shader, std::string& error)
{ {
shader = glCreateShader(shaderType); shader = glCreateShader(shaderType);
glShaderSource(shader, 1, &source, nullptr); glShaderSource(shader, 1, &source, nullptr);

View File

@@ -0,0 +1,56 @@
#pragma once
#include "GLExtensions.h"
#include "RuntimeShaderProgram.h"
#include "../../runtime/RuntimeShaderArtifact.h"
#include <cstdint>
#include <string>
#include <vector>
class RuntimeShaderRenderer
{
public:
RuntimeShaderRenderer() = default;
RuntimeShaderRenderer(const RuntimeShaderRenderer&) = delete;
RuntimeShaderRenderer& operator=(const RuntimeShaderRenderer&) = delete;
~RuntimeShaderRenderer();
bool CommitPreparedProgram(RuntimePreparedShaderProgram& preparedProgram, std::string& error);
bool HasProgram() const { return mProgram != 0; }
void UpdateArtifactState(const RuntimeShaderArtifact& artifact);
void RenderFrame(uint64_t frameIndex, unsigned width, unsigned height, GLuint sourceTexture = 0, GLuint layerInputTexture = 0);
void ShutdownGl();
static bool BuildPreparedProgram(
const std::string& layerId,
const std::string& sourceFingerprint,
const RuntimeShaderArtifact& artifact,
RuntimePreparedShaderProgram& preparedProgram);
static bool BuildPreparedPassProgram(
const std::string& layerId,
const std::string& sourceFingerprint,
const RuntimeShaderArtifact& artifact,
const RuntimeShaderPassArtifact& passArtifact,
RuntimePreparedShaderProgram& preparedProgram);
private:
bool EnsureStaticGlResources(std::string& error);
static bool CompileShader(GLenum shaderType, const char* source, GLuint& shader, std::string& error);
static bool BuildProgram(const std::string& fragmentShaderSource, GLuint& program, GLuint& vertexShader, GLuint& fragmentShader, std::string& error);
static void AssignSamplerUniforms(GLuint program);
void UpdateGlobalParams(uint64_t frameIndex, unsigned width, unsigned height);
void BindRuntimeTextures(GLuint sourceTexture, GLuint layerInputTexture);
void DestroyProgram();
void DestroyStaticGlResources();
RuntimeShaderArtifact mArtifact;
GLuint mProgram = 0;
GLuint mVertexShader = 0;
GLuint mFragmentShader = 0;
GLuint mVertexArray = 0;
GLuint mGlobalParamsBuffer = 0;
GLsizeiptr mGlobalParamsBufferSize = 0;
GLuint mFallbackSourceTexture = 0;
std::vector<unsigned char> mGlobalParamsScratch;
};

View File

@@ -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();
}
} }

View File

@@ -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();
}; };
} }

View File

@@ -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;
}; };

View File

@@ -22,19 +22,31 @@ void RuntimeShaderBridge::Start(const std::string& layerId, const std::string& s
mOnArtifactReady = std::move(onArtifactReady); mOnArtifactReady = std::move(onArtifactReady);
mOnError = std::move(onError); mOnError = std::move(onError);
mStopping.store(false, std::memory_order_release); mStopping.store(false, std::memory_order_release);
mFinished.store(false, std::memory_order_release);
mCompiler.StartShaderBuild(shaderId); mCompiler.StartShaderBuild(shaderId);
mThread = std::thread([this]() { ThreadMain(); }); mThread = std::thread([this]() { ThreadMain(); });
} }
void RuntimeShaderBridge::Stop() void RuntimeShaderBridge::RequestStop()
{ {
mStopping.store(true, std::memory_order_release); mStopping.store(true, std::memory_order_release);
}
void RuntimeShaderBridge::Stop()
{
RequestStop();
if (mThread.joinable()) if (mThread.joinable())
mThread.join(); mThread.join();
mCompiler.Stop(); mCompiler.Stop();
mLayerId.clear(); mLayerId.clear();
mOnArtifactReady = ArtifactCallback(); mOnArtifactReady = ArtifactCallback();
mOnError = ErrorCallback(); mOnError = ErrorCallback();
mFinished.store(true, std::memory_order_release);
}
bool RuntimeShaderBridge::CanStopWithoutWaiting() const
{
return mFinished.load(std::memory_order_acquire) && !mCompiler.Running();
} }
void RuntimeShaderBridge::ThreadMain() void RuntimeShaderBridge::ThreadMain()
@@ -54,8 +66,10 @@ void RuntimeShaderBridge::ThreadMain()
{ {
mOnError(build.message); mOnError(build.message);
} }
mFinished.store(true, std::memory_order_release);
return; return;
} }
std::this_thread::sleep_for(std::chrono::milliseconds(5)); std::this_thread::sleep_for(std::chrono::milliseconds(5));
} }
mFinished.store(true, std::memory_order_release);
} }

View File

@@ -21,7 +21,9 @@ public:
void Start(const std::string& shaderId, ArtifactCallback onArtifactReady, ErrorCallback onError); void Start(const std::string& shaderId, ArtifactCallback onArtifactReady, ErrorCallback onError);
void Start(const std::string& layerId, const std::string& shaderId, ArtifactCallback onArtifactReady, ErrorCallback onError); void Start(const std::string& layerId, const std::string& shaderId, ArtifactCallback onArtifactReady, ErrorCallback onError);
void RequestStop();
void Stop(); void Stop();
bool CanStopWithoutWaiting() const;
private: private:
void ThreadMain(); void ThreadMain();
@@ -29,6 +31,7 @@ private:
RuntimeSlangShaderCompiler mCompiler; RuntimeSlangShaderCompiler mCompiler;
std::thread mThread; std::thread mThread;
std::atomic<bool> mStopping{ false }; std::atomic<bool> mStopping{ false };
std::atomic<bool> mFinished{ true };
std::string mLayerId; std::string mLayerId;
ArtifactCallback mOnArtifactReady; ArtifactCallback mOnArtifactReady;
ErrorCallback mOnError; ErrorCallback mOnError;

View File

@@ -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,19 +120,32 @@ 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)
{
std::string fragmentShaderSource;
if (!compiler.BuildPassFragmentShaderSource(shaderPackage, pass, fragmentShaderSource, error))
{ {
build.succeeded = false; build.succeeded = false;
build.message = error.empty() ? "Slang compile failed." : error; build.message = error.empty() ? "Slang compile failed." : error;
return build; 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();
const double milliseconds = std::chrono::duration_cast<std::chrono::duration<double, std::milli>>(end - start).count(); const double milliseconds = std::chrono::duration_cast<std::chrono::duration<double, std::milli>>(end - start).count();
build.succeeded = true; build.succeeded = true;
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;

View File

@@ -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() };
} }

View File

@@ -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;
}; };
} }

View File

@@ -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();
} }

View File

@@ -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());
} }

View 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";
}
}

View 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 };
};
}

View 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;
};
}

View File

@@ -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;
} }

View File

@@ -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

View File

@@ -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))
{ {

View File

@@ -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.

View File

@@ -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.

View File

@@ -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

View File

@@ -11,7 +11,7 @@ Only the render thread may bind and use its primary OpenGL context.
Allowed on the render thread: Allowed on the render thread:
- GL resource creation and destruction for resources it owns - GL resource creation and destruction for resources it owns
- GL shader/program commit from an already-prepared artifact - GL shader/program swap from an already-prepared GL program
- drawing the next frame - drawing the next frame
- async readback queueing and completion polling - async readback queueing and completion polling
- publishing completed system-memory frames - publishing completed system-memory frames
@@ -28,7 +28,7 @@ Not allowed on the render thread:
- blocking console logging - blocking console logging
- config file discovery or parsing - config file discovery or parsing
If future GL preparation needs to happen off-thread, use an explicit shared-context GL prepare thread. Do not smuggle non-render work back into the cadence loop. If GL preparation happens off-thread, use an explicit shared-context GL prepare thread. Do not smuggle non-render work back into the cadence loop.
## 2. Render Cadence Does Not Chase Buffers ## 2. Render Cadence Does Not Chase Buffers
@@ -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,19 +56,23 @@ 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:
1. CPU/build phase outside the render thread 1. CPU/build phase outside the render thread
2. GL commit phase on the render thread 2. shared-context GL preparation outside the render thread where practical
3. GL program swap on the render thread
The CPU/build phase may parse manifests, invoke Slang, validate package shape, and prepare CPU-side data. The CPU/build phase may parse manifests, invoke Slang, validate package shape, and prepare CPU-side data.
The render thread receives a completed artifact and either commits it at a frame boundary or rejects it. A failed artifact must not disturb the current renderer. The render thread receives completed render-layer artifacts, asks the shared-context prepare worker to compile/link changed GL programs, and only swaps in prepared programs at a frame boundary. A failed artifact or failed GL preparation must not disturb the current renderer.
The display/render layer model is app-owned. It may track requested shaders, build state, display metadata, and render-ready artifacts, but it must not perform GL work or drive render cadence directly. The display/render layer model is app-owned. It may track requested shaders, build state, display metadata, and render-ready artifacts, but it must not perform GL work or drive render cadence directly.
@@ -90,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
@@ -111,6 +118,12 @@ Good examples:
- `completedPollMisses` - `completedPollMisses`
- `scheduleFailures` - `scheduleFailures`
- `decklinkBuffered` - `decklinkBuffered`
- `deckLinkScheduleLeadFrames`
- `deckLinkScheduleRealignments`
- `inputCaptureFps`
- `inputSubmitMs`
- `inputUploadMs`
- `inputConvertMs`
- `shaderCommitted` - `shaderCommitted`
- `shaderFailures` - `shaderFailures`

View File

@@ -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

View File

@@ -8,8 +8,8 @@ info:
The API is intended for local control tools and the bundled React UI. All mutating The API is intended for local control tools and the bundled React UI. All mutating
endpoints return a small action result object. endpoints return a small action result object.
WebSocket state streaming is planned for the control UI but is not currently served RenderCadenceCompositor serves `/api/state` for snapshots and `/ws` for local
by RenderCadenceCompositor. Clients should poll `/api/state` until `/ws` is implemented. WebSocket state updates consumed by the bundled control UI.
servers: servers:
- url: http://127.0.0.1:8080 - url: http://127.0.0.1:8080
description: Default local control server description: Default local control server
@@ -179,6 +179,24 @@ paths:
application/json: application/json:
schema: schema:
$ref: "#/components/schemas/RuntimeState" $ref: "#/components/schemas/RuntimeState"
/ws:
get:
tags: [State]
summary: Stream runtime state over WebSocket
description: |
Upgrades to a WebSocket connection. The server sends JSON runtime-state
snapshots using the same shape as `GET /api/state` whenever the serialized
state changes.
operationId: streamRuntimeState
responses:
"101":
description: WebSocket protocol upgrade accepted.
"400":
description: The request was not a valid WebSocket upgrade.
content:
text/plain:
schema:
type: string
/api/layers/add: /api/layers/add:
post: post:
tags: [Layers] tags: [Layers]
@@ -615,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:
@@ -631,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:

View File

@@ -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);
} }

View File

@@ -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");

View File

@@ -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)

View File

@@ -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)
{ {

View File

@@ -63,6 +63,14 @@ void TestStateEndpointUsesCallback()
ExpectEquals(response.body, "{\"ok\":true}", "state endpoint returns callback JSON"); ExpectEquals(response.body, "{\"ok\":true}", "state endpoint returns callback JSON");
} }
void TestWebSocketAcceptKey()
{
using namespace RenderCadenceCompositor;
const std::string acceptKey = HttpControlServer::WebSocketAcceptKey("dGhlIHNhbXBsZSBub25jZQ==");
ExpectEquals(acceptKey, "s3pPLMBiTxaQ9kYGzzhZRbK+xOo=", "WebSocket accept key matches RFC example");
}
void TestRootServesUiIndex() void TestRootServesUiIndex()
{ {
using namespace RenderCadenceCompositor; using namespace RenderCadenceCompositor;
@@ -139,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;
@@ -157,9 +188,11 @@ int main()
{ {
TestParsesHttpRequest(); TestParsesHttpRequest();
TestStateEndpointUsesCallback(); TestStateEndpointUsesCallback();
TestWebSocketAcceptKey();
TestRootServesUiIndex(); TestRootServesUiIndex();
TestKnownPostEndpointReturnsActionError(); TestKnownPostEndpointReturnsActionError();
TestLayerPostEndpointsUseCallbacks(); TestLayerPostEndpointsUseCallbacks();
TestGenericPostCallbackHandlesControlRoutes();
TestUnknownEndpointReturns404(); TestUnknownEndpointReturns404();
if (gFailures != 0) if (gFailures != 0)

View 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;
}

View File

@@ -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)
{ {

View File

@@ -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;

View File

@@ -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);

View File

@@ -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();

View File

@@ -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");
} }

View File

@@ -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();