telemetry and timing updates
All checks were successful
CI / React UI Build (push) Successful in 10s
CI / Native Windows Build And Tests (push) Successful in 2m58s
CI / Windows Release Package (push) Has been skipped

This commit is contained in:
Aiden
2026-05-13 00:21:28 +10:00
parent d411453f80
commit 5c1fc2a6cf
24 changed files with 260 additions and 38 deletions

View File

@@ -54,6 +54,12 @@ struct VideoIOState
uint64_t actualDeckLinkBufferedFrames = 0;
double deckLinkScheduleCallMilliseconds = 0.0;
uint64_t deckLinkScheduleFailureCount = 0;
bool deckLinkScheduleLeadAvailable = false;
int64_t deckLinkPlaybackStreamTime = 0;
uint64_t deckLinkPlaybackFrameIndex = 0;
uint64_t deckLinkNextScheduleFrameIndex = 0;
int64_t deckLinkScheduleLeadFrames = 0;
uint64_t deckLinkScheduleRealignmentCount = 0;
};
struct VideoIOFrame

View File

@@ -17,6 +17,7 @@ public:
double FrameBudgetMilliseconds() const;
uint64_t ScheduledFrameIndex() const { return mScheduledFrameIndex; }
uint64_t CompletedFrameIndex() const { return mCompletedFrameIndex; }
int64_t FrameDuration() const { return mFrameDuration; }
uint64_t LateStreak() const { return mLateStreak; }
uint64_t DropStreak() const { return mDropStreak; }
int64_t TimeScale() const { return mTimeScale; }

View File

@@ -532,17 +532,13 @@ bool DeckLinkSession::ScheduleFrame(IDeckLinkMutableVideoFrame* outputVideoFrame
return false;
}
BMDTimeValue streamTime = 0;
double playbackSpeed = 0.0;
if (output->GetScheduledStreamTime(mScheduler.TimeScale(), &streamTime, &playbackSpeed) == S_OK && playbackSpeed > 0.0)
if (mScheduleRealignmentPending)
{
RefreshBufferedVideoFrameCount();
const uint64_t leadFrames = mState.actualDeckLinkBufferedFramesAvailable
? static_cast<uint64_t>(mState.actualDeckLinkBufferedFrames) + 1
: static_cast<uint64_t>(mPlayoutPolicy.targetPrerollFrames);
mScheduler.AlignNextScheduleTimeToPlayback(streamTime, leadFrames);
RealignScheduleCursorToPlayback();
mScheduleRealignmentPending = false;
}
UpdateScheduleLeadTelemetry();
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);
@@ -554,6 +550,49 @@ bool DeckLinkSession::ScheduleFrame(IDeckLinkMutableVideoFrame* outputVideoFrame
return result == S_OK;
}
void DeckLinkSession::UpdateScheduleLeadTelemetry()
{
if (output == nullptr)
{
mState.deckLinkScheduleLeadAvailable = false;
return;
}
BMDTimeValue streamTime = 0;
double playbackSpeed = 0.0;
if (output->GetScheduledStreamTime(mScheduler.TimeScale(), &streamTime, &playbackSpeed) != S_OK || playbackSpeed <= 0.0)
{
mState.deckLinkScheduleLeadAvailable = false;
return;
}
const uint64_t playbackFrameIndex = streamTime >= 0 && mScheduler.FrameDuration() > 0
? static_cast<uint64_t>(streamTime / mScheduler.FrameDuration())
: 0;
const uint64_t nextScheduleFrameIndex = mScheduler.ScheduledFrameIndex();
mState.deckLinkScheduleLeadAvailable = true;
mState.deckLinkPlaybackStreamTime = streamTime;
mState.deckLinkPlaybackFrameIndex = playbackFrameIndex;
mState.deckLinkNextScheduleFrameIndex = nextScheduleFrameIndex;
mState.deckLinkScheduleLeadFrames = static_cast<int64_t>(nextScheduleFrameIndex) - static_cast<int64_t>(playbackFrameIndex);
}
void DeckLinkSession::RealignScheduleCursorToPlayback()
{
if (output == nullptr)
return;
BMDTimeValue streamTime = 0;
double playbackSpeed = 0.0;
if (output->GetScheduledStreamTime(mScheduler.TimeScale(), &streamTime, &playbackSpeed) != S_OK || playbackSpeed <= 0.0)
return;
const VideoPlayoutPolicy policy = NormalizeVideoPlayoutPolicy(mPlayoutPolicy);
mScheduler.AlignNextScheduleTimeToPlayback(streamTime, policy.targetPrerollFrames);
++mState.deckLinkScheduleRealignmentCount;
UpdateScheduleLeadTelemetry();
}
bool DeckLinkSession::ScheduleSystemMemoryFrame(const VideoIOOutputFrame& frame)
{
if (output == nullptr || frame.bytes == nullptr || frame.rowBytes <= 0 || frame.height == 0)
@@ -838,6 +877,18 @@ void DeckLinkSession::HandlePlayoutFrameCompleted(IDeckLinkVideoFrame* completed
VideoIOCompletion completion;
completion.result = TranslateCompletionResult(completionResult);
if (completion.result == VideoIOCompletionResult::DisplayedLate || completion.result == VideoIOCompletionResult::Dropped)
{
if (mScheduleRealignmentArmed)
{
mScheduleRealignmentPending = true;
mScheduleRealignmentArmed = false;
}
}
else if (completion.result == VideoIOCompletionResult::Completed)
{
mScheduleRealignmentArmed = true;
}
completion.outputFrameBuffer = completedSystemBuffer;
mOutputFrameCallback(completion);
}

View File

@@ -75,6 +75,8 @@ private:
bool AcquireNextOutputVideoFrame(CComPtr<IDeckLinkMutableVideoFrame>& outputVideoFrame);
bool PopulateOutputFrame(IDeckLinkMutableVideoFrame* outputVideoFrame, VideoIOOutputFrame& frame);
bool ScheduleFrame(IDeckLinkMutableVideoFrame* outputVideoFrame);
void UpdateScheduleLeadTelemetry();
void RealignScheduleCursorToPlayback();
bool ScheduleSystemMemoryFrame(const VideoIOOutputFrame& frame);
bool ScheduleBlackFrame(IDeckLinkMutableVideoFrame* outputVideoFrame);
void RefreshBufferedVideoFrameCount();
@@ -91,6 +93,8 @@ private:
VideoIOState mState;
VideoPlayoutPolicy mPlayoutPolicy;
VideoPlayoutScheduler mScheduler;
bool mScheduleRealignmentPending = false;
bool mScheduleRealignmentArmed = true;
InputFrameCallback mInputFrameCallback;
OutputFrameCallback mOutputFrameCallback;
};

View File

@@ -35,7 +35,7 @@ DeckLinkOutputThread
never renders
```
Startup builds one settled output reserve before DeckLink scheduled playback starts: the completed-frame reserve must reach the configured depth and remain ready for the configured settle window. When DeckLink input is available, startup also waits briefly for three ready input frames before the render thread starts so the first render ticks are deliberate rather than lucky.
Startup builds a small output preroll reserve before DeckLink scheduled playback starts. When DeckLink input is available, startup also waits briefly for three ready input frames before the render thread starts so the first render ticks are deliberate rather than lucky.
## Current Scope
@@ -53,7 +53,7 @@ Included now:
- render-thread-owned input texture upload
- async PBO readback
- latest-N system-memory frame exchange
- settled completed-frame output reserve before DeckLink preroll, with DeckLink scheduled depth still targeted at four
- bounded completed-frame output preroll reserve before DeckLink playback, with DeckLink scheduled depth still targeted at four
- background Slang compile of `shaders/happy-accident`
- app-owned display/render layer model for shader build readiness
- app-owned submission of a completed shader artifact
@@ -198,7 +198,6 @@ Currently consumed fields:
- `autoReload`
- `maxTemporalHistoryFrames`
- `previewFps`
- `startupSettleMs`
- `enableExternalKeying`
The loaded config is treated as a read-only startup snapshot. Subsystems that need config should receive this snapshot or a narrowed config struct from app orchestration; they should not reload files independently.
@@ -238,7 +237,7 @@ DeckLink output is an optional edge service in this app.
Startup order is:
1. start render thread
2. build a settled completed-frame output reserve at normal render cadence
2. build a bounded completed-frame output preroll reserve at normal render cadence
3. try to attach DeckLink output
4. start telemetry and HTTP either way
@@ -286,7 +285,7 @@ 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 by latest-N acquire paths; expected to stay flat in the cadence compositor output path
- `completedDrops`: completed unscheduled system-memory frames dropped; 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

View File

@@ -20,6 +20,10 @@
namespace
{
constexpr std::size_t kDeckLinkTargetBufferedFrames = 4;
constexpr std::size_t kReadbackDepth = 6;
constexpr std::size_t kWritableOutputReserveFrames = kReadbackDepth + 2;
class ComInitGuard
{
public:
@@ -95,7 +99,11 @@ int main(int argc, char** argv)
frameExchangeConfig.height);
frameExchangeConfig.pixelFormat = VideoIOPixelFormat::Bgra8;
frameExchangeConfig.rowBytes = VideoIORowBytes(frameExchangeConfig.pixelFormat, frameExchangeConfig.width);
frameExchangeConfig.capacity = 12;
frameExchangeConfig.capacity =
appConfig.warmupCompletedFrames +
kDeckLinkTargetBufferedFrames +
kWritableOutputReserveFrames;
frameExchangeConfig.maxCompletedFrames = appConfig.warmupCompletedFrames;
SystemFrameExchange frameExchange(frameExchangeConfig);
@@ -128,6 +136,10 @@ int main(int argc, char** argv)
"Unsupported DeckLink outputVideoFormat/outputFrameRate in config/runtime-host.json; render cadence will use parsed frame-rate fallback: " +
appConfig.outputVideoFormat + " / " + appConfig.outputFrameRate);
}
else
{
appConfig.deckLink.outputVideoMode = outputVideoMode;
}
RenderCadenceCompositor::DeckLinkInput deckLinkInput(inputMailbox);
RenderCadenceCompositor::DeckLinkInputThread deckLinkInputThread(deckLinkInput);
@@ -186,7 +198,7 @@ int main(int argc, char** argv)
renderConfig.frameDurationMilliseconds = outputVideoModeResolved
? RenderCadenceCompositor::FrameDurationMillisecondsFromDisplayMode(outputVideoMode.displayMode, fallbackFrameDurationMilliseconds)
: fallbackFrameDurationMilliseconds;
renderConfig.pboDepth = 6;
renderConfig.pboDepth = kReadbackDepth;
RenderThread renderThread(frameExchange, &inputMailbox, renderConfig);

View File

@@ -29,9 +29,8 @@ AppConfig DefaultAppConfig()
config.autoReload = true;
config.maxTemporalHistoryFrames = 12;
config.previewFps = 30.0;
config.warmupCompletedFrames = 8;
config.warmupCompletedFrames = 4;
config.warmupTimeout = std::chrono::seconds(3);
config.startupSettle = std::chrono::seconds(5);
config.prerollTimeout = std::chrono::seconds(3);
config.prerollPoll = std::chrono::milliseconds(2);
config.runtimeShaderId = "happy-accident";

View File

@@ -30,9 +30,8 @@ struct AppConfig
bool autoReload = true;
std::size_t maxTemporalHistoryFrames = 12;
double previewFps = 30.0;
std::size_t warmupCompletedFrames = 8;
std::size_t warmupCompletedFrames = 4;
std::chrono::milliseconds warmupTimeout = std::chrono::seconds(3);
std::chrono::milliseconds startupSettle = std::chrono::seconds(5);
std::chrono::milliseconds prerollTimeout = std::chrono::seconds(3);
std::chrono::milliseconds prerollPoll = std::chrono::milliseconds(2);
std::string runtimeShaderId = "happy-accident";

View File

@@ -134,9 +134,6 @@ bool AppConfigProvider::Load(const std::filesystem::path& path, std::string& err
ApplySize(root, "maxTemporalHistoryFrames", mConfig.maxTemporalHistoryFrames);
ApplyDouble(root, "previewFps", mConfig.previewFps);
ApplyBool(root, "enableExternalKeying", mConfig.deckLink.externalKeyingEnabled);
std::size_t startupSettleMilliseconds = static_cast<std::size_t>(mConfig.startupSettle.count());
ApplySize(root, "startupSettleMs", startupSettleMilliseconds);
mConfig.startupSettle = std::chrono::milliseconds(startupSettleMilliseconds);
mLoadedFromFile = true;
error.clear();

View File

@@ -163,23 +163,24 @@ private:
mVideoOutputEnabled = true;
mVideoOutputStatus = "DeckLink scheduled output running.";
Log("app", mVideoOutputStatus);
Log(
"app",
"DeckLink output mode: " + mOutput.State().outputDisplayModeName +
", frame budget " + std::to_string(mOutput.State().frameBudgetMilliseconds) + " ms.");
}
bool BuildSettledOutputReserve(std::string& error)
{
const auto reserveTimeout = mConfig.warmupTimeout + mConfig.startupSettle + mConfig.warmupTimeout;
const auto reserveTimeout = mConfig.warmupTimeout;
Log("app",
"Building settled output reserve: waiting for " + std::to_string(mConfig.warmupCompletedFrames) +
" completed frame(s) to remain ready for " + std::to_string(mConfig.startupSettle.count()) + " ms.");
if (mFrameExchange.WaitForStableCompletedDepth(
mConfig.warmupCompletedFrames,
mConfig.startupSettle,
reserveTimeout))
"Building output preroll reserve: waiting for " + std::to_string(mConfig.warmupCompletedFrames) +
" completed frame(s).");
if (mFrameExchange.WaitForCompletedDepth(mConfig.warmupCompletedFrames, reserveTimeout))
{
return true;
}
error = "Timed out waiting for settled output reserve.";
error = "Timed out waiting for output preroll reserve.";
return false;
}

View File

@@ -251,7 +251,6 @@ inline std::string RuntimeStateToJson(const RuntimeStateJsonInput& input)
writer.KeyUInt("maxTemporalHistoryFrames", static_cast<uint64_t>(input.config.maxTemporalHistoryFrames));
writer.KeyDouble("previewFps", input.config.previewFps);
writer.KeyBool("enableExternalKeying", input.config.deckLink.externalKeyingEnabled);
writer.KeyUInt("startupSettleMs", static_cast<uint64_t>(input.config.startupSettle.count()));
writer.KeyString("inputVideoFormat", input.config.inputVideoFormat);
writer.KeyString("inputFrameRate", input.config.inputFrameRate);
writer.KeyString("outputVideoFormat", input.config.outputVideoFormat);

View File

@@ -69,6 +69,7 @@ bool SystemFrameExchange::PublishCompleted(const SystemFrame& frame)
slot.state = SystemFrameSlotState::Completed;
slot.frameIndex = frame.frameIndex;
mCompletedIndices.push_back(frame.index);
TrimCompletedLocked();
++mCounters.completedFrames;
mCondition.notify_all();
return true;
@@ -231,6 +232,38 @@ 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;
}
void SystemFrameExchange::TrimCompletedLocked()
{
if (mConfig.maxCompletedFrames == 0)
return;
while (CompletedCountLocked() > mConfig.maxCompletedFrames)
{
if (!DropOldestCompletedLocked())
return;
}
}
bool SystemFrameExchange::IsValidLocked(const SystemFrame& frame) const
{
return frame.index < mSlots.size() && mSlots[frame.index].generation == frame.generation;

View File

@@ -40,6 +40,8 @@ private:
};
bool AcquireFreeLocked(SystemFrame& frame);
bool DropOldestCompletedLocked();
void TrimCompletedLocked();
bool IsValidLocked(const SystemFrame& frame) const;
void FillFrameLocked(std::size_t index, SystemFrame& frame);
std::size_t CompletedCountLocked() const;

View File

@@ -20,6 +20,7 @@ struct SystemFrameExchangeConfig
VideoIOPixelFormat pixelFormat = VideoIOPixelFormat::Bgra8;
unsigned rowBytes = 0;
std::size_t capacity = 0;
std::size_t maxCompletedFrames = 0;
};
struct SystemFrame

View File

@@ -53,6 +53,12 @@ struct CadenceTelemetrySnapshot
bool deckLinkBufferedAvailable = false;
uint64_t deckLinkBuffered = 0;
double deckLinkScheduleCallMilliseconds = 0.0;
bool deckLinkScheduleLeadAvailable = false;
int64_t deckLinkPlaybackStreamTime = 0;
uint64_t deckLinkPlaybackFrameIndex = 0;
uint64_t deckLinkNextScheduleFrameIndex = 0;
int64_t deckLinkScheduleLeadFrames = 0;
uint64_t deckLinkScheduleRealignments = 0;
};
class CadenceTelemetry
@@ -92,6 +98,12 @@ public:
snapshot.deckLinkBufferedAvailable = outputMetrics.actualBufferedFramesAvailable;
snapshot.deckLinkBuffered = outputMetrics.actualBufferedFrames;
snapshot.deckLinkScheduleCallMilliseconds = outputMetrics.scheduleCallMilliseconds;
snapshot.deckLinkScheduleLeadAvailable = outputMetrics.scheduleLeadAvailable;
snapshot.deckLinkPlaybackStreamTime = outputMetrics.playbackStreamTime;
snapshot.deckLinkPlaybackFrameIndex = outputMetrics.playbackFrameIndex;
snapshot.deckLinkNextScheduleFrameIndex = outputMetrics.nextScheduleFrameIndex;
snapshot.deckLinkScheduleLeadFrames = outputMetrics.scheduleLeadFrames;
snapshot.deckLinkScheduleRealignments = outputMetrics.scheduleRealignmentCount;
if (mHasLastSample && seconds > 0.0)
{

View File

@@ -61,6 +61,16 @@ inline void WriteCadenceTelemetryJson(JsonWriter& writer, const CadenceTelemetry
else
writer.Null();
writer.KeyDouble("scheduleCallMs", snapshot.deckLinkScheduleCallMilliseconds);
writer.KeyBool("deckLinkScheduleLeadAvailable", snapshot.deckLinkScheduleLeadAvailable);
writer.Key("deckLinkScheduleLeadFrames");
if (snapshot.deckLinkScheduleLeadAvailable)
writer.Int(snapshot.deckLinkScheduleLeadFrames);
else
writer.Null();
writer.KeyUInt("deckLinkPlaybackFrameIndex", snapshot.deckLinkPlaybackFrameIndex);
writer.KeyUInt("deckLinkNextScheduleFrameIndex", snapshot.deckLinkNextScheduleFrameIndex);
writer.KeyInt("deckLinkPlaybackStreamTime", snapshot.deckLinkPlaybackStreamTime);
writer.KeyUInt("deckLinkScheduleRealignments", snapshot.deckLinkScheduleRealignments);
writer.EndObject();
}

View File

@@ -15,6 +15,7 @@ bool DeckLinkOutput::Initialize(const DeckLinkOutputConfig& config, CompletionCa
mCompletionCallback = completionCallback;
VideoFormatSelection formats;
formats.output = config.outputVideoMode;
if (!mSession.DiscoverDevicesAndModes(formats, error))
return false;
if (!mSession.SelectPreferredFormats(formats, config.outputAlphaRequired, error))
@@ -76,6 +77,12 @@ DeckLinkOutputMetrics DeckLinkOutput::Metrics() const
metrics.actualBufferedFramesAvailable = state.actualDeckLinkBufferedFramesAvailable;
metrics.actualBufferedFrames = state.actualDeckLinkBufferedFrames;
metrics.scheduleCallMilliseconds = state.deckLinkScheduleCallMilliseconds;
metrics.scheduleLeadAvailable = state.deckLinkScheduleLeadAvailable;
metrics.playbackStreamTime = state.deckLinkPlaybackStreamTime;
metrics.playbackFrameIndex = state.deckLinkPlaybackFrameIndex;
metrics.nextScheduleFrameIndex = state.deckLinkNextScheduleFrameIndex;
metrics.scheduleLeadFrames = state.deckLinkScheduleLeadFrames;
metrics.scheduleRealignmentCount = state.deckLinkScheduleRealignmentCount;
return metrics;
}

View File

@@ -1,5 +1,6 @@
#pragma once
#include "DeckLinkDisplayMode.h"
#include "DeckLinkSession.h"
#include "VideoIOTypes.h"
@@ -12,6 +13,7 @@ namespace RenderCadenceCompositor
{
struct DeckLinkOutputConfig
{
VideoFormat outputVideoMode;
bool externalKeyingEnabled = false;
bool outputAlphaRequired = false;
};
@@ -26,6 +28,12 @@ struct DeckLinkOutputMetrics
bool actualBufferedFramesAvailable = false;
uint64_t actualBufferedFrames = 0;
double scheduleCallMilliseconds = 0.0;
bool scheduleLeadAvailable = false;
int64_t playbackStreamTime = 0;
uint64_t playbackFrameIndex = 0;
uint64_t nextScheduleFrameIndex = 0;
int64_t scheduleLeadFrames = 0;
uint64_t scheduleRealignmentCount = 0;
};
class DeckLinkOutput

View File

@@ -77,7 +77,11 @@ private:
while (!mStopping)
{
const auto exchangeMetrics = mExchange.Metrics();
if (exchangeMetrics.scheduledCount >= mConfig.targetBufferedFrames)
const auto outputMetrics = mOutput.Metrics();
const std::size_t bufferedFrames = outputMetrics.actualBufferedFramesAvailable
? static_cast<std::size_t>(outputMetrics.actualBufferedFrames)
: exchangeMetrics.scheduledCount;
if (bufferedFrames >= mConfig.targetBufferedFrames)
{
std::this_thread::sleep_for(mConfig.idleSleep);
continue;

View File

@@ -11,6 +11,5 @@
"autoReload": true,
"maxTemporalHistoryFrames": 12,
"previewFps": 30,
"startupSettleMs": 5000,
"enableExternalKeying": true
}

View File

@@ -557,8 +557,6 @@ components:
type: number
previewFps:
type: number
startupSettleMs:
type: number
enableExternalKeying:
type: boolean
inputVideoFormat:
@@ -721,6 +719,25 @@ components:
type: number
inputCaptureFormat:
type: string
deckLinkScheduleLeadAvailable:
type: boolean
description: Whether DeckLink playback stream-time lead telemetry is currently available.
deckLinkScheduleLeadFrames:
type: number
nullable: true
description: Estimated number of frame intervals between the next app schedule timestamp and the DeckLink playback frame index.
deckLinkPlaybackFrameIndex:
type: number
description: DeckLink playback stream time converted to frame index at the configured output cadence.
deckLinkNextScheduleFrameIndex:
type: number
description: Next frame index the app scheduler will assign to a DeckLink output frame.
deckLinkPlaybackStreamTime:
type: number
description: Raw DeckLink scheduled playback stream time in the output mode time scale.
deckLinkScheduleRealignments:
type: number
description: Count of schedule-cursor recovery realignments triggered by DeckLink late/drop pressure.
BackendPlayoutStatus:
type: object
properties:

View File

@@ -37,7 +37,6 @@ std::filesystem::path WriteConfigFixture()
<< " \"autoReload\": false,\n"
<< " \"maxTemporalHistoryFrames\": 8,\n"
<< " \"previewFps\": 24,\n"
<< " \"startupSettleMs\": 2500,\n"
<< " \"enableExternalKeying\": true\n"
<< "}\n";
return path;
@@ -68,7 +67,6 @@ void TestLoadsRuntimeHostConfig()
Expect(!config.autoReload, "auto reload loads");
Expect(config.maxTemporalHistoryFrames == 8, "history length loads");
Expect(config.previewFps == 24.0, "preview fps loads");
Expect(config.startupSettle == std::chrono::milliseconds(2500), "startup settle loads");
Expect(config.deckLink.externalKeyingEnabled, "external keying loads");
std::filesystem::remove(path);

View File

@@ -27,6 +27,13 @@ SystemFrameExchangeConfig MakeConfig(std::size_t capacity = 2)
return config;
}
SystemFrameExchangeConfig MakeBoundedCompletedConfig(std::size_t capacity = 4, std::size_t maxCompletedFrames = 2)
{
SystemFrameExchangeConfig config = MakeConfig(capacity);
config.maxCompletedFrames = maxCompletedFrames;
return config;
}
void TestAcquirePublishesAndSchedules()
{
SystemFrameExchange exchange(MakeConfig(1));
@@ -82,6 +89,31 @@ void TestAcquirePreservesCompletedFrames()
Expect(metrics.acquireMisses == 1, "preserving acquire miss is counted");
}
void TestCompletedReserveIsBoundedFifo()
{
SystemFrameExchange exchange(MakeBoundedCompletedConfig(4, 2));
for (uint64_t frameIndex = 1; frameIndex <= 3; ++frameIndex)
{
SystemFrame frame;
Expect(exchange.AcquireForRender(frame), "bounded reserve frame can be acquired");
frame.frameIndex = frameIndex;
Expect(exchange.PublishCompleted(frame), "bounded reserve frame can be completed");
}
SystemFrame firstScheduled;
Expect(exchange.ConsumeCompletedForSchedule(firstScheduled), "bounded reserve oldest retained frame can be scheduled");
Expect(firstScheduled.frameIndex == 2, "bounded reserve drops oldest overflow and keeps FIFO order");
SystemFrame secondScheduled;
Expect(exchange.ConsumeCompletedForSchedule(secondScheduled), "bounded reserve second retained frame can be scheduled");
Expect(secondScheduled.frameIndex == 3, "bounded reserve schedules next retained frame");
SystemFrameExchangeMetrics metrics = exchange.Metrics();
Expect(metrics.completedDrops == 1, "bounded completed reserve records oldest overflow drop");
Expect(metrics.scheduledFrames == 2, "bounded reserve schedules retained frames");
}
void TestScheduledFramesAreNotDropped()
{
SystemFrameExchange exchange(MakeConfig(1));
@@ -177,6 +209,7 @@ int main()
{
TestAcquirePublishesAndSchedules();
TestAcquirePreservesCompletedFrames();
TestCompletedReserveIsBoundedFifo();
TestScheduledFramesAreNotDropped();
TestGenerationValidationRejectsStaleFrames();
TestPixelFormatAwareSizing();

View File

@@ -57,6 +57,12 @@ struct FakeOutputMetrics
bool actualBufferedFramesAvailable = false;
uint64_t actualBufferedFrames = 0;
double scheduleCallMilliseconds = 0.0;
bool scheduleLeadAvailable = false;
int64_t playbackStreamTime = 0;
uint64_t playbackFrameIndex = 0;
uint64_t nextScheduleFrameIndex = 0;
int64_t scheduleLeadFrames = 0;
uint64_t scheduleRealignmentCount = 0;
};
struct FakeOutput
@@ -109,6 +115,12 @@ void TestTelemetrySamplesCompletedPollMissesAndShaderCounts()
FakeOutput output;
output.metrics.actualBufferedFramesAvailable = true;
output.metrics.actualBufferedFrames = 4;
output.metrics.scheduleLeadAvailable = true;
output.metrics.playbackStreamTime = 10010;
output.metrics.playbackFrameIndex = 10;
output.metrics.nextScheduleFrameIndex = 14;
output.metrics.scheduleLeadFrames = 4;
output.metrics.scheduleRealignmentCount = 1;
FakeOutputThread outputThread;
outputThread.metrics.completedPollMisses = 12;
@@ -163,6 +175,12 @@ void TestTelemetrySamplesCompletedPollMissesAndShaderCounts()
Expect(snapshot.inputSignalPresent, "input signal present is sampled");
Expect(snapshot.deckLinkBufferedAvailable, "buffer telemetry availability is sampled");
Expect(snapshot.deckLinkBuffered == 4, "buffer depth is sampled");
Expect(snapshot.deckLinkScheduleLeadAvailable, "schedule lead availability is sampled");
Expect(snapshot.deckLinkPlaybackStreamTime == 10010, "playback stream time is sampled");
Expect(snapshot.deckLinkPlaybackFrameIndex == 10, "playback frame index is sampled");
Expect(snapshot.deckLinkNextScheduleFrameIndex == 14, "next schedule frame index is sampled");
Expect(snapshot.deckLinkScheduleLeadFrames == 4, "schedule lead frames are sampled");
Expect(snapshot.deckLinkScheduleRealignments == 1, "schedule realignment count is sampled");
}
void TestTelemetryComputesRatesFromDeltas()
@@ -234,6 +252,12 @@ void TestTelemetrySerializesToJson()
snapshot.deckLinkBufferedAvailable = true;
snapshot.deckLinkBuffered = 4;
snapshot.deckLinkScheduleCallMilliseconds = 1.25;
snapshot.deckLinkScheduleLeadAvailable = true;
snapshot.deckLinkScheduleLeadFrames = 4;
snapshot.deckLinkPlaybackFrameIndex = 10;
snapshot.deckLinkNextScheduleFrameIndex = 14;
snapshot.deckLinkPlaybackStreamTime = 10010;
snapshot.deckLinkScheduleRealignments = 1;
const std::string json = RenderCadenceCompositor::CadenceTelemetryToJson(snapshot);
const std::string expected =
@@ -259,7 +283,13 @@ void TestTelemetrySerializesToJson()
"\"inputUnsupportedFrames\":3,\"inputSubmitMisses\":4,"
"\"inputCaptureFormat\":\"UYVY8\","
"\"deckLinkBufferedAvailable\":true,\"deckLinkBuffered\":4,"
"\"scheduleCallMs\":1.25}";
"\"scheduleCallMs\":1.25,"
"\"deckLinkScheduleLeadAvailable\":true,"
"\"deckLinkScheduleLeadFrames\":4,"
"\"deckLinkPlaybackFrameIndex\":10,"
"\"deckLinkNextScheduleFrameIndex\":14,"
"\"deckLinkPlaybackStreamTime\":10010,"
"\"deckLinkScheduleRealignments\":1}";
Expect(json == expected, "telemetry snapshot serializes to stable JSON");
}