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; uint64_t actualDeckLinkBufferedFrames = 0;
double deckLinkScheduleCallMilliseconds = 0.0; double deckLinkScheduleCallMilliseconds = 0.0;
uint64_t deckLinkScheduleFailureCount = 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 struct VideoIOFrame

View File

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

View File

@@ -532,17 +532,13 @@ bool DeckLinkSession::ScheduleFrame(IDeckLinkMutableVideoFrame* outputVideoFrame
return false; return false;
} }
BMDTimeValue streamTime = 0; if (mScheduleRealignmentPending)
double playbackSpeed = 0.0;
if (output->GetScheduledStreamTime(mScheduler.TimeScale(), &streamTime, &playbackSpeed) == S_OK && playbackSpeed > 0.0)
{ {
RefreshBufferedVideoFrameCount(); RealignScheduleCursorToPlayback();
const uint64_t leadFrames = mState.actualDeckLinkBufferedFramesAvailable mScheduleRealignmentPending = false;
? static_cast<uint64_t>(mState.actualDeckLinkBufferedFrames) + 1
: static_cast<uint64_t>(mPlayoutPolicy.targetPrerollFrames);
mScheduler.AlignNextScheduleTimeToPlayback(streamTime, leadFrames);
} }
UpdateScheduleLeadTelemetry();
const VideoIOScheduleTime scheduleTime = mScheduler.NextScheduleTime(); 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);
@@ -554,6 +550,49 @@ bool DeckLinkSession::ScheduleFrame(IDeckLinkMutableVideoFrame* outputVideoFrame
return result == S_OK; 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) bool DeckLinkSession::ScheduleSystemMemoryFrame(const VideoIOOutputFrame& frame)
{ {
if (output == nullptr || frame.bytes == nullptr || frame.rowBytes <= 0 || frame.height == 0) if (output == nullptr || frame.bytes == nullptr || frame.rowBytes <= 0 || frame.height == 0)
@@ -838,6 +877,18 @@ void DeckLinkSession::HandlePlayoutFrameCompleted(IDeckLinkVideoFrame* completed
VideoIOCompletion completion; VideoIOCompletion completion;
completion.result = TranslateCompletionResult(completionResult); 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; completion.outputFrameBuffer = completedSystemBuffer;
mOutputFrameCallback(completion); mOutputFrameCallback(completion);
} }

View File

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

View File

@@ -35,7 +35,7 @@ DeckLinkOutputThread
never renders 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 ## Current Scope
@@ -53,7 +53,7 @@ Included now:
- render-thread-owned input texture upload - render-thread-owned input texture upload
- async PBO readback - async PBO readback
- latest-N system-memory frame exchange - 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` - background Slang compile of `shaders/happy-accident`
- app-owned display/render layer model for shader build readiness - app-owned display/render layer model for shader build readiness
- app-owned submission of a completed shader artifact - app-owned submission of a completed shader artifact
@@ -198,7 +198,6 @@ Currently consumed fields:
- `autoReload` - `autoReload`
- `maxTemporalHistoryFrames` - `maxTemporalHistoryFrames`
- `previewFps` - `previewFps`
- `startupSettleMs`
- `enableExternalKeying` - `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. 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: Startup order is:
1. start render thread 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 3. try to attach DeckLink output
4. start telemetry and HTTP either way 4. start telemetry and HTTP either way
@@ -286,7 +285,7 @@ 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 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 - `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

View File

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

View File

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

View File

@@ -30,9 +30,8 @@ struct AppConfig
bool autoReload = true; bool autoReload = true;
std::size_t maxTemporalHistoryFrames = 12; std::size_t maxTemporalHistoryFrames = 12;
double previewFps = 30.0; 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 warmupTimeout = std::chrono::seconds(3);
std::chrono::milliseconds startupSettle = std::chrono::seconds(5);
std::chrono::milliseconds prerollTimeout = std::chrono::seconds(3); std::chrono::milliseconds prerollTimeout = std::chrono::seconds(3);
std::chrono::milliseconds prerollPoll = std::chrono::milliseconds(2); std::chrono::milliseconds prerollPoll = std::chrono::milliseconds(2);
std::string runtimeShaderId = "happy-accident"; 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); ApplySize(root, "maxTemporalHistoryFrames", mConfig.maxTemporalHistoryFrames);
ApplyDouble(root, "previewFps", mConfig.previewFps); ApplyDouble(root, "previewFps", mConfig.previewFps);
ApplyBool(root, "enableExternalKeying", mConfig.deckLink.externalKeyingEnabled); 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; mLoadedFromFile = true;
error.clear(); error.clear();

View File

@@ -163,23 +163,24 @@ private:
mVideoOutputEnabled = true; mVideoOutputEnabled = true;
mVideoOutputStatus = "DeckLink scheduled output running."; mVideoOutputStatus = "DeckLink scheduled output running.";
Log("app", mVideoOutputStatus); 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) bool BuildSettledOutputReserve(std::string& error)
{ {
const auto reserveTimeout = mConfig.warmupTimeout + mConfig.startupSettle + mConfig.warmupTimeout; const auto reserveTimeout = mConfig.warmupTimeout;
Log("app", Log("app",
"Building settled output reserve: waiting for " + std::to_string(mConfig.warmupCompletedFrames) + "Building output preroll reserve: waiting for " + std::to_string(mConfig.warmupCompletedFrames) +
" completed frame(s) to remain ready for " + std::to_string(mConfig.startupSettle.count()) + " ms."); " completed frame(s).");
if (mFrameExchange.WaitForStableCompletedDepth( if (mFrameExchange.WaitForCompletedDepth(mConfig.warmupCompletedFrames, reserveTimeout))
mConfig.warmupCompletedFrames,
mConfig.startupSettle,
reserveTimeout))
{ {
return true; return true;
} }
error = "Timed out waiting for settled output reserve."; error = "Timed out waiting for output preroll reserve.";
return false; 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.KeyUInt("maxTemporalHistoryFrames", static_cast<uint64_t>(input.config.maxTemporalHistoryFrames));
writer.KeyDouble("previewFps", input.config.previewFps); writer.KeyDouble("previewFps", input.config.previewFps);
writer.KeyBool("enableExternalKeying", input.config.deckLink.externalKeyingEnabled); 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("inputVideoFormat", input.config.inputVideoFormat);
writer.KeyString("inputFrameRate", input.config.inputFrameRate); writer.KeyString("inputFrameRate", input.config.inputFrameRate);
writer.KeyString("outputVideoFormat", input.config.outputVideoFormat); writer.KeyString("outputVideoFormat", input.config.outputVideoFormat);

View File

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

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

View File

@@ -53,6 +53,12 @@ struct CadenceTelemetrySnapshot
bool deckLinkBufferedAvailable = false; bool deckLinkBufferedAvailable = false;
uint64_t deckLinkBuffered = 0; uint64_t deckLinkBuffered = 0;
double deckLinkScheduleCallMilliseconds = 0.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 class CadenceTelemetry
@@ -92,6 +98,12 @@ public:
snapshot.deckLinkBufferedAvailable = outputMetrics.actualBufferedFramesAvailable; snapshot.deckLinkBufferedAvailable = outputMetrics.actualBufferedFramesAvailable;
snapshot.deckLinkBuffered = outputMetrics.actualBufferedFrames; snapshot.deckLinkBuffered = outputMetrics.actualBufferedFrames;
snapshot.deckLinkScheduleCallMilliseconds = outputMetrics.scheduleCallMilliseconds; 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) if (mHasLastSample && seconds > 0.0)
{ {

View File

@@ -61,6 +61,16 @@ inline void WriteCadenceTelemetryJson(JsonWriter& writer, const CadenceTelemetry
else else
writer.Null(); writer.Null();
writer.KeyDouble("scheduleCallMs", snapshot.deckLinkScheduleCallMilliseconds); 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(); writer.EndObject();
} }

View File

@@ -15,6 +15,7 @@ bool DeckLinkOutput::Initialize(const DeckLinkOutputConfig& config, CompletionCa
mCompletionCallback = completionCallback; mCompletionCallback = completionCallback;
VideoFormatSelection formats; VideoFormatSelection formats;
formats.output = config.outputVideoMode;
if (!mSession.DiscoverDevicesAndModes(formats, error)) if (!mSession.DiscoverDevicesAndModes(formats, error))
return false; return false;
if (!mSession.SelectPreferredFormats(formats, config.outputAlphaRequired, error)) if (!mSession.SelectPreferredFormats(formats, config.outputAlphaRequired, error))
@@ -76,6 +77,12 @@ DeckLinkOutputMetrics DeckLinkOutput::Metrics() const
metrics.actualBufferedFramesAvailable = state.actualDeckLinkBufferedFramesAvailable; metrics.actualBufferedFramesAvailable = state.actualDeckLinkBufferedFramesAvailable;
metrics.actualBufferedFrames = state.actualDeckLinkBufferedFrames; metrics.actualBufferedFrames = state.actualDeckLinkBufferedFrames;
metrics.scheduleCallMilliseconds = state.deckLinkScheduleCallMilliseconds; 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; return metrics;
} }

View File

@@ -1,5 +1,6 @@
#pragma once #pragma once
#include "DeckLinkDisplayMode.h"
#include "DeckLinkSession.h" #include "DeckLinkSession.h"
#include "VideoIOTypes.h" #include "VideoIOTypes.h"
@@ -12,6 +13,7 @@ namespace RenderCadenceCompositor
{ {
struct DeckLinkOutputConfig struct DeckLinkOutputConfig
{ {
VideoFormat outputVideoMode;
bool externalKeyingEnabled = false; bool externalKeyingEnabled = false;
bool outputAlphaRequired = false; bool outputAlphaRequired = false;
}; };
@@ -26,6 +28,12 @@ struct DeckLinkOutputMetrics
bool actualBufferedFramesAvailable = false; bool actualBufferedFramesAvailable = false;
uint64_t actualBufferedFrames = 0; uint64_t actualBufferedFrames = 0;
double scheduleCallMilliseconds = 0.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 class DeckLinkOutput

View File

@@ -77,7 +77,11 @@ private:
while (!mStopping) while (!mStopping)
{ {
const auto exchangeMetrics = mExchange.Metrics(); 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); std::this_thread::sleep_for(mConfig.idleSleep);
continue; continue;

View File

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

View File

@@ -557,8 +557,6 @@ components:
type: number type: number
previewFps: previewFps:
type: number type: number
startupSettleMs:
type: number
enableExternalKeying: enableExternalKeying:
type: boolean type: boolean
inputVideoFormat: inputVideoFormat:
@@ -721,6 +719,25 @@ components:
type: number type: number
inputCaptureFormat: inputCaptureFormat:
type: string 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: BackendPlayoutStatus:
type: object type: object
properties: properties:

View File

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

View File

@@ -27,6 +27,13 @@ SystemFrameExchangeConfig MakeConfig(std::size_t capacity = 2)
return config; 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() void TestAcquirePublishesAndSchedules()
{ {
SystemFrameExchange exchange(MakeConfig(1)); SystemFrameExchange exchange(MakeConfig(1));
@@ -82,6 +89,31 @@ void TestAcquirePreservesCompletedFrames()
Expect(metrics.acquireMisses == 1, "preserving acquire miss is counted"); 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() void TestScheduledFramesAreNotDropped()
{ {
SystemFrameExchange exchange(MakeConfig(1)); SystemFrameExchange exchange(MakeConfig(1));
@@ -177,6 +209,7 @@ int main()
{ {
TestAcquirePublishesAndSchedules(); TestAcquirePublishesAndSchedules();
TestAcquirePreservesCompletedFrames(); TestAcquirePreservesCompletedFrames();
TestCompletedReserveIsBoundedFifo();
TestScheduledFramesAreNotDropped(); TestScheduledFramesAreNotDropped();
TestGenerationValidationRejectsStaleFrames(); TestGenerationValidationRejectsStaleFrames();
TestPixelFormatAwareSizing(); TestPixelFormatAwareSizing();

View File

@@ -57,6 +57,12 @@ struct FakeOutputMetrics
bool actualBufferedFramesAvailable = false; bool actualBufferedFramesAvailable = false;
uint64_t actualBufferedFrames = 0; uint64_t actualBufferedFrames = 0;
double scheduleCallMilliseconds = 0.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 struct FakeOutput
@@ -109,6 +115,12 @@ void TestTelemetrySamplesCompletedPollMissesAndShaderCounts()
FakeOutput output; FakeOutput output;
output.metrics.actualBufferedFramesAvailable = true; output.metrics.actualBufferedFramesAvailable = true;
output.metrics.actualBufferedFrames = 4; 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; FakeOutputThread outputThread;
outputThread.metrics.completedPollMisses = 12; outputThread.metrics.completedPollMisses = 12;
@@ -163,6 +175,12 @@ void TestTelemetrySamplesCompletedPollMissesAndShaderCounts()
Expect(snapshot.inputSignalPresent, "input signal present is sampled"); Expect(snapshot.inputSignalPresent, "input signal present is sampled");
Expect(snapshot.deckLinkBufferedAvailable, "buffer telemetry availability is sampled"); Expect(snapshot.deckLinkBufferedAvailable, "buffer telemetry availability is sampled");
Expect(snapshot.deckLinkBuffered == 4, "buffer depth 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() void TestTelemetryComputesRatesFromDeltas()
@@ -234,6 +252,12 @@ void TestTelemetrySerializesToJson()
snapshot.deckLinkBufferedAvailable = true; snapshot.deckLinkBufferedAvailable = true;
snapshot.deckLinkBuffered = 4; snapshot.deckLinkBuffered = 4;
snapshot.deckLinkScheduleCallMilliseconds = 1.25; 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 json = RenderCadenceCompositor::CadenceTelemetryToJson(snapshot);
const std::string expected = const std::string expected =
@@ -259,7 +283,13 @@ void TestTelemetrySerializesToJson()
"\"inputUnsupportedFrames\":3,\"inputSubmitMisses\":4," "\"inputUnsupportedFrames\":3,\"inputSubmitMisses\":4,"
"\"inputCaptureFormat\":\"UYVY8\"," "\"inputCaptureFormat\":\"UYVY8\","
"\"deckLinkBufferedAvailable\":true,\"deckLinkBuffered\":4," "\"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"); Expect(json == expected, "telemetry snapshot serializes to stable JSON");
} }