Phase 7.5 step 2
This commit is contained in:
@@ -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/OutputProductionController.cpp"
|
||||
"${APP_DIR}/videoio/OutputProductionController.h"
|
||||
"${APP_DIR}/videoio/RenderOutputQueue.cpp"
|
||||
"${APP_DIR}/videoio/RenderOutputQueue.h"
|
||||
"${APP_DIR}/videoio/VideoPlayoutPolicy.h"
|
||||
@@ -543,6 +545,22 @@ endif()
|
||||
|
||||
add_test(NAME VideoPlayoutSchedulerTests COMMAND VideoPlayoutSchedulerTests)
|
||||
|
||||
add_executable(OutputProductionControllerTests
|
||||
"${APP_DIR}/videoio/OutputProductionController.cpp"
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/tests/OutputProductionControllerTests.cpp"
|
||||
)
|
||||
|
||||
target_include_directories(OutputProductionControllerTests PRIVATE
|
||||
"${APP_DIR}"
|
||||
"${APP_DIR}/videoio"
|
||||
)
|
||||
|
||||
if(MSVC)
|
||||
target_compile_options(OutputProductionControllerTests PRIVATE /W3)
|
||||
endif()
|
||||
|
||||
add_test(NAME OutputProductionControllerTests COMMAND OutputProductionControllerTests)
|
||||
|
||||
add_executable(RenderOutputQueueTests
|
||||
"${APP_DIR}/videoio/RenderOutputQueue.cpp"
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/tests/RenderOutputQueueTests.cpp"
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
#include "OutputProductionController.h"
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
namespace
|
||||
{
|
||||
std::size_t ClampReadyLimit(unsigned value, std::size_t capacity)
|
||||
{
|
||||
const std::size_t requested = static_cast<std::size_t>(value);
|
||||
if (capacity == 0)
|
||||
return requested;
|
||||
return (std::min)(requested, capacity);
|
||||
}
|
||||
}
|
||||
|
||||
OutputProductionController::OutputProductionController(const VideoPlayoutPolicy& policy) :
|
||||
mPolicy(NormalizeVideoPlayoutPolicy(policy))
|
||||
{
|
||||
}
|
||||
|
||||
void OutputProductionController::Configure(const VideoPlayoutPolicy& policy)
|
||||
{
|
||||
mPolicy = NormalizeVideoPlayoutPolicy(policy);
|
||||
}
|
||||
|
||||
OutputProductionDecision OutputProductionController::Decide(const OutputProductionPressure& pressure) const
|
||||
{
|
||||
OutputProductionDecision decision;
|
||||
|
||||
const std::size_t configuredMaxReadyFrames = static_cast<std::size_t>(mPolicy.maxReadyFrames);
|
||||
const std::size_t effectiveMaxReadyFrames = pressure.readyQueueCapacity > 0
|
||||
? (std::min)(configuredMaxReadyFrames, pressure.readyQueueCapacity)
|
||||
: configuredMaxReadyFrames;
|
||||
const std::size_t effectiveTargetReadyFrames = (std::min)(
|
||||
ClampReadyLimit(mPolicy.targetReadyFrames, pressure.readyQueueCapacity),
|
||||
effectiveMaxReadyFrames);
|
||||
|
||||
decision.targetReadyFrames = effectiveTargetReadyFrames;
|
||||
decision.maxReadyFrames = effectiveMaxReadyFrames;
|
||||
|
||||
if (effectiveMaxReadyFrames == 0)
|
||||
{
|
||||
decision.action = OutputProductionAction::Throttle;
|
||||
decision.reason = "no-ready-frame-capacity";
|
||||
return decision;
|
||||
}
|
||||
|
||||
if (pressure.readyQueueDepth >= effectiveMaxReadyFrames)
|
||||
{
|
||||
decision.action = OutputProductionAction::Throttle;
|
||||
decision.reason = "ready-queue-full";
|
||||
return decision;
|
||||
}
|
||||
|
||||
if (pressure.readyQueueDepth < effectiveTargetReadyFrames)
|
||||
{
|
||||
decision.action = OutputProductionAction::Produce;
|
||||
decision.requestedFrames = effectiveTargetReadyFrames - pressure.readyQueueDepth;
|
||||
decision.reason = "ready-queue-below-target";
|
||||
return decision;
|
||||
}
|
||||
|
||||
if ((pressure.lateStreak > 0 || pressure.dropStreak > 0 || pressure.readyQueueUnderrunCount > 0) &&
|
||||
pressure.readyQueueDepth < effectiveMaxReadyFrames)
|
||||
{
|
||||
decision.action = OutputProductionAction::Produce;
|
||||
decision.requestedFrames = 1;
|
||||
decision.reason = "playout-pressure";
|
||||
return decision;
|
||||
}
|
||||
|
||||
decision.action = OutputProductionAction::Wait;
|
||||
decision.reason = "ready-queue-at-target";
|
||||
return decision;
|
||||
}
|
||||
|
||||
const char* OutputProductionActionName(OutputProductionAction action)
|
||||
{
|
||||
switch (action)
|
||||
{
|
||||
case OutputProductionAction::Produce:
|
||||
return "Produce";
|
||||
case OutputProductionAction::Throttle:
|
||||
return "Throttle";
|
||||
case OutputProductionAction::Wait:
|
||||
default:
|
||||
return "Wait";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
#pragma once
|
||||
|
||||
#include "VideoPlayoutPolicy.h"
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
|
||||
enum class OutputProductionAction
|
||||
{
|
||||
Produce,
|
||||
Wait,
|
||||
Throttle
|
||||
};
|
||||
|
||||
struct OutputProductionPressure
|
||||
{
|
||||
std::size_t readyQueueDepth = 0;
|
||||
std::size_t readyQueueCapacity = 0;
|
||||
uint64_t readyQueueUnderrunCount = 0;
|
||||
uint64_t lateStreak = 0;
|
||||
uint64_t dropStreak = 0;
|
||||
};
|
||||
|
||||
struct OutputProductionDecision
|
||||
{
|
||||
OutputProductionAction action = OutputProductionAction::Wait;
|
||||
std::size_t requestedFrames = 0;
|
||||
std::size_t targetReadyFrames = 0;
|
||||
std::size_t maxReadyFrames = 0;
|
||||
std::string reason;
|
||||
};
|
||||
|
||||
class OutputProductionController
|
||||
{
|
||||
public:
|
||||
explicit OutputProductionController(const VideoPlayoutPolicy& policy = VideoPlayoutPolicy());
|
||||
|
||||
void Configure(const VideoPlayoutPolicy& policy);
|
||||
OutputProductionDecision Decide(const OutputProductionPressure& pressure) const;
|
||||
|
||||
private:
|
||||
VideoPlayoutPolicy mPolicy;
|
||||
};
|
||||
|
||||
const char* OutputProductionActionName(OutputProductionAction action);
|
||||
@@ -7,7 +7,7 @@ Phase 7 made backend lifecycle, playout policy, ready-frame queueing, late/drop
|
||||
## Status
|
||||
|
||||
- Phase 7.5 design package: proposed.
|
||||
- Phase 7.5 implementation: Step 1 in progress.
|
||||
- Phase 7.5 implementation: Step 2 in progress.
|
||||
- Current alignment: Phase 7 is complete. `RenderOutputQueue`, `VideoPlayoutPolicy`, `VideoPlayoutScheduler`, `VideoBackendLifecycle`, and backend playout telemetry exist. The backend worker fills the ready queue on completion demand, but render production is not yet proactively driven by queue pressure or video cadence.
|
||||
|
||||
Current footholds:
|
||||
@@ -18,6 +18,7 @@ Current footholds:
|
||||
- `VideoPlayoutPolicy` names ready-frame headroom and catch-up policy.
|
||||
- `HealthTelemetry::BackendPlayoutSnapshot` exposes queue depth, underruns, late/drop streaks, and recovery decisions.
|
||||
- Step 1 adds baseline timing fields for ready-queue min/max/zero-depth samples and output render duration.
|
||||
- Step 2 adds a pure `OutputProductionController` for queue-pressure production decisions.
|
||||
|
||||
## Timing Review Findings
|
||||
|
||||
@@ -177,13 +178,20 @@ Introduce a pure policy helper for queue-pressure decisions.
|
||||
|
||||
Initial target:
|
||||
|
||||
- input: ready depth, capacity, target depth, late/drop streaks, underrun count
|
||||
- output: produce, wait, or throttle
|
||||
- tests cover low queue, full queue, late/drop pressure, and normalized policy values
|
||||
- [x] input: ready depth, capacity, target depth, late/drop streaks, underrun count
|
||||
- [x] output: produce, wait, or throttle
|
||||
- [x] tests cover low queue, full queue, late/drop pressure, and normalized policy values
|
||||
|
||||
Exit criteria:
|
||||
|
||||
- production cadence policy can evolve without touching DeckLink or GL code
|
||||
- [x] production cadence policy can evolve without touching DeckLink or GL code
|
||||
|
||||
Implementation notes:
|
||||
|
||||
- `OutputProductionController` lives in `videoio` and depends only on `VideoPlayoutPolicy`.
|
||||
- `OutputProductionPressure` carries ready-queue depth/capacity plus underrun and late/drop pressure.
|
||||
- `OutputProductionDecision` returns `Produce`, `Wait`, or `Throttle`, a requested frame count, effective target/max limits, and a reason string.
|
||||
- Step 2 is intentionally not wired into live playback yet. Step 3 should use this policy to drive the proactive producer loop.
|
||||
|
||||
### Step 3. Add A Proactive Producer Loop
|
||||
|
||||
|
||||
142
tests/OutputProductionControllerTests.cpp
Normal file
142
tests/OutputProductionControllerTests.cpp
Normal file
@@ -0,0 +1,142 @@
|
||||
#include "OutputProductionController.h"
|
||||
|
||||
#include <iostream>
|
||||
#include <string>
|
||||
|
||||
namespace
|
||||
{
|
||||
int gFailures = 0;
|
||||
|
||||
void Expect(bool condition, const char* message)
|
||||
{
|
||||
if (condition)
|
||||
return;
|
||||
|
||||
std::cerr << "FAIL: " << message << "\n";
|
||||
++gFailures;
|
||||
}
|
||||
|
||||
void TestLowQueueRequestsProductionToTarget()
|
||||
{
|
||||
VideoPlayoutPolicy policy;
|
||||
policy.targetReadyFrames = 3;
|
||||
policy.maxReadyFrames = 5;
|
||||
OutputProductionController controller(policy);
|
||||
|
||||
OutputProductionPressure pressure;
|
||||
pressure.readyQueueDepth = 1;
|
||||
pressure.readyQueueCapacity = 5;
|
||||
|
||||
const OutputProductionDecision decision = controller.Decide(pressure);
|
||||
Expect(decision.action == OutputProductionAction::Produce, "low ready queue requests production");
|
||||
Expect(decision.requestedFrames == 2, "low ready queue requests enough frames to reach target");
|
||||
Expect(decision.targetReadyFrames == 3, "decision reports effective target");
|
||||
Expect(decision.maxReadyFrames == 5, "decision reports effective max");
|
||||
Expect(decision.reason == "ready-queue-below-target", "low queue decision names reason");
|
||||
}
|
||||
|
||||
void TestFullQueueThrottles()
|
||||
{
|
||||
VideoPlayoutPolicy policy;
|
||||
policy.targetReadyFrames = 2;
|
||||
policy.maxReadyFrames = 4;
|
||||
OutputProductionController controller(policy);
|
||||
|
||||
OutputProductionPressure pressure;
|
||||
pressure.readyQueueDepth = 4;
|
||||
pressure.readyQueueCapacity = 4;
|
||||
|
||||
const OutputProductionDecision decision = controller.Decide(pressure);
|
||||
Expect(decision.action == OutputProductionAction::Throttle, "full ready queue throttles production");
|
||||
Expect(decision.requestedFrames == 0, "full ready queue requests no frames");
|
||||
Expect(decision.reason == "ready-queue-full", "full queue decision names reason");
|
||||
}
|
||||
|
||||
void TestAtTargetWaitsWithoutPressure()
|
||||
{
|
||||
VideoPlayoutPolicy policy;
|
||||
policy.targetReadyFrames = 2;
|
||||
policy.maxReadyFrames = 4;
|
||||
OutputProductionController controller(policy);
|
||||
|
||||
OutputProductionPressure pressure;
|
||||
pressure.readyQueueDepth = 2;
|
||||
pressure.readyQueueCapacity = 4;
|
||||
|
||||
const OutputProductionDecision decision = controller.Decide(pressure);
|
||||
Expect(decision.action == OutputProductionAction::Wait, "ready queue at target waits without pressure");
|
||||
Expect(decision.requestedFrames == 0, "wait decision requests no frames");
|
||||
Expect(decision.reason == "ready-queue-at-target", "wait decision names reason");
|
||||
}
|
||||
|
||||
void TestLateDropPressureRequestsHeadroom()
|
||||
{
|
||||
VideoPlayoutPolicy policy;
|
||||
policy.targetReadyFrames = 2;
|
||||
policy.maxReadyFrames = 4;
|
||||
OutputProductionController controller(policy);
|
||||
|
||||
OutputProductionPressure pressure;
|
||||
pressure.readyQueueDepth = 2;
|
||||
pressure.readyQueueCapacity = 4;
|
||||
pressure.lateStreak = 1;
|
||||
|
||||
OutputProductionDecision decision = controller.Decide(pressure);
|
||||
Expect(decision.action == OutputProductionAction::Produce, "late pressure requests extra headroom");
|
||||
Expect(decision.requestedFrames == 1, "late pressure requests one frame");
|
||||
Expect(decision.reason == "playout-pressure", "late pressure decision names reason");
|
||||
|
||||
pressure.lateStreak = 0;
|
||||
pressure.dropStreak = 2;
|
||||
decision = controller.Decide(pressure);
|
||||
Expect(decision.action == OutputProductionAction::Produce, "drop pressure requests extra headroom");
|
||||
|
||||
pressure.dropStreak = 0;
|
||||
pressure.readyQueueUnderrunCount = 1;
|
||||
decision = controller.Decide(pressure);
|
||||
Expect(decision.action == OutputProductionAction::Produce, "underrun pressure requests extra headroom");
|
||||
}
|
||||
|
||||
void TestPolicyNormalizesAndClampsToCapacity()
|
||||
{
|
||||
VideoPlayoutPolicy policy;
|
||||
policy.targetReadyFrames = 0;
|
||||
policy.maxReadyFrames = 8;
|
||||
OutputProductionController controller(policy);
|
||||
|
||||
OutputProductionPressure pressure;
|
||||
pressure.readyQueueDepth = 1;
|
||||
pressure.readyQueueCapacity = 3;
|
||||
|
||||
const OutputProductionDecision decision = controller.Decide(pressure);
|
||||
Expect(decision.action == OutputProductionAction::Wait, "normalized target at current depth waits");
|
||||
Expect(decision.targetReadyFrames == 1, "target normalizes to at least one frame");
|
||||
Expect(decision.maxReadyFrames == 3, "max ready frames clamps to queue capacity");
|
||||
}
|
||||
|
||||
void TestActionNames()
|
||||
{
|
||||
Expect(OutputProductionActionName(OutputProductionAction::Produce) == std::string("Produce"), "produce action has name");
|
||||
Expect(OutputProductionActionName(OutputProductionAction::Wait) == std::string("Wait"), "wait action has name");
|
||||
Expect(OutputProductionActionName(OutputProductionAction::Throttle) == std::string("Throttle"), "throttle action has name");
|
||||
}
|
||||
}
|
||||
|
||||
int main()
|
||||
{
|
||||
TestLowQueueRequestsProductionToTarget();
|
||||
TestFullQueueThrottles();
|
||||
TestAtTargetWaitsWithoutPressure();
|
||||
TestLateDropPressureRequestsHeadroom();
|
||||
TestPolicyNormalizesAndClampsToCapacity();
|
||||
TestActionNames();
|
||||
|
||||
if (gFailures != 0)
|
||||
{
|
||||
std::cerr << gFailures << " OutputProductionController test failure(s).\n";
|
||||
return 1;
|
||||
}
|
||||
|
||||
std::cout << "OutputProductionController tests passed.\n";
|
||||
return 0;
|
||||
}
|
||||
Reference in New Issue
Block a user