Compare commits
13 Commits
3e45bba54b
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3ffb562ff7 | ||
|
|
c2d548499c | ||
|
|
6a0340d1b4 | ||
|
|
5c1fc2a6cf | ||
|
|
d411453f80 | ||
|
|
4a049a557a | ||
|
|
13586c611a | ||
|
|
3a83d9617f | ||
|
|
5c66cfdc64 | ||
|
|
d72272b5a8 | ||
|
|
c25ae7b25b | ||
|
|
a39be6fb20 | ||
|
|
0a1fe440d9 |
@@ -310,12 +310,15 @@ set(RENDER_CADENCE_APP_SOURCES
|
||||
"${RENDER_CADENCE_APP_DIR}/app/AppConfigProvider.h"
|
||||
"${RENDER_CADENCE_APP_DIR}/app/RenderCadenceApp.h"
|
||||
"${RENDER_CADENCE_APP_DIR}/app/RuntimeLayerController.cpp"
|
||||
"${RENDER_CADENCE_APP_DIR}/app/RuntimeLayerControllerBuild.cpp"
|
||||
"${RENDER_CADENCE_APP_DIR}/app/RuntimeLayerControllerControls.cpp"
|
||||
"${RENDER_CADENCE_APP_DIR}/app/RuntimeLayerController.h"
|
||||
"${RENDER_CADENCE_APP_DIR}/control/ControlActionResult.h"
|
||||
"${RENDER_CADENCE_APP_DIR}/control/RuntimeControlCommand.cpp"
|
||||
"${RENDER_CADENCE_APP_DIR}/control/RuntimeControlCommand.h"
|
||||
"${RENDER_CADENCE_APP_DIR}/control/http/HttpControlServer.cpp"
|
||||
"${RENDER_CADENCE_APP_DIR}/control/http/HttpControlServer.h"
|
||||
"${RENDER_CADENCE_APP_DIR}/control/http/HttpControlServerRoutes.cpp"
|
||||
"${RENDER_CADENCE_APP_DIR}/control/http/HttpControlServerWebSocket.cpp"
|
||||
"${RENDER_CADENCE_APP_DIR}/control/RuntimeStateJson.h"
|
||||
"${RENDER_CADENCE_APP_DIR}/frames/InputFrameMailbox.cpp"
|
||||
@@ -345,6 +348,7 @@ set(RENDER_CADENCE_APP_SOURCES
|
||||
"${RENDER_CADENCE_APP_DIR}/render/runtime/RuntimeShaderParams.h"
|
||||
"${RENDER_CADENCE_APP_DIR}/render/runtime/RuntimeRenderScene.cpp"
|
||||
"${RENDER_CADENCE_APP_DIR}/render/runtime/RuntimeRenderScene.h"
|
||||
"${RENDER_CADENCE_APP_DIR}/render/runtime/RuntimeRenderSceneRender.cpp"
|
||||
"${RENDER_CADENCE_APP_DIR}/render/runtime/RuntimeShaderPrepareWorker.cpp"
|
||||
"${RENDER_CADENCE_APP_DIR}/render/runtime/RuntimeShaderPrepareWorker.h"
|
||||
"${RENDER_CADENCE_APP_DIR}/render/runtime/RuntimeShaderProgram.h"
|
||||
@@ -973,6 +977,7 @@ add_executable(RenderCadenceCompositorHttpControlServerTests
|
||||
"${APP_DIR}/runtime/support/RuntimeJson.cpp"
|
||||
"${RENDER_CADENCE_APP_DIR}/control/RuntimeControlCommand.cpp"
|
||||
"${RENDER_CADENCE_APP_DIR}/control/http/HttpControlServer.cpp"
|
||||
"${RENDER_CADENCE_APP_DIR}/control/http/HttpControlServerRoutes.cpp"
|
||||
"${RENDER_CADENCE_APP_DIR}/control/http/HttpControlServerWebSocket.cpp"
|
||||
"${RENDER_CADENCE_APP_DIR}/json/JsonWriter.cpp"
|
||||
"${RENDER_CADENCE_APP_DIR}/logging/Logger.cpp"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,10 +12,12 @@ 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; }
|
||||
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:
|
||||
@@ -526,13 +529,21 @@ bool DeckLinkSession::PopulateOutputFrame(IDeckLinkMutableVideoFrame* outputVide
|
||||
|
||||
bool DeckLinkSession::ScheduleFrame(IDeckLinkMutableVideoFrame* outputVideoFrame)
|
||||
{
|
||||
const VideoIOScheduleTime scheduleTime = mScheduler.NextScheduleTime();
|
||||
if (outputVideoFrame == nullptr || output == nullptr)
|
||||
{
|
||||
++mState.deckLinkScheduleFailureCount;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (mScheduleRealignmentPending)
|
||||
{
|
||||
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);
|
||||
const auto scheduleEnd = std::chrono::steady_clock::now();
|
||||
@@ -543,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)
|
||||
@@ -827,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;
|
||||
};
|
||||
|
||||
@@ -11,20 +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 as a bounded FIFO reserve once they are waiting for playout
|
||||
protects scheduled frames until DeckLink completion
|
||||
|
||||
DeckLinkOutputThread
|
||||
@@ -33,7 +35,7 @@ DeckLinkOutputThread
|
||||
never renders
|
||||
```
|
||||
|
||||
Startup warms up real rendered frames before DeckLink scheduled playback starts.
|
||||
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
|
||||
|
||||
@@ -45,11 +47,14 @@ 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 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
|
||||
- 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
|
||||
@@ -119,7 +124,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 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`
|
||||
@@ -231,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 bounded completed-frame output preroll reserve at normal render cadence
|
||||
3. try to attach DeckLink output
|
||||
4. start telemetry and HTTP either way
|
||||
|
||||
@@ -249,35 +256,61 @@ 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. leave input absent if discovery, setup, format support, or stream startup fails
|
||||
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. 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.
|
||||
|
||||
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
|
||||
|
||||
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`: 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`
|
||||
- `inputReadingFrames`: input frames currently protected while render uploads them
|
||||
- `inputLatestAgeMs`: age of the newest submitted input frame
|
||||
- `inputUploadMs`: render-thread GL upload/decode submission time for the latest uploaded input frame
|
||||
- `inputFormatSupported`: whether the latest frame reaching the render upload path was BGRA8 or UYVY8 compatible
|
||||
- `inputSignalPresent`: whether any input frame has reached the mailbox
|
||||
- `inputCaptureFps`: DeckLink input callback capture rate
|
||||
- `inputConvertMs`: input-edge CPU conversion time; expected to remain `0` for BGRA8 and raw UYVY8 capture because UYVY8 decode is render-thread GPU work
|
||||
- `inputSubmitMs`: time spent submitting the latest captured/converted input frame to `InputFrameMailbox`
|
||||
- `inputSubmitMs`: time spent copying/submitting the latest captured input frame to `InputFrameMailbox`
|
||||
- `inputCaptureFormat`: selected DeckLink input format (`BGRA8`, `UYVY8`, or `none`)
|
||||
- `inputNoSignalFrames`: DeckLink callbacks reporting no input source
|
||||
- `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:
|
||||
|
||||
@@ -286,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
|
||||
@@ -319,7 +354,7 @@ Current runtime shader support is deliberately limited to stateless full-frame p
|
||||
|
||||
Shader source semantics:
|
||||
|
||||
- `gVideoInput` means the raw latest input frame for every layer.
|
||||
- `gVideoInput` means the latest decoded shader-visible video input for every layer.
|
||||
- `gLayerInput` means the previous layer output.
|
||||
- the first layer may receive `gLayerInput = gVideoInput`.
|
||||
- later layers receive `gVideoInput = original input` and `gLayerInput = previous layer`.
|
||||
@@ -360,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
|
||||
|
||||
@@ -379,8 +415,8 @@ 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
|
||||
- `render/InputFrameTexture`: render-thread-owned upload of the latest CPU input frame into GL, including raw UYVY8 decode into the shader-visible input texture
|
||||
- `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 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
|
||||
|
||||
@@ -12,12 +12,18 @@
|
||||
|
||||
#include <windows.h>
|
||||
|
||||
#include <chrono>
|
||||
#include <iostream>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
|
||||
namespace
|
||||
{
|
||||
constexpr std::size_t kDeckLinkTargetBufferedFrames = 4;
|
||||
constexpr std::size_t kReadbackDepth = 6;
|
||||
constexpr std::size_t kWritableOutputReserveFrames = kReadbackDepth + 2;
|
||||
|
||||
class ComInitGuard
|
||||
{
|
||||
public:
|
||||
@@ -41,6 +47,19 @@ private:
|
||||
bool mInitialized = false;
|
||||
HRESULT mResult = S_OK;
|
||||
};
|
||||
|
||||
bool WaitForInputWarmup(InputFrameMailbox& mailbox, std::size_t targetReadyFrames, std::chrono::milliseconds timeout)
|
||||
{
|
||||
const auto start = std::chrono::steady_clock::now();
|
||||
while (std::chrono::steady_clock::now() - start < timeout)
|
||||
{
|
||||
const InputFrameMailboxMetrics metrics = mailbox.Metrics();
|
||||
if (metrics.readyCount >= targetReadyFrames)
|
||||
return true;
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(2));
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
int main(int argc, char** argv)
|
||||
@@ -80,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);
|
||||
|
||||
@@ -92,17 +115,31 @@ 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);
|
||||
}
|
||||
else
|
||||
{
|
||||
appConfig.deckLink.outputVideoMode = outputVideoMode;
|
||||
}
|
||||
|
||||
RenderCadenceCompositor::DeckLinkInput deckLinkInput(inputMailbox);
|
||||
RenderCadenceCompositor::DeckLinkInputThread deckLinkInputThread(deckLinkInput);
|
||||
@@ -123,6 +160,25 @@ 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 = 3;
|
||||
constexpr std::chrono::milliseconds kInputWarmupTimeout(1000);
|
||||
if (WaitForInputWarmup(inputMailbox, kInputStartupBufferedFrames, kInputWarmupTimeout))
|
||||
{
|
||||
const InputFrameMailboxMetrics metrics = inputMailbox.Metrics();
|
||||
RenderCadenceCompositor::Log(
|
||||
"app",
|
||||
"DeckLink input warmup complete. ready=" + std::to_string(metrics.readyCount) +
|
||||
" submitted=" + std::to_string(metrics.submittedFrames) + ".");
|
||||
}
|
||||
else
|
||||
{
|
||||
const InputFrameMailboxMetrics metrics = inputMailbox.Metrics();
|
||||
RenderCadenceCompositor::LogWarning(
|
||||
"app",
|
||||
"DeckLink input warmup timed out; starting render cadence with current input buffer. ready=" +
|
||||
std::to_string(metrics.readyCount) + " submitted=" + std::to_string(metrics.submittedFrames) + ".");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -138,8 +194,11 @@ int main(int argc, char** argv)
|
||||
RenderThread::Config renderConfig;
|
||||
renderConfig.width = frameExchangeConfig.width;
|
||||
renderConfig.height = frameExchangeConfig.height;
|
||||
renderConfig.frameDurationMilliseconds = RenderCadenceCompositor::FrameDurationMillisecondsFromRateString(appConfig.outputFrameRate);
|
||||
renderConfig.pboDepth = 6;
|
||||
const double fallbackFrameDurationMilliseconds = RenderCadenceCompositor::FrameDurationMillisecondsFromRateString(appConfig.outputFrameRate);
|
||||
renderConfig.frameDurationMilliseconds = outputVideoModeResolved
|
||||
? RenderCadenceCompositor::FrameDurationMillisecondsFromDisplayMode(outputVideoMode.displayMode, fallbackFrameDurationMilliseconds)
|
||||
: fallbackFrameDurationMilliseconds;
|
||||
renderConfig.pboDepth = kReadbackDepth;
|
||||
|
||||
RenderThread renderThread(frameExchange, &inputMailbox, renderConfig);
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
#include <cstdint>
|
||||
#include <cstdlib>
|
||||
#include <fstream>
|
||||
#include <sstream>
|
||||
@@ -181,6 +182,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;
|
||||
@@ -165,6 +163,25 @@ 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;
|
||||
Log("app",
|
||||
"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 output preroll reserve.";
|
||||
return false;
|
||||
}
|
||||
|
||||
void DisableVideoOutput(const std::string& reason)
|
||||
|
||||
@@ -1,11 +1,7 @@
|
||||
#include "RuntimeLayerController.h"
|
||||
|
||||
#include "AppConfigProvider.h"
|
||||
#include "RuntimeJson.h"
|
||||
#include "../logging/Logger.h"
|
||||
|
||||
#include <filesystem>
|
||||
|
||||
namespace RenderCadenceCompositor
|
||||
{
|
||||
RuntimeLayerController::RuntimeLayerController(RenderLayerPublisher publisher) :
|
||||
@@ -48,137 +44,6 @@ void RuntimeLayerController::Stop()
|
||||
StopAllRuntimeShaderBuilds();
|
||||
}
|
||||
|
||||
ControlActionResult RuntimeLayerController::HandleAddLayer(const std::string& body)
|
||||
{
|
||||
CleanupRetiredShaderBuilds();
|
||||
|
||||
std::string shaderId;
|
||||
std::string error;
|
||||
if (!ExtractStringField(body, "shaderId", shaderId, error))
|
||||
return { false, error };
|
||||
|
||||
std::string layerId;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mRuntimeLayerMutex);
|
||||
if (!mRuntimeLayerModel.AddLayer(mShaderCatalog, shaderId, layerId, error))
|
||||
return { false, error };
|
||||
}
|
||||
|
||||
Log("runtime-shader", "Layer added: " + layerId + " shader=" + shaderId);
|
||||
StartLayerShaderBuild(layerId, shaderId);
|
||||
return { true, std::string() };
|
||||
}
|
||||
|
||||
ControlActionResult RuntimeLayerController::HandleRemoveLayer(const std::string& body)
|
||||
{
|
||||
CleanupRetiredShaderBuilds();
|
||||
|
||||
std::string layerId;
|
||||
std::string error;
|
||||
if (!ExtractStringField(body, "layerId", layerId, error))
|
||||
return { false, error };
|
||||
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mRuntimeLayerMutex);
|
||||
if (!mRuntimeLayerModel.RemoveLayer(layerId, error))
|
||||
return { false, error };
|
||||
}
|
||||
|
||||
Log("runtime-shader", "Layer removed: " + layerId);
|
||||
RetireLayerShaderBuild(layerId);
|
||||
PublishRuntimeRenderLayers();
|
||||
return { true, std::string() };
|
||||
}
|
||||
|
||||
ControlActionResult RuntimeLayerController::HandleControlCommand(const RuntimeControlCommand& command)
|
||||
{
|
||||
CleanupRetiredShaderBuilds();
|
||||
|
||||
std::string error;
|
||||
switch (command.type)
|
||||
{
|
||||
case RuntimeControlCommandType::AddLayer:
|
||||
{
|
||||
std::string layerId;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mRuntimeLayerMutex);
|
||||
if (!mRuntimeLayerModel.AddLayer(mShaderCatalog, command.shaderId, layerId, error))
|
||||
return { false, error };
|
||||
}
|
||||
Log("runtime-shader", "Layer added: " + layerId + " shader=" + command.shaderId);
|
||||
StartLayerShaderBuild(layerId, command.shaderId);
|
||||
return { true, std::string() };
|
||||
}
|
||||
case RuntimeControlCommandType::RemoveLayer:
|
||||
{
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mRuntimeLayerMutex);
|
||||
if (!mRuntimeLayerModel.RemoveLayer(command.layerId, error))
|
||||
return { false, error };
|
||||
}
|
||||
Log("runtime-shader", "Layer removed: " + command.layerId);
|
||||
RetireLayerShaderBuild(command.layerId);
|
||||
PublishRuntimeRenderLayers();
|
||||
return { true, std::string() };
|
||||
}
|
||||
case RuntimeControlCommandType::ReorderLayer:
|
||||
{
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mRuntimeLayerMutex);
|
||||
if (!mRuntimeLayerModel.ReorderLayer(command.layerId, command.targetIndex, error))
|
||||
return { false, error };
|
||||
}
|
||||
PublishRuntimeRenderLayers();
|
||||
return { true, std::string() };
|
||||
}
|
||||
case RuntimeControlCommandType::SetLayerBypass:
|
||||
{
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mRuntimeLayerMutex);
|
||||
if (!mRuntimeLayerModel.SetLayerBypass(command.layerId, command.bypass, error))
|
||||
return { false, error };
|
||||
}
|
||||
PublishRuntimeRenderLayers();
|
||||
return { true, std::string() };
|
||||
}
|
||||
case RuntimeControlCommandType::SetLayerShader:
|
||||
{
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mRuntimeLayerMutex);
|
||||
if (!mRuntimeLayerModel.SetLayerShader(mShaderCatalog, command.layerId, command.shaderId, error))
|
||||
return { false, error };
|
||||
}
|
||||
Log("runtime-shader", "Layer shader changed: " + command.layerId + " shader=" + command.shaderId);
|
||||
StartLayerShaderBuild(command.layerId, command.shaderId);
|
||||
return { true, std::string() };
|
||||
}
|
||||
case RuntimeControlCommandType::UpdateLayerParameter:
|
||||
{
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mRuntimeLayerMutex);
|
||||
if (!mRuntimeLayerModel.UpdateParameter(command.layerId, command.parameterId, command.value, error))
|
||||
return { false, error };
|
||||
}
|
||||
PublishRuntimeRenderLayers();
|
||||
return { true, std::string() };
|
||||
}
|
||||
case RuntimeControlCommandType::ResetLayerParameters:
|
||||
{
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mRuntimeLayerMutex);
|
||||
if (!mRuntimeLayerModel.ResetParameters(command.layerId, error))
|
||||
return { false, error };
|
||||
}
|
||||
PublishRuntimeRenderLayers();
|
||||
return { true, std::string() };
|
||||
}
|
||||
case RuntimeControlCommandType::Unsupported:
|
||||
break;
|
||||
}
|
||||
|
||||
return { false, "Unsupported runtime control command." };
|
||||
}
|
||||
|
||||
RuntimeLayerModelSnapshot RuntimeLayerController::Snapshot(const CadenceTelemetrySnapshot& telemetry) const
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mRuntimeLayerMutex);
|
||||
@@ -191,113 +56,6 @@ RuntimeLayerModelSnapshot RuntimeLayerController::Snapshot(const CadenceTelemetr
|
||||
return snapshot;
|
||||
}
|
||||
|
||||
void RuntimeLayerController::LoadSupportedShaderCatalog(const std::string& shaderLibrary, unsigned maxTemporalHistoryFrames)
|
||||
{
|
||||
const std::filesystem::path shaderRoot = FindRepoPath(shaderLibrary);
|
||||
std::string error;
|
||||
if (!mShaderCatalog.Load(shaderRoot, maxTemporalHistoryFrames, error))
|
||||
{
|
||||
LogWarning("runtime-shader", "Supported shader catalog is empty: " + error);
|
||||
return;
|
||||
}
|
||||
|
||||
Log("runtime-shader", "Supported shader catalog loaded with " + std::to_string(mShaderCatalog.Shaders().size()) + " shader(s).");
|
||||
}
|
||||
|
||||
void RuntimeLayerController::InitializeLayerModel(std::string& runtimeShaderId)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mRuntimeLayerMutex);
|
||||
std::string error;
|
||||
if (!mRuntimeLayerModel.InitializeSingleLayer(mShaderCatalog, runtimeShaderId, error))
|
||||
{
|
||||
LogWarning("runtime-shader", error + " Runtime shader build disabled.");
|
||||
runtimeShaderId.clear();
|
||||
mRuntimeLayerModel.Clear();
|
||||
}
|
||||
}
|
||||
|
||||
void RuntimeLayerController::StartLayerShaderBuild(const std::string& layerId, const std::string& shaderId)
|
||||
{
|
||||
CleanupRetiredShaderBuilds();
|
||||
RetireLayerShaderBuild(layerId);
|
||||
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mRuntimeLayerMutex);
|
||||
std::string error;
|
||||
mRuntimeLayerModel.MarkBuildStarted(layerId, "Runtime Slang build started for shader '" + shaderId + "'.", error);
|
||||
}
|
||||
|
||||
auto bridge = std::make_unique<RuntimeShaderBridge>();
|
||||
RuntimeShaderBridge* bridgePtr = bridge.get();
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mShaderBuildMutex);
|
||||
mShaderBuilds[layerId] = std::move(bridge);
|
||||
}
|
||||
|
||||
bridgePtr->Start(
|
||||
layerId,
|
||||
shaderId,
|
||||
[this](const RuntimeShaderArtifact& artifact) {
|
||||
if (MarkRuntimeBuildReady(artifact))
|
||||
PublishRuntimeRenderLayers();
|
||||
},
|
||||
[this, layerId](const std::string& message) {
|
||||
MarkRuntimeBuildFailedForLayer(layerId, message);
|
||||
LogError("runtime-shader", "Runtime Slang build failed: " + message);
|
||||
});
|
||||
}
|
||||
|
||||
void RuntimeLayerController::RetireLayerShaderBuild(const std::string& layerId)
|
||||
{
|
||||
std::unique_ptr<RuntimeShaderBridge> bridge;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mShaderBuildMutex);
|
||||
auto bridgeIt = mShaderBuilds.find(layerId);
|
||||
if (bridgeIt == mShaderBuilds.end())
|
||||
return;
|
||||
bridge = std::move(bridgeIt->second);
|
||||
mShaderBuilds.erase(bridgeIt);
|
||||
bridge->RequestStop();
|
||||
mRetiredShaderBuilds.push_back(std::move(bridge));
|
||||
}
|
||||
}
|
||||
|
||||
void RuntimeLayerController::CleanupRetiredShaderBuilds()
|
||||
{
|
||||
std::vector<std::unique_ptr<RuntimeShaderBridge>> readyToStop;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mShaderBuildMutex);
|
||||
for (auto it = mRetiredShaderBuilds.begin(); it != mRetiredShaderBuilds.end();)
|
||||
{
|
||||
if ((*it)->CanStopWithoutWaiting())
|
||||
{
|
||||
readyToStop.push_back(std::move(*it));
|
||||
it = mRetiredShaderBuilds.erase(it);
|
||||
continue;
|
||||
}
|
||||
++it;
|
||||
}
|
||||
}
|
||||
|
||||
for (std::unique_ptr<RuntimeShaderBridge>& bridge : readyToStop)
|
||||
bridge->Stop();
|
||||
}
|
||||
|
||||
void RuntimeLayerController::StopAllRuntimeShaderBuilds()
|
||||
{
|
||||
std::map<std::string, std::unique_ptr<RuntimeShaderBridge>> builds;
|
||||
std::vector<std::unique_ptr<RuntimeShaderBridge>> retiredBuilds;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mShaderBuildMutex);
|
||||
builds.swap(mShaderBuilds);
|
||||
retiredBuilds.swap(mRetiredShaderBuilds);
|
||||
}
|
||||
for (auto& entry : builds)
|
||||
entry.second->Stop();
|
||||
for (auto& bridge : retiredBuilds)
|
||||
bridge->Stop();
|
||||
}
|
||||
|
||||
void RuntimeLayerController::PublishRuntimeRenderLayers()
|
||||
{
|
||||
if (!mPublisher)
|
||||
@@ -331,31 +89,4 @@ void RuntimeLayerController::MarkRuntimeBuildFailedForLayer(const std::string& l
|
||||
LogWarning("runtime-shader", error);
|
||||
}
|
||||
|
||||
std::string RuntimeLayerController::FirstRuntimeLayerId() const
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mRuntimeLayerMutex);
|
||||
return mRuntimeLayerModel.FirstLayerId();
|
||||
}
|
||||
|
||||
bool RuntimeLayerController::ExtractStringField(const std::string& body, const char* fieldName, std::string& value, std::string& error)
|
||||
{
|
||||
JsonValue root;
|
||||
std::string parseError;
|
||||
if (!ParseJson(body.empty() ? "{}" : body, root, parseError) || !root.isObject())
|
||||
{
|
||||
error = parseError.empty() ? "Request body must be a JSON object." : parseError;
|
||||
return false;
|
||||
}
|
||||
|
||||
const JsonValue* field = root.find(fieldName);
|
||||
if (!field || !field->isString() || field->asString().empty())
|
||||
{
|
||||
error = std::string("Request field '") + fieldName + "' must be a non-empty string.";
|
||||
return false;
|
||||
}
|
||||
|
||||
value = field->asString();
|
||||
error.clear();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
122
apps/RenderCadenceCompositor/app/RuntimeLayerControllerBuild.cpp
Normal file
122
apps/RenderCadenceCompositor/app/RuntimeLayerControllerBuild.cpp
Normal file
@@ -0,0 +1,122 @@
|
||||
#include "RuntimeLayerController.h"
|
||||
|
||||
#include "AppConfigProvider.h"
|
||||
#include "../logging/Logger.h"
|
||||
|
||||
#include <filesystem>
|
||||
|
||||
namespace RenderCadenceCompositor
|
||||
{
|
||||
void RuntimeLayerController::LoadSupportedShaderCatalog(const std::string& shaderLibrary, unsigned maxTemporalHistoryFrames)
|
||||
{
|
||||
const std::filesystem::path shaderRoot = FindRepoPath(shaderLibrary);
|
||||
std::string error;
|
||||
if (!mShaderCatalog.Load(shaderRoot, maxTemporalHistoryFrames, error))
|
||||
{
|
||||
LogWarning("runtime-shader", "Supported shader catalog is empty: " + error);
|
||||
return;
|
||||
}
|
||||
|
||||
Log("runtime-shader", "Supported shader catalog loaded with " + std::to_string(mShaderCatalog.Shaders().size()) + " shader(s).");
|
||||
}
|
||||
|
||||
void RuntimeLayerController::InitializeLayerModel(std::string& runtimeShaderId)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mRuntimeLayerMutex);
|
||||
std::string error;
|
||||
if (!mRuntimeLayerModel.InitializeSingleLayer(mShaderCatalog, runtimeShaderId, error))
|
||||
{
|
||||
LogWarning("runtime-shader", error + " Runtime shader build disabled.");
|
||||
runtimeShaderId.clear();
|
||||
mRuntimeLayerModel.Clear();
|
||||
}
|
||||
}
|
||||
|
||||
void RuntimeLayerController::StartLayerShaderBuild(const std::string& layerId, const std::string& shaderId)
|
||||
{
|
||||
CleanupRetiredShaderBuilds();
|
||||
RetireLayerShaderBuild(layerId);
|
||||
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mRuntimeLayerMutex);
|
||||
std::string error;
|
||||
mRuntimeLayerModel.MarkBuildStarted(layerId, "Runtime Slang build started for shader '" + shaderId + "'.", error);
|
||||
}
|
||||
|
||||
auto bridge = std::make_unique<RuntimeShaderBridge>();
|
||||
RuntimeShaderBridge* bridgePtr = bridge.get();
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mShaderBuildMutex);
|
||||
mShaderBuilds[layerId] = std::move(bridge);
|
||||
}
|
||||
|
||||
bridgePtr->Start(
|
||||
layerId,
|
||||
shaderId,
|
||||
[this](const RuntimeShaderArtifact& artifact) {
|
||||
if (MarkRuntimeBuildReady(artifact))
|
||||
PublishRuntimeRenderLayers();
|
||||
},
|
||||
[this, layerId](const std::string& message) {
|
||||
MarkRuntimeBuildFailedForLayer(layerId, message);
|
||||
LogError("runtime-shader", "Runtime Slang build failed: " + message);
|
||||
});
|
||||
}
|
||||
|
||||
void RuntimeLayerController::RetireLayerShaderBuild(const std::string& layerId)
|
||||
{
|
||||
std::unique_ptr<RuntimeShaderBridge> bridge;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mShaderBuildMutex);
|
||||
auto bridgeIt = mShaderBuilds.find(layerId);
|
||||
if (bridgeIt == mShaderBuilds.end())
|
||||
return;
|
||||
bridge = std::move(bridgeIt->second);
|
||||
mShaderBuilds.erase(bridgeIt);
|
||||
bridge->RequestStop();
|
||||
mRetiredShaderBuilds.push_back(std::move(bridge));
|
||||
}
|
||||
}
|
||||
|
||||
void RuntimeLayerController::CleanupRetiredShaderBuilds()
|
||||
{
|
||||
std::vector<std::unique_ptr<RuntimeShaderBridge>> readyToStop;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mShaderBuildMutex);
|
||||
for (auto it = mRetiredShaderBuilds.begin(); it != mRetiredShaderBuilds.end();)
|
||||
{
|
||||
if ((*it)->CanStopWithoutWaiting())
|
||||
{
|
||||
readyToStop.push_back(std::move(*it));
|
||||
it = mRetiredShaderBuilds.erase(it);
|
||||
continue;
|
||||
}
|
||||
++it;
|
||||
}
|
||||
}
|
||||
|
||||
for (std::unique_ptr<RuntimeShaderBridge>& bridge : readyToStop)
|
||||
bridge->Stop();
|
||||
}
|
||||
|
||||
void RuntimeLayerController::StopAllRuntimeShaderBuilds()
|
||||
{
|
||||
std::map<std::string, std::unique_ptr<RuntimeShaderBridge>> builds;
|
||||
std::vector<std::unique_ptr<RuntimeShaderBridge>> retiredBuilds;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mShaderBuildMutex);
|
||||
builds.swap(mShaderBuilds);
|
||||
retiredBuilds.swap(mRetiredShaderBuilds);
|
||||
}
|
||||
for (auto& entry : builds)
|
||||
entry.second->Stop();
|
||||
for (auto& bridge : retiredBuilds)
|
||||
bridge->Stop();
|
||||
}
|
||||
|
||||
std::string RuntimeLayerController::FirstRuntimeLayerId() const
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mRuntimeLayerMutex);
|
||||
return mRuntimeLayerModel.FirstLayerId();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,160 @@
|
||||
#include "RuntimeLayerController.h"
|
||||
|
||||
#include "RuntimeJson.h"
|
||||
#include "../logging/Logger.h"
|
||||
|
||||
namespace RenderCadenceCompositor
|
||||
{
|
||||
ControlActionResult RuntimeLayerController::HandleAddLayer(const std::string& body)
|
||||
{
|
||||
CleanupRetiredShaderBuilds();
|
||||
|
||||
std::string shaderId;
|
||||
std::string error;
|
||||
if (!ExtractStringField(body, "shaderId", shaderId, error))
|
||||
return { false, error };
|
||||
|
||||
std::string layerId;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mRuntimeLayerMutex);
|
||||
if (!mRuntimeLayerModel.AddLayer(mShaderCatalog, shaderId, layerId, error))
|
||||
return { false, error };
|
||||
}
|
||||
|
||||
Log("runtime-shader", "Layer added: " + layerId + " shader=" + shaderId);
|
||||
StartLayerShaderBuild(layerId, shaderId);
|
||||
return { true, std::string() };
|
||||
}
|
||||
|
||||
ControlActionResult RuntimeLayerController::HandleRemoveLayer(const std::string& body)
|
||||
{
|
||||
CleanupRetiredShaderBuilds();
|
||||
|
||||
std::string layerId;
|
||||
std::string error;
|
||||
if (!ExtractStringField(body, "layerId", layerId, error))
|
||||
return { false, error };
|
||||
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mRuntimeLayerMutex);
|
||||
if (!mRuntimeLayerModel.RemoveLayer(layerId, error))
|
||||
return { false, error };
|
||||
}
|
||||
|
||||
Log("runtime-shader", "Layer removed: " + layerId);
|
||||
RetireLayerShaderBuild(layerId);
|
||||
PublishRuntimeRenderLayers();
|
||||
return { true, std::string() };
|
||||
}
|
||||
|
||||
ControlActionResult RuntimeLayerController::HandleControlCommand(const RuntimeControlCommand& command)
|
||||
{
|
||||
CleanupRetiredShaderBuilds();
|
||||
|
||||
std::string error;
|
||||
switch (command.type)
|
||||
{
|
||||
case RuntimeControlCommandType::AddLayer:
|
||||
{
|
||||
std::string layerId;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mRuntimeLayerMutex);
|
||||
if (!mRuntimeLayerModel.AddLayer(mShaderCatalog, command.shaderId, layerId, error))
|
||||
return { false, error };
|
||||
}
|
||||
Log("runtime-shader", "Layer added: " + layerId + " shader=" + command.shaderId);
|
||||
StartLayerShaderBuild(layerId, command.shaderId);
|
||||
return { true, std::string() };
|
||||
}
|
||||
case RuntimeControlCommandType::RemoveLayer:
|
||||
{
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mRuntimeLayerMutex);
|
||||
if (!mRuntimeLayerModel.RemoveLayer(command.layerId, error))
|
||||
return { false, error };
|
||||
}
|
||||
Log("runtime-shader", "Layer removed: " + command.layerId);
|
||||
RetireLayerShaderBuild(command.layerId);
|
||||
PublishRuntimeRenderLayers();
|
||||
return { true, std::string() };
|
||||
}
|
||||
case RuntimeControlCommandType::ReorderLayer:
|
||||
{
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mRuntimeLayerMutex);
|
||||
if (!mRuntimeLayerModel.ReorderLayer(command.layerId, command.targetIndex, error))
|
||||
return { false, error };
|
||||
}
|
||||
PublishRuntimeRenderLayers();
|
||||
return { true, std::string() };
|
||||
}
|
||||
case RuntimeControlCommandType::SetLayerBypass:
|
||||
{
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mRuntimeLayerMutex);
|
||||
if (!mRuntimeLayerModel.SetLayerBypass(command.layerId, command.bypass, error))
|
||||
return { false, error };
|
||||
}
|
||||
PublishRuntimeRenderLayers();
|
||||
return { true, std::string() };
|
||||
}
|
||||
case RuntimeControlCommandType::SetLayerShader:
|
||||
{
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mRuntimeLayerMutex);
|
||||
if (!mRuntimeLayerModel.SetLayerShader(mShaderCatalog, command.layerId, command.shaderId, error))
|
||||
return { false, error };
|
||||
}
|
||||
Log("runtime-shader", "Layer shader changed: " + command.layerId + " shader=" + command.shaderId);
|
||||
StartLayerShaderBuild(command.layerId, command.shaderId);
|
||||
return { true, std::string() };
|
||||
}
|
||||
case RuntimeControlCommandType::UpdateLayerParameter:
|
||||
{
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mRuntimeLayerMutex);
|
||||
if (!mRuntimeLayerModel.UpdateParameter(command.layerId, command.parameterId, command.value, error))
|
||||
return { false, error };
|
||||
}
|
||||
PublishRuntimeRenderLayers();
|
||||
return { true, std::string() };
|
||||
}
|
||||
case RuntimeControlCommandType::ResetLayerParameters:
|
||||
{
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mRuntimeLayerMutex);
|
||||
if (!mRuntimeLayerModel.ResetParameters(command.layerId, error))
|
||||
return { false, error };
|
||||
}
|
||||
PublishRuntimeRenderLayers();
|
||||
return { true, std::string() };
|
||||
}
|
||||
case RuntimeControlCommandType::Unsupported:
|
||||
break;
|
||||
}
|
||||
|
||||
return { false, "Unsupported runtime control command." };
|
||||
}
|
||||
|
||||
bool RuntimeLayerController::ExtractStringField(const std::string& body, const char* fieldName, std::string& value, std::string& error)
|
||||
{
|
||||
JsonValue root;
|
||||
std::string parseError;
|
||||
if (!ParseJson(body.empty() ? "{}" : body, root, parseError) || !root.isObject())
|
||||
{
|
||||
error = parseError.empty() ? "Request body must be a JSON object." : parseError;
|
||||
return false;
|
||||
}
|
||||
|
||||
const JsonValue* field = root.find(fieldName);
|
||||
if (!field || !field->isString() || field->asString().empty())
|
||||
{
|
||||
error = std::string("Request field '") + fieldName + "' must be a non-empty string.";
|
||||
return false;
|
||||
}
|
||||
|
||||
value = field->asString();
|
||||
error.clear();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -283,9 +283,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");
|
||||
|
||||
@@ -1,15 +1,11 @@
|
||||
#include "HttpControlServer.h"
|
||||
|
||||
#include "../json/JsonWriter.h"
|
||||
#include "../logging/Logger.h"
|
||||
|
||||
#include <ws2tcpip.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
#include <fstream>
|
||||
#include <sstream>
|
||||
#include <vector>
|
||||
|
||||
namespace RenderCadenceCompositor
|
||||
{
|
||||
@@ -27,22 +23,6 @@ bool InitializeWinsock(std::string& error)
|
||||
return true;
|
||||
}
|
||||
|
||||
bool IsKnownPostEndpoint(const std::string& path)
|
||||
{
|
||||
return path == "/api/layers/add"
|
||||
|| path == "/api/layers/remove"
|
||||
|| path == "/api/layers/move"
|
||||
|| path == "/api/layers/reorder"
|
||||
|| path == "/api/layers/set-bypass"
|
||||
|| path == "/api/layers/set-shader"
|
||||
|| path == "/api/layers/update-parameter"
|
||||
|| path == "/api/layers/reset-parameters"
|
||||
|| path == "/api/stack-presets/save"
|
||||
|| path == "/api/stack-presets/load"
|
||||
|| path == "/api/reload"
|
||||
|| path == "/api/screenshot";
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
UniqueSocket::UniqueSocket(SOCKET socket) :
|
||||
@@ -260,179 +240,6 @@ HttpControlServer::HttpResponse HttpControlServer::RouteRequest(const HttpReques
|
||||
return TextResponse("404 Not Found", "Not Found");
|
||||
}
|
||||
|
||||
HttpControlServer::HttpResponse HttpControlServer::ServeGet(const HttpRequest& request) const
|
||||
{
|
||||
if (request.path == "/api/state")
|
||||
return JsonResponse("200 OK", mCallbacks.getStateJson ? mCallbacks.getStateJson() : "{}");
|
||||
if (request.path == "/openapi.yaml" || request.path == "/docs/openapi.yaml")
|
||||
return ServeOpenApiSpec();
|
||||
if (request.path == "/docs" || request.path == "/docs/")
|
||||
return ServeSwaggerDocs();
|
||||
if (request.path == "/" || request.path == "/index.html")
|
||||
return ServeUiAsset("index.html");
|
||||
if (request.path.rfind("/assets/", 0) == 0)
|
||||
return ServeUiAsset(request.path.substr(1));
|
||||
if (request.path.size() > 1)
|
||||
{
|
||||
const HttpResponse asset = ServeUiAsset(request.path.substr(1));
|
||||
if (asset.status != "404 Not Found")
|
||||
return asset;
|
||||
}
|
||||
return ServeUiAsset("index.html");
|
||||
}
|
||||
|
||||
HttpControlServer::HttpResponse HttpControlServer::ServePost(const HttpRequest& request) const
|
||||
{
|
||||
if (!IsKnownPostEndpoint(request.path))
|
||||
return TextResponse("404 Not Found", "Not Found");
|
||||
|
||||
if (mCallbacks.executePost)
|
||||
{
|
||||
const ControlActionResult result = mCallbacks.executePost(request.path, request.body);
|
||||
return JsonResponse(result.ok ? "200 OK" : "400 Bad Request", ActionResponse(result.ok, result.error));
|
||||
}
|
||||
|
||||
if (request.path == "/api/layers/add" && mCallbacks.addLayer)
|
||||
{
|
||||
const ControlActionResult result = mCallbacks.addLayer(request.body);
|
||||
return JsonResponse(result.ok ? "200 OK" : "400 Bad Request", ActionResponse(result.ok, result.error));
|
||||
}
|
||||
|
||||
if (request.path == "/api/layers/remove" && mCallbacks.removeLayer)
|
||||
{
|
||||
const ControlActionResult result = mCallbacks.removeLayer(request.body);
|
||||
return JsonResponse(result.ok ? "200 OK" : "400 Bad Request", ActionResponse(result.ok, result.error));
|
||||
}
|
||||
|
||||
return {
|
||||
"400 Bad Request",
|
||||
"application/json",
|
||||
ActionResponse(false, "Endpoint is not implemented in RenderCadenceCompositor yet.")
|
||||
};
|
||||
}
|
||||
|
||||
HttpControlServer::HttpResponse HttpControlServer::ServeOpenApiSpec() const
|
||||
{
|
||||
const std::filesystem::path path = mDocsRoot / "openapi.yaml";
|
||||
const std::string body = LoadTextFile(path);
|
||||
return body.empty()
|
||||
? TextResponse("404 Not Found", "OpenAPI spec not found")
|
||||
: HttpResponse{ "200 OK", GuessContentType(path), body };
|
||||
}
|
||||
|
||||
HttpControlServer::HttpResponse HttpControlServer::ServeSwaggerDocs() const
|
||||
{
|
||||
std::ostringstream html;
|
||||
html << "<!doctype html>\n"
|
||||
<< "<html><head><meta charset=\"utf-8\"><title>Video Shader Toys API Docs</title>\n"
|
||||
<< "<link rel=\"stylesheet\" href=\"https://unpkg.com/swagger-ui-dist@5/swagger-ui.css\"></head>\n"
|
||||
<< "<body><div id=\"swagger-ui\"></div>\n"
|
||||
<< "<script src=\"https://unpkg.com/swagger-ui-dist@5/swagger-ui-bundle.js\"></script>\n"
|
||||
<< "<script>SwaggerUIBundle({url:'/docs/openapi.yaml',dom_id:'#swagger-ui'});</script>\n"
|
||||
<< "</body></html>\n";
|
||||
return { "200 OK", "text/html", html.str() };
|
||||
}
|
||||
|
||||
HttpControlServer::HttpResponse HttpControlServer::ServeUiAsset(const std::string& relativePath) const
|
||||
{
|
||||
if (mUiRoot.empty())
|
||||
return TextResponse("404 Not Found", "UI root is not configured");
|
||||
|
||||
const std::filesystem::path sanitizedPath = std::filesystem::path(relativePath).lexically_normal();
|
||||
if (!IsSafeRelativePath(sanitizedPath))
|
||||
return TextResponse("404 Not Found", "Not Found");
|
||||
|
||||
const std::filesystem::path path = mUiRoot / sanitizedPath;
|
||||
const std::string body = LoadTextFile(path);
|
||||
if (body.empty())
|
||||
return TextResponse("404 Not Found", "Not Found");
|
||||
return { "200 OK", GuessContentType(path), body };
|
||||
}
|
||||
|
||||
std::string HttpControlServer::LoadTextFile(const std::filesystem::path& path) const
|
||||
{
|
||||
std::ifstream input(path, std::ios::binary);
|
||||
if (!input)
|
||||
return std::string();
|
||||
|
||||
std::ostringstream buffer;
|
||||
buffer << input.rdbuf();
|
||||
return buffer.str();
|
||||
}
|
||||
|
||||
HttpControlServer::HttpResponse HttpControlServer::JsonResponse(const std::string& status, const std::string& body)
|
||||
{
|
||||
return { status, "application/json", body };
|
||||
}
|
||||
|
||||
HttpControlServer::HttpResponse HttpControlServer::TextResponse(const std::string& status, const std::string& body)
|
||||
{
|
||||
return { status, "text/plain", body };
|
||||
}
|
||||
|
||||
HttpControlServer::HttpResponse HttpControlServer::HtmlResponse(const std::string& status, const std::string& body)
|
||||
{
|
||||
return { status, "text/html", body };
|
||||
}
|
||||
|
||||
std::string HttpControlServer::ActionResponse(bool ok, const std::string& error)
|
||||
{
|
||||
JsonWriter writer;
|
||||
writer.BeginObject();
|
||||
writer.KeyBool("ok", ok);
|
||||
if (!error.empty())
|
||||
writer.KeyString("error", error);
|
||||
writer.EndObject();
|
||||
return writer.StringValue();
|
||||
}
|
||||
|
||||
std::string HttpControlServer::GuessContentType(const std::filesystem::path& path)
|
||||
{
|
||||
const std::string extension = ToLower(path.extension().string());
|
||||
if (extension == ".yaml" || extension == ".yml")
|
||||
return "application/yaml";
|
||||
if (extension == ".json")
|
||||
return "application/json";
|
||||
if (extension == ".js" || extension == ".mjs")
|
||||
return "text/javascript";
|
||||
if (extension == ".css")
|
||||
return "text/css";
|
||||
if (extension == ".html" || extension == ".htm")
|
||||
return "text/html";
|
||||
if (extension == ".svg")
|
||||
return "image/svg+xml";
|
||||
if (extension == ".png")
|
||||
return "image/png";
|
||||
if (extension == ".jpg" || extension == ".jpeg")
|
||||
return "image/jpeg";
|
||||
if (extension == ".ico")
|
||||
return "image/x-icon";
|
||||
if (extension == ".map")
|
||||
return "application/json";
|
||||
return "text/plain";
|
||||
}
|
||||
|
||||
bool HttpControlServer::IsSafeRelativePath(const std::filesystem::path& path)
|
||||
{
|
||||
if (path.empty() || path.is_absolute())
|
||||
return false;
|
||||
|
||||
for (const std::filesystem::path& part : path)
|
||||
{
|
||||
if (part == "..")
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
std::string HttpControlServer::ToLower(std::string text)
|
||||
{
|
||||
std::transform(text.begin(), text.end(), text.begin(), [](unsigned char character) {
|
||||
return static_cast<char>(std::tolower(character));
|
||||
});
|
||||
return text;
|
||||
}
|
||||
|
||||
bool HttpControlServer::ParseHttpRequest(const std::string& rawRequest, HttpRequest& request)
|
||||
{
|
||||
const std::size_t requestLineEnd = rawRequest.find("\r\n");
|
||||
|
||||
@@ -0,0 +1,203 @@
|
||||
#include "HttpControlServer.h"
|
||||
|
||||
#include "../json/JsonWriter.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
#include <fstream>
|
||||
#include <sstream>
|
||||
|
||||
namespace RenderCadenceCompositor
|
||||
{
|
||||
namespace
|
||||
{
|
||||
bool IsKnownPostEndpoint(const std::string& path)
|
||||
{
|
||||
return path == "/api/layers/add"
|
||||
|| path == "/api/layers/remove"
|
||||
|| path == "/api/layers/move"
|
||||
|| path == "/api/layers/reorder"
|
||||
|| path == "/api/layers/set-bypass"
|
||||
|| path == "/api/layers/set-shader"
|
||||
|| path == "/api/layers/update-parameter"
|
||||
|| path == "/api/layers/reset-parameters"
|
||||
|| path == "/api/stack-presets/save"
|
||||
|| path == "/api/stack-presets/load"
|
||||
|| path == "/api/reload"
|
||||
|| path == "/api/screenshot";
|
||||
}
|
||||
}
|
||||
|
||||
HttpControlServer::HttpResponse HttpControlServer::ServeGet(const HttpRequest& request) const
|
||||
{
|
||||
if (request.path == "/api/state")
|
||||
return JsonResponse("200 OK", mCallbacks.getStateJson ? mCallbacks.getStateJson() : "{}");
|
||||
if (request.path == "/openapi.yaml" || request.path == "/docs/openapi.yaml")
|
||||
return ServeOpenApiSpec();
|
||||
if (request.path == "/docs" || request.path == "/docs/")
|
||||
return ServeSwaggerDocs();
|
||||
if (request.path == "/" || request.path == "/index.html")
|
||||
return ServeUiAsset("index.html");
|
||||
if (request.path.rfind("/assets/", 0) == 0)
|
||||
return ServeUiAsset(request.path.substr(1));
|
||||
if (request.path.size() > 1)
|
||||
{
|
||||
const HttpResponse asset = ServeUiAsset(request.path.substr(1));
|
||||
if (asset.status != "404 Not Found")
|
||||
return asset;
|
||||
}
|
||||
return ServeUiAsset("index.html");
|
||||
}
|
||||
|
||||
HttpControlServer::HttpResponse HttpControlServer::ServePost(const HttpRequest& request) const
|
||||
{
|
||||
if (!IsKnownPostEndpoint(request.path))
|
||||
return TextResponse("404 Not Found", "Not Found");
|
||||
|
||||
if (mCallbacks.executePost)
|
||||
{
|
||||
const ControlActionResult result = mCallbacks.executePost(request.path, request.body);
|
||||
return JsonResponse(result.ok ? "200 OK" : "400 Bad Request", ActionResponse(result.ok, result.error));
|
||||
}
|
||||
|
||||
if (request.path == "/api/layers/add" && mCallbacks.addLayer)
|
||||
{
|
||||
const ControlActionResult result = mCallbacks.addLayer(request.body);
|
||||
return JsonResponse(result.ok ? "200 OK" : "400 Bad Request", ActionResponse(result.ok, result.error));
|
||||
}
|
||||
|
||||
if (request.path == "/api/layers/remove" && mCallbacks.removeLayer)
|
||||
{
|
||||
const ControlActionResult result = mCallbacks.removeLayer(request.body);
|
||||
return JsonResponse(result.ok ? "200 OK" : "400 Bad Request", ActionResponse(result.ok, result.error));
|
||||
}
|
||||
|
||||
return {
|
||||
"400 Bad Request",
|
||||
"application/json",
|
||||
ActionResponse(false, "Endpoint is not implemented in RenderCadenceCompositor yet.")
|
||||
};
|
||||
}
|
||||
|
||||
HttpControlServer::HttpResponse HttpControlServer::ServeOpenApiSpec() const
|
||||
{
|
||||
const std::filesystem::path path = mDocsRoot / "openapi.yaml";
|
||||
const std::string body = LoadTextFile(path);
|
||||
return body.empty()
|
||||
? TextResponse("404 Not Found", "OpenAPI spec not found")
|
||||
: HttpResponse{ "200 OK", GuessContentType(path), body };
|
||||
}
|
||||
|
||||
HttpControlServer::HttpResponse HttpControlServer::ServeSwaggerDocs() const
|
||||
{
|
||||
std::ostringstream html;
|
||||
html << "<!doctype html>\n"
|
||||
<< "<html><head><meta charset=\"utf-8\"><title>Video Shader Toys API Docs</title>\n"
|
||||
<< "<link rel=\"stylesheet\" href=\"https://unpkg.com/swagger-ui-dist@5/swagger-ui.css\"></head>\n"
|
||||
<< "<body><div id=\"swagger-ui\"></div>\n"
|
||||
<< "<script src=\"https://unpkg.com/swagger-ui-dist@5/swagger-ui-bundle.js\"></script>\n"
|
||||
<< "<script>SwaggerUIBundle({url:'/docs/openapi.yaml',dom_id:'#swagger-ui'});</script>\n"
|
||||
<< "</body></html>\n";
|
||||
return { "200 OK", "text/html", html.str() };
|
||||
}
|
||||
|
||||
HttpControlServer::HttpResponse HttpControlServer::ServeUiAsset(const std::string& relativePath) const
|
||||
{
|
||||
if (mUiRoot.empty())
|
||||
return TextResponse("404 Not Found", "UI root is not configured");
|
||||
|
||||
const std::filesystem::path sanitizedPath = std::filesystem::path(relativePath).lexically_normal();
|
||||
if (!IsSafeRelativePath(sanitizedPath))
|
||||
return TextResponse("404 Not Found", "Not Found");
|
||||
|
||||
const std::filesystem::path path = mUiRoot / sanitizedPath;
|
||||
const std::string body = LoadTextFile(path);
|
||||
if (body.empty())
|
||||
return TextResponse("404 Not Found", "Not Found");
|
||||
return { "200 OK", GuessContentType(path), body };
|
||||
}
|
||||
|
||||
std::string HttpControlServer::LoadTextFile(const std::filesystem::path& path) const
|
||||
{
|
||||
std::ifstream input(path, std::ios::binary);
|
||||
if (!input)
|
||||
return std::string();
|
||||
|
||||
std::ostringstream buffer;
|
||||
buffer << input.rdbuf();
|
||||
return buffer.str();
|
||||
}
|
||||
|
||||
HttpControlServer::HttpResponse HttpControlServer::JsonResponse(const std::string& status, const std::string& body)
|
||||
{
|
||||
return { status, "application/json", body };
|
||||
}
|
||||
|
||||
HttpControlServer::HttpResponse HttpControlServer::TextResponse(const std::string& status, const std::string& body)
|
||||
{
|
||||
return { status, "text/plain", body };
|
||||
}
|
||||
|
||||
HttpControlServer::HttpResponse HttpControlServer::HtmlResponse(const std::string& status, const std::string& body)
|
||||
{
|
||||
return { status, "text/html", body };
|
||||
}
|
||||
|
||||
std::string HttpControlServer::ActionResponse(bool ok, const std::string& error)
|
||||
{
|
||||
JsonWriter writer;
|
||||
writer.BeginObject();
|
||||
writer.KeyBool("ok", ok);
|
||||
if (!error.empty())
|
||||
writer.KeyString("error", error);
|
||||
writer.EndObject();
|
||||
return writer.StringValue();
|
||||
}
|
||||
|
||||
std::string HttpControlServer::GuessContentType(const std::filesystem::path& path)
|
||||
{
|
||||
const std::string extension = ToLower(path.extension().string());
|
||||
if (extension == ".yaml" || extension == ".yml")
|
||||
return "application/yaml";
|
||||
if (extension == ".json")
|
||||
return "application/json";
|
||||
if (extension == ".js" || extension == ".mjs")
|
||||
return "text/javascript";
|
||||
if (extension == ".css")
|
||||
return "text/css";
|
||||
if (extension == ".html" || extension == ".htm")
|
||||
return "text/html";
|
||||
if (extension == ".svg")
|
||||
return "image/svg+xml";
|
||||
if (extension == ".png")
|
||||
return "image/png";
|
||||
if (extension == ".jpg" || extension == ".jpeg")
|
||||
return "image/jpeg";
|
||||
if (extension == ".ico")
|
||||
return "image/x-icon";
|
||||
if (extension == ".map")
|
||||
return "application/json";
|
||||
return "text/plain";
|
||||
}
|
||||
|
||||
bool HttpControlServer::IsSafeRelativePath(const std::filesystem::path& path)
|
||||
{
|
||||
if (path.empty() || path.is_absolute())
|
||||
return false;
|
||||
|
||||
for (const std::filesystem::path& part : path)
|
||||
{
|
||||
if (part == "..")
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
std::string HttpControlServer::ToLower(std::string text)
|
||||
{
|
||||
std::transform(text.begin(), text.end(), text.begin(), [](unsigned char character) {
|
||||
return static_cast<char>(std::tolower(character));
|
||||
});
|
||||
return text;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
@@ -72,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;
|
||||
@@ -130,6 +128,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);
|
||||
@@ -210,6 +253,17 @@ bool SystemFrameExchange::DropOldestCompletedLocked()
|
||||
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;
|
||||
|
||||
@@ -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;
|
||||
@@ -37,6 +41,7 @@ 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
|
||||
|
||||
@@ -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,8 +85,17 @@ 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);
|
||||
metrics.inputUploadMisses = mInputUploadMisses.load(std::memory_order_relaxed);
|
||||
metrics.inputReadyFrames = mInputReadyFrames.load(std::memory_order_relaxed);
|
||||
metrics.inputReadingFrames = mInputReadingFrames.load(std::memory_order_relaxed);
|
||||
metrics.inputLatestAgeMilliseconds = mInputLatestAgeMilliseconds.load(std::memory_order_relaxed);
|
||||
metrics.inputUploadMilliseconds = mInputUploadMilliseconds.load(std::memory_order_relaxed);
|
||||
metrics.inputFormatSupported = mInputFormatSupported.load(std::memory_order_relaxed);
|
||||
@@ -150,6 +159,7 @@ void RenderThread::ThreadMain()
|
||||
CountAcquireMiss();
|
||||
},
|
||||
[this]() { CountCompleted(); });
|
||||
PublishReadbackMetrics(readback);
|
||||
|
||||
const auto now = RenderCadenceClock::Clock::now();
|
||||
const RenderCadenceClock::Tick tick = clock.Poll(now);
|
||||
@@ -174,6 +184,7 @@ void RenderThread::ThreadMain()
|
||||
{
|
||||
mPboQueueMisses.fetch_add(1, std::memory_order_relaxed);
|
||||
}
|
||||
PublishReadbackMetrics(readback);
|
||||
|
||||
CountRendered();
|
||||
++frameIndex;
|
||||
@@ -192,6 +203,7 @@ void RenderThread::ThreadMain()
|
||||
CountAcquireMiss();
|
||||
},
|
||||
[this]() { CountCompleted(); });
|
||||
PublishReadbackMetrics(readback);
|
||||
}
|
||||
|
||||
readback.Shutdown();
|
||||
@@ -233,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)
|
||||
@@ -240,6 +275,9 @@ void RenderThread::PublishInputMetrics(const InputFrameTexture& inputTexture)
|
||||
const InputFrameMailboxMetrics mailboxMetrics = mInputMailbox->Metrics();
|
||||
mInputFramesReceived.store(mailboxMetrics.submittedFrames, std::memory_order_relaxed);
|
||||
mInputFramesDropped.store(mailboxMetrics.droppedReadyFrames + mailboxMetrics.submitMisses, std::memory_order_relaxed);
|
||||
mInputConsumeMisses.store(mailboxMetrics.consumeMisses, std::memory_order_relaxed);
|
||||
mInputReadyFrames.store(mailboxMetrics.readyCount, std::memory_order_relaxed);
|
||||
mInputReadingFrames.store(mailboxMetrics.readingCount, std::memory_order_relaxed);
|
||||
mInputLatestAgeMilliseconds.store(mailboxMetrics.latestFrameAgeMilliseconds, std::memory_order_relaxed);
|
||||
mInputSignalPresent.store(mailboxMetrics.hasSubmittedFrame, std::memory_order_relaxed);
|
||||
}
|
||||
@@ -247,10 +285,14 @@ void RenderThread::PublishInputMetrics(const InputFrameTexture& inputTexture)
|
||||
{
|
||||
mInputFramesReceived.store(0, std::memory_order_relaxed);
|
||||
mInputFramesDropped.store(0, std::memory_order_relaxed);
|
||||
mInputConsumeMisses.store(0, std::memory_order_relaxed);
|
||||
mInputReadyFrames.store(0, std::memory_order_relaxed);
|
||||
mInputReadingFrames.store(0, std::memory_order_relaxed);
|
||||
mInputLatestAgeMilliseconds.store(0.0, std::memory_order_relaxed);
|
||||
mInputSignalPresent.store(false, std::memory_order_relaxed);
|
||||
}
|
||||
|
||||
mInputUploadMisses.store(inputTexture.UploadMisses(), std::memory_order_relaxed);
|
||||
mInputUploadMilliseconds.store(inputTexture.LastUploadMilliseconds(), std::memory_order_relaxed);
|
||||
mInputFormatSupported.store(inputTexture.LastFrameFormatSupported(), std::memory_order_relaxed);
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
class SystemFrameExchange;
|
||||
class InputFrameMailbox;
|
||||
class InputFrameTexture;
|
||||
class Bgra8ReadbackPipeline;
|
||||
|
||||
class RenderThread
|
||||
{
|
||||
@@ -38,8 +39,17 @@ 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;
|
||||
uint64_t inputUploadMisses = 0;
|
||||
std::size_t inputReadyFrames = 0;
|
||||
std::size_t inputReadingFrames = 0;
|
||||
double inputLatestAgeMilliseconds = 0.0;
|
||||
double inputUploadMilliseconds = 0.0;
|
||||
bool inputFormatSupported = true;
|
||||
@@ -67,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);
|
||||
@@ -92,8 +103,17 @@ 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 };
|
||||
std::atomic<uint64_t> mInputUploadMisses{ 0 };
|
||||
std::atomic<std::size_t> mInputReadyFrames{ 0 };
|
||||
std::atomic<std::size_t> mInputReadingFrames{ 0 };
|
||||
std::atomic<double> mInputLatestAgeMilliseconds{ 0.0 };
|
||||
std::atomic<double> mInputUploadMilliseconds{ 0.0 };
|
||||
std::atomic<bool> mInputFormatSupported{ true };
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -102,84 +102,6 @@ bool RuntimeRenderScene::CommitRenderLayers(const std::vector<RenderCadenceCompo
|
||||
return true;
|
||||
}
|
||||
|
||||
bool RuntimeRenderScene::HasLayers()
|
||||
{
|
||||
ConsumePreparedPrograms();
|
||||
|
||||
for (const std::string& layerId : mLayerOrder)
|
||||
{
|
||||
const LayerProgram* layer = FindLayer(layerId);
|
||||
if (!layer)
|
||||
continue;
|
||||
for (const LayerProgram::PassProgram& pass : layer->passes)
|
||||
{
|
||||
if (pass.renderer && pass.renderer->HasProgram())
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void RuntimeRenderScene::RenderFrame(uint64_t frameIndex, unsigned width, unsigned height, GLuint videoInputTexture)
|
||||
{
|
||||
ConsumePreparedPrograms();
|
||||
|
||||
std::vector<LayerProgram*> readyLayers;
|
||||
for (const std::string& layerId : mLayerOrder)
|
||||
{
|
||||
LayerProgram* layer = FindLayer(layerId);
|
||||
if (!layer)
|
||||
continue;
|
||||
for (const LayerProgram::PassProgram& pass : layer->passes)
|
||||
{
|
||||
if (pass.renderer && pass.renderer->HasProgram())
|
||||
{
|
||||
readyLayers.push_back(layer);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (readyLayers.empty())
|
||||
return;
|
||||
|
||||
GLint outputFramebuffer = 0;
|
||||
glGetIntegerv(GL_FRAMEBUFFER_BINDING, &outputFramebuffer);
|
||||
|
||||
if (readyLayers.size() == 1)
|
||||
{
|
||||
RenderLayer(*readyLayers.front(), frameIndex, width, height, videoInputTexture, videoInputTexture, static_cast<GLuint>(outputFramebuffer), true);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!EnsureLayerTargets(width, height))
|
||||
{
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, static_cast<GLuint>(outputFramebuffer));
|
||||
RenderLayer(*readyLayers.back(), frameIndex, width, height, videoInputTexture, videoInputTexture, static_cast<GLuint>(outputFramebuffer), true);
|
||||
return;
|
||||
}
|
||||
|
||||
// Shader source contract:
|
||||
// - gVideoInput is the raw/latest 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;
|
||||
for (std::size_t layerIndex = 0; layerIndex < readyLayers.size(); ++layerIndex)
|
||||
{
|
||||
const bool isFinalLayer = layerIndex == readyLayers.size() - 1;
|
||||
if (isFinalLayer)
|
||||
{
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, static_cast<GLuint>(outputFramebuffer));
|
||||
RenderLayer(*readyLayers[layerIndex], frameIndex, width, height, videoInputTexture, layerInputTexture, static_cast<GLuint>(outputFramebuffer), true);
|
||||
continue;
|
||||
}
|
||||
|
||||
RenderLayer(*readyLayers[layerIndex], frameIndex, width, height, videoInputTexture, layerInputTexture, mLayerFramebuffers[nextTargetIndex], true);
|
||||
layerInputTexture = mLayerTextures[nextTargetIndex];
|
||||
nextTargetIndex = 1 - nextTargetIndex;
|
||||
}
|
||||
}
|
||||
|
||||
void RuntimeRenderScene::ShutdownGl()
|
||||
{
|
||||
mPrepareWorker.Stop();
|
||||
@@ -307,139 +229,6 @@ void RuntimeRenderScene::TryCommitPendingPrograms(LayerProgram& layer)
|
||||
layer.pendingPreparedPrograms.clear();
|
||||
}
|
||||
|
||||
GLuint RuntimeRenderScene::RenderLayer(
|
||||
LayerProgram& layer,
|
||||
uint64_t frameIndex,
|
||||
unsigned width,
|
||||
unsigned height,
|
||||
GLuint videoInputTexture,
|
||||
GLuint layerInputTexture,
|
||||
GLuint outputFramebuffer,
|
||||
bool renderToOutput)
|
||||
{
|
||||
GLuint namedOutputs[2] = {};
|
||||
std::string namedOutputNames[2];
|
||||
std::size_t nextTargetIndex = 2;
|
||||
GLuint lastOutputTexture = layerInputTexture;
|
||||
|
||||
for (LayerProgram::PassProgram& pass : layer.passes)
|
||||
{
|
||||
if (!pass.renderer || !pass.renderer->HasProgram())
|
||||
continue;
|
||||
|
||||
GLuint sourceTexture = videoInputTexture;
|
||||
if (!pass.inputNames.empty())
|
||||
{
|
||||
const std::string& inputName = pass.inputNames.front();
|
||||
if (inputName == "videoInput")
|
||||
{
|
||||
sourceTexture = videoInputTexture;
|
||||
}
|
||||
else if (inputName != "layerInput")
|
||||
{
|
||||
// Named intermediate pass inputs currently use the gVideoInput binding slot as the
|
||||
// selected pass source. Layer stack shaders should use gLayerInput for previous-layer
|
||||
// sampling and gVideoInput for the original input frame.
|
||||
for (std::size_t index = 0; index < 2; ++index)
|
||||
{
|
||||
if (namedOutputNames[index] == inputName)
|
||||
{
|
||||
sourceTexture = namedOutputs[index];
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const bool writesLayerOutput = pass.outputName == "layerOutput";
|
||||
if (writesLayerOutput && renderToOutput)
|
||||
{
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, outputFramebuffer);
|
||||
pass.renderer->RenderFrame(frameIndex, width, height, sourceTexture, layerInputTexture);
|
||||
lastOutputTexture = 0;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!EnsureLayerTargets(width, height))
|
||||
continue;
|
||||
|
||||
const std::size_t targetIndex = nextTargetIndex;
|
||||
nextTargetIndex = nextTargetIndex == 2 ? 3 : 2;
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, mLayerFramebuffers[targetIndex]);
|
||||
pass.renderer->RenderFrame(frameIndex, width, height, sourceTexture, layerInputTexture);
|
||||
const std::size_t namedIndex = targetIndex - 2;
|
||||
namedOutputs[namedIndex] = mLayerTextures[targetIndex];
|
||||
namedOutputNames[namedIndex] = pass.outputName;
|
||||
lastOutputTexture = mLayerTextures[targetIndex];
|
||||
}
|
||||
|
||||
return lastOutputTexture;
|
||||
}
|
||||
|
||||
bool RuntimeRenderScene::EnsureLayerTargets(unsigned width, unsigned height)
|
||||
{
|
||||
if (width == 0 || height == 0)
|
||||
return false;
|
||||
if (mLayerFramebuffers[0] != 0 && mLayerFramebuffers[1] != 0 && mLayerFramebuffers[2] != 0 && mLayerFramebuffers[3] != 0
|
||||
&& mLayerTextures[0] != 0 && mLayerTextures[1] != 0 && mLayerTextures[2] != 0 && mLayerTextures[3] != 0
|
||||
&& mLayerTargetWidth == width && mLayerTargetHeight == height)
|
||||
return true;
|
||||
|
||||
DestroyLayerTargets();
|
||||
mLayerTargetWidth = width;
|
||||
mLayerTargetHeight = height;
|
||||
|
||||
glGenFramebuffers(4, mLayerFramebuffers);
|
||||
glGenTextures(4, mLayerTextures);
|
||||
for (int index = 0; index < 4; ++index)
|
||||
{
|
||||
glBindTexture(GL_TEXTURE_2D, mLayerTextures[index]);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
|
||||
glTexImage2D(
|
||||
GL_TEXTURE_2D,
|
||||
0,
|
||||
GL_RGBA8,
|
||||
static_cast<GLsizei>(width),
|
||||
static_cast<GLsizei>(height),
|
||||
0,
|
||||
GL_BGRA,
|
||||
GL_UNSIGNED_INT_8_8_8_8_REV,
|
||||
nullptr);
|
||||
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, mLayerFramebuffers[index]);
|
||||
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, mLayerTextures[index], 0);
|
||||
if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE)
|
||||
{
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||
glBindTexture(GL_TEXTURE_2D, 0);
|
||||
DestroyLayerTargets();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||
glBindTexture(GL_TEXTURE_2D, 0);
|
||||
return true;
|
||||
}
|
||||
|
||||
void RuntimeRenderScene::DestroyLayerTargets()
|
||||
{
|
||||
if (mLayerFramebuffers[0] != 0 || mLayerFramebuffers[1] != 0 || mLayerFramebuffers[2] != 0 || mLayerFramebuffers[3] != 0)
|
||||
glDeleteFramebuffers(4, mLayerFramebuffers);
|
||||
if (mLayerTextures[0] != 0 || mLayerTextures[1] != 0 || mLayerTextures[2] != 0 || mLayerTextures[3] != 0)
|
||||
glDeleteTextures(4, mLayerTextures);
|
||||
for (int index = 0; index < 4; ++index)
|
||||
{
|
||||
mLayerFramebuffers[index] = 0;
|
||||
mLayerTextures[index] = 0;
|
||||
}
|
||||
mLayerTargetWidth = 0;
|
||||
mLayerTargetHeight = 0;
|
||||
}
|
||||
|
||||
RuntimeRenderScene::LayerProgram* RuntimeRenderScene::FindLayer(const std::string& layerId)
|
||||
{
|
||||
for (LayerProgram& layer : mLayers)
|
||||
|
||||
@@ -0,0 +1,219 @@
|
||||
#include "RuntimeRenderScene.h"
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#ifndef GL_FRAMEBUFFER_BINDING
|
||||
#define GL_FRAMEBUFFER_BINDING 0x8CA6
|
||||
#endif
|
||||
|
||||
bool RuntimeRenderScene::HasLayers()
|
||||
{
|
||||
ConsumePreparedPrograms();
|
||||
|
||||
for (const std::string& layerId : mLayerOrder)
|
||||
{
|
||||
const LayerProgram* layer = FindLayer(layerId);
|
||||
if (!layer)
|
||||
continue;
|
||||
for (const LayerProgram::PassProgram& pass : layer->passes)
|
||||
{
|
||||
if (pass.renderer && pass.renderer->HasProgram())
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void RuntimeRenderScene::RenderFrame(uint64_t frameIndex, unsigned width, unsigned height, GLuint videoInputTexture)
|
||||
{
|
||||
ConsumePreparedPrograms();
|
||||
|
||||
std::vector<LayerProgram*> readyLayers;
|
||||
for (const std::string& layerId : mLayerOrder)
|
||||
{
|
||||
LayerProgram* layer = FindLayer(layerId);
|
||||
if (!layer)
|
||||
continue;
|
||||
for (const LayerProgram::PassProgram& pass : layer->passes)
|
||||
{
|
||||
if (pass.renderer && pass.renderer->HasProgram())
|
||||
{
|
||||
readyLayers.push_back(layer);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (readyLayers.empty())
|
||||
return;
|
||||
|
||||
GLint outputFramebuffer = 0;
|
||||
glGetIntegerv(GL_FRAMEBUFFER_BINDING, &outputFramebuffer);
|
||||
|
||||
if (readyLayers.size() == 1)
|
||||
{
|
||||
RenderLayer(*readyLayers.front(), frameIndex, width, height, videoInputTexture, videoInputTexture, static_cast<GLuint>(outputFramebuffer), true);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!EnsureLayerTargets(width, height))
|
||||
{
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, static_cast<GLuint>(outputFramebuffer));
|
||||
RenderLayer(*readyLayers.back(), frameIndex, width, height, videoInputTexture, videoInputTexture, static_cast<GLuint>(outputFramebuffer), true);
|
||||
return;
|
||||
}
|
||||
|
||||
// Shader source contract:
|
||||
// - 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;
|
||||
for (std::size_t layerIndex = 0; layerIndex < readyLayers.size(); ++layerIndex)
|
||||
{
|
||||
const bool isFinalLayer = layerIndex == readyLayers.size() - 1;
|
||||
if (isFinalLayer)
|
||||
{
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, static_cast<GLuint>(outputFramebuffer));
|
||||
RenderLayer(*readyLayers[layerIndex], frameIndex, width, height, videoInputTexture, layerInputTexture, static_cast<GLuint>(outputFramebuffer), true);
|
||||
continue;
|
||||
}
|
||||
|
||||
RenderLayer(*readyLayers[layerIndex], frameIndex, width, height, videoInputTexture, layerInputTexture, mLayerFramebuffers[nextTargetIndex], true);
|
||||
layerInputTexture = mLayerTextures[nextTargetIndex];
|
||||
nextTargetIndex = 1 - nextTargetIndex;
|
||||
}
|
||||
}
|
||||
|
||||
GLuint RuntimeRenderScene::RenderLayer(
|
||||
LayerProgram& layer,
|
||||
uint64_t frameIndex,
|
||||
unsigned width,
|
||||
unsigned height,
|
||||
GLuint videoInputTexture,
|
||||
GLuint layerInputTexture,
|
||||
GLuint outputFramebuffer,
|
||||
bool renderToOutput)
|
||||
{
|
||||
GLuint namedOutputs[2] = {};
|
||||
std::string namedOutputNames[2];
|
||||
std::size_t nextTargetIndex = 2;
|
||||
GLuint lastOutputTexture = layerInputTexture;
|
||||
|
||||
for (LayerProgram::PassProgram& pass : layer.passes)
|
||||
{
|
||||
if (!pass.renderer || !pass.renderer->HasProgram())
|
||||
continue;
|
||||
|
||||
GLuint sourceTexture = videoInputTexture;
|
||||
if (!pass.inputNames.empty())
|
||||
{
|
||||
const std::string& inputName = pass.inputNames.front();
|
||||
if (inputName == "videoInput")
|
||||
{
|
||||
sourceTexture = videoInputTexture;
|
||||
}
|
||||
else if (inputName != "layerInput")
|
||||
{
|
||||
// Named intermediate pass inputs currently use the gVideoInput binding slot as the
|
||||
// selected pass source. Layer stack shaders should use gLayerInput for previous-layer
|
||||
// sampling and gVideoInput for the original input frame.
|
||||
for (std::size_t index = 0; index < 2; ++index)
|
||||
{
|
||||
if (namedOutputNames[index] == inputName)
|
||||
{
|
||||
sourceTexture = namedOutputs[index];
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const bool writesLayerOutput = pass.outputName == "layerOutput";
|
||||
if (writesLayerOutput && renderToOutput)
|
||||
{
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, outputFramebuffer);
|
||||
pass.renderer->RenderFrame(frameIndex, width, height, sourceTexture, layerInputTexture);
|
||||
lastOutputTexture = 0;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!EnsureLayerTargets(width, height))
|
||||
continue;
|
||||
|
||||
const std::size_t targetIndex = nextTargetIndex;
|
||||
nextTargetIndex = nextTargetIndex == 2 ? 3 : 2;
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, mLayerFramebuffers[targetIndex]);
|
||||
pass.renderer->RenderFrame(frameIndex, width, height, sourceTexture, layerInputTexture);
|
||||
const std::size_t namedIndex = targetIndex - 2;
|
||||
namedOutputs[namedIndex] = mLayerTextures[targetIndex];
|
||||
namedOutputNames[namedIndex] = pass.outputName;
|
||||
lastOutputTexture = mLayerTextures[targetIndex];
|
||||
}
|
||||
|
||||
return lastOutputTexture;
|
||||
}
|
||||
|
||||
bool RuntimeRenderScene::EnsureLayerTargets(unsigned width, unsigned height)
|
||||
{
|
||||
if (width == 0 || height == 0)
|
||||
return false;
|
||||
if (mLayerFramebuffers[0] != 0 && mLayerFramebuffers[1] != 0 && mLayerFramebuffers[2] != 0 && mLayerFramebuffers[3] != 0
|
||||
&& mLayerTextures[0] != 0 && mLayerTextures[1] != 0 && mLayerTextures[2] != 0 && mLayerTextures[3] != 0
|
||||
&& mLayerTargetWidth == width && mLayerTargetHeight == height)
|
||||
return true;
|
||||
|
||||
DestroyLayerTargets();
|
||||
mLayerTargetWidth = width;
|
||||
mLayerTargetHeight = height;
|
||||
|
||||
glGenFramebuffers(4, mLayerFramebuffers);
|
||||
glGenTextures(4, mLayerTextures);
|
||||
for (int index = 0; index < 4; ++index)
|
||||
{
|
||||
glBindTexture(GL_TEXTURE_2D, mLayerTextures[index]);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
|
||||
glTexImage2D(
|
||||
GL_TEXTURE_2D,
|
||||
0,
|
||||
GL_RGBA8,
|
||||
static_cast<GLsizei>(width),
|
||||
static_cast<GLsizei>(height),
|
||||
0,
|
||||
GL_BGRA,
|
||||
GL_UNSIGNED_INT_8_8_8_8_REV,
|
||||
nullptr);
|
||||
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, mLayerFramebuffers[index]);
|
||||
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, mLayerTextures[index], 0);
|
||||
if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE)
|
||||
{
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||
glBindTexture(GL_TEXTURE_2D, 0);
|
||||
DestroyLayerTargets();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||
glBindTexture(GL_TEXTURE_2D, 0);
|
||||
return true;
|
||||
}
|
||||
|
||||
void RuntimeRenderScene::DestroyLayerTargets()
|
||||
{
|
||||
if (mLayerFramebuffers[0] != 0 || mLayerFramebuffers[1] != 0 || mLayerFramebuffers[2] != 0 || mLayerFramebuffers[3] != 0)
|
||||
glDeleteFramebuffers(4, mLayerFramebuffers);
|
||||
if (mLayerTextures[0] != 0 || mLayerTextures[1] != 0 || mLayerTextures[2] != 0 || mLayerTextures[3] != 0)
|
||||
glDeleteTextures(4, mLayerTextures);
|
||||
for (int index = 0; index < 4; ++index)
|
||||
{
|
||||
mLayerFramebuffers[index] = 0;
|
||||
mLayerTextures[index] = 0;
|
||||
}
|
||||
mLayerTargetWidth = 0;
|
||||
mLayerTargetHeight = 0;
|
||||
}
|
||||
@@ -19,13 +19,26 @@ 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;
|
||||
uint64_t inputUploadMisses = 0;
|
||||
std::size_t inputReadyFrames = 0;
|
||||
std::size_t inputReadingFrames = 0;
|
||||
double inputLatestAgeMilliseconds = 0.0;
|
||||
double inputUploadMilliseconds = 0.0;
|
||||
bool inputFormatSupported = true;
|
||||
@@ -40,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
|
||||
@@ -71,12 +90,20 @@ 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;
|
||||
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)
|
||||
{
|
||||
@@ -100,10 +127,21 @@ 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;
|
||||
snapshot.inputUploadMisses = renderMetrics.inputUploadMisses;
|
||||
snapshot.inputReadyFrames = renderMetrics.inputReadyFrames;
|
||||
snapshot.inputReadingFrames = renderMetrics.inputReadingFrames;
|
||||
snapshot.inputLatestAgeMilliseconds = renderMetrics.inputLatestAgeMilliseconds;
|
||||
snapshot.inputUploadMilliseconds = renderMetrics.inputUploadMilliseconds;
|
||||
snapshot.inputFormatSupported = renderMetrics.inputFormatSupported;
|
||||
|
||||
@@ -21,13 +21,28 @@ 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);
|
||||
writer.KeyUInt("inputUploadMisses", snapshot.inputUploadMisses);
|
||||
writer.KeyUInt("inputReadyFrames", static_cast<uint64_t>(snapshot.inputReadyFrames));
|
||||
writer.KeyUInt("inputReadingFrames", static_cast<uint64_t>(snapshot.inputReadingFrames));
|
||||
writer.KeyDouble("inputLatestAgeMs", snapshot.inputLatestAgeMilliseconds);
|
||||
writer.KeyDouble("inputUploadMs", snapshot.inputUploadMilliseconds);
|
||||
writer.KeyBool("inputFormatSupported", snapshot.inputFormatSupported);
|
||||
@@ -46,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());
|
||||
}
|
||||
|
||||
|
||||
@@ -189,7 +189,7 @@ void DeckLinkInput::HandleFrameArrived(IDeckLinkVideoInputFrame* inputFrame)
|
||||
mNoInputSourceFrames.fetch_add(1, std::memory_order_relaxed);
|
||||
bool expected = false;
|
||||
if (mLoggedNoInputSource.compare_exchange_strong(expected, true, std::memory_order_relaxed))
|
||||
LogWarning("decklink-input", "DeckLink input callback reports no input source.");
|
||||
TryLog(LogLevel::Warning, "decklink-input", "DeckLink input callback reports no input source.");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -199,7 +199,7 @@ void DeckLinkInput::HandleFrameArrived(IDeckLinkVideoInputFrame* inputFrame)
|
||||
mUnsupportedFrames.fetch_add(1, std::memory_order_relaxed);
|
||||
bool expected = false;
|
||||
if (mLoggedUnsupportedFrame.compare_exchange_strong(expected, true, std::memory_order_relaxed))
|
||||
LogWarning("decklink-input", "DeckLink input frame dimensions do not match the configured mailbox.");
|
||||
TryLog(LogLevel::Warning, "decklink-input", "DeckLink input frame dimensions do not match the configured mailbox.");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -209,7 +209,7 @@ void DeckLinkInput::HandleFrameArrived(IDeckLinkVideoInputFrame* inputFrame)
|
||||
mUnsupportedFrames.fetch_add(1, std::memory_order_relaxed);
|
||||
bool expected = false;
|
||||
if (mLoggedUnsupportedFrame.compare_exchange_strong(expected, true, std::memory_order_relaxed))
|
||||
LogWarning("decklink-input", "DeckLink input frame does not expose IDeckLinkVideoBuffer.");
|
||||
TryLog(LogLevel::Warning, "decklink-input", "DeckLink input frame does not expose IDeckLinkVideoBuffer.");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -218,7 +218,7 @@ void DeckLinkInput::HandleFrameArrived(IDeckLinkVideoInputFrame* inputFrame)
|
||||
mUnsupportedFrames.fetch_add(1, std::memory_order_relaxed);
|
||||
bool expected = false;
|
||||
if (mLoggedUnsupportedFrame.compare_exchange_strong(expected, true, std::memory_order_relaxed))
|
||||
LogWarning("decklink-input", "DeckLink input frame buffer could not be opened for read access.");
|
||||
TryLog(LogLevel::Warning, "decklink-input", "DeckLink input frame buffer could not be opened for read access.");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -237,13 +237,14 @@ void DeckLinkInput::HandleFrameArrived(IDeckLinkVideoInputFrame* inputFrame)
|
||||
mSubmitMisses.fetch_add(1, std::memory_order_relaxed);
|
||||
bool expected = false;
|
||||
if (mLoggedSubmitMiss.compare_exchange_strong(expected, true, std::memory_order_relaxed))
|
||||
LogWarning("decklink-input", "DeckLink input frame could not be submitted to InputFrameMailbox.");
|
||||
TryLog(LogLevel::Warning, "decklink-input", "DeckLink input frame could not be submitted to InputFrameMailbox.");
|
||||
}
|
||||
mCapturedFrames.fetch_add(1, std::memory_order_relaxed);
|
||||
bool expectedFirstFrame = false;
|
||||
if (mLoggedFirstFrame.compare_exchange_strong(expectedFirstFrame, true, std::memory_order_relaxed))
|
||||
{
|
||||
Log(
|
||||
TryLog(
|
||||
LogLevel::Log,
|
||||
"decklink-input",
|
||||
std::string("First DeckLink ") + (mCapturePixelFormat == bmdFormat8BitBGRA ? "BGRA8" : "UYVY8 raw") + " input frame submitted to InputFrameMailbox.");
|
||||
}
|
||||
@@ -254,7 +255,7 @@ void DeckLinkInput::HandleFrameArrived(IDeckLinkVideoInputFrame* inputFrame)
|
||||
void DeckLinkInput::HandleFormatChanged()
|
||||
{
|
||||
mUnsupportedFrames.fetch_add(1, std::memory_order_relaxed);
|
||||
LogWarning("decklink-input", "DeckLink input format changed; input edge does not auto-switch formats yet.");
|
||||
TryLog(LogLevel::Warning, "decklink-input", "DeckLink input format changed; input edge does not auto-switch formats yet.");
|
||||
}
|
||||
|
||||
bool DeckLinkInput::DiscoverInput(const DeckLinkInputConfig& config, std::string& error)
|
||||
|
||||
@@ -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,12 +77,15 @@ 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;
|
||||
}
|
||||
|
||||
SystemFrame frame;
|
||||
if (!mExchange.ConsumeCompletedForSchedule(frame))
|
||||
{
|
||||
|
||||
@@ -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
|
||||
@@ -282,6 +301,24 @@ Lesson:
|
||||
- test policies such as `one_before_output` or `skip_before_output`
|
||||
- prefer latest-input semantics over draining every pending upload
|
||||
|
||||
### CPU Input Conversion Can Be Worse Than Input Copy
|
||||
|
||||
When DeckLink input only exposed UYVY8 on the test machine, an initial CPU UYVY-to-BGRA conversion in the input callback measured around a full-frame budget on sampled runs and reduced input cadence dramatically.
|
||||
|
||||
Moving the input edge to raw UYVY8 capture changed the ownership:
|
||||
|
||||
- DeckLink callback copies raw supported input bytes into `InputFrameMailbox`
|
||||
- the mailbox keeps latest-frame semantics and uses a contiguous copy when row strides match
|
||||
- the render thread uploads/decodes UYVY8 into the shader-visible `gVideoInput` texture
|
||||
- runtime shaders continue to see decoded input, not packed capture bytes
|
||||
|
||||
Lesson:
|
||||
|
||||
- keep input callbacks as capture/copy edges
|
||||
- keep GL decode/upload in the render-owned path
|
||||
- measure input copy, upload, and decode separately
|
||||
- do not hide expensive format conversion inside the DeckLink callback
|
||||
|
||||
### Preview And Screenshot Must Stay Secondary
|
||||
|
||||
Preview is useful, but DeckLink output is the real-time path.
|
||||
@@ -322,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
|
||||
@@ -369,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,9 +56,12 @@ 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.
|
||||
|
||||
DeckLink input is also an edge, not a renderer. It may capture/copy the latest supported CPU frame into an input mailbox and update input telemetry, but it must not call GL, schedule output, compile shaders, or drive render cadence. Format decode that requires GL belongs to the render-owned input upload path.
|
||||
|
||||
## 4. Runtime Build Work Produces Artifacts
|
||||
|
||||
Runtime shader work is split into two phases:
|
||||
@@ -91,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
|
||||
|
||||
@@ -112,6 +118,12 @@ Good examples:
|
||||
- `completedPollMisses`
|
||||
- `scheduleFailures`
|
||||
- `decklinkBuffered`
|
||||
- `deckLinkScheduleLeadFrames`
|
||||
- `deckLinkScheduleRealignments`
|
||||
- `inputCaptureFps`
|
||||
- `inputSubmitMs`
|
||||
- `inputUploadMs`
|
||||
- `inputConvertMs`
|
||||
- `shaderCommitted`
|
||||
- `shaderFailures`
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -633,10 +633,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:
|
||||
@@ -649,6 +651,93 @@ components:
|
||||
type: number
|
||||
flushedFrameCount:
|
||||
type: number
|
||||
cadence:
|
||||
$ref: "#/components/schemas/CadenceTelemetry"
|
||||
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:
|
||||
type: number
|
||||
inputConsumeMisses:
|
||||
type: number
|
||||
description: Render ticks where no ready input frame was available to upload.
|
||||
inputUploadMisses:
|
||||
type: number
|
||||
description: Input texture upload attempts that reused the previous GL input texture.
|
||||
inputReadyFrames:
|
||||
type: number
|
||||
description: Ready input frames currently queued in the input mailbox.
|
||||
inputReadingFrames:
|
||||
type: number
|
||||
description: Input frames currently protected while render uploads them.
|
||||
inputLatestAgeMs:
|
||||
type: number
|
||||
inputUploadMs:
|
||||
type: number
|
||||
inputCaptureFps:
|
||||
type: number
|
||||
inputConvertMs:
|
||||
type: number
|
||||
inputSubmitMs:
|
||||
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:
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#include "AppConfigProvider.h"
|
||||
|
||||
#include <chrono>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <iostream>
|
||||
@@ -104,6 +105,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)
|
||||
|
||||
@@ -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));
|
||||
@@ -57,32 +64,54 @@ 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 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()
|
||||
@@ -154,16 +183,39 @@ 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();
|
||||
TestCompletedReserveIsBoundedFifo();
|
||||
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
|
||||
@@ -55,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
|
||||
@@ -65,10 +73,21 @@ 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;
|
||||
uint64_t inputUploadMisses = 0;
|
||||
std::size_t inputReadyFrames = 0;
|
||||
std::size_t inputReadingFrames = 0;
|
||||
double inputLatestAgeMilliseconds = 0.0;
|
||||
double inputUploadMilliseconds = 0.0;
|
||||
bool inputFormatSupported = true;
|
||||
@@ -90,20 +109,39 @@ 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;
|
||||
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;
|
||||
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;
|
||||
renderThread.metrics.inputUploadMisses = 4;
|
||||
renderThread.metrics.inputReadyFrames = 1;
|
||||
renderThread.metrics.inputReadingFrames = 0;
|
||||
renderThread.metrics.inputLatestAgeMilliseconds = 4.5;
|
||||
renderThread.metrics.inputUploadMilliseconds = 0.25;
|
||||
renderThread.metrics.inputFormatSupported = true;
|
||||
@@ -114,16 +152,35 @@ 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");
|
||||
Expect(snapshot.inputUploadMisses == 4, "input upload miss count is sampled");
|
||||
Expect(snapshot.inputReadyFrames == 1, "input ready frame count is sampled");
|
||||
Expect(snapshot.inputReadingFrames == 0, "input reading frame count is sampled");
|
||||
Expect(snapshot.inputLatestAgeMilliseconds == 4.5, "input latest age is sampled");
|
||||
Expect(snapshot.inputUploadMilliseconds == 0.25, "input upload timing is sampled");
|
||||
Expect(snapshot.inputFormatSupported, "input format support is sampled");
|
||||
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()
|
||||
@@ -161,13 +218,26 @@ 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;
|
||||
snapshot.inputUploadMisses = 3;
|
||||
snapshot.inputReadyFrames = 1;
|
||||
snapshot.inputReadingFrames = 0;
|
||||
snapshot.inputLatestAgeMilliseconds = 3.5;
|
||||
snapshot.inputUploadMilliseconds = 0.75;
|
||||
snapshot.inputFormatSupported = true;
|
||||
@@ -182,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 =
|
||||
@@ -189,9 +265,17 @@ 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,"
|
||||
"\"inputLatestAgeMs\":3.5,\"inputUploadMs\":0.75,"
|
||||
"\"inputFormatSupported\":true,\"inputSignalPresent\":true,"
|
||||
"\"inputCaptureFps\":59.94,\"inputConvertMs\":4.25,"
|
||||
@@ -199,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");
|
||||
}
|
||||
|
||||
|
||||
@@ -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