timing refactor
All checks were successful
CI / React UI Build (push) Successful in 10s
CI / Native Windows Build And Tests (push) Successful in 3m1s
CI / Windows Release Package (push) Successful in 3m20s

This commit is contained in:
Aiden
2026-05-12 23:39:57 +10:00
parent 4a049a557a
commit d411453f80
17 changed files with 125 additions and 112 deletions

View File

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

View File

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

View File

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

View File

@@ -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`

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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