timing refactor
This commit is contained in:
@@ -32,6 +32,17 @@ VideoIOScheduleTime VideoPlayoutScheduler::NextScheduleTime()
|
|||||||
return time;
|
return time;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void VideoPlayoutScheduler::AlignNextScheduleTimeToPlayback(int64_t streamTime, uint64_t leadFrames)
|
||||||
|
{
|
||||||
|
if (mFrameDuration <= 0 || streamTime < 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
const uint64_t playbackFrameIndex = static_cast<uint64_t>(streamTime / mFrameDuration);
|
||||||
|
const uint64_t minimumScheduleIndex = playbackFrameIndex + leadFrames;
|
||||||
|
if (minimumScheduleIndex > mScheduledFrameIndex)
|
||||||
|
mScheduledFrameIndex = minimumScheduleIndex;
|
||||||
|
}
|
||||||
|
|
||||||
VideoPlayoutRecoveryDecision VideoPlayoutScheduler::AccountForCompletionResult(VideoIOCompletionResult result, uint64_t readyQueueDepth)
|
VideoPlayoutRecoveryDecision VideoPlayoutScheduler::AccountForCompletionResult(VideoIOCompletionResult result, uint64_t readyQueueDepth)
|
||||||
{
|
{
|
||||||
++mCompletedFrameIndex;
|
++mCompletedFrameIndex;
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ public:
|
|||||||
void Configure(int64_t frameDuration, int64_t timeScale, const VideoPlayoutPolicy& policy);
|
void Configure(int64_t frameDuration, int64_t timeScale, const VideoPlayoutPolicy& policy);
|
||||||
void Reset();
|
void Reset();
|
||||||
VideoIOScheduleTime NextScheduleTime();
|
VideoIOScheduleTime NextScheduleTime();
|
||||||
|
void AlignNextScheduleTimeToPlayback(int64_t streamTime, uint64_t leadFrames);
|
||||||
VideoPlayoutRecoveryDecision AccountForCompletionResult(VideoIOCompletionResult result, uint64_t readyQueueDepth = 0);
|
VideoPlayoutRecoveryDecision AccountForCompletionResult(VideoIOCompletionResult result, uint64_t readyQueueDepth = 0);
|
||||||
double FrameBudgetMilliseconds() const;
|
double FrameBudgetMilliseconds() const;
|
||||||
uint64_t ScheduledFrameIndex() const { return mScheduledFrameIndex; }
|
uint64_t ScheduledFrameIndex() const { return mScheduledFrameIndex; }
|
||||||
|
|||||||
@@ -526,13 +526,24 @@ bool DeckLinkSession::PopulateOutputFrame(IDeckLinkMutableVideoFrame* outputVide
|
|||||||
|
|
||||||
bool DeckLinkSession::ScheduleFrame(IDeckLinkMutableVideoFrame* outputVideoFrame)
|
bool DeckLinkSession::ScheduleFrame(IDeckLinkMutableVideoFrame* outputVideoFrame)
|
||||||
{
|
{
|
||||||
const VideoIOScheduleTime scheduleTime = mScheduler.NextScheduleTime();
|
|
||||||
if (outputVideoFrame == nullptr || output == nullptr)
|
if (outputVideoFrame == nullptr || output == nullptr)
|
||||||
{
|
{
|
||||||
++mState.deckLinkScheduleFailureCount;
|
++mState.deckLinkScheduleFailureCount;
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
BMDTimeValue streamTime = 0;
|
||||||
|
double playbackSpeed = 0.0;
|
||||||
|
if (output->GetScheduledStreamTime(mScheduler.TimeScale(), &streamTime, &playbackSpeed) == S_OK && playbackSpeed > 0.0)
|
||||||
|
{
|
||||||
|
RefreshBufferedVideoFrameCount();
|
||||||
|
const uint64_t leadFrames = mState.actualDeckLinkBufferedFramesAvailable
|
||||||
|
? static_cast<uint64_t>(mState.actualDeckLinkBufferedFrames) + 1
|
||||||
|
: static_cast<uint64_t>(mPlayoutPolicy.targetPrerollFrames);
|
||||||
|
mScheduler.AlignNextScheduleTimeToPlayback(streamTime, leadFrames);
|
||||||
|
}
|
||||||
|
|
||||||
|
const VideoIOScheduleTime scheduleTime = mScheduler.NextScheduleTime();
|
||||||
const auto scheduleStart = std::chrono::steady_clock::now();
|
const auto scheduleStart = std::chrono::steady_clock::now();
|
||||||
const HRESULT result = output->ScheduleVideoFrame(outputVideoFrame, scheduleTime.streamTime, scheduleTime.duration, scheduleTime.timeScale);
|
const HRESULT result = output->ScheduleVideoFrame(outputVideoFrame, scheduleTime.streamTime, scheduleTime.duration, scheduleTime.timeScale);
|
||||||
const auto scheduleEnd = std::chrono::steady_clock::now();
|
const auto scheduleEnd = std::chrono::steady_clock::now();
|
||||||
|
|||||||
@@ -11,14 +11,14 @@ Before adding features here, read the guardrails in [Render Cadence Golden Rules
|
|||||||
```text
|
```text
|
||||||
RenderThread
|
RenderThread
|
||||||
owns a hidden OpenGL context
|
owns a hidden OpenGL context
|
||||||
polls latest input frames without waiting
|
polls the oldest ready input frame without waiting
|
||||||
uploads input frames into a render-owned GL texture
|
uploads input frames into a render-owned GL texture
|
||||||
renders simple BGRA8 motion at selected cadence
|
renders simple BGRA8 motion at selected cadence
|
||||||
queues async PBO readback
|
queues async PBO readback
|
||||||
publishes completed frames into SystemFrameExchange
|
publishes completed frames into SystemFrameExchange
|
||||||
|
|
||||||
InputFrameMailbox
|
InputFrameMailbox
|
||||||
owns latest disposable CPU input slots
|
owns bounded FIFO CPU input slots
|
||||||
keeps a bounded three-ready-frame input buffer for render
|
keeps a bounded three-ready-frame input buffer for render
|
||||||
trims frames beyond that bound to avoid runaway input latency
|
trims frames beyond that bound to avoid runaway input latency
|
||||||
protects the one frame currently being uploaded by render
|
protects the one frame currently being uploaded by render
|
||||||
@@ -26,7 +26,7 @@ InputFrameMailbox
|
|||||||
|
|
||||||
SystemFrameExchange
|
SystemFrameExchange
|
||||||
owns Free / Rendering / Completed / Scheduled slots
|
owns Free / Rendering / Completed / Scheduled slots
|
||||||
drops old completed unscheduled frames when render needs space
|
preserves completed output frames once they are waiting for playout
|
||||||
protects scheduled frames until DeckLink completion
|
protects scheduled frames until DeckLink completion
|
||||||
|
|
||||||
DeckLinkOutputThread
|
DeckLinkOutputThread
|
||||||
@@ -286,8 +286,8 @@ Input telemetry:
|
|||||||
- `renderFrameMaxMs`: maximum observed render-thread draw duration for this process
|
- `renderFrameMaxMs`: maximum observed render-thread draw duration for this process
|
||||||
- `readbackQueueMs`: time spent queueing the most recent async BGRA8 PBO readback
|
- `readbackQueueMs`: time spent queueing the most recent async BGRA8 PBO readback
|
||||||
- `completedReadbackCopyMs`: time spent mapping/copying the most recent completed readback into system-memory frame storage
|
- `completedReadbackCopyMs`: time spent mapping/copying the most recent completed readback into system-memory frame storage
|
||||||
- `completedDrops`: completed unscheduled system-memory frames dropped so render could reuse the slot
|
- `completedDrops`: completed unscheduled system-memory frames dropped by latest-N acquire paths; expected to stay flat in the cadence compositor output path
|
||||||
- `acquireMisses`: times render/readback could not acquire a writable system-memory frame slot
|
- `acquireMisses`: times render/readback could not acquire a writable system-memory frame slot; completed frames waiting for playout are preserved instead of being displaced
|
||||||
- `inputConsumeMisses`: render ticks where no ready input frame was available to upload
|
- `inputConsumeMisses`: render ticks where no ready input frame was available to upload
|
||||||
- `inputUploadMisses`: input texture upload attempts that reused the previous GL input texture
|
- `inputUploadMisses`: input texture upload attempts that reused the previous GL input texture
|
||||||
- `inputReadyFrames`: ready input frames currently queued in `InputFrameMailbox`
|
- `inputReadyFrames`: ready input frames currently queued in `InputFrameMailbox`
|
||||||
|
|||||||
@@ -111,14 +111,23 @@ int main(int argc, char** argv)
|
|||||||
InputFrameMailbox inputMailbox(inputMailboxConfig);
|
InputFrameMailbox inputMailbox(inputMailboxConfig);
|
||||||
|
|
||||||
VideoFormat inputVideoMode;
|
VideoFormat inputVideoMode;
|
||||||
|
VideoFormat outputVideoMode;
|
||||||
std::string inputVideoModeError;
|
std::string inputVideoModeError;
|
||||||
const bool inputVideoModeResolved = ResolveConfiguredVideoFormat(appConfig.inputVideoFormat, appConfig.inputFrameRate, inputVideoMode);
|
const bool inputVideoModeResolved = ResolveConfiguredVideoFormat(appConfig.inputVideoFormat, appConfig.inputFrameRate, inputVideoMode);
|
||||||
|
const bool outputVideoModeResolved = ResolveConfiguredVideoFormat(appConfig.outputVideoFormat, appConfig.outputFrameRate, outputVideoMode);
|
||||||
if (!inputVideoModeResolved)
|
if (!inputVideoModeResolved)
|
||||||
{
|
{
|
||||||
inputVideoModeError = "Unsupported DeckLink inputVideoFormat/inputFrameRate in config/runtime-host.json: " +
|
inputVideoModeError = "Unsupported DeckLink inputVideoFormat/inputFrameRate in config/runtime-host.json: " +
|
||||||
appConfig.inputVideoFormat + " / " + appConfig.inputFrameRate;
|
appConfig.inputVideoFormat + " / " + appConfig.inputFrameRate;
|
||||||
RenderCadenceCompositor::LogWarning("app", inputVideoModeError);
|
RenderCadenceCompositor::LogWarning("app", inputVideoModeError);
|
||||||
}
|
}
|
||||||
|
if (!outputVideoModeResolved)
|
||||||
|
{
|
||||||
|
RenderCadenceCompositor::LogWarning(
|
||||||
|
"app",
|
||||||
|
"Unsupported DeckLink outputVideoFormat/outputFrameRate in config/runtime-host.json; render cadence will use parsed frame-rate fallback: " +
|
||||||
|
appConfig.outputVideoFormat + " / " + appConfig.outputFrameRate);
|
||||||
|
}
|
||||||
|
|
||||||
RenderCadenceCompositor::DeckLinkInput deckLinkInput(inputMailbox);
|
RenderCadenceCompositor::DeckLinkInput deckLinkInput(inputMailbox);
|
||||||
RenderCadenceCompositor::DeckLinkInputThread deckLinkInputThread(deckLinkInput);
|
RenderCadenceCompositor::DeckLinkInputThread deckLinkInputThread(deckLinkInput);
|
||||||
@@ -173,7 +182,10 @@ int main(int argc, char** argv)
|
|||||||
RenderThread::Config renderConfig;
|
RenderThread::Config renderConfig;
|
||||||
renderConfig.width = frameExchangeConfig.width;
|
renderConfig.width = frameExchangeConfig.width;
|
||||||
renderConfig.height = frameExchangeConfig.height;
|
renderConfig.height = frameExchangeConfig.height;
|
||||||
renderConfig.frameDurationMilliseconds = RenderCadenceCompositor::FrameDurationMillisecondsFromRateString(appConfig.outputFrameRate);
|
const double fallbackFrameDurationMilliseconds = RenderCadenceCompositor::FrameDurationMillisecondsFromRateString(appConfig.outputFrameRate);
|
||||||
|
renderConfig.frameDurationMilliseconds = outputVideoModeResolved
|
||||||
|
? RenderCadenceCompositor::FrameDurationMillisecondsFromDisplayMode(outputVideoMode.displayMode, fallbackFrameDurationMilliseconds)
|
||||||
|
: fallbackFrameDurationMilliseconds;
|
||||||
renderConfig.pboDepth = 6;
|
renderConfig.pboDepth = 6;
|
||||||
|
|
||||||
RenderThread renderThread(frameExchange, &inputMailbox, renderConfig);
|
RenderThread renderThread(frameExchange, &inputMailbox, renderConfig);
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <cctype>
|
#include <cctype>
|
||||||
|
#include <cstdint>
|
||||||
#include <cstdlib>
|
#include <cstdlib>
|
||||||
#include <fstream>
|
#include <fstream>
|
||||||
#include <sstream>
|
#include <sstream>
|
||||||
@@ -184,6 +185,50 @@ double FrameDurationMillisecondsFromRateString(const std::string& rateText, doub
|
|||||||
return 1000.0 / rate;
|
return 1000.0 / rate;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
double FrameDurationMillisecondsFromDisplayMode(BMDDisplayMode displayMode, double fallbackMilliseconds)
|
||||||
|
{
|
||||||
|
struct ModeRate
|
||||||
|
{
|
||||||
|
BMDDisplayMode mode;
|
||||||
|
int64_t frameDuration;
|
||||||
|
int64_t timeScale;
|
||||||
|
};
|
||||||
|
|
||||||
|
static const ModeRate rates[] =
|
||||||
|
{
|
||||||
|
{ bmdModeHD720p50, 1, 50 },
|
||||||
|
{ bmdModeHD720p5994, 1001, 60000 },
|
||||||
|
{ bmdModeHD720p60, 1, 60 },
|
||||||
|
{ bmdModeHD1080i50, 1, 25 },
|
||||||
|
{ bmdModeHD1080i5994, 1001, 30000 },
|
||||||
|
{ bmdModeHD1080i6000, 1, 30 },
|
||||||
|
{ bmdModeHD1080p2398, 1001, 24000 },
|
||||||
|
{ bmdModeHD1080p24, 1, 24 },
|
||||||
|
{ bmdModeHD1080p25, 1, 25 },
|
||||||
|
{ bmdModeHD1080p2997, 1001, 30000 },
|
||||||
|
{ bmdModeHD1080p30, 1, 30 },
|
||||||
|
{ bmdModeHD1080p50, 1, 50 },
|
||||||
|
{ bmdModeHD1080p5994, 1001, 60000 },
|
||||||
|
{ bmdModeHD1080p6000, 1, 60 },
|
||||||
|
{ bmdMode4K2160p2398, 1001, 24000 },
|
||||||
|
{ bmdMode4K2160p24, 1, 24 },
|
||||||
|
{ bmdMode4K2160p25, 1, 25 },
|
||||||
|
{ bmdMode4K2160p2997, 1001, 30000 },
|
||||||
|
{ bmdMode4K2160p30, 1, 30 },
|
||||||
|
{ bmdMode4K2160p50, 1, 50 },
|
||||||
|
{ bmdMode4K2160p5994, 1001, 60000 },
|
||||||
|
{ bmdMode4K2160p60, 1, 60 }
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const ModeRate& rate : rates)
|
||||||
|
{
|
||||||
|
if (rate.mode == displayMode && rate.timeScale > 0)
|
||||||
|
return (static_cast<double>(rate.frameDuration) * 1000.0) / static_cast<double>(rate.timeScale);
|
||||||
|
}
|
||||||
|
|
||||||
|
return fallbackMilliseconds;
|
||||||
|
}
|
||||||
|
|
||||||
void VideoFormatDimensions(const std::string& formatName, unsigned& width, unsigned& height)
|
void VideoFormatDimensions(const std::string& formatName, unsigned& width, unsigned& height)
|
||||||
{
|
{
|
||||||
std::string normalized = formatName;
|
std::string normalized = formatName;
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include "AppConfig.h"
|
#include "AppConfig.h"
|
||||||
|
#include "DeckLinkDisplayMode.h"
|
||||||
|
|
||||||
#include <filesystem>
|
#include <filesystem>
|
||||||
#include <string>
|
#include <string>
|
||||||
@@ -27,6 +28,7 @@ private:
|
|||||||
};
|
};
|
||||||
|
|
||||||
double FrameDurationMillisecondsFromRateString(const std::string& rateText, double fallbackRate = 59.94);
|
double FrameDurationMillisecondsFromRateString(const std::string& rateText, double fallbackRate = 59.94);
|
||||||
|
double FrameDurationMillisecondsFromDisplayMode(BMDDisplayMode displayMode, double fallbackMilliseconds);
|
||||||
void VideoFormatDimensions(const std::string& formatName, unsigned& width, unsigned& height);
|
void VideoFormatDimensions(const std::string& formatName, unsigned& width, unsigned& height);
|
||||||
std::filesystem::path FindConfigFile(const std::filesystem::path& relativePath = "config/runtime-host.json");
|
std::filesystem::path FindConfigFile(const std::filesystem::path& relativePath = "config/runtime-host.json");
|
||||||
std::filesystem::path FindRepoPath(const std::filesystem::path& relativePath);
|
std::filesystem::path FindRepoPath(const std::filesystem::path& relativePath);
|
||||||
|
|||||||
@@ -120,38 +120,6 @@ bool InputFrameMailbox::SubmitFrame(const void* bytes, unsigned rowBytes, uint64
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool InputFrameMailbox::TryAcquireLatest(InputFrame& frame)
|
|
||||||
{
|
|
||||||
std::lock_guard<std::mutex> lock(mMutex);
|
|
||||||
while (!mReadyIndices.empty())
|
|
||||||
{
|
|
||||||
const std::size_t index = mReadyIndices.back();
|
|
||||||
mReadyIndices.pop_back();
|
|
||||||
if (index >= mSlots.size() || mSlots[index].state != InputFrameSlotState::Ready)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
while (!mReadyIndices.empty())
|
|
||||||
{
|
|
||||||
const std::size_t olderIndex = mReadyIndices.front();
|
|
||||||
mReadyIndices.pop_front();
|
|
||||||
if (olderIndex >= mSlots.size() || mSlots[olderIndex].state != InputFrameSlotState::Ready)
|
|
||||||
continue;
|
|
||||||
mSlots[olderIndex].state = InputFrameSlotState::Free;
|
|
||||||
++mSlots[olderIndex].generation;
|
|
||||||
++mCounters.droppedReadyFrames;
|
|
||||||
}
|
|
||||||
|
|
||||||
mSlots[index].state = InputFrameSlotState::Reading;
|
|
||||||
FillFrameLocked(index, frame);
|
|
||||||
++mCounters.consumedFrames;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
frame = InputFrame();
|
|
||||||
++mCounters.consumeMisses;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool InputFrameMailbox::TryAcquireOldest(InputFrame& frame)
|
bool InputFrameMailbox::TryAcquireOldest(InputFrame& frame)
|
||||||
{
|
{
|
||||||
std::lock_guard<std::mutex> lock(mMutex);
|
std::lock_guard<std::mutex> lock(mMutex);
|
||||||
|
|||||||
@@ -64,7 +64,6 @@ public:
|
|||||||
InputFrameMailboxConfig Config() const;
|
InputFrameMailboxConfig Config() const;
|
||||||
|
|
||||||
bool SubmitFrame(const void* bytes, unsigned rowBytes, uint64_t frameIndex);
|
bool SubmitFrame(const void* bytes, unsigned rowBytes, uint64_t frameIndex);
|
||||||
bool TryAcquireLatest(InputFrame& frame);
|
|
||||||
bool TryAcquireOldest(InputFrame& frame);
|
bool TryAcquireOldest(InputFrame& frame);
|
||||||
bool Release(const InputFrame& frame);
|
bool Release(const InputFrame& frame);
|
||||||
void Clear();
|
void Clear();
|
||||||
|
|||||||
@@ -47,12 +47,9 @@ bool SystemFrameExchange::AcquireForRender(SystemFrame& frame)
|
|||||||
std::lock_guard<std::mutex> lock(mMutex);
|
std::lock_guard<std::mutex> lock(mMutex);
|
||||||
if (!AcquireFreeLocked(frame))
|
if (!AcquireFreeLocked(frame))
|
||||||
{
|
{
|
||||||
if (!DropOldestCompletedLocked() || !AcquireFreeLocked(frame))
|
frame = SystemFrame();
|
||||||
{
|
++mCounters.acquireMisses;
|
||||||
frame = SystemFrame();
|
return false;
|
||||||
++mCounters.acquireMisses;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
++mCounters.acquiredFrames;
|
++mCounters.acquiredFrames;
|
||||||
@@ -234,27 +231,6 @@ bool SystemFrameExchange::AcquireFreeLocked(SystemFrame& frame)
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool SystemFrameExchange::DropOldestCompletedLocked()
|
|
||||||
{
|
|
||||||
while (!mCompletedIndices.empty())
|
|
||||||
{
|
|
||||||
const std::size_t index = mCompletedIndices.front();
|
|
||||||
mCompletedIndices.pop_front();
|
|
||||||
if (index >= mSlots.size() || mSlots[index].state != SystemFrameSlotState::Completed)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
Slot& slot = mSlots[index];
|
|
||||||
slot.state = SystemFrameSlotState::Free;
|
|
||||||
slot.frameIndex = 0;
|
|
||||||
++slot.generation;
|
|
||||||
++mCounters.completedDrops;
|
|
||||||
mCondition.notify_all();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool SystemFrameExchange::IsValidLocked(const SystemFrame& frame) const
|
bool SystemFrameExchange::IsValidLocked(const SystemFrame& frame) const
|
||||||
{
|
{
|
||||||
return frame.index < mSlots.size() && mSlots[frame.index].generation == frame.generation;
|
return frame.index < mSlots.size() && mSlots[frame.index].generation == frame.generation;
|
||||||
|
|||||||
@@ -40,7 +40,6 @@ private:
|
|||||||
};
|
};
|
||||||
|
|
||||||
bool AcquireFreeLocked(SystemFrame& frame);
|
bool AcquireFreeLocked(SystemFrame& frame);
|
||||||
bool DropOldestCompletedLocked();
|
|
||||||
bool IsValidLocked(const SystemFrame& frame) const;
|
bool IsValidLocked(const SystemFrame& frame) const;
|
||||||
void FillFrameLocked(std::size_t index, SystemFrame& frame);
|
void FillFrameLocked(std::size_t index, SystemFrame& frame);
|
||||||
std::size_t CompletedCountLocked() const;
|
std::size_t CompletedCountLocked() const;
|
||||||
|
|||||||
@@ -65,7 +65,7 @@ void RuntimeRenderScene::RenderFrame(uint64_t frameIndex, unsigned width, unsign
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Shader source contract:
|
// Shader source contract:
|
||||||
// - gVideoInput is the decoded latest input texture for every layer in the stack.
|
// - gVideoInput is the decoded current input texture for every layer in the stack.
|
||||||
// - gLayerInput starts as gVideoInput for the first layer, then becomes the previous layer output.
|
// - gLayerInput starts as gVideoInput for the first layer, then becomes the previous layer output.
|
||||||
GLuint layerInputTexture = videoInputTexture;
|
GLuint layerInputTexture = videoInputTexture;
|
||||||
std::size_t nextTargetIndex = 0;
|
std::size_t nextTargetIndex = 0;
|
||||||
|
|||||||
@@ -82,7 +82,6 @@ private:
|
|||||||
std::this_thread::sleep_for(mConfig.idleSleep);
|
std::this_thread::sleep_for(mConfig.idleSleep);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
SystemFrame frame;
|
SystemFrame frame;
|
||||||
if (!mExchange.ConsumeCompletedForSchedule(frame))
|
if (!mExchange.ConsumeCompletedForSchedule(frame))
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -107,6 +107,8 @@ void TestHelpers()
|
|||||||
|
|
||||||
const double duration = FrameDurationMillisecondsFromRateString("50");
|
const double duration = FrameDurationMillisecondsFromRateString("50");
|
||||||
Expect(duration > 19.9 && duration < 20.1, "frame duration parses numeric rate");
|
Expect(duration > 19.9 && duration < 20.1, "frame duration parses numeric rate");
|
||||||
|
const double deckLinkDuration = FrameDurationMillisecondsFromDisplayMode(bmdModeHD1080p5994, 0.0);
|
||||||
|
Expect(deckLinkDuration > 16.6833 && deckLinkDuration < 16.6834, "DeckLink 59.94 display mode duration is exact");
|
||||||
|
|
||||||
const std::filesystem::path configPath = FindConfigFile();
|
const std::filesystem::path configPath = FindConfigFile();
|
||||||
Expect(!configPath.empty(), "default config is discoverable from test working directory");
|
Expect(!configPath.empty(), "default config is discoverable from test working directory");
|
||||||
|
|||||||
@@ -57,32 +57,29 @@ void TestAcquirePublishesAndSchedules()
|
|||||||
Expect(metrics.scheduledFrames == 1, "scheduled metric is counted");
|
Expect(metrics.scheduledFrames == 1, "scheduled metric is counted");
|
||||||
}
|
}
|
||||||
|
|
||||||
void TestAcquireDropsOldestCompletedUnscheduled()
|
void TestAcquirePreservesCompletedFrames()
|
||||||
{
|
{
|
||||||
SystemFrameExchange exchange(MakeConfig(2));
|
SystemFrameExchange exchange(MakeConfig(2));
|
||||||
|
|
||||||
SystemFrame first;
|
SystemFrame first;
|
||||||
SystemFrame second;
|
SystemFrame second;
|
||||||
SystemFrame third;
|
SystemFrame third;
|
||||||
Expect(exchange.AcquireForRender(first), "first frame can be acquired");
|
Expect(exchange.AcquireForRender(first), "first preserving frame can be acquired");
|
||||||
first.frameIndex = 1;
|
first.frameIndex = 1;
|
||||||
Expect(exchange.PublishCompleted(first), "first frame can be completed");
|
Expect(exchange.PublishCompleted(first), "first preserving frame can be completed");
|
||||||
Expect(exchange.AcquireForRender(second), "second frame can be acquired");
|
Expect(exchange.AcquireForRender(second), "second preserving frame can be acquired");
|
||||||
second.frameIndex = 2;
|
second.frameIndex = 2;
|
||||||
Expect(exchange.PublishCompleted(second), "second frame can be completed");
|
Expect(exchange.PublishCompleted(second), "second preserving frame can be completed");
|
||||||
|
|
||||||
Expect(exchange.AcquireForRender(third), "third acquire drops the oldest completed frame");
|
Expect(!exchange.AcquireForRender(third), "render acquire refuses to drop completed frames");
|
||||||
Expect(third.index == first.index, "oldest completed slot is reused");
|
|
||||||
|
|
||||||
SystemFrame scheduled;
|
SystemFrame scheduled;
|
||||||
Expect(exchange.ConsumeCompletedForSchedule(scheduled), "remaining completed frame can be scheduled");
|
Expect(exchange.ConsumeCompletedForSchedule(scheduled), "oldest completed frame survives acquire miss");
|
||||||
Expect(scheduled.index == second.index, "newer completed frame survives drop");
|
Expect(scheduled.frameIndex == 1, "preserving acquire keeps FIFO output continuity");
|
||||||
Expect(scheduled.frameIndex == 2, "newer frame index survives drop");
|
|
||||||
|
|
||||||
SystemFrameExchangeMetrics metrics = exchange.Metrics();
|
SystemFrameExchangeMetrics metrics = exchange.Metrics();
|
||||||
Expect(metrics.completedDrops == 1, "drop metric is counted");
|
Expect(metrics.completedDrops == 0, "preserving acquire does not count completed drops");
|
||||||
Expect(metrics.renderingCount == 1, "reused slot is rendering");
|
Expect(metrics.acquireMisses == 1, "preserving acquire miss is counted");
|
||||||
Expect(metrics.scheduledCount == 1, "consumed slot is scheduled");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void TestScheduledFramesAreNotDropped()
|
void TestScheduledFramesAreNotDropped()
|
||||||
@@ -179,7 +176,7 @@ void TestStableCompletedDepthTimesOut()
|
|||||||
int main()
|
int main()
|
||||||
{
|
{
|
||||||
TestAcquirePublishesAndSchedules();
|
TestAcquirePublishesAndSchedules();
|
||||||
TestAcquireDropsOldestCompletedUnscheduled();
|
TestAcquirePreservesCompletedFrames();
|
||||||
TestScheduledFramesAreNotDropped();
|
TestScheduledFramesAreNotDropped();
|
||||||
TestGenerationValidationRejectsStaleFrames();
|
TestGenerationValidationRejectsStaleFrames();
|
||||||
TestPixelFormatAwareSizing();
|
TestPixelFormatAwareSizing();
|
||||||
|
|||||||
@@ -41,28 +41,6 @@ std::vector<unsigned char> MakeFrame(unsigned char value)
|
|||||||
return std::vector<unsigned char>(16, value);
|
return std::vector<unsigned char>(16, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
void TestAcquireLatestDropsOlderReadyFrames()
|
|
||||||
{
|
|
||||||
InputFrameMailbox mailbox(MakeConfig(3));
|
|
||||||
const std::vector<unsigned char> frame1 = MakeFrame(1);
|
|
||||||
const std::vector<unsigned char> frame2 = MakeFrame(2);
|
|
||||||
const std::vector<unsigned char> frame3 = MakeFrame(3);
|
|
||||||
|
|
||||||
Expect(mailbox.SubmitFrame(frame1.data(), 8, 1), "first input frame submits");
|
|
||||||
Expect(mailbox.SubmitFrame(frame2.data(), 8, 2), "second input frame submits");
|
|
||||||
Expect(mailbox.SubmitFrame(frame3.data(), 8, 3), "third input frame submits");
|
|
||||||
|
|
||||||
InputFrame latest;
|
|
||||||
Expect(mailbox.TryAcquireLatest(latest), "latest input frame can be acquired");
|
|
||||||
Expect(latest.frameIndex == 3, "mailbox returns newest frame");
|
|
||||||
Expect(latest.bytes != nullptr && static_cast<const unsigned char*>(latest.bytes)[0] == 3, "latest frame bytes match newest frame");
|
|
||||||
Expect(mailbox.Release(latest), "latest input frame can be released");
|
|
||||||
|
|
||||||
const InputFrameMailboxMetrics metrics = mailbox.Metrics();
|
|
||||||
Expect(metrics.droppedReadyFrames == 2, "older ready input frames are dropped after latest acquire");
|
|
||||||
Expect(metrics.freeCount == 3, "all slots are free after release");
|
|
||||||
}
|
|
||||||
|
|
||||||
void TestSubmitDropsOldestWhenFull()
|
void TestSubmitDropsOldestWhenFull()
|
||||||
{
|
{
|
||||||
InputFrameMailbox mailbox(MakeConfig(2));
|
InputFrameMailbox mailbox(MakeConfig(2));
|
||||||
@@ -74,10 +52,10 @@ void TestSubmitDropsOldestWhenFull()
|
|||||||
Expect(mailbox.SubmitFrame(frame2.data(), 8, 2), "second frame submits into full test");
|
Expect(mailbox.SubmitFrame(frame2.data(), 8, 2), "second frame submits into full test");
|
||||||
Expect(mailbox.SubmitFrame(frame3.data(), 8, 3), "third frame submits by dropping oldest ready frame");
|
Expect(mailbox.SubmitFrame(frame3.data(), 8, 3), "third frame submits by dropping oldest ready frame");
|
||||||
|
|
||||||
InputFrame latest;
|
InputFrame oldest;
|
||||||
Expect(mailbox.TryAcquireLatest(latest), "latest frame available after drop");
|
Expect(mailbox.TryAcquireOldest(oldest), "oldest frame available after drop");
|
||||||
Expect(latest.frameIndex == 3, "newest frame survived full mailbox");
|
Expect(oldest.frameIndex == 2, "bounded mailbox keeps FIFO order after trimming oldest overflow");
|
||||||
Expect(mailbox.Release(latest), "newest frame releases");
|
Expect(mailbox.Release(oldest), "oldest frame releases");
|
||||||
|
|
||||||
const InputFrameMailboxMetrics metrics = mailbox.Metrics();
|
const InputFrameMailboxMetrics metrics = mailbox.Metrics();
|
||||||
Expect(metrics.droppedReadyFrames >= 1, "full mailbox records ready drop");
|
Expect(metrics.droppedReadyFrames >= 1, "full mailbox records ready drop");
|
||||||
@@ -92,7 +70,7 @@ void TestReadingFrameIsProtected()
|
|||||||
|
|
||||||
Expect(mailbox.SubmitFrame(frame1.data(), 8, 1), "protected frame submits");
|
Expect(mailbox.SubmitFrame(frame1.data(), 8, 1), "protected frame submits");
|
||||||
InputFrame acquired;
|
InputFrame acquired;
|
||||||
Expect(mailbox.TryAcquireLatest(acquired), "protected frame acquired");
|
Expect(mailbox.TryAcquireOldest(acquired), "protected frame acquired");
|
||||||
Expect(!mailbox.SubmitFrame(frame2.data(), 8, 2), "producer cannot overwrite frame currently being read");
|
Expect(!mailbox.SubmitFrame(frame2.data(), 8, 2), "producer cannot overwrite frame currently being read");
|
||||||
Expect(mailbox.Release(acquired), "protected frame releases");
|
Expect(mailbox.Release(acquired), "protected frame releases");
|
||||||
Expect(mailbox.SubmitFrame(frame2.data(), 8, 2), "producer can submit after release");
|
Expect(mailbox.SubmitFrame(frame2.data(), 8, 2), "producer can submit after release");
|
||||||
@@ -143,7 +121,6 @@ void TestMaxReadyFramesKeepsConfiguredInputBuffer()
|
|||||||
|
|
||||||
int main()
|
int main()
|
||||||
{
|
{
|
||||||
TestAcquireLatestDropsOlderReadyFrames();
|
|
||||||
TestSubmitDropsOldestWhenFull();
|
TestSubmitDropsOldestWhenFull();
|
||||||
TestReadingFrameIsProtected();
|
TestReadingFrameIsProtected();
|
||||||
TestAcquireOldestConsumesFifoWithoutDroppingReadyFrames();
|
TestAcquireOldestConsumesFifoWithoutDroppingReadyFrames();
|
||||||
|
|||||||
@@ -70,6 +70,19 @@ void TestDefaultPolicyReportsLagWithoutSkippingScheduleTime()
|
|||||||
Expect(scheduler.NextScheduleTime().streamTime == 1000, "default recovery keeps stream time continuous");
|
Expect(scheduler.NextScheduleTime().streamTime == 1000, "default recovery keeps stream time continuous");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void TestScheduleCursorCanAlignToPlaybackClock()
|
||||||
|
{
|
||||||
|
VideoPlayoutScheduler scheduler;
|
||||||
|
scheduler.Configure(1000, 50000);
|
||||||
|
|
||||||
|
(void)scheduler.NextScheduleTime();
|
||||||
|
scheduler.AlignNextScheduleTimeToPlayback(10000, 4);
|
||||||
|
Expect(scheduler.NextScheduleTime().streamTime == 14000, "schedule cursor skips stale stream time after underfeed");
|
||||||
|
|
||||||
|
scheduler.AlignNextScheduleTimeToPlayback(11000, 1);
|
||||||
|
Expect(scheduler.NextScheduleTime().streamTime == 15000, "schedule cursor does not move backward");
|
||||||
|
}
|
||||||
|
|
||||||
void TestMeasuredRecoveryIsCappedByPolicy()
|
void TestMeasuredRecoveryIsCappedByPolicy()
|
||||||
{
|
{
|
||||||
VideoPlayoutPolicy policy;
|
VideoPlayoutPolicy policy;
|
||||||
@@ -133,6 +146,7 @@ int main()
|
|||||||
TestScheduleAdvancesFromZero();
|
TestScheduleAdvancesFromZero();
|
||||||
TestLateAndDroppedRecoveryUsesMeasuredPressure();
|
TestLateAndDroppedRecoveryUsesMeasuredPressure();
|
||||||
TestDefaultPolicyReportsLagWithoutSkippingScheduleTime();
|
TestDefaultPolicyReportsLagWithoutSkippingScheduleTime();
|
||||||
|
TestScheduleCursorCanAlignToPlaybackClock();
|
||||||
TestMeasuredRecoveryIsCappedByPolicy();
|
TestMeasuredRecoveryIsCappedByPolicy();
|
||||||
TestCleanCompletionTracksCompletedIndexAndClearsStreaks();
|
TestCleanCompletionTracksCompletedIndexAndClearsStreaks();
|
||||||
TestPolicyNormalization();
|
TestPolicyNormalization();
|
||||||
|
|||||||
Reference in New Issue
Block a user