Phase 7.5 step 2
All checks were successful
CI / React UI Build (push) Successful in 10s
CI / Native Windows Build And Tests (push) Successful in 2m45s
CI / Windows Release Package (push) Successful in 2m52s

This commit is contained in:
Aiden
2026-05-11 21:36:17 +10:00
parent f8adbbe0fe
commit c5cead6003
5 changed files with 308 additions and 5 deletions

View File

@@ -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/OutputProductionController.cpp"
"${APP_DIR}/videoio/OutputProductionController.h"
"${APP_DIR}/videoio/RenderOutputQueue.cpp" "${APP_DIR}/videoio/RenderOutputQueue.cpp"
"${APP_DIR}/videoio/RenderOutputQueue.h" "${APP_DIR}/videoio/RenderOutputQueue.h"
"${APP_DIR}/videoio/VideoPlayoutPolicy.h" "${APP_DIR}/videoio/VideoPlayoutPolicy.h"
@@ -543,6 +545,22 @@ endif()
add_test(NAME VideoPlayoutSchedulerTests COMMAND VideoPlayoutSchedulerTests) 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 add_executable(RenderOutputQueueTests
"${APP_DIR}/videoio/RenderOutputQueue.cpp" "${APP_DIR}/videoio/RenderOutputQueue.cpp"
"${CMAKE_CURRENT_SOURCE_DIR}/tests/RenderOutputQueueTests.cpp" "${CMAKE_CURRENT_SOURCE_DIR}/tests/RenderOutputQueueTests.cpp"

View File

@@ -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";
}
}

View File

@@ -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);

View File

@@ -7,7 +7,7 @@ Phase 7 made backend lifecycle, playout policy, ready-frame queueing, late/drop
## Status ## Status
- Phase 7.5 design package: proposed. - 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 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: Current footholds:
@@ -18,6 +18,7 @@ Current footholds:
- `VideoPlayoutPolicy` names ready-frame headroom and catch-up policy. - `VideoPlayoutPolicy` names ready-frame headroom and catch-up policy.
- `HealthTelemetry::BackendPlayoutSnapshot` exposes queue depth, underruns, late/drop streaks, and recovery decisions. - `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 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 ## Timing Review Findings
@@ -177,13 +178,20 @@ Introduce a pure policy helper for queue-pressure decisions.
Initial target: Initial target:
- input: ready depth, capacity, target depth, late/drop streaks, underrun count - [x] input: ready depth, capacity, target depth, late/drop streaks, underrun count
- output: produce, wait, or throttle - [x] output: produce, wait, or throttle
- tests cover low queue, full queue, late/drop pressure, and normalized policy values - [x] tests cover low queue, full queue, late/drop pressure, and normalized policy values
Exit criteria: 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 ### Step 3. Add A Proactive Producer Loop

View 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;
}