telemetry and timing updates
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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; }
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -11,6 +11,5 @@
|
||||
"autoReload": true,
|
||||
"maxTemporalHistoryFrames": 12,
|
||||
"previewFps": 30,
|
||||
"startupSettleMs": 5000,
|
||||
"enableExternalKeying": true
|
||||
}
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user