diff --git a/apps/LoopThroughWithOpenGLCompositing/videoio/VideoPlayoutScheduler.cpp b/apps/LoopThroughWithOpenGLCompositing/videoio/VideoPlayoutScheduler.cpp index c0480b3..750e301 100644 --- a/apps/LoopThroughWithOpenGLCompositing/videoio/VideoPlayoutScheduler.cpp +++ b/apps/LoopThroughWithOpenGLCompositing/videoio/VideoPlayoutScheduler.cpp @@ -32,6 +32,17 @@ VideoIOScheduleTime VideoPlayoutScheduler::NextScheduleTime() return time; } +void VideoPlayoutScheduler::AlignNextScheduleTimeToPlayback(int64_t streamTime, uint64_t leadFrames) +{ + if (mFrameDuration <= 0 || streamTime < 0) + return; + + const uint64_t playbackFrameIndex = static_cast(streamTime / mFrameDuration); + const uint64_t minimumScheduleIndex = playbackFrameIndex + leadFrames; + if (minimumScheduleIndex > mScheduledFrameIndex) + mScheduledFrameIndex = minimumScheduleIndex; +} + VideoPlayoutRecoveryDecision VideoPlayoutScheduler::AccountForCompletionResult(VideoIOCompletionResult result, uint64_t readyQueueDepth) { ++mCompletedFrameIndex; diff --git a/apps/LoopThroughWithOpenGLCompositing/videoio/VideoPlayoutScheduler.h b/apps/LoopThroughWithOpenGLCompositing/videoio/VideoPlayoutScheduler.h index d55aa1d..8635899 100644 --- a/apps/LoopThroughWithOpenGLCompositing/videoio/VideoPlayoutScheduler.h +++ b/apps/LoopThroughWithOpenGLCompositing/videoio/VideoPlayoutScheduler.h @@ -12,6 +12,7 @@ public: void Configure(int64_t frameDuration, int64_t timeScale, const VideoPlayoutPolicy& policy); void Reset(); VideoIOScheduleTime NextScheduleTime(); + void AlignNextScheduleTimeToPlayback(int64_t streamTime, uint64_t leadFrames); VideoPlayoutRecoveryDecision AccountForCompletionResult(VideoIOCompletionResult result, uint64_t readyQueueDepth = 0); double FrameBudgetMilliseconds() const; uint64_t ScheduledFrameIndex() const { return mScheduledFrameIndex; } diff --git a/apps/LoopThroughWithOpenGLCompositing/videoio/decklink/DeckLinkSession.cpp b/apps/LoopThroughWithOpenGLCompositing/videoio/decklink/DeckLinkSession.cpp index 047c359..dd5c446 100644 --- a/apps/LoopThroughWithOpenGLCompositing/videoio/decklink/DeckLinkSession.cpp +++ b/apps/LoopThroughWithOpenGLCompositing/videoio/decklink/DeckLinkSession.cpp @@ -526,13 +526,24 @@ bool DeckLinkSession::PopulateOutputFrame(IDeckLinkMutableVideoFrame* outputVide bool DeckLinkSession::ScheduleFrame(IDeckLinkMutableVideoFrame* outputVideoFrame) { - const VideoIOScheduleTime scheduleTime = mScheduler.NextScheduleTime(); if (outputVideoFrame == nullptr || output == nullptr) { ++mState.deckLinkScheduleFailureCount; 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(mState.actualDeckLinkBufferedFrames) + 1 + : static_cast(mPlayoutPolicy.targetPrerollFrames); + mScheduler.AlignNextScheduleTimeToPlayback(streamTime, leadFrames); + } + + const VideoIOScheduleTime scheduleTime = mScheduler.NextScheduleTime(); 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(); diff --git a/apps/RenderCadenceCompositor/README.md b/apps/RenderCadenceCompositor/README.md index 62cb559..ff1a16e 100644 --- a/apps/RenderCadenceCompositor/README.md +++ b/apps/RenderCadenceCompositor/README.md @@ -11,14 +11,14 @@ Before adding features here, read the guardrails in [Render Cadence Golden Rules ```text RenderThread 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 renders simple BGRA8 motion at selected cadence queues async PBO readback publishes completed frames into SystemFrameExchange InputFrameMailbox - owns latest disposable CPU input slots + owns bounded FIFO CPU input slots keeps a bounded three-ready-frame input buffer for render trims frames beyond that bound to avoid runaway input latency protects the one frame currently being uploaded by render @@ -26,7 +26,7 @@ InputFrameMailbox SystemFrameExchange 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 DeckLinkOutputThread @@ -286,8 +286,8 @@ Input telemetry: - `renderFrameMaxMs`: maximum observed render-thread draw duration for this process - `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 -- `completedDrops`: completed unscheduled system-memory frames dropped so render could reuse the slot -- `acquireMisses`: times render/readback could not acquire a writable system-memory frame 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; completed frames waiting for playout are preserved instead of being displaced - `inputConsumeMisses`: render ticks where no ready input frame was available to upload - `inputUploadMisses`: input texture upload attempts that reused the previous GL input texture - `inputReadyFrames`: ready input frames currently queued in `InputFrameMailbox` diff --git a/apps/RenderCadenceCompositor/RenderCadenceCompositor.cpp b/apps/RenderCadenceCompositor/RenderCadenceCompositor.cpp index 7a707a1..267735b 100644 --- a/apps/RenderCadenceCompositor/RenderCadenceCompositor.cpp +++ b/apps/RenderCadenceCompositor/RenderCadenceCompositor.cpp @@ -111,14 +111,23 @@ int main(int argc, char** argv) InputFrameMailbox inputMailbox(inputMailboxConfig); VideoFormat inputVideoMode; + VideoFormat outputVideoMode; std::string inputVideoModeError; const bool inputVideoModeResolved = ResolveConfiguredVideoFormat(appConfig.inputVideoFormat, appConfig.inputFrameRate, inputVideoMode); + const bool outputVideoModeResolved = ResolveConfiguredVideoFormat(appConfig.outputVideoFormat, appConfig.outputFrameRate, outputVideoMode); if (!inputVideoModeResolved) { inputVideoModeError = "Unsupported DeckLink inputVideoFormat/inputFrameRate in config/runtime-host.json: " + appConfig.inputVideoFormat + " / " + appConfig.inputFrameRate; 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::DeckLinkInputThread deckLinkInputThread(deckLinkInput); @@ -173,7 +182,10 @@ int main(int argc, char** argv) RenderThread::Config renderConfig; renderConfig.width = frameExchangeConfig.width; 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; RenderThread renderThread(frameExchange, &inputMailbox, renderConfig); diff --git a/apps/RenderCadenceCompositor/app/AppConfigProvider.cpp b/apps/RenderCadenceCompositor/app/AppConfigProvider.cpp index bef9c57..d10604c 100644 --- a/apps/RenderCadenceCompositor/app/AppConfigProvider.cpp +++ b/apps/RenderCadenceCompositor/app/AppConfigProvider.cpp @@ -4,6 +4,7 @@ #include #include +#include #include #include #include @@ -184,6 +185,50 @@ double FrameDurationMillisecondsFromRateString(const std::string& rateText, doub 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(rate.frameDuration) * 1000.0) / static_cast(rate.timeScale); + } + + return fallbackMilliseconds; +} + void VideoFormatDimensions(const std::string& formatName, unsigned& width, unsigned& height) { std::string normalized = formatName; diff --git a/apps/RenderCadenceCompositor/app/AppConfigProvider.h b/apps/RenderCadenceCompositor/app/AppConfigProvider.h index 5477b7d..4992311 100644 --- a/apps/RenderCadenceCompositor/app/AppConfigProvider.h +++ b/apps/RenderCadenceCompositor/app/AppConfigProvider.h @@ -1,6 +1,7 @@ #pragma once #include "AppConfig.h" +#include "DeckLinkDisplayMode.h" #include #include @@ -27,6 +28,7 @@ private: }; 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); std::filesystem::path FindConfigFile(const std::filesystem::path& relativePath = "config/runtime-host.json"); std::filesystem::path FindRepoPath(const std::filesystem::path& relativePath); diff --git a/apps/RenderCadenceCompositor/frames/InputFrameMailbox.cpp b/apps/RenderCadenceCompositor/frames/InputFrameMailbox.cpp index c3137d2..b60a92d 100644 --- a/apps/RenderCadenceCompositor/frames/InputFrameMailbox.cpp +++ b/apps/RenderCadenceCompositor/frames/InputFrameMailbox.cpp @@ -120,38 +120,6 @@ bool InputFrameMailbox::SubmitFrame(const void* bytes, unsigned rowBytes, uint64 return true; } -bool InputFrameMailbox::TryAcquireLatest(InputFrame& frame) -{ - std::lock_guard 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) { std::lock_guard lock(mMutex); diff --git a/apps/RenderCadenceCompositor/frames/InputFrameMailbox.h b/apps/RenderCadenceCompositor/frames/InputFrameMailbox.h index 60284a6..74a8522 100644 --- a/apps/RenderCadenceCompositor/frames/InputFrameMailbox.h +++ b/apps/RenderCadenceCompositor/frames/InputFrameMailbox.h @@ -64,7 +64,6 @@ public: InputFrameMailboxConfig Config() const; bool SubmitFrame(const void* bytes, unsigned rowBytes, uint64_t frameIndex); - bool TryAcquireLatest(InputFrame& frame); bool TryAcquireOldest(InputFrame& frame); bool Release(const InputFrame& frame); void Clear(); diff --git a/apps/RenderCadenceCompositor/frames/SystemFrameExchange.cpp b/apps/RenderCadenceCompositor/frames/SystemFrameExchange.cpp index 59d3e37..6f06682 100644 --- a/apps/RenderCadenceCompositor/frames/SystemFrameExchange.cpp +++ b/apps/RenderCadenceCompositor/frames/SystemFrameExchange.cpp @@ -47,12 +47,9 @@ bool SystemFrameExchange::AcquireForRender(SystemFrame& frame) std::lock_guard lock(mMutex); if (!AcquireFreeLocked(frame)) { - if (!DropOldestCompletedLocked() || !AcquireFreeLocked(frame)) - { - frame = SystemFrame(); - ++mCounters.acquireMisses; - return false; - } + frame = SystemFrame(); + ++mCounters.acquireMisses; + return false; } ++mCounters.acquiredFrames; @@ -234,27 +231,6 @@ bool SystemFrameExchange::AcquireFreeLocked(SystemFrame& frame) 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 { return frame.index < mSlots.size() && mSlots[frame.index].generation == frame.generation; diff --git a/apps/RenderCadenceCompositor/frames/SystemFrameExchange.h b/apps/RenderCadenceCompositor/frames/SystemFrameExchange.h index 7968b4c..5dd429b 100644 --- a/apps/RenderCadenceCompositor/frames/SystemFrameExchange.h +++ b/apps/RenderCadenceCompositor/frames/SystemFrameExchange.h @@ -40,7 +40,6 @@ private: }; bool AcquireFreeLocked(SystemFrame& frame); - bool DropOldestCompletedLocked(); bool IsValidLocked(const SystemFrame& frame) const; void FillFrameLocked(std::size_t index, SystemFrame& frame); std::size_t CompletedCountLocked() const; diff --git a/apps/RenderCadenceCompositor/render/runtime/RuntimeRenderSceneRender.cpp b/apps/RenderCadenceCompositor/render/runtime/RuntimeRenderSceneRender.cpp index c3e2cfb..65ff2e5 100644 --- a/apps/RenderCadenceCompositor/render/runtime/RuntimeRenderSceneRender.cpp +++ b/apps/RenderCadenceCompositor/render/runtime/RuntimeRenderSceneRender.cpp @@ -65,7 +65,7 @@ void RuntimeRenderScene::RenderFrame(uint64_t frameIndex, unsigned width, unsign } // 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. GLuint layerInputTexture = videoInputTexture; std::size_t nextTargetIndex = 0; diff --git a/apps/RenderCadenceCompositor/video/DeckLinkOutputThread.h b/apps/RenderCadenceCompositor/video/DeckLinkOutputThread.h index 6aacae9..0c06bac 100644 --- a/apps/RenderCadenceCompositor/video/DeckLinkOutputThread.h +++ b/apps/RenderCadenceCompositor/video/DeckLinkOutputThread.h @@ -82,7 +82,6 @@ private: std::this_thread::sleep_for(mConfig.idleSleep); continue; } - SystemFrame frame; if (!mExchange.ConsumeCompletedForSchedule(frame)) { diff --git a/tests/RenderCadenceCompositorAppConfigProviderTests.cpp b/tests/RenderCadenceCompositorAppConfigProviderTests.cpp index 7986912..408fb50 100644 --- a/tests/RenderCadenceCompositorAppConfigProviderTests.cpp +++ b/tests/RenderCadenceCompositorAppConfigProviderTests.cpp @@ -107,6 +107,8 @@ void TestHelpers() const double duration = FrameDurationMillisecondsFromRateString("50"); 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(); Expect(!configPath.empty(), "default config is discoverable from test working directory"); diff --git a/tests/RenderCadenceCompositorFrameExchangeTests.cpp b/tests/RenderCadenceCompositorFrameExchangeTests.cpp index 93dbc14..9c7af12 100644 --- a/tests/RenderCadenceCompositorFrameExchangeTests.cpp +++ b/tests/RenderCadenceCompositorFrameExchangeTests.cpp @@ -57,32 +57,29 @@ void TestAcquirePublishesAndSchedules() Expect(metrics.scheduledFrames == 1, "scheduled metric is counted"); } -void TestAcquireDropsOldestCompletedUnscheduled() +void TestAcquirePreservesCompletedFrames() { SystemFrameExchange exchange(MakeConfig(2)); SystemFrame first; SystemFrame second; SystemFrame third; - Expect(exchange.AcquireForRender(first), "first frame can be acquired"); + Expect(exchange.AcquireForRender(first), "first preserving frame can be acquired"); first.frameIndex = 1; - Expect(exchange.PublishCompleted(first), "first frame can be completed"); - Expect(exchange.AcquireForRender(second), "second frame can be acquired"); + Expect(exchange.PublishCompleted(first), "first preserving frame can be completed"); + Expect(exchange.AcquireForRender(second), "second preserving frame can be acquired"); 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(third.index == first.index, "oldest completed slot is reused"); + Expect(!exchange.AcquireForRender(third), "render acquire refuses to drop completed frames"); SystemFrame scheduled; - Expect(exchange.ConsumeCompletedForSchedule(scheduled), "remaining completed frame can be scheduled"); - Expect(scheduled.index == second.index, "newer completed frame survives drop"); - Expect(scheduled.frameIndex == 2, "newer frame index survives drop"); + Expect(exchange.ConsumeCompletedForSchedule(scheduled), "oldest completed frame survives acquire miss"); + Expect(scheduled.frameIndex == 1, "preserving acquire keeps FIFO output continuity"); SystemFrameExchangeMetrics metrics = exchange.Metrics(); - Expect(metrics.completedDrops == 1, "drop metric is counted"); - Expect(metrics.renderingCount == 1, "reused slot is rendering"); - Expect(metrics.scheduledCount == 1, "consumed slot is scheduled"); + Expect(metrics.completedDrops == 0, "preserving acquire does not count completed drops"); + Expect(metrics.acquireMisses == 1, "preserving acquire miss is counted"); } void TestScheduledFramesAreNotDropped() @@ -179,7 +176,7 @@ void TestStableCompletedDepthTimesOut() int main() { TestAcquirePublishesAndSchedules(); - TestAcquireDropsOldestCompletedUnscheduled(); + TestAcquirePreservesCompletedFrames(); TestScheduledFramesAreNotDropped(); TestGenerationValidationRejectsStaleFrames(); TestPixelFormatAwareSizing(); diff --git a/tests/RenderCadenceCompositorInputFrameMailboxTests.cpp b/tests/RenderCadenceCompositorInputFrameMailboxTests.cpp index 19bfb4b..3702a44 100644 --- a/tests/RenderCadenceCompositorInputFrameMailboxTests.cpp +++ b/tests/RenderCadenceCompositorInputFrameMailboxTests.cpp @@ -41,28 +41,6 @@ std::vector MakeFrame(unsigned char value) return std::vector(16, value); } -void TestAcquireLatestDropsOlderReadyFrames() -{ - InputFrameMailbox mailbox(MakeConfig(3)); - const std::vector frame1 = MakeFrame(1); - const std::vector frame2 = MakeFrame(2); - const std::vector 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(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() { 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(frame3.data(), 8, 3), "third frame submits by dropping oldest ready frame"); - InputFrame latest; - Expect(mailbox.TryAcquireLatest(latest), "latest frame available after drop"); - Expect(latest.frameIndex == 3, "newest frame survived full mailbox"); - Expect(mailbox.Release(latest), "newest frame releases"); + InputFrame oldest; + Expect(mailbox.TryAcquireOldest(oldest), "oldest frame available after drop"); + Expect(oldest.frameIndex == 2, "bounded mailbox keeps FIFO order after trimming oldest overflow"); + Expect(mailbox.Release(oldest), "oldest frame releases"); const InputFrameMailboxMetrics metrics = mailbox.Metrics(); 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"); 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.Release(acquired), "protected frame releases"); Expect(mailbox.SubmitFrame(frame2.data(), 8, 2), "producer can submit after release"); @@ -143,7 +121,6 @@ void TestMaxReadyFramesKeepsConfiguredInputBuffer() int main() { - TestAcquireLatestDropsOlderReadyFrames(); TestSubmitDropsOldestWhenFull(); TestReadingFrameIsProtected(); TestAcquireOldestConsumesFifoWithoutDroppingReadyFrames(); diff --git a/tests/VideoPlayoutSchedulerTests.cpp b/tests/VideoPlayoutSchedulerTests.cpp index cac491e..61524e0 100644 --- a/tests/VideoPlayoutSchedulerTests.cpp +++ b/tests/VideoPlayoutSchedulerTests.cpp @@ -70,6 +70,19 @@ void TestDefaultPolicyReportsLagWithoutSkippingScheduleTime() 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() { VideoPlayoutPolicy policy; @@ -133,6 +146,7 @@ int main() TestScheduleAdvancesFromZero(); TestLateAndDroppedRecoveryUsesMeasuredPressure(); TestDefaultPolicyReportsLagWithoutSkippingScheduleTime(); + TestScheduleCursorCanAlignToPlaybackClock(); TestMeasuredRecoveryIsCappedByPolicy(); TestCleanCompletionTracksCompletedIndexAndClearsStreaks(); TestPolicyNormalization();