Compare commits
4 Commits
5c66cfdc64
...
d411453f80
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d411453f80 | ||
|
|
4a049a557a | ||
|
|
13586c611a | ||
|
|
3a83d9617f |
@@ -32,6 +32,17 @@ VideoIOScheduleTime VideoPlayoutScheduler::NextScheduleTime()
|
||||
return time;
|
||||
}
|
||||
|
||||
void VideoPlayoutScheduler::AlignNextScheduleTimeToPlayback(int64_t streamTime, uint64_t leadFrames)
|
||||
{
|
||||
if (mFrameDuration <= 0 || streamTime < 0)
|
||||
return;
|
||||
|
||||
const uint64_t playbackFrameIndex = static_cast<uint64_t>(streamTime / mFrameDuration);
|
||||
const uint64_t minimumScheduleIndex = playbackFrameIndex + leadFrames;
|
||||
if (minimumScheduleIndex > mScheduledFrameIndex)
|
||||
mScheduledFrameIndex = minimumScheduleIndex;
|
||||
}
|
||||
|
||||
VideoPlayoutRecoveryDecision VideoPlayoutScheduler::AccountForCompletionResult(VideoIOCompletionResult result, uint64_t readyQueueDepth)
|
||||
{
|
||||
++mCompletedFrameIndex;
|
||||
|
||||
@@ -12,6 +12,7 @@ public:
|
||||
void Configure(int64_t frameDuration, int64_t timeScale, const VideoPlayoutPolicy& policy);
|
||||
void Reset();
|
||||
VideoIOScheduleTime NextScheduleTime();
|
||||
void AlignNextScheduleTimeToPlayback(int64_t streamTime, uint64_t leadFrames);
|
||||
VideoPlayoutRecoveryDecision AccountForCompletionResult(VideoIOCompletionResult result, uint64_t readyQueueDepth = 0);
|
||||
double FrameBudgetMilliseconds() const;
|
||||
uint64_t ScheduledFrameIndex() const { return mScheduledFrameIndex; }
|
||||
|
||||
@@ -526,13 +526,24 @@ bool DeckLinkSession::PopulateOutputFrame(IDeckLinkMutableVideoFrame* outputVide
|
||||
|
||||
bool DeckLinkSession::ScheduleFrame(IDeckLinkMutableVideoFrame* outputVideoFrame)
|
||||
{
|
||||
const VideoIOScheduleTime scheduleTime = mScheduler.NextScheduleTime();
|
||||
if (outputVideoFrame == nullptr || output == nullptr)
|
||||
{
|
||||
++mState.deckLinkScheduleFailureCount;
|
||||
return false;
|
||||
}
|
||||
|
||||
BMDTimeValue streamTime = 0;
|
||||
double playbackSpeed = 0.0;
|
||||
if (output->GetScheduledStreamTime(mScheduler.TimeScale(), &streamTime, &playbackSpeed) == S_OK && playbackSpeed > 0.0)
|
||||
{
|
||||
RefreshBufferedVideoFrameCount();
|
||||
const uint64_t leadFrames = mState.actualDeckLinkBufferedFramesAvailable
|
||||
? static_cast<uint64_t>(mState.actualDeckLinkBufferedFrames) + 1
|
||||
: static_cast<uint64_t>(mPlayoutPolicy.targetPrerollFrames);
|
||||
mScheduler.AlignNextScheduleTimeToPlayback(streamTime, leadFrames);
|
||||
}
|
||||
|
||||
const VideoIOScheduleTime scheduleTime = mScheduler.NextScheduleTime();
|
||||
const auto scheduleStart = std::chrono::steady_clock::now();
|
||||
const HRESULT result = output->ScheduleVideoFrame(outputVideoFrame, scheduleTime.streamTime, scheduleTime.duration, scheduleTime.timeScale);
|
||||
const auto scheduleEnd = std::chrono::steady_clock::now();
|
||||
|
||||
@@ -11,21 +11,22 @@ Before adding features here, read the guardrails in [Render Cadence Golden Rules
|
||||
```text
|
||||
RenderThread
|
||||
owns a hidden OpenGL context
|
||||
polls latest input frames without waiting
|
||||
polls the oldest ready input frame without waiting
|
||||
uploads input frames into a render-owned GL texture
|
||||
renders simple BGRA8 motion at selected cadence
|
||||
queues async PBO readback
|
||||
publishes completed frames into SystemFrameExchange
|
||||
|
||||
InputFrameMailbox
|
||||
owns latest disposable CPU input slots
|
||||
drops older unsampled input frames when newer frames arrive
|
||||
owns bounded FIFO CPU input slots
|
||||
keeps a bounded three-ready-frame input buffer for render
|
||||
trims frames beyond that bound to avoid runaway input latency
|
||||
protects the one frame currently being uploaded by render
|
||||
uses a single contiguous copy when capture row stride matches mailbox row stride
|
||||
|
||||
SystemFrameExchange
|
||||
owns Free / Rendering / Completed / Scheduled slots
|
||||
drops old completed unscheduled frames when render needs space
|
||||
preserves completed output frames once they are waiting for playout
|
||||
protects scheduled frames until DeckLink completion
|
||||
|
||||
DeckLinkOutputThread
|
||||
@@ -34,7 +35,7 @@ DeckLinkOutputThread
|
||||
never renders
|
||||
```
|
||||
|
||||
Startup warms up real rendered frames before DeckLink scheduled playback starts. When DeckLink input is available, startup also waits briefly for two ready input frames before the render thread starts so the first render ticks are deliberate rather than lucky.
|
||||
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.
|
||||
|
||||
## Current Scope
|
||||
|
||||
@@ -46,13 +47,13 @@ Included now:
|
||||
- hidden render-thread-owned OpenGL context
|
||||
- simple smooth-motion renderer
|
||||
- BGRA8-only output
|
||||
- non-blocking latest-frame input mailbox
|
||||
- non-blocking three-frame FIFO input mailbox for render
|
||||
- fast contiguous mailbox copy path for matching input row strides
|
||||
- bounded two-frame input warmup before render cadence starts
|
||||
- bounded three-frame input warmup before render cadence starts
|
||||
- render-thread-owned input texture upload
|
||||
- async PBO readback
|
||||
- latest-N system-memory frame exchange
|
||||
- rendered-frame warmup
|
||||
- settled completed-frame output reserve before DeckLink preroll, 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
|
||||
@@ -122,9 +123,9 @@ This tracks parity with `apps/LoopThroughWithOpenGLCompositing`.
|
||||
- [x] Trigger parameter pulse count/time for latest trigger events
|
||||
- [x] Optional DeckLink input capture
|
||||
- [x] UYVY8 input capture with render-thread GPU decode to shader input texture
|
||||
- [x] Latest-frame CPU input mailbox
|
||||
- [x] Three-frame FIFO CPU input mailbox for render
|
||||
- [x] Fast contiguous input mailbox copy when source/destination stride matches
|
||||
- [x] Bounded two-frame input warmup before render cadence starts
|
||||
- [x] Bounded three-frame input warmup before render cadence starts
|
||||
- [x] Render-owned input texture upload
|
||||
- [x] Runtime shaders receive input through `gVideoInput`
|
||||
- [x] Live DeckLink input bound to `gVideoInput`
|
||||
@@ -197,6 +198,7 @@ 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.
|
||||
@@ -236,7 +238,7 @@ DeckLink output is an optional edge service in this app.
|
||||
Startup order is:
|
||||
|
||||
1. start render thread
|
||||
2. warm up rendered system-memory frames
|
||||
2. build a settled completed-frame output reserve at normal render cadence
|
||||
3. try to attach DeckLink output
|
||||
4. start telemetry and HTTP either way
|
||||
|
||||
@@ -254,10 +256,10 @@ Startup order is:
|
||||
2. try to attach DeckLink input for the configured input mode
|
||||
3. prefer BGRA8 capture, otherwise accept raw UYVY8 capture and configure the mailbox for UYVY8 bytes
|
||||
4. start `DeckLinkInputThread`
|
||||
5. wait briefly for two ready input warmup frames before starting render cadence
|
||||
5. wait briefly for three ready input warmup frames before starting render cadence
|
||||
6. leave input absent if discovery, setup, format support, or stream startup fails
|
||||
|
||||
`DeckLinkInput` and `DeckLinkInputThread` are deliberately narrow. They capture BGRA8 frames directly or raw UYVY8 frames into `InputFrameMailbox`; they do not call GL, render, preview, screenshot, shader, or output scheduling code. UYVY8-to-RGBA decode happens later inside the render-thread-owned input texture upload path, so the DeckLink callback stays a capture/copy edge only. The mailbox uses one contiguous copy when the capture row stride matches the configured mailbox row stride, and falls back to row-by-row copy only for padded or mismatched frames. Unsupported input modes or formats outside BGRA8/UYVY8 are reported explicitly and treated as an unavailable edge rather than silently converted.
|
||||
`DeckLinkInput` and `DeckLinkInputThread` are deliberately narrow. They capture BGRA8 frames directly or raw UYVY8 frames into `InputFrameMailbox`; they do not call GL, render, preview, screenshot, shader, or output scheduling code. UYVY8-to-RGBA decode happens later inside the render-thread-owned input texture upload path, so the DeckLink callback stays a capture/copy edge only. The render upload path consumes the oldest ready input frame from a bounded three-ready-frame queue, so the input behaves like a small jitter buffer instead of a latest-frame preview mailbox. The mailbox trims older frames beyond that bound to avoid runaway latency, uses one contiguous copy when the capture row stride matches the configured mailbox row stride, and falls back to row-by-row copy only for padded or mismatched frames. Unsupported input modes or formats outside BGRA8/UYVY8 are reported explicitly and treated as an unavailable edge rather than silently converted.
|
||||
|
||||
Input warmup is startup-only and bounded. It may delay render-thread startup for a short window, but it does not add waits to the steady-state render cadence loop.
|
||||
|
||||
@@ -269,10 +271,23 @@ Normal cadence samples are available through `GET /api/state` and are not printe
|
||||
- warning when schedule failures increase
|
||||
- error when the app/DeckLink output buffer is starved
|
||||
|
||||
Render cadence telemetry:
|
||||
|
||||
- `clockOverruns`: render cadence overruns where missed time was detected
|
||||
- `clockSkippedFrames`: selected-cadence frame intervals skipped instead of catch-up rendering
|
||||
- `clockOveruns` / `clockSkipped`: compatibility aliases for quick polling scripts
|
||||
|
||||
Input telemetry:
|
||||
|
||||
- `inputFramesReceived`: frames accepted into `InputFrameMailbox`
|
||||
- `inputFramesDropped`: ready input frames dropped or missed because the mailbox was full
|
||||
- `renderFrameMs`: most recent render-thread draw duration, excluding completed-readback copy and readback queue work
|
||||
- `renderFrameBudgetUsedPercent`: most recent render draw time as a percentage of the selected frame budget
|
||||
- `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
|
||||
- `acquireMisses`: times render/readback could not acquire a writable system-memory frame slot; completed frames waiting for playout are preserved instead of being displaced
|
||||
- `inputConsumeMisses`: render ticks where no ready input frame was available to upload
|
||||
- `inputUploadMisses`: input texture upload attempts that reused the previous GL input texture
|
||||
- `inputReadyFrames`: ready input frames currently queued in `InputFrameMailbox`
|
||||
@@ -391,7 +406,7 @@ This app keeps the same core behavior but splits it into modules that can grow:
|
||||
- `frames/`: system-memory handoff
|
||||
- `platform/`: COM/Win32/hidden GL context support
|
||||
- `render/`: cadence thread, clock, and simple renderer
|
||||
- `frames/InputFrameMailbox`: non-blocking latest-frame CPU input handoff with contiguous-copy fast path for matching row strides
|
||||
- `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/readback/`: PBO-backed BGRA8 readback and completed-frame publication
|
||||
- `render/runtime/RuntimeRenderScene`: render-thread-owned GL scene for ready runtime shader layers
|
||||
|
||||
@@ -107,17 +107,27 @@ int main(int argc, char** argv)
|
||||
inputMailboxConfig.pixelFormat = VideoIOPixelFormat::Bgra8;
|
||||
inputMailboxConfig.rowBytes = VideoIORowBytes(inputMailboxConfig.pixelFormat, inputMailboxConfig.width);
|
||||
inputMailboxConfig.capacity = 4;
|
||||
inputMailboxConfig.maxReadyFrames = 3;
|
||||
InputFrameMailbox inputMailbox(inputMailboxConfig);
|
||||
|
||||
VideoFormat inputVideoMode;
|
||||
VideoFormat outputVideoMode;
|
||||
std::string inputVideoModeError;
|
||||
const bool inputVideoModeResolved = ResolveConfiguredVideoFormat(appConfig.inputVideoFormat, appConfig.inputFrameRate, inputVideoMode);
|
||||
const bool outputVideoModeResolved = ResolveConfiguredVideoFormat(appConfig.outputVideoFormat, appConfig.outputFrameRate, outputVideoMode);
|
||||
if (!inputVideoModeResolved)
|
||||
{
|
||||
inputVideoModeError = "Unsupported DeckLink inputVideoFormat/inputFrameRate in config/runtime-host.json: " +
|
||||
appConfig.inputVideoFormat + " / " + appConfig.inputFrameRate;
|
||||
RenderCadenceCompositor::LogWarning("app", inputVideoModeError);
|
||||
}
|
||||
if (!outputVideoModeResolved)
|
||||
{
|
||||
RenderCadenceCompositor::LogWarning(
|
||||
"app",
|
||||
"Unsupported DeckLink outputVideoFormat/outputFrameRate in config/runtime-host.json; render cadence will use parsed frame-rate fallback: " +
|
||||
appConfig.outputVideoFormat + " / " + appConfig.outputFrameRate);
|
||||
}
|
||||
|
||||
RenderCadenceCompositor::DeckLinkInput deckLinkInput(inputMailbox);
|
||||
RenderCadenceCompositor::DeckLinkInputThread deckLinkInputThread(deckLinkInput);
|
||||
@@ -139,7 +149,7 @@ int main(int argc, char** argv)
|
||||
deckLinkInputStarted = true;
|
||||
RenderCadenceCompositor::Log("app", "DeckLink input edge started for " + inputVideoMode.displayName + ".");
|
||||
RenderCadenceCompositor::Log("app", "Waiting for DeckLink input warmup frames.");
|
||||
constexpr std::size_t kInputStartupBufferedFrames = 2;
|
||||
constexpr std::size_t kInputStartupBufferedFrames = 3;
|
||||
constexpr std::chrono::milliseconds kInputWarmupTimeout(1000);
|
||||
if (WaitForInputWarmup(inputMailbox, kInputStartupBufferedFrames, kInputWarmupTimeout))
|
||||
{
|
||||
@@ -172,7 +182,10 @@ int main(int argc, char** argv)
|
||||
RenderThread::Config renderConfig;
|
||||
renderConfig.width = frameExchangeConfig.width;
|
||||
renderConfig.height = frameExchangeConfig.height;
|
||||
renderConfig.frameDurationMilliseconds = RenderCadenceCompositor::FrameDurationMillisecondsFromRateString(appConfig.outputFrameRate);
|
||||
const double fallbackFrameDurationMilliseconds = RenderCadenceCompositor::FrameDurationMillisecondsFromRateString(appConfig.outputFrameRate);
|
||||
renderConfig.frameDurationMilliseconds = outputVideoModeResolved
|
||||
? RenderCadenceCompositor::FrameDurationMillisecondsFromDisplayMode(outputVideoMode.displayMode, fallbackFrameDurationMilliseconds)
|
||||
: fallbackFrameDurationMilliseconds;
|
||||
renderConfig.pboDepth = 6;
|
||||
|
||||
RenderThread renderThread(frameExchange, &inputMailbox, renderConfig);
|
||||
|
||||
@@ -29,8 +29,9 @@ AppConfig DefaultAppConfig()
|
||||
config.autoReload = true;
|
||||
config.maxTemporalHistoryFrames = 12;
|
||||
config.previewFps = 30.0;
|
||||
config.warmupCompletedFrames = 4;
|
||||
config.warmupCompletedFrames = 8;
|
||||
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,8 +30,9 @@ struct AppConfig
|
||||
bool autoReload = true;
|
||||
std::size_t maxTemporalHistoryFrames = 12;
|
||||
double previewFps = 30.0;
|
||||
std::size_t warmupCompletedFrames = 4;
|
||||
std::size_t warmupCompletedFrames = 8;
|
||||
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";
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
#include <cstdint>
|
||||
#include <cstdlib>
|
||||
#include <fstream>
|
||||
#include <sstream>
|
||||
@@ -133,6 +134,9 @@ 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();
|
||||
@@ -181,6 +185,50 @@ double FrameDurationMillisecondsFromRateString(const std::string& rateText, doub
|
||||
return 1000.0 / rate;
|
||||
}
|
||||
|
||||
double FrameDurationMillisecondsFromDisplayMode(BMDDisplayMode displayMode, double fallbackMilliseconds)
|
||||
{
|
||||
struct ModeRate
|
||||
{
|
||||
BMDDisplayMode mode;
|
||||
int64_t frameDuration;
|
||||
int64_t timeScale;
|
||||
};
|
||||
|
||||
static const ModeRate rates[] =
|
||||
{
|
||||
{ bmdModeHD720p50, 1, 50 },
|
||||
{ bmdModeHD720p5994, 1001, 60000 },
|
||||
{ bmdModeHD720p60, 1, 60 },
|
||||
{ bmdModeHD1080i50, 1, 25 },
|
||||
{ bmdModeHD1080i5994, 1001, 30000 },
|
||||
{ bmdModeHD1080i6000, 1, 30 },
|
||||
{ bmdModeHD1080p2398, 1001, 24000 },
|
||||
{ bmdModeHD1080p24, 1, 24 },
|
||||
{ bmdModeHD1080p25, 1, 25 },
|
||||
{ bmdModeHD1080p2997, 1001, 30000 },
|
||||
{ bmdModeHD1080p30, 1, 30 },
|
||||
{ bmdModeHD1080p50, 1, 50 },
|
||||
{ bmdModeHD1080p5994, 1001, 60000 },
|
||||
{ bmdModeHD1080p6000, 1, 60 },
|
||||
{ bmdMode4K2160p2398, 1001, 24000 },
|
||||
{ bmdMode4K2160p24, 1, 24 },
|
||||
{ bmdMode4K2160p25, 1, 25 },
|
||||
{ bmdMode4K2160p2997, 1001, 30000 },
|
||||
{ bmdMode4K2160p30, 1, 30 },
|
||||
{ bmdMode4K2160p50, 1, 50 },
|
||||
{ bmdMode4K2160p5994, 1001, 60000 },
|
||||
{ bmdMode4K2160p60, 1, 60 }
|
||||
};
|
||||
|
||||
for (const ModeRate& rate : rates)
|
||||
{
|
||||
if (rate.mode == displayMode && rate.timeScale > 0)
|
||||
return (static_cast<double>(rate.frameDuration) * 1000.0) / static_cast<double>(rate.timeScale);
|
||||
}
|
||||
|
||||
return fallbackMilliseconds;
|
||||
}
|
||||
|
||||
void VideoFormatDimensions(const std::string& formatName, unsigned& width, unsigned& height)
|
||||
{
|
||||
std::string normalized = formatName;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#pragma once
|
||||
|
||||
#include "AppConfig.h"
|
||||
#include "DeckLinkDisplayMode.h"
|
||||
|
||||
#include <filesystem>
|
||||
#include <string>
|
||||
@@ -27,6 +28,7 @@ private:
|
||||
};
|
||||
|
||||
double FrameDurationMillisecondsFromRateString(const std::string& rateText, double fallbackRate = 59.94);
|
||||
double FrameDurationMillisecondsFromDisplayMode(BMDDisplayMode displayMode, double fallbackMilliseconds);
|
||||
void VideoFormatDimensions(const std::string& formatName, unsigned& width, unsigned& height);
|
||||
std::filesystem::path FindConfigFile(const std::filesystem::path& relativePath = "config/runtime-host.json");
|
||||
std::filesystem::path FindRepoPath(const std::filesystem::path& relativePath);
|
||||
|
||||
@@ -87,10 +87,8 @@ public:
|
||||
}
|
||||
mRuntimeLayers.StartStartupBuild(mConfig.runtimeShaderId);
|
||||
|
||||
Log("app", "Waiting for rendered warmup frames.");
|
||||
if (!mFrameExchange.WaitForCompletedDepth(mConfig.warmupCompletedFrames, mConfig.warmupTimeout))
|
||||
if (!BuildSettledOutputReserve(error))
|
||||
{
|
||||
error = "Timed out waiting for rendered warmup frames.";
|
||||
LogError("app", error);
|
||||
Stop();
|
||||
return false;
|
||||
@@ -167,6 +165,24 @@ private:
|
||||
Log("app", mVideoOutputStatus);
|
||||
}
|
||||
|
||||
bool BuildSettledOutputReserve(std::string& error)
|
||||
{
|
||||
const auto reserveTimeout = mConfig.warmupTimeout + mConfig.startupSettle + 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))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
error = "Timed out waiting for settled output reserve.";
|
||||
return false;
|
||||
}
|
||||
|
||||
void DisableVideoOutput(const std::string& reason)
|
||||
{
|
||||
mOutputThread.Stop();
|
||||
|
||||
@@ -251,6 +251,7 @@ 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);
|
||||
@@ -283,9 +284,9 @@ inline std::string RuntimeStateToJson(const RuntimeStateJsonInput& input)
|
||||
writer.Key("performance");
|
||||
writer.BeginObject();
|
||||
writer.KeyDouble("frameBudgetMs", FrameDurationMillisecondsFromRateString(input.config.outputFrameRate));
|
||||
writer.KeyNull("renderMs");
|
||||
writer.KeyDouble("renderMs", input.telemetry.renderFrameMilliseconds);
|
||||
writer.KeyNull("smoothedRenderMs");
|
||||
writer.KeyNull("budgetUsedPercent");
|
||||
writer.KeyDouble("budgetUsedPercent", input.telemetry.renderFrameBudgetUsedPercent);
|
||||
writer.KeyNull("completionIntervalMs");
|
||||
writer.KeyNull("smoothedCompletionIntervalMs");
|
||||
writer.KeyNull("maxCompletionIntervalMs");
|
||||
|
||||
@@ -112,6 +112,7 @@ bool InputFrameMailbox::SubmitFrame(const void* bytes, unsigned rowBytes, uint64
|
||||
slot.frameIndex = frameIndex;
|
||||
++slot.generation;
|
||||
mReadyIndices.push_back(slotIndex);
|
||||
TrimReadyFramesLocked();
|
||||
++mCounters.submittedFrames;
|
||||
mCounters.latestFrameIndex = frameIndex;
|
||||
mCounters.hasSubmittedFrame = true;
|
||||
@@ -119,27 +120,16 @@ bool InputFrameMailbox::SubmitFrame(const void* bytes, unsigned rowBytes, uint64
|
||||
return true;
|
||||
}
|
||||
|
||||
bool InputFrameMailbox::TryAcquireLatest(InputFrame& frame)
|
||||
bool InputFrameMailbox::TryAcquireOldest(InputFrame& frame)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mMutex);
|
||||
while (!mReadyIndices.empty())
|
||||
{
|
||||
const std::size_t index = mReadyIndices.back();
|
||||
mReadyIndices.pop_back();
|
||||
const std::size_t index = mReadyIndices.front();
|
||||
mReadyIndices.pop_front();
|
||||
if (index >= mSlots.size() || mSlots[index].state != InputFrameSlotState::Ready)
|
||||
continue;
|
||||
|
||||
while (!mReadyIndices.empty())
|
||||
{
|
||||
const std::size_t olderIndex = mReadyIndices.front();
|
||||
mReadyIndices.pop_front();
|
||||
if (olderIndex >= mSlots.size() || mSlots[olderIndex].state != InputFrameSlotState::Ready)
|
||||
continue;
|
||||
mSlots[olderIndex].state = InputFrameSlotState::Free;
|
||||
++mSlots[olderIndex].generation;
|
||||
++mCounters.droppedReadyFrames;
|
||||
}
|
||||
|
||||
mSlots[index].state = InputFrameSlotState::Reading;
|
||||
FillFrameLocked(index, frame);
|
||||
++mCounters.consumedFrames;
|
||||
@@ -246,6 +236,14 @@ bool InputFrameMailbox::DropOldestReadyLocked()
|
||||
return false;
|
||||
}
|
||||
|
||||
void InputFrameMailbox::TrimReadyFramesLocked()
|
||||
{
|
||||
if (mConfig.maxReadyFrames == 0)
|
||||
return;
|
||||
while (mReadyIndices.size() > mConfig.maxReadyFrames)
|
||||
DropOldestReadyLocked();
|
||||
}
|
||||
|
||||
std::size_t InputFrameMailbox::FrameByteCount() const
|
||||
{
|
||||
return static_cast<std::size_t>(mConfig.rowBytes) * static_cast<std::size_t>(mConfig.height);
|
||||
|
||||
@@ -23,6 +23,7 @@ struct InputFrameMailboxConfig
|
||||
VideoIOPixelFormat pixelFormat = VideoIOPixelFormat::Bgra8;
|
||||
unsigned rowBytes = 0;
|
||||
std::size_t capacity = 0;
|
||||
std::size_t maxReadyFrames = 0;
|
||||
};
|
||||
|
||||
struct InputFrame
|
||||
@@ -63,7 +64,7 @@ public:
|
||||
InputFrameMailboxConfig Config() const;
|
||||
|
||||
bool SubmitFrame(const void* bytes, unsigned rowBytes, uint64_t frameIndex);
|
||||
bool TryAcquireLatest(InputFrame& frame);
|
||||
bool TryAcquireOldest(InputFrame& frame);
|
||||
bool Release(const InputFrame& frame);
|
||||
void Clear();
|
||||
InputFrameMailboxMetrics Metrics() const;
|
||||
@@ -80,6 +81,7 @@ private:
|
||||
bool IsValidLocked(const InputFrame& frame) const;
|
||||
void FillFrameLocked(std::size_t index, InputFrame& frame) const;
|
||||
bool DropOldestReadyLocked();
|
||||
void TrimReadyFramesLocked();
|
||||
std::size_t FrameByteCount() const;
|
||||
|
||||
mutable std::mutex mMutex;
|
||||
|
||||
@@ -46,14 +46,11 @@ bool SystemFrameExchange::AcquireForRender(SystemFrame& frame)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mMutex);
|
||||
if (!AcquireFreeLocked(frame))
|
||||
{
|
||||
if (!DropOldestCompletedLocked() || !AcquireFreeLocked(frame))
|
||||
{
|
||||
frame = SystemFrame();
|
||||
++mCounters.acquireMisses;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
++mCounters.acquiredFrames;
|
||||
return true;
|
||||
@@ -130,6 +127,51 @@ bool SystemFrameExchange::WaitForCompletedDepth(std::size_t targetDepth, std::ch
|
||||
});
|
||||
}
|
||||
|
||||
bool SystemFrameExchange::WaitForStableCompletedDepth(
|
||||
std::size_t targetDepth,
|
||||
std::chrono::milliseconds stableDuration,
|
||||
std::chrono::milliseconds timeout)
|
||||
{
|
||||
if (targetDepth == 0)
|
||||
return true;
|
||||
|
||||
const auto deadline = std::chrono::steady_clock::now() + timeout;
|
||||
std::unique_lock<std::mutex> lock(mMutex);
|
||||
bool stableWindowStarted = false;
|
||||
std::chrono::steady_clock::time_point stableSince;
|
||||
|
||||
while (true)
|
||||
{
|
||||
const auto now = std::chrono::steady_clock::now();
|
||||
if (now >= deadline)
|
||||
return false;
|
||||
|
||||
if (CompletedCountLocked() >= targetDepth)
|
||||
{
|
||||
if (stableDuration <= std::chrono::milliseconds::zero())
|
||||
return true;
|
||||
|
||||
if (!stableWindowStarted)
|
||||
{
|
||||
stableSince = now;
|
||||
stableWindowStarted = true;
|
||||
}
|
||||
|
||||
const auto stableDeadline = stableSince + stableDuration;
|
||||
if (now >= stableDeadline)
|
||||
return true;
|
||||
|
||||
mCondition.wait_until(lock, stableDeadline < deadline ? stableDeadline : deadline);
|
||||
continue;
|
||||
}
|
||||
|
||||
stableWindowStarted = false;
|
||||
mCondition.wait_until(lock, deadline, [&]() {
|
||||
return CompletedCountLocked() >= targetDepth;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void SystemFrameExchange::Clear()
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mMutex);
|
||||
@@ -189,27 +231,6 @@ bool SystemFrameExchange::AcquireFreeLocked(SystemFrame& frame)
|
||||
return false;
|
||||
}
|
||||
|
||||
bool SystemFrameExchange::DropOldestCompletedLocked()
|
||||
{
|
||||
while (!mCompletedIndices.empty())
|
||||
{
|
||||
const std::size_t index = mCompletedIndices.front();
|
||||
mCompletedIndices.pop_front();
|
||||
if (index >= mSlots.size() || mSlots[index].state != SystemFrameSlotState::Completed)
|
||||
continue;
|
||||
|
||||
Slot& slot = mSlots[index];
|
||||
slot.state = SystemFrameSlotState::Free;
|
||||
slot.frameIndex = 0;
|
||||
++slot.generation;
|
||||
++mCounters.completedDrops;
|
||||
mCondition.notify_all();
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
bool SystemFrameExchange::IsValidLocked(const SystemFrame& frame) const
|
||||
{
|
||||
return frame.index < mSlots.size() && mSlots[frame.index].generation == frame.generation;
|
||||
|
||||
@@ -22,6 +22,10 @@ public:
|
||||
bool ConsumeCompletedForSchedule(SystemFrame& frame);
|
||||
bool ReleaseScheduledByBytes(void* bytes);
|
||||
bool WaitForCompletedDepth(std::size_t targetDepth, std::chrono::milliseconds timeout);
|
||||
bool WaitForStableCompletedDepth(
|
||||
std::size_t targetDepth,
|
||||
std::chrono::milliseconds stableDuration,
|
||||
std::chrono::milliseconds timeout);
|
||||
void Clear();
|
||||
|
||||
SystemFrameExchangeMetrics Metrics() const;
|
||||
@@ -36,7 +40,6 @@ private:
|
||||
};
|
||||
|
||||
bool AcquireFreeLocked(SystemFrame& frame);
|
||||
bool DropOldestCompletedLocked();
|
||||
bool IsValidLocked(const SystemFrame& frame) const;
|
||||
void FillFrameLocked(std::size_t index, SystemFrame& frame);
|
||||
std::size_t CompletedCountLocked() const;
|
||||
|
||||
@@ -71,7 +71,7 @@ GLuint InputFrameTexture::PollAndUpload(InputFrameMailbox* mailbox)
|
||||
return mTexture;
|
||||
|
||||
InputFrame frame;
|
||||
if (!mailbox->TryAcquireLatest(frame))
|
||||
if (!mailbox->TryAcquireOldest(frame))
|
||||
{
|
||||
++mUploadMisses;
|
||||
mLastUploadMilliseconds = 0.0;
|
||||
|
||||
@@ -13,6 +13,7 @@ RenderCadenceClock::RenderCadenceClock(double frameDurationMilliseconds)
|
||||
void RenderCadenceClock::Reset(TimePoint now)
|
||||
{
|
||||
mNextRenderTime = now;
|
||||
mPendingFrameAdvance = 1;
|
||||
mOverrunCount = 0;
|
||||
mSkippedFrameCount = 0;
|
||||
}
|
||||
@@ -27,10 +28,12 @@ RenderCadenceClock::Tick RenderCadenceClock::Poll(TimePoint now)
|
||||
}
|
||||
|
||||
tick.due = true;
|
||||
mPendingFrameAdvance = 1;
|
||||
const Duration lateBy = now - mNextRenderTime;
|
||||
if (lateBy > mFrameDuration)
|
||||
{
|
||||
tick.skippedFrames = static_cast<uint64_t>(lateBy / mFrameDuration);
|
||||
mPendingFrameAdvance += tick.skippedFrames;
|
||||
++mOverrunCount;
|
||||
mSkippedFrameCount += tick.skippedFrames;
|
||||
}
|
||||
@@ -39,7 +42,8 @@ RenderCadenceClock::Tick RenderCadenceClock::Poll(TimePoint now)
|
||||
|
||||
void RenderCadenceClock::MarkRendered(TimePoint now)
|
||||
{
|
||||
mNextRenderTime += mFrameDuration;
|
||||
mNextRenderTime += mFrameDuration * mPendingFrameAdvance;
|
||||
mPendingFrameAdvance = 1;
|
||||
if (now - mNextRenderTime > mFrameDuration * 4)
|
||||
mNextRenderTime = now + mFrameDuration;
|
||||
}
|
||||
|
||||
@@ -31,6 +31,7 @@ public:
|
||||
private:
|
||||
Duration mFrameDuration;
|
||||
TimePoint mNextRenderTime = Clock::now();
|
||||
uint64_t mPendingFrameAdvance = 1;
|
||||
uint64_t mOverrunCount = 0;
|
||||
uint64_t mSkippedFrameCount = 0;
|
||||
};
|
||||
|
||||
@@ -85,6 +85,11 @@ RenderThread::Metrics RenderThread::GetMetrics() const
|
||||
metrics.skippedFrames = mSkippedFrames.load(std::memory_order_relaxed);
|
||||
metrics.shaderBuildsCommitted = mShaderBuildsCommitted.load(std::memory_order_relaxed);
|
||||
metrics.shaderBuildFailures = mShaderBuildFailures.load(std::memory_order_relaxed);
|
||||
metrics.renderFrameMilliseconds = mRenderFrameMilliseconds.load(std::memory_order_relaxed);
|
||||
metrics.renderFrameBudgetUsedPercent = mRenderFrameBudgetUsedPercent.load(std::memory_order_relaxed);
|
||||
metrics.renderFrameMaxMilliseconds = mRenderFrameMaxMilliseconds.load(std::memory_order_relaxed);
|
||||
metrics.readbackQueueMilliseconds = mReadbackQueueMilliseconds.load(std::memory_order_relaxed);
|
||||
metrics.completedReadbackCopyMilliseconds = mCompletedReadbackCopyMilliseconds.load(std::memory_order_relaxed);
|
||||
metrics.inputFramesReceived = mInputFramesReceived.load(std::memory_order_relaxed);
|
||||
metrics.inputFramesDropped = mInputFramesDropped.load(std::memory_order_relaxed);
|
||||
metrics.inputConsumeMisses = mInputConsumeMisses.load(std::memory_order_relaxed);
|
||||
@@ -154,6 +159,7 @@ void RenderThread::ThreadMain()
|
||||
CountAcquireMiss();
|
||||
},
|
||||
[this]() { CountCompleted(); });
|
||||
PublishReadbackMetrics(readback);
|
||||
|
||||
const auto now = RenderCadenceClock::Clock::now();
|
||||
const RenderCadenceClock::Tick tick = clock.Poll(now);
|
||||
@@ -178,6 +184,7 @@ void RenderThread::ThreadMain()
|
||||
{
|
||||
mPboQueueMisses.fetch_add(1, std::memory_order_relaxed);
|
||||
}
|
||||
PublishReadbackMetrics(readback);
|
||||
|
||||
CountRendered();
|
||||
++frameIndex;
|
||||
@@ -196,6 +203,7 @@ void RenderThread::ThreadMain()
|
||||
CountAcquireMiss();
|
||||
},
|
||||
[this]() { CountCompleted(); });
|
||||
PublishReadbackMetrics(readback);
|
||||
}
|
||||
|
||||
readback.Shutdown();
|
||||
@@ -237,6 +245,29 @@ void RenderThread::CountAcquireMiss()
|
||||
mAcquireMisses.fetch_add(1, std::memory_order_relaxed);
|
||||
}
|
||||
|
||||
void RenderThread::PublishReadbackMetrics(const Bgra8ReadbackPipeline& readback)
|
||||
{
|
||||
const double renderMilliseconds = readback.LastRenderFrameMilliseconds();
|
||||
mRenderFrameMilliseconds.store(renderMilliseconds, std::memory_order_relaxed);
|
||||
if (mConfig.frameDurationMilliseconds > 0.0)
|
||||
{
|
||||
mRenderFrameBudgetUsedPercent.store(
|
||||
(renderMilliseconds / mConfig.frameDurationMilliseconds) * 100.0,
|
||||
std::memory_order_relaxed);
|
||||
}
|
||||
else
|
||||
{
|
||||
mRenderFrameBudgetUsedPercent.store(0.0, std::memory_order_relaxed);
|
||||
}
|
||||
|
||||
const double previousMax = mRenderFrameMaxMilliseconds.load(std::memory_order_relaxed);
|
||||
if (renderMilliseconds > previousMax)
|
||||
mRenderFrameMaxMilliseconds.store(renderMilliseconds, std::memory_order_relaxed);
|
||||
|
||||
mReadbackQueueMilliseconds.store(readback.LastReadbackQueueMilliseconds(), std::memory_order_relaxed);
|
||||
mCompletedReadbackCopyMilliseconds.store(readback.LastCompletedReadbackCopyMilliseconds(), std::memory_order_relaxed);
|
||||
}
|
||||
|
||||
void RenderThread::PublishInputMetrics(const InputFrameTexture& inputTexture)
|
||||
{
|
||||
if (mInputMailbox != nullptr)
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
class SystemFrameExchange;
|
||||
class InputFrameMailbox;
|
||||
class InputFrameTexture;
|
||||
class Bgra8ReadbackPipeline;
|
||||
|
||||
class RenderThread
|
||||
{
|
||||
@@ -38,6 +39,11 @@ public:
|
||||
uint64_t skippedFrames = 0;
|
||||
uint64_t shaderBuildsCommitted = 0;
|
||||
uint64_t shaderBuildFailures = 0;
|
||||
double renderFrameMilliseconds = 0.0;
|
||||
double renderFrameBudgetUsedPercent = 0.0;
|
||||
double renderFrameMaxMilliseconds = 0.0;
|
||||
double readbackQueueMilliseconds = 0.0;
|
||||
double completedReadbackCopyMilliseconds = 0.0;
|
||||
uint64_t inputFramesReceived = 0;
|
||||
uint64_t inputFramesDropped = 0;
|
||||
uint64_t inputConsumeMisses = 0;
|
||||
@@ -71,6 +77,7 @@ private:
|
||||
void CountRendered();
|
||||
void CountCompleted();
|
||||
void CountAcquireMiss();
|
||||
void PublishReadbackMetrics(const Bgra8ReadbackPipeline& readback);
|
||||
void PublishInputMetrics(const InputFrameTexture& inputTexture);
|
||||
void TryCommitReadyRuntimeShader(RuntimeRenderScene& runtimeRenderScene);
|
||||
bool TryTakePendingRuntimeShaderArtifact(RuntimeShaderArtifact& artifact);
|
||||
@@ -96,6 +103,11 @@ private:
|
||||
std::atomic<uint64_t> mSkippedFrames{ 0 };
|
||||
std::atomic<uint64_t> mShaderBuildsCommitted{ 0 };
|
||||
std::atomic<uint64_t> mShaderBuildFailures{ 0 };
|
||||
std::atomic<double> mRenderFrameMilliseconds{ 0.0 };
|
||||
std::atomic<double> mRenderFrameBudgetUsedPercent{ 0.0 };
|
||||
std::atomic<double> mRenderFrameMaxMilliseconds{ 0.0 };
|
||||
std::atomic<double> mReadbackQueueMilliseconds{ 0.0 };
|
||||
std::atomic<double> mCompletedReadbackCopyMilliseconds{ 0.0 };
|
||||
std::atomic<uint64_t> mInputFramesReceived{ 0 };
|
||||
std::atomic<uint64_t> mInputFramesDropped{ 0 };
|
||||
std::atomic<uint64_t> mInputConsumeMisses{ 0 };
|
||||
|
||||
@@ -2,8 +2,18 @@
|
||||
|
||||
#include "../frames/SystemFrameTypes.h"
|
||||
|
||||
#include <chrono>
|
||||
#include <cstring>
|
||||
|
||||
namespace
|
||||
{
|
||||
double MillisecondsSince(std::chrono::steady_clock::time_point start)
|
||||
{
|
||||
return std::chrono::duration_cast<std::chrono::duration<double, std::milli>>(
|
||||
std::chrono::steady_clock::now() - start).count();
|
||||
}
|
||||
}
|
||||
|
||||
Bgra8ReadbackPipeline::~Bgra8ReadbackPipeline()
|
||||
{
|
||||
Shutdown();
|
||||
@@ -50,10 +60,15 @@ bool Bgra8ReadbackPipeline::RenderAndQueue(uint64_t frameIndex, const RenderCall
|
||||
return false;
|
||||
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, mFramebuffer);
|
||||
const auto renderStart = std::chrono::steady_clock::now();
|
||||
renderFrame(frameIndex);
|
||||
mLastRenderFrameMilliseconds = MillisecondsSince(renderStart);
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||
|
||||
return mPboRing.QueueReadback(mFramebuffer, mWidth, mHeight, frameIndex);
|
||||
const auto queueStart = std::chrono::steady_clock::now();
|
||||
const bool queued = mPboRing.QueueReadback(mFramebuffer, mWidth, mHeight, frameIndex);
|
||||
mLastReadbackQueueMilliseconds = MillisecondsSince(queueStart);
|
||||
return queued;
|
||||
}
|
||||
|
||||
void Bgra8ReadbackPipeline::ConsumeCompleted(
|
||||
@@ -68,12 +83,14 @@ void Bgra8ReadbackPipeline::ConsumeCompleted(
|
||||
PboReadbackRing::CompletedReadback readback;
|
||||
while (mPboRing.TryAcquireCompleted(readback))
|
||||
{
|
||||
const auto copyStart = std::chrono::steady_clock::now();
|
||||
glBindBuffer(GL_PIXEL_PACK_BUFFER, readback.pbo);
|
||||
void* mapped = glMapBuffer(GL_PIXEL_PACK_BUFFER, GL_READ_ONLY);
|
||||
if (!mapped)
|
||||
{
|
||||
glBindBuffer(GL_PIXEL_PACK_BUFFER, 0);
|
||||
mPboRing.ReleaseCompleted(readback);
|
||||
mLastCompletedReadbackCopyMilliseconds = MillisecondsSince(copyStart);
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -99,6 +116,7 @@ void Bgra8ReadbackPipeline::ConsumeCompleted(
|
||||
glUnmapBuffer(GL_PIXEL_PACK_BUFFER);
|
||||
glBindBuffer(GL_PIXEL_PACK_BUFFER, 0);
|
||||
mPboRing.ReleaseCompleted(readback);
|
||||
mLastCompletedReadbackCopyMilliseconds = MillisecondsSince(copyStart);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -38,6 +38,9 @@ public:
|
||||
unsigned RowBytes() const { return mRowBytes; }
|
||||
VideoIOPixelFormat PixelFormat() const { return VideoIOPixelFormat::Bgra8; }
|
||||
uint64_t PboQueueMisses() const { return mPboRing.QueueMisses(); }
|
||||
double LastRenderFrameMilliseconds() const { return mLastRenderFrameMilliseconds; }
|
||||
double LastReadbackQueueMilliseconds() const { return mLastReadbackQueueMilliseconds; }
|
||||
double LastCompletedReadbackCopyMilliseconds() const { return mLastCompletedReadbackCopyMilliseconds; }
|
||||
|
||||
private:
|
||||
bool CreateRenderTarget();
|
||||
@@ -48,5 +51,8 @@ private:
|
||||
unsigned mRowBytes = 0;
|
||||
GLuint mFramebuffer = 0;
|
||||
GLuint mTexture = 0;
|
||||
double mLastRenderFrameMilliseconds = 0.0;
|
||||
double mLastReadbackQueueMilliseconds = 0.0;
|
||||
double mLastCompletedReadbackCopyMilliseconds = 0.0;
|
||||
PboReadbackRing mPboRing;
|
||||
};
|
||||
|
||||
@@ -65,7 +65,7 @@ void RuntimeRenderScene::RenderFrame(uint64_t frameIndex, unsigned width, unsign
|
||||
}
|
||||
|
||||
// Shader source contract:
|
||||
// - gVideoInput is the decoded latest input texture for every layer in the stack.
|
||||
// - gVideoInput is the decoded current input texture for every layer in the stack.
|
||||
// - gLayerInput starts as gVideoInput for the first layer, then becomes the previous layer output.
|
||||
GLuint layerInputTexture = videoInputTexture;
|
||||
std::size_t nextTargetIndex = 0;
|
||||
|
||||
@@ -19,11 +19,20 @@ struct CadenceTelemetrySnapshot
|
||||
uint64_t scheduledTotal = 0;
|
||||
uint64_t completedPollMisses = 0;
|
||||
uint64_t scheduleFailures = 0;
|
||||
uint64_t completedDrops = 0;
|
||||
uint64_t acquireMisses = 0;
|
||||
uint64_t completions = 0;
|
||||
uint64_t displayedLate = 0;
|
||||
uint64_t dropped = 0;
|
||||
uint64_t clockOverruns = 0;
|
||||
uint64_t clockSkippedFrames = 0;
|
||||
uint64_t shaderBuildsCommitted = 0;
|
||||
uint64_t shaderBuildFailures = 0;
|
||||
double renderFrameMilliseconds = 0.0;
|
||||
double renderFrameBudgetUsedPercent = 0.0;
|
||||
double renderFrameMaxMilliseconds = 0.0;
|
||||
double readbackQueueMilliseconds = 0.0;
|
||||
double completedReadbackCopyMilliseconds = 0.0;
|
||||
uint64_t inputFramesReceived = 0;
|
||||
uint64_t inputFramesDropped = 0;
|
||||
uint64_t inputConsumeMisses = 0;
|
||||
@@ -75,6 +84,8 @@ public:
|
||||
snapshot.scheduleFailures = outputMetrics.scheduleFailures > threadMetrics.scheduleFailures
|
||||
? outputMetrics.scheduleFailures
|
||||
: threadMetrics.scheduleFailures;
|
||||
snapshot.completedDrops = exchangeMetrics.completedDrops;
|
||||
snapshot.acquireMisses = exchangeMetrics.acquireMisses;
|
||||
snapshot.completions = outputMetrics.completions;
|
||||
snapshot.displayedLate = outputMetrics.displayedLate;
|
||||
snapshot.dropped = outputMetrics.dropped;
|
||||
@@ -104,8 +115,15 @@ public:
|
||||
{
|
||||
CadenceTelemetrySnapshot snapshot = Sample(exchange, output, outputThread);
|
||||
const auto renderMetrics = renderThread.GetMetrics();
|
||||
snapshot.clockOverruns = renderMetrics.clockOverruns;
|
||||
snapshot.clockSkippedFrames = renderMetrics.skippedFrames;
|
||||
snapshot.shaderBuildsCommitted = renderMetrics.shaderBuildsCommitted;
|
||||
snapshot.shaderBuildFailures = renderMetrics.shaderBuildFailures;
|
||||
snapshot.renderFrameMilliseconds = renderMetrics.renderFrameMilliseconds;
|
||||
snapshot.renderFrameBudgetUsedPercent = renderMetrics.renderFrameBudgetUsedPercent;
|
||||
snapshot.renderFrameMaxMilliseconds = renderMetrics.renderFrameMaxMilliseconds;
|
||||
snapshot.readbackQueueMilliseconds = renderMetrics.readbackQueueMilliseconds;
|
||||
snapshot.completedReadbackCopyMilliseconds = renderMetrics.completedReadbackCopyMilliseconds;
|
||||
snapshot.inputFramesReceived = renderMetrics.inputFramesReceived;
|
||||
snapshot.inputFramesDropped = renderMetrics.inputFramesDropped;
|
||||
snapshot.inputConsumeMisses = renderMetrics.inputConsumeMisses;
|
||||
|
||||
@@ -21,11 +21,22 @@ inline void WriteCadenceTelemetryJson(JsonWriter& writer, const CadenceTelemetry
|
||||
writer.KeyUInt("scheduledTotal", snapshot.scheduledTotal);
|
||||
writer.KeyUInt("completedPollMisses", snapshot.completedPollMisses);
|
||||
writer.KeyUInt("scheduleFailures", snapshot.scheduleFailures);
|
||||
writer.KeyUInt("completedDrops", snapshot.completedDrops);
|
||||
writer.KeyUInt("acquireMisses", snapshot.acquireMisses);
|
||||
writer.KeyUInt("completions", snapshot.completions);
|
||||
writer.KeyUInt("late", snapshot.displayedLate);
|
||||
writer.KeyUInt("dropped", snapshot.dropped);
|
||||
writer.KeyUInt("clockOverruns", snapshot.clockOverruns);
|
||||
writer.KeyUInt("clockSkippedFrames", snapshot.clockSkippedFrames);
|
||||
writer.KeyUInt("clockOveruns", snapshot.clockOverruns);
|
||||
writer.KeyUInt("clockSkipped", snapshot.clockSkippedFrames);
|
||||
writer.KeyUInt("shaderCommitted", snapshot.shaderBuildsCommitted);
|
||||
writer.KeyUInt("shaderFailures", snapshot.shaderBuildFailures);
|
||||
writer.KeyDouble("renderFrameMs", snapshot.renderFrameMilliseconds);
|
||||
writer.KeyDouble("renderFrameBudgetUsedPercent", snapshot.renderFrameBudgetUsedPercent);
|
||||
writer.KeyDouble("renderFrameMaxMs", snapshot.renderFrameMaxMilliseconds);
|
||||
writer.KeyDouble("readbackQueueMs", snapshot.readbackQueueMilliseconds);
|
||||
writer.KeyDouble("completedReadbackCopyMs", snapshot.completedReadbackCopyMilliseconds);
|
||||
writer.KeyUInt("inputFramesReceived", snapshot.inputFramesReceived);
|
||||
writer.KeyUInt("inputFramesDropped", snapshot.inputFramesDropped);
|
||||
writer.KeyUInt("inputConsumeMisses", snapshot.inputConsumeMisses);
|
||||
|
||||
@@ -82,7 +82,6 @@ private:
|
||||
std::this_thread::sleep_for(mConfig.idleSleep);
|
||||
continue;
|
||||
}
|
||||
|
||||
SystemFrame frame;
|
||||
if (!mExchange.ConsumeCompletedForSchedule(frame))
|
||||
{
|
||||
|
||||
@@ -11,5 +11,6 @@
|
||||
"autoReload": true,
|
||||
"maxTemporalHistoryFrames": 12,
|
||||
"previewFps": 30,
|
||||
"startupSettleMs": 5000,
|
||||
"enableExternalKeying": true
|
||||
}
|
||||
|
||||
@@ -557,6 +557,8 @@ components:
|
||||
type: number
|
||||
previewFps:
|
||||
type: number
|
||||
startupSettleMs:
|
||||
type: number
|
||||
enableExternalKeying:
|
||||
type: boolean
|
||||
inputVideoFormat:
|
||||
@@ -633,10 +635,12 @@ components:
|
||||
type: number
|
||||
renderMs:
|
||||
type: number
|
||||
description: Alias of cadence.renderFrameMs for clients that read the top-level performance object.
|
||||
smoothedRenderMs:
|
||||
type: number
|
||||
budgetUsedPercent:
|
||||
type: number
|
||||
description: Alias of cadence.renderFrameBudgetUsedPercent for clients that read the top-level performance object.
|
||||
completionIntervalMs:
|
||||
type: number
|
||||
smoothedCompletionIntervalMs:
|
||||
@@ -654,6 +658,41 @@ components:
|
||||
CadenceTelemetry:
|
||||
type: object
|
||||
properties:
|
||||
clockOverruns:
|
||||
type: number
|
||||
description: Render cadence overruns where the render thread was late enough to skip one or more frame intervals.
|
||||
clockSkippedFrames:
|
||||
type: number
|
||||
description: Total render cadence frame intervals skipped instead of catch-up rendering.
|
||||
clockOveruns:
|
||||
type: number
|
||||
deprecated: true
|
||||
description: Deprecated misspelled alias for clockOverruns.
|
||||
clockSkipped:
|
||||
type: number
|
||||
deprecated: true
|
||||
description: Deprecated alias for clockSkippedFrames.
|
||||
renderFrameMs:
|
||||
type: number
|
||||
description: Most recent render-thread frame draw duration in milliseconds, excluding completed-readback copy and readback queue work.
|
||||
renderFrameBudgetUsedPercent:
|
||||
type: number
|
||||
description: Most recent render-thread frame draw duration as a percentage of the selected frame budget.
|
||||
renderFrameMaxMs:
|
||||
type: number
|
||||
description: Maximum observed render-thread frame draw duration in milliseconds for this process.
|
||||
readbackQueueMs:
|
||||
type: number
|
||||
description: Most recent duration spent queueing BGRA8 async PBO readback after rendering.
|
||||
completedReadbackCopyMs:
|
||||
type: number
|
||||
description: Most recent duration spent mapping and copying a completed BGRA8 readback into system-memory frame storage.
|
||||
completedDrops:
|
||||
type: number
|
||||
description: Number of completed unscheduled system-memory frames dropped so render could reuse the slot.
|
||||
acquireMisses:
|
||||
type: number
|
||||
description: Number of times render/readback could not acquire a writable system-memory frame slot.
|
||||
inputFramesReceived:
|
||||
type: number
|
||||
inputFramesDropped:
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#include "AppConfigProvider.h"
|
||||
|
||||
#include <chrono>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <iostream>
|
||||
@@ -36,6 +37,7 @@ std::filesystem::path WriteConfigFixture()
|
||||
<< " \"autoReload\": false,\n"
|
||||
<< " \"maxTemporalHistoryFrames\": 8,\n"
|
||||
<< " \"previewFps\": 24,\n"
|
||||
<< " \"startupSettleMs\": 2500,\n"
|
||||
<< " \"enableExternalKeying\": true\n"
|
||||
<< "}\n";
|
||||
return path;
|
||||
@@ -66,6 +68,7 @@ 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);
|
||||
@@ -104,6 +107,8 @@ void TestHelpers()
|
||||
|
||||
const double duration = FrameDurationMillisecondsFromRateString("50");
|
||||
Expect(duration > 19.9 && duration < 20.1, "frame duration parses numeric rate");
|
||||
const double deckLinkDuration = FrameDurationMillisecondsFromDisplayMode(bmdModeHD1080p5994, 0.0);
|
||||
Expect(deckLinkDuration > 16.6833 && deckLinkDuration < 16.6834, "DeckLink 59.94 display mode duration is exact");
|
||||
|
||||
const std::filesystem::path configPath = FindConfigFile();
|
||||
Expect(!configPath.empty(), "default config is discoverable from test working directory");
|
||||
|
||||
@@ -60,6 +60,27 @@ void TestLatePollRecordsSkippedFrames()
|
||||
Expect(cadence.SkippedFrameCount() == 3, "late poll accumulates skipped frames");
|
||||
}
|
||||
|
||||
void TestLatePollSkipsMissedIntervalsInsteadOfCatchingUp()
|
||||
{
|
||||
using Clock = RenderCadenceClock::Clock;
|
||||
RenderCadenceClock cadence(10.0);
|
||||
const auto start = Clock::now();
|
||||
cadence.Reset(start);
|
||||
|
||||
const auto late = start + std::chrono::milliseconds(35);
|
||||
const auto tick = cadence.Poll(late);
|
||||
Expect(tick.due, "late skipped-interval poll is due");
|
||||
Expect(tick.skippedFrames == 3, "late skipped-interval poll counts missed frames");
|
||||
|
||||
cadence.MarkRendered(late);
|
||||
Expect(cadence.NextRenderTime() > late, "late render schedules the next tick in the future");
|
||||
Expect(cadence.NextRenderTime() - late <= std::chrono::milliseconds(6), "late render does not leave catch-up frames due immediately");
|
||||
|
||||
const auto immediateFollowup = cadence.Poll(late);
|
||||
Expect(!immediateFollowup.due, "cadence does not allow an immediate catch-up render after a late frame");
|
||||
Expect(immediateFollowup.sleepFor > RenderCadenceClock::Duration::zero(), "cadence reports wait time after skipping missed intervals");
|
||||
}
|
||||
|
||||
void TestMarkRenderedRebasesAfterLargeStall()
|
||||
{
|
||||
using Clock = RenderCadenceClock::Clock;
|
||||
@@ -81,6 +102,7 @@ int main()
|
||||
TestEarlyPollWaitsWithoutAdvancing();
|
||||
TestDuePollRendersWithoutSkipping();
|
||||
TestLatePollRecordsSkippedFrames();
|
||||
TestLatePollSkipsMissedIntervalsInsteadOfCatchingUp();
|
||||
TestMarkRenderedRebasesAfterLargeStall();
|
||||
|
||||
if (gFailures != 0)
|
||||
|
||||
@@ -57,32 +57,29 @@ void TestAcquirePublishesAndSchedules()
|
||||
Expect(metrics.scheduledFrames == 1, "scheduled metric is counted");
|
||||
}
|
||||
|
||||
void TestAcquireDropsOldestCompletedUnscheduled()
|
||||
void TestAcquirePreservesCompletedFrames()
|
||||
{
|
||||
SystemFrameExchange exchange(MakeConfig(2));
|
||||
|
||||
SystemFrame first;
|
||||
SystemFrame second;
|
||||
SystemFrame third;
|
||||
Expect(exchange.AcquireForRender(first), "first frame can be acquired");
|
||||
Expect(exchange.AcquireForRender(first), "first preserving frame can be acquired");
|
||||
first.frameIndex = 1;
|
||||
Expect(exchange.PublishCompleted(first), "first frame can be completed");
|
||||
Expect(exchange.AcquireForRender(second), "second frame can be acquired");
|
||||
Expect(exchange.PublishCompleted(first), "first preserving frame can be completed");
|
||||
Expect(exchange.AcquireForRender(second), "second preserving frame can be acquired");
|
||||
second.frameIndex = 2;
|
||||
Expect(exchange.PublishCompleted(second), "second frame can be completed");
|
||||
Expect(exchange.PublishCompleted(second), "second preserving frame can be completed");
|
||||
|
||||
Expect(exchange.AcquireForRender(third), "third acquire drops the oldest completed frame");
|
||||
Expect(third.index == first.index, "oldest completed slot is reused");
|
||||
Expect(!exchange.AcquireForRender(third), "render acquire refuses to drop completed frames");
|
||||
|
||||
SystemFrame scheduled;
|
||||
Expect(exchange.ConsumeCompletedForSchedule(scheduled), "remaining completed frame can be scheduled");
|
||||
Expect(scheduled.index == second.index, "newer completed frame survives drop");
|
||||
Expect(scheduled.frameIndex == 2, "newer frame index survives drop");
|
||||
Expect(exchange.ConsumeCompletedForSchedule(scheduled), "oldest completed frame survives acquire miss");
|
||||
Expect(scheduled.frameIndex == 1, "preserving acquire keeps FIFO output continuity");
|
||||
|
||||
SystemFrameExchangeMetrics metrics = exchange.Metrics();
|
||||
Expect(metrics.completedDrops == 1, "drop metric is counted");
|
||||
Expect(metrics.renderingCount == 1, "reused slot is rendering");
|
||||
Expect(metrics.scheduledCount == 1, "consumed slot is scheduled");
|
||||
Expect(metrics.completedDrops == 0, "preserving acquire does not count completed drops");
|
||||
Expect(metrics.acquireMisses == 1, "preserving acquire miss is counted");
|
||||
}
|
||||
|
||||
void TestScheduledFramesAreNotDropped()
|
||||
@@ -154,16 +151,38 @@ void TestCompletedPollMissIsCounted()
|
||||
SystemFrameExchangeMetrics metrics = exchange.Metrics();
|
||||
Expect(metrics.completedPollMisses == 1, "completed poll miss is counted");
|
||||
}
|
||||
|
||||
void TestStableCompletedDepthCanBeObserved()
|
||||
{
|
||||
SystemFrameExchange exchange(MakeConfig(1));
|
||||
SystemFrame frame;
|
||||
Expect(exchange.AcquireForRender(frame), "stable-depth frame can be acquired");
|
||||
Expect(exchange.PublishCompleted(frame), "stable-depth frame can be completed");
|
||||
|
||||
Expect(
|
||||
exchange.WaitForStableCompletedDepth(1, std::chrono::milliseconds(1), std::chrono::milliseconds(50)),
|
||||
"stable completed depth can be observed");
|
||||
}
|
||||
|
||||
void TestStableCompletedDepthTimesOut()
|
||||
{
|
||||
SystemFrameExchange exchange(MakeConfig(1));
|
||||
Expect(
|
||||
!exchange.WaitForStableCompletedDepth(1, std::chrono::milliseconds(1), std::chrono::milliseconds(1)),
|
||||
"missing stable completed depth times out");
|
||||
}
|
||||
}
|
||||
|
||||
int main()
|
||||
{
|
||||
TestAcquirePublishesAndSchedules();
|
||||
TestAcquireDropsOldestCompletedUnscheduled();
|
||||
TestAcquirePreservesCompletedFrames();
|
||||
TestScheduledFramesAreNotDropped();
|
||||
TestGenerationValidationRejectsStaleFrames();
|
||||
TestPixelFormatAwareSizing();
|
||||
TestCompletedPollMissIsCounted();
|
||||
TestStableCompletedDepthCanBeObserved();
|
||||
TestStableCompletedDepthTimesOut();
|
||||
|
||||
if (gFailures != 0)
|
||||
{
|
||||
|
||||
@@ -29,33 +29,18 @@ InputFrameMailboxConfig MakeConfig(std::size_t capacity = 2)
|
||||
return config;
|
||||
}
|
||||
|
||||
InputFrameMailboxConfig MakeBufferedConfig(std::size_t capacity = 4, std::size_t maxReadyFrames = 2)
|
||||
{
|
||||
InputFrameMailboxConfig config = MakeConfig(capacity);
|
||||
config.maxReadyFrames = maxReadyFrames;
|
||||
return config;
|
||||
}
|
||||
|
||||
std::vector<unsigned char> MakeFrame(unsigned char value)
|
||||
{
|
||||
return std::vector<unsigned char>(16, value);
|
||||
}
|
||||
|
||||
void TestAcquireLatestDropsOlderReadyFrames()
|
||||
{
|
||||
InputFrameMailbox mailbox(MakeConfig(3));
|
||||
const std::vector<unsigned char> frame1 = MakeFrame(1);
|
||||
const std::vector<unsigned char> frame2 = MakeFrame(2);
|
||||
const std::vector<unsigned char> frame3 = MakeFrame(3);
|
||||
|
||||
Expect(mailbox.SubmitFrame(frame1.data(), 8, 1), "first input frame submits");
|
||||
Expect(mailbox.SubmitFrame(frame2.data(), 8, 2), "second input frame submits");
|
||||
Expect(mailbox.SubmitFrame(frame3.data(), 8, 3), "third input frame submits");
|
||||
|
||||
InputFrame latest;
|
||||
Expect(mailbox.TryAcquireLatest(latest), "latest input frame can be acquired");
|
||||
Expect(latest.frameIndex == 3, "mailbox returns newest frame");
|
||||
Expect(latest.bytes != nullptr && static_cast<const unsigned char*>(latest.bytes)[0] == 3, "latest frame bytes match newest frame");
|
||||
Expect(mailbox.Release(latest), "latest input frame can be released");
|
||||
|
||||
const InputFrameMailboxMetrics metrics = mailbox.Metrics();
|
||||
Expect(metrics.droppedReadyFrames == 2, "older ready input frames are dropped after latest acquire");
|
||||
Expect(metrics.freeCount == 3, "all slots are free after release");
|
||||
}
|
||||
|
||||
void TestSubmitDropsOldestWhenFull()
|
||||
{
|
||||
InputFrameMailbox mailbox(MakeConfig(2));
|
||||
@@ -67,10 +52,10 @@ void TestSubmitDropsOldestWhenFull()
|
||||
Expect(mailbox.SubmitFrame(frame2.data(), 8, 2), "second frame submits into full test");
|
||||
Expect(mailbox.SubmitFrame(frame3.data(), 8, 3), "third frame submits by dropping oldest ready frame");
|
||||
|
||||
InputFrame latest;
|
||||
Expect(mailbox.TryAcquireLatest(latest), "latest frame available after drop");
|
||||
Expect(latest.frameIndex == 3, "newest frame survived full mailbox");
|
||||
Expect(mailbox.Release(latest), "newest frame releases");
|
||||
InputFrame oldest;
|
||||
Expect(mailbox.TryAcquireOldest(oldest), "oldest frame available after drop");
|
||||
Expect(oldest.frameIndex == 2, "bounded mailbox keeps FIFO order after trimming oldest overflow");
|
||||
Expect(mailbox.Release(oldest), "oldest frame releases");
|
||||
|
||||
const InputFrameMailboxMetrics metrics = mailbox.Metrics();
|
||||
Expect(metrics.droppedReadyFrames >= 1, "full mailbox records ready drop");
|
||||
@@ -85,18 +70,61 @@ void TestReadingFrameIsProtected()
|
||||
|
||||
Expect(mailbox.SubmitFrame(frame1.data(), 8, 1), "protected frame submits");
|
||||
InputFrame acquired;
|
||||
Expect(mailbox.TryAcquireLatest(acquired), "protected frame acquired");
|
||||
Expect(mailbox.TryAcquireOldest(acquired), "protected frame acquired");
|
||||
Expect(!mailbox.SubmitFrame(frame2.data(), 8, 2), "producer cannot overwrite frame currently being read");
|
||||
Expect(mailbox.Release(acquired), "protected frame releases");
|
||||
Expect(mailbox.SubmitFrame(frame2.data(), 8, 2), "producer can submit after release");
|
||||
}
|
||||
|
||||
void TestAcquireOldestConsumesFifoWithoutDroppingReadyFrames()
|
||||
{
|
||||
InputFrameMailbox mailbox(MakeConfig(3));
|
||||
const std::vector<unsigned char> frame1 = MakeFrame(1);
|
||||
const std::vector<unsigned char> frame2 = MakeFrame(2);
|
||||
|
||||
Expect(mailbox.SubmitFrame(frame1.data(), 8, 1), "fifo first frame submits");
|
||||
Expect(mailbox.SubmitFrame(frame2.data(), 8, 2), "fifo second frame submits");
|
||||
|
||||
InputFrame acquired;
|
||||
Expect(mailbox.TryAcquireOldest(acquired), "fifo oldest frame acquired");
|
||||
Expect(acquired.frameIndex == 1, "fifo acquire returns oldest frame");
|
||||
Expect(mailbox.Release(acquired), "fifo acquired frame releases");
|
||||
|
||||
const InputFrameMailboxMetrics metrics = mailbox.Metrics();
|
||||
Expect(metrics.readyCount == 1, "fifo acquire leaves newer frame ready");
|
||||
Expect(metrics.droppedReadyFrames == 0, "fifo acquire does not drop newer ready frame");
|
||||
}
|
||||
|
||||
void TestMaxReadyFramesKeepsConfiguredInputBuffer()
|
||||
{
|
||||
InputFrameMailbox mailbox(MakeBufferedConfig(4, 3));
|
||||
const std::vector<unsigned char> frame1 = MakeFrame(1);
|
||||
const std::vector<unsigned char> frame2 = MakeFrame(2);
|
||||
const std::vector<unsigned char> frame3 = MakeFrame(3);
|
||||
const std::vector<unsigned char> frame4 = MakeFrame(4);
|
||||
|
||||
Expect(mailbox.SubmitFrame(frame1.data(), 8, 1), "bounded first frame submits");
|
||||
Expect(mailbox.SubmitFrame(frame2.data(), 8, 2), "bounded second frame submits");
|
||||
Expect(mailbox.SubmitFrame(frame3.data(), 8, 3), "bounded third frame submits");
|
||||
Expect(mailbox.SubmitFrame(frame4.data(), 8, 4), "bounded fourth frame submits");
|
||||
|
||||
InputFrame acquired;
|
||||
Expect(mailbox.TryAcquireOldest(acquired), "bounded oldest available frame acquired");
|
||||
Expect(acquired.frameIndex == 2, "bounded buffer trims oldest beyond configured ready frame limit");
|
||||
Expect(mailbox.Release(acquired), "bounded acquired frame releases");
|
||||
|
||||
const InputFrameMailboxMetrics metrics = mailbox.Metrics();
|
||||
Expect(metrics.readyCount == 2, "bounded acquire leaves remaining configured ready frames");
|
||||
Expect(metrics.droppedReadyFrames == 1, "bounded buffer records trimmed frame");
|
||||
}
|
||||
}
|
||||
|
||||
int main()
|
||||
{
|
||||
TestAcquireLatestDropsOlderReadyFrames();
|
||||
TestSubmitDropsOldestWhenFull();
|
||||
TestReadingFrameIsProtected();
|
||||
TestAcquireOldestConsumesFifoWithoutDroppingReadyFrames();
|
||||
TestMaxReadyFramesKeepsConfiguredInputBuffer();
|
||||
|
||||
if (gFailures != 0)
|
||||
{
|
||||
|
||||
@@ -43,6 +43,13 @@ int main()
|
||||
|
||||
RenderCadenceCompositor::CadenceTelemetrySnapshot telemetry;
|
||||
telemetry.renderFps = 59.94;
|
||||
telemetry.renderFrameMilliseconds = 2.5;
|
||||
telemetry.renderFrameBudgetUsedPercent = 15.0;
|
||||
telemetry.renderFrameMaxMilliseconds = 4.0;
|
||||
telemetry.readbackQueueMilliseconds = 0.6;
|
||||
telemetry.completedReadbackCopyMilliseconds = 1.2;
|
||||
telemetry.completedDrops = 3;
|
||||
telemetry.acquireMisses = 4;
|
||||
telemetry.shaderBuildsCommitted = 1;
|
||||
|
||||
const std::filesystem::path root = MakeTestRoot();
|
||||
@@ -98,6 +105,13 @@ int main()
|
||||
ExpectContains(json, "\"type\":\"color\"", "state JSON should serialize parameter types for the UI");
|
||||
ExpectContains(json, "\"width\":1920", "state JSON should expose output width");
|
||||
ExpectContains(json, "\"height\":1080", "state JSON should expose output height");
|
||||
ExpectContains(json, "\"renderMs\":2.5", "state JSON should expose top-level render timing");
|
||||
ExpectContains(json, "\"budgetUsedPercent\":15", "state JSON should expose top-level render budget percentage");
|
||||
ExpectContains(json, "\"renderFrameMs\":2.5", "state JSON should expose cadence render timing");
|
||||
ExpectContains(json, "\"readbackQueueMs\":0.6", "state JSON should expose readback queue timing");
|
||||
ExpectContains(json, "\"completedReadbackCopyMs\":1.2", "state JSON should expose completed readback copy timing");
|
||||
ExpectContains(json, "\"completedDrops\":3", "state JSON should expose completed drop count");
|
||||
ExpectContains(json, "\"acquireMisses\":4", "state JSON should expose acquire miss count");
|
||||
|
||||
std::filesystem::remove_all(root);
|
||||
|
||||
|
||||
@@ -26,6 +26,8 @@ struct FakeExchangeMetrics
|
||||
std::size_t scheduledCount = 0;
|
||||
uint64_t completedFrames = 0;
|
||||
uint64_t scheduledFrames = 0;
|
||||
uint64_t completedDrops = 0;
|
||||
uint64_t acquireMisses = 0;
|
||||
};
|
||||
|
||||
struct FakeExchange
|
||||
@@ -65,8 +67,15 @@ struct FakeOutput
|
||||
|
||||
struct FakeRenderThreadMetrics
|
||||
{
|
||||
uint64_t clockOverruns = 0;
|
||||
uint64_t skippedFrames = 0;
|
||||
uint64_t shaderBuildsCommitted = 0;
|
||||
uint64_t shaderBuildFailures = 0;
|
||||
double renderFrameMilliseconds = 0.0;
|
||||
double renderFrameBudgetUsedPercent = 0.0;
|
||||
double renderFrameMaxMilliseconds = 0.0;
|
||||
double readbackQueueMilliseconds = 0.0;
|
||||
double completedReadbackCopyMilliseconds = 0.0;
|
||||
uint64_t inputFramesReceived = 0;
|
||||
uint64_t inputFramesDropped = 0;
|
||||
uint64_t inputConsumeMisses = 0;
|
||||
@@ -94,6 +103,8 @@ void TestTelemetrySamplesCompletedPollMissesAndShaderCounts()
|
||||
exchange.metrics.scheduledCount = 4;
|
||||
exchange.metrics.completedFrames = 100;
|
||||
exchange.metrics.scheduledFrames = 96;
|
||||
exchange.metrics.completedDrops = 2;
|
||||
exchange.metrics.acquireMisses = 3;
|
||||
|
||||
FakeOutput output;
|
||||
output.metrics.actualBufferedFramesAvailable = true;
|
||||
@@ -104,8 +115,15 @@ void TestTelemetrySamplesCompletedPollMissesAndShaderCounts()
|
||||
outputThread.metrics.scheduleFailures = 0;
|
||||
|
||||
FakeRenderThread renderThread;
|
||||
renderThread.metrics.clockOverruns = 5;
|
||||
renderThread.metrics.skippedFrames = 8;
|
||||
renderThread.metrics.shaderBuildsCommitted = 1;
|
||||
renderThread.metrics.shaderBuildFailures = 0;
|
||||
renderThread.metrics.renderFrameMilliseconds = 2.5;
|
||||
renderThread.metrics.renderFrameBudgetUsedPercent = 15.0;
|
||||
renderThread.metrics.renderFrameMaxMilliseconds = 4.0;
|
||||
renderThread.metrics.readbackQueueMilliseconds = 0.6;
|
||||
renderThread.metrics.completedReadbackCopyMilliseconds = 1.2;
|
||||
renderThread.metrics.inputFramesReceived = 9;
|
||||
renderThread.metrics.inputFramesDropped = 2;
|
||||
renderThread.metrics.inputConsumeMisses = 3;
|
||||
@@ -122,8 +140,17 @@ void TestTelemetrySamplesCompletedPollMissesAndShaderCounts()
|
||||
Expect(snapshot.completedFrames == 1, "completed frame count is sampled");
|
||||
Expect(snapshot.scheduledFrames == 4, "scheduled frame count is sampled");
|
||||
Expect(snapshot.completedPollMisses == 12, "completed poll misses are sampled");
|
||||
Expect(snapshot.completedDrops == 2, "completed drops are sampled");
|
||||
Expect(snapshot.acquireMisses == 3, "acquire misses are sampled");
|
||||
Expect(snapshot.clockOverruns == 5, "clock overrun count is sampled");
|
||||
Expect(snapshot.clockSkippedFrames == 8, "clock skipped frame count is sampled");
|
||||
Expect(snapshot.shaderBuildsCommitted == 1, "shader committed count is sampled");
|
||||
Expect(snapshot.shaderBuildFailures == 0, "shader failure count is sampled");
|
||||
Expect(snapshot.renderFrameMilliseconds == 2.5, "render frame timing is sampled");
|
||||
Expect(snapshot.renderFrameBudgetUsedPercent == 15.0, "render budget percentage is sampled");
|
||||
Expect(snapshot.renderFrameMaxMilliseconds == 4.0, "render frame max timing is sampled");
|
||||
Expect(snapshot.readbackQueueMilliseconds == 0.6, "readback queue timing is sampled");
|
||||
Expect(snapshot.completedReadbackCopyMilliseconds == 1.2, "completed readback copy timing is sampled");
|
||||
Expect(snapshot.inputFramesReceived == 9, "input received count is sampled");
|
||||
Expect(snapshot.inputFramesDropped == 2, "input dropped count is sampled");
|
||||
Expect(snapshot.inputConsumeMisses == 3, "input consume miss count is sampled");
|
||||
@@ -173,11 +200,20 @@ void TestTelemetrySerializesToJson()
|
||||
snapshot.scheduledTotal = 118;
|
||||
snapshot.completedPollMisses = 3;
|
||||
snapshot.scheduleFailures = 0;
|
||||
snapshot.completedDrops = 4;
|
||||
snapshot.acquireMisses = 5;
|
||||
snapshot.completions = 117;
|
||||
snapshot.displayedLate = 1;
|
||||
snapshot.dropped = 2;
|
||||
snapshot.clockOverruns = 3;
|
||||
snapshot.clockSkippedFrames = 5;
|
||||
snapshot.shaderBuildsCommitted = 1;
|
||||
snapshot.shaderBuildFailures = 0;
|
||||
snapshot.renderFrameMilliseconds = 2.5;
|
||||
snapshot.renderFrameBudgetUsedPercent = 15.0;
|
||||
snapshot.renderFrameMaxMilliseconds = 4.0;
|
||||
snapshot.readbackQueueMilliseconds = 0.6;
|
||||
snapshot.completedReadbackCopyMilliseconds = 1.2;
|
||||
snapshot.inputFramesReceived = 10;
|
||||
snapshot.inputFramesDropped = 1;
|
||||
snapshot.inputConsumeMisses = 2;
|
||||
@@ -205,8 +241,14 @@ void TestTelemetrySerializesToJson()
|
||||
"\"free\":7,\"completed\":1,\"scheduled\":4,"
|
||||
"\"renderedTotal\":120,\"scheduledTotal\":118,"
|
||||
"\"completedPollMisses\":3,\"scheduleFailures\":0,"
|
||||
"\"completedDrops\":4,\"acquireMisses\":5,"
|
||||
"\"completions\":117,\"late\":1,\"dropped\":2,"
|
||||
"\"clockOverruns\":3,\"clockSkippedFrames\":5,"
|
||||
"\"clockOveruns\":3,\"clockSkipped\":5,"
|
||||
"\"shaderCommitted\":1,\"shaderFailures\":0,"
|
||||
"\"renderFrameMs\":2.5,\"renderFrameBudgetUsedPercent\":15,"
|
||||
"\"renderFrameMaxMs\":4,\"readbackQueueMs\":0.6,"
|
||||
"\"completedReadbackCopyMs\":1.2,"
|
||||
"\"inputFramesReceived\":10,\"inputFramesDropped\":1,"
|
||||
"\"inputConsumeMisses\":2,\"inputUploadMisses\":3,"
|
||||
"\"inputReadyFrames\":1,\"inputReadingFrames\":0,"
|
||||
|
||||
@@ -70,6 +70,19 @@ void TestDefaultPolicyReportsLagWithoutSkippingScheduleTime()
|
||||
Expect(scheduler.NextScheduleTime().streamTime == 1000, "default recovery keeps stream time continuous");
|
||||
}
|
||||
|
||||
void TestScheduleCursorCanAlignToPlaybackClock()
|
||||
{
|
||||
VideoPlayoutScheduler scheduler;
|
||||
scheduler.Configure(1000, 50000);
|
||||
|
||||
(void)scheduler.NextScheduleTime();
|
||||
scheduler.AlignNextScheduleTimeToPlayback(10000, 4);
|
||||
Expect(scheduler.NextScheduleTime().streamTime == 14000, "schedule cursor skips stale stream time after underfeed");
|
||||
|
||||
scheduler.AlignNextScheduleTimeToPlayback(11000, 1);
|
||||
Expect(scheduler.NextScheduleTime().streamTime == 15000, "schedule cursor does not move backward");
|
||||
}
|
||||
|
||||
void TestMeasuredRecoveryIsCappedByPolicy()
|
||||
{
|
||||
VideoPlayoutPolicy policy;
|
||||
@@ -133,6 +146,7 @@ int main()
|
||||
TestScheduleAdvancesFromZero();
|
||||
TestLateAndDroppedRecoveryUsesMeasuredPressure();
|
||||
TestDefaultPolicyReportsLagWithoutSkippingScheduleTime();
|
||||
TestScheduleCursorCanAlignToPlaybackClock();
|
||||
TestMeasuredRecoveryIsCappedByPolicy();
|
||||
TestCleanCompletionTracksCompletedIndexAndClearsStreaks();
|
||||
TestPolicyNormalization();
|
||||
|
||||
Reference in New Issue
Block a user