From 5c1fc2a6cf7183536e5900fc050d9e1ee507945c Mon Sep 17 00:00:00 2001 From: Aiden <68633820+awils27@users.noreply.github.com> Date: Wed, 13 May 2026 00:21:28 +1000 Subject: [PATCH] telemetry and timing updates --- .../videoio/VideoIOTypes.h | 6 ++ .../videoio/VideoPlayoutScheduler.h | 1 + .../videoio/decklink/DeckLinkSession.cpp | 67 ++++++++++++++++--- .../videoio/decklink/DeckLinkSession.h | 4 ++ apps/RenderCadenceCompositor/README.md | 9 ++- .../RenderCadenceCompositor.cpp | 16 ++++- .../RenderCadenceCompositor/app/AppConfig.cpp | 3 +- apps/RenderCadenceCompositor/app/AppConfig.h | 3 +- .../app/AppConfigProvider.cpp | 3 - .../app/RenderCadenceApp.h | 17 ++--- .../control/RuntimeStateJson.h | 1 - .../frames/SystemFrameExchange.cpp | 33 +++++++++ .../frames/SystemFrameExchange.h | 2 + .../frames/SystemFrameTypes.h | 1 + .../telemetry/CadenceTelemetry.h | 12 ++++ .../telemetry/CadenceTelemetryJson.h | 10 +++ .../video/DeckLinkOutput.cpp | 7 ++ .../video/DeckLinkOutput.h | 8 +++ .../video/DeckLinkOutputThread.h | 6 +- config/runtime-host.json | 1 - docs/openapi.yaml | 21 +++++- ...adenceCompositorAppConfigProviderTests.cpp | 2 - ...derCadenceCompositorFrameExchangeTests.cpp | 33 +++++++++ .../RenderCadenceCompositorTelemetryTests.cpp | 32 ++++++++- 24 files changed, 260 insertions(+), 38 deletions(-) diff --git a/apps/LoopThroughWithOpenGLCompositing/videoio/VideoIOTypes.h b/apps/LoopThroughWithOpenGLCompositing/videoio/VideoIOTypes.h index bf88558..77217bc 100644 --- a/apps/LoopThroughWithOpenGLCompositing/videoio/VideoIOTypes.h +++ b/apps/LoopThroughWithOpenGLCompositing/videoio/VideoIOTypes.h @@ -54,6 +54,12 @@ struct VideoIOState uint64_t actualDeckLinkBufferedFrames = 0; double deckLinkScheduleCallMilliseconds = 0.0; uint64_t deckLinkScheduleFailureCount = 0; + bool deckLinkScheduleLeadAvailable = false; + int64_t deckLinkPlaybackStreamTime = 0; + uint64_t deckLinkPlaybackFrameIndex = 0; + uint64_t deckLinkNextScheduleFrameIndex = 0; + int64_t deckLinkScheduleLeadFrames = 0; + uint64_t deckLinkScheduleRealignmentCount = 0; }; struct VideoIOFrame diff --git a/apps/LoopThroughWithOpenGLCompositing/videoio/VideoPlayoutScheduler.h b/apps/LoopThroughWithOpenGLCompositing/videoio/VideoPlayoutScheduler.h index 8635899..a231ea8 100644 --- a/apps/LoopThroughWithOpenGLCompositing/videoio/VideoPlayoutScheduler.h +++ b/apps/LoopThroughWithOpenGLCompositing/videoio/VideoPlayoutScheduler.h @@ -17,6 +17,7 @@ public: double FrameBudgetMilliseconds() const; uint64_t ScheduledFrameIndex() const { return mScheduledFrameIndex; } uint64_t CompletedFrameIndex() const { return mCompletedFrameIndex; } + int64_t FrameDuration() const { return mFrameDuration; } uint64_t LateStreak() const { return mLateStreak; } uint64_t DropStreak() const { return mDropStreak; } int64_t TimeScale() const { return mTimeScale; } diff --git a/apps/LoopThroughWithOpenGLCompositing/videoio/decklink/DeckLinkSession.cpp b/apps/LoopThroughWithOpenGLCompositing/videoio/decklink/DeckLinkSession.cpp index dd5c446..91e022c 100644 --- a/apps/LoopThroughWithOpenGLCompositing/videoio/decklink/DeckLinkSession.cpp +++ b/apps/LoopThroughWithOpenGLCompositing/videoio/decklink/DeckLinkSession.cpp @@ -532,17 +532,13 @@ bool DeckLinkSession::ScheduleFrame(IDeckLinkMutableVideoFrame* outputVideoFrame return false; } - BMDTimeValue streamTime = 0; - double playbackSpeed = 0.0; - if (output->GetScheduledStreamTime(mScheduler.TimeScale(), &streamTime, &playbackSpeed) == S_OK && playbackSpeed > 0.0) + if (mScheduleRealignmentPending) { - RefreshBufferedVideoFrameCount(); - const uint64_t leadFrames = mState.actualDeckLinkBufferedFramesAvailable - ? static_cast(mState.actualDeckLinkBufferedFrames) + 1 - : static_cast(mPlayoutPolicy.targetPrerollFrames); - mScheduler.AlignNextScheduleTimeToPlayback(streamTime, leadFrames); + RealignScheduleCursorToPlayback(); + mScheduleRealignmentPending = false; } + UpdateScheduleLeadTelemetry(); const VideoIOScheduleTime scheduleTime = mScheduler.NextScheduleTime(); const auto scheduleStart = std::chrono::steady_clock::now(); const HRESULT result = output->ScheduleVideoFrame(outputVideoFrame, scheduleTime.streamTime, scheduleTime.duration, scheduleTime.timeScale); @@ -554,6 +550,49 @@ bool DeckLinkSession::ScheduleFrame(IDeckLinkMutableVideoFrame* outputVideoFrame 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(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(nextScheduleFrameIndex) - static_cast(playbackFrameIndex); +} + +void DeckLinkSession::RealignScheduleCursorToPlayback() +{ + if (output == nullptr) + return; + + BMDTimeValue streamTime = 0; + double playbackSpeed = 0.0; + if (output->GetScheduledStreamTime(mScheduler.TimeScale(), &streamTime, &playbackSpeed) != S_OK || playbackSpeed <= 0.0) + return; + + const VideoPlayoutPolicy policy = NormalizeVideoPlayoutPolicy(mPlayoutPolicy); + mScheduler.AlignNextScheduleTimeToPlayback(streamTime, policy.targetPrerollFrames); + ++mState.deckLinkScheduleRealignmentCount; + UpdateScheduleLeadTelemetry(); +} + bool DeckLinkSession::ScheduleSystemMemoryFrame(const VideoIOOutputFrame& frame) { if (output == nullptr || frame.bytes == nullptr || frame.rowBytes <= 0 || frame.height == 0) @@ -838,6 +877,18 @@ void DeckLinkSession::HandlePlayoutFrameCompleted(IDeckLinkVideoFrame* completed VideoIOCompletion completion; completion.result = TranslateCompletionResult(completionResult); + if (completion.result == VideoIOCompletionResult::DisplayedLate || completion.result == VideoIOCompletionResult::Dropped) + { + if (mScheduleRealignmentArmed) + { + mScheduleRealignmentPending = true; + mScheduleRealignmentArmed = false; + } + } + else if (completion.result == VideoIOCompletionResult::Completed) + { + mScheduleRealignmentArmed = true; + } completion.outputFrameBuffer = completedSystemBuffer; mOutputFrameCallback(completion); } diff --git a/apps/LoopThroughWithOpenGLCompositing/videoio/decklink/DeckLinkSession.h b/apps/LoopThroughWithOpenGLCompositing/videoio/decklink/DeckLinkSession.h index 9866b61..21aa215 100644 --- a/apps/LoopThroughWithOpenGLCompositing/videoio/decklink/DeckLinkSession.h +++ b/apps/LoopThroughWithOpenGLCompositing/videoio/decklink/DeckLinkSession.h @@ -75,6 +75,8 @@ private: bool AcquireNextOutputVideoFrame(CComPtr& outputVideoFrame); bool PopulateOutputFrame(IDeckLinkMutableVideoFrame* outputVideoFrame, VideoIOOutputFrame& frame); bool ScheduleFrame(IDeckLinkMutableVideoFrame* outputVideoFrame); + void UpdateScheduleLeadTelemetry(); + void RealignScheduleCursorToPlayback(); bool ScheduleSystemMemoryFrame(const VideoIOOutputFrame& frame); bool ScheduleBlackFrame(IDeckLinkMutableVideoFrame* outputVideoFrame); void RefreshBufferedVideoFrameCount(); @@ -91,6 +93,8 @@ private: VideoIOState mState; VideoPlayoutPolicy mPlayoutPolicy; VideoPlayoutScheduler mScheduler; + bool mScheduleRealignmentPending = false; + bool mScheduleRealignmentArmed = true; InputFrameCallback mInputFrameCallback; OutputFrameCallback mOutputFrameCallback; }; diff --git a/apps/RenderCadenceCompositor/README.md b/apps/RenderCadenceCompositor/README.md index ff1a16e..ba251d4 100644 --- a/apps/RenderCadenceCompositor/README.md +++ b/apps/RenderCadenceCompositor/README.md @@ -35,7 +35,7 @@ DeckLinkOutputThread never renders ``` -Startup builds one settled output reserve before DeckLink scheduled playback starts: the completed-frame reserve must reach the configured depth and remain ready for the configured settle window. 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. +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 @@ -53,7 +53,7 @@ Included now: - render-thread-owned input texture upload - async PBO readback - latest-N system-memory frame exchange -- settled completed-frame output reserve before DeckLink preroll, with DeckLink scheduled depth still targeted at four +- bounded completed-frame output preroll reserve before DeckLink playback, with DeckLink scheduled depth still targeted at four - background Slang compile of `shaders/happy-accident` - app-owned display/render layer model for shader build readiness - app-owned submission of a completed shader artifact @@ -198,7 +198,6 @@ Currently consumed fields: - `autoReload` - `maxTemporalHistoryFrames` - `previewFps` -- `startupSettleMs` - `enableExternalKeying` The loaded config is treated as a read-only startup snapshot. Subsystems that need config should receive this snapshot or a narrowed config struct from app orchestration; they should not reload files independently. @@ -238,7 +237,7 @@ DeckLink output is an optional edge service in this app. Startup order is: 1. start render thread -2. build a settled completed-frame output reserve at normal render cadence +2. build a bounded completed-frame output preroll reserve at normal render cadence 3. try to attach DeckLink output 4. start telemetry and HTTP either way @@ -286,7 +285,7 @@ Input telemetry: - `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`: completed unscheduled system-memory frames dropped by latest-N acquire paths; expected to stay flat in the cadence compositor output path +- `completedDrops`: completed unscheduled system-memory frames dropped; expected to stay flat in the cadence compositor output path - `acquireMisses`: times render/readback could not acquire a writable system-memory frame slot; completed frames waiting for playout are preserved instead of being displaced - `inputConsumeMisses`: render ticks where no ready input frame was available to upload - `inputUploadMisses`: input texture upload attempts that reused the previous GL input texture diff --git a/apps/RenderCadenceCompositor/RenderCadenceCompositor.cpp b/apps/RenderCadenceCompositor/RenderCadenceCompositor.cpp index 267735b..f885110 100644 --- a/apps/RenderCadenceCompositor/RenderCadenceCompositor.cpp +++ b/apps/RenderCadenceCompositor/RenderCadenceCompositor.cpp @@ -20,6 +20,10 @@ namespace { +constexpr std::size_t kDeckLinkTargetBufferedFrames = 4; +constexpr std::size_t kReadbackDepth = 6; +constexpr std::size_t kWritableOutputReserveFrames = kReadbackDepth + 2; + class ComInitGuard { public: @@ -95,7 +99,11 @@ int main(int argc, char** argv) frameExchangeConfig.height); frameExchangeConfig.pixelFormat = VideoIOPixelFormat::Bgra8; frameExchangeConfig.rowBytes = VideoIORowBytes(frameExchangeConfig.pixelFormat, frameExchangeConfig.width); - frameExchangeConfig.capacity = 12; + frameExchangeConfig.capacity = + appConfig.warmupCompletedFrames + + kDeckLinkTargetBufferedFrames + + kWritableOutputReserveFrames; + frameExchangeConfig.maxCompletedFrames = appConfig.warmupCompletedFrames; SystemFrameExchange frameExchange(frameExchangeConfig); @@ -128,6 +136,10 @@ int main(int argc, char** argv) "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); @@ -186,7 +198,7 @@ int main(int argc, char** argv) renderConfig.frameDurationMilliseconds = outputVideoModeResolved ? RenderCadenceCompositor::FrameDurationMillisecondsFromDisplayMode(outputVideoMode.displayMode, fallbackFrameDurationMilliseconds) : fallbackFrameDurationMilliseconds; - renderConfig.pboDepth = 6; + renderConfig.pboDepth = kReadbackDepth; RenderThread renderThread(frameExchange, &inputMailbox, renderConfig); diff --git a/apps/RenderCadenceCompositor/app/AppConfig.cpp b/apps/RenderCadenceCompositor/app/AppConfig.cpp index 7aeabb9..b9adea7 100644 --- a/apps/RenderCadenceCompositor/app/AppConfig.cpp +++ b/apps/RenderCadenceCompositor/app/AppConfig.cpp @@ -29,9 +29,8 @@ AppConfig DefaultAppConfig() config.autoReload = true; config.maxTemporalHistoryFrames = 12; config.previewFps = 30.0; - config.warmupCompletedFrames = 8; + config.warmupCompletedFrames = 4; config.warmupTimeout = std::chrono::seconds(3); - config.startupSettle = std::chrono::seconds(5); config.prerollTimeout = std::chrono::seconds(3); config.prerollPoll = std::chrono::milliseconds(2); config.runtimeShaderId = "happy-accident"; diff --git a/apps/RenderCadenceCompositor/app/AppConfig.h b/apps/RenderCadenceCompositor/app/AppConfig.h index 5acf2d2..7509760 100644 --- a/apps/RenderCadenceCompositor/app/AppConfig.h +++ b/apps/RenderCadenceCompositor/app/AppConfig.h @@ -30,9 +30,8 @@ struct AppConfig bool autoReload = true; std::size_t maxTemporalHistoryFrames = 12; double previewFps = 30.0; - std::size_t warmupCompletedFrames = 8; + std::size_t warmupCompletedFrames = 4; std::chrono::milliseconds warmupTimeout = std::chrono::seconds(3); - std::chrono::milliseconds startupSettle = std::chrono::seconds(5); std::chrono::milliseconds prerollTimeout = std::chrono::seconds(3); std::chrono::milliseconds prerollPoll = std::chrono::milliseconds(2); std::string runtimeShaderId = "happy-accident"; diff --git a/apps/RenderCadenceCompositor/app/AppConfigProvider.cpp b/apps/RenderCadenceCompositor/app/AppConfigProvider.cpp index d10604c..c5ddbb4 100644 --- a/apps/RenderCadenceCompositor/app/AppConfigProvider.cpp +++ b/apps/RenderCadenceCompositor/app/AppConfigProvider.cpp @@ -134,9 +134,6 @@ bool AppConfigProvider::Load(const std::filesystem::path& path, std::string& err ApplySize(root, "maxTemporalHistoryFrames", mConfig.maxTemporalHistoryFrames); ApplyDouble(root, "previewFps", mConfig.previewFps); ApplyBool(root, "enableExternalKeying", mConfig.deckLink.externalKeyingEnabled); - std::size_t startupSettleMilliseconds = static_cast(mConfig.startupSettle.count()); - ApplySize(root, "startupSettleMs", startupSettleMilliseconds); - mConfig.startupSettle = std::chrono::milliseconds(startupSettleMilliseconds); mLoadedFromFile = true; error.clear(); diff --git a/apps/RenderCadenceCompositor/app/RenderCadenceApp.h b/apps/RenderCadenceCompositor/app/RenderCadenceApp.h index 7a56d3d..cc89742 100644 --- a/apps/RenderCadenceCompositor/app/RenderCadenceApp.h +++ b/apps/RenderCadenceCompositor/app/RenderCadenceApp.h @@ -163,23 +163,24 @@ private: mVideoOutputEnabled = true; mVideoOutputStatus = "DeckLink scheduled output running."; Log("app", mVideoOutputStatus); + Log( + "app", + "DeckLink output mode: " + mOutput.State().outputDisplayModeName + + ", frame budget " + std::to_string(mOutput.State().frameBudgetMilliseconds) + " ms."); } bool BuildSettledOutputReserve(std::string& error) { - const auto reserveTimeout = mConfig.warmupTimeout + mConfig.startupSettle + mConfig.warmupTimeout; + const auto reserveTimeout = mConfig.warmupTimeout; Log("app", - "Building settled output reserve: waiting for " + std::to_string(mConfig.warmupCompletedFrames) + - " completed frame(s) to remain ready for " + std::to_string(mConfig.startupSettle.count()) + " ms."); - if (mFrameExchange.WaitForStableCompletedDepth( - mConfig.warmupCompletedFrames, - mConfig.startupSettle, - reserveTimeout)) + "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 settled output reserve."; + error = "Timed out waiting for output preroll reserve."; return false; } diff --git a/apps/RenderCadenceCompositor/control/RuntimeStateJson.h b/apps/RenderCadenceCompositor/control/RuntimeStateJson.h index 8cb3fa4..56c03c6 100644 --- a/apps/RenderCadenceCompositor/control/RuntimeStateJson.h +++ b/apps/RenderCadenceCompositor/control/RuntimeStateJson.h @@ -251,7 +251,6 @@ inline std::string RuntimeStateToJson(const RuntimeStateJsonInput& input) writer.KeyUInt("maxTemporalHistoryFrames", static_cast(input.config.maxTemporalHistoryFrames)); writer.KeyDouble("previewFps", input.config.previewFps); writer.KeyBool("enableExternalKeying", input.config.deckLink.externalKeyingEnabled); - writer.KeyUInt("startupSettleMs", static_cast(input.config.startupSettle.count())); writer.KeyString("inputVideoFormat", input.config.inputVideoFormat); writer.KeyString("inputFrameRate", input.config.inputFrameRate); writer.KeyString("outputVideoFormat", input.config.outputVideoFormat); diff --git a/apps/RenderCadenceCompositor/frames/SystemFrameExchange.cpp b/apps/RenderCadenceCompositor/frames/SystemFrameExchange.cpp index 6f06682..7880789 100644 --- a/apps/RenderCadenceCompositor/frames/SystemFrameExchange.cpp +++ b/apps/RenderCadenceCompositor/frames/SystemFrameExchange.cpp @@ -69,6 +69,7 @@ bool SystemFrameExchange::PublishCompleted(const SystemFrame& frame) slot.state = SystemFrameSlotState::Completed; slot.frameIndex = frame.frameIndex; mCompletedIndices.push_back(frame.index); + TrimCompletedLocked(); ++mCounters.completedFrames; mCondition.notify_all(); return true; @@ -231,6 +232,38 @@ bool SystemFrameExchange::AcquireFreeLocked(SystemFrame& frame) return false; } +bool SystemFrameExchange::DropOldestCompletedLocked() +{ + while (!mCompletedIndices.empty()) + { + const std::size_t index = mCompletedIndices.front(); + mCompletedIndices.pop_front(); + if (index >= mSlots.size() || mSlots[index].state != SystemFrameSlotState::Completed) + continue; + + Slot& slot = mSlots[index]; + slot.state = SystemFrameSlotState::Free; + slot.frameIndex = 0; + ++slot.generation; + ++mCounters.completedDrops; + mCondition.notify_all(); + return true; + } + + return false; +} + +void SystemFrameExchange::TrimCompletedLocked() +{ + if (mConfig.maxCompletedFrames == 0) + return; + while (CompletedCountLocked() > mConfig.maxCompletedFrames) + { + if (!DropOldestCompletedLocked()) + return; + } +} + bool SystemFrameExchange::IsValidLocked(const SystemFrame& frame) const { return frame.index < mSlots.size() && mSlots[frame.index].generation == frame.generation; diff --git a/apps/RenderCadenceCompositor/frames/SystemFrameExchange.h b/apps/RenderCadenceCompositor/frames/SystemFrameExchange.h index 5dd429b..9767c7c 100644 --- a/apps/RenderCadenceCompositor/frames/SystemFrameExchange.h +++ b/apps/RenderCadenceCompositor/frames/SystemFrameExchange.h @@ -40,6 +40,8 @@ private: }; bool AcquireFreeLocked(SystemFrame& frame); + bool DropOldestCompletedLocked(); + void TrimCompletedLocked(); bool IsValidLocked(const SystemFrame& frame) const; void FillFrameLocked(std::size_t index, SystemFrame& frame); std::size_t CompletedCountLocked() const; diff --git a/apps/RenderCadenceCompositor/frames/SystemFrameTypes.h b/apps/RenderCadenceCompositor/frames/SystemFrameTypes.h index 7003b74..a35144f 100644 --- a/apps/RenderCadenceCompositor/frames/SystemFrameTypes.h +++ b/apps/RenderCadenceCompositor/frames/SystemFrameTypes.h @@ -20,6 +20,7 @@ struct SystemFrameExchangeConfig VideoIOPixelFormat pixelFormat = VideoIOPixelFormat::Bgra8; unsigned rowBytes = 0; std::size_t capacity = 0; + std::size_t maxCompletedFrames = 0; }; struct SystemFrame diff --git a/apps/RenderCadenceCompositor/telemetry/CadenceTelemetry.h b/apps/RenderCadenceCompositor/telemetry/CadenceTelemetry.h index 7236c7d..6bdd73f 100644 --- a/apps/RenderCadenceCompositor/telemetry/CadenceTelemetry.h +++ b/apps/RenderCadenceCompositor/telemetry/CadenceTelemetry.h @@ -53,6 +53,12 @@ struct CadenceTelemetrySnapshot bool deckLinkBufferedAvailable = false; uint64_t deckLinkBuffered = 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 @@ -92,6 +98,12 @@ public: snapshot.deckLinkBufferedAvailable = outputMetrics.actualBufferedFramesAvailable; snapshot.deckLinkBuffered = outputMetrics.actualBufferedFrames; 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) { diff --git a/apps/RenderCadenceCompositor/telemetry/CadenceTelemetryJson.h b/apps/RenderCadenceCompositor/telemetry/CadenceTelemetryJson.h index d159743..d4ef75d 100644 --- a/apps/RenderCadenceCompositor/telemetry/CadenceTelemetryJson.h +++ b/apps/RenderCadenceCompositor/telemetry/CadenceTelemetryJson.h @@ -61,6 +61,16 @@ inline void WriteCadenceTelemetryJson(JsonWriter& writer, const CadenceTelemetry else writer.Null(); 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(); } diff --git a/apps/RenderCadenceCompositor/video/DeckLinkOutput.cpp b/apps/RenderCadenceCompositor/video/DeckLinkOutput.cpp index 6311cf1..5212f8c 100644 --- a/apps/RenderCadenceCompositor/video/DeckLinkOutput.cpp +++ b/apps/RenderCadenceCompositor/video/DeckLinkOutput.cpp @@ -15,6 +15,7 @@ bool DeckLinkOutput::Initialize(const DeckLinkOutputConfig& config, CompletionCa mCompletionCallback = completionCallback; VideoFormatSelection formats; + formats.output = config.outputVideoMode; if (!mSession.DiscoverDevicesAndModes(formats, error)) return false; if (!mSession.SelectPreferredFormats(formats, config.outputAlphaRequired, error)) @@ -76,6 +77,12 @@ DeckLinkOutputMetrics DeckLinkOutput::Metrics() const metrics.actualBufferedFramesAvailable = state.actualDeckLinkBufferedFramesAvailable; metrics.actualBufferedFrames = state.actualDeckLinkBufferedFrames; 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; } diff --git a/apps/RenderCadenceCompositor/video/DeckLinkOutput.h b/apps/RenderCadenceCompositor/video/DeckLinkOutput.h index a153368..14cf58e 100644 --- a/apps/RenderCadenceCompositor/video/DeckLinkOutput.h +++ b/apps/RenderCadenceCompositor/video/DeckLinkOutput.h @@ -1,5 +1,6 @@ #pragma once +#include "DeckLinkDisplayMode.h" #include "DeckLinkSession.h" #include "VideoIOTypes.h" @@ -12,6 +13,7 @@ namespace RenderCadenceCompositor { struct DeckLinkOutputConfig { + VideoFormat outputVideoMode; bool externalKeyingEnabled = false; bool outputAlphaRequired = false; }; @@ -26,6 +28,12 @@ struct DeckLinkOutputMetrics bool actualBufferedFramesAvailable = false; uint64_t actualBufferedFrames = 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 diff --git a/apps/RenderCadenceCompositor/video/DeckLinkOutputThread.h b/apps/RenderCadenceCompositor/video/DeckLinkOutputThread.h index 0c06bac..37fc567 100644 --- a/apps/RenderCadenceCompositor/video/DeckLinkOutputThread.h +++ b/apps/RenderCadenceCompositor/video/DeckLinkOutputThread.h @@ -77,7 +77,11 @@ private: while (!mStopping) { const auto exchangeMetrics = mExchange.Metrics(); - if (exchangeMetrics.scheduledCount >= mConfig.targetBufferedFrames) + const auto outputMetrics = mOutput.Metrics(); + const std::size_t bufferedFrames = outputMetrics.actualBufferedFramesAvailable + ? static_cast(outputMetrics.actualBufferedFrames) + : exchangeMetrics.scheduledCount; + if (bufferedFrames >= mConfig.targetBufferedFrames) { std::this_thread::sleep_for(mConfig.idleSleep); continue; diff --git a/config/runtime-host.json b/config/runtime-host.json index 8fad7c5..b6944e7 100644 --- a/config/runtime-host.json +++ b/config/runtime-host.json @@ -11,6 +11,5 @@ "autoReload": true, "maxTemporalHistoryFrames": 12, "previewFps": 30, - "startupSettleMs": 5000, "enableExternalKeying": true } diff --git a/docs/openapi.yaml b/docs/openapi.yaml index ea47c36..fe73306 100644 --- a/docs/openapi.yaml +++ b/docs/openapi.yaml @@ -557,8 +557,6 @@ components: type: number previewFps: type: number - startupSettleMs: - type: number enableExternalKeying: type: boolean inputVideoFormat: @@ -721,6 +719,25 @@ components: type: number inputCaptureFormat: type: string + deckLinkScheduleLeadAvailable: + type: boolean + description: Whether DeckLink playback stream-time lead telemetry is currently available. + deckLinkScheduleLeadFrames: + type: number + nullable: true + description: Estimated number of frame intervals between the next app schedule timestamp and the DeckLink playback frame index. + deckLinkPlaybackFrameIndex: + type: number + description: DeckLink playback stream time converted to frame index at the configured output cadence. + deckLinkNextScheduleFrameIndex: + type: number + description: Next frame index the app scheduler will assign to a DeckLink output frame. + deckLinkPlaybackStreamTime: + type: number + description: Raw DeckLink scheduled playback stream time in the output mode time scale. + deckLinkScheduleRealignments: + type: number + description: Count of schedule-cursor recovery realignments triggered by DeckLink late/drop pressure. BackendPlayoutStatus: type: object properties: diff --git a/tests/RenderCadenceCompositorAppConfigProviderTests.cpp b/tests/RenderCadenceCompositorAppConfigProviderTests.cpp index 408fb50..25eff1d 100644 --- a/tests/RenderCadenceCompositorAppConfigProviderTests.cpp +++ b/tests/RenderCadenceCompositorAppConfigProviderTests.cpp @@ -37,7 +37,6 @@ std::filesystem::path WriteConfigFixture() << " \"autoReload\": false,\n" << " \"maxTemporalHistoryFrames\": 8,\n" << " \"previewFps\": 24,\n" - << " \"startupSettleMs\": 2500,\n" << " \"enableExternalKeying\": true\n" << "}\n"; return path; @@ -68,7 +67,6 @@ void TestLoadsRuntimeHostConfig() Expect(!config.autoReload, "auto reload loads"); Expect(config.maxTemporalHistoryFrames == 8, "history length loads"); Expect(config.previewFps == 24.0, "preview fps loads"); - Expect(config.startupSettle == std::chrono::milliseconds(2500), "startup settle loads"); Expect(config.deckLink.externalKeyingEnabled, "external keying loads"); std::filesystem::remove(path); diff --git a/tests/RenderCadenceCompositorFrameExchangeTests.cpp b/tests/RenderCadenceCompositorFrameExchangeTests.cpp index 9c7af12..62bd540 100644 --- a/tests/RenderCadenceCompositorFrameExchangeTests.cpp +++ b/tests/RenderCadenceCompositorFrameExchangeTests.cpp @@ -27,6 +27,13 @@ SystemFrameExchangeConfig MakeConfig(std::size_t capacity = 2) 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() { SystemFrameExchange exchange(MakeConfig(1)); @@ -82,6 +89,31 @@ void TestAcquirePreservesCompletedFrames() Expect(metrics.acquireMisses == 1, "preserving acquire miss is counted"); } +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() { SystemFrameExchange exchange(MakeConfig(1)); @@ -177,6 +209,7 @@ int main() { TestAcquirePublishesAndSchedules(); TestAcquirePreservesCompletedFrames(); + TestCompletedReserveIsBoundedFifo(); TestScheduledFramesAreNotDropped(); TestGenerationValidationRejectsStaleFrames(); TestPixelFormatAwareSizing(); diff --git a/tests/RenderCadenceCompositorTelemetryTests.cpp b/tests/RenderCadenceCompositorTelemetryTests.cpp index 627833a..7df4f9f 100644 --- a/tests/RenderCadenceCompositorTelemetryTests.cpp +++ b/tests/RenderCadenceCompositorTelemetryTests.cpp @@ -57,6 +57,12 @@ struct FakeOutputMetrics bool actualBufferedFramesAvailable = false; uint64_t actualBufferedFrames = 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 @@ -109,6 +115,12 @@ void TestTelemetrySamplesCompletedPollMissesAndShaderCounts() FakeOutput output; output.metrics.actualBufferedFramesAvailable = true; 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; outputThread.metrics.completedPollMisses = 12; @@ -163,6 +175,12 @@ void TestTelemetrySamplesCompletedPollMissesAndShaderCounts() Expect(snapshot.inputSignalPresent, "input signal present is sampled"); Expect(snapshot.deckLinkBufferedAvailable, "buffer telemetry availability 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() @@ -234,6 +252,12 @@ void TestTelemetrySerializesToJson() snapshot.deckLinkBufferedAvailable = true; snapshot.deckLinkBuffered = 4; 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 expected = @@ -259,7 +283,13 @@ void TestTelemetrySerializesToJson() "\"inputUnsupportedFrames\":3,\"inputSubmitMisses\":4," "\"inputCaptureFormat\":\"UYVY8\"," "\"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"); }