From 52eaf16a8ce7f4664826685b2c42086c47aeaaf3 Mon Sep 17 00:00:00 2001 From: Aiden <68633820+awils27@users.noreply.github.com> Date: Mon, 11 May 2026 20:45:58 +1000 Subject: [PATCH] Phase 7 step 2 --- CMakeLists.txt | 1 + .../gl/renderer/GlRenderConstants.h | 1 - .../videoio/VideoPlayoutPolicy.h | 34 +++++++++++++++++++ .../videoio/VideoPlayoutScheduler.cpp | 8 ++++- .../videoio/VideoPlayoutScheduler.h | 4 +++ .../videoio/decklink/DeckLinkSession.cpp | 12 ++++--- .../videoio/decklink/DeckLinkSession.h | 2 ++ ...HASE_7_BACKEND_LIFECYCLE_PLAYOUT_DESIGN.md | 18 +++++++--- tests/VideoPlayoutSchedulerTests.cpp | 29 ++++++++++++++++ 9 files changed, 97 insertions(+), 12 deletions(-) create mode 100644 apps/LoopThroughWithOpenGLCompositing/videoio/VideoPlayoutPolicy.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 48447c3..4dd1ec8 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -168,6 +168,7 @@ set(APP_SOURCES "${APP_DIR}/videoio/VideoBackendLifecycle.cpp" "${APP_DIR}/videoio/VideoBackendLifecycle.h" "${APP_DIR}/videoio/VideoIOTypes.h" + "${APP_DIR}/videoio/VideoPlayoutPolicy.h" "${APP_DIR}/videoio/VideoPlayoutScheduler.cpp" "${APP_DIR}/videoio/VideoPlayoutScheduler.h" ) diff --git a/apps/LoopThroughWithOpenGLCompositing/gl/renderer/GlRenderConstants.h b/apps/LoopThroughWithOpenGLCompositing/gl/renderer/GlRenderConstants.h index a6b6fbf..35c1cb7 100644 --- a/apps/LoopThroughWithOpenGLCompositing/gl/renderer/GlRenderConstants.h +++ b/apps/LoopThroughWithOpenGLCompositing/gl/renderer/GlRenderConstants.h @@ -7,4 +7,3 @@ constexpr GLuint kDecodedVideoTextureUnit = 1; constexpr GLuint kSourceHistoryTextureUnitBase = 2; constexpr GLuint kPackedVideoTextureUnit = 2; constexpr GLuint kGlobalParamsBindingPoint = 0; -constexpr unsigned kPrerollFrameCount = 12; diff --git a/apps/LoopThroughWithOpenGLCompositing/videoio/VideoPlayoutPolicy.h b/apps/LoopThroughWithOpenGLCompositing/videoio/VideoPlayoutPolicy.h new file mode 100644 index 0000000..47db122 --- /dev/null +++ b/apps/LoopThroughWithOpenGLCompositing/videoio/VideoPlayoutPolicy.h @@ -0,0 +1,34 @@ +#pragma once + +#include + +enum class VideoUnderrunBehavior +{ + ReuseLastCompletedFrame, + BlackFrame +}; + +struct VideoPlayoutPolicy +{ + unsigned outputFramePoolSize = 10; + unsigned targetPrerollFrames = 12; + unsigned targetReadyFrames = 2; + unsigned maxReadyFrames = 4; + unsigned minimumSpareDeviceFrames = 1; + uint64_t lateOrDropCatchUpFrames = 2; + VideoUnderrunBehavior underrunBehavior = VideoUnderrunBehavior::ReuseLastCompletedFrame; + bool adaptiveHeadroomEnabled = false; +}; + +inline VideoPlayoutPolicy NormalizeVideoPlayoutPolicy(VideoPlayoutPolicy policy) +{ + if (policy.outputFramePoolSize == 0) + policy.outputFramePoolSize = 1; + if (policy.targetPrerollFrames == 0) + policy.targetPrerollFrames = 1; + if (policy.targetReadyFrames == 0) + policy.targetReadyFrames = 1; + if (policy.maxReadyFrames < policy.targetReadyFrames) + policy.maxReadyFrames = policy.targetReadyFrames; + return policy; +} diff --git a/apps/LoopThroughWithOpenGLCompositing/videoio/VideoPlayoutScheduler.cpp b/apps/LoopThroughWithOpenGLCompositing/videoio/VideoPlayoutScheduler.cpp index e8068ed..f97af32 100644 --- a/apps/LoopThroughWithOpenGLCompositing/videoio/VideoPlayoutScheduler.cpp +++ b/apps/LoopThroughWithOpenGLCompositing/videoio/VideoPlayoutScheduler.cpp @@ -1,9 +1,15 @@ #include "VideoPlayoutScheduler.h" void VideoPlayoutScheduler::Configure(int64_t frameDuration, int64_t timeScale) +{ + Configure(frameDuration, timeScale, VideoPlayoutPolicy()); +} + +void VideoPlayoutScheduler::Configure(int64_t frameDuration, int64_t timeScale, const VideoPlayoutPolicy& policy) { mFrameDuration = frameDuration; mTimeScale = timeScale; + mPolicy = NormalizeVideoPlayoutPolicy(policy); Reset(); } @@ -26,7 +32,7 @@ VideoIOScheduleTime VideoPlayoutScheduler::NextScheduleTime() void VideoPlayoutScheduler::AccountForCompletionResult(VideoIOCompletionResult result) { if (result == VideoIOCompletionResult::DisplayedLate || result == VideoIOCompletionResult::Dropped) - mScheduledFrameIndex += 2; + mScheduledFrameIndex += mPolicy.lateOrDropCatchUpFrames; } double VideoPlayoutScheduler::FrameBudgetMilliseconds() const diff --git a/apps/LoopThroughWithOpenGLCompositing/videoio/VideoPlayoutScheduler.h b/apps/LoopThroughWithOpenGLCompositing/videoio/VideoPlayoutScheduler.h index 984f606..2c0d03a 100644 --- a/apps/LoopThroughWithOpenGLCompositing/videoio/VideoPlayoutScheduler.h +++ b/apps/LoopThroughWithOpenGLCompositing/videoio/VideoPlayoutScheduler.h @@ -1,6 +1,7 @@ #pragma once #include "VideoIOTypes.h" +#include "VideoPlayoutPolicy.h" #include @@ -8,15 +9,18 @@ class VideoPlayoutScheduler { public: void Configure(int64_t frameDuration, int64_t timeScale); + void Configure(int64_t frameDuration, int64_t timeScale, const VideoPlayoutPolicy& policy); void Reset(); VideoIOScheduleTime NextScheduleTime(); void AccountForCompletionResult(VideoIOCompletionResult result); double FrameBudgetMilliseconds() const; uint64_t ScheduledFrameIndex() const { return mScheduledFrameIndex; } int64_t TimeScale() const { return mTimeScale; } + const VideoPlayoutPolicy& Policy() const { return mPolicy; } private: int64_t mFrameDuration = 0; int64_t mTimeScale = 0; uint64_t mScheduledFrameIndex = 0; + VideoPlayoutPolicy mPolicy; }; diff --git a/apps/LoopThroughWithOpenGLCompositing/videoio/decklink/DeckLinkSession.cpp b/apps/LoopThroughWithOpenGLCompositing/videoio/decklink/DeckLinkSession.cpp index 2a29432..8d07e8a 100644 --- a/apps/LoopThroughWithOpenGLCompositing/videoio/decklink/DeckLinkSession.cpp +++ b/apps/LoopThroughWithOpenGLCompositing/videoio/decklink/DeckLinkSession.cpp @@ -1,7 +1,5 @@ #include "DeckLinkSession.h" -#include "GlRenderConstants.h" - #include #include #include @@ -210,7 +208,7 @@ bool DeckLinkSession::DiscoverDevicesAndModes(const VideoFormatSelection& videoM BMDTimeValue frameDuration = 0; BMDTimeScale frameTimescale = 0; outputMode->GetFrameRate(&frameDuration, &frameTimescale); - mScheduler.Configure(frameDuration, frameTimescale); + mScheduler.Configure(frameDuration, frameTimescale, mPlayoutPolicy); mState.frameBudgetMilliseconds = mScheduler.FrameBudgetMilliseconds(); mState.inputFrameRowBytes = mState.inputFrameSize.width * 2u; @@ -379,7 +377,9 @@ bool DeckLinkSession::ConfigureOutput(OutputFrameCallback callback, const VideoF mState.statusMessage = "Selected DeckLink output supports external keying. Set enableExternalKeying to true in runtime-host.json to request it."; } - for (int i = 0; i < 10; i++) + const VideoPlayoutPolicy policy = NormalizeVideoPlayoutPolicy(mPlayoutPolicy); + mPlayoutPolicy = policy; + for (unsigned i = 0; i < policy.outputFramePoolSize; i++) { CComPtr outputFrame; @@ -523,7 +523,9 @@ bool DeckLinkSession::Start() return false; } - for (unsigned i = 0; i < kPrerollFrameCount; i++) + const VideoPlayoutPolicy policy = NormalizeVideoPlayoutPolicy(mPlayoutPolicy); + mPlayoutPolicy = policy; + for (unsigned i = 0; i < policy.targetPrerollFrames; i++) { CComPtr outputVideoFrame; if (!AcquireNextOutputVideoFrame(outputVideoFrame)) diff --git a/apps/LoopThroughWithOpenGLCompositing/videoio/decklink/DeckLinkSession.h b/apps/LoopThroughWithOpenGLCompositing/videoio/decklink/DeckLinkSession.h index 5d695e8..6bd55fe 100644 --- a/apps/LoopThroughWithOpenGLCompositing/videoio/decklink/DeckLinkSession.h +++ b/apps/LoopThroughWithOpenGLCompositing/videoio/decklink/DeckLinkSession.h @@ -6,6 +6,7 @@ #include "DeckLinkVideoIOFormat.h" #include "VideoIOFormat.h" #include "VideoIOTypes.h" +#include "VideoPlayoutPolicy.h" #include "VideoPlayoutScheduler.h" #include @@ -79,6 +80,7 @@ private: CComPtr keyer; std::deque> outputVideoFrameQueue; VideoIOState mState; + VideoPlayoutPolicy mPlayoutPolicy; VideoPlayoutScheduler mScheduler; InputFrameCallback mInputFrameCallback; OutputFrameCallback mOutputFrameCallback; diff --git a/docs/PHASE_7_BACKEND_LIFECYCLE_PLAYOUT_DESIGN.md b/docs/PHASE_7_BACKEND_LIFECYCLE_PLAYOUT_DESIGN.md index 54e79c6..1e39755 100644 --- a/docs/PHASE_7_BACKEND_LIFECYCLE_PLAYOUT_DESIGN.md +++ b/docs/PHASE_7_BACKEND_LIFECYCLE_PLAYOUT_DESIGN.md @@ -9,13 +9,14 @@ Phase 5 clarified that live parameter layering stops at final render-state compo ## Status - Phase 7 design package: proposed. -- Phase 7 implementation: Step 1 complete. +- Phase 7 implementation: Step 2 complete. - Current alignment: `VideoBackend`, `VideoIODevice`, `DeckLinkSession`, `VideoBackendLifecycle`, and `VideoPlayoutScheduler` exist. Phase 4 removed callback-thread GL ownership, but the DeckLink completion path still waits for render-thread output production. Current backend footholds: - `VideoBackend` wraps device discovery/configuration, start/stop, input callback handling, output completion handling, and telemetry publication. - `DeckLinkSession` owns DeckLink device handles, frame pool creation, preroll, keyer configuration, and scheduled playback. +- `VideoPlayoutPolicy` names current frame pool, preroll, ready-frame, underrun, and catch-up policy defaults. - `VideoPlayoutScheduler` owns basic schedule time generation and simple late/drop skip-ahead behavior. - `OpenGLVideoIOBridge` is the current adapter between `VideoBackend` and `RenderEngine`. - `HealthTelemetry` receives some signal, render, and pacing stats. @@ -223,9 +224,16 @@ Unify fixed constants and scheduler assumptions. Initial target: -- frame pool size derives from policy -- preroll count derives from policy -- late/drop recovery reads policy +- [x] frame pool size derives from policy +- [x] preroll count derives from policy +- [x] late/drop recovery reads policy + +Current implementation: + +- `VideoPlayoutPolicy` defines current output frame pool, preroll, ready-frame, spare-buffer, underrun, catch-up, and adaptive-headroom settings. +- `DeckLinkSession` uses the policy for output frame pool creation and preroll count. +- `VideoPlayoutScheduler` stores the policy and uses `lateOrDropCatchUpFrames` instead of a hard-coded `+2` recovery step. +- `VideoPlayoutSchedulerTests` cover default compatibility behavior, policy-driven catch-up, and policy normalization. ### Step 3. Add Ready Output Queue @@ -321,7 +329,7 @@ Backend lifecycle and playout queue are related, but either can grow large. Impl Phase 7 can be considered complete once the project can say: - [x] backend lifecycle states and transitions are explicit -- [ ] playout policy owns preroll, pool size, headroom, and underrun behavior +- [x] playout policy owns preroll, pool size, headroom, and underrun behavior - [ ] output callbacks no longer synchronously wait for render production - [ ] render produces completed output frames into a bounded queue - [ ] underrun behavior is explicit and observable diff --git a/tests/VideoPlayoutSchedulerTests.cpp b/tests/VideoPlayoutSchedulerTests.cpp index 28b2b13..40bf66c 100644 --- a/tests/VideoPlayoutSchedulerTests.cpp +++ b/tests/VideoPlayoutSchedulerTests.cpp @@ -50,6 +50,33 @@ void TestLateAndDroppedSkipAhead() Expect(scheduler.NextScheduleTime().streamTime == 6000, "dropped completion preserves the existing two-frame skip policy"); } +void TestLateAndDroppedRecoveryUsesPolicy() +{ + VideoPlayoutPolicy policy; + policy.lateOrDropCatchUpFrames = 4; + + VideoPlayoutScheduler scheduler; + scheduler.Configure(1000, 50000, policy); + + (void)scheduler.NextScheduleTime(); + scheduler.AccountForCompletionResult(VideoIOCompletionResult::Dropped); + Expect(scheduler.NextScheduleTime().streamTime == 5000, "drop recovery uses policy catch-up frame count"); +} + +void TestPolicyNormalization() +{ + VideoPlayoutPolicy policy; + policy.outputFramePoolSize = 0; + policy.targetPrerollFrames = 0; + policy.targetReadyFrames = 5; + policy.maxReadyFrames = 2; + + VideoPlayoutPolicy normalized = NormalizeVideoPlayoutPolicy(policy); + Expect(normalized.outputFramePoolSize == 1, "policy normalization keeps at least one output frame"); + Expect(normalized.targetPrerollFrames == 1, "policy normalization keeps at least one preroll frame"); + Expect(normalized.maxReadyFrames == normalized.targetReadyFrames, "policy normalization keeps max ready frames above target"); +} + void TestFrameBudgets() { VideoPlayoutScheduler scheduler; @@ -68,6 +95,8 @@ int main() { TestScheduleAdvancesFromZero(); TestLateAndDroppedSkipAhead(); + TestLateAndDroppedRecoveryUsesPolicy(); + TestPolicyNormalization(); TestFrameBudgets(); if (gFailures != 0)