Compare commits
4 Commits
d411453f80
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3ffb562ff7 | ||
|
|
c2d548499c | ||
|
|
6a0340d1b4 | ||
|
|
5c1fc2a6cf |
@@ -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; }
|
||||
|
||||
@@ -12,6 +12,9 @@
|
||||
|
||||
namespace
|
||||
{
|
||||
constexpr int64_t kMinimumHealthyScheduleLeadFrames = 4;
|
||||
constexpr int64_t kProactiveScheduleLeadFloorFrames = 1;
|
||||
|
||||
class SystemMemoryDeckLinkVideoBuffer : public IDeckLinkVideoBuffer
|
||||
{
|
||||
public:
|
||||
@@ -532,17 +535,14 @@ 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();
|
||||
MaybeRealignScheduleCursorForLowLead();
|
||||
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 +554,67 @@ 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::MaybeRealignScheduleCursorForLowLead()
|
||||
{
|
||||
if (!mState.deckLinkScheduleLeadAvailable)
|
||||
return;
|
||||
|
||||
if (mState.deckLinkScheduleLeadFrames >= kMinimumHealthyScheduleLeadFrames)
|
||||
{
|
||||
mProactiveScheduleRealignmentArmed = true;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!mProactiveScheduleRealignmentArmed || mState.deckLinkScheduleLeadFrames > kProactiveScheduleLeadFloorFrames)
|
||||
return;
|
||||
|
||||
RealignScheduleCursorToPlayback();
|
||||
mProactiveScheduleRealignmentArmed = false;
|
||||
}
|
||||
|
||||
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 +899,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,9 @@ private:
|
||||
bool AcquireNextOutputVideoFrame(CComPtr<IDeckLinkMutableVideoFrame>& outputVideoFrame);
|
||||
bool PopulateOutputFrame(IDeckLinkMutableVideoFrame* outputVideoFrame, VideoIOOutputFrame& frame);
|
||||
bool ScheduleFrame(IDeckLinkMutableVideoFrame* outputVideoFrame);
|
||||
void UpdateScheduleLeadTelemetry();
|
||||
void MaybeRealignScheduleCursorForLowLead();
|
||||
void RealignScheduleCursorToPlayback();
|
||||
bool ScheduleSystemMemoryFrame(const VideoIOOutputFrame& frame);
|
||||
bool ScheduleBlackFrame(IDeckLinkMutableVideoFrame* outputVideoFrame);
|
||||
void RefreshBufferedVideoFrameCount();
|
||||
@@ -91,6 +94,9 @@ private:
|
||||
VideoIOState mState;
|
||||
VideoPlayoutPolicy mPlayoutPolicy;
|
||||
VideoPlayoutScheduler mScheduler;
|
||||
bool mScheduleRealignmentPending = false;
|
||||
bool mScheduleRealignmentArmed = true;
|
||||
bool mProactiveScheduleRealignmentArmed = true;
|
||||
InputFrameCallback mInputFrameCallback;
|
||||
OutputFrameCallback mOutputFrameCallback;
|
||||
};
|
||||
|
||||
@@ -26,7 +26,7 @@ InputFrameMailbox
|
||||
|
||||
SystemFrameExchange
|
||||
owns Free / Rendering / Completed / Scheduled slots
|
||||
preserves completed output frames once they are waiting for playout
|
||||
preserves completed output frames as a bounded FIFO reserve once they are waiting for playout
|
||||
protects scheduled frames until DeckLink completion
|
||||
|
||||
DeckLinkOutputThread
|
||||
@@ -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
|
||||
|
||||
@@ -52,8 +52,9 @@ Included now:
|
||||
- bounded three-frame input warmup before render cadence starts
|
||||
- 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 FIFO system-memory frame exchange
|
||||
- bounded completed-frame output preroll reserve before DeckLink playback, with DeckLink scheduled depth still targeted at four
|
||||
- conservative DeckLink schedule-lead telemetry and recovery
|
||||
- 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 +199,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 +238,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
|
||||
|
||||
@@ -267,7 +267,7 @@ The app samples telemetry once per second.
|
||||
|
||||
Normal cadence samples are available through `GET /api/state` and are not printed to the console. The telemetry monitor only logs health events:
|
||||
|
||||
- warning when DeckLink late/dropped-frame counters increase
|
||||
- warning when DeckLink late/dropped-frame counters increase, including schedule lead and recovery count
|
||||
- warning when schedule failures increase
|
||||
- error when the app/DeckLink output buffer is starved
|
||||
|
||||
@@ -286,8 +286,14 @@ 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`: oldest completed unscheduled system-memory frames dropped because the bounded completed reserve overflowed; this is an app-side reserve drop, not a DeckLink dropped-frame report
|
||||
- `acquireMisses`: times render/readback could not acquire a writable system-memory frame slot; completed frames waiting for playout are preserved instead of being displaced
|
||||
- `deckLinkScheduleLeadAvailable`: whether DeckLink schedule-lead telemetry was available for the latest sample
|
||||
- `deckLinkScheduleLeadFrames`: estimated distance between the DeckLink playback cursor and the next scheduled stream time
|
||||
- `deckLinkPlaybackFrameIndex`: latest sampled DeckLink playback frame index
|
||||
- `deckLinkNextScheduleFrameIndex`: next stream frame index the app intends to schedule
|
||||
- `deckLinkPlaybackStreamTime`: latest sampled DeckLink playback stream time in DeckLink time units
|
||||
- `deckLinkScheduleRealignments`: conservative schedule-cursor recoveries triggered by late/drop pressure or dangerously low lead
|
||||
- `inputConsumeMisses`: render ticks where no ready input frame was available to upload
|
||||
- `inputUploadMisses`: input texture upload attempts that reused the previous GL input texture
|
||||
- `inputReadyFrames`: ready input frames currently queued in `InputFrameMailbox`
|
||||
@@ -304,7 +310,7 @@ Input telemetry:
|
||||
- `inputUnsupportedFrames`: input frames rejected before mailbox submission
|
||||
- `inputSubmitMisses`: input frames that could not be submitted to the mailbox
|
||||
|
||||
Runtime shaders continue rendering when input is missing. If no mailbox frame has been uploaded yet, shader samplers use the runtime fallback source texture; once DeckLink input is flowing, shaders such as CRT and trigger-ripple sample the real/latest input through `gVideoInput`.
|
||||
Runtime shaders continue rendering when input is missing. If no mailbox frame has been uploaded yet, shader samplers use the runtime fallback source texture; once DeckLink input is flowing, shaders such as CRT and trigger-ripple sample the current input through `gVideoInput`.
|
||||
|
||||
Healthy first-run signs:
|
||||
|
||||
@@ -313,6 +319,8 @@ Healthy first-run signs:
|
||||
- `scheduleFps` is close to the selected cadence after warmup
|
||||
- `scheduled` stays near 4
|
||||
- `decklinkBuffered` stays near 4 when available
|
||||
- `deckLinkScheduleLeadFrames` remains positive and stable when available
|
||||
- `deckLinkScheduleRealignments` does not increase continuously
|
||||
- `late` and `dropped` do not increase continuously
|
||||
- `scheduleFailures` does not increase
|
||||
- `shaderCommitted` becomes `1` after the background Happy Accident compile completes
|
||||
@@ -387,6 +395,7 @@ Read:
|
||||
- render cadence and DeckLink schedule cadence both held roughly 60 fps
|
||||
- app scheduled depth stayed at 4
|
||||
- actual DeckLink buffered depth stayed at 4
|
||||
- DeckLink schedule lead remained positive during healthy playback
|
||||
- no late frames, dropped frames, or schedule failures were observed
|
||||
- completed poll misses were benign because playout remained fully fed
|
||||
|
||||
@@ -407,7 +416,7 @@ This app keeps the same core behavior but splits it into modules that can grow:
|
||||
- `platform/`: COM/Win32/hidden GL context support
|
||||
- `render/`: cadence thread, clock, and simple renderer
|
||||
- `frames/InputFrameMailbox`: non-blocking bounded FIFO CPU input handoff with contiguous-copy fast path for matching row strides
|
||||
- `render/InputFrameTexture`: render-thread-owned upload of the latest CPU input frame into GL, including raw UYVY8 decode into the shader-visible input texture
|
||||
- `render/InputFrameTexture`: render-thread-owned upload of the currently acquired CPU input frame into GL, including raw UYVY8 decode into the shader-visible input texture
|
||||
- `render/readback/`: PBO-backed BGRA8 readback and completed-frame publication
|
||||
- `render/runtime/RuntimeRenderScene`: render-thread-owned GL scene for ready runtime shader layers
|
||||
- `render/runtime/RuntimeShaderPrepareWorker`: shared-context runtime shader program compile/link worker
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -78,7 +78,13 @@ private:
|
||||
message << "DeckLink reported frame timing issue: lateDelta=" << lateDelta
|
||||
<< " droppedDelta=" << droppedDelta
|
||||
<< " totalLate=" << snapshot.displayedLate
|
||||
<< " totalDropped=" << snapshot.dropped;
|
||||
<< " totalDropped=" << snapshot.dropped
|
||||
<< " scheduleLead=";
|
||||
if (snapshot.deckLinkScheduleLeadAvailable)
|
||||
message << snapshot.deckLinkScheduleLeadFrames;
|
||||
else
|
||||
message << "n/a";
|
||||
message << " realignments=" << snapshot.deckLinkScheduleRealignments;
|
||||
LogWarning("telemetry", message.str());
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -10,7 +10,10 @@ The active plan for tightening render-thread ownership is:
|
||||
|
||||
The plan for building a fresh modular app around the proven probe architecture is:
|
||||
|
||||
- [New Render Cadence App Plan](NEW_RENDER_CADENCE_APP_PLAN.md)
|
||||
- [RenderCadenceCompositor README](../apps/RenderCadenceCompositor/README.md)
|
||||
- [Render Cadence Golden Rules](RENDER_CADENCE_GOLDEN_RULES.md)
|
||||
|
||||
`NEW_RENDER_CADENCE_APP_PLAN.md` remains as historical planning context, but the README and golden rules are the current contract for the new cadence-first app.
|
||||
|
||||
## Application Shape
|
||||
|
||||
@@ -287,7 +290,7 @@ Slots have four states:
|
||||
- `Completed`
|
||||
- `Scheduled`
|
||||
|
||||
Completed-but-unscheduled frames are treated as a latest-N cache. If render cadence needs space and old completed frames have not been scheduled, the oldest unscheduled completed frame can be recycled.
|
||||
In the current legacy app, completed-but-unscheduled frames are treated as a latest-N cache. The newer `RenderCadenceCompositor` uses a bounded FIFO completed reserve instead; see its README for the cadence-first contract.
|
||||
|
||||
Scheduled frames are protected until DeckLink reports completion.
|
||||
|
||||
@@ -295,7 +298,7 @@ Scheduled frames are protected until DeckLink reports completion.
|
||||
|
||||
`RenderOutputQueue` holds completed unscheduled output frames waiting to be scheduled.
|
||||
|
||||
It is bounded and latest-N:
|
||||
In the legacy app it is bounded and latest-N:
|
||||
|
||||
- pushing beyond capacity releases/drops the oldest ready frame
|
||||
- `DropOldestFrame()` is used when the frame pool needs to recycle old completed work
|
||||
@@ -363,7 +366,7 @@ The probe does not use the main runtime, shader system, preview path, input uplo
|
||||
- one OpenGL render thread with its own hidden GL context
|
||||
- simple BGRA8 motion rendering
|
||||
- async PBO readback
|
||||
- latest-N system-memory frame slots
|
||||
- legacy latest-N system-memory frame slots; bounded FIFO completed reserve in `RenderCadenceCompositor`
|
||||
- a playout thread that feeds DeckLink
|
||||
- real rendered warmup before scheduled playback
|
||||
|
||||
@@ -531,7 +534,7 @@ When `VST_DISABLE_INPUT_CAPTURE=1`, this flow is skipped.
|
||||
- Keep one owner for each kind of state.
|
||||
- Keep GL work on the render thread.
|
||||
- Keep DeckLink completion callbacks passive.
|
||||
- Treat completed unscheduled output frames as latest-N cache entries.
|
||||
- In the legacy app, treat completed unscheduled output frames as latest-N cache entries; in `RenderCadenceCompositor`, preserve completed frames as a bounded FIFO reserve.
|
||||
- Protect scheduled output frames until DeckLink completion.
|
||||
- Keep output timing more important than preview/screenshot.
|
||||
- Measure timing by domain instead of adding fallback branches blindly.
|
||||
|
||||
@@ -115,6 +115,24 @@ Lesson:
|
||||
- keep synthetic counters only as diagnostics
|
||||
- do not infer device health from internal stream indexes alone
|
||||
|
||||
### Schedule Cursor Recovery Must Be Conservative
|
||||
|
||||
The DeckLink schedule cursor should normally advance as a continuous stream timeline. Continuously realigning the next scheduled stream time to the sampled playback cursor can create its own timing fault: output may look like low FPS even when render and scheduling counters average 59.94/60 fps.
|
||||
|
||||
What worked better:
|
||||
|
||||
- use the exact DeckLink frame duration for the render cadence
|
||||
- keep healthy scheduling on a continuous stream cursor
|
||||
- measure schedule lead from DeckLink playback time versus the next schedule time
|
||||
- realign only after real pressure, such as a late/drop report or dangerously low measured lead
|
||||
- re-arm proactive realignment only after lead has recovered
|
||||
|
||||
Lesson:
|
||||
|
||||
- schedule recovery is an output-edge safety valve, not a per-frame timing policy
|
||||
- if recovery increments continuously, the recovery path has become the problem
|
||||
- include schedule lead and realignment count in telemetry/logs so drift is visible before guessing
|
||||
|
||||
### More Buffer Is Not Automatically Smoother
|
||||
|
||||
Increasing DeckLink scheduled frames sometimes made the reported device buffer look healthier while visible motion still stuttered.
|
||||
@@ -196,7 +214,7 @@ Lesson:
|
||||
|
||||
- system-memory slots are the contract between render and playout
|
||||
- scheduled slots must not be recycled early
|
||||
- completed-but-unscheduled slots can be latest-N cache entries
|
||||
- completed-but-unscheduled slots should form a bounded FIFO reserve for playout
|
||||
|
||||
### Startup Needs Real Preroll
|
||||
|
||||
@@ -222,18 +240,18 @@ Lesson:
|
||||
|
||||
The app has at least two important frame stores:
|
||||
|
||||
- system-memory completed/latest-N frames
|
||||
- system-memory completed FIFO reserve frames
|
||||
- DeckLink scheduled/device buffer
|
||||
|
||||
They have different ownership rules.
|
||||
|
||||
Completed-but-unscheduled frames are disposable if a newer frame is available and cadence needs the slot.
|
||||
Completed-but-unscheduled frames should be a bounded FIFO reserve for playout. If that reserve overflows, dropping the oldest completed frame is an app-side reserve policy and should be counted separately from DeckLink dropped frames.
|
||||
|
||||
Scheduled frames are not disposable because DeckLink may still read them.
|
||||
|
||||
Lesson:
|
||||
|
||||
- latest-N completed frames are a cache
|
||||
- completed frames waiting for playout are a bounded FIFO reserve
|
||||
- scheduled frames are owned by DeckLink until completion
|
||||
- keep metrics for both
|
||||
|
||||
@@ -246,7 +264,8 @@ That couples the clocks again.
|
||||
Lesson:
|
||||
|
||||
- render cadence should keep rendering at selected cadence
|
||||
- if completed cache is full, recycle/drop the oldest unscheduled completed frame
|
||||
- render acquire should not evict completed frames that are waiting for playout
|
||||
- if the completed reserve overflows, drop/count the oldest unscheduled completed frame
|
||||
- only scheduled/in-flight saturation should prevent rendering to a safe slot
|
||||
|
||||
## Render Thread Lessons
|
||||
@@ -340,7 +359,7 @@ The current direction is still sound:
|
||||
```text
|
||||
Render cadence loop
|
||||
renders at selected output cadence
|
||||
writes latest-N completed system-memory frames
|
||||
writes completed system-memory frames into a bounded FIFO reserve
|
||||
never sprints to refill DeckLink
|
||||
|
||||
Frame store
|
||||
@@ -387,7 +406,7 @@ A full rewrite becomes attractive only if the current GL ownership model cannot
|
||||
- Render cadence is time-driven, not completion-driven.
|
||||
- DeckLink scheduling is device-buffer-driven, not render-driven.
|
||||
- Completion callbacks release and report; they do not render.
|
||||
- System-memory completed frames are latest-N cache entries.
|
||||
- System-memory completed frames are a bounded FIFO reserve.
|
||||
- Scheduled frames are protected until DeckLink completion.
|
||||
- Startup uses real rendered warmup/preroll.
|
||||
- Black fallback is degraded/error behavior, not steady-state behavior.
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# New Render Cadence App Plan
|
||||
|
||||
Status: historical implementation plan. `apps/RenderCadenceCompositor` now exists; use [apps/RenderCadenceCompositor/README.md](../apps/RenderCadenceCompositor/README.md) and [Render Cadence Golden Rules](RENDER_CADENCE_GOLDEN_RULES.md) as the current implementation contract.
|
||||
|
||||
This plan describes a new application folder that rebuilds the output path from the proven `DeckLinkRenderCadenceProbe` architecture, but as a maintainable app foundation rather than a monolithic probe file.
|
||||
|
||||
The first goal is not to port the current compositor feature set. The first goal is to reproduce the probe's smooth 59.94/60 fps DeckLink output with clean module boundaries, tests where possible, and a structure that can later accept the shader/runtime/control systems without compromising timing.
|
||||
@@ -43,7 +45,7 @@ Render cadence thread
|
||||
|
||||
System frame exchange
|
||||
-> owns Free / Rendering / Completed / Scheduled slots
|
||||
-> latest-N semantics for completed unscheduled frames
|
||||
-> bounded FIFO reserve for completed unscheduled frames
|
||||
-> protects scheduled frames until DeckLink completion
|
||||
|
||||
DeckLink output thread
|
||||
@@ -63,7 +65,7 @@ Everything else must fit around that spine.
|
||||
- Completion callbacks never render.
|
||||
- No synchronous render request exists in the output path.
|
||||
- Preview, screenshot, input upload, shader rebuild, and runtime control cannot run ahead of a due output frame.
|
||||
- Completed unscheduled frames are latest-N and disposable.
|
||||
- Completed unscheduled frames are a bounded FIFO reserve; overflow drops are counted separately from DeckLink drops.
|
||||
- Scheduled frames are protected until DeckLink completion.
|
||||
- Startup warms up real rendered frames before scheduled playback starts.
|
||||
|
||||
@@ -77,7 +79,7 @@ Keep these behaviors from `DeckLinkRenderCadenceProbe`:
|
||||
- PBO ring readback
|
||||
- non-blocking fence polling with zero timeout
|
||||
- system-memory slots with `Free`, `Rendering`, `Completed`, `Scheduled`
|
||||
- drop oldest completed unscheduled frame if render needs space
|
||||
- preserve completed frames waiting for playout; drop/count the oldest completed frame only if the bounded reserve overflows
|
||||
- DeckLink playout thread only schedules completed frames
|
||||
- warmup completed frames before `StartScheduledPlayback()`
|
||||
- one-line-per-second timing telemetry
|
||||
@@ -430,7 +432,7 @@ Feature set:
|
||||
- simple motion renderer
|
||||
- BGRA8 only
|
||||
- PBO async readback
|
||||
- latest-N system-memory frame exchange
|
||||
- bounded FIFO system-memory frame exchange
|
||||
- warmup before playback
|
||||
- one-line telemetry
|
||||
|
||||
|
||||
@@ -48,6 +48,7 @@ The output/scheduling side may:
|
||||
- release frames after DeckLink completion
|
||||
- report late/dropped/schedule telemetry
|
||||
- record app-side poll misses
|
||||
- conservatively realign the DeckLink schedule cursor after measured timing pressure
|
||||
|
||||
It must not:
|
||||
|
||||
@@ -55,6 +56,7 @@ It must not:
|
||||
- invoke GL
|
||||
- compile shaders
|
||||
- block the render cadence waiting for DeckLink
|
||||
- continuously rewrite healthy scheduled timestamps
|
||||
|
||||
If no completed frame is available, record the miss and keep the ownership boundary intact.
|
||||
|
||||
@@ -93,9 +95,11 @@ Short mutex use for exchanging small already-prepared objects is acceptable. Hol
|
||||
|
||||
## 6. System Memory Frames Are A Handoff, Not A Render Driver
|
||||
|
||||
The system-memory frame exchange stores the latest rendered frames and protects frames scheduled to DeckLink.
|
||||
The system-memory frame exchange stores completed frames as a bounded FIFO reserve and protects frames scheduled to DeckLink.
|
||||
|
||||
It may drop old completed, unscheduled frames when the render thread needs a free slot. It must never force the render thread to wait for the output side to consume a frame.
|
||||
Render acquire must not evict completed frames that are waiting for playout, and it must never force the render thread to wait for the output side to consume a frame.
|
||||
|
||||
If the completed reserve overflows, the exchange may drop the oldest completed, unscheduled frame and record `completedDrops`. That is an app-side reserve drop, not a DeckLink dropped frame.
|
||||
|
||||
## 7. Startup Uses Warmup, Not Burst Rendering
|
||||
|
||||
@@ -114,6 +118,8 @@ Good examples:
|
||||
- `completedPollMisses`
|
||||
- `scheduleFailures`
|
||||
- `decklinkBuffered`
|
||||
- `deckLinkScheduleLeadFrames`
|
||||
- `deckLinkScheduleRealignments`
|
||||
- `inputCaptureFps`
|
||||
- `inputSubmitMs`
|
||||
- `inputUploadMs`
|
||||
|
||||
@@ -98,7 +98,7 @@ render cadence thread
|
||||
-> samples latest render input/state
|
||||
-> renders one frame
|
||||
-> queues async readback/copies completed readback into system-memory slot
|
||||
-> publishes completed frame to latest-N output buffer
|
||||
-> publishes completed frame to bounded FIFO output reserve
|
||||
|
||||
video output thread
|
||||
-> consumes completed system-memory frames
|
||||
|
||||
@@ -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