Compare commits
11 Commits
a39be6fb20
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3ffb562ff7 | ||
|
|
c2d548499c | ||
|
|
6a0340d1b4 | ||
|
|
5c1fc2a6cf | ||
|
|
d411453f80 | ||
|
|
4a049a557a | ||
|
|
13586c611a | ||
|
|
3a83d9617f | ||
|
|
5c66cfdc64 | ||
|
|
d72272b5a8 | ||
|
|
c25ae7b25b |
@@ -310,12 +310,15 @@ set(RENDER_CADENCE_APP_SOURCES
|
|||||||
"${RENDER_CADENCE_APP_DIR}/app/AppConfigProvider.h"
|
"${RENDER_CADENCE_APP_DIR}/app/AppConfigProvider.h"
|
||||||
"${RENDER_CADENCE_APP_DIR}/app/RenderCadenceApp.h"
|
"${RENDER_CADENCE_APP_DIR}/app/RenderCadenceApp.h"
|
||||||
"${RENDER_CADENCE_APP_DIR}/app/RuntimeLayerController.cpp"
|
"${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}/app/RuntimeLayerController.h"
|
||||||
"${RENDER_CADENCE_APP_DIR}/control/ControlActionResult.h"
|
"${RENDER_CADENCE_APP_DIR}/control/ControlActionResult.h"
|
||||||
"${RENDER_CADENCE_APP_DIR}/control/RuntimeControlCommand.cpp"
|
"${RENDER_CADENCE_APP_DIR}/control/RuntimeControlCommand.cpp"
|
||||||
"${RENDER_CADENCE_APP_DIR}/control/RuntimeControlCommand.h"
|
"${RENDER_CADENCE_APP_DIR}/control/RuntimeControlCommand.h"
|
||||||
"${RENDER_CADENCE_APP_DIR}/control/http/HttpControlServer.cpp"
|
"${RENDER_CADENCE_APP_DIR}/control/http/HttpControlServer.cpp"
|
||||||
"${RENDER_CADENCE_APP_DIR}/control/http/HttpControlServer.h"
|
"${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/http/HttpControlServerWebSocket.cpp"
|
||||||
"${RENDER_CADENCE_APP_DIR}/control/RuntimeStateJson.h"
|
"${RENDER_CADENCE_APP_DIR}/control/RuntimeStateJson.h"
|
||||||
"${RENDER_CADENCE_APP_DIR}/frames/InputFrameMailbox.cpp"
|
"${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/RuntimeShaderParams.h"
|
||||||
"${RENDER_CADENCE_APP_DIR}/render/runtime/RuntimeRenderScene.cpp"
|
"${RENDER_CADENCE_APP_DIR}/render/runtime/RuntimeRenderScene.cpp"
|
||||||
"${RENDER_CADENCE_APP_DIR}/render/runtime/RuntimeRenderScene.h"
|
"${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.cpp"
|
||||||
"${RENDER_CADENCE_APP_DIR}/render/runtime/RuntimeShaderPrepareWorker.h"
|
"${RENDER_CADENCE_APP_DIR}/render/runtime/RuntimeShaderPrepareWorker.h"
|
||||||
"${RENDER_CADENCE_APP_DIR}/render/runtime/RuntimeShaderProgram.h"
|
"${RENDER_CADENCE_APP_DIR}/render/runtime/RuntimeShaderProgram.h"
|
||||||
@@ -973,6 +977,7 @@ add_executable(RenderCadenceCompositorHttpControlServerTests
|
|||||||
"${APP_DIR}/runtime/support/RuntimeJson.cpp"
|
"${APP_DIR}/runtime/support/RuntimeJson.cpp"
|
||||||
"${RENDER_CADENCE_APP_DIR}/control/RuntimeControlCommand.cpp"
|
"${RENDER_CADENCE_APP_DIR}/control/RuntimeControlCommand.cpp"
|
||||||
"${RENDER_CADENCE_APP_DIR}/control/http/HttpControlServer.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}/control/http/HttpControlServerWebSocket.cpp"
|
||||||
"${RENDER_CADENCE_APP_DIR}/json/JsonWriter.cpp"
|
"${RENDER_CADENCE_APP_DIR}/json/JsonWriter.cpp"
|
||||||
"${RENDER_CADENCE_APP_DIR}/logging/Logger.cpp"
|
"${RENDER_CADENCE_APP_DIR}/logging/Logger.cpp"
|
||||||
|
|||||||
@@ -54,6 +54,12 @@ struct VideoIOState
|
|||||||
uint64_t actualDeckLinkBufferedFrames = 0;
|
uint64_t actualDeckLinkBufferedFrames = 0;
|
||||||
double deckLinkScheduleCallMilliseconds = 0.0;
|
double deckLinkScheduleCallMilliseconds = 0.0;
|
||||||
uint64_t deckLinkScheduleFailureCount = 0;
|
uint64_t deckLinkScheduleFailureCount = 0;
|
||||||
|
bool deckLinkScheduleLeadAvailable = false;
|
||||||
|
int64_t deckLinkPlaybackStreamTime = 0;
|
||||||
|
uint64_t deckLinkPlaybackFrameIndex = 0;
|
||||||
|
uint64_t deckLinkNextScheduleFrameIndex = 0;
|
||||||
|
int64_t deckLinkScheduleLeadFrames = 0;
|
||||||
|
uint64_t deckLinkScheduleRealignmentCount = 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
struct VideoIOFrame
|
struct VideoIOFrame
|
||||||
|
|||||||
@@ -32,6 +32,17 @@ VideoIOScheduleTime VideoPlayoutScheduler::NextScheduleTime()
|
|||||||
return time;
|
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)
|
VideoPlayoutRecoveryDecision VideoPlayoutScheduler::AccountForCompletionResult(VideoIOCompletionResult result, uint64_t readyQueueDepth)
|
||||||
{
|
{
|
||||||
++mCompletedFrameIndex;
|
++mCompletedFrameIndex;
|
||||||
|
|||||||
@@ -12,10 +12,12 @@ public:
|
|||||||
void Configure(int64_t frameDuration, int64_t timeScale, const VideoPlayoutPolicy& policy);
|
void Configure(int64_t frameDuration, int64_t timeScale, const VideoPlayoutPolicy& policy);
|
||||||
void Reset();
|
void Reset();
|
||||||
VideoIOScheduleTime NextScheduleTime();
|
VideoIOScheduleTime NextScheduleTime();
|
||||||
|
void AlignNextScheduleTimeToPlayback(int64_t streamTime, uint64_t leadFrames);
|
||||||
VideoPlayoutRecoveryDecision AccountForCompletionResult(VideoIOCompletionResult result, uint64_t readyQueueDepth = 0);
|
VideoPlayoutRecoveryDecision AccountForCompletionResult(VideoIOCompletionResult result, uint64_t readyQueueDepth = 0);
|
||||||
double FrameBudgetMilliseconds() const;
|
double FrameBudgetMilliseconds() const;
|
||||||
uint64_t ScheduledFrameIndex() const { return mScheduledFrameIndex; }
|
uint64_t ScheduledFrameIndex() const { return mScheduledFrameIndex; }
|
||||||
uint64_t CompletedFrameIndex() const { return mCompletedFrameIndex; }
|
uint64_t CompletedFrameIndex() const { return mCompletedFrameIndex; }
|
||||||
|
int64_t FrameDuration() const { return mFrameDuration; }
|
||||||
uint64_t LateStreak() const { return mLateStreak; }
|
uint64_t LateStreak() const { return mLateStreak; }
|
||||||
uint64_t DropStreak() const { return mDropStreak; }
|
uint64_t DropStreak() const { return mDropStreak; }
|
||||||
int64_t TimeScale() const { return mTimeScale; }
|
int64_t TimeScale() const { return mTimeScale; }
|
||||||
|
|||||||
@@ -12,6 +12,9 @@
|
|||||||
|
|
||||||
namespace
|
namespace
|
||||||
{
|
{
|
||||||
|
constexpr int64_t kMinimumHealthyScheduleLeadFrames = 4;
|
||||||
|
constexpr int64_t kProactiveScheduleLeadFloorFrames = 1;
|
||||||
|
|
||||||
class SystemMemoryDeckLinkVideoBuffer : public IDeckLinkVideoBuffer
|
class SystemMemoryDeckLinkVideoBuffer : public IDeckLinkVideoBuffer
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
@@ -526,13 +529,21 @@ bool DeckLinkSession::PopulateOutputFrame(IDeckLinkMutableVideoFrame* outputVide
|
|||||||
|
|
||||||
bool DeckLinkSession::ScheduleFrame(IDeckLinkMutableVideoFrame* outputVideoFrame)
|
bool DeckLinkSession::ScheduleFrame(IDeckLinkMutableVideoFrame* outputVideoFrame)
|
||||||
{
|
{
|
||||||
const VideoIOScheduleTime scheduleTime = mScheduler.NextScheduleTime();
|
|
||||||
if (outputVideoFrame == nullptr || output == nullptr)
|
if (outputVideoFrame == nullptr || output == nullptr)
|
||||||
{
|
{
|
||||||
++mState.deckLinkScheduleFailureCount;
|
++mState.deckLinkScheduleFailureCount;
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (mScheduleRealignmentPending)
|
||||||
|
{
|
||||||
|
RealignScheduleCursorToPlayback();
|
||||||
|
mScheduleRealignmentPending = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
UpdateScheduleLeadTelemetry();
|
||||||
|
MaybeRealignScheduleCursorForLowLead();
|
||||||
|
const VideoIOScheduleTime scheduleTime = mScheduler.NextScheduleTime();
|
||||||
const auto scheduleStart = std::chrono::steady_clock::now();
|
const auto scheduleStart = std::chrono::steady_clock::now();
|
||||||
const HRESULT result = output->ScheduleVideoFrame(outputVideoFrame, scheduleTime.streamTime, scheduleTime.duration, scheduleTime.timeScale);
|
const HRESULT result = output->ScheduleVideoFrame(outputVideoFrame, scheduleTime.streamTime, scheduleTime.duration, scheduleTime.timeScale);
|
||||||
const auto scheduleEnd = std::chrono::steady_clock::now();
|
const auto scheduleEnd = std::chrono::steady_clock::now();
|
||||||
@@ -543,6 +554,67 @@ bool DeckLinkSession::ScheduleFrame(IDeckLinkMutableVideoFrame* outputVideoFrame
|
|||||||
return result == S_OK;
|
return result == S_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void DeckLinkSession::UpdateScheduleLeadTelemetry()
|
||||||
|
{
|
||||||
|
if (output == nullptr)
|
||||||
|
{
|
||||||
|
mState.deckLinkScheduleLeadAvailable = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
BMDTimeValue streamTime = 0;
|
||||||
|
double playbackSpeed = 0.0;
|
||||||
|
if (output->GetScheduledStreamTime(mScheduler.TimeScale(), &streamTime, &playbackSpeed) != S_OK || playbackSpeed <= 0.0)
|
||||||
|
{
|
||||||
|
mState.deckLinkScheduleLeadAvailable = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const uint64_t playbackFrameIndex = streamTime >= 0 && mScheduler.FrameDuration() > 0
|
||||||
|
? static_cast<uint64_t>(streamTime / mScheduler.FrameDuration())
|
||||||
|
: 0;
|
||||||
|
const uint64_t nextScheduleFrameIndex = mScheduler.ScheduledFrameIndex();
|
||||||
|
mState.deckLinkScheduleLeadAvailable = true;
|
||||||
|
mState.deckLinkPlaybackStreamTime = streamTime;
|
||||||
|
mState.deckLinkPlaybackFrameIndex = playbackFrameIndex;
|
||||||
|
mState.deckLinkNextScheduleFrameIndex = nextScheduleFrameIndex;
|
||||||
|
mState.deckLinkScheduleLeadFrames = static_cast<int64_t>(nextScheduleFrameIndex) - static_cast<int64_t>(playbackFrameIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
void DeckLinkSession::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)
|
bool DeckLinkSession::ScheduleSystemMemoryFrame(const VideoIOOutputFrame& frame)
|
||||||
{
|
{
|
||||||
if (output == nullptr || frame.bytes == nullptr || frame.rowBytes <= 0 || frame.height == 0)
|
if (output == nullptr || frame.bytes == nullptr || frame.rowBytes <= 0 || frame.height == 0)
|
||||||
@@ -827,6 +899,18 @@ void DeckLinkSession::HandlePlayoutFrameCompleted(IDeckLinkVideoFrame* completed
|
|||||||
|
|
||||||
VideoIOCompletion completion;
|
VideoIOCompletion completion;
|
||||||
completion.result = TranslateCompletionResult(completionResult);
|
completion.result = TranslateCompletionResult(completionResult);
|
||||||
|
if (completion.result == VideoIOCompletionResult::DisplayedLate || completion.result == VideoIOCompletionResult::Dropped)
|
||||||
|
{
|
||||||
|
if (mScheduleRealignmentArmed)
|
||||||
|
{
|
||||||
|
mScheduleRealignmentPending = true;
|
||||||
|
mScheduleRealignmentArmed = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (completion.result == VideoIOCompletionResult::Completed)
|
||||||
|
{
|
||||||
|
mScheduleRealignmentArmed = true;
|
||||||
|
}
|
||||||
completion.outputFrameBuffer = completedSystemBuffer;
|
completion.outputFrameBuffer = completedSystemBuffer;
|
||||||
mOutputFrameCallback(completion);
|
mOutputFrameCallback(completion);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -75,6 +75,9 @@ private:
|
|||||||
bool AcquireNextOutputVideoFrame(CComPtr<IDeckLinkMutableVideoFrame>& outputVideoFrame);
|
bool AcquireNextOutputVideoFrame(CComPtr<IDeckLinkMutableVideoFrame>& outputVideoFrame);
|
||||||
bool PopulateOutputFrame(IDeckLinkMutableVideoFrame* outputVideoFrame, VideoIOOutputFrame& frame);
|
bool PopulateOutputFrame(IDeckLinkMutableVideoFrame* outputVideoFrame, VideoIOOutputFrame& frame);
|
||||||
bool ScheduleFrame(IDeckLinkMutableVideoFrame* outputVideoFrame);
|
bool ScheduleFrame(IDeckLinkMutableVideoFrame* outputVideoFrame);
|
||||||
|
void UpdateScheduleLeadTelemetry();
|
||||||
|
void MaybeRealignScheduleCursorForLowLead();
|
||||||
|
void RealignScheduleCursorToPlayback();
|
||||||
bool ScheduleSystemMemoryFrame(const VideoIOOutputFrame& frame);
|
bool ScheduleSystemMemoryFrame(const VideoIOOutputFrame& frame);
|
||||||
bool ScheduleBlackFrame(IDeckLinkMutableVideoFrame* outputVideoFrame);
|
bool ScheduleBlackFrame(IDeckLinkMutableVideoFrame* outputVideoFrame);
|
||||||
void RefreshBufferedVideoFrameCount();
|
void RefreshBufferedVideoFrameCount();
|
||||||
@@ -91,6 +94,9 @@ private:
|
|||||||
VideoIOState mState;
|
VideoIOState mState;
|
||||||
VideoPlayoutPolicy mPlayoutPolicy;
|
VideoPlayoutPolicy mPlayoutPolicy;
|
||||||
VideoPlayoutScheduler mScheduler;
|
VideoPlayoutScheduler mScheduler;
|
||||||
|
bool mScheduleRealignmentPending = false;
|
||||||
|
bool mScheduleRealignmentArmed = true;
|
||||||
|
bool mProactiveScheduleRealignmentArmed = true;
|
||||||
InputFrameCallback mInputFrameCallback;
|
InputFrameCallback mInputFrameCallback;
|
||||||
OutputFrameCallback mOutputFrameCallback;
|
OutputFrameCallback mOutputFrameCallback;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -11,21 +11,22 @@ Before adding features here, read the guardrails in [Render Cadence Golden Rules
|
|||||||
```text
|
```text
|
||||||
RenderThread
|
RenderThread
|
||||||
owns a hidden OpenGL context
|
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
|
uploads input frames into a render-owned GL texture
|
||||||
renders simple BGRA8 motion at selected cadence
|
renders simple BGRA8 motion at selected cadence
|
||||||
queues async PBO readback
|
queues async PBO readback
|
||||||
publishes completed frames into SystemFrameExchange
|
publishes completed frames into SystemFrameExchange
|
||||||
|
|
||||||
InputFrameMailbox
|
InputFrameMailbox
|
||||||
owns latest disposable CPU input slots
|
owns bounded FIFO CPU input slots
|
||||||
drops older unsampled input frames when newer frames arrive
|
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
|
protects the one frame currently being uploaded by render
|
||||||
uses a single contiguous copy when capture row stride matches mailbox row stride
|
uses a single contiguous copy when capture row stride matches mailbox row stride
|
||||||
|
|
||||||
SystemFrameExchange
|
SystemFrameExchange
|
||||||
owns Free / Rendering / Completed / Scheduled slots
|
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
|
protects scheduled frames until DeckLink completion
|
||||||
|
|
||||||
DeckLinkOutputThread
|
DeckLinkOutputThread
|
||||||
@@ -34,7 +35,7 @@ DeckLinkOutputThread
|
|||||||
never renders
|
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
|
## Current Scope
|
||||||
|
|
||||||
@@ -46,12 +47,14 @@ Included now:
|
|||||||
- hidden render-thread-owned OpenGL context
|
- hidden render-thread-owned OpenGL context
|
||||||
- simple smooth-motion renderer
|
- simple smooth-motion renderer
|
||||||
- BGRA8-only output
|
- 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
|
- 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
|
- render-thread-owned input texture upload
|
||||||
- async PBO readback
|
- async PBO readback
|
||||||
- latest-N system-memory frame exchange
|
- bounded FIFO system-memory frame exchange
|
||||||
- rendered-frame warmup
|
- 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`
|
- background Slang compile of `shaders/happy-accident`
|
||||||
- app-owned display/render layer model for shader build readiness
|
- app-owned display/render layer model for shader build readiness
|
||||||
- app-owned submission of a completed shader artifact
|
- app-owned submission of a completed shader artifact
|
||||||
@@ -121,8 +124,9 @@ This tracks parity with `apps/LoopThroughWithOpenGLCompositing`.
|
|||||||
- [x] Trigger parameter pulse count/time for latest trigger events
|
- [x] Trigger parameter pulse count/time for latest trigger events
|
||||||
- [x] Optional DeckLink input capture
|
- [x] Optional DeckLink input capture
|
||||||
- [x] UYVY8 input capture with render-thread GPU decode to shader input texture
|
- [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] 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] Render-owned input texture upload
|
||||||
- [x] Runtime shaders receive input through `gVideoInput`
|
- [x] Runtime shaders receive input through `gVideoInput`
|
||||||
- [x] Live DeckLink input bound to `gVideoInput`
|
- [x] Live DeckLink input bound to `gVideoInput`
|
||||||
@@ -234,7 +238,7 @@ DeckLink output is an optional edge service in this app.
|
|||||||
Startup order is:
|
Startup order is:
|
||||||
|
|
||||||
1. start render thread
|
1. start render thread
|
||||||
2. 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
|
3. try to attach DeckLink output
|
||||||
4. start telemetry and HTTP either way
|
4. start telemetry and HTTP either way
|
||||||
|
|
||||||
@@ -252,22 +256,48 @@ Startup order is:
|
|||||||
2. try to attach DeckLink input for the configured input mode
|
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
|
3. prefer BGRA8 capture, otherwise accept raw UYVY8 capture and configure the mailbox for UYVY8 bytes
|
||||||
4. start `DeckLinkInputThread`
|
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. The mailbox uses one contiguous copy when the capture row stride matches the configured mailbox row stride, and falls back to row-by-row copy only for padded or mismatched frames. Unsupported input modes or formats outside BGRA8/UYVY8 are reported explicitly and treated as an unavailable edge rather than silently converted.
|
`DeckLinkInput` and `DeckLinkInputThread` are deliberately narrow. They capture BGRA8 frames directly or raw UYVY8 frames into `InputFrameMailbox`; they do not call GL, render, preview, screenshot, shader, or output scheduling code. UYVY8-to-RGBA decode happens later inside the render-thread-owned input texture upload path, so the DeckLink callback stays a capture/copy edge only. The render upload path consumes the oldest ready input frame from a bounded three-ready-frame queue, so the input behaves like a small jitter buffer instead of a latest-frame preview mailbox. The mailbox trims older frames beyond that bound to avoid runaway latency, uses one contiguous copy when the capture row stride matches the configured mailbox row stride, and falls back to row-by-row copy only for padded or mismatched frames. Unsupported input modes or formats outside BGRA8/UYVY8 are reported explicitly and treated as an unavailable edge rather than silently converted.
|
||||||
|
|
||||||
|
Input warmup is startup-only and bounded. It may delay render-thread startup for a short window, but it does not add waits to the steady-state render cadence loop.
|
||||||
|
|
||||||
The app samples telemetry once per second.
|
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:
|
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
|
- warning when schedule failures increase
|
||||||
- error when the app/DeckLink output buffer is starved
|
- 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:
|
Input telemetry:
|
||||||
|
|
||||||
- `inputFramesReceived`: frames accepted into `InputFrameMailbox`
|
- `inputFramesReceived`: frames accepted into `InputFrameMailbox`
|
||||||
- `inputFramesDropped`: ready input frames dropped or missed because the mailbox was full
|
- `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
|
- `inputLatestAgeMs`: age of the newest submitted input frame
|
||||||
- `inputUploadMs`: render-thread GL upload/decode submission time for the latest uploaded 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
|
- `inputFormatSupported`: whether the latest frame reaching the render upload path was BGRA8 or UYVY8 compatible
|
||||||
@@ -280,7 +310,7 @@ Input telemetry:
|
|||||||
- `inputUnsupportedFrames`: input frames rejected before mailbox submission
|
- `inputUnsupportedFrames`: input frames rejected before mailbox submission
|
||||||
- `inputSubmitMisses`: input frames that could not be submitted to the mailbox
|
- `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:
|
Healthy first-run signs:
|
||||||
|
|
||||||
@@ -289,6 +319,8 @@ Healthy first-run signs:
|
|||||||
- `scheduleFps` is close to the selected cadence after warmup
|
- `scheduleFps` is close to the selected cadence after warmup
|
||||||
- `scheduled` stays near 4
|
- `scheduled` stays near 4
|
||||||
- `decklinkBuffered` stays near 4 when available
|
- `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
|
- `late` and `dropped` do not increase continuously
|
||||||
- `scheduleFailures` does not increase
|
- `scheduleFailures` does not increase
|
||||||
- `shaderCommitted` becomes `1` after the background Happy Accident compile completes
|
- `shaderCommitted` becomes `1` after the background Happy Accident compile completes
|
||||||
@@ -363,6 +395,7 @@ Read:
|
|||||||
- render cadence and DeckLink schedule cadence both held roughly 60 fps
|
- render cadence and DeckLink schedule cadence both held roughly 60 fps
|
||||||
- app scheduled depth stayed at 4
|
- app scheduled depth stayed at 4
|
||||||
- actual DeckLink buffered 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
|
- no late frames, dropped frames, or schedule failures were observed
|
||||||
- completed poll misses were benign because playout remained fully fed
|
- completed poll misses were benign because playout remained fully fed
|
||||||
|
|
||||||
@@ -382,8 +415,8 @@ This app keeps the same core behavior but splits it into modules that can grow:
|
|||||||
- `frames/`: system-memory handoff
|
- `frames/`: system-memory handoff
|
||||||
- `platform/`: COM/Win32/hidden GL context support
|
- `platform/`: COM/Win32/hidden GL context support
|
||||||
- `render/`: cadence thread, clock, and simple renderer
|
- `render/`: cadence thread, clock, and simple renderer
|
||||||
- `frames/InputFrameMailbox`: non-blocking latest-frame CPU input handoff with contiguous-copy fast path for matching row strides
|
- `frames/InputFrameMailbox`: non-blocking bounded FIFO CPU input handoff with contiguous-copy fast path for matching row strides
|
||||||
- `render/InputFrameTexture`: render-thread-owned upload of the latest CPU input frame into GL, including raw UYVY8 decode into the shader-visible input texture
|
- `render/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/readback/`: PBO-backed BGRA8 readback and completed-frame publication
|
||||||
- `render/runtime/RuntimeRenderScene`: render-thread-owned GL scene for ready runtime shader layers
|
- `render/runtime/RuntimeRenderScene`: render-thread-owned GL scene for ready runtime shader layers
|
||||||
- `render/runtime/RuntimeShaderPrepareWorker`: shared-context runtime shader program compile/link worker
|
- `render/runtime/RuntimeShaderPrepareWorker`: shared-context runtime shader program compile/link worker
|
||||||
|
|||||||
@@ -12,12 +12,18 @@
|
|||||||
|
|
||||||
#include <windows.h>
|
#include <windows.h>
|
||||||
|
|
||||||
|
#include <chrono>
|
||||||
#include <iostream>
|
#include <iostream>
|
||||||
#include <sstream>
|
#include <sstream>
|
||||||
#include <string>
|
#include <string>
|
||||||
|
#include <thread>
|
||||||
|
|
||||||
namespace
|
namespace
|
||||||
{
|
{
|
||||||
|
constexpr std::size_t kDeckLinkTargetBufferedFrames = 4;
|
||||||
|
constexpr std::size_t kReadbackDepth = 6;
|
||||||
|
constexpr std::size_t kWritableOutputReserveFrames = kReadbackDepth + 2;
|
||||||
|
|
||||||
class ComInitGuard
|
class ComInitGuard
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
@@ -41,6 +47,19 @@ private:
|
|||||||
bool mInitialized = false;
|
bool mInitialized = false;
|
||||||
HRESULT mResult = S_OK;
|
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)
|
int main(int argc, char** argv)
|
||||||
@@ -80,7 +99,11 @@ int main(int argc, char** argv)
|
|||||||
frameExchangeConfig.height);
|
frameExchangeConfig.height);
|
||||||
frameExchangeConfig.pixelFormat = VideoIOPixelFormat::Bgra8;
|
frameExchangeConfig.pixelFormat = VideoIOPixelFormat::Bgra8;
|
||||||
frameExchangeConfig.rowBytes = VideoIORowBytes(frameExchangeConfig.pixelFormat, frameExchangeConfig.width);
|
frameExchangeConfig.rowBytes = VideoIORowBytes(frameExchangeConfig.pixelFormat, frameExchangeConfig.width);
|
||||||
frameExchangeConfig.capacity = 12;
|
frameExchangeConfig.capacity =
|
||||||
|
appConfig.warmupCompletedFrames +
|
||||||
|
kDeckLinkTargetBufferedFrames +
|
||||||
|
kWritableOutputReserveFrames;
|
||||||
|
frameExchangeConfig.maxCompletedFrames = appConfig.warmupCompletedFrames;
|
||||||
|
|
||||||
SystemFrameExchange frameExchange(frameExchangeConfig);
|
SystemFrameExchange frameExchange(frameExchangeConfig);
|
||||||
|
|
||||||
@@ -92,17 +115,31 @@ int main(int argc, char** argv)
|
|||||||
inputMailboxConfig.pixelFormat = VideoIOPixelFormat::Bgra8;
|
inputMailboxConfig.pixelFormat = VideoIOPixelFormat::Bgra8;
|
||||||
inputMailboxConfig.rowBytes = VideoIORowBytes(inputMailboxConfig.pixelFormat, inputMailboxConfig.width);
|
inputMailboxConfig.rowBytes = VideoIORowBytes(inputMailboxConfig.pixelFormat, inputMailboxConfig.width);
|
||||||
inputMailboxConfig.capacity = 4;
|
inputMailboxConfig.capacity = 4;
|
||||||
|
inputMailboxConfig.maxReadyFrames = 3;
|
||||||
InputFrameMailbox inputMailbox(inputMailboxConfig);
|
InputFrameMailbox inputMailbox(inputMailboxConfig);
|
||||||
|
|
||||||
VideoFormat inputVideoMode;
|
VideoFormat inputVideoMode;
|
||||||
|
VideoFormat outputVideoMode;
|
||||||
std::string inputVideoModeError;
|
std::string inputVideoModeError;
|
||||||
const bool inputVideoModeResolved = ResolveConfiguredVideoFormat(appConfig.inputVideoFormat, appConfig.inputFrameRate, inputVideoMode);
|
const bool inputVideoModeResolved = ResolveConfiguredVideoFormat(appConfig.inputVideoFormat, appConfig.inputFrameRate, inputVideoMode);
|
||||||
|
const bool outputVideoModeResolved = ResolveConfiguredVideoFormat(appConfig.outputVideoFormat, appConfig.outputFrameRate, outputVideoMode);
|
||||||
if (!inputVideoModeResolved)
|
if (!inputVideoModeResolved)
|
||||||
{
|
{
|
||||||
inputVideoModeError = "Unsupported DeckLink inputVideoFormat/inputFrameRate in config/runtime-host.json: " +
|
inputVideoModeError = "Unsupported DeckLink inputVideoFormat/inputFrameRate in config/runtime-host.json: " +
|
||||||
appConfig.inputVideoFormat + " / " + appConfig.inputFrameRate;
|
appConfig.inputVideoFormat + " / " + appConfig.inputFrameRate;
|
||||||
RenderCadenceCompositor::LogWarning("app", inputVideoModeError);
|
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::DeckLinkInput deckLinkInput(inputMailbox);
|
||||||
RenderCadenceCompositor::DeckLinkInputThread deckLinkInputThread(deckLinkInput);
|
RenderCadenceCompositor::DeckLinkInputThread deckLinkInputThread(deckLinkInput);
|
||||||
@@ -123,6 +160,25 @@ int main(int argc, char** argv)
|
|||||||
{
|
{
|
||||||
deckLinkInputStarted = true;
|
deckLinkInputStarted = true;
|
||||||
RenderCadenceCompositor::Log("app", "DeckLink input edge started for " + inputVideoMode.displayName + ".");
|
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
|
else
|
||||||
{
|
{
|
||||||
@@ -138,8 +194,11 @@ int main(int argc, char** argv)
|
|||||||
RenderThread::Config renderConfig;
|
RenderThread::Config renderConfig;
|
||||||
renderConfig.width = frameExchangeConfig.width;
|
renderConfig.width = frameExchangeConfig.width;
|
||||||
renderConfig.height = frameExchangeConfig.height;
|
renderConfig.height = frameExchangeConfig.height;
|
||||||
renderConfig.frameDurationMilliseconds = RenderCadenceCompositor::FrameDurationMillisecondsFromRateString(appConfig.outputFrameRate);
|
const double fallbackFrameDurationMilliseconds = RenderCadenceCompositor::FrameDurationMillisecondsFromRateString(appConfig.outputFrameRate);
|
||||||
renderConfig.pboDepth = 6;
|
renderConfig.frameDurationMilliseconds = outputVideoModeResolved
|
||||||
|
? RenderCadenceCompositor::FrameDurationMillisecondsFromDisplayMode(outputVideoMode.displayMode, fallbackFrameDurationMilliseconds)
|
||||||
|
: fallbackFrameDurationMilliseconds;
|
||||||
|
renderConfig.pboDepth = kReadbackDepth;
|
||||||
|
|
||||||
RenderThread renderThread(frameExchange, &inputMailbox, renderConfig);
|
RenderThread renderThread(frameExchange, &inputMailbox, renderConfig);
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <cctype>
|
#include <cctype>
|
||||||
|
#include <cstdint>
|
||||||
#include <cstdlib>
|
#include <cstdlib>
|
||||||
#include <fstream>
|
#include <fstream>
|
||||||
#include <sstream>
|
#include <sstream>
|
||||||
@@ -181,6 +182,50 @@ double FrameDurationMillisecondsFromRateString(const std::string& rateText, doub
|
|||||||
return 1000.0 / rate;
|
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)
|
void VideoFormatDimensions(const std::string& formatName, unsigned& width, unsigned& height)
|
||||||
{
|
{
|
||||||
std::string normalized = formatName;
|
std::string normalized = formatName;
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include "AppConfig.h"
|
#include "AppConfig.h"
|
||||||
|
#include "DeckLinkDisplayMode.h"
|
||||||
|
|
||||||
#include <filesystem>
|
#include <filesystem>
|
||||||
#include <string>
|
#include <string>
|
||||||
@@ -27,6 +28,7 @@ private:
|
|||||||
};
|
};
|
||||||
|
|
||||||
double FrameDurationMillisecondsFromRateString(const std::string& rateText, double fallbackRate = 59.94);
|
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);
|
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 FindConfigFile(const std::filesystem::path& relativePath = "config/runtime-host.json");
|
||||||
std::filesystem::path FindRepoPath(const std::filesystem::path& relativePath);
|
std::filesystem::path FindRepoPath(const std::filesystem::path& relativePath);
|
||||||
|
|||||||
@@ -87,10 +87,8 @@ public:
|
|||||||
}
|
}
|
||||||
mRuntimeLayers.StartStartupBuild(mConfig.runtimeShaderId);
|
mRuntimeLayers.StartStartupBuild(mConfig.runtimeShaderId);
|
||||||
|
|
||||||
Log("app", "Waiting for rendered warmup frames.");
|
if (!BuildSettledOutputReserve(error))
|
||||||
if (!mFrameExchange.WaitForCompletedDepth(mConfig.warmupCompletedFrames, mConfig.warmupTimeout))
|
|
||||||
{
|
{
|
||||||
error = "Timed out waiting for rendered warmup frames.";
|
|
||||||
LogError("app", error);
|
LogError("app", error);
|
||||||
Stop();
|
Stop();
|
||||||
return false;
|
return false;
|
||||||
@@ -165,6 +163,25 @@ private:
|
|||||||
mVideoOutputEnabled = true;
|
mVideoOutputEnabled = true;
|
||||||
mVideoOutputStatus = "DeckLink scheduled output running.";
|
mVideoOutputStatus = "DeckLink scheduled output running.";
|
||||||
Log("app", mVideoOutputStatus);
|
Log("app", mVideoOutputStatus);
|
||||||
|
Log(
|
||||||
|
"app",
|
||||||
|
"DeckLink output mode: " + mOutput.State().outputDisplayModeName +
|
||||||
|
", frame budget " + std::to_string(mOutput.State().frameBudgetMilliseconds) + " ms.");
|
||||||
|
}
|
||||||
|
|
||||||
|
bool BuildSettledOutputReserve(std::string& error)
|
||||||
|
{
|
||||||
|
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)
|
void DisableVideoOutput(const std::string& reason)
|
||||||
|
|||||||
@@ -1,11 +1,7 @@
|
|||||||
#include "RuntimeLayerController.h"
|
#include "RuntimeLayerController.h"
|
||||||
|
|
||||||
#include "AppConfigProvider.h"
|
|
||||||
#include "RuntimeJson.h"
|
|
||||||
#include "../logging/Logger.h"
|
#include "../logging/Logger.h"
|
||||||
|
|
||||||
#include <filesystem>
|
|
||||||
|
|
||||||
namespace RenderCadenceCompositor
|
namespace RenderCadenceCompositor
|
||||||
{
|
{
|
||||||
RuntimeLayerController::RuntimeLayerController(RenderLayerPublisher publisher) :
|
RuntimeLayerController::RuntimeLayerController(RenderLayerPublisher publisher) :
|
||||||
@@ -48,137 +44,6 @@ void RuntimeLayerController::Stop()
|
|||||||
StopAllRuntimeShaderBuilds();
|
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
|
RuntimeLayerModelSnapshot RuntimeLayerController::Snapshot(const CadenceTelemetrySnapshot& telemetry) const
|
||||||
{
|
{
|
||||||
std::lock_guard<std::mutex> lock(mRuntimeLayerMutex);
|
std::lock_guard<std::mutex> lock(mRuntimeLayerMutex);
|
||||||
@@ -191,113 +56,6 @@ RuntimeLayerModelSnapshot RuntimeLayerController::Snapshot(const CadenceTelemetr
|
|||||||
return snapshot;
|
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()
|
void RuntimeLayerController::PublishRuntimeRenderLayers()
|
||||||
{
|
{
|
||||||
if (!mPublisher)
|
if (!mPublisher)
|
||||||
@@ -331,31 +89,4 @@ void RuntimeLayerController::MarkRuntimeBuildFailedForLayer(const std::string& l
|
|||||||
LogWarning("runtime-shader", error);
|
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.Key("performance");
|
||||||
writer.BeginObject();
|
writer.BeginObject();
|
||||||
writer.KeyDouble("frameBudgetMs", FrameDurationMillisecondsFromRateString(input.config.outputFrameRate));
|
writer.KeyDouble("frameBudgetMs", FrameDurationMillisecondsFromRateString(input.config.outputFrameRate));
|
||||||
writer.KeyNull("renderMs");
|
writer.KeyDouble("renderMs", input.telemetry.renderFrameMilliseconds);
|
||||||
writer.KeyNull("smoothedRenderMs");
|
writer.KeyNull("smoothedRenderMs");
|
||||||
writer.KeyNull("budgetUsedPercent");
|
writer.KeyDouble("budgetUsedPercent", input.telemetry.renderFrameBudgetUsedPercent);
|
||||||
writer.KeyNull("completionIntervalMs");
|
writer.KeyNull("completionIntervalMs");
|
||||||
writer.KeyNull("smoothedCompletionIntervalMs");
|
writer.KeyNull("smoothedCompletionIntervalMs");
|
||||||
writer.KeyNull("maxCompletionIntervalMs");
|
writer.KeyNull("maxCompletionIntervalMs");
|
||||||
|
|||||||
@@ -1,15 +1,11 @@
|
|||||||
#include "HttpControlServer.h"
|
#include "HttpControlServer.h"
|
||||||
|
|
||||||
#include "../json/JsonWriter.h"
|
|
||||||
#include "../logging/Logger.h"
|
#include "../logging/Logger.h"
|
||||||
|
|
||||||
#include <ws2tcpip.h>
|
#include <ws2tcpip.h>
|
||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <cctype>
|
|
||||||
#include <fstream>
|
|
||||||
#include <sstream>
|
#include <sstream>
|
||||||
#include <vector>
|
|
||||||
|
|
||||||
namespace RenderCadenceCompositor
|
namespace RenderCadenceCompositor
|
||||||
{
|
{
|
||||||
@@ -27,22 +23,6 @@ bool InitializeWinsock(std::string& error)
|
|||||||
return true;
|
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) :
|
UniqueSocket::UniqueSocket(SOCKET socket) :
|
||||||
@@ -260,179 +240,6 @@ HttpControlServer::HttpResponse HttpControlServer::RouteRequest(const HttpReques
|
|||||||
return TextResponse("404 Not Found", "Not Found");
|
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)
|
bool HttpControlServer::ParseHttpRequest(const std::string& rawRequest, HttpRequest& request)
|
||||||
{
|
{
|
||||||
const std::size_t requestLineEnd = rawRequest.find("\r\n");
|
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.frameIndex = frameIndex;
|
||||||
++slot.generation;
|
++slot.generation;
|
||||||
mReadyIndices.push_back(slotIndex);
|
mReadyIndices.push_back(slotIndex);
|
||||||
|
TrimReadyFramesLocked();
|
||||||
++mCounters.submittedFrames;
|
++mCounters.submittedFrames;
|
||||||
mCounters.latestFrameIndex = frameIndex;
|
mCounters.latestFrameIndex = frameIndex;
|
||||||
mCounters.hasSubmittedFrame = true;
|
mCounters.hasSubmittedFrame = true;
|
||||||
@@ -119,27 +120,16 @@ bool InputFrameMailbox::SubmitFrame(const void* bytes, unsigned rowBytes, uint64
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool InputFrameMailbox::TryAcquireLatest(InputFrame& frame)
|
bool InputFrameMailbox::TryAcquireOldest(InputFrame& frame)
|
||||||
{
|
{
|
||||||
std::lock_guard<std::mutex> lock(mMutex);
|
std::lock_guard<std::mutex> lock(mMutex);
|
||||||
while (!mReadyIndices.empty())
|
while (!mReadyIndices.empty())
|
||||||
{
|
{
|
||||||
const std::size_t index = mReadyIndices.back();
|
const std::size_t index = mReadyIndices.front();
|
||||||
mReadyIndices.pop_back();
|
mReadyIndices.pop_front();
|
||||||
if (index >= mSlots.size() || mSlots[index].state != InputFrameSlotState::Ready)
|
if (index >= mSlots.size() || mSlots[index].state != InputFrameSlotState::Ready)
|
||||||
continue;
|
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;
|
mSlots[index].state = InputFrameSlotState::Reading;
|
||||||
FillFrameLocked(index, frame);
|
FillFrameLocked(index, frame);
|
||||||
++mCounters.consumedFrames;
|
++mCounters.consumedFrames;
|
||||||
@@ -246,6 +236,14 @@ bool InputFrameMailbox::DropOldestReadyLocked()
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void InputFrameMailbox::TrimReadyFramesLocked()
|
||||||
|
{
|
||||||
|
if (mConfig.maxReadyFrames == 0)
|
||||||
|
return;
|
||||||
|
while (mReadyIndices.size() > mConfig.maxReadyFrames)
|
||||||
|
DropOldestReadyLocked();
|
||||||
|
}
|
||||||
|
|
||||||
std::size_t InputFrameMailbox::FrameByteCount() const
|
std::size_t InputFrameMailbox::FrameByteCount() const
|
||||||
{
|
{
|
||||||
return static_cast<std::size_t>(mConfig.rowBytes) * static_cast<std::size_t>(mConfig.height);
|
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;
|
VideoIOPixelFormat pixelFormat = VideoIOPixelFormat::Bgra8;
|
||||||
unsigned rowBytes = 0;
|
unsigned rowBytes = 0;
|
||||||
std::size_t capacity = 0;
|
std::size_t capacity = 0;
|
||||||
|
std::size_t maxReadyFrames = 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
struct InputFrame
|
struct InputFrame
|
||||||
@@ -63,7 +64,7 @@ public:
|
|||||||
InputFrameMailboxConfig Config() const;
|
InputFrameMailboxConfig Config() const;
|
||||||
|
|
||||||
bool SubmitFrame(const void* bytes, unsigned rowBytes, uint64_t frameIndex);
|
bool SubmitFrame(const void* bytes, unsigned rowBytes, uint64_t frameIndex);
|
||||||
bool TryAcquireLatest(InputFrame& frame);
|
bool TryAcquireOldest(InputFrame& frame);
|
||||||
bool Release(const InputFrame& frame);
|
bool Release(const InputFrame& frame);
|
||||||
void Clear();
|
void Clear();
|
||||||
InputFrameMailboxMetrics Metrics() const;
|
InputFrameMailboxMetrics Metrics() const;
|
||||||
@@ -80,6 +81,7 @@ private:
|
|||||||
bool IsValidLocked(const InputFrame& frame) const;
|
bool IsValidLocked(const InputFrame& frame) const;
|
||||||
void FillFrameLocked(std::size_t index, InputFrame& frame) const;
|
void FillFrameLocked(std::size_t index, InputFrame& frame) const;
|
||||||
bool DropOldestReadyLocked();
|
bool DropOldestReadyLocked();
|
||||||
|
void TrimReadyFramesLocked();
|
||||||
std::size_t FrameByteCount() const;
|
std::size_t FrameByteCount() const;
|
||||||
|
|
||||||
mutable std::mutex mMutex;
|
mutable std::mutex mMutex;
|
||||||
|
|||||||
@@ -46,14 +46,11 @@ bool SystemFrameExchange::AcquireForRender(SystemFrame& frame)
|
|||||||
{
|
{
|
||||||
std::lock_guard<std::mutex> lock(mMutex);
|
std::lock_guard<std::mutex> lock(mMutex);
|
||||||
if (!AcquireFreeLocked(frame))
|
if (!AcquireFreeLocked(frame))
|
||||||
{
|
|
||||||
if (!DropOldestCompletedLocked() || !AcquireFreeLocked(frame))
|
|
||||||
{
|
{
|
||||||
frame = SystemFrame();
|
frame = SystemFrame();
|
||||||
++mCounters.acquireMisses;
|
++mCounters.acquireMisses;
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
++mCounters.acquiredFrames;
|
++mCounters.acquiredFrames;
|
||||||
return true;
|
return true;
|
||||||
@@ -72,6 +69,7 @@ bool SystemFrameExchange::PublishCompleted(const SystemFrame& frame)
|
|||||||
slot.state = SystemFrameSlotState::Completed;
|
slot.state = SystemFrameSlotState::Completed;
|
||||||
slot.frameIndex = frame.frameIndex;
|
slot.frameIndex = frame.frameIndex;
|
||||||
mCompletedIndices.push_back(frame.index);
|
mCompletedIndices.push_back(frame.index);
|
||||||
|
TrimCompletedLocked();
|
||||||
++mCounters.completedFrames;
|
++mCounters.completedFrames;
|
||||||
mCondition.notify_all();
|
mCondition.notify_all();
|
||||||
return true;
|
return true;
|
||||||
@@ -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()
|
void SystemFrameExchange::Clear()
|
||||||
{
|
{
|
||||||
std::lock_guard<std::mutex> lock(mMutex);
|
std::lock_guard<std::mutex> lock(mMutex);
|
||||||
@@ -210,6 +253,17 @@ bool SystemFrameExchange::DropOldestCompletedLocked()
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void SystemFrameExchange::TrimCompletedLocked()
|
||||||
|
{
|
||||||
|
if (mConfig.maxCompletedFrames == 0)
|
||||||
|
return;
|
||||||
|
while (CompletedCountLocked() > mConfig.maxCompletedFrames)
|
||||||
|
{
|
||||||
|
if (!DropOldestCompletedLocked())
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
bool SystemFrameExchange::IsValidLocked(const SystemFrame& frame) const
|
bool SystemFrameExchange::IsValidLocked(const SystemFrame& frame) const
|
||||||
{
|
{
|
||||||
return frame.index < mSlots.size() && mSlots[frame.index].generation == frame.generation;
|
return frame.index < mSlots.size() && mSlots[frame.index].generation == frame.generation;
|
||||||
|
|||||||
@@ -22,6 +22,10 @@ public:
|
|||||||
bool ConsumeCompletedForSchedule(SystemFrame& frame);
|
bool ConsumeCompletedForSchedule(SystemFrame& frame);
|
||||||
bool ReleaseScheduledByBytes(void* bytes);
|
bool ReleaseScheduledByBytes(void* bytes);
|
||||||
bool WaitForCompletedDepth(std::size_t targetDepth, std::chrono::milliseconds timeout);
|
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();
|
void Clear();
|
||||||
|
|
||||||
SystemFrameExchangeMetrics Metrics() const;
|
SystemFrameExchangeMetrics Metrics() const;
|
||||||
@@ -37,6 +41,7 @@ private:
|
|||||||
|
|
||||||
bool AcquireFreeLocked(SystemFrame& frame);
|
bool AcquireFreeLocked(SystemFrame& frame);
|
||||||
bool DropOldestCompletedLocked();
|
bool DropOldestCompletedLocked();
|
||||||
|
void TrimCompletedLocked();
|
||||||
bool IsValidLocked(const SystemFrame& frame) const;
|
bool IsValidLocked(const SystemFrame& frame) const;
|
||||||
void FillFrameLocked(std::size_t index, SystemFrame& frame);
|
void FillFrameLocked(std::size_t index, SystemFrame& frame);
|
||||||
std::size_t CompletedCountLocked() const;
|
std::size_t CompletedCountLocked() const;
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ struct SystemFrameExchangeConfig
|
|||||||
VideoIOPixelFormat pixelFormat = VideoIOPixelFormat::Bgra8;
|
VideoIOPixelFormat pixelFormat = VideoIOPixelFormat::Bgra8;
|
||||||
unsigned rowBytes = 0;
|
unsigned rowBytes = 0;
|
||||||
std::size_t capacity = 0;
|
std::size_t capacity = 0;
|
||||||
|
std::size_t maxCompletedFrames = 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
struct SystemFrame
|
struct SystemFrame
|
||||||
|
|||||||
@@ -71,7 +71,7 @@ GLuint InputFrameTexture::PollAndUpload(InputFrameMailbox* mailbox)
|
|||||||
return mTexture;
|
return mTexture;
|
||||||
|
|
||||||
InputFrame frame;
|
InputFrame frame;
|
||||||
if (!mailbox->TryAcquireLatest(frame))
|
if (!mailbox->TryAcquireOldest(frame))
|
||||||
{
|
{
|
||||||
++mUploadMisses;
|
++mUploadMisses;
|
||||||
mLastUploadMilliseconds = 0.0;
|
mLastUploadMilliseconds = 0.0;
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ RenderCadenceClock::RenderCadenceClock(double frameDurationMilliseconds)
|
|||||||
void RenderCadenceClock::Reset(TimePoint now)
|
void RenderCadenceClock::Reset(TimePoint now)
|
||||||
{
|
{
|
||||||
mNextRenderTime = now;
|
mNextRenderTime = now;
|
||||||
|
mPendingFrameAdvance = 1;
|
||||||
mOverrunCount = 0;
|
mOverrunCount = 0;
|
||||||
mSkippedFrameCount = 0;
|
mSkippedFrameCount = 0;
|
||||||
}
|
}
|
||||||
@@ -27,10 +28,12 @@ RenderCadenceClock::Tick RenderCadenceClock::Poll(TimePoint now)
|
|||||||
}
|
}
|
||||||
|
|
||||||
tick.due = true;
|
tick.due = true;
|
||||||
|
mPendingFrameAdvance = 1;
|
||||||
const Duration lateBy = now - mNextRenderTime;
|
const Duration lateBy = now - mNextRenderTime;
|
||||||
if (lateBy > mFrameDuration)
|
if (lateBy > mFrameDuration)
|
||||||
{
|
{
|
||||||
tick.skippedFrames = static_cast<uint64_t>(lateBy / mFrameDuration);
|
tick.skippedFrames = static_cast<uint64_t>(lateBy / mFrameDuration);
|
||||||
|
mPendingFrameAdvance += tick.skippedFrames;
|
||||||
++mOverrunCount;
|
++mOverrunCount;
|
||||||
mSkippedFrameCount += tick.skippedFrames;
|
mSkippedFrameCount += tick.skippedFrames;
|
||||||
}
|
}
|
||||||
@@ -39,7 +42,8 @@ RenderCadenceClock::Tick RenderCadenceClock::Poll(TimePoint now)
|
|||||||
|
|
||||||
void RenderCadenceClock::MarkRendered(TimePoint now)
|
void RenderCadenceClock::MarkRendered(TimePoint now)
|
||||||
{
|
{
|
||||||
mNextRenderTime += mFrameDuration;
|
mNextRenderTime += mFrameDuration * mPendingFrameAdvance;
|
||||||
|
mPendingFrameAdvance = 1;
|
||||||
if (now - mNextRenderTime > mFrameDuration * 4)
|
if (now - mNextRenderTime > mFrameDuration * 4)
|
||||||
mNextRenderTime = now + mFrameDuration;
|
mNextRenderTime = now + mFrameDuration;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ public:
|
|||||||
private:
|
private:
|
||||||
Duration mFrameDuration;
|
Duration mFrameDuration;
|
||||||
TimePoint mNextRenderTime = Clock::now();
|
TimePoint mNextRenderTime = Clock::now();
|
||||||
|
uint64_t mPendingFrameAdvance = 1;
|
||||||
uint64_t mOverrunCount = 0;
|
uint64_t mOverrunCount = 0;
|
||||||
uint64_t mSkippedFrameCount = 0;
|
uint64_t mSkippedFrameCount = 0;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -85,8 +85,17 @@ RenderThread::Metrics RenderThread::GetMetrics() const
|
|||||||
metrics.skippedFrames = mSkippedFrames.load(std::memory_order_relaxed);
|
metrics.skippedFrames = mSkippedFrames.load(std::memory_order_relaxed);
|
||||||
metrics.shaderBuildsCommitted = mShaderBuildsCommitted.load(std::memory_order_relaxed);
|
metrics.shaderBuildsCommitted = mShaderBuildsCommitted.load(std::memory_order_relaxed);
|
||||||
metrics.shaderBuildFailures = mShaderBuildFailures.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.inputFramesReceived = mInputFramesReceived.load(std::memory_order_relaxed);
|
||||||
metrics.inputFramesDropped = mInputFramesDropped.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.inputLatestAgeMilliseconds = mInputLatestAgeMilliseconds.load(std::memory_order_relaxed);
|
||||||
metrics.inputUploadMilliseconds = mInputUploadMilliseconds.load(std::memory_order_relaxed);
|
metrics.inputUploadMilliseconds = mInputUploadMilliseconds.load(std::memory_order_relaxed);
|
||||||
metrics.inputFormatSupported = mInputFormatSupported.load(std::memory_order_relaxed);
|
metrics.inputFormatSupported = mInputFormatSupported.load(std::memory_order_relaxed);
|
||||||
@@ -150,6 +159,7 @@ void RenderThread::ThreadMain()
|
|||||||
CountAcquireMiss();
|
CountAcquireMiss();
|
||||||
},
|
},
|
||||||
[this]() { CountCompleted(); });
|
[this]() { CountCompleted(); });
|
||||||
|
PublishReadbackMetrics(readback);
|
||||||
|
|
||||||
const auto now = RenderCadenceClock::Clock::now();
|
const auto now = RenderCadenceClock::Clock::now();
|
||||||
const RenderCadenceClock::Tick tick = clock.Poll(now);
|
const RenderCadenceClock::Tick tick = clock.Poll(now);
|
||||||
@@ -174,6 +184,7 @@ void RenderThread::ThreadMain()
|
|||||||
{
|
{
|
||||||
mPboQueueMisses.fetch_add(1, std::memory_order_relaxed);
|
mPboQueueMisses.fetch_add(1, std::memory_order_relaxed);
|
||||||
}
|
}
|
||||||
|
PublishReadbackMetrics(readback);
|
||||||
|
|
||||||
CountRendered();
|
CountRendered();
|
||||||
++frameIndex;
|
++frameIndex;
|
||||||
@@ -192,6 +203,7 @@ void RenderThread::ThreadMain()
|
|||||||
CountAcquireMiss();
|
CountAcquireMiss();
|
||||||
},
|
},
|
||||||
[this]() { CountCompleted(); });
|
[this]() { CountCompleted(); });
|
||||||
|
PublishReadbackMetrics(readback);
|
||||||
}
|
}
|
||||||
|
|
||||||
readback.Shutdown();
|
readback.Shutdown();
|
||||||
@@ -233,6 +245,29 @@ void RenderThread::CountAcquireMiss()
|
|||||||
mAcquireMisses.fetch_add(1, std::memory_order_relaxed);
|
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)
|
void RenderThread::PublishInputMetrics(const InputFrameTexture& inputTexture)
|
||||||
{
|
{
|
||||||
if (mInputMailbox != nullptr)
|
if (mInputMailbox != nullptr)
|
||||||
@@ -240,6 +275,9 @@ void RenderThread::PublishInputMetrics(const InputFrameTexture& inputTexture)
|
|||||||
const InputFrameMailboxMetrics mailboxMetrics = mInputMailbox->Metrics();
|
const InputFrameMailboxMetrics mailboxMetrics = mInputMailbox->Metrics();
|
||||||
mInputFramesReceived.store(mailboxMetrics.submittedFrames, std::memory_order_relaxed);
|
mInputFramesReceived.store(mailboxMetrics.submittedFrames, std::memory_order_relaxed);
|
||||||
mInputFramesDropped.store(mailboxMetrics.droppedReadyFrames + mailboxMetrics.submitMisses, 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);
|
mInputLatestAgeMilliseconds.store(mailboxMetrics.latestFrameAgeMilliseconds, std::memory_order_relaxed);
|
||||||
mInputSignalPresent.store(mailboxMetrics.hasSubmittedFrame, 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);
|
mInputFramesReceived.store(0, std::memory_order_relaxed);
|
||||||
mInputFramesDropped.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);
|
mInputLatestAgeMilliseconds.store(0.0, std::memory_order_relaxed);
|
||||||
mInputSignalPresent.store(false, 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);
|
mInputUploadMilliseconds.store(inputTexture.LastUploadMilliseconds(), std::memory_order_relaxed);
|
||||||
mInputFormatSupported.store(inputTexture.LastFrameFormatSupported(), std::memory_order_relaxed);
|
mInputFormatSupported.store(inputTexture.LastFrameFormatSupported(), std::memory_order_relaxed);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,6 +16,7 @@
|
|||||||
class SystemFrameExchange;
|
class SystemFrameExchange;
|
||||||
class InputFrameMailbox;
|
class InputFrameMailbox;
|
||||||
class InputFrameTexture;
|
class InputFrameTexture;
|
||||||
|
class Bgra8ReadbackPipeline;
|
||||||
|
|
||||||
class RenderThread
|
class RenderThread
|
||||||
{
|
{
|
||||||
@@ -38,8 +39,17 @@ public:
|
|||||||
uint64_t skippedFrames = 0;
|
uint64_t skippedFrames = 0;
|
||||||
uint64_t shaderBuildsCommitted = 0;
|
uint64_t shaderBuildsCommitted = 0;
|
||||||
uint64_t shaderBuildFailures = 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 inputFramesReceived = 0;
|
||||||
uint64_t inputFramesDropped = 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 inputLatestAgeMilliseconds = 0.0;
|
||||||
double inputUploadMilliseconds = 0.0;
|
double inputUploadMilliseconds = 0.0;
|
||||||
bool inputFormatSupported = true;
|
bool inputFormatSupported = true;
|
||||||
@@ -67,6 +77,7 @@ private:
|
|||||||
void CountRendered();
|
void CountRendered();
|
||||||
void CountCompleted();
|
void CountCompleted();
|
||||||
void CountAcquireMiss();
|
void CountAcquireMiss();
|
||||||
|
void PublishReadbackMetrics(const Bgra8ReadbackPipeline& readback);
|
||||||
void PublishInputMetrics(const InputFrameTexture& inputTexture);
|
void PublishInputMetrics(const InputFrameTexture& inputTexture);
|
||||||
void TryCommitReadyRuntimeShader(RuntimeRenderScene& runtimeRenderScene);
|
void TryCommitReadyRuntimeShader(RuntimeRenderScene& runtimeRenderScene);
|
||||||
bool TryTakePendingRuntimeShaderArtifact(RuntimeShaderArtifact& artifact);
|
bool TryTakePendingRuntimeShaderArtifact(RuntimeShaderArtifact& artifact);
|
||||||
@@ -92,8 +103,17 @@ private:
|
|||||||
std::atomic<uint64_t> mSkippedFrames{ 0 };
|
std::atomic<uint64_t> mSkippedFrames{ 0 };
|
||||||
std::atomic<uint64_t> mShaderBuildsCommitted{ 0 };
|
std::atomic<uint64_t> mShaderBuildsCommitted{ 0 };
|
||||||
std::atomic<uint64_t> mShaderBuildFailures{ 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> mInputFramesReceived{ 0 };
|
||||||
std::atomic<uint64_t> mInputFramesDropped{ 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> mInputLatestAgeMilliseconds{ 0.0 };
|
||||||
std::atomic<double> mInputUploadMilliseconds{ 0.0 };
|
std::atomic<double> mInputUploadMilliseconds{ 0.0 };
|
||||||
std::atomic<bool> mInputFormatSupported{ true };
|
std::atomic<bool> mInputFormatSupported{ true };
|
||||||
|
|||||||
@@ -2,8 +2,18 @@
|
|||||||
|
|
||||||
#include "../frames/SystemFrameTypes.h"
|
#include "../frames/SystemFrameTypes.h"
|
||||||
|
|
||||||
|
#include <chrono>
|
||||||
#include <cstring>
|
#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()
|
Bgra8ReadbackPipeline::~Bgra8ReadbackPipeline()
|
||||||
{
|
{
|
||||||
Shutdown();
|
Shutdown();
|
||||||
@@ -50,10 +60,15 @@ bool Bgra8ReadbackPipeline::RenderAndQueue(uint64_t frameIndex, const RenderCall
|
|||||||
return false;
|
return false;
|
||||||
|
|
||||||
glBindFramebuffer(GL_FRAMEBUFFER, mFramebuffer);
|
glBindFramebuffer(GL_FRAMEBUFFER, mFramebuffer);
|
||||||
|
const auto renderStart = std::chrono::steady_clock::now();
|
||||||
renderFrame(frameIndex);
|
renderFrame(frameIndex);
|
||||||
|
mLastRenderFrameMilliseconds = MillisecondsSince(renderStart);
|
||||||
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
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(
|
void Bgra8ReadbackPipeline::ConsumeCompleted(
|
||||||
@@ -68,12 +83,14 @@ void Bgra8ReadbackPipeline::ConsumeCompleted(
|
|||||||
PboReadbackRing::CompletedReadback readback;
|
PboReadbackRing::CompletedReadback readback;
|
||||||
while (mPboRing.TryAcquireCompleted(readback))
|
while (mPboRing.TryAcquireCompleted(readback))
|
||||||
{
|
{
|
||||||
|
const auto copyStart = std::chrono::steady_clock::now();
|
||||||
glBindBuffer(GL_PIXEL_PACK_BUFFER, readback.pbo);
|
glBindBuffer(GL_PIXEL_PACK_BUFFER, readback.pbo);
|
||||||
void* mapped = glMapBuffer(GL_PIXEL_PACK_BUFFER, GL_READ_ONLY);
|
void* mapped = glMapBuffer(GL_PIXEL_PACK_BUFFER, GL_READ_ONLY);
|
||||||
if (!mapped)
|
if (!mapped)
|
||||||
{
|
{
|
||||||
glBindBuffer(GL_PIXEL_PACK_BUFFER, 0);
|
glBindBuffer(GL_PIXEL_PACK_BUFFER, 0);
|
||||||
mPboRing.ReleaseCompleted(readback);
|
mPboRing.ReleaseCompleted(readback);
|
||||||
|
mLastCompletedReadbackCopyMilliseconds = MillisecondsSince(copyStart);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -99,6 +116,7 @@ void Bgra8ReadbackPipeline::ConsumeCompleted(
|
|||||||
glUnmapBuffer(GL_PIXEL_PACK_BUFFER);
|
glUnmapBuffer(GL_PIXEL_PACK_BUFFER);
|
||||||
glBindBuffer(GL_PIXEL_PACK_BUFFER, 0);
|
glBindBuffer(GL_PIXEL_PACK_BUFFER, 0);
|
||||||
mPboRing.ReleaseCompleted(readback);
|
mPboRing.ReleaseCompleted(readback);
|
||||||
|
mLastCompletedReadbackCopyMilliseconds = MillisecondsSince(copyStart);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -38,6 +38,9 @@ public:
|
|||||||
unsigned RowBytes() const { return mRowBytes; }
|
unsigned RowBytes() const { return mRowBytes; }
|
||||||
VideoIOPixelFormat PixelFormat() const { return VideoIOPixelFormat::Bgra8; }
|
VideoIOPixelFormat PixelFormat() const { return VideoIOPixelFormat::Bgra8; }
|
||||||
uint64_t PboQueueMisses() const { return mPboRing.QueueMisses(); }
|
uint64_t PboQueueMisses() const { return mPboRing.QueueMisses(); }
|
||||||
|
double LastRenderFrameMilliseconds() const { return mLastRenderFrameMilliseconds; }
|
||||||
|
double LastReadbackQueueMilliseconds() const { return mLastReadbackQueueMilliseconds; }
|
||||||
|
double LastCompletedReadbackCopyMilliseconds() const { return mLastCompletedReadbackCopyMilliseconds; }
|
||||||
|
|
||||||
private:
|
private:
|
||||||
bool CreateRenderTarget();
|
bool CreateRenderTarget();
|
||||||
@@ -48,5 +51,8 @@ private:
|
|||||||
unsigned mRowBytes = 0;
|
unsigned mRowBytes = 0;
|
||||||
GLuint mFramebuffer = 0;
|
GLuint mFramebuffer = 0;
|
||||||
GLuint mTexture = 0;
|
GLuint mTexture = 0;
|
||||||
|
double mLastRenderFrameMilliseconds = 0.0;
|
||||||
|
double mLastReadbackQueueMilliseconds = 0.0;
|
||||||
|
double mLastCompletedReadbackCopyMilliseconds = 0.0;
|
||||||
PboReadbackRing mPboRing;
|
PboReadbackRing mPboRing;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -102,84 +102,6 @@ bool RuntimeRenderScene::CommitRenderLayers(const std::vector<RenderCadenceCompo
|
|||||||
return true;
|
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 decoded 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()
|
void RuntimeRenderScene::ShutdownGl()
|
||||||
{
|
{
|
||||||
mPrepareWorker.Stop();
|
mPrepareWorker.Stop();
|
||||||
@@ -307,139 +229,6 @@ void RuntimeRenderScene::TryCommitPendingPrograms(LayerProgram& layer)
|
|||||||
layer.pendingPreparedPrograms.clear();
|
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)
|
RuntimeRenderScene::LayerProgram* RuntimeRenderScene::FindLayer(const std::string& layerId)
|
||||||
{
|
{
|
||||||
for (LayerProgram& layer : mLayers)
|
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 scheduledTotal = 0;
|
||||||
uint64_t completedPollMisses = 0;
|
uint64_t completedPollMisses = 0;
|
||||||
uint64_t scheduleFailures = 0;
|
uint64_t scheduleFailures = 0;
|
||||||
|
uint64_t completedDrops = 0;
|
||||||
|
uint64_t acquireMisses = 0;
|
||||||
uint64_t completions = 0;
|
uint64_t completions = 0;
|
||||||
uint64_t displayedLate = 0;
|
uint64_t displayedLate = 0;
|
||||||
uint64_t dropped = 0;
|
uint64_t dropped = 0;
|
||||||
|
uint64_t clockOverruns = 0;
|
||||||
|
uint64_t clockSkippedFrames = 0;
|
||||||
uint64_t shaderBuildsCommitted = 0;
|
uint64_t shaderBuildsCommitted = 0;
|
||||||
uint64_t shaderBuildFailures = 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 inputFramesReceived = 0;
|
||||||
uint64_t inputFramesDropped = 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 inputLatestAgeMilliseconds = 0.0;
|
||||||
double inputUploadMilliseconds = 0.0;
|
double inputUploadMilliseconds = 0.0;
|
||||||
bool inputFormatSupported = true;
|
bool inputFormatSupported = true;
|
||||||
@@ -40,6 +53,12 @@ struct CadenceTelemetrySnapshot
|
|||||||
bool deckLinkBufferedAvailable = false;
|
bool deckLinkBufferedAvailable = false;
|
||||||
uint64_t deckLinkBuffered = 0;
|
uint64_t deckLinkBuffered = 0;
|
||||||
double deckLinkScheduleCallMilliseconds = 0.0;
|
double deckLinkScheduleCallMilliseconds = 0.0;
|
||||||
|
bool deckLinkScheduleLeadAvailable = false;
|
||||||
|
int64_t deckLinkPlaybackStreamTime = 0;
|
||||||
|
uint64_t deckLinkPlaybackFrameIndex = 0;
|
||||||
|
uint64_t deckLinkNextScheduleFrameIndex = 0;
|
||||||
|
int64_t deckLinkScheduleLeadFrames = 0;
|
||||||
|
uint64_t deckLinkScheduleRealignments = 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
class CadenceTelemetry
|
class CadenceTelemetry
|
||||||
@@ -71,12 +90,20 @@ public:
|
|||||||
snapshot.scheduleFailures = outputMetrics.scheduleFailures > threadMetrics.scheduleFailures
|
snapshot.scheduleFailures = outputMetrics.scheduleFailures > threadMetrics.scheduleFailures
|
||||||
? outputMetrics.scheduleFailures
|
? outputMetrics.scheduleFailures
|
||||||
: threadMetrics.scheduleFailures;
|
: threadMetrics.scheduleFailures;
|
||||||
|
snapshot.completedDrops = exchangeMetrics.completedDrops;
|
||||||
|
snapshot.acquireMisses = exchangeMetrics.acquireMisses;
|
||||||
snapshot.completions = outputMetrics.completions;
|
snapshot.completions = outputMetrics.completions;
|
||||||
snapshot.displayedLate = outputMetrics.displayedLate;
|
snapshot.displayedLate = outputMetrics.displayedLate;
|
||||||
snapshot.dropped = outputMetrics.dropped;
|
snapshot.dropped = outputMetrics.dropped;
|
||||||
snapshot.deckLinkBufferedAvailable = outputMetrics.actualBufferedFramesAvailable;
|
snapshot.deckLinkBufferedAvailable = outputMetrics.actualBufferedFramesAvailable;
|
||||||
snapshot.deckLinkBuffered = outputMetrics.actualBufferedFrames;
|
snapshot.deckLinkBuffered = outputMetrics.actualBufferedFrames;
|
||||||
snapshot.deckLinkScheduleCallMilliseconds = outputMetrics.scheduleCallMilliseconds;
|
snapshot.deckLinkScheduleCallMilliseconds = outputMetrics.scheduleCallMilliseconds;
|
||||||
|
snapshot.deckLinkScheduleLeadAvailable = outputMetrics.scheduleLeadAvailable;
|
||||||
|
snapshot.deckLinkPlaybackStreamTime = outputMetrics.playbackStreamTime;
|
||||||
|
snapshot.deckLinkPlaybackFrameIndex = outputMetrics.playbackFrameIndex;
|
||||||
|
snapshot.deckLinkNextScheduleFrameIndex = outputMetrics.nextScheduleFrameIndex;
|
||||||
|
snapshot.deckLinkScheduleLeadFrames = outputMetrics.scheduleLeadFrames;
|
||||||
|
snapshot.deckLinkScheduleRealignments = outputMetrics.scheduleRealignmentCount;
|
||||||
|
|
||||||
if (mHasLastSample && seconds > 0.0)
|
if (mHasLastSample && seconds > 0.0)
|
||||||
{
|
{
|
||||||
@@ -100,10 +127,21 @@ public:
|
|||||||
{
|
{
|
||||||
CadenceTelemetrySnapshot snapshot = Sample(exchange, output, outputThread);
|
CadenceTelemetrySnapshot snapshot = Sample(exchange, output, outputThread);
|
||||||
const auto renderMetrics = renderThread.GetMetrics();
|
const auto renderMetrics = renderThread.GetMetrics();
|
||||||
|
snapshot.clockOverruns = renderMetrics.clockOverruns;
|
||||||
|
snapshot.clockSkippedFrames = renderMetrics.skippedFrames;
|
||||||
snapshot.shaderBuildsCommitted = renderMetrics.shaderBuildsCommitted;
|
snapshot.shaderBuildsCommitted = renderMetrics.shaderBuildsCommitted;
|
||||||
snapshot.shaderBuildFailures = renderMetrics.shaderBuildFailures;
|
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.inputFramesReceived = renderMetrics.inputFramesReceived;
|
||||||
snapshot.inputFramesDropped = renderMetrics.inputFramesDropped;
|
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.inputLatestAgeMilliseconds = renderMetrics.inputLatestAgeMilliseconds;
|
||||||
snapshot.inputUploadMilliseconds = renderMetrics.inputUploadMilliseconds;
|
snapshot.inputUploadMilliseconds = renderMetrics.inputUploadMilliseconds;
|
||||||
snapshot.inputFormatSupported = renderMetrics.inputFormatSupported;
|
snapshot.inputFormatSupported = renderMetrics.inputFormatSupported;
|
||||||
|
|||||||
@@ -21,13 +21,28 @@ inline void WriteCadenceTelemetryJson(JsonWriter& writer, const CadenceTelemetry
|
|||||||
writer.KeyUInt("scheduledTotal", snapshot.scheduledTotal);
|
writer.KeyUInt("scheduledTotal", snapshot.scheduledTotal);
|
||||||
writer.KeyUInt("completedPollMisses", snapshot.completedPollMisses);
|
writer.KeyUInt("completedPollMisses", snapshot.completedPollMisses);
|
||||||
writer.KeyUInt("scheduleFailures", snapshot.scheduleFailures);
|
writer.KeyUInt("scheduleFailures", snapshot.scheduleFailures);
|
||||||
|
writer.KeyUInt("completedDrops", snapshot.completedDrops);
|
||||||
|
writer.KeyUInt("acquireMisses", snapshot.acquireMisses);
|
||||||
writer.KeyUInt("completions", snapshot.completions);
|
writer.KeyUInt("completions", snapshot.completions);
|
||||||
writer.KeyUInt("late", snapshot.displayedLate);
|
writer.KeyUInt("late", snapshot.displayedLate);
|
||||||
writer.KeyUInt("dropped", snapshot.dropped);
|
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("shaderCommitted", snapshot.shaderBuildsCommitted);
|
||||||
writer.KeyUInt("shaderFailures", snapshot.shaderBuildFailures);
|
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("inputFramesReceived", snapshot.inputFramesReceived);
|
||||||
writer.KeyUInt("inputFramesDropped", snapshot.inputFramesDropped);
|
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("inputLatestAgeMs", snapshot.inputLatestAgeMilliseconds);
|
||||||
writer.KeyDouble("inputUploadMs", snapshot.inputUploadMilliseconds);
|
writer.KeyDouble("inputUploadMs", snapshot.inputUploadMilliseconds);
|
||||||
writer.KeyBool("inputFormatSupported", snapshot.inputFormatSupported);
|
writer.KeyBool("inputFormatSupported", snapshot.inputFormatSupported);
|
||||||
@@ -46,6 +61,16 @@ inline void WriteCadenceTelemetryJson(JsonWriter& writer, const CadenceTelemetry
|
|||||||
else
|
else
|
||||||
writer.Null();
|
writer.Null();
|
||||||
writer.KeyDouble("scheduleCallMs", snapshot.deckLinkScheduleCallMilliseconds);
|
writer.KeyDouble("scheduleCallMs", snapshot.deckLinkScheduleCallMilliseconds);
|
||||||
|
writer.KeyBool("deckLinkScheduleLeadAvailable", snapshot.deckLinkScheduleLeadAvailable);
|
||||||
|
writer.Key("deckLinkScheduleLeadFrames");
|
||||||
|
if (snapshot.deckLinkScheduleLeadAvailable)
|
||||||
|
writer.Int(snapshot.deckLinkScheduleLeadFrames);
|
||||||
|
else
|
||||||
|
writer.Null();
|
||||||
|
writer.KeyUInt("deckLinkPlaybackFrameIndex", snapshot.deckLinkPlaybackFrameIndex);
|
||||||
|
writer.KeyUInt("deckLinkNextScheduleFrameIndex", snapshot.deckLinkNextScheduleFrameIndex);
|
||||||
|
writer.KeyInt("deckLinkPlaybackStreamTime", snapshot.deckLinkPlaybackStreamTime);
|
||||||
|
writer.KeyUInt("deckLinkScheduleRealignments", snapshot.deckLinkScheduleRealignments);
|
||||||
writer.EndObject();
|
writer.EndObject();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -78,7 +78,13 @@ private:
|
|||||||
message << "DeckLink reported frame timing issue: lateDelta=" << lateDelta
|
message << "DeckLink reported frame timing issue: lateDelta=" << lateDelta
|
||||||
<< " droppedDelta=" << droppedDelta
|
<< " droppedDelta=" << droppedDelta
|
||||||
<< " totalLate=" << snapshot.displayedLate
|
<< " 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());
|
LogWarning("telemetry", message.str());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ bool DeckLinkOutput::Initialize(const DeckLinkOutputConfig& config, CompletionCa
|
|||||||
mCompletionCallback = completionCallback;
|
mCompletionCallback = completionCallback;
|
||||||
|
|
||||||
VideoFormatSelection formats;
|
VideoFormatSelection formats;
|
||||||
|
formats.output = config.outputVideoMode;
|
||||||
if (!mSession.DiscoverDevicesAndModes(formats, error))
|
if (!mSession.DiscoverDevicesAndModes(formats, error))
|
||||||
return false;
|
return false;
|
||||||
if (!mSession.SelectPreferredFormats(formats, config.outputAlphaRequired, error))
|
if (!mSession.SelectPreferredFormats(formats, config.outputAlphaRequired, error))
|
||||||
@@ -76,6 +77,12 @@ DeckLinkOutputMetrics DeckLinkOutput::Metrics() const
|
|||||||
metrics.actualBufferedFramesAvailable = state.actualDeckLinkBufferedFramesAvailable;
|
metrics.actualBufferedFramesAvailable = state.actualDeckLinkBufferedFramesAvailable;
|
||||||
metrics.actualBufferedFrames = state.actualDeckLinkBufferedFrames;
|
metrics.actualBufferedFrames = state.actualDeckLinkBufferedFrames;
|
||||||
metrics.scheduleCallMilliseconds = state.deckLinkScheduleCallMilliseconds;
|
metrics.scheduleCallMilliseconds = state.deckLinkScheduleCallMilliseconds;
|
||||||
|
metrics.scheduleLeadAvailable = state.deckLinkScheduleLeadAvailable;
|
||||||
|
metrics.playbackStreamTime = state.deckLinkPlaybackStreamTime;
|
||||||
|
metrics.playbackFrameIndex = state.deckLinkPlaybackFrameIndex;
|
||||||
|
metrics.nextScheduleFrameIndex = state.deckLinkNextScheduleFrameIndex;
|
||||||
|
metrics.scheduleLeadFrames = state.deckLinkScheduleLeadFrames;
|
||||||
|
metrics.scheduleRealignmentCount = state.deckLinkScheduleRealignmentCount;
|
||||||
return metrics;
|
return metrics;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
#include "DeckLinkDisplayMode.h"
|
||||||
#include "DeckLinkSession.h"
|
#include "DeckLinkSession.h"
|
||||||
#include "VideoIOTypes.h"
|
#include "VideoIOTypes.h"
|
||||||
|
|
||||||
@@ -12,6 +13,7 @@ namespace RenderCadenceCompositor
|
|||||||
{
|
{
|
||||||
struct DeckLinkOutputConfig
|
struct DeckLinkOutputConfig
|
||||||
{
|
{
|
||||||
|
VideoFormat outputVideoMode;
|
||||||
bool externalKeyingEnabled = false;
|
bool externalKeyingEnabled = false;
|
||||||
bool outputAlphaRequired = false;
|
bool outputAlphaRequired = false;
|
||||||
};
|
};
|
||||||
@@ -26,6 +28,12 @@ struct DeckLinkOutputMetrics
|
|||||||
bool actualBufferedFramesAvailable = false;
|
bool actualBufferedFramesAvailable = false;
|
||||||
uint64_t actualBufferedFrames = 0;
|
uint64_t actualBufferedFrames = 0;
|
||||||
double scheduleCallMilliseconds = 0.0;
|
double scheduleCallMilliseconds = 0.0;
|
||||||
|
bool scheduleLeadAvailable = false;
|
||||||
|
int64_t playbackStreamTime = 0;
|
||||||
|
uint64_t playbackFrameIndex = 0;
|
||||||
|
uint64_t nextScheduleFrameIndex = 0;
|
||||||
|
int64_t scheduleLeadFrames = 0;
|
||||||
|
uint64_t scheduleRealignmentCount = 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
class DeckLinkOutput
|
class DeckLinkOutput
|
||||||
|
|||||||
@@ -77,12 +77,15 @@ private:
|
|||||||
while (!mStopping)
|
while (!mStopping)
|
||||||
{
|
{
|
||||||
const auto exchangeMetrics = mExchange.Metrics();
|
const auto exchangeMetrics = mExchange.Metrics();
|
||||||
if (exchangeMetrics.scheduledCount >= mConfig.targetBufferedFrames)
|
const auto outputMetrics = mOutput.Metrics();
|
||||||
|
const std::size_t bufferedFrames = outputMetrics.actualBufferedFramesAvailable
|
||||||
|
? static_cast<std::size_t>(outputMetrics.actualBufferedFrames)
|
||||||
|
: exchangeMetrics.scheduledCount;
|
||||||
|
if (bufferedFrames >= mConfig.targetBufferedFrames)
|
||||||
{
|
{
|
||||||
std::this_thread::sleep_for(mConfig.idleSleep);
|
std::this_thread::sleep_for(mConfig.idleSleep);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
SystemFrame frame;
|
SystemFrame frame;
|
||||||
if (!mExchange.ConsumeCompletedForSchedule(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:
|
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
|
## Application Shape
|
||||||
|
|
||||||
@@ -287,7 +290,7 @@ Slots have four states:
|
|||||||
- `Completed`
|
- `Completed`
|
||||||
- `Scheduled`
|
- `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.
|
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.
|
`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
|
- pushing beyond capacity releases/drops the oldest ready frame
|
||||||
- `DropOldestFrame()` is used when the frame pool needs to recycle old completed work
|
- `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
|
- one OpenGL render thread with its own hidden GL context
|
||||||
- simple BGRA8 motion rendering
|
- simple BGRA8 motion rendering
|
||||||
- async PBO readback
|
- 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
|
- a playout thread that feeds DeckLink
|
||||||
- real rendered warmup before scheduled playback
|
- 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 one owner for each kind of state.
|
||||||
- Keep GL work on the render thread.
|
- Keep GL work on the render thread.
|
||||||
- Keep DeckLink completion callbacks passive.
|
- 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.
|
- Protect scheduled output frames until DeckLink completion.
|
||||||
- Keep output timing more important than preview/screenshot.
|
- Keep output timing more important than preview/screenshot.
|
||||||
- Measure timing by domain instead of adding fallback branches blindly.
|
- Measure timing by domain instead of adding fallback branches blindly.
|
||||||
|
|||||||
@@ -115,6 +115,24 @@ Lesson:
|
|||||||
- keep synthetic counters only as diagnostics
|
- keep synthetic counters only as diagnostics
|
||||||
- do not infer device health from internal stream indexes alone
|
- 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
|
### More Buffer Is Not Automatically Smoother
|
||||||
|
|
||||||
Increasing DeckLink scheduled frames sometimes made the reported device buffer look healthier while visible motion still stuttered.
|
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
|
- system-memory slots are the contract between render and playout
|
||||||
- scheduled slots must not be recycled early
|
- 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
|
### Startup Needs Real Preroll
|
||||||
|
|
||||||
@@ -222,18 +240,18 @@ Lesson:
|
|||||||
|
|
||||||
The app has at least two important frame stores:
|
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
|
- DeckLink scheduled/device buffer
|
||||||
|
|
||||||
They have different ownership rules.
|
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.
|
Scheduled frames are not disposable because DeckLink may still read them.
|
||||||
|
|
||||||
Lesson:
|
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
|
- scheduled frames are owned by DeckLink until completion
|
||||||
- keep metrics for both
|
- keep metrics for both
|
||||||
|
|
||||||
@@ -246,7 +264,8 @@ That couples the clocks again.
|
|||||||
Lesson:
|
Lesson:
|
||||||
|
|
||||||
- render cadence should keep rendering at selected cadence
|
- 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
|
- only scheduled/in-flight saturation should prevent rendering to a safe slot
|
||||||
|
|
||||||
## Render Thread Lessons
|
## Render Thread Lessons
|
||||||
@@ -340,7 +359,7 @@ The current direction is still sound:
|
|||||||
```text
|
```text
|
||||||
Render cadence loop
|
Render cadence loop
|
||||||
renders at selected output cadence
|
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
|
never sprints to refill DeckLink
|
||||||
|
|
||||||
Frame store
|
Frame store
|
||||||
@@ -387,7 +406,7 @@ A full rewrite becomes attractive only if the current GL ownership model cannot
|
|||||||
- Render cadence is time-driven, not completion-driven.
|
- Render cadence is time-driven, not completion-driven.
|
||||||
- DeckLink scheduling is device-buffer-driven, not render-driven.
|
- DeckLink scheduling is device-buffer-driven, not render-driven.
|
||||||
- Completion callbacks release and report; they do not render.
|
- 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.
|
- Scheduled frames are protected until DeckLink completion.
|
||||||
- Startup uses real rendered warmup/preroll.
|
- Startup uses real rendered warmup/preroll.
|
||||||
- Black fallback is degraded/error behavior, not steady-state behavior.
|
- Black fallback is degraded/error behavior, not steady-state behavior.
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
# New Render Cadence App Plan
|
# 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.
|
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.
|
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
|
System frame exchange
|
||||||
-> owns Free / Rendering / Completed / Scheduled slots
|
-> 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
|
-> protects scheduled frames until DeckLink completion
|
||||||
|
|
||||||
DeckLink output thread
|
DeckLink output thread
|
||||||
@@ -63,7 +65,7 @@ Everything else must fit around that spine.
|
|||||||
- Completion callbacks never render.
|
- Completion callbacks never render.
|
||||||
- No synchronous render request exists in the output path.
|
- 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.
|
- 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.
|
- Scheduled frames are protected until DeckLink completion.
|
||||||
- Startup warms up real rendered frames before scheduled playback starts.
|
- Startup warms up real rendered frames before scheduled playback starts.
|
||||||
|
|
||||||
@@ -77,7 +79,7 @@ Keep these behaviors from `DeckLinkRenderCadenceProbe`:
|
|||||||
- PBO ring readback
|
- PBO ring readback
|
||||||
- non-blocking fence polling with zero timeout
|
- non-blocking fence polling with zero timeout
|
||||||
- system-memory slots with `Free`, `Rendering`, `Completed`, `Scheduled`
|
- 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
|
- DeckLink playout thread only schedules completed frames
|
||||||
- warmup completed frames before `StartScheduledPlayback()`
|
- warmup completed frames before `StartScheduledPlayback()`
|
||||||
- one-line-per-second timing telemetry
|
- one-line-per-second timing telemetry
|
||||||
@@ -430,7 +432,7 @@ Feature set:
|
|||||||
- simple motion renderer
|
- simple motion renderer
|
||||||
- BGRA8 only
|
- BGRA8 only
|
||||||
- PBO async readback
|
- PBO async readback
|
||||||
- latest-N system-memory frame exchange
|
- bounded FIFO system-memory frame exchange
|
||||||
- warmup before playback
|
- warmup before playback
|
||||||
- one-line telemetry
|
- one-line telemetry
|
||||||
|
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ The output/scheduling side may:
|
|||||||
- release frames after DeckLink completion
|
- release frames after DeckLink completion
|
||||||
- report late/dropped/schedule telemetry
|
- report late/dropped/schedule telemetry
|
||||||
- record app-side poll misses
|
- record app-side poll misses
|
||||||
|
- conservatively realign the DeckLink schedule cursor after measured timing pressure
|
||||||
|
|
||||||
It must not:
|
It must not:
|
||||||
|
|
||||||
@@ -55,6 +56,7 @@ It must not:
|
|||||||
- invoke GL
|
- invoke GL
|
||||||
- compile shaders
|
- compile shaders
|
||||||
- block the render cadence waiting for DeckLink
|
- 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.
|
If no completed frame is available, record the miss and keep the ownership boundary intact.
|
||||||
|
|
||||||
@@ -93,9 +95,11 @@ Short mutex use for exchanging small already-prepared objects is acceptable. Hol
|
|||||||
|
|
||||||
## 6. System Memory Frames Are A Handoff, Not A Render Driver
|
## 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
|
## 7. Startup Uses Warmup, Not Burst Rendering
|
||||||
|
|
||||||
@@ -114,6 +118,8 @@ Good examples:
|
|||||||
- `completedPollMisses`
|
- `completedPollMisses`
|
||||||
- `scheduleFailures`
|
- `scheduleFailures`
|
||||||
- `decklinkBuffered`
|
- `decklinkBuffered`
|
||||||
|
- `deckLinkScheduleLeadFrames`
|
||||||
|
- `deckLinkScheduleRealignments`
|
||||||
- `inputCaptureFps`
|
- `inputCaptureFps`
|
||||||
- `inputSubmitMs`
|
- `inputSubmitMs`
|
||||||
- `inputUploadMs`
|
- `inputUploadMs`
|
||||||
|
|||||||
@@ -98,7 +98,7 @@ render cadence thread
|
|||||||
-> samples latest render input/state
|
-> samples latest render input/state
|
||||||
-> renders one frame
|
-> renders one frame
|
||||||
-> queues async readback/copies completed readback into system-memory slot
|
-> 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
|
video output thread
|
||||||
-> consumes completed system-memory frames
|
-> consumes completed system-memory frames
|
||||||
|
|||||||
@@ -633,10 +633,12 @@ components:
|
|||||||
type: number
|
type: number
|
||||||
renderMs:
|
renderMs:
|
||||||
type: number
|
type: number
|
||||||
|
description: Alias of cadence.renderFrameMs for clients that read the top-level performance object.
|
||||||
smoothedRenderMs:
|
smoothedRenderMs:
|
||||||
type: number
|
type: number
|
||||||
budgetUsedPercent:
|
budgetUsedPercent:
|
||||||
type: number
|
type: number
|
||||||
|
description: Alias of cadence.renderFrameBudgetUsedPercent for clients that read the top-level performance object.
|
||||||
completionIntervalMs:
|
completionIntervalMs:
|
||||||
type: number
|
type: number
|
||||||
smoothedCompletionIntervalMs:
|
smoothedCompletionIntervalMs:
|
||||||
@@ -649,6 +651,93 @@ components:
|
|||||||
type: number
|
type: number
|
||||||
flushedFrameCount:
|
flushedFrameCount:
|
||||||
type: number
|
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:
|
BackendPlayoutStatus:
|
||||||
type: object
|
type: object
|
||||||
properties:
|
properties:
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
#include "AppConfigProvider.h"
|
#include "AppConfigProvider.h"
|
||||||
|
|
||||||
|
#include <chrono>
|
||||||
#include <filesystem>
|
#include <filesystem>
|
||||||
#include <fstream>
|
#include <fstream>
|
||||||
#include <iostream>
|
#include <iostream>
|
||||||
@@ -104,6 +105,8 @@ void TestHelpers()
|
|||||||
|
|
||||||
const double duration = FrameDurationMillisecondsFromRateString("50");
|
const double duration = FrameDurationMillisecondsFromRateString("50");
|
||||||
Expect(duration > 19.9 && duration < 20.1, "frame duration parses numeric rate");
|
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();
|
const std::filesystem::path configPath = FindConfigFile();
|
||||||
Expect(!configPath.empty(), "default config is discoverable from test working directory");
|
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");
|
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()
|
void TestMarkRenderedRebasesAfterLargeStall()
|
||||||
{
|
{
|
||||||
using Clock = RenderCadenceClock::Clock;
|
using Clock = RenderCadenceClock::Clock;
|
||||||
@@ -81,6 +102,7 @@ int main()
|
|||||||
TestEarlyPollWaitsWithoutAdvancing();
|
TestEarlyPollWaitsWithoutAdvancing();
|
||||||
TestDuePollRendersWithoutSkipping();
|
TestDuePollRendersWithoutSkipping();
|
||||||
TestLatePollRecordsSkippedFrames();
|
TestLatePollRecordsSkippedFrames();
|
||||||
|
TestLatePollSkipsMissedIntervalsInsteadOfCatchingUp();
|
||||||
TestMarkRenderedRebasesAfterLargeStall();
|
TestMarkRenderedRebasesAfterLargeStall();
|
||||||
|
|
||||||
if (gFailures != 0)
|
if (gFailures != 0)
|
||||||
|
|||||||
@@ -27,6 +27,13 @@ SystemFrameExchangeConfig MakeConfig(std::size_t capacity = 2)
|
|||||||
return config;
|
return config;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
SystemFrameExchangeConfig MakeBoundedCompletedConfig(std::size_t capacity = 4, std::size_t maxCompletedFrames = 2)
|
||||||
|
{
|
||||||
|
SystemFrameExchangeConfig config = MakeConfig(capacity);
|
||||||
|
config.maxCompletedFrames = maxCompletedFrames;
|
||||||
|
return config;
|
||||||
|
}
|
||||||
|
|
||||||
void TestAcquirePublishesAndSchedules()
|
void TestAcquirePublishesAndSchedules()
|
||||||
{
|
{
|
||||||
SystemFrameExchange exchange(MakeConfig(1));
|
SystemFrameExchange exchange(MakeConfig(1));
|
||||||
@@ -57,32 +64,54 @@ void TestAcquirePublishesAndSchedules()
|
|||||||
Expect(metrics.scheduledFrames == 1, "scheduled metric is counted");
|
Expect(metrics.scheduledFrames == 1, "scheduled metric is counted");
|
||||||
}
|
}
|
||||||
|
|
||||||
void TestAcquireDropsOldestCompletedUnscheduled()
|
void TestAcquirePreservesCompletedFrames()
|
||||||
{
|
{
|
||||||
SystemFrameExchange exchange(MakeConfig(2));
|
SystemFrameExchange exchange(MakeConfig(2));
|
||||||
|
|
||||||
SystemFrame first;
|
SystemFrame first;
|
||||||
SystemFrame second;
|
SystemFrame second;
|
||||||
SystemFrame third;
|
SystemFrame third;
|
||||||
Expect(exchange.AcquireForRender(first), "first frame can be acquired");
|
Expect(exchange.AcquireForRender(first), "first preserving frame can be acquired");
|
||||||
first.frameIndex = 1;
|
first.frameIndex = 1;
|
||||||
Expect(exchange.PublishCompleted(first), "first frame can be completed");
|
Expect(exchange.PublishCompleted(first), "first preserving frame can be completed");
|
||||||
Expect(exchange.AcquireForRender(second), "second frame can be acquired");
|
Expect(exchange.AcquireForRender(second), "second preserving frame can be acquired");
|
||||||
second.frameIndex = 2;
|
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(!exchange.AcquireForRender(third), "render acquire refuses to drop completed frames");
|
||||||
Expect(third.index == first.index, "oldest completed slot is reused");
|
|
||||||
|
|
||||||
SystemFrame scheduled;
|
SystemFrame scheduled;
|
||||||
Expect(exchange.ConsumeCompletedForSchedule(scheduled), "remaining completed frame can be scheduled");
|
Expect(exchange.ConsumeCompletedForSchedule(scheduled), "oldest completed frame survives acquire miss");
|
||||||
Expect(scheduled.index == second.index, "newer completed frame survives drop");
|
Expect(scheduled.frameIndex == 1, "preserving acquire keeps FIFO output continuity");
|
||||||
Expect(scheduled.frameIndex == 2, "newer frame index survives drop");
|
|
||||||
|
|
||||||
SystemFrameExchangeMetrics metrics = exchange.Metrics();
|
SystemFrameExchangeMetrics metrics = exchange.Metrics();
|
||||||
Expect(metrics.completedDrops == 1, "drop metric is counted");
|
Expect(metrics.completedDrops == 0, "preserving acquire does not count completed drops");
|
||||||
Expect(metrics.renderingCount == 1, "reused slot is rendering");
|
Expect(metrics.acquireMisses == 1, "preserving acquire miss is counted");
|
||||||
Expect(metrics.scheduledCount == 1, "consumed slot is scheduled");
|
}
|
||||||
|
|
||||||
|
void TestCompletedReserveIsBoundedFifo()
|
||||||
|
{
|
||||||
|
SystemFrameExchange exchange(MakeBoundedCompletedConfig(4, 2));
|
||||||
|
|
||||||
|
for (uint64_t frameIndex = 1; frameIndex <= 3; ++frameIndex)
|
||||||
|
{
|
||||||
|
SystemFrame frame;
|
||||||
|
Expect(exchange.AcquireForRender(frame), "bounded reserve frame can be acquired");
|
||||||
|
frame.frameIndex = frameIndex;
|
||||||
|
Expect(exchange.PublishCompleted(frame), "bounded reserve frame can be completed");
|
||||||
|
}
|
||||||
|
|
||||||
|
SystemFrame firstScheduled;
|
||||||
|
Expect(exchange.ConsumeCompletedForSchedule(firstScheduled), "bounded reserve oldest retained frame can be scheduled");
|
||||||
|
Expect(firstScheduled.frameIndex == 2, "bounded reserve drops oldest overflow and keeps FIFO order");
|
||||||
|
|
||||||
|
SystemFrame secondScheduled;
|
||||||
|
Expect(exchange.ConsumeCompletedForSchedule(secondScheduled), "bounded reserve second retained frame can be scheduled");
|
||||||
|
Expect(secondScheduled.frameIndex == 3, "bounded reserve schedules next retained frame");
|
||||||
|
|
||||||
|
SystemFrameExchangeMetrics metrics = exchange.Metrics();
|
||||||
|
Expect(metrics.completedDrops == 1, "bounded completed reserve records oldest overflow drop");
|
||||||
|
Expect(metrics.scheduledFrames == 2, "bounded reserve schedules retained frames");
|
||||||
}
|
}
|
||||||
|
|
||||||
void TestScheduledFramesAreNotDropped()
|
void TestScheduledFramesAreNotDropped()
|
||||||
@@ -154,16 +183,39 @@ void TestCompletedPollMissIsCounted()
|
|||||||
SystemFrameExchangeMetrics metrics = exchange.Metrics();
|
SystemFrameExchangeMetrics metrics = exchange.Metrics();
|
||||||
Expect(metrics.completedPollMisses == 1, "completed poll miss is counted");
|
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()
|
int main()
|
||||||
{
|
{
|
||||||
TestAcquirePublishesAndSchedules();
|
TestAcquirePublishesAndSchedules();
|
||||||
TestAcquireDropsOldestCompletedUnscheduled();
|
TestAcquirePreservesCompletedFrames();
|
||||||
|
TestCompletedReserveIsBoundedFifo();
|
||||||
TestScheduledFramesAreNotDropped();
|
TestScheduledFramesAreNotDropped();
|
||||||
TestGenerationValidationRejectsStaleFrames();
|
TestGenerationValidationRejectsStaleFrames();
|
||||||
TestPixelFormatAwareSizing();
|
TestPixelFormatAwareSizing();
|
||||||
TestCompletedPollMissIsCounted();
|
TestCompletedPollMissIsCounted();
|
||||||
|
TestStableCompletedDepthCanBeObserved();
|
||||||
|
TestStableCompletedDepthTimesOut();
|
||||||
|
|
||||||
if (gFailures != 0)
|
if (gFailures != 0)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -29,33 +29,18 @@ InputFrameMailboxConfig MakeConfig(std::size_t capacity = 2)
|
|||||||
return config;
|
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)
|
std::vector<unsigned char> MakeFrame(unsigned char value)
|
||||||
{
|
{
|
||||||
return std::vector<unsigned char>(16, 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()
|
void TestSubmitDropsOldestWhenFull()
|
||||||
{
|
{
|
||||||
InputFrameMailbox mailbox(MakeConfig(2));
|
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(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");
|
Expect(mailbox.SubmitFrame(frame3.data(), 8, 3), "third frame submits by dropping oldest ready frame");
|
||||||
|
|
||||||
InputFrame latest;
|
InputFrame oldest;
|
||||||
Expect(mailbox.TryAcquireLatest(latest), "latest frame available after drop");
|
Expect(mailbox.TryAcquireOldest(oldest), "oldest frame available after drop");
|
||||||
Expect(latest.frameIndex == 3, "newest frame survived full mailbox");
|
Expect(oldest.frameIndex == 2, "bounded mailbox keeps FIFO order after trimming oldest overflow");
|
||||||
Expect(mailbox.Release(latest), "newest frame releases");
|
Expect(mailbox.Release(oldest), "oldest frame releases");
|
||||||
|
|
||||||
const InputFrameMailboxMetrics metrics = mailbox.Metrics();
|
const InputFrameMailboxMetrics metrics = mailbox.Metrics();
|
||||||
Expect(metrics.droppedReadyFrames >= 1, "full mailbox records ready drop");
|
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");
|
Expect(mailbox.SubmitFrame(frame1.data(), 8, 1), "protected frame submits");
|
||||||
InputFrame acquired;
|
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.SubmitFrame(frame2.data(), 8, 2), "producer cannot overwrite frame currently being read");
|
||||||
Expect(mailbox.Release(acquired), "protected frame releases");
|
Expect(mailbox.Release(acquired), "protected frame releases");
|
||||||
Expect(mailbox.SubmitFrame(frame2.data(), 8, 2), "producer can submit after release");
|
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()
|
int main()
|
||||||
{
|
{
|
||||||
TestAcquireLatestDropsOlderReadyFrames();
|
|
||||||
TestSubmitDropsOldestWhenFull();
|
TestSubmitDropsOldestWhenFull();
|
||||||
TestReadingFrameIsProtected();
|
TestReadingFrameIsProtected();
|
||||||
|
TestAcquireOldestConsumesFifoWithoutDroppingReadyFrames();
|
||||||
|
TestMaxReadyFramesKeepsConfiguredInputBuffer();
|
||||||
|
|
||||||
if (gFailures != 0)
|
if (gFailures != 0)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -43,6 +43,13 @@ int main()
|
|||||||
|
|
||||||
RenderCadenceCompositor::CadenceTelemetrySnapshot telemetry;
|
RenderCadenceCompositor::CadenceTelemetrySnapshot telemetry;
|
||||||
telemetry.renderFps = 59.94;
|
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;
|
telemetry.shaderBuildsCommitted = 1;
|
||||||
|
|
||||||
const std::filesystem::path root = MakeTestRoot();
|
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, "\"type\":\"color\"", "state JSON should serialize parameter types for the UI");
|
||||||
ExpectContains(json, "\"width\":1920", "state JSON should expose output width");
|
ExpectContains(json, "\"width\":1920", "state JSON should expose output width");
|
||||||
ExpectContains(json, "\"height\":1080", "state JSON should expose output height");
|
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);
|
std::filesystem::remove_all(root);
|
||||||
|
|
||||||
|
|||||||
@@ -26,6 +26,8 @@ struct FakeExchangeMetrics
|
|||||||
std::size_t scheduledCount = 0;
|
std::size_t scheduledCount = 0;
|
||||||
uint64_t completedFrames = 0;
|
uint64_t completedFrames = 0;
|
||||||
uint64_t scheduledFrames = 0;
|
uint64_t scheduledFrames = 0;
|
||||||
|
uint64_t completedDrops = 0;
|
||||||
|
uint64_t acquireMisses = 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
struct FakeExchange
|
struct FakeExchange
|
||||||
@@ -55,6 +57,12 @@ struct FakeOutputMetrics
|
|||||||
bool actualBufferedFramesAvailable = false;
|
bool actualBufferedFramesAvailable = false;
|
||||||
uint64_t actualBufferedFrames = 0;
|
uint64_t actualBufferedFrames = 0;
|
||||||
double scheduleCallMilliseconds = 0.0;
|
double scheduleCallMilliseconds = 0.0;
|
||||||
|
bool scheduleLeadAvailable = false;
|
||||||
|
int64_t playbackStreamTime = 0;
|
||||||
|
uint64_t playbackFrameIndex = 0;
|
||||||
|
uint64_t nextScheduleFrameIndex = 0;
|
||||||
|
int64_t scheduleLeadFrames = 0;
|
||||||
|
uint64_t scheduleRealignmentCount = 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
struct FakeOutput
|
struct FakeOutput
|
||||||
@@ -65,10 +73,21 @@ struct FakeOutput
|
|||||||
|
|
||||||
struct FakeRenderThreadMetrics
|
struct FakeRenderThreadMetrics
|
||||||
{
|
{
|
||||||
|
uint64_t clockOverruns = 0;
|
||||||
|
uint64_t skippedFrames = 0;
|
||||||
uint64_t shaderBuildsCommitted = 0;
|
uint64_t shaderBuildsCommitted = 0;
|
||||||
uint64_t shaderBuildFailures = 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 inputFramesReceived = 0;
|
||||||
uint64_t inputFramesDropped = 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 inputLatestAgeMilliseconds = 0.0;
|
||||||
double inputUploadMilliseconds = 0.0;
|
double inputUploadMilliseconds = 0.0;
|
||||||
bool inputFormatSupported = true;
|
bool inputFormatSupported = true;
|
||||||
@@ -90,20 +109,39 @@ void TestTelemetrySamplesCompletedPollMissesAndShaderCounts()
|
|||||||
exchange.metrics.scheduledCount = 4;
|
exchange.metrics.scheduledCount = 4;
|
||||||
exchange.metrics.completedFrames = 100;
|
exchange.metrics.completedFrames = 100;
|
||||||
exchange.metrics.scheduledFrames = 96;
|
exchange.metrics.scheduledFrames = 96;
|
||||||
|
exchange.metrics.completedDrops = 2;
|
||||||
|
exchange.metrics.acquireMisses = 3;
|
||||||
|
|
||||||
FakeOutput output;
|
FakeOutput output;
|
||||||
output.metrics.actualBufferedFramesAvailable = true;
|
output.metrics.actualBufferedFramesAvailable = true;
|
||||||
output.metrics.actualBufferedFrames = 4;
|
output.metrics.actualBufferedFrames = 4;
|
||||||
|
output.metrics.scheduleLeadAvailable = true;
|
||||||
|
output.metrics.playbackStreamTime = 10010;
|
||||||
|
output.metrics.playbackFrameIndex = 10;
|
||||||
|
output.metrics.nextScheduleFrameIndex = 14;
|
||||||
|
output.metrics.scheduleLeadFrames = 4;
|
||||||
|
output.metrics.scheduleRealignmentCount = 1;
|
||||||
|
|
||||||
FakeOutputThread outputThread;
|
FakeOutputThread outputThread;
|
||||||
outputThread.metrics.completedPollMisses = 12;
|
outputThread.metrics.completedPollMisses = 12;
|
||||||
outputThread.metrics.scheduleFailures = 0;
|
outputThread.metrics.scheduleFailures = 0;
|
||||||
|
|
||||||
FakeRenderThread renderThread;
|
FakeRenderThread renderThread;
|
||||||
|
renderThread.metrics.clockOverruns = 5;
|
||||||
|
renderThread.metrics.skippedFrames = 8;
|
||||||
renderThread.metrics.shaderBuildsCommitted = 1;
|
renderThread.metrics.shaderBuildsCommitted = 1;
|
||||||
renderThread.metrics.shaderBuildFailures = 0;
|
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.inputFramesReceived = 9;
|
||||||
renderThread.metrics.inputFramesDropped = 2;
|
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.inputLatestAgeMilliseconds = 4.5;
|
||||||
renderThread.metrics.inputUploadMilliseconds = 0.25;
|
renderThread.metrics.inputUploadMilliseconds = 0.25;
|
||||||
renderThread.metrics.inputFormatSupported = true;
|
renderThread.metrics.inputFormatSupported = true;
|
||||||
@@ -114,16 +152,35 @@ void TestTelemetrySamplesCompletedPollMissesAndShaderCounts()
|
|||||||
Expect(snapshot.completedFrames == 1, "completed frame count is sampled");
|
Expect(snapshot.completedFrames == 1, "completed frame count is sampled");
|
||||||
Expect(snapshot.scheduledFrames == 4, "scheduled frame count is sampled");
|
Expect(snapshot.scheduledFrames == 4, "scheduled frame count is sampled");
|
||||||
Expect(snapshot.completedPollMisses == 12, "completed poll misses are 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.shaderBuildsCommitted == 1, "shader committed count is sampled");
|
||||||
Expect(snapshot.shaderBuildFailures == 0, "shader failure 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.inputFramesReceived == 9, "input received count is sampled");
|
||||||
Expect(snapshot.inputFramesDropped == 2, "input dropped 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.inputLatestAgeMilliseconds == 4.5, "input latest age is sampled");
|
||||||
Expect(snapshot.inputUploadMilliseconds == 0.25, "input upload timing is sampled");
|
Expect(snapshot.inputUploadMilliseconds == 0.25, "input upload timing is sampled");
|
||||||
Expect(snapshot.inputFormatSupported, "input format support is sampled");
|
Expect(snapshot.inputFormatSupported, "input format support is sampled");
|
||||||
Expect(snapshot.inputSignalPresent, "input signal present is sampled");
|
Expect(snapshot.inputSignalPresent, "input signal present is sampled");
|
||||||
Expect(snapshot.deckLinkBufferedAvailable, "buffer telemetry availability is sampled");
|
Expect(snapshot.deckLinkBufferedAvailable, "buffer telemetry availability is sampled");
|
||||||
Expect(snapshot.deckLinkBuffered == 4, "buffer depth is sampled");
|
Expect(snapshot.deckLinkBuffered == 4, "buffer depth is sampled");
|
||||||
|
Expect(snapshot.deckLinkScheduleLeadAvailable, "schedule lead availability is sampled");
|
||||||
|
Expect(snapshot.deckLinkPlaybackStreamTime == 10010, "playback stream time is sampled");
|
||||||
|
Expect(snapshot.deckLinkPlaybackFrameIndex == 10, "playback frame index is sampled");
|
||||||
|
Expect(snapshot.deckLinkNextScheduleFrameIndex == 14, "next schedule frame index is sampled");
|
||||||
|
Expect(snapshot.deckLinkScheduleLeadFrames == 4, "schedule lead frames are sampled");
|
||||||
|
Expect(snapshot.deckLinkScheduleRealignments == 1, "schedule realignment count is sampled");
|
||||||
}
|
}
|
||||||
|
|
||||||
void TestTelemetryComputesRatesFromDeltas()
|
void TestTelemetryComputesRatesFromDeltas()
|
||||||
@@ -161,13 +218,26 @@ void TestTelemetrySerializesToJson()
|
|||||||
snapshot.scheduledTotal = 118;
|
snapshot.scheduledTotal = 118;
|
||||||
snapshot.completedPollMisses = 3;
|
snapshot.completedPollMisses = 3;
|
||||||
snapshot.scheduleFailures = 0;
|
snapshot.scheduleFailures = 0;
|
||||||
|
snapshot.completedDrops = 4;
|
||||||
|
snapshot.acquireMisses = 5;
|
||||||
snapshot.completions = 117;
|
snapshot.completions = 117;
|
||||||
snapshot.displayedLate = 1;
|
snapshot.displayedLate = 1;
|
||||||
snapshot.dropped = 2;
|
snapshot.dropped = 2;
|
||||||
|
snapshot.clockOverruns = 3;
|
||||||
|
snapshot.clockSkippedFrames = 5;
|
||||||
snapshot.shaderBuildsCommitted = 1;
|
snapshot.shaderBuildsCommitted = 1;
|
||||||
snapshot.shaderBuildFailures = 0;
|
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.inputFramesReceived = 10;
|
||||||
snapshot.inputFramesDropped = 1;
|
snapshot.inputFramesDropped = 1;
|
||||||
|
snapshot.inputConsumeMisses = 2;
|
||||||
|
snapshot.inputUploadMisses = 3;
|
||||||
|
snapshot.inputReadyFrames = 1;
|
||||||
|
snapshot.inputReadingFrames = 0;
|
||||||
snapshot.inputLatestAgeMilliseconds = 3.5;
|
snapshot.inputLatestAgeMilliseconds = 3.5;
|
||||||
snapshot.inputUploadMilliseconds = 0.75;
|
snapshot.inputUploadMilliseconds = 0.75;
|
||||||
snapshot.inputFormatSupported = true;
|
snapshot.inputFormatSupported = true;
|
||||||
@@ -182,6 +252,12 @@ void TestTelemetrySerializesToJson()
|
|||||||
snapshot.deckLinkBufferedAvailable = true;
|
snapshot.deckLinkBufferedAvailable = true;
|
||||||
snapshot.deckLinkBuffered = 4;
|
snapshot.deckLinkBuffered = 4;
|
||||||
snapshot.deckLinkScheduleCallMilliseconds = 1.25;
|
snapshot.deckLinkScheduleCallMilliseconds = 1.25;
|
||||||
|
snapshot.deckLinkScheduleLeadAvailable = true;
|
||||||
|
snapshot.deckLinkScheduleLeadFrames = 4;
|
||||||
|
snapshot.deckLinkPlaybackFrameIndex = 10;
|
||||||
|
snapshot.deckLinkNextScheduleFrameIndex = 14;
|
||||||
|
snapshot.deckLinkPlaybackStreamTime = 10010;
|
||||||
|
snapshot.deckLinkScheduleRealignments = 1;
|
||||||
|
|
||||||
const std::string json = RenderCadenceCompositor::CadenceTelemetryToJson(snapshot);
|
const std::string json = RenderCadenceCompositor::CadenceTelemetryToJson(snapshot);
|
||||||
const std::string expected =
|
const std::string expected =
|
||||||
@@ -189,9 +265,17 @@ void TestTelemetrySerializesToJson()
|
|||||||
"\"free\":7,\"completed\":1,\"scheduled\":4,"
|
"\"free\":7,\"completed\":1,\"scheduled\":4,"
|
||||||
"\"renderedTotal\":120,\"scheduledTotal\":118,"
|
"\"renderedTotal\":120,\"scheduledTotal\":118,"
|
||||||
"\"completedPollMisses\":3,\"scheduleFailures\":0,"
|
"\"completedPollMisses\":3,\"scheduleFailures\":0,"
|
||||||
|
"\"completedDrops\":4,\"acquireMisses\":5,"
|
||||||
"\"completions\":117,\"late\":1,\"dropped\":2,"
|
"\"completions\":117,\"late\":1,\"dropped\":2,"
|
||||||
|
"\"clockOverruns\":3,\"clockSkippedFrames\":5,"
|
||||||
|
"\"clockOveruns\":3,\"clockSkipped\":5,"
|
||||||
"\"shaderCommitted\":1,\"shaderFailures\":0,"
|
"\"shaderCommitted\":1,\"shaderFailures\":0,"
|
||||||
|
"\"renderFrameMs\":2.5,\"renderFrameBudgetUsedPercent\":15,"
|
||||||
|
"\"renderFrameMaxMs\":4,\"readbackQueueMs\":0.6,"
|
||||||
|
"\"completedReadbackCopyMs\":1.2,"
|
||||||
"\"inputFramesReceived\":10,\"inputFramesDropped\":1,"
|
"\"inputFramesReceived\":10,\"inputFramesDropped\":1,"
|
||||||
|
"\"inputConsumeMisses\":2,\"inputUploadMisses\":3,"
|
||||||
|
"\"inputReadyFrames\":1,\"inputReadingFrames\":0,"
|
||||||
"\"inputLatestAgeMs\":3.5,\"inputUploadMs\":0.75,"
|
"\"inputLatestAgeMs\":3.5,\"inputUploadMs\":0.75,"
|
||||||
"\"inputFormatSupported\":true,\"inputSignalPresent\":true,"
|
"\"inputFormatSupported\":true,\"inputSignalPresent\":true,"
|
||||||
"\"inputCaptureFps\":59.94,\"inputConvertMs\":4.25,"
|
"\"inputCaptureFps\":59.94,\"inputConvertMs\":4.25,"
|
||||||
@@ -199,7 +283,13 @@ void TestTelemetrySerializesToJson()
|
|||||||
"\"inputUnsupportedFrames\":3,\"inputSubmitMisses\":4,"
|
"\"inputUnsupportedFrames\":3,\"inputSubmitMisses\":4,"
|
||||||
"\"inputCaptureFormat\":\"UYVY8\","
|
"\"inputCaptureFormat\":\"UYVY8\","
|
||||||
"\"deckLinkBufferedAvailable\":true,\"deckLinkBuffered\":4,"
|
"\"deckLinkBufferedAvailable\":true,\"deckLinkBuffered\":4,"
|
||||||
"\"scheduleCallMs\":1.25}";
|
"\"scheduleCallMs\":1.25,"
|
||||||
|
"\"deckLinkScheduleLeadAvailable\":true,"
|
||||||
|
"\"deckLinkScheduleLeadFrames\":4,"
|
||||||
|
"\"deckLinkPlaybackFrameIndex\":10,"
|
||||||
|
"\"deckLinkNextScheduleFrameIndex\":14,"
|
||||||
|
"\"deckLinkPlaybackStreamTime\":10010,"
|
||||||
|
"\"deckLinkScheduleRealignments\":1}";
|
||||||
Expect(json == expected, "telemetry snapshot serializes to stable JSON");
|
Expect(json == expected, "telemetry snapshot serializes to stable JSON");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -70,6 +70,19 @@ void TestDefaultPolicyReportsLagWithoutSkippingScheduleTime()
|
|||||||
Expect(scheduler.NextScheduleTime().streamTime == 1000, "default recovery keeps stream time continuous");
|
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()
|
void TestMeasuredRecoveryIsCappedByPolicy()
|
||||||
{
|
{
|
||||||
VideoPlayoutPolicy policy;
|
VideoPlayoutPolicy policy;
|
||||||
@@ -133,6 +146,7 @@ int main()
|
|||||||
TestScheduleAdvancesFromZero();
|
TestScheduleAdvancesFromZero();
|
||||||
TestLateAndDroppedRecoveryUsesMeasuredPressure();
|
TestLateAndDroppedRecoveryUsesMeasuredPressure();
|
||||||
TestDefaultPolicyReportsLagWithoutSkippingScheduleTime();
|
TestDefaultPolicyReportsLagWithoutSkippingScheduleTime();
|
||||||
|
TestScheduleCursorCanAlignToPlaybackClock();
|
||||||
TestMeasuredRecoveryIsCappedByPolicy();
|
TestMeasuredRecoveryIsCappedByPolicy();
|
||||||
TestCleanCompletionTracksCompletedIndexAndClearsStreaks();
|
TestCleanCompletionTracksCompletedIndexAndClearsStreaks();
|
||||||
TestPolicyNormalization();
|
TestPolicyNormalization();
|
||||||
|
|||||||
Reference in New Issue
Block a user