From 50d58808353c94ad32501bcdd6d63381a855e433 Mon Sep 17 00:00:00 2001 From: Aiden <68633820+awils27@users.noreply.github.com> Date: Mon, 11 May 2026 20:49:36 +1000 Subject: [PATCH] Step 3 --- CMakeLists.txt | 19 +++ .../videoio/RenderOutputQueue.cpp | 70 +++++++++++ .../videoio/RenderOutputQueue.h | 48 ++++++++ ...HASE_7_BACKEND_LIFECYCLE_PLAYOUT_DESIGN.md | 17 ++- tests/RenderOutputQueueTests.cpp | 110 ++++++++++++++++++ 5 files changed, 260 insertions(+), 4 deletions(-) create mode 100644 apps/LoopThroughWithOpenGLCompositing/videoio/RenderOutputQueue.cpp create mode 100644 apps/LoopThroughWithOpenGLCompositing/videoio/RenderOutputQueue.h create mode 100644 tests/RenderOutputQueueTests.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 4dd1ec8..29bd2ee 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -168,6 +168,8 @@ set(APP_SOURCES "${APP_DIR}/videoio/VideoBackendLifecycle.cpp" "${APP_DIR}/videoio/VideoBackendLifecycle.h" "${APP_DIR}/videoio/VideoIOTypes.h" + "${APP_DIR}/videoio/RenderOutputQueue.cpp" + "${APP_DIR}/videoio/RenderOutputQueue.h" "${APP_DIR}/videoio/VideoPlayoutPolicy.h" "${APP_DIR}/videoio/VideoPlayoutScheduler.cpp" "${APP_DIR}/videoio/VideoPlayoutScheduler.h" @@ -541,6 +543,23 @@ endif() add_test(NAME VideoPlayoutSchedulerTests COMMAND VideoPlayoutSchedulerTests) +add_executable(RenderOutputQueueTests + "${APP_DIR}/videoio/RenderOutputQueue.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/tests/RenderOutputQueueTests.cpp" +) + +target_include_directories(RenderOutputQueueTests PRIVATE + "${APP_DIR}" + "${APP_DIR}/videoio" + "${APP_DIR}/videoio/decklink" +) + +if(MSVC) + target_compile_options(RenderOutputQueueTests PRIVATE /W3) +endif() + +add_test(NAME RenderOutputQueueTests COMMAND RenderOutputQueueTests) + add_executable(VideoBackendLifecycleTests "${APP_DIR}/videoio/VideoBackendLifecycle.cpp" "${CMAKE_CURRENT_SOURCE_DIR}/tests/VideoBackendLifecycleTests.cpp" diff --git a/apps/LoopThroughWithOpenGLCompositing/videoio/RenderOutputQueue.cpp b/apps/LoopThroughWithOpenGLCompositing/videoio/RenderOutputQueue.cpp new file mode 100644 index 0000000..9d3aab5 --- /dev/null +++ b/apps/LoopThroughWithOpenGLCompositing/videoio/RenderOutputQueue.cpp @@ -0,0 +1,70 @@ +#include "RenderOutputQueue.h" + +RenderOutputQueue::RenderOutputQueue(const VideoPlayoutPolicy& policy) : + mPolicy(NormalizeVideoPlayoutPolicy(policy)) +{ +} + +void RenderOutputQueue::Configure(const VideoPlayoutPolicy& policy) +{ + std::lock_guard lock(mMutex); + mPolicy = NormalizeVideoPlayoutPolicy(policy); + while (mReadyFrames.size() > CapacityLocked()) + { + mReadyFrames.pop_front(); + ++mDroppedCount; + } +} + +bool RenderOutputQueue::Push(RenderOutputFrame frame) +{ + std::lock_guard lock(mMutex); + if (mReadyFrames.size() >= CapacityLocked()) + { + mReadyFrames.pop_front(); + ++mDroppedCount; + } + + mReadyFrames.push_back(frame); + ++mPushedCount; + return true; +} + +bool RenderOutputQueue::TryPop(RenderOutputFrame& frame) +{ + std::lock_guard lock(mMutex); + if (mReadyFrames.empty()) + { + ++mUnderrunCount; + return false; + } + + frame = mReadyFrames.front(); + mReadyFrames.pop_front(); + ++mPoppedCount; + return true; +} + +void RenderOutputQueue::Clear() +{ + std::lock_guard lock(mMutex); + mReadyFrames.clear(); +} + +RenderOutputQueueMetrics RenderOutputQueue::GetMetrics() const +{ + std::lock_guard lock(mMutex); + RenderOutputQueueMetrics metrics; + metrics.depth = mReadyFrames.size(); + metrics.capacity = CapacityLocked(); + metrics.pushedCount = mPushedCount; + metrics.poppedCount = mPoppedCount; + metrics.droppedCount = mDroppedCount; + metrics.underrunCount = mUnderrunCount; + return metrics; +} + +std::size_t RenderOutputQueue::CapacityLocked() const +{ + return static_cast(mPolicy.maxReadyFrames); +} diff --git a/apps/LoopThroughWithOpenGLCompositing/videoio/RenderOutputQueue.h b/apps/LoopThroughWithOpenGLCompositing/videoio/RenderOutputQueue.h new file mode 100644 index 0000000..649e539 --- /dev/null +++ b/apps/LoopThroughWithOpenGLCompositing/videoio/RenderOutputQueue.h @@ -0,0 +1,48 @@ +#pragma once + +#include "VideoIOTypes.h" +#include "VideoPlayoutPolicy.h" + +#include +#include +#include + +struct RenderOutputFrame +{ + VideoIOOutputFrame frame; + uint64_t frameIndex = 0; + bool stale = false; +}; + +struct RenderOutputQueueMetrics +{ + std::size_t depth = 0; + std::size_t capacity = 0; + uint64_t pushedCount = 0; + uint64_t poppedCount = 0; + uint64_t droppedCount = 0; + uint64_t underrunCount = 0; +}; + +class RenderOutputQueue +{ +public: + explicit RenderOutputQueue(const VideoPlayoutPolicy& policy = VideoPlayoutPolicy()); + + void Configure(const VideoPlayoutPolicy& policy); + bool Push(RenderOutputFrame frame); + bool TryPop(RenderOutputFrame& frame); + void Clear(); + RenderOutputQueueMetrics GetMetrics() const; + +private: + std::size_t CapacityLocked() const; + + mutable std::mutex mMutex; + VideoPlayoutPolicy mPolicy; + std::deque mReadyFrames; + uint64_t mPushedCount = 0; + uint64_t mPoppedCount = 0; + uint64_t mDroppedCount = 0; + uint64_t mUnderrunCount = 0; +}; diff --git a/docs/PHASE_7_BACKEND_LIFECYCLE_PLAYOUT_DESIGN.md b/docs/PHASE_7_BACKEND_LIFECYCLE_PLAYOUT_DESIGN.md index 1e39755..4f88046 100644 --- a/docs/PHASE_7_BACKEND_LIFECYCLE_PLAYOUT_DESIGN.md +++ b/docs/PHASE_7_BACKEND_LIFECYCLE_PLAYOUT_DESIGN.md @@ -9,7 +9,7 @@ Phase 5 clarified that live parameter layering stops at final render-state compo ## Status - Phase 7 design package: proposed. -- Phase 7 implementation: Step 2 complete. +- Phase 7 implementation: Step 3 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: @@ -17,6 +17,7 @@ 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. +- `RenderOutputQueue` names the future bounded ready-output-frame handoff and has pure queue tests. - `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. @@ -241,9 +242,17 @@ Introduce a bounded queue for completed output frames. Initial target: -- pure queue tests -- explicit depth/underrun metrics -- no DeckLink dependency in queue tests +- [x] pure queue tests +- [x] explicit depth/underrun metrics +- [x] no DeckLink dependency in queue tests + +Current implementation: + +- `RenderOutputQueue` owns a bounded FIFO of `RenderOutputFrame` values. +- The queue is configured from `VideoPlayoutPolicy::maxReadyFrames`. +- Queue metrics report depth, capacity, pushed, popped, dropped, and underrun counts. +- Overflow drops the oldest ready frame, preserving the newest completed output for scheduling. +- `RenderOutputQueueTests` cover ordering, bounded overflow, underrun counting, and capacity shrink behavior without DeckLink hardware. ### Step 4. Move Callback Toward Dequeue/Schedule diff --git a/tests/RenderOutputQueueTests.cpp b/tests/RenderOutputQueueTests.cpp new file mode 100644 index 0000000..e2e1584 --- /dev/null +++ b/tests/RenderOutputQueueTests.cpp @@ -0,0 +1,110 @@ +#include "RenderOutputQueue.h" + +#include + +namespace +{ +int gFailures = 0; + +void Expect(bool condition, const char* message) +{ + if (condition) + return; + + std::cerr << "FAIL: " << message << "\n"; + ++gFailures; +} + +RenderOutputFrame MakeFrame(uint64_t index) +{ + RenderOutputFrame frame; + frame.frameIndex = index; + frame.frame.nativeFrame = reinterpret_cast(static_cast(index + 1)); + return frame; +} + +void TestQueuePreservesOrdering() +{ + VideoPlayoutPolicy policy; + policy.maxReadyFrames = 3; + RenderOutputQueue queue(policy); + + Expect(queue.Push(MakeFrame(1)), "first ready frame pushes"); + Expect(queue.Push(MakeFrame(2)), "second ready frame pushes"); + + RenderOutputFrame frame; + Expect(queue.TryPop(frame), "first ready frame pops"); + Expect(frame.frameIndex == 1, "queue pops first frame first"); + Expect(queue.TryPop(frame), "second ready frame pops"); + Expect(frame.frameIndex == 2, "queue pops second frame second"); +} + +void TestBoundedQueueDropsOldestFrame() +{ + VideoPlayoutPolicy policy; + policy.maxReadyFrames = 2; + RenderOutputQueue queue(policy); + + queue.Push(MakeFrame(1)); + queue.Push(MakeFrame(2)); + queue.Push(MakeFrame(3)); + + RenderOutputQueueMetrics metrics = queue.GetMetrics(); + Expect(metrics.depth == 2, "bounded queue depth stays at capacity"); + Expect(metrics.droppedCount == 1, "bounded queue counts dropped oldest frame"); + + RenderOutputFrame frame; + Expect(queue.TryPop(frame), "bounded queue pops after drop"); + Expect(frame.frameIndex == 2, "oldest frame was dropped when queue overflowed"); +} + +void TestUnderrunIsCounted() +{ + RenderOutputQueue queue; + RenderOutputFrame frame; + Expect(!queue.TryPop(frame), "empty queue reports underrun"); + + RenderOutputQueueMetrics metrics = queue.GetMetrics(); + Expect(metrics.underrunCount == 1, "empty pop increments underrun count"); +} + +void TestConfigureShrinksDepthToNewCapacity() +{ + VideoPlayoutPolicy policy; + policy.maxReadyFrames = 4; + RenderOutputQueue queue(policy); + queue.Push(MakeFrame(1)); + queue.Push(MakeFrame(2)); + queue.Push(MakeFrame(3)); + + VideoPlayoutPolicy smallerPolicy; + smallerPolicy.targetReadyFrames = 1; + smallerPolicy.maxReadyFrames = 1; + queue.Configure(smallerPolicy); + + RenderOutputQueueMetrics metrics = queue.GetMetrics(); + Expect(metrics.depth == 1, "configure trims queue to new capacity"); + Expect(metrics.droppedCount == 2, "configure counts trimmed frames as drops"); + + RenderOutputFrame frame; + Expect(queue.TryPop(frame), "trimmed queue still has newest frame"); + Expect(frame.frameIndex == 3, "configure keeps newest ready frame"); +} +} + +int main() +{ + TestQueuePreservesOrdering(); + TestBoundedQueueDropsOldestFrame(); + TestUnderrunIsCounted(); + TestConfigureShrinksDepthToNewCapacity(); + + if (gFailures != 0) + { + std::cerr << gFailures << " render output queue test failure(s).\n"; + return 1; + } + + std::cout << "RenderOutputQueue tests passed.\n"; + return 0; +}