Compare commits
2 Commits
bf23cd880a
...
f1f4e3421b
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f1f4e3421b | ||
|
|
ac729dc2b9 |
@@ -170,6 +170,8 @@ set(APP_SOURCES
|
|||||||
"${APP_DIR}/videoio/VideoIOTypes.h"
|
"${APP_DIR}/videoio/VideoIOTypes.h"
|
||||||
"${APP_DIR}/videoio/OutputProductionController.cpp"
|
"${APP_DIR}/videoio/OutputProductionController.cpp"
|
||||||
"${APP_DIR}/videoio/OutputProductionController.h"
|
"${APP_DIR}/videoio/OutputProductionController.h"
|
||||||
|
"${APP_DIR}/videoio/RenderCadenceController.cpp"
|
||||||
|
"${APP_DIR}/videoio/RenderCadenceController.h"
|
||||||
"${APP_DIR}/videoio/RenderOutputQueue.cpp"
|
"${APP_DIR}/videoio/RenderOutputQueue.cpp"
|
||||||
"${APP_DIR}/videoio/RenderOutputQueue.h"
|
"${APP_DIR}/videoio/RenderOutputQueue.h"
|
||||||
"${APP_DIR}/videoio/SystemOutputFramePool.cpp"
|
"${APP_DIR}/videoio/SystemOutputFramePool.cpp"
|
||||||
@@ -580,6 +582,22 @@ endif()
|
|||||||
|
|
||||||
add_test(NAME RenderOutputQueueTests COMMAND RenderOutputQueueTests)
|
add_test(NAME RenderOutputQueueTests COMMAND RenderOutputQueueTests)
|
||||||
|
|
||||||
|
add_executable(RenderCadenceControllerTests
|
||||||
|
"${APP_DIR}/videoio/RenderCadenceController.cpp"
|
||||||
|
"${CMAKE_CURRENT_SOURCE_DIR}/tests/RenderCadenceControllerTests.cpp"
|
||||||
|
)
|
||||||
|
|
||||||
|
target_include_directories(RenderCadenceControllerTests PRIVATE
|
||||||
|
"${APP_DIR}"
|
||||||
|
"${APP_DIR}/videoio"
|
||||||
|
)
|
||||||
|
|
||||||
|
if(MSVC)
|
||||||
|
target_compile_options(RenderCadenceControllerTests PRIVATE /W3)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
add_test(NAME RenderCadenceControllerTests COMMAND RenderCadenceControllerTests)
|
||||||
|
|
||||||
add_executable(SystemOutputFramePoolTests
|
add_executable(SystemOutputFramePoolTests
|
||||||
"${APP_DIR}/videoio/SystemOutputFramePool.cpp"
|
"${APP_DIR}/videoio/SystemOutputFramePool.cpp"
|
||||||
"${APP_DIR}/videoio/VideoIOFormat.cpp"
|
"${APP_DIR}/videoio/VideoIOFormat.cpp"
|
||||||
|
|||||||
@@ -51,6 +51,11 @@ JsonValue RuntimeStatePresenter::BuildRuntimeStateValue(const RuntimeStore& runt
|
|||||||
deckLink.set("externalKeyingRequested", JsonValue(telemetrySnapshot.videoIO.externalKeyingRequested));
|
deckLink.set("externalKeyingRequested", JsonValue(telemetrySnapshot.videoIO.externalKeyingRequested));
|
||||||
deckLink.set("externalKeyingActive", JsonValue(telemetrySnapshot.videoIO.externalKeyingActive));
|
deckLink.set("externalKeyingActive", JsonValue(telemetrySnapshot.videoIO.externalKeyingActive));
|
||||||
deckLink.set("statusMessage", JsonValue(telemetrySnapshot.videoIO.statusMessage));
|
deckLink.set("statusMessage", JsonValue(telemetrySnapshot.videoIO.statusMessage));
|
||||||
|
deckLink.set("actualBufferedFramesAvailable", JsonValue(telemetrySnapshot.backendPlayout.actualDeckLinkBufferedFramesAvailable));
|
||||||
|
deckLink.set("actualBufferedFrames", JsonValue(static_cast<double>(telemetrySnapshot.backendPlayout.actualDeckLinkBufferedFrames)));
|
||||||
|
deckLink.set("targetBufferedFrames", JsonValue(static_cast<double>(telemetrySnapshot.backendPlayout.targetDeckLinkBufferedFrames)));
|
||||||
|
deckLink.set("scheduleCallMs", JsonValue(telemetrySnapshot.backendPlayout.deckLinkScheduleCallMilliseconds));
|
||||||
|
deckLink.set("scheduleFailures", JsonValue(static_cast<double>(telemetrySnapshot.backendPlayout.deckLinkScheduleFailureCount)));
|
||||||
root.set("decklink", deckLink);
|
root.set("decklink", deckLink);
|
||||||
|
|
||||||
JsonValue videoIO = JsonValue::MakeObject();
|
JsonValue videoIO = JsonValue::MakeObject();
|
||||||
@@ -129,11 +134,22 @@ JsonValue RuntimeStatePresenter::BuildRuntimeStateValue(const RuntimeStore& runt
|
|||||||
recovery.set("completedFrameIndex", JsonValue(static_cast<double>(telemetrySnapshot.backendPlayout.completedFrameIndex)));
|
recovery.set("completedFrameIndex", JsonValue(static_cast<double>(telemetrySnapshot.backendPlayout.completedFrameIndex)));
|
||||||
recovery.set("scheduledFrameIndex", JsonValue(static_cast<double>(telemetrySnapshot.backendPlayout.scheduledFrameIndex)));
|
recovery.set("scheduledFrameIndex", JsonValue(static_cast<double>(telemetrySnapshot.backendPlayout.scheduledFrameIndex)));
|
||||||
recovery.set("scheduledLeadFrames", JsonValue(static_cast<double>(telemetrySnapshot.backendPlayout.scheduledLeadFrames)));
|
recovery.set("scheduledLeadFrames", JsonValue(static_cast<double>(telemetrySnapshot.backendPlayout.scheduledLeadFrames)));
|
||||||
|
recovery.set("syntheticScheduledLeadFrames", JsonValue(static_cast<double>(telemetrySnapshot.backendPlayout.scheduledLeadFrames)));
|
||||||
recovery.set("measuredLagFrames", JsonValue(static_cast<double>(telemetrySnapshot.backendPlayout.measuredLagFrames)));
|
recovery.set("measuredLagFrames", JsonValue(static_cast<double>(telemetrySnapshot.backendPlayout.measuredLagFrames)));
|
||||||
recovery.set("catchUpFrames", JsonValue(static_cast<double>(telemetrySnapshot.backendPlayout.catchUpFrames)));
|
recovery.set("catchUpFrames", JsonValue(static_cast<double>(telemetrySnapshot.backendPlayout.catchUpFrames)));
|
||||||
recovery.set("lateStreak", JsonValue(static_cast<double>(telemetrySnapshot.backendPlayout.lateStreak)));
|
recovery.set("lateStreak", JsonValue(static_cast<double>(telemetrySnapshot.backendPlayout.lateStreak)));
|
||||||
recovery.set("dropStreak", JsonValue(static_cast<double>(telemetrySnapshot.backendPlayout.dropStreak)));
|
recovery.set("dropStreak", JsonValue(static_cast<double>(telemetrySnapshot.backendPlayout.dropStreak)));
|
||||||
|
|
||||||
|
JsonValue deckLinkPlayout = JsonValue::MakeObject();
|
||||||
|
deckLinkPlayout.set("actualBufferedFramesAvailable", JsonValue(telemetrySnapshot.backendPlayout.actualDeckLinkBufferedFramesAvailable));
|
||||||
|
deckLinkPlayout.set("actualBufferedFrames", JsonValue(static_cast<double>(telemetrySnapshot.backendPlayout.actualDeckLinkBufferedFrames)));
|
||||||
|
deckLinkPlayout.set("targetBufferedFrames", JsonValue(static_cast<double>(telemetrySnapshot.backendPlayout.targetDeckLinkBufferedFrames)));
|
||||||
|
deckLinkPlayout.set("scheduleCallMs", JsonValue(telemetrySnapshot.backendPlayout.deckLinkScheduleCallMilliseconds));
|
||||||
|
deckLinkPlayout.set("scheduleFailures", JsonValue(static_cast<double>(telemetrySnapshot.backendPlayout.deckLinkScheduleFailureCount)));
|
||||||
|
|
||||||
|
JsonValue scheduler = JsonValue::MakeObject();
|
||||||
|
scheduler.set("syntheticLeadFrames", JsonValue(static_cast<double>(telemetrySnapshot.backendPlayout.scheduledLeadFrames)));
|
||||||
|
|
||||||
JsonValue backendPlayout = JsonValue::MakeObject();
|
JsonValue backendPlayout = JsonValue::MakeObject();
|
||||||
backendPlayout.set("lifecycleState", JsonValue(telemetrySnapshot.backendPlayout.lifecycleState));
|
backendPlayout.set("lifecycleState", JsonValue(telemetrySnapshot.backendPlayout.lifecycleState));
|
||||||
backendPlayout.set("degraded", JsonValue(telemetrySnapshot.backendPlayout.degraded));
|
backendPlayout.set("degraded", JsonValue(telemetrySnapshot.backendPlayout.degraded));
|
||||||
@@ -144,6 +160,8 @@ JsonValue RuntimeStatePresenter::BuildRuntimeStateValue(const RuntimeStore& runt
|
|||||||
backendPlayout.set("readyQueue", readyQueue);
|
backendPlayout.set("readyQueue", readyQueue);
|
||||||
backendPlayout.set("systemMemory", systemMemory);
|
backendPlayout.set("systemMemory", systemMemory);
|
||||||
backendPlayout.set("outputRender", outputRender);
|
backendPlayout.set("outputRender", outputRender);
|
||||||
|
backendPlayout.set("decklink", deckLinkPlayout);
|
||||||
|
backendPlayout.set("scheduler", scheduler);
|
||||||
backendPlayout.set("recovery", recovery);
|
backendPlayout.set("recovery", recovery);
|
||||||
root.set("backendPlayout", backendPlayout);
|
root.set("backendPlayout", backendPlayout);
|
||||||
|
|
||||||
|
|||||||
@@ -347,6 +347,32 @@ bool HealthTelemetry::TryRecordSystemMemoryPlayoutStats(std::size_t freeFrameCou
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void HealthTelemetry::RecordDeckLinkBufferTelemetry(bool actualBufferedFramesAvailable, uint64_t actualBufferedFrames,
|
||||||
|
std::size_t targetBufferedFrames, double scheduleCallMilliseconds, uint64_t scheduleFailureCount)
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(mMutex);
|
||||||
|
mBackendPlayout.actualDeckLinkBufferedFramesAvailable = actualBufferedFramesAvailable;
|
||||||
|
mBackendPlayout.actualDeckLinkBufferedFrames = actualBufferedFramesAvailable ? actualBufferedFrames : 0;
|
||||||
|
mBackendPlayout.targetDeckLinkBufferedFrames = targetBufferedFrames;
|
||||||
|
mBackendPlayout.deckLinkScheduleCallMilliseconds = std::max(scheduleCallMilliseconds, 0.0);
|
||||||
|
mBackendPlayout.deckLinkScheduleFailureCount = scheduleFailureCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool HealthTelemetry::TryRecordDeckLinkBufferTelemetry(bool actualBufferedFramesAvailable, uint64_t actualBufferedFrames,
|
||||||
|
std::size_t targetBufferedFrames, double scheduleCallMilliseconds, uint64_t scheduleFailureCount)
|
||||||
|
{
|
||||||
|
std::unique_lock<std::mutex> lock(mMutex, std::try_to_lock);
|
||||||
|
if (!lock.owns_lock())
|
||||||
|
return false;
|
||||||
|
|
||||||
|
mBackendPlayout.actualDeckLinkBufferedFramesAvailable = actualBufferedFramesAvailable;
|
||||||
|
mBackendPlayout.actualDeckLinkBufferedFrames = actualBufferedFramesAvailable ? actualBufferedFrames : 0;
|
||||||
|
mBackendPlayout.targetDeckLinkBufferedFrames = targetBufferedFrames;
|
||||||
|
mBackendPlayout.deckLinkScheduleCallMilliseconds = std::max(scheduleCallMilliseconds, 0.0);
|
||||||
|
mBackendPlayout.deckLinkScheduleFailureCount = scheduleFailureCount;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
void HealthTelemetry::RecordOutputRenderPipelineTiming(
|
void HealthTelemetry::RecordOutputRenderPipelineTiming(
|
||||||
double drawMilliseconds,
|
double drawMilliseconds,
|
||||||
double fenceWaitMilliseconds,
|
double fenceWaitMilliseconds,
|
||||||
|
|||||||
@@ -126,6 +126,11 @@ public:
|
|||||||
uint64_t completedFrameIndex = 0;
|
uint64_t completedFrameIndex = 0;
|
||||||
uint64_t scheduledFrameIndex = 0;
|
uint64_t scheduledFrameIndex = 0;
|
||||||
uint64_t scheduledLeadFrames = 0;
|
uint64_t scheduledLeadFrames = 0;
|
||||||
|
bool actualDeckLinkBufferedFramesAvailable = false;
|
||||||
|
uint64_t actualDeckLinkBufferedFrames = 0;
|
||||||
|
std::size_t targetDeckLinkBufferedFrames = 0;
|
||||||
|
double deckLinkScheduleCallMilliseconds = 0.0;
|
||||||
|
uint64_t deckLinkScheduleFailureCount = 0;
|
||||||
uint64_t measuredLagFrames = 0;
|
uint64_t measuredLagFrames = 0;
|
||||||
uint64_t catchUpFrames = 0;
|
uint64_t catchUpFrames = 0;
|
||||||
uint64_t lateStreak = 0;
|
uint64_t lateStreak = 0;
|
||||||
@@ -213,6 +218,11 @@ public:
|
|||||||
std::size_t scheduledFrameCount, uint64_t underrunCount, uint64_t repeatCount, uint64_t dropCount,
|
std::size_t scheduledFrameCount, uint64_t underrunCount, uint64_t repeatCount, uint64_t dropCount,
|
||||||
double frameAgeAtScheduleMilliseconds, double frameAgeAtCompletionMilliseconds);
|
double frameAgeAtScheduleMilliseconds, double frameAgeAtCompletionMilliseconds);
|
||||||
|
|
||||||
|
void RecordDeckLinkBufferTelemetry(bool actualBufferedFramesAvailable, uint64_t actualBufferedFrames,
|
||||||
|
std::size_t targetBufferedFrames, double scheduleCallMilliseconds, uint64_t scheduleFailureCount);
|
||||||
|
bool TryRecordDeckLinkBufferTelemetry(bool actualBufferedFramesAvailable, uint64_t actualBufferedFrames,
|
||||||
|
std::size_t targetBufferedFrames, double scheduleCallMilliseconds, uint64_t scheduleFailureCount);
|
||||||
|
|
||||||
void RecordOutputRenderPipelineTiming(
|
void RecordOutputRenderPipelineTiming(
|
||||||
double drawMilliseconds,
|
double drawMilliseconds,
|
||||||
double fenceWaitMilliseconds,
|
double fenceWaitMilliseconds,
|
||||||
|
|||||||
@@ -0,0 +1,102 @@
|
|||||||
|
#include "RenderCadenceController.h"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cmath>
|
||||||
|
|
||||||
|
void RenderCadenceController::Configure(Duration targetFrameDuration, TimePoint firstRenderTime, const RenderCadencePolicy& policy)
|
||||||
|
{
|
||||||
|
mTargetFrameDuration = IsPositive(targetFrameDuration) ? targetFrameDuration : std::chrono::milliseconds(1);
|
||||||
|
mPolicy = policy;
|
||||||
|
if (mPolicy.skipThresholdFrames < 1.0)
|
||||||
|
mPolicy.skipThresholdFrames = 1.0;
|
||||||
|
Reset(firstRenderTime);
|
||||||
|
}
|
||||||
|
|
||||||
|
void RenderCadenceController::Reset(TimePoint firstRenderTime)
|
||||||
|
{
|
||||||
|
mNextRenderTime = firstRenderTime;
|
||||||
|
mNextFrameIndex = 0;
|
||||||
|
mMetrics = RenderCadenceMetrics();
|
||||||
|
}
|
||||||
|
|
||||||
|
RenderCadenceDecision RenderCadenceController::Tick(TimePoint now)
|
||||||
|
{
|
||||||
|
RenderCadenceDecision decision;
|
||||||
|
decision.frameIndex = mNextFrameIndex;
|
||||||
|
decision.renderTargetTime = mNextRenderTime;
|
||||||
|
decision.nextRenderTime = mNextRenderTime;
|
||||||
|
|
||||||
|
if (now < mNextRenderTime)
|
||||||
|
{
|
||||||
|
decision.action = RenderCadenceAction::Wait;
|
||||||
|
decision.waitDuration = mNextRenderTime - now;
|
||||||
|
decision.reason = "waiting-for-next-render-tick";
|
||||||
|
return decision;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Duration lateness = now - mNextRenderTime;
|
||||||
|
const uint64_t skippedTicks = SkippedTicksForLateness(lateness);
|
||||||
|
if (skippedTicks > 0)
|
||||||
|
{
|
||||||
|
decision.skippedTicks = skippedTicks;
|
||||||
|
decision.frameIndex = mNextFrameIndex + skippedTicks;
|
||||||
|
decision.renderTargetTime = mNextRenderTime + (mTargetFrameDuration * skippedTicks);
|
||||||
|
decision.reason = "late-skip-render-ticks";
|
||||||
|
mMetrics.skippedTickCount += skippedTicks;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
decision.reason = IsPositive(lateness) ? "late-render-now" : "on-time-render";
|
||||||
|
}
|
||||||
|
|
||||||
|
decision.action = RenderCadenceAction::Render;
|
||||||
|
decision.lateness = now > decision.renderTargetTime
|
||||||
|
? now - decision.renderTargetTime
|
||||||
|
: Duration::zero();
|
||||||
|
mNextFrameIndex = decision.frameIndex + 1;
|
||||||
|
mNextRenderTime = decision.renderTargetTime + mTargetFrameDuration;
|
||||||
|
decision.nextRenderTime = mNextRenderTime;
|
||||||
|
|
||||||
|
++mMetrics.renderedFrameCount;
|
||||||
|
mMetrics.nextFrameIndex = mNextFrameIndex;
|
||||||
|
mMetrics.lastLateness = decision.lateness;
|
||||||
|
if (IsPositive(decision.lateness))
|
||||||
|
{
|
||||||
|
++mMetrics.lateFrameCount;
|
||||||
|
mMetrics.maxLateness = (std::max)(mMetrics.maxLateness, decision.lateness);
|
||||||
|
}
|
||||||
|
|
||||||
|
return decision;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint64_t RenderCadenceController::SkippedTicksForLateness(Duration lateness) const
|
||||||
|
{
|
||||||
|
if (!mPolicy.skipLateTicks || !IsPositive(lateness) || !IsPositive(mTargetFrameDuration))
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
const double lateFrames = static_cast<double>(lateness.count()) / static_cast<double>(mTargetFrameDuration.count());
|
||||||
|
if (lateFrames < mPolicy.skipThresholdFrames)
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
const uint64_t elapsedTicks = static_cast<uint64_t>(std::floor(lateFrames));
|
||||||
|
if (elapsedTicks == 0)
|
||||||
|
return 0;
|
||||||
|
return (std::min)(elapsedTicks, mPolicy.maxSkippedTicksPerDecision);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool RenderCadenceController::IsPositive(Duration duration)
|
||||||
|
{
|
||||||
|
return duration > Duration::zero();
|
||||||
|
}
|
||||||
|
|
||||||
|
const char* RenderCadenceActionName(RenderCadenceAction action)
|
||||||
|
{
|
||||||
|
switch (action)
|
||||||
|
{
|
||||||
|
case RenderCadenceAction::Render:
|
||||||
|
return "Render";
|
||||||
|
case RenderCadenceAction::Wait:
|
||||||
|
default:
|
||||||
|
return "Wait";
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <chrono>
|
||||||
|
#include <cstdint>
|
||||||
|
|
||||||
|
enum class RenderCadenceAction
|
||||||
|
{
|
||||||
|
Wait,
|
||||||
|
Render
|
||||||
|
};
|
||||||
|
|
||||||
|
struct RenderCadencePolicy
|
||||||
|
{
|
||||||
|
bool skipLateTicks = true;
|
||||||
|
uint64_t maxSkippedTicksPerDecision = 4;
|
||||||
|
double skipThresholdFrames = 2.0;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct RenderCadenceDecision
|
||||||
|
{
|
||||||
|
RenderCadenceAction action = RenderCadenceAction::Wait;
|
||||||
|
uint64_t frameIndex = 0;
|
||||||
|
uint64_t skippedTicks = 0;
|
||||||
|
std::chrono::steady_clock::time_point renderTargetTime;
|
||||||
|
std::chrono::steady_clock::time_point nextRenderTime;
|
||||||
|
std::chrono::steady_clock::duration waitDuration = std::chrono::steady_clock::duration::zero();
|
||||||
|
std::chrono::steady_clock::duration lateness = std::chrono::steady_clock::duration::zero();
|
||||||
|
const char* reason = "waiting-for-next-render-tick";
|
||||||
|
};
|
||||||
|
|
||||||
|
struct RenderCadenceMetrics
|
||||||
|
{
|
||||||
|
uint64_t nextFrameIndex = 0;
|
||||||
|
uint64_t renderedFrameCount = 0;
|
||||||
|
uint64_t skippedTickCount = 0;
|
||||||
|
uint64_t lateFrameCount = 0;
|
||||||
|
std::chrono::steady_clock::duration lastLateness = std::chrono::steady_clock::duration::zero();
|
||||||
|
std::chrono::steady_clock::duration maxLateness = std::chrono::steady_clock::duration::zero();
|
||||||
|
};
|
||||||
|
|
||||||
|
class RenderCadenceController
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
using Clock = std::chrono::steady_clock;
|
||||||
|
using TimePoint = Clock::time_point;
|
||||||
|
using Duration = Clock::duration;
|
||||||
|
|
||||||
|
void Configure(Duration targetFrameDuration, TimePoint firstRenderTime, const RenderCadencePolicy& policy = RenderCadencePolicy());
|
||||||
|
void Reset(TimePoint firstRenderTime);
|
||||||
|
RenderCadenceDecision Tick(TimePoint now);
|
||||||
|
|
||||||
|
Duration TargetFrameDuration() const { return mTargetFrameDuration; }
|
||||||
|
TimePoint NextRenderTime() const { return mNextRenderTime; }
|
||||||
|
uint64_t NextFrameIndex() const { return mNextFrameIndex; }
|
||||||
|
const RenderCadenceMetrics& Metrics() const { return mMetrics; }
|
||||||
|
|
||||||
|
private:
|
||||||
|
uint64_t SkippedTicksForLateness(Duration lateness) const;
|
||||||
|
static bool IsPositive(Duration duration);
|
||||||
|
|
||||||
|
Duration mTargetFrameDuration = std::chrono::milliseconds(16);
|
||||||
|
TimePoint mNextRenderTime;
|
||||||
|
uint64_t mNextFrameIndex = 0;
|
||||||
|
RenderCadencePolicy mPolicy;
|
||||||
|
RenderCadenceMetrics mMetrics;
|
||||||
|
};
|
||||||
|
|
||||||
|
const char* RenderCadenceActionName(RenderCadenceAction action);
|
||||||
@@ -47,6 +47,18 @@ bool RenderOutputQueue::TryPop(RenderOutputFrame& frame)
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool RenderOutputQueue::DropOldestFrame()
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(mMutex);
|
||||||
|
if (mReadyFrames.empty())
|
||||||
|
return false;
|
||||||
|
|
||||||
|
ReleaseFrame(mReadyFrames.front());
|
||||||
|
mReadyFrames.pop_front();
|
||||||
|
++mDroppedCount;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
void RenderOutputQueue::Clear()
|
void RenderOutputQueue::Clear()
|
||||||
{
|
{
|
||||||
std::lock_guard<std::mutex> lock(mMutex);
|
std::lock_guard<std::mutex> lock(mMutex);
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ public:
|
|||||||
void Configure(const VideoPlayoutPolicy& policy);
|
void Configure(const VideoPlayoutPolicy& policy);
|
||||||
bool Push(RenderOutputFrame frame);
|
bool Push(RenderOutputFrame frame);
|
||||||
bool TryPop(RenderOutputFrame& frame);
|
bool TryPop(RenderOutputFrame& frame);
|
||||||
|
bool DropOldestFrame();
|
||||||
void Clear();
|
void Clear();
|
||||||
RenderOutputQueueMetrics GetMetrics() const;
|
RenderOutputQueueMetrics GetMetrics() const;
|
||||||
|
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ bool SystemOutputFramePool::AcquireFreeSlot(OutputFrameSlot& slot)
|
|||||||
if (mSlots[index].state != OutputFrameSlotState::Free)
|
if (mSlots[index].state != OutputFrameSlotState::Free)
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
mSlots[index].state = OutputFrameSlotState::Acquired;
|
mSlots[index].state = OutputFrameSlotState::Rendering;
|
||||||
++mSlots[index].generation;
|
++mSlots[index].generation;
|
||||||
FillOutputSlotLocked(index, slot);
|
FillOutputSlotLocked(index, slot);
|
||||||
return true;
|
return true;
|
||||||
@@ -62,16 +62,26 @@ bool SystemOutputFramePool::AcquireFreeSlot(OutputFrameSlot& slot)
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool SystemOutputFramePool::AcquireRenderingSlot(OutputFrameSlot& slot)
|
||||||
|
{
|
||||||
|
return AcquireFreeSlot(slot);
|
||||||
|
}
|
||||||
|
|
||||||
bool SystemOutputFramePool::PublishReadySlot(const OutputFrameSlot& slot)
|
bool SystemOutputFramePool::PublishReadySlot(const OutputFrameSlot& slot)
|
||||||
{
|
{
|
||||||
std::lock_guard<std::mutex> lock(mMutex);
|
std::lock_guard<std::mutex> lock(mMutex);
|
||||||
if (!TransitionSlotLocked(slot, OutputFrameSlotState::Acquired, OutputFrameSlotState::Ready))
|
if (!TransitionSlotLocked(slot, OutputFrameSlotState::Rendering, OutputFrameSlotState::Completed))
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
mReadySlots.push_back(slot.index);
|
mReadySlots.push_back(slot.index);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool SystemOutputFramePool::PublishCompletedSlot(const OutputFrameSlot& slot)
|
||||||
|
{
|
||||||
|
return PublishReadySlot(slot);
|
||||||
|
}
|
||||||
|
|
||||||
bool SystemOutputFramePool::ConsumeReadySlot(OutputFrameSlot& slot)
|
bool SystemOutputFramePool::ConsumeReadySlot(OutputFrameSlot& slot)
|
||||||
{
|
{
|
||||||
std::lock_guard<std::mutex> lock(mMutex);
|
std::lock_guard<std::mutex> lock(mMutex);
|
||||||
@@ -79,10 +89,9 @@ bool SystemOutputFramePool::ConsumeReadySlot(OutputFrameSlot& slot)
|
|||||||
{
|
{
|
||||||
const std::size_t index = mReadySlots.front();
|
const std::size_t index = mReadySlots.front();
|
||||||
mReadySlots.pop_front();
|
mReadySlots.pop_front();
|
||||||
if (index >= mSlots.size() || mSlots[index].state != OutputFrameSlotState::Ready)
|
if (index >= mSlots.size() || mSlots[index].state != OutputFrameSlotState::Completed)
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
mSlots[index].state = OutputFrameSlotState::Consumed;
|
|
||||||
FillOutputSlotLocked(index, slot);
|
FillOutputSlotLocked(index, slot);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -92,16 +101,18 @@ bool SystemOutputFramePool::ConsumeReadySlot(OutputFrameSlot& slot)
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool SystemOutputFramePool::ConsumeCompletedSlot(OutputFrameSlot& slot)
|
||||||
|
{
|
||||||
|
return ConsumeReadySlot(slot);
|
||||||
|
}
|
||||||
|
|
||||||
bool SystemOutputFramePool::MarkScheduled(const OutputFrameSlot& slot)
|
bool SystemOutputFramePool::MarkScheduled(const OutputFrameSlot& slot)
|
||||||
{
|
{
|
||||||
std::lock_guard<std::mutex> lock(mMutex);
|
std::lock_guard<std::mutex> lock(mMutex);
|
||||||
if (!IsValidSlotLocked(slot))
|
if (!IsValidSlotLocked(slot))
|
||||||
return false;
|
return false;
|
||||||
if (mSlots[slot.index].state != OutputFrameSlotState::Ready &&
|
if (mSlots[slot.index].state != OutputFrameSlotState::Completed)
|
||||||
mSlots[slot.index].state != OutputFrameSlotState::Consumed)
|
|
||||||
{
|
|
||||||
return false;
|
return false;
|
||||||
}
|
|
||||||
|
|
||||||
RemoveReadyIndexLocked(slot.index);
|
RemoveReadyIndexLocked(slot.index);
|
||||||
mSlots[slot.index].state = OutputFrameSlotState::Scheduled;
|
mSlots[slot.index].state = OutputFrameSlotState::Scheduled;
|
||||||
@@ -118,11 +129,8 @@ bool SystemOutputFramePool::MarkScheduledByBuffer(void* bytes)
|
|||||||
{
|
{
|
||||||
if (mSlots[index].bytes.empty() || mSlots[index].bytes.data() != bytes)
|
if (mSlots[index].bytes.empty() || mSlots[index].bytes.data() != bytes)
|
||||||
continue;
|
continue;
|
||||||
if (mSlots[index].state != OutputFrameSlotState::Ready &&
|
if (mSlots[index].state != OutputFrameSlotState::Completed)
|
||||||
mSlots[index].state != OutputFrameSlotState::Consumed)
|
|
||||||
{
|
|
||||||
return false;
|
return false;
|
||||||
}
|
|
||||||
|
|
||||||
RemoveReadyIndexLocked(index);
|
RemoveReadyIndexLocked(index);
|
||||||
mSlots[index].state = OutputFrameSlotState::Scheduled;
|
mSlots[index].state = OutputFrameSlotState::Scheduled;
|
||||||
@@ -187,13 +195,12 @@ SystemOutputFramePoolMetrics SystemOutputFramePool::GetMetrics() const
|
|||||||
case OutputFrameSlotState::Free:
|
case OutputFrameSlotState::Free:
|
||||||
++metrics.freeCount;
|
++metrics.freeCount;
|
||||||
break;
|
break;
|
||||||
case OutputFrameSlotState::Acquired:
|
case OutputFrameSlotState::Rendering:
|
||||||
|
++metrics.renderingCount;
|
||||||
++metrics.acquiredCount;
|
++metrics.acquiredCount;
|
||||||
break;
|
break;
|
||||||
case OutputFrameSlotState::Ready:
|
case OutputFrameSlotState::Completed:
|
||||||
break;
|
++metrics.completedCount;
|
||||||
case OutputFrameSlotState::Consumed:
|
|
||||||
++metrics.consumedCount;
|
|
||||||
break;
|
break;
|
||||||
case OutputFrameSlotState::Scheduled:
|
case OutputFrameSlotState::Scheduled:
|
||||||
++metrics.scheduledCount;
|
++metrics.scheduledCount;
|
||||||
|
|||||||
@@ -11,9 +11,8 @@
|
|||||||
enum class OutputFrameSlotState
|
enum class OutputFrameSlotState
|
||||||
{
|
{
|
||||||
Free,
|
Free,
|
||||||
Acquired,
|
Rendering,
|
||||||
Ready,
|
Completed,
|
||||||
Consumed,
|
|
||||||
Scheduled
|
Scheduled
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -37,10 +36,12 @@ struct SystemOutputFramePoolMetrics
|
|||||||
{
|
{
|
||||||
std::size_t capacity = 0;
|
std::size_t capacity = 0;
|
||||||
std::size_t freeCount = 0;
|
std::size_t freeCount = 0;
|
||||||
|
std::size_t renderingCount = 0;
|
||||||
|
std::size_t completedCount = 0;
|
||||||
|
std::size_t scheduledCount = 0;
|
||||||
std::size_t acquiredCount = 0;
|
std::size_t acquiredCount = 0;
|
||||||
std::size_t readyCount = 0;
|
std::size_t readyCount = 0;
|
||||||
std::size_t consumedCount = 0;
|
std::size_t consumedCount = 0;
|
||||||
std::size_t scheduledCount = 0;
|
|
||||||
uint64_t acquireMissCount = 0;
|
uint64_t acquireMissCount = 0;
|
||||||
uint64_t readyUnderrunCount = 0;
|
uint64_t readyUnderrunCount = 0;
|
||||||
};
|
};
|
||||||
@@ -55,8 +56,11 @@ public:
|
|||||||
SystemOutputFramePoolConfig Config() const;
|
SystemOutputFramePoolConfig Config() const;
|
||||||
|
|
||||||
bool AcquireFreeSlot(OutputFrameSlot& slot);
|
bool AcquireFreeSlot(OutputFrameSlot& slot);
|
||||||
|
bool AcquireRenderingSlot(OutputFrameSlot& slot);
|
||||||
bool PublishReadySlot(const OutputFrameSlot& slot);
|
bool PublishReadySlot(const OutputFrameSlot& slot);
|
||||||
|
bool PublishCompletedSlot(const OutputFrameSlot& slot);
|
||||||
bool ConsumeReadySlot(OutputFrameSlot& slot);
|
bool ConsumeReadySlot(OutputFrameSlot& slot);
|
||||||
|
bool ConsumeCompletedSlot(OutputFrameSlot& slot);
|
||||||
bool MarkScheduled(const OutputFrameSlot& slot);
|
bool MarkScheduled(const OutputFrameSlot& slot);
|
||||||
bool MarkScheduledByBuffer(void* bytes);
|
bool MarkScheduledByBuffer(void* bytes);
|
||||||
bool ReleaseSlot(const OutputFrameSlot& slot);
|
bool ReleaseSlot(const OutputFrameSlot& slot);
|
||||||
|
|||||||
@@ -359,6 +359,12 @@ void VideoBackend::StartOutputProducerWorker()
|
|||||||
if (mOutputProducerWorkerRunning)
|
if (mOutputProducerWorkerRunning)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
const double frameBudgetMilliseconds = State().frameBudgetMilliseconds;
|
||||||
|
const auto frameDuration = frameBudgetMilliseconds > 0.0
|
||||||
|
? std::chrono::duration_cast<RenderCadenceController::Duration>(
|
||||||
|
std::chrono::duration<double, std::milli>(frameBudgetMilliseconds))
|
||||||
|
: std::chrono::milliseconds(16);
|
||||||
|
mRenderCadenceController.Configure(frameDuration, std::chrono::steady_clock::now());
|
||||||
mLastOutputProductionCompletion = VideoIOCompletion();
|
mLastOutputProductionCompletion = VideoIOCompletion();
|
||||||
mLastOutputProductionTime = std::chrono::steady_clock::time_point();
|
mLastOutputProductionTime = std::chrono::steady_clock::time_point();
|
||||||
mOutputProducerWorkerStopping = false;
|
mOutputProducerWorkerStopping = false;
|
||||||
@@ -433,11 +439,16 @@ void VideoBackend::OutputProducerWorkerMain()
|
|||||||
|
|
||||||
const RenderOutputQueueMetrics metrics = mReadyOutputQueue.GetMetrics();
|
const RenderOutputQueueMetrics metrics = mReadyOutputQueue.GetMetrics();
|
||||||
RecordReadyQueueDepthSample(metrics);
|
RecordReadyQueueDepthSample(metrics);
|
||||||
const OutputProductionDecision decision = mOutputProductionController.Decide(BuildOutputProductionPressure(metrics));
|
|
||||||
if (decision.action != OutputProductionAction::Produce || decision.requestedFrames == 0)
|
const auto now = std::chrono::steady_clock::now();
|
||||||
|
RenderCadenceDecision cadenceDecision = mRenderCadenceController.Tick(now);
|
||||||
|
if (cadenceDecision.action == RenderCadenceAction::Wait)
|
||||||
{
|
{
|
||||||
|
const auto waitDuration = (std::min)(
|
||||||
|
std::chrono::duration_cast<std::chrono::milliseconds>(cadenceDecision.waitDuration),
|
||||||
|
OutputProducerWakeInterval());
|
||||||
std::unique_lock<std::mutex> lock(mOutputProducerMutex);
|
std::unique_lock<std::mutex> lock(mOutputProducerMutex);
|
||||||
mOutputProducerCondition.wait_for(lock, OutputProducerWakeInterval());
|
mOutputProducerCondition.wait_for(lock, waitDuration);
|
||||||
if (mOutputProducerWorkerStopping)
|
if (mOutputProducerWorkerStopping)
|
||||||
{
|
{
|
||||||
mOutputProducerWorkerRunning = false;
|
mOutputProducerWorkerRunning = false;
|
||||||
@@ -454,16 +465,7 @@ void VideoBackend::OutputProducerWorkerMain()
|
|||||||
completion = mLastOutputProductionCompletion;
|
completion = mLastOutputProductionCompletion;
|
||||||
}
|
}
|
||||||
|
|
||||||
const bool belowTargetDepth = metrics.depth < decision.targetReadyFrames;
|
const std::size_t producedFrames = ProduceReadyOutputFrames(completion, 1);
|
||||||
const auto now = std::chrono::steady_clock::now();
|
|
||||||
if (!belowTargetDepth &&
|
|
||||||
mLastOutputProductionTime != std::chrono::steady_clock::time_point() &&
|
|
||||||
now - mLastOutputProductionTime < OutputProducerWakeInterval())
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const std::size_t producedFrames = ProduceReadyOutputFrames(completion, decision.requestedFrames);
|
|
||||||
if (producedFrames > 0)
|
if (producedFrames > 0)
|
||||||
{
|
{
|
||||||
mLastOutputProductionTime = std::chrono::steady_clock::now();
|
mLastOutputProductionTime = std::chrono::steady_clock::now();
|
||||||
@@ -513,7 +515,6 @@ void VideoBackend::ProcessOutputFrameCompletion(const VideoIOCompletion& complet
|
|||||||
}
|
}
|
||||||
NotifyOutputProducer();
|
NotifyOutputProducer();
|
||||||
|
|
||||||
NotifyOutputProducer();
|
|
||||||
RecordBackendPlayoutHealth(completion.result, recoveryDecision);
|
RecordBackendPlayoutHealth(completion.result, recoveryDecision);
|
||||||
RecordSystemMemoryPlayoutStats();
|
RecordSystemMemoryPlayoutStats();
|
||||||
}
|
}
|
||||||
@@ -601,10 +602,6 @@ std::size_t VideoBackend::ProduceReadyOutputFrames(const VideoIOCompletion& comp
|
|||||||
std::size_t producedFrames = 0;
|
std::size_t producedFrames = 0;
|
||||||
while (producedFrames < maxFrames)
|
while (producedFrames < maxFrames)
|
||||||
{
|
{
|
||||||
const OutputProductionDecision decision = mOutputProductionController.Decide(BuildOutputProductionPressure(metrics));
|
|
||||||
if (decision.action != OutputProductionAction::Produce)
|
|
||||||
break;
|
|
||||||
|
|
||||||
if (!RenderReadyOutputFrame(mVideoIODevice->State(), completion))
|
if (!RenderReadyOutputFrame(mVideoIODevice->State(), completion))
|
||||||
break;
|
break;
|
||||||
++producedFrames;
|
++producedFrames;
|
||||||
@@ -635,7 +632,10 @@ bool VideoBackend::RenderReadyOutputFrame(const VideoIOState& state, const Video
|
|||||||
VideoIOOutputFrame outputFrame;
|
VideoIOOutputFrame outputFrame;
|
||||||
const auto acquireStart = std::chrono::steady_clock::now();
|
const auto acquireStart = std::chrono::steady_clock::now();
|
||||||
if (!mSystemOutputFramePool.AcquireFreeSlot(outputSlot))
|
if (!mSystemOutputFramePool.AcquireFreeSlot(outputSlot))
|
||||||
|
{
|
||||||
|
if (!mReadyOutputQueue.DropOldestFrame() || !mSystemOutputFramePool.AcquireFreeSlot(outputSlot))
|
||||||
return false;
|
return false;
|
||||||
|
}
|
||||||
outputFrame = outputSlot.frame;
|
outputFrame = outputSlot.frame;
|
||||||
const auto acquireEnd = std::chrono::steady_clock::now();
|
const auto acquireEnd = std::chrono::steady_clock::now();
|
||||||
|
|
||||||
@@ -702,10 +702,12 @@ bool VideoBackend::ScheduleReadyOutputFrame()
|
|||||||
|
|
||||||
if (!ScheduleOutputFrame(readyFrame.frame))
|
if (!ScheduleOutputFrame(readyFrame.frame))
|
||||||
{
|
{
|
||||||
|
RecordDeckLinkBufferTelemetry();
|
||||||
mSystemOutputFramePool.ReleaseSlotByBuffer(readyFrame.frame.bytes);
|
mSystemOutputFramePool.ReleaseSlotByBuffer(readyFrame.frame.bytes);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
RecordDeckLinkBufferTelemetry();
|
||||||
PublishOutputFrameScheduled(readyFrame.frame);
|
PublishOutputFrameScheduled(readyFrame.frame);
|
||||||
RecordSystemMemoryPlayoutStats();
|
RecordSystemMemoryPlayoutStats();
|
||||||
return true;
|
return true;
|
||||||
@@ -726,10 +728,12 @@ bool VideoBackend::ScheduleBlackUnderrunFrame()
|
|||||||
|
|
||||||
if (!ScheduleOutputFrame(outputFrame))
|
if (!ScheduleOutputFrame(outputFrame))
|
||||||
{
|
{
|
||||||
|
RecordDeckLinkBufferTelemetry();
|
||||||
ApplyLifecycleTransition(VideoBackendLifecycleState::Degraded, "Output underrun: black fallback frame scheduling failed.");
|
ApplyLifecycleTransition(VideoBackendLifecycleState::Degraded, "Output underrun: black fallback frame scheduling failed.");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
RecordDeckLinkBufferTelemetry();
|
||||||
ApplyLifecycleTransition(VideoBackendLifecycleState::Degraded, "Output underrun: scheduled black fallback frame.");
|
ApplyLifecycleTransition(VideoBackendLifecycleState::Degraded, "Output underrun: scheduled black fallback frame.");
|
||||||
PublishOutputFrameScheduled(outputFrame);
|
PublishOutputFrameScheduled(outputFrame);
|
||||||
return true;
|
return true;
|
||||||
@@ -787,10 +791,25 @@ void VideoBackend::RecordReadyQueueDepthSample(const RenderOutputQueueMetrics& m
|
|||||||
++mReadyQueueZeroDepthCount;
|
++mReadyQueueZeroDepthCount;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void VideoBackend::RecordDeckLinkBufferTelemetry()
|
||||||
|
{
|
||||||
|
if (!mVideoIODevice)
|
||||||
|
return;
|
||||||
|
|
||||||
|
const VideoIOState& state = mVideoIODevice->State();
|
||||||
|
mHealthTelemetry.TryRecordDeckLinkBufferTelemetry(
|
||||||
|
state.actualDeckLinkBufferedFramesAvailable,
|
||||||
|
state.actualDeckLinkBufferedFrames,
|
||||||
|
static_cast<std::size_t>(mPlayoutPolicy.targetPrerollFrames),
|
||||||
|
state.deckLinkScheduleCallMilliseconds,
|
||||||
|
state.deckLinkScheduleFailureCount);
|
||||||
|
}
|
||||||
|
|
||||||
void VideoBackend::RecordSystemMemoryPlayoutStats()
|
void VideoBackend::RecordSystemMemoryPlayoutStats()
|
||||||
{
|
{
|
||||||
const SystemOutputFramePoolMetrics poolMetrics = mSystemOutputFramePool.GetMetrics();
|
const SystemOutputFramePoolMetrics poolMetrics = mSystemOutputFramePool.GetMetrics();
|
||||||
const RenderOutputQueueMetrics queueMetrics = mReadyOutputQueue.GetMetrics();
|
const RenderOutputQueueMetrics queueMetrics = mReadyOutputQueue.GetMetrics();
|
||||||
|
RecordDeckLinkBufferTelemetry();
|
||||||
mHealthTelemetry.TryRecordSystemMemoryPlayoutStats(
|
mHealthTelemetry.TryRecordSystemMemoryPlayoutStats(
|
||||||
poolMetrics.freeCount,
|
poolMetrics.freeCount,
|
||||||
poolMetrics.readyCount,
|
poolMetrics.readyCount,
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include "OutputProductionController.h"
|
#include "OutputProductionController.h"
|
||||||
|
#include "RenderCadenceController.h"
|
||||||
#include "RenderOutputQueue.h"
|
#include "RenderOutputQueue.h"
|
||||||
#include "SystemOutputFramePool.h"
|
#include "SystemOutputFramePool.h"
|
||||||
#include "VideoBackendLifecycle.h"
|
#include "VideoBackendLifecycle.h"
|
||||||
@@ -86,6 +87,7 @@ private:
|
|||||||
bool ScheduleBlackUnderrunFrame();
|
bool ScheduleBlackUnderrunFrame();
|
||||||
void RecordFramePacing(VideoIOCompletionResult completionResult);
|
void RecordFramePacing(VideoIOCompletionResult completionResult);
|
||||||
void RecordReadyQueueDepthSample(const RenderOutputQueueMetrics& metrics);
|
void RecordReadyQueueDepthSample(const RenderOutputQueueMetrics& metrics);
|
||||||
|
void RecordDeckLinkBufferTelemetry();
|
||||||
void RecordSystemMemoryPlayoutStats();
|
void RecordSystemMemoryPlayoutStats();
|
||||||
void RecordOutputRenderDuration(double renderMilliseconds, double acquireMilliseconds, double renderRequestMilliseconds, double endAccessMilliseconds);
|
void RecordOutputRenderDuration(double renderMilliseconds, double acquireMilliseconds, double renderRequestMilliseconds, double endAccessMilliseconds);
|
||||||
bool ApplyLifecycleTransition(VideoBackendLifecycleState state, const std::string& message);
|
bool ApplyLifecycleTransition(VideoBackendLifecycleState state, const std::string& message);
|
||||||
@@ -104,6 +106,7 @@ private:
|
|||||||
VideoBackendLifecycle mLifecycle;
|
VideoBackendLifecycle mLifecycle;
|
||||||
VideoPlayoutPolicy mPlayoutPolicy;
|
VideoPlayoutPolicy mPlayoutPolicy;
|
||||||
OutputProductionController mOutputProductionController;
|
OutputProductionController mOutputProductionController;
|
||||||
|
RenderCadenceController mRenderCadenceController;
|
||||||
RenderOutputQueue mReadyOutputQueue;
|
RenderOutputQueue mReadyOutputQueue;
|
||||||
SystemOutputFramePool mSystemOutputFramePool;
|
SystemOutputFramePool mSystemOutputFramePool;
|
||||||
std::unique_ptr<VideoIODevice> mVideoIODevice;
|
std::unique_ptr<VideoIODevice> mVideoIODevice;
|
||||||
|
|||||||
@@ -50,6 +50,10 @@ struct VideoIOState
|
|||||||
bool keyerInterfaceAvailable = false;
|
bool keyerInterfaceAvailable = false;
|
||||||
bool externalKeyingActive = false;
|
bool externalKeyingActive = false;
|
||||||
double frameBudgetMilliseconds = 0.0;
|
double frameBudgetMilliseconds = 0.0;
|
||||||
|
bool actualDeckLinkBufferedFramesAvailable = false;
|
||||||
|
uint64_t actualDeckLinkBufferedFrames = 0;
|
||||||
|
double deckLinkScheduleCallMilliseconds = 0.0;
|
||||||
|
uint64_t deckLinkScheduleFailureCount = 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
struct VideoIOFrame
|
struct VideoIOFrame
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
#include <atlbase.h>
|
#include <atlbase.h>
|
||||||
#include <atomic>
|
#include <atomic>
|
||||||
|
#include <chrono>
|
||||||
#include <cstdio>
|
#include <cstdio>
|
||||||
#include <cstring>
|
#include <cstring>
|
||||||
#include <new>
|
#include <new>
|
||||||
@@ -526,8 +527,20 @@ bool DeckLinkSession::PopulateOutputFrame(IDeckLinkMutableVideoFrame* outputVide
|
|||||||
bool DeckLinkSession::ScheduleFrame(IDeckLinkMutableVideoFrame* outputVideoFrame)
|
bool DeckLinkSession::ScheduleFrame(IDeckLinkMutableVideoFrame* outputVideoFrame)
|
||||||
{
|
{
|
||||||
const VideoIOScheduleTime scheduleTime = mScheduler.NextScheduleTime();
|
const VideoIOScheduleTime scheduleTime = mScheduler.NextScheduleTime();
|
||||||
return outputVideoFrame != nullptr &&
|
if (outputVideoFrame == nullptr || output == nullptr)
|
||||||
output->ScheduleVideoFrame(outputVideoFrame, scheduleTime.streamTime, scheduleTime.duration, scheduleTime.timeScale) == S_OK;
|
{
|
||||||
|
++mState.deckLinkScheduleFailureCount;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto scheduleStart = std::chrono::steady_clock::now();
|
||||||
|
const HRESULT result = output->ScheduleVideoFrame(outputVideoFrame, scheduleTime.streamTime, scheduleTime.duration, scheduleTime.timeScale);
|
||||||
|
const auto scheduleEnd = std::chrono::steady_clock::now();
|
||||||
|
mState.deckLinkScheduleCallMilliseconds = std::chrono::duration_cast<std::chrono::duration<double, std::milli>>(scheduleEnd - scheduleStart).count();
|
||||||
|
if (result != S_OK)
|
||||||
|
++mState.deckLinkScheduleFailureCount;
|
||||||
|
RefreshBufferedVideoFrameCount();
|
||||||
|
return result == S_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool DeckLinkSession::ScheduleSystemMemoryFrame(const VideoIOOutputFrame& frame)
|
bool DeckLinkSession::ScheduleSystemMemoryFrame(const VideoIOOutputFrame& frame)
|
||||||
@@ -592,6 +605,26 @@ bool DeckLinkSession::ScheduleBlackFrame(IDeckLinkMutableVideoFrame* outputVideo
|
|||||||
return ScheduleFrame(outputVideoFrame);
|
return ScheduleFrame(outputVideoFrame);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void DeckLinkSession::RefreshBufferedVideoFrameCount()
|
||||||
|
{
|
||||||
|
if (output == nullptr)
|
||||||
|
{
|
||||||
|
mState.actualDeckLinkBufferedFramesAvailable = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
unsigned int bufferedFrameCount = 0;
|
||||||
|
if (output->GetBufferedVideoFrameCount(&bufferedFrameCount) == S_OK)
|
||||||
|
{
|
||||||
|
mState.actualDeckLinkBufferedFrames = bufferedFrameCount;
|
||||||
|
mState.actualDeckLinkBufferedFramesAvailable = true;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
mState.actualDeckLinkBufferedFramesAvailable = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
bool DeckLinkSession::BeginOutputFrame(VideoIOOutputFrame& frame)
|
bool DeckLinkSession::BeginOutputFrame(VideoIOOutputFrame& frame)
|
||||||
{
|
{
|
||||||
CComPtr<IDeckLinkMutableVideoFrame> outputVideoFrame;
|
CComPtr<IDeckLinkMutableVideoFrame> outputVideoFrame;
|
||||||
@@ -736,6 +769,8 @@ void DeckLinkSession::HandleVideoInputFrame(IDeckLinkVideoInputFrame* inputFrame
|
|||||||
|
|
||||||
void DeckLinkSession::HandlePlayoutFrameCompleted(IDeckLinkVideoFrame* completedFrame, BMDOutputFrameCompletionResult completionResult)
|
void DeckLinkSession::HandlePlayoutFrameCompleted(IDeckLinkVideoFrame* completedFrame, BMDOutputFrameCompletionResult completionResult)
|
||||||
{
|
{
|
||||||
|
RefreshBufferedVideoFrameCount();
|
||||||
|
|
||||||
void* completedSystemBuffer = nullptr;
|
void* completedSystemBuffer = nullptr;
|
||||||
if (completedFrame != nullptr)
|
if (completedFrame != nullptr)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -74,6 +74,7 @@ private:
|
|||||||
bool ScheduleFrame(IDeckLinkMutableVideoFrame* outputVideoFrame);
|
bool ScheduleFrame(IDeckLinkMutableVideoFrame* outputVideoFrame);
|
||||||
bool ScheduleSystemMemoryFrame(const VideoIOOutputFrame& frame);
|
bool ScheduleSystemMemoryFrame(const VideoIOOutputFrame& frame);
|
||||||
bool ScheduleBlackFrame(IDeckLinkMutableVideoFrame* outputVideoFrame);
|
bool ScheduleBlackFrame(IDeckLinkMutableVideoFrame* outputVideoFrame);
|
||||||
|
void RefreshBufferedVideoFrameCount();
|
||||||
static VideoIOCompletionResult TranslateCompletionResult(BMDOutputFrameCompletionResult completionResult);
|
static VideoIOCompletionResult TranslateCompletionResult(BMDOutputFrameCompletionResult completionResult);
|
||||||
|
|
||||||
CComPtr<CaptureDelegate> captureDelegate;
|
CComPtr<CaptureDelegate> captureDelegate;
|
||||||
|
|||||||
@@ -2,6 +2,8 @@
|
|||||||
|
|
||||||
This log tracks short readback experiments during the proactive playout timing work.
|
This log tracks short readback experiments during the proactive playout timing work.
|
||||||
|
|
||||||
|
The later experiments point to a larger ownership change rather than more local fallback tweaks. The proposed follow-up design is [PHASE_7_7_RENDER_CADENCE_PLAYOUT_DESIGN.md](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/docs/PHASE_7_7_RENDER_CADENCE_PLAYOUT_DESIGN.md).
|
||||||
|
|
||||||
## How To Run
|
## How To Run
|
||||||
|
|
||||||
The default debugger launch keeps the current production path:
|
The default debugger launch keeps the current production path:
|
||||||
|
|||||||
@@ -4,6 +4,11 @@
|
|||||||
|
|
||||||
In progress.
|
In progress.
|
||||||
|
|
||||||
|
Follow-up direction:
|
||||||
|
|
||||||
|
- Phase 7.6 proved the BGRA8 system-memory path and exposed the need for a larger cadence/scheduler split.
|
||||||
|
- Continue the broader rewrite in [PHASE_7_7_RENDER_CADENCE_PLAYOUT_DESIGN.md](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/docs/PHASE_7_7_RENDER_CADENCE_PLAYOUT_DESIGN.md).
|
||||||
|
|
||||||
Implemented so far:
|
Implemented so far:
|
||||||
|
|
||||||
- BGRA8 `SystemOutputFramePool` with non-GL tests
|
- BGRA8 `SystemOutputFramePool` with non-GL tests
|
||||||
|
|||||||
492
docs/PHASE_7_7_RENDER_CADENCE_PLAYOUT_DESIGN.md
Normal file
492
docs/PHASE_7_7_RENDER_CADENCE_PLAYOUT_DESIGN.md
Normal file
@@ -0,0 +1,492 @@
|
|||||||
|
# Phase 7.7: Render Cadence And Playout Separation Design
|
||||||
|
|
||||||
|
## Status
|
||||||
|
|
||||||
|
In progress.
|
||||||
|
|
||||||
|
Implemented so far:
|
||||||
|
|
||||||
|
- real DeckLink buffered-frame telemetry is exposed separately from synthetic scheduler lead
|
||||||
|
- pure `RenderCadenceController` exists with non-GL tests
|
||||||
|
- `SystemOutputFramePool` now exposes the Phase 7.7 state vocabulary: `Free`, `Rendering`, `Completed`, `Scheduled`
|
||||||
|
- the output producer now uses `RenderCadenceController` to render one output frame per cadence tick
|
||||||
|
- DeckLink scheduling remains a separate top-up pass capped by the configured preroll target
|
||||||
|
|
||||||
|
Phase 7.5 and 7.6 proved useful pieces individually:
|
||||||
|
|
||||||
|
- BGRA8 pack/readback can be fast enough on the current test machine.
|
||||||
|
- System-memory frame slots can be wrapped for DeckLink scheduling.
|
||||||
|
- A producer can keep frames ready and keep a small scheduled buffer filled.
|
||||||
|
|
||||||
|
But the experiments also showed that the current hybrid ownership model is fragile:
|
||||||
|
|
||||||
|
- completion-driven rendering caused app-ready starvation
|
||||||
|
- completion-time black fallback caused visible black flicker
|
||||||
|
- producer-side scheduling without a cadence target overfed the schedule timeline
|
||||||
|
- capping scheduled count helped, but completion and producer scheduling fought each other
|
||||||
|
- making completion passive exposed startup and scheduling-trigger gaps
|
||||||
|
- late/drop catch-up skipping created smooth/freeze/smooth cadence
|
||||||
|
|
||||||
|
The lesson is that the app needs a larger architectural split, not more local recovery branches.
|
||||||
|
|
||||||
|
## Goal
|
||||||
|
|
||||||
|
Make the output path behave like two cooperating real-time systems:
|
||||||
|
|
||||||
|
```text
|
||||||
|
Render cadence thread
|
||||||
|
renders at the selected output cadence, for example 59.94 fps
|
||||||
|
writes completed frames into system-memory slots
|
||||||
|
|
||||||
|
DeckLink playout scheduler
|
||||||
|
keeps the device scheduled buffer topped up
|
||||||
|
consumes completed system-memory frames
|
||||||
|
never asks rendering to happen synchronously
|
||||||
|
```
|
||||||
|
|
||||||
|
The system-memory frame buffer becomes the contract between render timing and device timing.
|
||||||
|
|
||||||
|
Core principle:
|
||||||
|
|
||||||
|
- The render cadence should be stable and boring.
|
||||||
|
- If the selected output mode is 59.94 fps, the render producer should attempt to render at 59.94 fps.
|
||||||
|
- It should not speed up just because the DeckLink buffer is empty.
|
||||||
|
- It should not slow down because DeckLink is full or because completed frames have not drained.
|
||||||
|
- Completed-but-unscheduled frames are a latest-N cache. Old completed frames may be dropped/recycled to keep rendering at cadence.
|
||||||
|
- Scheduled frames are protected until DeckLink completes them.
|
||||||
|
- The only normal reason for the render cadence to deviate is that rendering/GPU work itself overruns the frame budget.
|
||||||
|
|
||||||
|
## Non-Goals
|
||||||
|
|
||||||
|
- Do not hide failure by repeating frames as the primary strategy.
|
||||||
|
- Do not make DeckLink completion callbacks render frames.
|
||||||
|
- Do not use synthetic schedule-index catch-up as normal recovery.
|
||||||
|
- Do not change shader semantics or live-state semantics.
|
||||||
|
- Do not require v210/YUV packing in the first implementation.
|
||||||
|
- Do not pursue DVP/pinned-memory fast transfer as the main path on unsupported hardware.
|
||||||
|
|
||||||
|
## Target Architecture
|
||||||
|
|
||||||
|
### Current Problem Shape
|
||||||
|
|
||||||
|
The current Phase 7.5/7.6 implementation still has too many timing authorities:
|
||||||
|
|
||||||
|
- DeckLink completion callbacks release frames and influence scheduling
|
||||||
|
- the producer renders based on queue pressure
|
||||||
|
- the producer also schedules some frames
|
||||||
|
- `VideoPlayoutScheduler` advances synthetic stream-time indexes
|
||||||
|
- fallback behavior can schedule black when the app-ready queue is briefly empty
|
||||||
|
|
||||||
|
That means the system can be full and still look wrong, because "full" is not tied to one clear cadence owner.
|
||||||
|
|
||||||
|
### Target Shape
|
||||||
|
|
||||||
|
```text
|
||||||
|
Startup / warmup
|
||||||
|
render cadence starts first
|
||||||
|
render thread produces warmup frames at the selected cadence
|
||||||
|
completed system-memory queue reaches warmup target
|
||||||
|
DeckLink preroll is scheduled from completed frames
|
||||||
|
DeckLink playback starts with a filled buffer
|
||||||
|
|
||||||
|
Steady state
|
||||||
|
RenderCadenceController
|
||||||
|
owns output frame tick: frame 0, 1, 2...
|
||||||
|
owns render target time
|
||||||
|
asks RenderEngine to render frame N
|
||||||
|
publishes completed frame N into PlayoutFrameStore
|
||||||
|
|
||||||
|
PlayoutFrameStore
|
||||||
|
owns free / rendering / completed / scheduled slots
|
||||||
|
tracks frame number, render time, completion time, and schedule state
|
||||||
|
exposes latest completed frames to DeckLink scheduler
|
||||||
|
may drop/recycle oldest unscheduled completed frames when render cadence needs space
|
||||||
|
|
||||||
|
DeckLinkPlayoutScheduler
|
||||||
|
owns DeckLink schedule time
|
||||||
|
tops up device buffered frames to target depth
|
||||||
|
consumes completed frames only
|
||||||
|
releases scheduled slots on completion callbacks
|
||||||
|
|
||||||
|
DeckLink completion callback
|
||||||
|
releases completed slots
|
||||||
|
records result and device timing
|
||||||
|
wakes scheduler
|
||||||
|
does not render
|
||||||
|
```
|
||||||
|
|
||||||
|
## Cadence Model
|
||||||
|
|
||||||
|
The render side should be time-driven, not completion-driven.
|
||||||
|
|
||||||
|
For a 59.94 fps mode:
|
||||||
|
|
||||||
|
```text
|
||||||
|
frameDuration = 1001 / 60000 seconds
|
||||||
|
nextRenderTime = now
|
||||||
|
|
||||||
|
loop:
|
||||||
|
wait until nextRenderTime, or run immediately if behind
|
||||||
|
render frameIndex for nextRenderTime
|
||||||
|
read back into free system-memory slot
|
||||||
|
publish completed slot
|
||||||
|
frameIndex += 1
|
||||||
|
nextRenderTime += frameDuration
|
||||||
|
```
|
||||||
|
|
||||||
|
Rules:
|
||||||
|
|
||||||
|
- If the render thread is early, it waits/yields.
|
||||||
|
- If it is slightly late, it renders the next frame immediately and records lateness.
|
||||||
|
- If it is badly late because render/GPU work overran the frame budget, policy may skip render ticks before rendering the newest frame.
|
||||||
|
- Skipping render ticks is an overrun policy, not a buffer-fill strategy.
|
||||||
|
- DeckLink schedule time should remain continuous unless a deliberate device recovery policy says otherwise.
|
||||||
|
|
||||||
|
Non-rule:
|
||||||
|
|
||||||
|
- The render producer must not render faster than the selected cadence to refill DeckLink.
|
||||||
|
- DeckLink should start only after warmup/preroll has filled enough completed frames.
|
||||||
|
- If the DeckLink buffer drains in steady state, that is a real timing failure to measure, not a signal for the render thread to sprint.
|
||||||
|
|
||||||
|
## Buffer Model
|
||||||
|
|
||||||
|
Use a fixed system-memory slot pool.
|
||||||
|
|
||||||
|
The completed portion of the pool is not a strict consume-before-render queue. It is a latest-N rendered-frame cache:
|
||||||
|
|
||||||
|
- render cadence writes one frame per selected output tick
|
||||||
|
- if completed-but-unscheduled frames are full, the oldest completed frame is disposable
|
||||||
|
- DeckLink scheduling consumes from the completed cache when it needs frames
|
||||||
|
- frames already scheduled to DeckLink are never recycled until completion
|
||||||
|
- if all slots are scheduled/in flight, cadence may miss because there is genuinely no safe system-memory target
|
||||||
|
|
||||||
|
Suggested starting values:
|
||||||
|
|
||||||
|
- completed-frame target: 2-4 frames
|
||||||
|
- DeckLink scheduled target: 4 frames for experiments
|
||||||
|
- total system slots: scheduled target + completed target + rendering spare + safety spare
|
||||||
|
|
||||||
|
For example:
|
||||||
|
|
||||||
|
```text
|
||||||
|
scheduled target: 4
|
||||||
|
completed target: 3
|
||||||
|
rendering/spare: 2
|
||||||
|
total slots: 9
|
||||||
|
```
|
||||||
|
|
||||||
|
Slot states:
|
||||||
|
|
||||||
|
- `Free`
|
||||||
|
- `Rendering`
|
||||||
|
- `Completed`
|
||||||
|
- `Scheduled`
|
||||||
|
|
||||||
|
Each slot should carry:
|
||||||
|
|
||||||
|
- frame index
|
||||||
|
- render target timestamp
|
||||||
|
- render completion timestamp
|
||||||
|
- pixel format
|
||||||
|
- row bytes and size
|
||||||
|
- schedule timestamp/index when scheduled
|
||||||
|
- completion result when released
|
||||||
|
|
||||||
|
## Scheduling Model
|
||||||
|
|
||||||
|
The DeckLink scheduler should top up to a target device depth.
|
||||||
|
|
||||||
|
```text
|
||||||
|
on scheduler wake:
|
||||||
|
while actualDeckLinkBufferedFrames < targetScheduledFrames:
|
||||||
|
frame = completedStore.popOldestCompleted()
|
||||||
|
if no frame:
|
||||||
|
record completed-frame underrun
|
||||||
|
break
|
||||||
|
schedule frame at next continuous DeckLink stream time
|
||||||
|
```
|
||||||
|
|
||||||
|
Important:
|
||||||
|
|
||||||
|
- Use DeckLink `GetBufferedVideoFrameCount()` where available.
|
||||||
|
- Keep synthetic scheduled/completed indexes as diagnostics only.
|
||||||
|
- Do not infer device buffer depth from `mScheduledFrameIndex - mCompletedFrameIndex`.
|
||||||
|
- Do not schedule black because the app completed queue is momentarily empty while the device still has frames buffered.
|
||||||
|
- Use black only before the first valid frame or in explicit emergency fallback.
|
||||||
|
|
||||||
|
## Thread Ownership
|
||||||
|
|
||||||
|
### Render Cadence Thread
|
||||||
|
|
||||||
|
Owns:
|
||||||
|
|
||||||
|
- render tick timing
|
||||||
|
- acquiring a free system-memory slot
|
||||||
|
- requesting render-thread output render/readback
|
||||||
|
- publishing completed frames
|
||||||
|
|
||||||
|
Does not own:
|
||||||
|
|
||||||
|
- DeckLink schedule time
|
||||||
|
- completion callback processing
|
||||||
|
- fallback black scheduling
|
||||||
|
|
||||||
|
### RenderEngine Render Thread
|
||||||
|
|
||||||
|
Owns:
|
||||||
|
|
||||||
|
- GL context
|
||||||
|
- input upload
|
||||||
|
- shader rendering
|
||||||
|
- output packing/readback
|
||||||
|
- preview present when allowed
|
||||||
|
|
||||||
|
Output render work should have priority over preview/screenshot work.
|
||||||
|
|
||||||
|
### DeckLink Scheduler Thread
|
||||||
|
|
||||||
|
Owns:
|
||||||
|
|
||||||
|
- schedule top-up policy
|
||||||
|
- DeckLink `ScheduleVideoFrame`
|
||||||
|
- device buffered-frame telemetry
|
||||||
|
- consuming completed frames
|
||||||
|
|
||||||
|
Does not own:
|
||||||
|
|
||||||
|
- rendering a missing frame
|
||||||
|
- running live-state composition directly
|
||||||
|
|
||||||
|
### Completion Callback / Worker
|
||||||
|
|
||||||
|
Owns:
|
||||||
|
|
||||||
|
- releasing scheduled system slots
|
||||||
|
- recording completion result
|
||||||
|
- waking scheduler and render cadence loops
|
||||||
|
|
||||||
|
Does not own:
|
||||||
|
|
||||||
|
- rendering
|
||||||
|
- scheduling fallback black during normal steady state
|
||||||
|
|
||||||
|
## What Happens Under Stress
|
||||||
|
|
||||||
|
### Render Is Temporarily Late
|
||||||
|
|
||||||
|
- Completed-frame queue drains.
|
||||||
|
- DeckLink scheduled buffer drains.
|
||||||
|
- Telemetry shows render lateness and completed queue depth drop.
|
||||||
|
- If render catches up before device buffer reaches zero, output remains smooth.
|
||||||
|
|
||||||
|
### Render Cannot Sustain Cadence
|
||||||
|
|
||||||
|
- Completed-frame queue stays low.
|
||||||
|
- DeckLink buffer trends down.
|
||||||
|
- Late/drop telemetry increases.
|
||||||
|
- Policy may choose to skip render ticks, lower preview load, or enter degraded state.
|
||||||
|
|
||||||
|
### DeckLink Timing Jitters
|
||||||
|
|
||||||
|
- Scheduler tops up based on actual device buffered count.
|
||||||
|
- Render cadence continues independently.
|
||||||
|
- System-memory buffer absorbs short mismatch.
|
||||||
|
|
||||||
|
### UI Loses Focus
|
||||||
|
|
||||||
|
- Render cadence should continue.
|
||||||
|
- Preview present may be disabled or deprioritized.
|
||||||
|
- Output/render threads may need elevated priority.
|
||||||
|
- Device buffer telemetry should reveal whether Windows focus changes affect render cadence or only preview.
|
||||||
|
|
||||||
|
## Migration Plan
|
||||||
|
|
||||||
|
### Step 1: Add Real DeckLink Buffer Telemetry
|
||||||
|
|
||||||
|
Before more scheduling changes, measure the real device buffer.
|
||||||
|
|
||||||
|
Deliverables:
|
||||||
|
|
||||||
|
- [x] call DeckLink `GetBufferedVideoFrameCount()` after schedule/completion where available
|
||||||
|
- [x] expose `actualDeckLinkBufferedFrames`
|
||||||
|
- [x] keep `scheduledLeadFrames` but label it synthetic/internal
|
||||||
|
- [x] record schedule-call duration and failures
|
||||||
|
|
||||||
|
Exit criteria:
|
||||||
|
|
||||||
|
- [x] runtime telemetry distinguishes app completed queue, system scheduled slots, synthetic lead, and actual DeckLink buffer depth
|
||||||
|
|
||||||
|
### Step 2: Rename Existing Queues To Match Their Roles
|
||||||
|
|
||||||
|
Clarify vocabulary before rewriting behavior.
|
||||||
|
|
||||||
|
Deliverables:
|
||||||
|
|
||||||
|
- rename or document `RenderOutputQueue` as completed/unscheduled frame queue
|
||||||
|
- distinguish completed-frame depth from device scheduled depth
|
||||||
|
- update telemetry labels where possible
|
||||||
|
|
||||||
|
Exit criteria:
|
||||||
|
|
||||||
|
- logs no longer imply `readyQueue.depth == 0` means DeckLink starvation
|
||||||
|
|
||||||
|
### Step 3: Introduce `RenderCadenceController`
|
||||||
|
|
||||||
|
Add a pure timing helper first.
|
||||||
|
|
||||||
|
Responsibilities:
|
||||||
|
|
||||||
|
- [x] compute next render tick
|
||||||
|
- [x] track frame duration
|
||||||
|
- [x] report early/late/drift
|
||||||
|
- [x] decide whether to render, wait, or skip render ticks
|
||||||
|
|
||||||
|
Tests:
|
||||||
|
|
||||||
|
- [x] exact cadence advances
|
||||||
|
- [x] late ticks are measured
|
||||||
|
- [x] large lateness can skip according to policy
|
||||||
|
- [x] no dependency on GL or DeckLink
|
||||||
|
|
||||||
|
### Step 4: Move Output Production To Cadence Ticks
|
||||||
|
|
||||||
|
Replace queue-pressure-only production with cadence-driven production.
|
||||||
|
|
||||||
|
Initial behavior:
|
||||||
|
|
||||||
|
- [x] render at selected output cadence
|
||||||
|
- [x] produce into system-memory slots
|
||||||
|
- [x] publish completed frames
|
||||||
|
- [x] recycle/drop oldest unscheduled completed frames when cadence needs a slot
|
||||||
|
- [ ] only wait when every safe slot is scheduled/in flight
|
||||||
|
|
||||||
|
Exit criteria:
|
||||||
|
|
||||||
|
- output rendering continues without DeckLink completions
|
||||||
|
- output rendering does not schedule DeckLink directly
|
||||||
|
- completed-frame buffering behaves as latest-N, not consume-before-render
|
||||||
|
|
||||||
|
### Step 4a: Add Warmup Before DeckLink Playback
|
||||||
|
|
||||||
|
DeckLink output should not start consuming before the render cadence has prepared an initial cushion.
|
||||||
|
|
||||||
|
Initial behavior:
|
||||||
|
|
||||||
|
- configure DeckLink output without starting scheduled playback
|
||||||
|
- start the render cadence producer
|
||||||
|
- render warmup frames at the selected cadence, not faster
|
||||||
|
- wait until completed-frame depth reaches `targetWarmupFrames`
|
||||||
|
- schedule those completed frames as DeckLink preroll
|
||||||
|
- call `StartScheduledPlayback()`
|
||||||
|
|
||||||
|
Exit criteria:
|
||||||
|
|
||||||
|
- startup does not require the render producer to catch up by rendering faster than cadence
|
||||||
|
- DeckLink begins playback with a real completed-frame buffer
|
||||||
|
- if warmup cannot fill within a bounded timeout, startup enters degraded state with telemetry
|
||||||
|
|
||||||
|
### Step 5: Make DeckLink Scheduler A Separate Top-Up Loop
|
||||||
|
|
||||||
|
Create a scheduler loop that consumes completed frames.
|
||||||
|
|
||||||
|
Initial behavior:
|
||||||
|
|
||||||
|
- wake on completion, completed-frame publish, and periodic safety timer
|
||||||
|
- top up actual DeckLink buffer to target
|
||||||
|
- schedule only completed system-memory frames
|
||||||
|
- do not render or black-fill during normal steady state
|
||||||
|
|
||||||
|
Exit criteria:
|
||||||
|
|
||||||
|
- producer and DeckLink scheduler are separate loops
|
||||||
|
- one component owns schedule time
|
||||||
|
|
||||||
|
### Step 6: Remove Synthetic Catch-Up From Steady State
|
||||||
|
|
||||||
|
Disable catch-up frame skipping for proactive mode.
|
||||||
|
|
||||||
|
Replacement:
|
||||||
|
|
||||||
|
- render cadence may skip render ticks if the renderer is late
|
||||||
|
- completed queue may drop oldest or newest according to explicit policy
|
||||||
|
- DeckLink schedule time remains continuous
|
||||||
|
|
||||||
|
Exit criteria:
|
||||||
|
|
||||||
|
- scheduled stream time advances one frame per scheduled frame unless emergency recovery is explicitly enabled
|
||||||
|
|
||||||
|
### Step 7: Prioritize Output Render Work
|
||||||
|
|
||||||
|
Reduce render-thread interference.
|
||||||
|
|
||||||
|
Deliverables:
|
||||||
|
|
||||||
|
- output render commands outrank preview present
|
||||||
|
- preview skipped/deferred count is visible
|
||||||
|
- input upload timing is measured separately
|
||||||
|
- screenshot/readback cannot block output cadence unless explicitly requested
|
||||||
|
|
||||||
|
Exit criteria:
|
||||||
|
|
||||||
|
- focus changes and preview present do not drain playout buffer
|
||||||
|
|
||||||
|
### Step 8: Tune Thread Priority And Wait Strategy
|
||||||
|
|
||||||
|
Only after ownership is separated, tune scheduling.
|
||||||
|
|
||||||
|
Deliverables:
|
||||||
|
|
||||||
|
- set render cadence and DeckLink scheduler threads to appropriate Windows priorities
|
||||||
|
- avoid busy spinning
|
||||||
|
- use waitable timers or high-resolution waits where useful
|
||||||
|
- record wake jitter
|
||||||
|
|
||||||
|
Exit criteria:
|
||||||
|
|
||||||
|
- cadence jitter is measurable and bounded
|
||||||
|
|
||||||
|
## Telemetry
|
||||||
|
|
||||||
|
Add or clarify:
|
||||||
|
|
||||||
|
- `renderCadence.targetFps`
|
||||||
|
- `renderCadence.frameIndex`
|
||||||
|
- `renderCadence.lateMs`
|
||||||
|
- `renderCadence.maxLateMs`
|
||||||
|
- `renderCadence.skippedTicks`
|
||||||
|
- `completedFrames.depth`
|
||||||
|
- `completedFrames.capacity`
|
||||||
|
- `completedFrames.underruns`
|
||||||
|
- `systemMemory.free`
|
||||||
|
- `systemMemory.rendering`
|
||||||
|
- `systemMemory.completed`
|
||||||
|
- `systemMemory.scheduled`
|
||||||
|
- `decklink.actualBufferedFrames`
|
||||||
|
- `decklink.targetBufferedFrames`
|
||||||
|
- `decklink.scheduleCallMs`
|
||||||
|
- `decklink.scheduleFailures`
|
||||||
|
- `decklink.completionIntervalMs`
|
||||||
|
- `decklink.lateFrames`
|
||||||
|
- `decklink.droppedFrames`
|
||||||
|
- `scheduler.syntheticLeadFrames`
|
||||||
|
|
||||||
|
## Risks
|
||||||
|
|
||||||
|
- A cadence thread can render frames that DeckLink later drops if scheduling is wrong.
|
||||||
|
- Too much buffering adds latency.
|
||||||
|
- Too little buffering exposes Windows scheduling jitter.
|
||||||
|
- If output render and input upload still share one GL thread, render cadence can still be disturbed by uploads.
|
||||||
|
- Actual DeckLink buffer telemetry may differ from app-owned scheduled-slot counts.
|
||||||
|
|
||||||
|
## Exit Criteria
|
||||||
|
|
||||||
|
Phase 7.7 is complete when:
|
||||||
|
|
||||||
|
- output rendering is driven by a render cadence controller
|
||||||
|
- DeckLink completion callbacks do not render
|
||||||
|
- DeckLink scheduling is owned by a scheduler/top-up loop
|
||||||
|
- system-memory completed frames are the only contract between render and DeckLink scheduling
|
||||||
|
- real DeckLink buffered-frame count is visible
|
||||||
|
- synthetic schedule lead no longer drives normal recovery
|
||||||
|
- black fallback is startup/emergency only
|
||||||
|
- playback can be tested with 4-frame and larger buffers without changing ownership logic
|
||||||
@@ -129,6 +129,7 @@ void TestBackendPlayoutHealth()
|
|||||||
Expect(playout.outputFrameEndAccessMilliseconds == 0.5, "backend playout health stores output frame end access duration");
|
Expect(playout.outputFrameEndAccessMilliseconds == 0.5, "backend playout health stores output frame end access duration");
|
||||||
Expect(playout.completedFrameIndex == 8, "backend playout health stores completed index");
|
Expect(playout.completedFrameIndex == 8, "backend playout health stores completed index");
|
||||||
Expect(playout.scheduledFrameIndex == 11, "backend playout health stores scheduled index");
|
Expect(playout.scheduledFrameIndex == 11, "backend playout health stores scheduled index");
|
||||||
|
Expect(playout.scheduledLeadFrames == 3, "backend playout health stores synthetic scheduled lead");
|
||||||
Expect(playout.measuredLagFrames == 2, "backend playout health stores measured lag");
|
Expect(playout.measuredLagFrames == 2, "backend playout health stores measured lag");
|
||||||
Expect(playout.catchUpFrames == 2, "backend playout health stores catch-up frames");
|
Expect(playout.catchUpFrames == 2, "backend playout health stores catch-up frames");
|
||||||
Expect(playout.lateStreak == 1, "backend playout health stores late streak");
|
Expect(playout.lateStreak == 1, "backend playout health stores late streak");
|
||||||
@@ -228,6 +229,28 @@ void TestSystemMemoryPlayoutStats()
|
|||||||
Expect(playout.systemFrameAgeAtScheduleMilliseconds == 0.0, "system-memory playout clamps negative schedule age");
|
Expect(playout.systemFrameAgeAtScheduleMilliseconds == 0.0, "system-memory playout clamps negative schedule age");
|
||||||
Expect(playout.systemFrameAgeAtCompletionMilliseconds == 0.0, "system-memory playout clamps negative completion age");
|
Expect(playout.systemFrameAgeAtCompletionMilliseconds == 0.0, "system-memory playout clamps negative completion age");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void TestDeckLinkBufferTelemetry()
|
||||||
|
{
|
||||||
|
HealthTelemetry telemetry;
|
||||||
|
telemetry.RecordDeckLinkBufferTelemetry(true, 4, 5, 0.25, 2);
|
||||||
|
|
||||||
|
HealthTelemetry::BackendPlayoutSnapshot playout = telemetry.GetBackendPlayoutSnapshot();
|
||||||
|
Expect(playout.actualDeckLinkBufferedFramesAvailable, "DeckLink buffer telemetry records availability");
|
||||||
|
Expect(playout.actualDeckLinkBufferedFrames == 4, "DeckLink buffer telemetry stores actual device depth");
|
||||||
|
Expect(playout.targetDeckLinkBufferedFrames == 5, "DeckLink buffer telemetry stores target device depth");
|
||||||
|
Expect(playout.deckLinkScheduleCallMilliseconds == 0.25, "DeckLink buffer telemetry stores schedule call duration");
|
||||||
|
Expect(playout.deckLinkScheduleFailureCount == 2, "DeckLink buffer telemetry stores schedule failures");
|
||||||
|
|
||||||
|
Expect(telemetry.TryRecordDeckLinkBufferTelemetry(false, 9, 3, -1.0, 7),
|
||||||
|
"try DeckLink buffer telemetry succeeds when uncontended");
|
||||||
|
playout = telemetry.GetBackendPlayoutSnapshot();
|
||||||
|
Expect(!playout.actualDeckLinkBufferedFramesAvailable, "DeckLink buffer telemetry records unavailable device depth");
|
||||||
|
Expect(playout.actualDeckLinkBufferedFrames == 0, "unavailable DeckLink device depth clears actual count");
|
||||||
|
Expect(playout.targetDeckLinkBufferedFrames == 3, "try DeckLink buffer telemetry stores target device depth");
|
||||||
|
Expect(playout.deckLinkScheduleCallMilliseconds == 0.0, "DeckLink buffer telemetry clamps negative schedule call duration");
|
||||||
|
Expect(playout.deckLinkScheduleFailureCount == 7, "try DeckLink buffer telemetry stores schedule failures");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
int main()
|
int main()
|
||||||
@@ -239,6 +262,7 @@ int main()
|
|||||||
TestBackendPlayoutHealth();
|
TestBackendPlayoutHealth();
|
||||||
TestOutputRenderPipelineTiming();
|
TestOutputRenderPipelineTiming();
|
||||||
TestSystemMemoryPlayoutStats();
|
TestSystemMemoryPlayoutStats();
|
||||||
|
TestDeckLinkBufferTelemetry();
|
||||||
|
|
||||||
if (gFailures != 0)
|
if (gFailures != 0)
|
||||||
{
|
{
|
||||||
|
|||||||
172
tests/RenderCadenceControllerTests.cpp
Normal file
172
tests/RenderCadenceControllerTests.cpp
Normal file
@@ -0,0 +1,172 @@
|
|||||||
|
#include "RenderCadenceController.h"
|
||||||
|
|
||||||
|
#include <chrono>
|
||||||
|
#include <iostream>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
int gFailures = 0;
|
||||||
|
|
||||||
|
using Clock = RenderCadenceController::Clock;
|
||||||
|
using Duration = RenderCadenceController::Duration;
|
||||||
|
using TimePoint = RenderCadenceController::TimePoint;
|
||||||
|
|
||||||
|
void Expect(bool condition, const char* message)
|
||||||
|
{
|
||||||
|
if (condition)
|
||||||
|
return;
|
||||||
|
|
||||||
|
std::cerr << "FAIL: " << message << "\n";
|
||||||
|
++gFailures;
|
||||||
|
}
|
||||||
|
|
||||||
|
Duration Ms(int64_t value)
|
||||||
|
{
|
||||||
|
return std::chrono::duration_cast<Duration>(std::chrono::milliseconds(value));
|
||||||
|
}
|
||||||
|
|
||||||
|
void TestExactCadenceAdvancesFrameIndexAndNextTick()
|
||||||
|
{
|
||||||
|
RenderCadenceController controller;
|
||||||
|
const TimePoint start = Clock::time_point(Ms(1000));
|
||||||
|
controller.Configure(Ms(20), start);
|
||||||
|
|
||||||
|
RenderCadenceDecision first = controller.Tick(start);
|
||||||
|
Expect(first.action == RenderCadenceAction::Render, "first exact tick renders");
|
||||||
|
Expect(first.frameIndex == 0, "first exact tick renders frame zero");
|
||||||
|
Expect(first.renderTargetTime == start, "first exact target is configured start");
|
||||||
|
Expect(first.nextRenderTime == start + Ms(20), "first exact tick advances next render time");
|
||||||
|
Expect(first.skippedTicks == 0, "first exact tick skips no ticks");
|
||||||
|
Expect(first.lateness == Duration::zero(), "first exact tick records no lateness");
|
||||||
|
|
||||||
|
RenderCadenceDecision second = controller.Tick(start + Ms(20));
|
||||||
|
Expect(second.action == RenderCadenceAction::Render, "second exact tick renders");
|
||||||
|
Expect(second.frameIndex == 1, "second exact tick renders frame one");
|
||||||
|
Expect(controller.NextFrameIndex() == 2, "controller tracks next frame index after exact ticks");
|
||||||
|
Expect(controller.Metrics().renderedFrameCount == 2, "metrics count exact rendered frames");
|
||||||
|
}
|
||||||
|
|
||||||
|
void TestEarlyTickWaitsWithoutAdvancing()
|
||||||
|
{
|
||||||
|
RenderCadenceController controller;
|
||||||
|
const TimePoint start = Clock::time_point(Ms(0));
|
||||||
|
controller.Configure(Ms(20), start);
|
||||||
|
(void)controller.Tick(start);
|
||||||
|
|
||||||
|
RenderCadenceDecision decision = controller.Tick(start + Ms(10));
|
||||||
|
Expect(decision.action == RenderCadenceAction::Wait, "early tick waits");
|
||||||
|
Expect(decision.waitDuration == Ms(10), "early tick reports wait duration");
|
||||||
|
Expect(decision.frameIndex == 1, "early tick reports next pending frame");
|
||||||
|
Expect(controller.NextFrameIndex() == 1, "early tick does not advance frame index");
|
||||||
|
Expect(controller.NextRenderTime() == start + Ms(20), "early tick does not advance next render time");
|
||||||
|
}
|
||||||
|
|
||||||
|
void TestSlightLatenessRendersAndRecordsMetrics()
|
||||||
|
{
|
||||||
|
RenderCadencePolicy policy;
|
||||||
|
policy.skipThresholdFrames = 3.0;
|
||||||
|
|
||||||
|
RenderCadenceController controller;
|
||||||
|
const TimePoint start = Clock::time_point(Ms(0));
|
||||||
|
controller.Configure(Ms(20), start, policy);
|
||||||
|
|
||||||
|
RenderCadenceDecision decision = controller.Tick(start + Ms(5));
|
||||||
|
Expect(decision.action == RenderCadenceAction::Render, "slightly late tick renders");
|
||||||
|
Expect(decision.frameIndex == 0, "slightly late tick keeps pending frame");
|
||||||
|
Expect(decision.skippedTicks == 0, "slightly late tick skips no ticks");
|
||||||
|
Expect(decision.lateness == Ms(5), "slightly late tick reports lateness");
|
||||||
|
Expect(controller.Metrics().lateFrameCount == 1, "metrics count late rendered frame");
|
||||||
|
Expect(controller.Metrics().lastLateness == Ms(5), "metrics keep last lateness");
|
||||||
|
Expect(controller.Metrics().maxLateness == Ms(5), "metrics keep max lateness");
|
||||||
|
}
|
||||||
|
|
||||||
|
void TestLargeLatenessSkipsTicksAccordingToPolicy()
|
||||||
|
{
|
||||||
|
RenderCadencePolicy policy;
|
||||||
|
policy.skipLateTicks = true;
|
||||||
|
policy.skipThresholdFrames = 2.0;
|
||||||
|
policy.maxSkippedTicksPerDecision = 8;
|
||||||
|
|
||||||
|
RenderCadenceController controller;
|
||||||
|
const TimePoint start = Clock::time_point(Ms(0));
|
||||||
|
controller.Configure(Ms(20), start, policy);
|
||||||
|
|
||||||
|
RenderCadenceDecision decision = controller.Tick(start + Ms(70));
|
||||||
|
Expect(decision.action == RenderCadenceAction::Render, "large late tick renders newest allowed frame");
|
||||||
|
Expect(decision.skippedTicks == 3, "large late tick skips elapsed render ticks");
|
||||||
|
Expect(decision.frameIndex == 3, "large late tick renders skipped-to frame");
|
||||||
|
Expect(decision.renderTargetTime == start + Ms(60), "large late tick targets newest elapsed tick");
|
||||||
|
Expect(decision.lateness == Ms(10), "large late tick measures residual lateness");
|
||||||
|
Expect(controller.NextFrameIndex() == 4, "large late tick advances past rendered frame");
|
||||||
|
Expect(controller.NextRenderTime() == start + Ms(80), "large late tick advances to following cadence");
|
||||||
|
Expect(controller.Metrics().skippedTickCount == 3, "metrics count skipped ticks");
|
||||||
|
}
|
||||||
|
|
||||||
|
void TestSkipPolicyCanDisableOrCapSkippedTicks()
|
||||||
|
{
|
||||||
|
const TimePoint start = Clock::time_point(Ms(0));
|
||||||
|
|
||||||
|
RenderCadencePolicy disabledPolicy;
|
||||||
|
disabledPolicy.skipLateTicks = false;
|
||||||
|
RenderCadenceController disabledController;
|
||||||
|
disabledController.Configure(Ms(20), start, disabledPolicy);
|
||||||
|
RenderCadenceDecision disabled = disabledController.Tick(start + Ms(90));
|
||||||
|
Expect(disabled.skippedTicks == 0, "disabled skip policy renders pending frame");
|
||||||
|
Expect(disabled.frameIndex == 0, "disabled skip policy preserves pending frame index");
|
||||||
|
|
||||||
|
RenderCadencePolicy cappedPolicy;
|
||||||
|
cappedPolicy.skipThresholdFrames = 1.0;
|
||||||
|
cappedPolicy.maxSkippedTicksPerDecision = 2;
|
||||||
|
RenderCadenceController cappedController;
|
||||||
|
cappedController.Configure(Ms(20), start, cappedPolicy);
|
||||||
|
RenderCadenceDecision capped = cappedController.Tick(start + Ms(90));
|
||||||
|
Expect(capped.skippedTicks == 2, "skip policy caps skipped ticks");
|
||||||
|
Expect(capped.frameIndex == 2, "capped skip renders capped frame index");
|
||||||
|
}
|
||||||
|
|
||||||
|
void TestResetRestartsCadenceAndMetrics()
|
||||||
|
{
|
||||||
|
RenderCadenceController controller;
|
||||||
|
const TimePoint start = Clock::time_point(Ms(0));
|
||||||
|
controller.Configure(Ms(20), start);
|
||||||
|
(void)controller.Tick(start + Ms(50));
|
||||||
|
|
||||||
|
const TimePoint restarted = start + Ms(200);
|
||||||
|
controller.Reset(restarted);
|
||||||
|
|
||||||
|
Expect(controller.NextFrameIndex() == 0, "reset restarts frame index");
|
||||||
|
Expect(controller.NextRenderTime() == restarted, "reset restarts next render time");
|
||||||
|
Expect(controller.Metrics().renderedFrameCount == 0, "reset clears rendered metrics");
|
||||||
|
|
||||||
|
RenderCadenceDecision decision = controller.Tick(restarted);
|
||||||
|
Expect(decision.action == RenderCadenceAction::Render, "reset cadence renders at new start");
|
||||||
|
Expect(decision.frameIndex == 0, "reset cadence renders frame zero");
|
||||||
|
}
|
||||||
|
|
||||||
|
void TestActionNames()
|
||||||
|
{
|
||||||
|
Expect(RenderCadenceActionName(RenderCadenceAction::Render) == std::string("Render"), "render action has name");
|
||||||
|
Expect(RenderCadenceActionName(RenderCadenceAction::Wait) == std::string("Wait"), "wait action has name");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int main()
|
||||||
|
{
|
||||||
|
TestExactCadenceAdvancesFrameIndexAndNextTick();
|
||||||
|
TestEarlyTickWaitsWithoutAdvancing();
|
||||||
|
TestSlightLatenessRendersAndRecordsMetrics();
|
||||||
|
TestLargeLatenessSkipsTicksAccordingToPolicy();
|
||||||
|
TestSkipPolicyCanDisableOrCapSkippedTicks();
|
||||||
|
TestResetRestartsCadenceAndMetrics();
|
||||||
|
TestActionNames();
|
||||||
|
|
||||||
|
if (gFailures != 0)
|
||||||
|
{
|
||||||
|
std::cerr << gFailures << " RenderCadenceController test failure(s).\n";
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::cout << "RenderCadenceController tests passed.\n";
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
@@ -94,6 +94,29 @@ void TestOverflowReleasesDroppedFrame()
|
|||||||
Expect(gReleasedFrames == 1, "pop transfers ownership without releasing");
|
Expect(gReleasedFrames == 1, "pop transfers ownership without releasing");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void TestDropOldestFrameReleasesFrame()
|
||||||
|
{
|
||||||
|
gReleasedFrames = 0;
|
||||||
|
VideoPlayoutPolicy policy;
|
||||||
|
policy.maxReadyFrames = 2;
|
||||||
|
RenderOutputQueue queue(policy);
|
||||||
|
|
||||||
|
queue.Push(MakeOwnedFrame(1));
|
||||||
|
queue.Push(MakeOwnedFrame(2));
|
||||||
|
|
||||||
|
Expect(queue.DropOldestFrame(), "oldest ready frame can be explicitly dropped");
|
||||||
|
Expect(gReleasedFrames == 1, "explicit drop releases oldest frame");
|
||||||
|
|
||||||
|
RenderOutputQueueMetrics metrics = queue.GetMetrics();
|
||||||
|
Expect(metrics.depth == 1, "explicit drop reduces queue depth");
|
||||||
|
Expect(metrics.droppedCount == 1, "explicit drop increments dropped count");
|
||||||
|
|
||||||
|
RenderOutputFrame frame;
|
||||||
|
Expect(queue.TryPop(frame), "newest frame remains after explicit drop");
|
||||||
|
Expect(frame.frameIndex == 2, "explicit drop keeps newest frame");
|
||||||
|
Expect(!queue.DropOldestFrame(), "empty queue cannot drop a frame");
|
||||||
|
}
|
||||||
|
|
||||||
void TestUnderrunIsCounted()
|
void TestUnderrunIsCounted()
|
||||||
{
|
{
|
||||||
RenderOutputQueue queue;
|
RenderOutputQueue queue;
|
||||||
@@ -169,6 +192,7 @@ int main()
|
|||||||
TestQueuePreservesOrdering();
|
TestQueuePreservesOrdering();
|
||||||
TestBoundedQueueDropsOldestFrame();
|
TestBoundedQueueDropsOldestFrame();
|
||||||
TestOverflowReleasesDroppedFrame();
|
TestOverflowReleasesDroppedFrame();
|
||||||
|
TestDropOldestFrameReleasesFrame();
|
||||||
TestUnderrunIsCounted();
|
TestUnderrunIsCounted();
|
||||||
TestConfigureShrinksDepthToNewCapacity();
|
TestConfigureShrinksDepthToNewCapacity();
|
||||||
TestConfigureReleasesTrimmedFrames();
|
TestConfigureReleasesTrimmedFrames();
|
||||||
|
|||||||
@@ -48,10 +48,50 @@ void TestAcquireHonorsCapacityAndFrameShape()
|
|||||||
|
|
||||||
SystemOutputFramePoolMetrics metrics = pool.GetMetrics();
|
SystemOutputFramePoolMetrics metrics = pool.GetMetrics();
|
||||||
Expect(metrics.freeCount == 0, "all slots are in use");
|
Expect(metrics.freeCount == 0, "all slots are in use");
|
||||||
|
Expect(metrics.renderingCount == 2, "rendering slots are counted");
|
||||||
Expect(metrics.acquiredCount == 2, "acquired slots are counted");
|
Expect(metrics.acquiredCount == 2, "acquired slots are counted");
|
||||||
Expect(metrics.acquireMissCount == 1, "capacity miss is counted");
|
Expect(metrics.acquireMissCount == 1, "capacity miss is counted");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void TestPhase77StateContract()
|
||||||
|
{
|
||||||
|
SystemOutputFramePool pool(MakeConfig(1));
|
||||||
|
|
||||||
|
SystemOutputFramePoolMetrics metrics = pool.GetMetrics();
|
||||||
|
Expect(metrics.freeCount == 1, "new pool starts with one free slot");
|
||||||
|
Expect(metrics.renderingCount == 0, "new pool starts with no rendering slots");
|
||||||
|
Expect(metrics.completedCount == 0, "new pool starts with no completed slots");
|
||||||
|
Expect(metrics.scheduledCount == 0, "new pool starts with no scheduled slots");
|
||||||
|
|
||||||
|
OutputFrameSlot slot;
|
||||||
|
Expect(pool.AcquireRenderingSlot(slot), "free slot moves to rendering");
|
||||||
|
metrics = pool.GetMetrics();
|
||||||
|
Expect(metrics.freeCount == 0, "rendering slot leaves free pool");
|
||||||
|
Expect(metrics.renderingCount == 1, "rendering slot is counted");
|
||||||
|
|
||||||
|
Expect(pool.PublishCompletedSlot(slot), "rendering slot moves to completed");
|
||||||
|
metrics = pool.GetMetrics();
|
||||||
|
Expect(metrics.renderingCount == 0, "completed slot leaves rendering");
|
||||||
|
Expect(metrics.completedCount == 1, "completed slot is counted");
|
||||||
|
Expect(metrics.readyCount == 1, "completed slot is available to scheduler");
|
||||||
|
|
||||||
|
OutputFrameSlot completed;
|
||||||
|
Expect(pool.ConsumeCompletedSlot(completed), "completed slot can be dequeued for scheduling");
|
||||||
|
metrics = pool.GetMetrics();
|
||||||
|
Expect(metrics.completedCount == 1, "dequeued completed slot remains completed until scheduled");
|
||||||
|
Expect(metrics.readyCount == 0, "dequeued completed slot leaves ready queue");
|
||||||
|
|
||||||
|
Expect(pool.MarkScheduled(completed), "completed slot moves to scheduled");
|
||||||
|
metrics = pool.GetMetrics();
|
||||||
|
Expect(metrics.completedCount == 0, "scheduled slot leaves completed state");
|
||||||
|
Expect(metrics.scheduledCount == 1, "scheduled slot is counted");
|
||||||
|
|
||||||
|
Expect(pool.ReleaseScheduledSlot(completed), "scheduled slot returns to free");
|
||||||
|
metrics = pool.GetMetrics();
|
||||||
|
Expect(metrics.freeCount == 1, "released scheduled slot returns to free");
|
||||||
|
Expect(metrics.scheduledCount == 0, "released scheduled slot leaves scheduled state");
|
||||||
|
}
|
||||||
|
|
||||||
void TestReadySlotsAreConsumedFifo()
|
void TestReadySlotsAreConsumedFifo()
|
||||||
{
|
{
|
||||||
SystemOutputFramePool pool(MakeConfig(2));
|
SystemOutputFramePool pool(MakeConfig(2));
|
||||||
@@ -78,6 +118,25 @@ void TestReadySlotsAreConsumedFifo()
|
|||||||
Expect(metrics.readyCount == 0, "ready queue is empty after consumption");
|
Expect(metrics.readyCount == 0, "ready queue is empty after consumption");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void TestCompletedSlotCannotBeAcquiredUntilReleased()
|
||||||
|
{
|
||||||
|
SystemOutputFramePool pool(MakeConfig(1));
|
||||||
|
|
||||||
|
OutputFrameSlot slot;
|
||||||
|
OutputFrameSlot extra;
|
||||||
|
Expect(pool.AcquireRenderingSlot(slot), "single slot can be acquired for rendering");
|
||||||
|
Expect(pool.PublishCompletedSlot(slot), "single slot can be published completed");
|
||||||
|
Expect(!pool.AcquireRenderingSlot(extra), "completed slot is not available for rendering");
|
||||||
|
|
||||||
|
OutputFrameSlot completed;
|
||||||
|
Expect(pool.ConsumeCompletedSlot(completed), "completed slot can be dequeued");
|
||||||
|
Expect(!pool.AcquireRenderingSlot(extra), "dequeued completed slot is still not free");
|
||||||
|
Expect(pool.MarkScheduled(completed), "dequeued completed slot can be scheduled");
|
||||||
|
Expect(!pool.AcquireRenderingSlot(extra), "scheduled slot is still not free");
|
||||||
|
Expect(pool.ReleaseScheduledSlot(completed), "scheduled slot can be released");
|
||||||
|
Expect(pool.AcquireRenderingSlot(extra), "released slot can be acquired again");
|
||||||
|
}
|
||||||
|
|
||||||
void TestReadySlotCanBeScheduledByBuffer()
|
void TestReadySlotCanBeScheduledByBuffer()
|
||||||
{
|
{
|
||||||
SystemOutputFramePool pool(MakeConfig(1));
|
SystemOutputFramePool pool(MakeConfig(1));
|
||||||
@@ -153,7 +212,9 @@ void TestEmptyReadyQueueUnderrunIsCounted()
|
|||||||
int main()
|
int main()
|
||||||
{
|
{
|
||||||
TestAcquireHonorsCapacityAndFrameShape();
|
TestAcquireHonorsCapacityAndFrameShape();
|
||||||
|
TestPhase77StateContract();
|
||||||
TestReadySlotsAreConsumedFifo();
|
TestReadySlotsAreConsumedFifo();
|
||||||
|
TestCompletedSlotCannotBeAcquiredUntilReleased();
|
||||||
TestReadySlotCanBeScheduledByBuffer();
|
TestReadySlotCanBeScheduledByBuffer();
|
||||||
TestInvalidTransitionsAreRejected();
|
TestInvalidTransitionsAreRejected();
|
||||||
TestPixelFormatAwareSizing();
|
TestPixelFormatAwareSizing();
|
||||||
|
|||||||
@@ -39,8 +39,11 @@ void TestScheduleAdvancesFromZero()
|
|||||||
|
|
||||||
void TestLateAndDroppedRecoveryUsesMeasuredPressure()
|
void TestLateAndDroppedRecoveryUsesMeasuredPressure()
|
||||||
{
|
{
|
||||||
|
VideoPlayoutPolicy policy;
|
||||||
|
policy.lateOrDropCatchUpFrames = 2;
|
||||||
|
|
||||||
VideoPlayoutScheduler scheduler;
|
VideoPlayoutScheduler scheduler;
|
||||||
scheduler.Configure(1000, 50000);
|
scheduler.Configure(1000, 50000, policy);
|
||||||
|
|
||||||
(void)scheduler.NextScheduleTime();
|
(void)scheduler.NextScheduleTime();
|
||||||
VideoPlayoutRecoveryDecision lateDecision = scheduler.AccountForCompletionResult(VideoIOCompletionResult::DisplayedLate, 2);
|
VideoPlayoutRecoveryDecision lateDecision = scheduler.AccountForCompletionResult(VideoIOCompletionResult::DisplayedLate, 2);
|
||||||
@@ -55,6 +58,18 @@ void TestLateAndDroppedRecoveryUsesMeasuredPressure()
|
|||||||
Expect(scheduler.NextScheduleTime().streamTime == 5000, "drop recovery advances by measured lag");
|
Expect(scheduler.NextScheduleTime().streamTime == 5000, "drop recovery advances by measured lag");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void TestDefaultPolicyReportsLagWithoutSkippingScheduleTime()
|
||||||
|
{
|
||||||
|
VideoPlayoutScheduler scheduler;
|
||||||
|
scheduler.Configure(1000, 50000);
|
||||||
|
|
||||||
|
(void)scheduler.NextScheduleTime();
|
||||||
|
VideoPlayoutRecoveryDecision decision = scheduler.AccountForCompletionResult(VideoIOCompletionResult::Dropped, 0);
|
||||||
|
Expect(decision.measuredLagFrames > 0, "default policy still measures dropped-frame lag");
|
||||||
|
Expect(decision.catchUpFrames == 0, "default policy does not skip schedule time");
|
||||||
|
Expect(scheduler.NextScheduleTime().streamTime == 1000, "default recovery keeps stream time continuous");
|
||||||
|
}
|
||||||
|
|
||||||
void TestMeasuredRecoveryIsCappedByPolicy()
|
void TestMeasuredRecoveryIsCappedByPolicy()
|
||||||
{
|
{
|
||||||
VideoPlayoutPolicy policy;
|
VideoPlayoutPolicy policy;
|
||||||
@@ -117,6 +132,7 @@ int main()
|
|||||||
{
|
{
|
||||||
TestScheduleAdvancesFromZero();
|
TestScheduleAdvancesFromZero();
|
||||||
TestLateAndDroppedRecoveryUsesMeasuredPressure();
|
TestLateAndDroppedRecoveryUsesMeasuredPressure();
|
||||||
|
TestDefaultPolicyReportsLagWithoutSkippingScheduleTime();
|
||||||
TestMeasuredRecoveryIsCappedByPolicy();
|
TestMeasuredRecoveryIsCappedByPolicy();
|
||||||
TestCleanCompletionTracksCompletedIndexAndClearsStreaks();
|
TestCleanCompletionTracksCompletedIndexAndClearsStreaks();
|
||||||
TestPolicyNormalization();
|
TestPolicyNormalization();
|
||||||
|
|||||||
Reference in New Issue
Block a user