Step 3
This commit is contained in:
@@ -168,6 +168,8 @@ set(APP_SOURCES
|
|||||||
"${APP_DIR}/videoio/VideoBackendLifecycle.cpp"
|
"${APP_DIR}/videoio/VideoBackendLifecycle.cpp"
|
||||||
"${APP_DIR}/videoio/VideoBackendLifecycle.h"
|
"${APP_DIR}/videoio/VideoBackendLifecycle.h"
|
||||||
"${APP_DIR}/videoio/VideoIOTypes.h"
|
"${APP_DIR}/videoio/VideoIOTypes.h"
|
||||||
|
"${APP_DIR}/videoio/RenderOutputQueue.cpp"
|
||||||
|
"${APP_DIR}/videoio/RenderOutputQueue.h"
|
||||||
"${APP_DIR}/videoio/VideoPlayoutPolicy.h"
|
"${APP_DIR}/videoio/VideoPlayoutPolicy.h"
|
||||||
"${APP_DIR}/videoio/VideoPlayoutScheduler.cpp"
|
"${APP_DIR}/videoio/VideoPlayoutScheduler.cpp"
|
||||||
"${APP_DIR}/videoio/VideoPlayoutScheduler.h"
|
"${APP_DIR}/videoio/VideoPlayoutScheduler.h"
|
||||||
@@ -541,6 +543,23 @@ endif()
|
|||||||
|
|
||||||
add_test(NAME VideoPlayoutSchedulerTests COMMAND VideoPlayoutSchedulerTests)
|
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
|
add_executable(VideoBackendLifecycleTests
|
||||||
"${APP_DIR}/videoio/VideoBackendLifecycle.cpp"
|
"${APP_DIR}/videoio/VideoBackendLifecycle.cpp"
|
||||||
"${CMAKE_CURRENT_SOURCE_DIR}/tests/VideoBackendLifecycleTests.cpp"
|
"${CMAKE_CURRENT_SOURCE_DIR}/tests/VideoBackendLifecycleTests.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<std::mutex> lock(mMutex);
|
||||||
|
mPolicy = NormalizeVideoPlayoutPolicy(policy);
|
||||||
|
while (mReadyFrames.size() > CapacityLocked())
|
||||||
|
{
|
||||||
|
mReadyFrames.pop_front();
|
||||||
|
++mDroppedCount;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool RenderOutputQueue::Push(RenderOutputFrame frame)
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> 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<std::mutex> lock(mMutex);
|
||||||
|
if (mReadyFrames.empty())
|
||||||
|
{
|
||||||
|
++mUnderrunCount;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
frame = mReadyFrames.front();
|
||||||
|
mReadyFrames.pop_front();
|
||||||
|
++mPoppedCount;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void RenderOutputQueue::Clear()
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(mMutex);
|
||||||
|
mReadyFrames.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
RenderOutputQueueMetrics RenderOutputQueue::GetMetrics() const
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> 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<std::size_t>(mPolicy.maxReadyFrames);
|
||||||
|
}
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "VideoIOTypes.h"
|
||||||
|
#include "VideoPlayoutPolicy.h"
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
#include <deque>
|
||||||
|
#include <mutex>
|
||||||
|
|
||||||
|
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<RenderOutputFrame> mReadyFrames;
|
||||||
|
uint64_t mPushedCount = 0;
|
||||||
|
uint64_t mPoppedCount = 0;
|
||||||
|
uint64_t mDroppedCount = 0;
|
||||||
|
uint64_t mUnderrunCount = 0;
|
||||||
|
};
|
||||||
@@ -9,7 +9,7 @@ Phase 5 clarified that live parameter layering stops at final render-state compo
|
|||||||
## Status
|
## Status
|
||||||
|
|
||||||
- Phase 7 design package: proposed.
|
- 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 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:
|
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.
|
- `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.
|
- `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.
|
- `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.
|
- `VideoPlayoutScheduler` owns basic schedule time generation and simple late/drop skip-ahead behavior.
|
||||||
- `OpenGLVideoIOBridge` is the current adapter between `VideoBackend` and `RenderEngine`.
|
- `OpenGLVideoIOBridge` is the current adapter between `VideoBackend` and `RenderEngine`.
|
||||||
- `HealthTelemetry` receives some signal, render, and pacing stats.
|
- `HealthTelemetry` receives some signal, render, and pacing stats.
|
||||||
@@ -241,9 +242,17 @@ Introduce a bounded queue for completed output frames.
|
|||||||
|
|
||||||
Initial target:
|
Initial target:
|
||||||
|
|
||||||
- pure queue tests
|
- [x] pure queue tests
|
||||||
- explicit depth/underrun metrics
|
- [x] explicit depth/underrun metrics
|
||||||
- no DeckLink dependency in queue tests
|
- [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
|
### Step 4. Move Callback Toward Dequeue/Schedule
|
||||||
|
|
||||||
|
|||||||
110
tests/RenderOutputQueueTests.cpp
Normal file
110
tests/RenderOutputQueueTests.cpp
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
#include "RenderOutputQueue.h"
|
||||||
|
|
||||||
|
#include <iostream>
|
||||||
|
|
||||||
|
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<void*>(static_cast<uintptr_t>(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;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user