Compare commits
23 Commits
1429b2e660
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3ffb562ff7 | ||
|
|
c2d548499c | ||
|
|
6a0340d1b4 | ||
|
|
5c1fc2a6cf | ||
|
|
d411453f80 | ||
|
|
4a049a557a | ||
|
|
13586c611a | ||
|
|
3a83d9617f | ||
|
|
5c66cfdc64 | ||
|
|
d72272b5a8 | ||
|
|
c25ae7b25b | ||
|
|
a39be6fb20 | ||
|
|
0a1fe440d9 | ||
|
|
3e45bba54b | ||
|
|
fd4b70ec9c | ||
|
|
ce28904891 | ||
|
|
2c5e925b97 | ||
|
|
957c0be05a | ||
|
|
0a8b335048 | ||
|
|
6e32941675 | ||
|
|
5fb4607d8c | ||
|
|
f43b6f6519 | ||
|
|
dfd49fd0e3 |
@@ -290,6 +290,8 @@ set(RENDER_CADENCE_APP_SOURCES
|
||||
"${APP_DIR}/gl/shader/Std140Buffer.h"
|
||||
"${APP_DIR}/runtime/support/RuntimeJson.cpp"
|
||||
"${APP_DIR}/runtime/support/RuntimeJson.h"
|
||||
"${APP_DIR}/runtime/support/RuntimeParameterUtils.cpp"
|
||||
"${APP_DIR}/runtime/support/RuntimeParameterUtils.h"
|
||||
"${APP_DIR}/shader/ShaderCompiler.cpp"
|
||||
"${APP_DIR}/shader/ShaderCompiler.h"
|
||||
"${APP_DIR}/shader/ShaderPackageRegistry.cpp"
|
||||
@@ -308,12 +310,19 @@ set(RENDER_CADENCE_APP_SOURCES
|
||||
"${RENDER_CADENCE_APP_DIR}/app/AppConfigProvider.h"
|
||||
"${RENDER_CADENCE_APP_DIR}/app/RenderCadenceApp.h"
|
||||
"${RENDER_CADENCE_APP_DIR}/app/RuntimeLayerController.cpp"
|
||||
"${RENDER_CADENCE_APP_DIR}/app/RuntimeLayerControllerBuild.cpp"
|
||||
"${RENDER_CADENCE_APP_DIR}/app/RuntimeLayerControllerControls.cpp"
|
||||
"${RENDER_CADENCE_APP_DIR}/app/RuntimeLayerController.h"
|
||||
"${RENDER_CADENCE_APP_DIR}/control/ControlActionResult.h"
|
||||
"${RENDER_CADENCE_APP_DIR}/control/RuntimeControlCommand.cpp"
|
||||
"${RENDER_CADENCE_APP_DIR}/control/RuntimeControlCommand.h"
|
||||
"${RENDER_CADENCE_APP_DIR}/control/http/HttpControlServer.cpp"
|
||||
"${RENDER_CADENCE_APP_DIR}/control/http/HttpControlServer.h"
|
||||
"${RENDER_CADENCE_APP_DIR}/control/http/HttpControlServerRoutes.cpp"
|
||||
"${RENDER_CADENCE_APP_DIR}/control/http/HttpControlServerWebSocket.cpp"
|
||||
"${RENDER_CADENCE_APP_DIR}/control/RuntimeStateJson.h"
|
||||
"${RENDER_CADENCE_APP_DIR}/frames/InputFrameMailbox.cpp"
|
||||
"${RENDER_CADENCE_APP_DIR}/frames/InputFrameMailbox.h"
|
||||
"${RENDER_CADENCE_APP_DIR}/frames/SystemFrameExchange.cpp"
|
||||
"${RENDER_CADENCE_APP_DIR}/frames/SystemFrameExchange.h"
|
||||
"${RENDER_CADENCE_APP_DIR}/frames/SystemFrameTypes.h"
|
||||
@@ -323,6 +332,8 @@ set(RENDER_CADENCE_APP_SOURCES
|
||||
"${RENDER_CADENCE_APP_DIR}/logging/Logger.h"
|
||||
"${RENDER_CADENCE_APP_DIR}/platform/HiddenGlWindow.cpp"
|
||||
"${RENDER_CADENCE_APP_DIR}/platform/HiddenGlWindow.h"
|
||||
"${RENDER_CADENCE_APP_DIR}/render/InputFrameTexture.cpp"
|
||||
"${RENDER_CADENCE_APP_DIR}/render/InputFrameTexture.h"
|
||||
"${RENDER_CADENCE_APP_DIR}/render/readback/Bgra8ReadbackPipeline.cpp"
|
||||
"${RENDER_CADENCE_APP_DIR}/render/readback/Bgra8ReadbackPipeline.h"
|
||||
"${RENDER_CADENCE_APP_DIR}/render/readback/PboReadbackRing.cpp"
|
||||
@@ -337,6 +348,7 @@ set(RENDER_CADENCE_APP_SOURCES
|
||||
"${RENDER_CADENCE_APP_DIR}/render/runtime/RuntimeShaderParams.h"
|
||||
"${RENDER_CADENCE_APP_DIR}/render/runtime/RuntimeRenderScene.cpp"
|
||||
"${RENDER_CADENCE_APP_DIR}/render/runtime/RuntimeRenderScene.h"
|
||||
"${RENDER_CADENCE_APP_DIR}/render/runtime/RuntimeRenderSceneRender.cpp"
|
||||
"${RENDER_CADENCE_APP_DIR}/render/runtime/RuntimeShaderPrepareWorker.cpp"
|
||||
"${RENDER_CADENCE_APP_DIR}/render/runtime/RuntimeShaderPrepareWorker.h"
|
||||
"${RENDER_CADENCE_APP_DIR}/render/runtime/RuntimeShaderProgram.h"
|
||||
@@ -353,6 +365,9 @@ set(RENDER_CADENCE_APP_SOURCES
|
||||
"${RENDER_CADENCE_APP_DIR}/telemetry/CadenceTelemetryJson.h"
|
||||
"${RENDER_CADENCE_APP_DIR}/telemetry/CadenceTelemetry.h"
|
||||
"${RENDER_CADENCE_APP_DIR}/telemetry/TelemetryHealthMonitor.h"
|
||||
"${RENDER_CADENCE_APP_DIR}/video/DeckLinkInput.cpp"
|
||||
"${RENDER_CADENCE_APP_DIR}/video/DeckLinkInput.h"
|
||||
"${RENDER_CADENCE_APP_DIR}/video/DeckLinkInputThread.h"
|
||||
"${RENDER_CADENCE_APP_DIR}/video/DeckLinkOutput.cpp"
|
||||
"${RENDER_CADENCE_APP_DIR}/video/DeckLinkOutput.h"
|
||||
"${RENDER_CADENCE_APP_DIR}/video/DeckLinkOutputThread.h"
|
||||
@@ -786,6 +801,23 @@ endif()
|
||||
|
||||
add_test(NAME RenderCadenceCompositorFrameExchangeTests COMMAND RenderCadenceCompositorFrameExchangeTests)
|
||||
|
||||
add_executable(RenderCadenceCompositorInputFrameMailboxTests
|
||||
"${APP_DIR}/videoio/VideoIOFormat.cpp"
|
||||
"${RENDER_CADENCE_APP_DIR}/frames/InputFrameMailbox.cpp"
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/tests/RenderCadenceCompositorInputFrameMailboxTests.cpp"
|
||||
)
|
||||
|
||||
target_include_directories(RenderCadenceCompositorInputFrameMailboxTests PRIVATE
|
||||
"${APP_DIR}/videoio"
|
||||
"${RENDER_CADENCE_APP_DIR}/frames"
|
||||
)
|
||||
|
||||
if(MSVC)
|
||||
target_compile_options(RenderCadenceCompositorInputFrameMailboxTests PRIVATE /W3)
|
||||
endif()
|
||||
|
||||
add_test(NAME RenderCadenceCompositorInputFrameMailboxTests COMMAND RenderCadenceCompositorInputFrameMailboxTests)
|
||||
|
||||
add_executable(RenderCadenceCompositorClockTests
|
||||
"${RENDER_CADENCE_APP_DIR}/render/RenderCadenceClock.cpp"
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/tests/RenderCadenceCompositorClockTests.cpp"
|
||||
@@ -841,6 +873,7 @@ add_executable(RenderCadenceCompositorRuntimeLayerModelTests
|
||||
"${RENDER_CADENCE_APP_DIR}/runtime/SupportedShaderCatalog.cpp"
|
||||
"${APP_DIR}/shader/ShaderPackageRegistry.cpp"
|
||||
"${APP_DIR}/runtime/support/RuntimeJson.cpp"
|
||||
"${APP_DIR}/runtime/support/RuntimeParameterUtils.cpp"
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/tests/RenderCadenceCompositorRuntimeLayerModelTests.cpp"
|
||||
)
|
||||
|
||||
@@ -909,6 +942,7 @@ add_test(NAME RenderCadenceCompositorJsonWriterTests COMMAND RenderCadenceCompos
|
||||
|
||||
add_executable(RenderCadenceCompositorRuntimeStateJsonTests
|
||||
"${APP_DIR}/runtime/support/RuntimeJson.cpp"
|
||||
"${APP_DIR}/runtime/support/RuntimeParameterUtils.cpp"
|
||||
"${APP_DIR}/shader/ShaderPackageRegistry.cpp"
|
||||
"${RENDER_CADENCE_APP_DIR}/app/AppConfig.cpp"
|
||||
"${RENDER_CADENCE_APP_DIR}/app/AppConfigProvider.cpp"
|
||||
@@ -940,7 +974,10 @@ endif()
|
||||
add_test(NAME RenderCadenceCompositorRuntimeStateJsonTests COMMAND RenderCadenceCompositorRuntimeStateJsonTests)
|
||||
|
||||
add_executable(RenderCadenceCompositorHttpControlServerTests
|
||||
"${APP_DIR}/runtime/support/RuntimeJson.cpp"
|
||||
"${RENDER_CADENCE_APP_DIR}/control/RuntimeControlCommand.cpp"
|
||||
"${RENDER_CADENCE_APP_DIR}/control/http/HttpControlServer.cpp"
|
||||
"${RENDER_CADENCE_APP_DIR}/control/http/HttpControlServerRoutes.cpp"
|
||||
"${RENDER_CADENCE_APP_DIR}/control/http/HttpControlServerWebSocket.cpp"
|
||||
"${RENDER_CADENCE_APP_DIR}/json/JsonWriter.cpp"
|
||||
"${RENDER_CADENCE_APP_DIR}/logging/Logger.cpp"
|
||||
@@ -948,6 +985,8 @@ add_executable(RenderCadenceCompositorHttpControlServerTests
|
||||
)
|
||||
|
||||
target_include_directories(RenderCadenceCompositorHttpControlServerTests PRIVATE
|
||||
"${APP_DIR}"
|
||||
"${APP_DIR}/runtime/support"
|
||||
"${RENDER_CADENCE_APP_DIR}/control"
|
||||
"${RENDER_CADENCE_APP_DIR}/control/http"
|
||||
"${RENDER_CADENCE_APP_DIR}/json"
|
||||
|
||||
@@ -54,6 +54,12 @@ struct VideoIOState
|
||||
uint64_t actualDeckLinkBufferedFrames = 0;
|
||||
double deckLinkScheduleCallMilliseconds = 0.0;
|
||||
uint64_t deckLinkScheduleFailureCount = 0;
|
||||
bool deckLinkScheduleLeadAvailable = false;
|
||||
int64_t deckLinkPlaybackStreamTime = 0;
|
||||
uint64_t deckLinkPlaybackFrameIndex = 0;
|
||||
uint64_t deckLinkNextScheduleFrameIndex = 0;
|
||||
int64_t deckLinkScheduleLeadFrames = 0;
|
||||
uint64_t deckLinkScheduleRealignmentCount = 0;
|
||||
};
|
||||
|
||||
struct VideoIOFrame
|
||||
|
||||
@@ -32,6 +32,17 @@ VideoIOScheduleTime VideoPlayoutScheduler::NextScheduleTime()
|
||||
return time;
|
||||
}
|
||||
|
||||
void VideoPlayoutScheduler::AlignNextScheduleTimeToPlayback(int64_t streamTime, uint64_t leadFrames)
|
||||
{
|
||||
if (mFrameDuration <= 0 || streamTime < 0)
|
||||
return;
|
||||
|
||||
const uint64_t playbackFrameIndex = static_cast<uint64_t>(streamTime / mFrameDuration);
|
||||
const uint64_t minimumScheduleIndex = playbackFrameIndex + leadFrames;
|
||||
if (minimumScheduleIndex > mScheduledFrameIndex)
|
||||
mScheduledFrameIndex = minimumScheduleIndex;
|
||||
}
|
||||
|
||||
VideoPlayoutRecoveryDecision VideoPlayoutScheduler::AccountForCompletionResult(VideoIOCompletionResult result, uint64_t readyQueueDepth)
|
||||
{
|
||||
++mCompletedFrameIndex;
|
||||
|
||||
@@ -12,10 +12,12 @@ public:
|
||||
void Configure(int64_t frameDuration, int64_t timeScale, const VideoPlayoutPolicy& policy);
|
||||
void Reset();
|
||||
VideoIOScheduleTime NextScheduleTime();
|
||||
void AlignNextScheduleTimeToPlayback(int64_t streamTime, uint64_t leadFrames);
|
||||
VideoPlayoutRecoveryDecision AccountForCompletionResult(VideoIOCompletionResult result, uint64_t readyQueueDepth = 0);
|
||||
double FrameBudgetMilliseconds() const;
|
||||
uint64_t ScheduledFrameIndex() const { return mScheduledFrameIndex; }
|
||||
uint64_t CompletedFrameIndex() const { return mCompletedFrameIndex; }
|
||||
int64_t FrameDuration() const { return mFrameDuration; }
|
||||
uint64_t LateStreak() const { return mLateStreak; }
|
||||
uint64_t DropStreak() const { return mDropStreak; }
|
||||
int64_t TimeScale() const { return mTimeScale; }
|
||||
|
||||
@@ -12,6 +12,9 @@
|
||||
|
||||
namespace
|
||||
{
|
||||
constexpr int64_t kMinimumHealthyScheduleLeadFrames = 4;
|
||||
constexpr int64_t kProactiveScheduleLeadFloorFrames = 1;
|
||||
|
||||
class SystemMemoryDeckLinkVideoBuffer : public IDeckLinkVideoBuffer
|
||||
{
|
||||
public:
|
||||
@@ -526,13 +529,21 @@ bool DeckLinkSession::PopulateOutputFrame(IDeckLinkMutableVideoFrame* outputVide
|
||||
|
||||
bool DeckLinkSession::ScheduleFrame(IDeckLinkMutableVideoFrame* outputVideoFrame)
|
||||
{
|
||||
const VideoIOScheduleTime scheduleTime = mScheduler.NextScheduleTime();
|
||||
if (outputVideoFrame == nullptr || output == nullptr)
|
||||
{
|
||||
++mState.deckLinkScheduleFailureCount;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (mScheduleRealignmentPending)
|
||||
{
|
||||
RealignScheduleCursorToPlayback();
|
||||
mScheduleRealignmentPending = false;
|
||||
}
|
||||
|
||||
UpdateScheduleLeadTelemetry();
|
||||
MaybeRealignScheduleCursorForLowLead();
|
||||
const VideoIOScheduleTime scheduleTime = mScheduler.NextScheduleTime();
|
||||
const auto scheduleStart = std::chrono::steady_clock::now();
|
||||
const HRESULT result = output->ScheduleVideoFrame(outputVideoFrame, scheduleTime.streamTime, scheduleTime.duration, scheduleTime.timeScale);
|
||||
const auto scheduleEnd = std::chrono::steady_clock::now();
|
||||
@@ -543,6 +554,67 @@ bool DeckLinkSession::ScheduleFrame(IDeckLinkMutableVideoFrame* outputVideoFrame
|
||||
return result == S_OK;
|
||||
}
|
||||
|
||||
void DeckLinkSession::UpdateScheduleLeadTelemetry()
|
||||
{
|
||||
if (output == nullptr)
|
||||
{
|
||||
mState.deckLinkScheduleLeadAvailable = false;
|
||||
return;
|
||||
}
|
||||
|
||||
BMDTimeValue streamTime = 0;
|
||||
double playbackSpeed = 0.0;
|
||||
if (output->GetScheduledStreamTime(mScheduler.TimeScale(), &streamTime, &playbackSpeed) != S_OK || playbackSpeed <= 0.0)
|
||||
{
|
||||
mState.deckLinkScheduleLeadAvailable = false;
|
||||
return;
|
||||
}
|
||||
|
||||
const uint64_t playbackFrameIndex = streamTime >= 0 && mScheduler.FrameDuration() > 0
|
||||
? static_cast<uint64_t>(streamTime / mScheduler.FrameDuration())
|
||||
: 0;
|
||||
const uint64_t nextScheduleFrameIndex = mScheduler.ScheduledFrameIndex();
|
||||
mState.deckLinkScheduleLeadAvailable = true;
|
||||
mState.deckLinkPlaybackStreamTime = streamTime;
|
||||
mState.deckLinkPlaybackFrameIndex = playbackFrameIndex;
|
||||
mState.deckLinkNextScheduleFrameIndex = nextScheduleFrameIndex;
|
||||
mState.deckLinkScheduleLeadFrames = static_cast<int64_t>(nextScheduleFrameIndex) - static_cast<int64_t>(playbackFrameIndex);
|
||||
}
|
||||
|
||||
void DeckLinkSession::MaybeRealignScheduleCursorForLowLead()
|
||||
{
|
||||
if (!mState.deckLinkScheduleLeadAvailable)
|
||||
return;
|
||||
|
||||
if (mState.deckLinkScheduleLeadFrames >= kMinimumHealthyScheduleLeadFrames)
|
||||
{
|
||||
mProactiveScheduleRealignmentArmed = true;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!mProactiveScheduleRealignmentArmed || mState.deckLinkScheduleLeadFrames > kProactiveScheduleLeadFloorFrames)
|
||||
return;
|
||||
|
||||
RealignScheduleCursorToPlayback();
|
||||
mProactiveScheduleRealignmentArmed = false;
|
||||
}
|
||||
|
||||
void DeckLinkSession::RealignScheduleCursorToPlayback()
|
||||
{
|
||||
if (output == nullptr)
|
||||
return;
|
||||
|
||||
BMDTimeValue streamTime = 0;
|
||||
double playbackSpeed = 0.0;
|
||||
if (output->GetScheduledStreamTime(mScheduler.TimeScale(), &streamTime, &playbackSpeed) != S_OK || playbackSpeed <= 0.0)
|
||||
return;
|
||||
|
||||
const VideoPlayoutPolicy policy = NormalizeVideoPlayoutPolicy(mPlayoutPolicy);
|
||||
mScheduler.AlignNextScheduleTimeToPlayback(streamTime, policy.targetPrerollFrames);
|
||||
++mState.deckLinkScheduleRealignmentCount;
|
||||
UpdateScheduleLeadTelemetry();
|
||||
}
|
||||
|
||||
bool DeckLinkSession::ScheduleSystemMemoryFrame(const VideoIOOutputFrame& frame)
|
||||
{
|
||||
if (output == nullptr || frame.bytes == nullptr || frame.rowBytes <= 0 || frame.height == 0)
|
||||
@@ -827,6 +899,18 @@ void DeckLinkSession::HandlePlayoutFrameCompleted(IDeckLinkVideoFrame* completed
|
||||
|
||||
VideoIOCompletion completion;
|
||||
completion.result = TranslateCompletionResult(completionResult);
|
||||
if (completion.result == VideoIOCompletionResult::DisplayedLate || completion.result == VideoIOCompletionResult::Dropped)
|
||||
{
|
||||
if (mScheduleRealignmentArmed)
|
||||
{
|
||||
mScheduleRealignmentPending = true;
|
||||
mScheduleRealignmentArmed = false;
|
||||
}
|
||||
}
|
||||
else if (completion.result == VideoIOCompletionResult::Completed)
|
||||
{
|
||||
mScheduleRealignmentArmed = true;
|
||||
}
|
||||
completion.outputFrameBuffer = completedSystemBuffer;
|
||||
mOutputFrameCallback(completion);
|
||||
}
|
||||
|
||||
@@ -75,6 +75,9 @@ private:
|
||||
bool AcquireNextOutputVideoFrame(CComPtr<IDeckLinkMutableVideoFrame>& outputVideoFrame);
|
||||
bool PopulateOutputFrame(IDeckLinkMutableVideoFrame* outputVideoFrame, VideoIOOutputFrame& frame);
|
||||
bool ScheduleFrame(IDeckLinkMutableVideoFrame* outputVideoFrame);
|
||||
void UpdateScheduleLeadTelemetry();
|
||||
void MaybeRealignScheduleCursorForLowLead();
|
||||
void RealignScheduleCursorToPlayback();
|
||||
bool ScheduleSystemMemoryFrame(const VideoIOOutputFrame& frame);
|
||||
bool ScheduleBlackFrame(IDeckLinkMutableVideoFrame* outputVideoFrame);
|
||||
void RefreshBufferedVideoFrameCount();
|
||||
@@ -91,6 +94,9 @@ private:
|
||||
VideoIOState mState;
|
||||
VideoPlayoutPolicy mPlayoutPolicy;
|
||||
VideoPlayoutScheduler mScheduler;
|
||||
bool mScheduleRealignmentPending = false;
|
||||
bool mScheduleRealignmentArmed = true;
|
||||
bool mProactiveScheduleRealignmentArmed = true;
|
||||
InputFrameCallback mInputFrameCallback;
|
||||
OutputFrameCallback mOutputFrameCallback;
|
||||
};
|
||||
|
||||
@@ -11,13 +11,22 @@ Before adding features here, read the guardrails in [Render Cadence Golden Rules
|
||||
```text
|
||||
RenderThread
|
||||
owns a hidden OpenGL context
|
||||
polls the oldest ready input frame without waiting
|
||||
uploads input frames into a render-owned GL texture
|
||||
renders simple BGRA8 motion at selected cadence
|
||||
queues async PBO readback
|
||||
publishes completed frames into SystemFrameExchange
|
||||
|
||||
InputFrameMailbox
|
||||
owns bounded FIFO CPU input slots
|
||||
keeps a bounded three-ready-frame input buffer for render
|
||||
trims frames beyond that bound to avoid runaway input latency
|
||||
protects the one frame currently being uploaded by render
|
||||
uses a single contiguous copy when capture row stride matches mailbox row stride
|
||||
|
||||
SystemFrameExchange
|
||||
owns Free / Rendering / Completed / Scheduled slots
|
||||
drops old completed unscheduled frames when render needs space
|
||||
preserves completed output frames as a bounded FIFO reserve once they are waiting for playout
|
||||
protects scheduled frames until DeckLink completion
|
||||
|
||||
DeckLinkOutputThread
|
||||
@@ -26,20 +35,26 @@ DeckLinkOutputThread
|
||||
never renders
|
||||
```
|
||||
|
||||
Startup warms up real rendered frames before DeckLink scheduled playback starts.
|
||||
Startup builds a small output preroll reserve before DeckLink scheduled playback starts. When DeckLink input is available, startup also waits briefly for three ready input frames before the render thread starts so the first render ticks are deliberate rather than lucky.
|
||||
|
||||
## Current Scope
|
||||
|
||||
Included now:
|
||||
|
||||
- output-only DeckLink
|
||||
- optional DeckLink input edge with BGRA8 capture or raw UYVY8 capture decoded on the render thread
|
||||
- non-blocking startup when DeckLink output is unavailable
|
||||
- hidden render-thread-owned OpenGL context
|
||||
- simple smooth-motion renderer
|
||||
- BGRA8-only output
|
||||
- non-blocking three-frame FIFO input mailbox for render
|
||||
- fast contiguous mailbox copy path for matching input row strides
|
||||
- bounded three-frame input warmup before render cadence starts
|
||||
- render-thread-owned input texture upload
|
||||
- async PBO readback
|
||||
- latest-N system-memory frame exchange
|
||||
- rendered-frame warmup
|
||||
- bounded FIFO system-memory frame exchange
|
||||
- bounded completed-frame output preroll reserve before DeckLink playback, with DeckLink scheduled depth still targeted at four
|
||||
- conservative DeckLink schedule-lead telemetry and recovery
|
||||
- background Slang compile of `shaders/happy-accident`
|
||||
- app-owned display/render layer model for shader build readiness
|
||||
- app-owned submission of a completed shader artifact
|
||||
@@ -47,25 +62,31 @@ Included now:
|
||||
- shared-context GL prepare worker for runtime shader program compile/link
|
||||
- render-thread-only GL program swap once a prepared program is ready
|
||||
- manifest-driven stateless single-pass shader packages
|
||||
- HTTP shader list populated from supported stateless single-pass shader packages
|
||||
- manifest-driven stateless named-pass shader packages
|
||||
- atomic render-plan swap after every pass program is prepared
|
||||
- HTTP shader list populated from supported stateless full-frame shader packages
|
||||
- default float, vec2, color, boolean, enum, and trigger parameters
|
||||
- small JSON writer for future HTTP/WebSocket payloads
|
||||
- JSON serialization for cadence telemetry snapshots
|
||||
- background logging with `log`, `warning`, and `error` levels
|
||||
- local HTTP control server matching the OpenAPI route surface
|
||||
- HTTP layer controls for add, remove, reorder, bypass, shader change, parameter update, and parameter reset
|
||||
- trigger parameters as latest-pulse controls with shader-visible count/time
|
||||
- startup config provider for `config/runtime-host.json`
|
||||
- quiet telemetry health monitor
|
||||
- non-GL frame-exchange tests
|
||||
- non-GL input-mailbox tests
|
||||
|
||||
Intentionally not included yet:
|
||||
|
||||
- DeckLink input
|
||||
- multipass shader rendering
|
||||
- additional input format conversion/scaling
|
||||
- temporal/history/feedback shader storage
|
||||
- texture/LUT asset upload
|
||||
- text-parameter rasterization
|
||||
- runtime state
|
||||
- OSC/API control
|
||||
- OSC control
|
||||
- persistent control/state writes
|
||||
- trigger event history for stacked repeated pulses
|
||||
- preview
|
||||
- screenshots
|
||||
- persistence
|
||||
@@ -86,6 +107,8 @@ This tracks parity with `apps/LoopThroughWithOpenGLCompositing`.
|
||||
- [x] Shared-context GL shader/program preparation
|
||||
- [x] Render-thread program swap at a frame boundary
|
||||
- [x] Stateless single-pass shader rendering
|
||||
- [x] Stateless named-pass shader rendering
|
||||
- [x] Atomic multipass render-plan commit
|
||||
- [x] Shader add/remove control path
|
||||
- [x] Previous-layer texture handoff for stacked shaders
|
||||
- [x] Supported shader list in HTTP/UI state
|
||||
@@ -96,17 +119,24 @@ This tracks parity with `apps/LoopThroughWithOpenGLCompositing`.
|
||||
- [x] Startup config loading from `config/runtime-host.json`
|
||||
- [x] Cadence telemetry JSON
|
||||
- [x] Health logging for schedule/drop/starvation events
|
||||
- [ ] DeckLink input capture
|
||||
- [ ] Input frame upload into the render scene
|
||||
- [ ] Live video input bound to `gVideoInput`
|
||||
- [ ] Multipass shader rendering
|
||||
- [x] Runtime parameter updates from HTTP controls
|
||||
- [x] Layer reorder/bypass/set-shader/update-parameter/reset-parameter HTTP controls
|
||||
- [x] Trigger parameter pulse count/time for latest trigger events
|
||||
- [x] Optional DeckLink input capture
|
||||
- [x] UYVY8 input capture with render-thread GPU decode to shader input texture
|
||||
- [x] Three-frame FIFO CPU input mailbox for render
|
||||
- [x] Fast contiguous input mailbox copy when source/destination stride matches
|
||||
- [x] Bounded three-frame input warmup before render cadence starts
|
||||
- [x] Render-owned input texture upload
|
||||
- [x] Runtime shaders receive input through `gVideoInput`
|
||||
- [x] Live DeckLink input bound to `gVideoInput`
|
||||
- [ ] Input format conversion/scaling
|
||||
- [ ] Temporal history buffers
|
||||
- [ ] Feedback buffers
|
||||
- [ ] Texture asset loading and upload
|
||||
- [ ] LUT asset loading and upload
|
||||
- [ ] Text parameter rasterization
|
||||
- [ ] Runtime parameter updates from controls
|
||||
- [ ] Layer reorder/bypass/set-shader/update-parameter/reset-parameter controls
|
||||
- [ ] Trigger history/event buffers for overlapping repeated trigger effects
|
||||
- [ ] Full runtime state store/read model
|
||||
- [ ] Persistent layer stack/config writes
|
||||
- [ ] OSC ingress
|
||||
@@ -196,10 +226,10 @@ Current endpoints:
|
||||
- `GET /ws`: upgrades to a WebSocket and streams state snapshots when they change
|
||||
- `GET /docs/openapi.yaml` and `GET /openapi.yaml`: serves the OpenAPI document
|
||||
- `GET /docs`: serves Swagger UI
|
||||
- `POST /api/layers/add` and `POST /api/layers/remove` mutate the app-owned display layer model only
|
||||
- `POST /api/layers/add`, `/remove`, `/reorder`, `/set-bypass`, `/set-shader`, `/update-parameter`, and `/reset-parameters` use the shared runtime control-command path
|
||||
- other OpenAPI POST routes are present but return `{ "ok": false, "error": "Endpoint is not implemented in RenderCadenceCompositor yet." }`
|
||||
|
||||
The HTTP server runs on its own thread. It serves static UI/docs files, samples/copies telemetry through callbacks, and does not call render work or DeckLink scheduling.
|
||||
The HTTP server runs on its own thread. It serves static UI/docs files, samples/copies telemetry through callbacks, and translates POST bodies into runtime control commands. Command execution is app-owned, so future OSC ingress can create the same commands without depending on HTTP route code. Control commands may update the display layer model, start background shader builds, or publish an already-built render-layer snapshot, but they do not call render work or DeckLink scheduling directly.
|
||||
|
||||
## Optional DeckLink Output
|
||||
|
||||
@@ -208,7 +238,7 @@ DeckLink output is an optional edge service in this app.
|
||||
Startup order is:
|
||||
|
||||
1. start render thread
|
||||
2. warm up rendered system-memory frames
|
||||
2. build a bounded completed-frame output preroll reserve at normal render cadence
|
||||
3. try to attach DeckLink output
|
||||
4. start telemetry and HTTP either way
|
||||
|
||||
@@ -216,14 +246,72 @@ If DeckLink discovery or output setup fails, the app logs a warning and continue
|
||||
|
||||
`/api/state` reports the output status in `videoIO.statusMessage`.
|
||||
|
||||
## Optional DeckLink Input
|
||||
|
||||
DeckLink input is an optional edge service in this app.
|
||||
|
||||
Startup order is:
|
||||
|
||||
1. create `InputFrameMailbox`
|
||||
2. try to attach DeckLink input for the configured input mode
|
||||
3. prefer BGRA8 capture, otherwise accept raw UYVY8 capture and configure the mailbox for UYVY8 bytes
|
||||
4. start `DeckLinkInputThread`
|
||||
5. wait briefly for 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 render upload path consumes the oldest ready input frame from a bounded three-ready-frame queue, so the input behaves like a small jitter buffer instead of a latest-frame preview mailbox. The mailbox trims older frames beyond that bound to avoid runaway latency, uses one contiguous copy when the capture row stride matches the configured mailbox row stride, and falls back to row-by-row copy only for padded or mismatched frames. Unsupported input modes or formats outside BGRA8/UYVY8 are reported explicitly and treated as an unavailable edge rather than silently converted.
|
||||
|
||||
Input warmup is startup-only and bounded. It may delay render-thread startup for a short window, but it does not add waits to the steady-state render cadence loop.
|
||||
|
||||
The app samples telemetry once per second.
|
||||
|
||||
Normal cadence samples are available through `GET /api/state` and are not printed to the console. The telemetry monitor only logs health events:
|
||||
|
||||
- warning when DeckLink late/dropped-frame counters increase
|
||||
- warning when DeckLink late/dropped-frame counters increase, including schedule lead and recovery count
|
||||
- warning when schedule failures increase
|
||||
- error when the app/DeckLink output buffer is starved
|
||||
|
||||
Render cadence telemetry:
|
||||
|
||||
- `clockOverruns`: render cadence overruns where missed time was detected
|
||||
- `clockSkippedFrames`: selected-cadence frame intervals skipped instead of catch-up rendering
|
||||
- `clockOveruns` / `clockSkipped`: compatibility aliases for quick polling scripts
|
||||
|
||||
Input telemetry:
|
||||
|
||||
- `inputFramesReceived`: frames accepted into `InputFrameMailbox`
|
||||
- `inputFramesDropped`: ready input frames dropped or missed because the mailbox was full
|
||||
- `renderFrameMs`: most recent render-thread draw duration, excluding completed-readback copy and readback queue work
|
||||
- `renderFrameBudgetUsedPercent`: most recent render draw time as a percentage of the selected frame budget
|
||||
- `renderFrameMaxMs`: maximum observed render-thread draw duration for this process
|
||||
- `readbackQueueMs`: time spent queueing the most recent async BGRA8 PBO readback
|
||||
- `completedReadbackCopyMs`: time spent mapping/copying the most recent completed readback into system-memory frame storage
|
||||
- `completedDrops`: oldest completed unscheduled system-memory frames dropped because the bounded completed reserve overflowed; this is an app-side reserve drop, not a DeckLink dropped-frame report
|
||||
- `acquireMisses`: times render/readback could not acquire a writable system-memory frame slot; completed frames waiting for playout are preserved instead of being displaced
|
||||
- `deckLinkScheduleLeadAvailable`: whether DeckLink schedule-lead telemetry was available for the latest sample
|
||||
- `deckLinkScheduleLeadFrames`: estimated distance between the DeckLink playback cursor and the next scheduled stream time
|
||||
- `deckLinkPlaybackFrameIndex`: latest sampled DeckLink playback frame index
|
||||
- `deckLinkNextScheduleFrameIndex`: next stream frame index the app intends to schedule
|
||||
- `deckLinkPlaybackStreamTime`: latest sampled DeckLink playback stream time in DeckLink time units
|
||||
- `deckLinkScheduleRealignments`: conservative schedule-cursor recoveries triggered by late/drop pressure or dangerously low lead
|
||||
- `inputConsumeMisses`: render ticks where no ready input frame was available to upload
|
||||
- `inputUploadMisses`: input texture upload attempts that reused the previous GL input texture
|
||||
- `inputReadyFrames`: ready input frames currently queued in `InputFrameMailbox`
|
||||
- `inputReadingFrames`: input frames currently protected while render uploads them
|
||||
- `inputLatestAgeMs`: age of the newest submitted input frame
|
||||
- `inputUploadMs`: render-thread GL upload/decode submission time for the latest uploaded input frame
|
||||
- `inputFormatSupported`: whether the latest frame reaching the render upload path was BGRA8 or UYVY8 compatible
|
||||
- `inputSignalPresent`: whether any input frame has reached the mailbox
|
||||
- `inputCaptureFps`: DeckLink input callback capture rate
|
||||
- `inputConvertMs`: input-edge CPU conversion time; expected to remain `0` for BGRA8 and raw UYVY8 capture because UYVY8 decode is render-thread GPU work
|
||||
- `inputSubmitMs`: time spent copying/submitting the latest captured input frame to `InputFrameMailbox`
|
||||
- `inputCaptureFormat`: selected DeckLink input format (`BGRA8`, `UYVY8`, or `none`)
|
||||
- `inputNoSignalFrames`: DeckLink callbacks reporting no input source
|
||||
- `inputUnsupportedFrames`: input frames rejected before mailbox submission
|
||||
- `inputSubmitMisses`: input frames that could not be submitted to the mailbox
|
||||
|
||||
Runtime shaders continue rendering when input is missing. If no mailbox frame has been uploaded yet, shader samplers use the runtime fallback source texture; once DeckLink input is flowing, shaders such as CRT and trigger-ripple sample the current input through `gVideoInput`.
|
||||
|
||||
Healthy first-run signs:
|
||||
|
||||
- visible DeckLink output is smooth
|
||||
@@ -231,6 +319,8 @@ Healthy first-run signs:
|
||||
- `scheduleFps` is close to the selected cadence after warmup
|
||||
- `scheduled` stays near 4
|
||||
- `decklinkBuffered` stays near 4 when available
|
||||
- `deckLinkScheduleLeadFrames` remains positive and stable when available
|
||||
- `deckLinkScheduleRealignments` does not increase continuously
|
||||
- `late` and `dropped` do not increase continuously
|
||||
- `scheduleFailures` does not increase
|
||||
- `shaderCommitted` becomes `1` after the background Happy Accident compile completes
|
||||
@@ -244,22 +334,37 @@ On startup the app begins compiling the selected shader package on a background
|
||||
|
||||
The render thread keeps drawing the simple motion renderer while Slang compiles. It does not choose packages, launch Slang, or track build lifecycle. Once a completed shader artifact is published, the render-thread-owned runtime scene queues changed layers to a shared-context GL prepare worker. That worker compiles/links runtime shader programs off the cadence thread. The render thread only swaps in an already-prepared GL program at a frame boundary. If either the Slang build or GL preparation fails, the app keeps rendering the current renderer or simple motion fallback.
|
||||
|
||||
Current runtime shader support is deliberately limited to stateless single-pass packages:
|
||||
Current runtime shader support is deliberately limited to stateless full-frame packages:
|
||||
|
||||
- one pass only
|
||||
- one or more named passes
|
||||
- one sampled source input per pass
|
||||
- named intermediate outputs routed by the pass manifest
|
||||
- final visible output must be named `layerOutput`
|
||||
- no temporal history
|
||||
- no feedback storage
|
||||
- no texture/LUT assets yet
|
||||
- no text parameters yet
|
||||
- manifest defaults are used for parameters
|
||||
- the first layer receives a small fallback source texture until DeckLink input is added
|
||||
- stacked layers receive the previous ready layer output through both `gVideoInput` and `gLayerInput`
|
||||
- manifest defaults initialize parameters
|
||||
- HTTP controls can update runtime parameter values without rebuilding GL programs when the shader program is unchanged
|
||||
- trigger parameters are treated as latest-pulse controls: each press increments the trigger count and records the current runtime time
|
||||
- repeated trigger history is not stored yet, so effects such as `trigger-ripple` restart from the latest trigger rather than accumulating overlapping ripples
|
||||
- the first layer receives a small fallback source texture until DeckLink input is available
|
||||
- the first layer receives the latest DeckLink input texture through both `gVideoInput` and `gLayerInput` when input frames are available
|
||||
- stacked layers receive the original input through `gVideoInput` and the previous ready layer output through `gLayerInput`
|
||||
|
||||
The `/api/state` shader list uses the same support rules as runtime shader compilation and reports only packages this app can run today. Unsupported manifest feature sets such as multipass, temporal, feedback, texture-backed, font-backed, or text-parameter shaders are hidden from the control UI for now.
|
||||
Shader source semantics:
|
||||
|
||||
Runtime shaders are exposed through `RuntimeLayerModel` as display layers with manifest parameter defaults. The model also records whether each layer has a render-ready artifact. Add/remove POST controls mutate this app-owned model and may start background shader builds.
|
||||
- `gVideoInput` means the latest decoded shader-visible video input for every layer.
|
||||
- `gLayerInput` means the previous layer output.
|
||||
- the first layer may receive `gLayerInput = gVideoInput`.
|
||||
- later layers receive `gVideoInput = original input` and `gLayerInput = previous layer`.
|
||||
- named intermediate pass inputs inside a multipass layer are still routed through the selected pass-source slot; layer stacking should use `gLayerInput`.
|
||||
|
||||
When a layer becomes render-ready, the app publishes the ready render-layer snapshot to the render thread. The render thread owns the GL-side `RuntimeRenderScene`, diffs that snapshot at a frame boundary, queues new or changed programs to the shared-context prepare worker, swaps in prepared programs when available, removes obsolete GL programs, and renders ready layers in order. Stacked stateless full-frame shaders render through internal ping-pong targets so each layer can sample the previous layer through `gLayerInput`; the final ready layer renders to the output target.
|
||||
The `/api/state` shader list uses the same support rules as runtime shader compilation and reports only packages this app can run today. Unsupported manifest feature sets such as temporal, feedback, texture-backed, font-backed, or text-parameter shaders are hidden from the control UI for now.
|
||||
|
||||
Runtime shaders are exposed through `RuntimeLayerModel` as display layers with manifest parameter definitions, current parameter values, build status, and render-ready artifacts. POST controls mutate this app-owned model and may start background shader builds when the selected shader changes.
|
||||
|
||||
When a layer becomes render-ready, the app publishes the ready render-layer snapshot to the render thread. The render thread owns the GL-side `RuntimeRenderScene`, diffs that snapshot at a frame boundary, queues new or changed pass programs to the shared-context prepare worker, swaps in a full prepared render plan only after every pass is ready, removes obsolete GL programs, and renders ready layers in order. Stacked stateless full-frame shaders render through internal ping-pong targets so each layer can sample the previous layer through `gLayerInput`; multipass shaders route named intermediate outputs through their manifest-declared pass inputs, and the final ready layer renders to the output target.
|
||||
|
||||
Successful handoff signs:
|
||||
|
||||
@@ -290,6 +395,7 @@ Read:
|
||||
- render cadence and DeckLink schedule cadence both held roughly 60 fps
|
||||
- app scheduled depth stayed at 4
|
||||
- actual DeckLink buffered depth stayed at 4
|
||||
- DeckLink schedule lead remained positive during healthy playback
|
||||
- no late frames, dropped frames, or schedule failures were observed
|
||||
- completed poll misses were benign because playout remained fully fed
|
||||
|
||||
@@ -309,6 +415,8 @@ This app keeps the same core behavior but splits it into modules that can grow:
|
||||
- `frames/`: system-memory handoff
|
||||
- `platform/`: COM/Win32/hidden GL context support
|
||||
- `render/`: cadence thread, clock, and simple renderer
|
||||
- `frames/InputFrameMailbox`: non-blocking bounded FIFO CPU input handoff with contiguous-copy fast path for matching row strides
|
||||
- `render/InputFrameTexture`: render-thread-owned upload of the currently acquired CPU input frame into GL, including raw UYVY8 decode into the shader-visible input texture
|
||||
- `render/readback/`: PBO-backed BGRA8 readback and completed-frame publication
|
||||
- `render/runtime/RuntimeRenderScene`: render-thread-owned GL scene for ready runtime shader layers
|
||||
- `render/runtime/RuntimeShaderPrepareWorker`: shared-context runtime shader program compile/link worker
|
||||
@@ -331,4 +439,4 @@ Only after this app matches the probe's smooth output:
|
||||
3. port runtime snapshots/live state
|
||||
4. add control services
|
||||
5. add preview/screenshot from system-memory frames
|
||||
6. add DeckLink input as a CPU latest-frame mailbox
|
||||
6. add scaling and additional input format support after the BGRA8/raw-UYVY8 input edge is stable
|
||||
|
||||
@@ -1,19 +1,29 @@
|
||||
#include "app/AppConfig.h"
|
||||
#include "app/AppConfigProvider.h"
|
||||
#include "app/RenderCadenceApp.h"
|
||||
#include "frames/InputFrameMailbox.h"
|
||||
#include "frames/SystemFrameExchange.h"
|
||||
#include "logging/Logger.h"
|
||||
#include "render/RenderThread.h"
|
||||
#include "video/DeckLinkInput.h"
|
||||
#include "video/DeckLinkInputThread.h"
|
||||
#include "DeckLinkDisplayMode.h"
|
||||
#include "VideoIOFormat.h"
|
||||
|
||||
#include <windows.h>
|
||||
|
||||
#include <chrono>
|
||||
#include <iostream>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
|
||||
namespace
|
||||
{
|
||||
constexpr std::size_t kDeckLinkTargetBufferedFrames = 4;
|
||||
constexpr std::size_t kReadbackDepth = 6;
|
||||
constexpr std::size_t kWritableOutputReserveFrames = kReadbackDepth + 2;
|
||||
|
||||
class ComInitGuard
|
||||
{
|
||||
public:
|
||||
@@ -37,6 +47,19 @@ private:
|
||||
bool mInitialized = false;
|
||||
HRESULT mResult = S_OK;
|
||||
};
|
||||
|
||||
bool WaitForInputWarmup(InputFrameMailbox& mailbox, std::size_t targetReadyFrames, std::chrono::milliseconds timeout)
|
||||
{
|
||||
const auto start = std::chrono::steady_clock::now();
|
||||
while (std::chrono::steady_clock::now() - start < timeout)
|
||||
{
|
||||
const InputFrameMailboxMetrics metrics = mailbox.Metrics();
|
||||
if (metrics.readyCount >= targetReadyFrames)
|
||||
return true;
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(2));
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
int main(int argc, char** argv)
|
||||
@@ -76,24 +99,120 @@ int main(int argc, char** argv)
|
||||
frameExchangeConfig.height);
|
||||
frameExchangeConfig.pixelFormat = VideoIOPixelFormat::Bgra8;
|
||||
frameExchangeConfig.rowBytes = VideoIORowBytes(frameExchangeConfig.pixelFormat, frameExchangeConfig.width);
|
||||
frameExchangeConfig.capacity = 12;
|
||||
frameExchangeConfig.capacity =
|
||||
appConfig.warmupCompletedFrames +
|
||||
kDeckLinkTargetBufferedFrames +
|
||||
kWritableOutputReserveFrames;
|
||||
frameExchangeConfig.maxCompletedFrames = appConfig.warmupCompletedFrames;
|
||||
|
||||
SystemFrameExchange frameExchange(frameExchangeConfig);
|
||||
|
||||
InputFrameMailboxConfig inputMailboxConfig;
|
||||
RenderCadenceCompositor::VideoFormatDimensions(
|
||||
appConfig.inputVideoFormat,
|
||||
inputMailboxConfig.width,
|
||||
inputMailboxConfig.height);
|
||||
inputMailboxConfig.pixelFormat = VideoIOPixelFormat::Bgra8;
|
||||
inputMailboxConfig.rowBytes = VideoIORowBytes(inputMailboxConfig.pixelFormat, inputMailboxConfig.width);
|
||||
inputMailboxConfig.capacity = 4;
|
||||
inputMailboxConfig.maxReadyFrames = 3;
|
||||
InputFrameMailbox inputMailbox(inputMailboxConfig);
|
||||
|
||||
VideoFormat inputVideoMode;
|
||||
VideoFormat outputVideoMode;
|
||||
std::string inputVideoModeError;
|
||||
const bool inputVideoModeResolved = ResolveConfiguredVideoFormat(appConfig.inputVideoFormat, appConfig.inputFrameRate, inputVideoMode);
|
||||
const bool outputVideoModeResolved = ResolveConfiguredVideoFormat(appConfig.outputVideoFormat, appConfig.outputFrameRate, outputVideoMode);
|
||||
if (!inputVideoModeResolved)
|
||||
{
|
||||
inputVideoModeError = "Unsupported DeckLink inputVideoFormat/inputFrameRate in config/runtime-host.json: " +
|
||||
appConfig.inputVideoFormat + " / " + appConfig.inputFrameRate;
|
||||
RenderCadenceCompositor::LogWarning("app", inputVideoModeError);
|
||||
}
|
||||
if (!outputVideoModeResolved)
|
||||
{
|
||||
RenderCadenceCompositor::LogWarning(
|
||||
"app",
|
||||
"Unsupported DeckLink outputVideoFormat/outputFrameRate in config/runtime-host.json; render cadence will use parsed frame-rate fallback: " +
|
||||
appConfig.outputVideoFormat + " / " + appConfig.outputFrameRate);
|
||||
}
|
||||
else
|
||||
{
|
||||
appConfig.deckLink.outputVideoMode = outputVideoMode;
|
||||
}
|
||||
|
||||
RenderCadenceCompositor::DeckLinkInput deckLinkInput(inputMailbox);
|
||||
RenderCadenceCompositor::DeckLinkInputThread deckLinkInputThread(deckLinkInput);
|
||||
bool deckLinkInputStarted = false;
|
||||
if (inputVideoModeResolved)
|
||||
{
|
||||
RenderCadenceCompositor::DeckLinkInputConfig deckLinkInputConfig;
|
||||
deckLinkInputConfig.videoFormat = inputVideoMode;
|
||||
std::string deckLinkInputError;
|
||||
if (deckLinkInput.Initialize(deckLinkInputConfig, deckLinkInputError))
|
||||
{
|
||||
inputMailboxConfig.pixelFormat = deckLinkInput.CapturePixelFormat();
|
||||
inputMailboxConfig.rowBytes = VideoIORowBytes(inputMailboxConfig.pixelFormat, inputMailboxConfig.width);
|
||||
inputMailbox.Configure(inputMailboxConfig);
|
||||
}
|
||||
|
||||
if (deckLinkInput.IsInitialized() && deckLinkInputThread.Start(deckLinkInputError))
|
||||
{
|
||||
deckLinkInputStarted = true;
|
||||
RenderCadenceCompositor::Log("app", "DeckLink input edge started for " + inputVideoMode.displayName + ".");
|
||||
RenderCadenceCompositor::Log("app", "Waiting for DeckLink input warmup frames.");
|
||||
constexpr std::size_t kInputStartupBufferedFrames = 3;
|
||||
constexpr std::chrono::milliseconds kInputWarmupTimeout(1000);
|
||||
if (WaitForInputWarmup(inputMailbox, kInputStartupBufferedFrames, kInputWarmupTimeout))
|
||||
{
|
||||
const InputFrameMailboxMetrics metrics = inputMailbox.Metrics();
|
||||
RenderCadenceCompositor::Log(
|
||||
"app",
|
||||
"DeckLink input warmup complete. ready=" + std::to_string(metrics.readyCount) +
|
||||
" submitted=" + std::to_string(metrics.submittedFrames) + ".");
|
||||
}
|
||||
else
|
||||
{
|
||||
const InputFrameMailboxMetrics metrics = inputMailbox.Metrics();
|
||||
RenderCadenceCompositor::LogWarning(
|
||||
"app",
|
||||
"DeckLink input warmup timed out; starting render cadence with current input buffer. ready=" +
|
||||
std::to_string(metrics.readyCount) + " submitted=" + std::to_string(metrics.submittedFrames) + ".");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
RenderCadenceCompositor::LogWarning("app", "DeckLink input edge unavailable; runtime shaders will use fallback input until a real input edge is available. " + deckLinkInputError);
|
||||
deckLinkInput.ReleaseResources();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
RenderCadenceCompositor::LogWarning("app", "DeckLink input mode was not resolved; runtime shaders will use fallback input until a real input edge is available.");
|
||||
}
|
||||
|
||||
RenderThread::Config renderConfig;
|
||||
renderConfig.width = frameExchangeConfig.width;
|
||||
renderConfig.height = frameExchangeConfig.height;
|
||||
renderConfig.frameDurationMilliseconds = RenderCadenceCompositor::FrameDurationMillisecondsFromRateString(appConfig.outputFrameRate);
|
||||
renderConfig.pboDepth = 6;
|
||||
const double fallbackFrameDurationMilliseconds = RenderCadenceCompositor::FrameDurationMillisecondsFromRateString(appConfig.outputFrameRate);
|
||||
renderConfig.frameDurationMilliseconds = outputVideoModeResolved
|
||||
? RenderCadenceCompositor::FrameDurationMillisecondsFromDisplayMode(outputVideoMode.displayMode, fallbackFrameDurationMilliseconds)
|
||||
: fallbackFrameDurationMilliseconds;
|
||||
renderConfig.pboDepth = kReadbackDepth;
|
||||
|
||||
RenderThread renderThread(frameExchange, renderConfig);
|
||||
RenderThread renderThread(frameExchange, &inputMailbox, renderConfig);
|
||||
|
||||
RenderCadenceCompositor::RenderCadenceApp<RenderThread, SystemFrameExchange> app(renderThread, frameExchange, appConfig);
|
||||
app.SetDeckLinkInputMetricsProvider([&deckLinkInput]() {
|
||||
return deckLinkInput.Metrics();
|
||||
});
|
||||
|
||||
std::string error;
|
||||
if (!app.Start(error))
|
||||
{
|
||||
RenderCadenceCompositor::LogError("app", "RenderCadenceCompositor start failed: " + error);
|
||||
if (deckLinkInputStarted)
|
||||
deckLinkInputThread.Stop();
|
||||
RenderCadenceCompositor::Logger::Instance().Stop();
|
||||
return 1;
|
||||
}
|
||||
@@ -101,6 +220,8 @@ int main(int argc, char** argv)
|
||||
std::string line;
|
||||
std::getline(std::cin, line);
|
||||
app.Stop();
|
||||
if (deckLinkInputStarted)
|
||||
deckLinkInputThread.Stop();
|
||||
RenderCadenceCompositor::Log("app", "RenderCadenceCompositor stopped.");
|
||||
RenderCadenceCompositor::Logger::Instance().Stop();
|
||||
return 0;
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
#include <cstdint>
|
||||
#include <cstdlib>
|
||||
#include <fstream>
|
||||
#include <sstream>
|
||||
@@ -181,6 +182,50 @@ double FrameDurationMillisecondsFromRateString(const std::string& rateText, doub
|
||||
return 1000.0 / rate;
|
||||
}
|
||||
|
||||
double FrameDurationMillisecondsFromDisplayMode(BMDDisplayMode displayMode, double fallbackMilliseconds)
|
||||
{
|
||||
struct ModeRate
|
||||
{
|
||||
BMDDisplayMode mode;
|
||||
int64_t frameDuration;
|
||||
int64_t timeScale;
|
||||
};
|
||||
|
||||
static const ModeRate rates[] =
|
||||
{
|
||||
{ bmdModeHD720p50, 1, 50 },
|
||||
{ bmdModeHD720p5994, 1001, 60000 },
|
||||
{ bmdModeHD720p60, 1, 60 },
|
||||
{ bmdModeHD1080i50, 1, 25 },
|
||||
{ bmdModeHD1080i5994, 1001, 30000 },
|
||||
{ bmdModeHD1080i6000, 1, 30 },
|
||||
{ bmdModeHD1080p2398, 1001, 24000 },
|
||||
{ bmdModeHD1080p24, 1, 24 },
|
||||
{ bmdModeHD1080p25, 1, 25 },
|
||||
{ bmdModeHD1080p2997, 1001, 30000 },
|
||||
{ bmdModeHD1080p30, 1, 30 },
|
||||
{ bmdModeHD1080p50, 1, 50 },
|
||||
{ bmdModeHD1080p5994, 1001, 60000 },
|
||||
{ bmdModeHD1080p6000, 1, 60 },
|
||||
{ bmdMode4K2160p2398, 1001, 24000 },
|
||||
{ bmdMode4K2160p24, 1, 24 },
|
||||
{ bmdMode4K2160p25, 1, 25 },
|
||||
{ bmdMode4K2160p2997, 1001, 30000 },
|
||||
{ bmdMode4K2160p30, 1, 30 },
|
||||
{ bmdMode4K2160p50, 1, 50 },
|
||||
{ bmdMode4K2160p5994, 1001, 60000 },
|
||||
{ bmdMode4K2160p60, 1, 60 }
|
||||
};
|
||||
|
||||
for (const ModeRate& rate : rates)
|
||||
{
|
||||
if (rate.mode == displayMode && rate.timeScale > 0)
|
||||
return (static_cast<double>(rate.frameDuration) * 1000.0) / static_cast<double>(rate.timeScale);
|
||||
}
|
||||
|
||||
return fallbackMilliseconds;
|
||||
}
|
||||
|
||||
void VideoFormatDimensions(const std::string& formatName, unsigned& width, unsigned& height)
|
||||
{
|
||||
std::string normalized = formatName;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#pragma once
|
||||
|
||||
#include "AppConfig.h"
|
||||
#include "DeckLinkDisplayMode.h"
|
||||
|
||||
#include <filesystem>
|
||||
#include <string>
|
||||
@@ -27,6 +28,7 @@ private:
|
||||
};
|
||||
|
||||
double FrameDurationMillisecondsFromRateString(const std::string& rateText, double fallbackRate = 59.94);
|
||||
double FrameDurationMillisecondsFromDisplayMode(BMDDisplayMode displayMode, double fallbackMilliseconds);
|
||||
void VideoFormatDimensions(const std::string& formatName, unsigned& width, unsigned& height);
|
||||
std::filesystem::path FindConfigFile(const std::filesystem::path& relativePath = "config/runtime-host.json");
|
||||
std::filesystem::path FindRepoPath(const std::filesystem::path& relativePath);
|
||||
|
||||
@@ -6,11 +6,13 @@
|
||||
#include "../logging/Logger.h"
|
||||
#include "../control/RuntimeStateJson.h"
|
||||
#include "../telemetry/TelemetryHealthMonitor.h"
|
||||
#include "../video/DeckLinkInput.h"
|
||||
#include "../video/DeckLinkOutput.h"
|
||||
#include "../video/DeckLinkOutputThread.h"
|
||||
|
||||
#include <chrono>
|
||||
#include <filesystem>
|
||||
#include <functional>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
#include <type_traits>
|
||||
@@ -85,10 +87,8 @@ public:
|
||||
}
|
||||
mRuntimeLayers.StartStartupBuild(mConfig.runtimeShaderId);
|
||||
|
||||
Log("app", "Waiting for rendered warmup frames.");
|
||||
if (!mFrameExchange.WaitForCompletedDepth(mConfig.warmupCompletedFrames, mConfig.warmupTimeout))
|
||||
if (!BuildSettledOutputReserve(error))
|
||||
{
|
||||
error = "Timed out waiting for rendered warmup frames.";
|
||||
LogError("app", error);
|
||||
Stop();
|
||||
return false;
|
||||
@@ -118,6 +118,10 @@ public:
|
||||
|
||||
bool Started() const { return mStarted; }
|
||||
const DeckLinkOutput& Output() const { return mOutput; }
|
||||
void SetDeckLinkInputMetricsProvider(std::function<DeckLinkInputMetrics()> provider)
|
||||
{
|
||||
mDeckLinkInputMetricsProvider = std::move(provider);
|
||||
}
|
||||
|
||||
private:
|
||||
void StartOptionalVideoOutput()
|
||||
@@ -159,6 +163,25 @@ private:
|
||||
mVideoOutputEnabled = true;
|
||||
mVideoOutputStatus = "DeckLink scheduled output running.";
|
||||
Log("app", mVideoOutputStatus);
|
||||
Log(
|
||||
"app",
|
||||
"DeckLink output mode: " + mOutput.State().outputDisplayModeName +
|
||||
", frame budget " + std::to_string(mOutput.State().frameBudgetMilliseconds) + " ms.");
|
||||
}
|
||||
|
||||
bool BuildSettledOutputReserve(std::string& error)
|
||||
{
|
||||
const auto reserveTimeout = mConfig.warmupTimeout;
|
||||
Log("app",
|
||||
"Building output preroll reserve: waiting for " + std::to_string(mConfig.warmupCompletedFrames) +
|
||||
" completed frame(s).");
|
||||
if (mFrameExchange.WaitForCompletedDepth(mConfig.warmupCompletedFrames, reserveTimeout))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
error = "Timed out waiting for output preroll reserve.";
|
||||
return false;
|
||||
}
|
||||
|
||||
void DisableVideoOutput(const std::string& reason)
|
||||
@@ -184,6 +207,13 @@ private:
|
||||
callbacks.removeLayer = [this](const std::string& body) {
|
||||
return mRuntimeLayers.HandleRemoveLayer(body);
|
||||
};
|
||||
callbacks.executePost = [this](const std::string& path, const std::string& body) {
|
||||
RuntimeControlCommand command;
|
||||
std::string error;
|
||||
if (!ParseRuntimeControlCommand(path, body, command, error))
|
||||
return ControlActionResult{ false, error };
|
||||
return mRuntimeLayers.HandleControlCommand(command);
|
||||
};
|
||||
|
||||
std::string error;
|
||||
if (!mHttpServer.Start(
|
||||
@@ -201,6 +231,7 @@ private:
|
||||
std::string BuildStateJson()
|
||||
{
|
||||
CadenceTelemetrySnapshot telemetry = mHttpTelemetry.Sample(mFrameExchange, mOutput, mOutputThread, mRenderThread);
|
||||
ApplyDeckLinkInputMetrics(telemetry);
|
||||
RuntimeLayerModelSnapshot layerSnapshot = mRuntimeLayers.Snapshot(telemetry);
|
||||
return RuntimeStateToJson(RuntimeStateJsonInput{
|
||||
mConfig,
|
||||
@@ -213,6 +244,23 @@ private:
|
||||
});
|
||||
}
|
||||
|
||||
void ApplyDeckLinkInputMetrics(CadenceTelemetrySnapshot& telemetry)
|
||||
{
|
||||
if (!mDeckLinkInputMetricsProvider)
|
||||
return;
|
||||
|
||||
const DeckLinkInputMetrics inputMetrics = mDeckLinkInputMetricsProvider();
|
||||
telemetry.inputConvertMilliseconds = inputMetrics.convertMilliseconds;
|
||||
telemetry.inputSubmitMilliseconds = inputMetrics.submitMilliseconds;
|
||||
telemetry.inputNoSignalFrames = inputMetrics.noInputSourceFrames;
|
||||
telemetry.inputUnsupportedFrames = inputMetrics.unsupportedFrames;
|
||||
telemetry.inputSubmitMisses = inputMetrics.submitMisses;
|
||||
telemetry.inputCaptureFormat = inputMetrics.captureFormat ? inputMetrics.captureFormat : "none";
|
||||
if (telemetry.sampleSeconds > 0.0)
|
||||
telemetry.inputCaptureFps = static_cast<double>(inputMetrics.capturedFrames - mLastInputCapturedFrames) / telemetry.sampleSeconds;
|
||||
mLastInputCapturedFrames = inputMetrics.capturedFrames;
|
||||
}
|
||||
|
||||
bool WaitForPreroll() const
|
||||
{
|
||||
const auto deadline = std::chrono::steady_clock::now() + mConfig.prerollTimeout;
|
||||
@@ -234,6 +282,8 @@ private:
|
||||
CadenceTelemetry mHttpTelemetry;
|
||||
HttpControlServer mHttpServer;
|
||||
RuntimeLayerController mRuntimeLayers;
|
||||
std::function<DeckLinkInputMetrics()> mDeckLinkInputMetricsProvider;
|
||||
uint64_t mLastInputCapturedFrames = 0;
|
||||
bool mStarted = false;
|
||||
bool mVideoOutputEnabled = false;
|
||||
std::string mVideoOutputStatus = "DeckLink output not started.";
|
||||
|
||||
@@ -1,11 +1,7 @@
|
||||
#include "RuntimeLayerController.h"
|
||||
|
||||
#include "AppConfigProvider.h"
|
||||
#include "RuntimeJson.h"
|
||||
#include "../logging/Logger.h"
|
||||
|
||||
#include <filesystem>
|
||||
|
||||
namespace RenderCadenceCompositor
|
||||
{
|
||||
RuntimeLayerController::RuntimeLayerController(RenderLayerPublisher publisher) :
|
||||
@@ -48,48 +44,6 @@ void RuntimeLayerController::Stop()
|
||||
StopAllRuntimeShaderBuilds();
|
||||
}
|
||||
|
||||
ControlActionResult RuntimeLayerController::HandleAddLayer(const std::string& body)
|
||||
{
|
||||
CleanupRetiredShaderBuilds();
|
||||
|
||||
std::string shaderId;
|
||||
std::string error;
|
||||
if (!ExtractStringField(body, "shaderId", shaderId, error))
|
||||
return { false, error };
|
||||
|
||||
std::string layerId;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mRuntimeLayerMutex);
|
||||
if (!mRuntimeLayerModel.AddLayer(mShaderCatalog, shaderId, layerId, error))
|
||||
return { false, error };
|
||||
}
|
||||
|
||||
Log("runtime-shader", "Layer added: " + layerId + " shader=" + shaderId);
|
||||
StartLayerShaderBuild(layerId, shaderId);
|
||||
return { true, std::string() };
|
||||
}
|
||||
|
||||
ControlActionResult RuntimeLayerController::HandleRemoveLayer(const std::string& body)
|
||||
{
|
||||
CleanupRetiredShaderBuilds();
|
||||
|
||||
std::string layerId;
|
||||
std::string error;
|
||||
if (!ExtractStringField(body, "layerId", layerId, error))
|
||||
return { false, error };
|
||||
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mRuntimeLayerMutex);
|
||||
if (!mRuntimeLayerModel.RemoveLayer(layerId, error))
|
||||
return { false, error };
|
||||
}
|
||||
|
||||
Log("runtime-shader", "Layer removed: " + layerId);
|
||||
RetireLayerShaderBuild(layerId);
|
||||
PublishRuntimeRenderLayers();
|
||||
return { true, std::string() };
|
||||
}
|
||||
|
||||
RuntimeLayerModelSnapshot RuntimeLayerController::Snapshot(const CadenceTelemetrySnapshot& telemetry) const
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mRuntimeLayerMutex);
|
||||
@@ -102,113 +56,6 @@ RuntimeLayerModelSnapshot RuntimeLayerController::Snapshot(const CadenceTelemetr
|
||||
return snapshot;
|
||||
}
|
||||
|
||||
void RuntimeLayerController::LoadSupportedShaderCatalog(const std::string& shaderLibrary, unsigned maxTemporalHistoryFrames)
|
||||
{
|
||||
const std::filesystem::path shaderRoot = FindRepoPath(shaderLibrary);
|
||||
std::string error;
|
||||
if (!mShaderCatalog.Load(shaderRoot, maxTemporalHistoryFrames, error))
|
||||
{
|
||||
LogWarning("runtime-shader", "Supported shader catalog is empty: " + error);
|
||||
return;
|
||||
}
|
||||
|
||||
Log("runtime-shader", "Supported shader catalog loaded with " + std::to_string(mShaderCatalog.Shaders().size()) + " shader(s).");
|
||||
}
|
||||
|
||||
void RuntimeLayerController::InitializeLayerModel(std::string& runtimeShaderId)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mRuntimeLayerMutex);
|
||||
std::string error;
|
||||
if (!mRuntimeLayerModel.InitializeSingleLayer(mShaderCatalog, runtimeShaderId, error))
|
||||
{
|
||||
LogWarning("runtime-shader", error + " Runtime shader build disabled.");
|
||||
runtimeShaderId.clear();
|
||||
mRuntimeLayerModel.Clear();
|
||||
}
|
||||
}
|
||||
|
||||
void RuntimeLayerController::StartLayerShaderBuild(const std::string& layerId, const std::string& shaderId)
|
||||
{
|
||||
CleanupRetiredShaderBuilds();
|
||||
RetireLayerShaderBuild(layerId);
|
||||
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mRuntimeLayerMutex);
|
||||
std::string error;
|
||||
mRuntimeLayerModel.MarkBuildStarted(layerId, "Runtime Slang build started for shader '" + shaderId + "'.", error);
|
||||
}
|
||||
|
||||
auto bridge = std::make_unique<RuntimeShaderBridge>();
|
||||
RuntimeShaderBridge* bridgePtr = bridge.get();
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mShaderBuildMutex);
|
||||
mShaderBuilds[layerId] = std::move(bridge);
|
||||
}
|
||||
|
||||
bridgePtr->Start(
|
||||
layerId,
|
||||
shaderId,
|
||||
[this](const RuntimeShaderArtifact& artifact) {
|
||||
if (MarkRuntimeBuildReady(artifact))
|
||||
PublishRuntimeRenderLayers();
|
||||
},
|
||||
[this, layerId](const std::string& message) {
|
||||
MarkRuntimeBuildFailedForLayer(layerId, message);
|
||||
LogError("runtime-shader", "Runtime Slang build failed: " + message);
|
||||
});
|
||||
}
|
||||
|
||||
void RuntimeLayerController::RetireLayerShaderBuild(const std::string& layerId)
|
||||
{
|
||||
std::unique_ptr<RuntimeShaderBridge> bridge;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mShaderBuildMutex);
|
||||
auto bridgeIt = mShaderBuilds.find(layerId);
|
||||
if (bridgeIt == mShaderBuilds.end())
|
||||
return;
|
||||
bridge = std::move(bridgeIt->second);
|
||||
mShaderBuilds.erase(bridgeIt);
|
||||
bridge->RequestStop();
|
||||
mRetiredShaderBuilds.push_back(std::move(bridge));
|
||||
}
|
||||
}
|
||||
|
||||
void RuntimeLayerController::CleanupRetiredShaderBuilds()
|
||||
{
|
||||
std::vector<std::unique_ptr<RuntimeShaderBridge>> readyToStop;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mShaderBuildMutex);
|
||||
for (auto it = mRetiredShaderBuilds.begin(); it != mRetiredShaderBuilds.end();)
|
||||
{
|
||||
if ((*it)->CanStopWithoutWaiting())
|
||||
{
|
||||
readyToStop.push_back(std::move(*it));
|
||||
it = mRetiredShaderBuilds.erase(it);
|
||||
continue;
|
||||
}
|
||||
++it;
|
||||
}
|
||||
}
|
||||
|
||||
for (std::unique_ptr<RuntimeShaderBridge>& bridge : readyToStop)
|
||||
bridge->Stop();
|
||||
}
|
||||
|
||||
void RuntimeLayerController::StopAllRuntimeShaderBuilds()
|
||||
{
|
||||
std::map<std::string, std::unique_ptr<RuntimeShaderBridge>> builds;
|
||||
std::vector<std::unique_ptr<RuntimeShaderBridge>> retiredBuilds;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mShaderBuildMutex);
|
||||
builds.swap(mShaderBuilds);
|
||||
retiredBuilds.swap(mRetiredShaderBuilds);
|
||||
}
|
||||
for (auto& entry : builds)
|
||||
entry.second->Stop();
|
||||
for (auto& bridge : retiredBuilds)
|
||||
bridge->Stop();
|
||||
}
|
||||
|
||||
void RuntimeLayerController::PublishRuntimeRenderLayers()
|
||||
{
|
||||
if (!mPublisher)
|
||||
@@ -242,31 +89,4 @@ void RuntimeLayerController::MarkRuntimeBuildFailedForLayer(const std::string& l
|
||||
LogWarning("runtime-shader", error);
|
||||
}
|
||||
|
||||
std::string RuntimeLayerController::FirstRuntimeLayerId() const
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mRuntimeLayerMutex);
|
||||
return mRuntimeLayerModel.FirstLayerId();
|
||||
}
|
||||
|
||||
bool RuntimeLayerController::ExtractStringField(const std::string& body, const char* fieldName, std::string& value, std::string& error)
|
||||
{
|
||||
JsonValue root;
|
||||
std::string parseError;
|
||||
if (!ParseJson(body.empty() ? "{}" : body, root, parseError) || !root.isObject())
|
||||
{
|
||||
error = parseError.empty() ? "Request body must be a JSON object." : parseError;
|
||||
return false;
|
||||
}
|
||||
|
||||
const JsonValue* field = root.find(fieldName);
|
||||
if (!field || !field->isString() || field->asString().empty())
|
||||
{
|
||||
error = std::string("Request field '") + fieldName + "' must be a non-empty string.";
|
||||
return false;
|
||||
}
|
||||
|
||||
value = field->asString();
|
||||
error.clear();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#pragma once
|
||||
|
||||
#include "../control/ControlActionResult.h"
|
||||
#include "../control/RuntimeControlCommand.h"
|
||||
#include "../runtime/RuntimeLayerModel.h"
|
||||
#include "../runtime/RuntimeShaderBridge.h"
|
||||
#include "../runtime/SupportedShaderCatalog.h"
|
||||
@@ -32,6 +33,7 @@ public:
|
||||
|
||||
ControlActionResult HandleAddLayer(const std::string& body);
|
||||
ControlActionResult HandleRemoveLayer(const std::string& body);
|
||||
ControlActionResult HandleControlCommand(const RuntimeControlCommand& command);
|
||||
|
||||
RuntimeLayerModelSnapshot Snapshot(const CadenceTelemetrySnapshot& telemetry) const;
|
||||
const SupportedShaderCatalog& ShaderCatalog() const { return mShaderCatalog; }
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
127
apps/RenderCadenceCompositor/control/RuntimeControlCommand.cpp
Normal file
127
apps/RenderCadenceCompositor/control/RuntimeControlCommand.cpp
Normal file
@@ -0,0 +1,127 @@
|
||||
#include "RuntimeControlCommand.h"
|
||||
|
||||
namespace RenderCadenceCompositor
|
||||
{
|
||||
namespace
|
||||
{
|
||||
const JsonValue* RequireObjectField(const JsonValue& root, const char* fieldName, std::string& error)
|
||||
{
|
||||
const JsonValue* field = root.find(fieldName);
|
||||
if (!field)
|
||||
error = std::string("Request field '") + fieldName + "' is required.";
|
||||
return field;
|
||||
}
|
||||
|
||||
bool RequireStringField(const JsonValue& root, const char* fieldName, std::string& value, std::string& error)
|
||||
{
|
||||
const JsonValue* field = RequireObjectField(root, fieldName, error);
|
||||
if (!field)
|
||||
return false;
|
||||
if (!field->isString() || field->asString().empty())
|
||||
{
|
||||
error = std::string("Request field '") + fieldName + "' must be a non-empty string.";
|
||||
return false;
|
||||
}
|
||||
value = field->asString();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool RequireBoolField(const JsonValue& root, const char* fieldName, bool& value, std::string& error)
|
||||
{
|
||||
const JsonValue* field = RequireObjectField(root, fieldName, error);
|
||||
if (!field)
|
||||
return false;
|
||||
if (!field->isBoolean())
|
||||
{
|
||||
error = std::string("Request field '") + fieldName + "' must be a boolean.";
|
||||
return false;
|
||||
}
|
||||
value = field->asBoolean();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool RequireIntegerField(const JsonValue& root, const char* fieldName, int& value, std::string& error)
|
||||
{
|
||||
const JsonValue* field = RequireObjectField(root, fieldName, error);
|
||||
if (!field)
|
||||
return false;
|
||||
if (!field->isNumber())
|
||||
{
|
||||
error = std::string("Request field '") + fieldName + "' must be a number.";
|
||||
return false;
|
||||
}
|
||||
value = static_cast<int>(field->asNumber());
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
bool ParseRuntimeControlCommand(
|
||||
const std::string& path,
|
||||
const std::string& body,
|
||||
RuntimeControlCommand& command,
|
||||
std::string& error)
|
||||
{
|
||||
command = RuntimeControlCommand();
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
if (path == "/api/layers/add")
|
||||
{
|
||||
command.type = RuntimeControlCommandType::AddLayer;
|
||||
return RequireStringField(root, "shaderId", command.shaderId, error);
|
||||
}
|
||||
if (path == "/api/layers/remove")
|
||||
{
|
||||
command.type = RuntimeControlCommandType::RemoveLayer;
|
||||
return RequireStringField(root, "layerId", command.layerId, error);
|
||||
}
|
||||
if (path == "/api/layers/reorder")
|
||||
{
|
||||
command.type = RuntimeControlCommandType::ReorderLayer;
|
||||
return RequireStringField(root, "layerId", command.layerId, error)
|
||||
&& RequireIntegerField(root, "targetIndex", command.targetIndex, error);
|
||||
}
|
||||
if (path == "/api/layers/set-bypass")
|
||||
{
|
||||
command.type = RuntimeControlCommandType::SetLayerBypass;
|
||||
return RequireStringField(root, "layerId", command.layerId, error)
|
||||
&& RequireBoolField(root, "bypass", command.bypass, error);
|
||||
}
|
||||
if (path == "/api/layers/set-shader")
|
||||
{
|
||||
command.type = RuntimeControlCommandType::SetLayerShader;
|
||||
return RequireStringField(root, "layerId", command.layerId, error)
|
||||
&& RequireStringField(root, "shaderId", command.shaderId, error);
|
||||
}
|
||||
if (path == "/api/layers/update-parameter")
|
||||
{
|
||||
command.type = RuntimeControlCommandType::UpdateLayerParameter;
|
||||
const JsonValue* value = nullptr;
|
||||
if (!RequireStringField(root, "layerId", command.layerId, error)
|
||||
|| !RequireStringField(root, "parameterId", command.parameterId, error))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
value = RequireObjectField(root, "value", error);
|
||||
if (!value)
|
||||
return false;
|
||||
command.value = *value;
|
||||
return true;
|
||||
}
|
||||
if (path == "/api/layers/reset-parameters")
|
||||
{
|
||||
command.type = RuntimeControlCommandType::ResetLayerParameters;
|
||||
return RequireStringField(root, "layerId", command.layerId, error);
|
||||
}
|
||||
|
||||
command.type = RuntimeControlCommandType::Unsupported;
|
||||
error = "Endpoint is not implemented in RenderCadenceCompositor yet.";
|
||||
return false;
|
||||
}
|
||||
}
|
||||
37
apps/RenderCadenceCompositor/control/RuntimeControlCommand.h
Normal file
37
apps/RenderCadenceCompositor/control/RuntimeControlCommand.h
Normal file
@@ -0,0 +1,37 @@
|
||||
#pragma once
|
||||
|
||||
#include "RuntimeJson.h"
|
||||
|
||||
#include <string>
|
||||
|
||||
namespace RenderCadenceCompositor
|
||||
{
|
||||
enum class RuntimeControlCommandType
|
||||
{
|
||||
AddLayer,
|
||||
RemoveLayer,
|
||||
ReorderLayer,
|
||||
SetLayerBypass,
|
||||
SetLayerShader,
|
||||
UpdateLayerParameter,
|
||||
ResetLayerParameters,
|
||||
Unsupported
|
||||
};
|
||||
|
||||
struct RuntimeControlCommand
|
||||
{
|
||||
RuntimeControlCommandType type = RuntimeControlCommandType::Unsupported;
|
||||
std::string layerId;
|
||||
std::string shaderId;
|
||||
std::string parameterId;
|
||||
int targetIndex = 0;
|
||||
bool bypass = false;
|
||||
JsonValue value;
|
||||
};
|
||||
|
||||
bool ParseRuntimeControlCommand(
|
||||
const std::string& path,
|
||||
const std::string& body,
|
||||
RuntimeControlCommand& command,
|
||||
std::string& error);
|
||||
}
|
||||
@@ -93,6 +93,31 @@ inline void WriteDefaultParameterValue(JsonWriter& writer, const ShaderParameter
|
||||
writer.Null();
|
||||
}
|
||||
|
||||
inline void WriteParameterValue(JsonWriter& writer, const ShaderParameterDefinition& parameter, const ShaderParameterValue& value)
|
||||
{
|
||||
switch (parameter.type)
|
||||
{
|
||||
case ShaderParameterType::Boolean:
|
||||
writer.Bool(value.booleanValue);
|
||||
return;
|
||||
case ShaderParameterType::Enum:
|
||||
writer.String(value.enumValue);
|
||||
return;
|
||||
case ShaderParameterType::Text:
|
||||
writer.String(value.textValue);
|
||||
return;
|
||||
case ShaderParameterType::Trigger:
|
||||
case ShaderParameterType::Float:
|
||||
writer.Double(value.numberValues.empty() ? 0.0 : value.numberValues.front());
|
||||
return;
|
||||
case ShaderParameterType::Vec2:
|
||||
case ShaderParameterType::Color:
|
||||
WriteNumberArray(writer, value.numberValues);
|
||||
return;
|
||||
}
|
||||
writer.Null();
|
||||
}
|
||||
|
||||
inline void WriteTemporalJson(JsonWriter& writer, const TemporalSettings& temporal)
|
||||
{
|
||||
writer.BeginObject();
|
||||
@@ -122,7 +147,7 @@ inline const char* RuntimeLayerBuildStateName(RuntimeLayerBuildState state)
|
||||
return "unknown";
|
||||
}
|
||||
|
||||
inline void WriteParameterDefinitionJson(JsonWriter& writer, const ShaderParameterDefinition& parameter)
|
||||
inline void WriteParameterDefinitionJson(JsonWriter& writer, const ShaderParameterDefinition& parameter, const ShaderParameterValue* value)
|
||||
{
|
||||
writer.BeginObject();
|
||||
writer.KeyString("id", parameter.id);
|
||||
@@ -132,7 +157,10 @@ inline void WriteParameterDefinitionJson(JsonWriter& writer, const ShaderParamet
|
||||
writer.Key("defaultValue");
|
||||
WriteDefaultParameterValue(writer, parameter);
|
||||
writer.Key("value");
|
||||
WriteDefaultParameterValue(writer, parameter);
|
||||
if (value)
|
||||
WriteParameterValue(writer, parameter, *value);
|
||||
else
|
||||
WriteDefaultParameterValue(writer, parameter);
|
||||
|
||||
if (!parameter.minNumbers.empty())
|
||||
{
|
||||
@@ -197,10 +225,10 @@ inline void WriteLayersJson(JsonWriter& writer, const RuntimeStateJsonInput& inp
|
||||
WriteFeedbackJson(writer, FeedbackSettings());
|
||||
writer.Key("parameters");
|
||||
writer.BeginArray();
|
||||
if (shaderPackage)
|
||||
for (const ShaderParameterDefinition& parameter : layer.parameterDefinitions)
|
||||
{
|
||||
for (const ShaderParameterDefinition& parameter : shaderPackage->parameters)
|
||||
WriteParameterDefinitionJson(writer, parameter);
|
||||
const auto valueIt = layer.parameterValues.find(parameter.id);
|
||||
WriteParameterDefinitionJson(writer, parameter, valueIt == layer.parameterValues.end() ? nullptr : &valueIt->second);
|
||||
}
|
||||
writer.EndArray();
|
||||
writer.EndObject();
|
||||
@@ -255,9 +283,9 @@ inline std::string RuntimeStateToJson(const RuntimeStateJsonInput& input)
|
||||
writer.Key("performance");
|
||||
writer.BeginObject();
|
||||
writer.KeyDouble("frameBudgetMs", FrameDurationMillisecondsFromRateString(input.config.outputFrameRate));
|
||||
writer.KeyNull("renderMs");
|
||||
writer.KeyDouble("renderMs", input.telemetry.renderFrameMilliseconds);
|
||||
writer.KeyNull("smoothedRenderMs");
|
||||
writer.KeyNull("budgetUsedPercent");
|
||||
writer.KeyDouble("budgetUsedPercent", input.telemetry.renderFrameBudgetUsedPercent);
|
||||
writer.KeyNull("completionIntervalMs");
|
||||
writer.KeyNull("smoothedCompletionIntervalMs");
|
||||
writer.KeyNull("maxCompletionIntervalMs");
|
||||
|
||||
@@ -1,15 +1,11 @@
|
||||
#include "HttpControlServer.h"
|
||||
|
||||
#include "../json/JsonWriter.h"
|
||||
#include "../logging/Logger.h"
|
||||
|
||||
#include <ws2tcpip.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
#include <fstream>
|
||||
#include <sstream>
|
||||
#include <vector>
|
||||
|
||||
namespace RenderCadenceCompositor
|
||||
{
|
||||
@@ -27,22 +23,6 @@ bool InitializeWinsock(std::string& error)
|
||||
return true;
|
||||
}
|
||||
|
||||
bool IsKnownPostEndpoint(const std::string& path)
|
||||
{
|
||||
return path == "/api/layers/add"
|
||||
|| path == "/api/layers/remove"
|
||||
|| path == "/api/layers/move"
|
||||
|| path == "/api/layers/reorder"
|
||||
|| path == "/api/layers/set-bypass"
|
||||
|| path == "/api/layers/set-shader"
|
||||
|| path == "/api/layers/update-parameter"
|
||||
|| path == "/api/layers/reset-parameters"
|
||||
|| path == "/api/stack-presets/save"
|
||||
|| path == "/api/stack-presets/load"
|
||||
|| path == "/api/reload"
|
||||
|| path == "/api/screenshot";
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
UniqueSocket::UniqueSocket(SOCKET socket) :
|
||||
@@ -260,173 +240,6 @@ HttpControlServer::HttpResponse HttpControlServer::RouteRequest(const HttpReques
|
||||
return TextResponse("404 Not Found", "Not Found");
|
||||
}
|
||||
|
||||
HttpControlServer::HttpResponse HttpControlServer::ServeGet(const HttpRequest& request) const
|
||||
{
|
||||
if (request.path == "/api/state")
|
||||
return JsonResponse("200 OK", mCallbacks.getStateJson ? mCallbacks.getStateJson() : "{}");
|
||||
if (request.path == "/openapi.yaml" || request.path == "/docs/openapi.yaml")
|
||||
return ServeOpenApiSpec();
|
||||
if (request.path == "/docs" || request.path == "/docs/")
|
||||
return ServeSwaggerDocs();
|
||||
if (request.path == "/" || request.path == "/index.html")
|
||||
return ServeUiAsset("index.html");
|
||||
if (request.path.rfind("/assets/", 0) == 0)
|
||||
return ServeUiAsset(request.path.substr(1));
|
||||
if (request.path.size() > 1)
|
||||
{
|
||||
const HttpResponse asset = ServeUiAsset(request.path.substr(1));
|
||||
if (asset.status != "404 Not Found")
|
||||
return asset;
|
||||
}
|
||||
return ServeUiAsset("index.html");
|
||||
}
|
||||
|
||||
HttpControlServer::HttpResponse HttpControlServer::ServePost(const HttpRequest& request) const
|
||||
{
|
||||
if (!IsKnownPostEndpoint(request.path))
|
||||
return TextResponse("404 Not Found", "Not Found");
|
||||
|
||||
if (request.path == "/api/layers/add" && mCallbacks.addLayer)
|
||||
{
|
||||
const ControlActionResult result = mCallbacks.addLayer(request.body);
|
||||
return JsonResponse(result.ok ? "200 OK" : "400 Bad Request", ActionResponse(result.ok, result.error));
|
||||
}
|
||||
|
||||
if (request.path == "/api/layers/remove" && mCallbacks.removeLayer)
|
||||
{
|
||||
const ControlActionResult result = mCallbacks.removeLayer(request.body);
|
||||
return JsonResponse(result.ok ? "200 OK" : "400 Bad Request", ActionResponse(result.ok, result.error));
|
||||
}
|
||||
|
||||
return {
|
||||
"400 Bad Request",
|
||||
"application/json",
|
||||
ActionResponse(false, "Endpoint is not implemented in RenderCadenceCompositor yet.")
|
||||
};
|
||||
}
|
||||
|
||||
HttpControlServer::HttpResponse HttpControlServer::ServeOpenApiSpec() const
|
||||
{
|
||||
const std::filesystem::path path = mDocsRoot / "openapi.yaml";
|
||||
const std::string body = LoadTextFile(path);
|
||||
return body.empty()
|
||||
? TextResponse("404 Not Found", "OpenAPI spec not found")
|
||||
: HttpResponse{ "200 OK", GuessContentType(path), body };
|
||||
}
|
||||
|
||||
HttpControlServer::HttpResponse HttpControlServer::ServeSwaggerDocs() const
|
||||
{
|
||||
std::ostringstream html;
|
||||
html << "<!doctype html>\n"
|
||||
<< "<html><head><meta charset=\"utf-8\"><title>Video Shader Toys API Docs</title>\n"
|
||||
<< "<link rel=\"stylesheet\" href=\"https://unpkg.com/swagger-ui-dist@5/swagger-ui.css\"></head>\n"
|
||||
<< "<body><div id=\"swagger-ui\"></div>\n"
|
||||
<< "<script src=\"https://unpkg.com/swagger-ui-dist@5/swagger-ui-bundle.js\"></script>\n"
|
||||
<< "<script>SwaggerUIBundle({url:'/docs/openapi.yaml',dom_id:'#swagger-ui'});</script>\n"
|
||||
<< "</body></html>\n";
|
||||
return { "200 OK", "text/html", html.str() };
|
||||
}
|
||||
|
||||
HttpControlServer::HttpResponse HttpControlServer::ServeUiAsset(const std::string& relativePath) const
|
||||
{
|
||||
if (mUiRoot.empty())
|
||||
return TextResponse("404 Not Found", "UI root is not configured");
|
||||
|
||||
const std::filesystem::path sanitizedPath = std::filesystem::path(relativePath).lexically_normal();
|
||||
if (!IsSafeRelativePath(sanitizedPath))
|
||||
return TextResponse("404 Not Found", "Not Found");
|
||||
|
||||
const std::filesystem::path path = mUiRoot / sanitizedPath;
|
||||
const std::string body = LoadTextFile(path);
|
||||
if (body.empty())
|
||||
return TextResponse("404 Not Found", "Not Found");
|
||||
return { "200 OK", GuessContentType(path), body };
|
||||
}
|
||||
|
||||
std::string HttpControlServer::LoadTextFile(const std::filesystem::path& path) const
|
||||
{
|
||||
std::ifstream input(path, std::ios::binary);
|
||||
if (!input)
|
||||
return std::string();
|
||||
|
||||
std::ostringstream buffer;
|
||||
buffer << input.rdbuf();
|
||||
return buffer.str();
|
||||
}
|
||||
|
||||
HttpControlServer::HttpResponse HttpControlServer::JsonResponse(const std::string& status, const std::string& body)
|
||||
{
|
||||
return { status, "application/json", body };
|
||||
}
|
||||
|
||||
HttpControlServer::HttpResponse HttpControlServer::TextResponse(const std::string& status, const std::string& body)
|
||||
{
|
||||
return { status, "text/plain", body };
|
||||
}
|
||||
|
||||
HttpControlServer::HttpResponse HttpControlServer::HtmlResponse(const std::string& status, const std::string& body)
|
||||
{
|
||||
return { status, "text/html", body };
|
||||
}
|
||||
|
||||
std::string HttpControlServer::ActionResponse(bool ok, const std::string& error)
|
||||
{
|
||||
JsonWriter writer;
|
||||
writer.BeginObject();
|
||||
writer.KeyBool("ok", ok);
|
||||
if (!error.empty())
|
||||
writer.KeyString("error", error);
|
||||
writer.EndObject();
|
||||
return writer.StringValue();
|
||||
}
|
||||
|
||||
std::string HttpControlServer::GuessContentType(const std::filesystem::path& path)
|
||||
{
|
||||
const std::string extension = ToLower(path.extension().string());
|
||||
if (extension == ".yaml" || extension == ".yml")
|
||||
return "application/yaml";
|
||||
if (extension == ".json")
|
||||
return "application/json";
|
||||
if (extension == ".js" || extension == ".mjs")
|
||||
return "text/javascript";
|
||||
if (extension == ".css")
|
||||
return "text/css";
|
||||
if (extension == ".html" || extension == ".htm")
|
||||
return "text/html";
|
||||
if (extension == ".svg")
|
||||
return "image/svg+xml";
|
||||
if (extension == ".png")
|
||||
return "image/png";
|
||||
if (extension == ".jpg" || extension == ".jpeg")
|
||||
return "image/jpeg";
|
||||
if (extension == ".ico")
|
||||
return "image/x-icon";
|
||||
if (extension == ".map")
|
||||
return "application/json";
|
||||
return "text/plain";
|
||||
}
|
||||
|
||||
bool HttpControlServer::IsSafeRelativePath(const std::filesystem::path& path)
|
||||
{
|
||||
if (path.empty() || path.is_absolute())
|
||||
return false;
|
||||
|
||||
for (const std::filesystem::path& part : path)
|
||||
{
|
||||
if (part == "..")
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
std::string HttpControlServer::ToLower(std::string text)
|
||||
{
|
||||
std::transform(text.begin(), text.end(), text.begin(), [](unsigned char character) {
|
||||
return static_cast<char>(std::tolower(character));
|
||||
});
|
||||
return text;
|
||||
}
|
||||
|
||||
bool HttpControlServer::ParseHttpRequest(const std::string& rawRequest, HttpRequest& request)
|
||||
{
|
||||
const std::size_t requestLineEnd = rawRequest.find("\r\n");
|
||||
|
||||
@@ -28,6 +28,7 @@ struct HttpControlServerCallbacks
|
||||
std::function<std::string()> getStateJson;
|
||||
std::function<ControlActionResult(const std::string&)> addLayer;
|
||||
std::function<ControlActionResult(const std::string&)> removeLayer;
|
||||
std::function<ControlActionResult(const std::string&, const std::string&)> executePost;
|
||||
};
|
||||
|
||||
class UniqueSocket
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
250
apps/RenderCadenceCompositor/frames/InputFrameMailbox.cpp
Normal file
250
apps/RenderCadenceCompositor/frames/InputFrameMailbox.cpp
Normal file
@@ -0,0 +1,250 @@
|
||||
#include "InputFrameMailbox.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <chrono>
|
||||
#include <cstring>
|
||||
|
||||
namespace
|
||||
{
|
||||
InputFrameMailboxConfig NormalizeConfig(InputFrameMailboxConfig config)
|
||||
{
|
||||
if (config.rowBytes == 0)
|
||||
config.rowBytes = VideoIORowBytes(config.pixelFormat, config.width);
|
||||
return config;
|
||||
}
|
||||
}
|
||||
|
||||
InputFrameMailbox::InputFrameMailbox(const InputFrameMailboxConfig& config)
|
||||
{
|
||||
Configure(config);
|
||||
}
|
||||
|
||||
void InputFrameMailbox::Configure(const InputFrameMailboxConfig& config)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mMutex);
|
||||
mConfig = NormalizeConfig(config);
|
||||
mReadyIndices.clear();
|
||||
mSlots.clear();
|
||||
mSlots.resize(mConfig.capacity);
|
||||
|
||||
const std::size_t byteCount = FrameByteCount();
|
||||
for (Slot& slot : mSlots)
|
||||
{
|
||||
slot.bytes.resize(byteCount);
|
||||
slot.state = InputFrameSlotState::Free;
|
||||
slot.frameIndex = 0;
|
||||
++slot.generation;
|
||||
}
|
||||
|
||||
mCounters = InputFrameMailboxMetrics();
|
||||
}
|
||||
|
||||
InputFrameMailboxConfig InputFrameMailbox::Config() const
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mMutex);
|
||||
return mConfig;
|
||||
}
|
||||
|
||||
bool InputFrameMailbox::SubmitFrame(const void* bytes, unsigned rowBytes, uint64_t frameIndex)
|
||||
{
|
||||
if (bytes == nullptr || rowBytes == 0)
|
||||
return false;
|
||||
|
||||
std::lock_guard<std::mutex> lock(mMutex);
|
||||
if (mSlots.empty() || mConfig.width == 0 || mConfig.height == 0)
|
||||
return false;
|
||||
|
||||
std::size_t slotIndex = mSlots.size();
|
||||
for (std::size_t index = 0; index < mSlots.size(); ++index)
|
||||
{
|
||||
if (mSlots[index].state == InputFrameSlotState::Free)
|
||||
{
|
||||
slotIndex = index;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (slotIndex == mSlots.size())
|
||||
{
|
||||
if (!DropOldestReadyLocked())
|
||||
{
|
||||
++mCounters.submitMisses;
|
||||
return false;
|
||||
}
|
||||
|
||||
for (std::size_t index = 0; index < mSlots.size(); ++index)
|
||||
{
|
||||
if (mSlots[index].state == InputFrameSlotState::Free)
|
||||
{
|
||||
slotIndex = index;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (slotIndex == mSlots.size())
|
||||
{
|
||||
++mCounters.submitMisses;
|
||||
return false;
|
||||
}
|
||||
|
||||
Slot& slot = mSlots[slotIndex];
|
||||
const std::size_t destinationRowBytes = mConfig.rowBytes;
|
||||
const std::size_t sourceRowBytes = static_cast<std::size_t>(rowBytes);
|
||||
const unsigned char* source = static_cast<const unsigned char*>(bytes);
|
||||
if (sourceRowBytes == destinationRowBytes)
|
||||
{
|
||||
std::memcpy(slot.bytes.data(), source, destinationRowBytes * static_cast<std::size_t>(mConfig.height));
|
||||
}
|
||||
else
|
||||
{
|
||||
const std::size_t copyRowBytes = (std::min)(sourceRowBytes, destinationRowBytes);
|
||||
for (unsigned y = 0; y < mConfig.height; ++y)
|
||||
{
|
||||
std::memcpy(
|
||||
slot.bytes.data() + static_cast<std::size_t>(y) * destinationRowBytes,
|
||||
source + static_cast<std::size_t>(y) * sourceRowBytes,
|
||||
copyRowBytes);
|
||||
}
|
||||
}
|
||||
|
||||
slot.state = InputFrameSlotState::Ready;
|
||||
slot.frameIndex = frameIndex;
|
||||
++slot.generation;
|
||||
mReadyIndices.push_back(slotIndex);
|
||||
TrimReadyFramesLocked();
|
||||
++mCounters.submittedFrames;
|
||||
mCounters.latestFrameIndex = frameIndex;
|
||||
mCounters.hasSubmittedFrame = true;
|
||||
mLatestSubmitTime = std::chrono::steady_clock::now();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool InputFrameMailbox::TryAcquireOldest(InputFrame& frame)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mMutex);
|
||||
while (!mReadyIndices.empty())
|
||||
{
|
||||
const std::size_t index = mReadyIndices.front();
|
||||
mReadyIndices.pop_front();
|
||||
if (index >= mSlots.size() || mSlots[index].state != InputFrameSlotState::Ready)
|
||||
continue;
|
||||
|
||||
mSlots[index].state = InputFrameSlotState::Reading;
|
||||
FillFrameLocked(index, frame);
|
||||
++mCounters.consumedFrames;
|
||||
return true;
|
||||
}
|
||||
|
||||
frame = InputFrame();
|
||||
++mCounters.consumeMisses;
|
||||
return false;
|
||||
}
|
||||
|
||||
bool InputFrameMailbox::Release(const InputFrame& frame)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mMutex);
|
||||
if (!IsValidLocked(frame))
|
||||
return false;
|
||||
|
||||
Slot& slot = mSlots[frame.index];
|
||||
if (slot.state != InputFrameSlotState::Reading)
|
||||
return false;
|
||||
|
||||
slot.state = InputFrameSlotState::Free;
|
||||
slot.frameIndex = 0;
|
||||
++slot.generation;
|
||||
return true;
|
||||
}
|
||||
|
||||
void InputFrameMailbox::Clear()
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mMutex);
|
||||
mReadyIndices.clear();
|
||||
for (Slot& slot : mSlots)
|
||||
{
|
||||
slot.state = InputFrameSlotState::Free;
|
||||
slot.frameIndex = 0;
|
||||
++slot.generation;
|
||||
}
|
||||
}
|
||||
|
||||
InputFrameMailboxMetrics InputFrameMailbox::Metrics() const
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mMutex);
|
||||
InputFrameMailboxMetrics metrics = mCounters;
|
||||
metrics.capacity = mSlots.size();
|
||||
if (metrics.hasSubmittedFrame)
|
||||
{
|
||||
metrics.latestFrameAgeMilliseconds = std::chrono::duration_cast<std::chrono::duration<double, std::milli>>(
|
||||
std::chrono::steady_clock::now() - mLatestSubmitTime).count();
|
||||
}
|
||||
|
||||
for (const Slot& slot : mSlots)
|
||||
{
|
||||
switch (slot.state)
|
||||
{
|
||||
case InputFrameSlotState::Free:
|
||||
++metrics.freeCount;
|
||||
break;
|
||||
case InputFrameSlotState::Ready:
|
||||
++metrics.readyCount;
|
||||
break;
|
||||
case InputFrameSlotState::Reading:
|
||||
++metrics.readingCount;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return metrics;
|
||||
}
|
||||
|
||||
bool InputFrameMailbox::IsValidLocked(const InputFrame& frame) const
|
||||
{
|
||||
return frame.index < mSlots.size() && mSlots[frame.index].generation == frame.generation;
|
||||
}
|
||||
|
||||
void InputFrameMailbox::FillFrameLocked(std::size_t index, InputFrame& frame) const
|
||||
{
|
||||
const Slot& slot = mSlots[index];
|
||||
frame.bytes = slot.bytes.empty() ? nullptr : slot.bytes.data();
|
||||
frame.rowBytes = static_cast<long>(mConfig.rowBytes);
|
||||
frame.width = mConfig.width;
|
||||
frame.height = mConfig.height;
|
||||
frame.pixelFormat = mConfig.pixelFormat;
|
||||
frame.index = index;
|
||||
frame.generation = slot.generation;
|
||||
frame.frameIndex = slot.frameIndex;
|
||||
}
|
||||
|
||||
bool InputFrameMailbox::DropOldestReadyLocked()
|
||||
{
|
||||
while (!mReadyIndices.empty())
|
||||
{
|
||||
const std::size_t index = mReadyIndices.front();
|
||||
mReadyIndices.pop_front();
|
||||
if (index >= mSlots.size() || mSlots[index].state != InputFrameSlotState::Ready)
|
||||
continue;
|
||||
|
||||
mSlots[index].state = InputFrameSlotState::Free;
|
||||
mSlots[index].frameIndex = 0;
|
||||
++mSlots[index].generation;
|
||||
++mCounters.droppedReadyFrames;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
void InputFrameMailbox::TrimReadyFramesLocked()
|
||||
{
|
||||
if (mConfig.maxReadyFrames == 0)
|
||||
return;
|
||||
while (mReadyIndices.size() > mConfig.maxReadyFrames)
|
||||
DropOldestReadyLocked();
|
||||
}
|
||||
|
||||
std::size_t InputFrameMailbox::FrameByteCount() const
|
||||
{
|
||||
return static_cast<std::size_t>(mConfig.rowBytes) * static_cast<std::size_t>(mConfig.height);
|
||||
}
|
||||
93
apps/RenderCadenceCompositor/frames/InputFrameMailbox.h
Normal file
93
apps/RenderCadenceCompositor/frames/InputFrameMailbox.h
Normal file
@@ -0,0 +1,93 @@
|
||||
#pragma once
|
||||
|
||||
#include "VideoIOFormat.h"
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <chrono>
|
||||
#include <deque>
|
||||
#include <mutex>
|
||||
#include <vector>
|
||||
|
||||
enum class InputFrameSlotState
|
||||
{
|
||||
Free,
|
||||
Ready,
|
||||
Reading
|
||||
};
|
||||
|
||||
struct InputFrameMailboxConfig
|
||||
{
|
||||
unsigned width = 0;
|
||||
unsigned height = 0;
|
||||
VideoIOPixelFormat pixelFormat = VideoIOPixelFormat::Bgra8;
|
||||
unsigned rowBytes = 0;
|
||||
std::size_t capacity = 0;
|
||||
std::size_t maxReadyFrames = 0;
|
||||
};
|
||||
|
||||
struct InputFrame
|
||||
{
|
||||
const void* bytes = nullptr;
|
||||
long rowBytes = 0;
|
||||
unsigned width = 0;
|
||||
unsigned height = 0;
|
||||
VideoIOPixelFormat pixelFormat = VideoIOPixelFormat::Bgra8;
|
||||
std::size_t index = 0;
|
||||
uint64_t generation = 0;
|
||||
uint64_t frameIndex = 0;
|
||||
};
|
||||
|
||||
struct InputFrameMailboxMetrics
|
||||
{
|
||||
std::size_t capacity = 0;
|
||||
std::size_t freeCount = 0;
|
||||
std::size_t readyCount = 0;
|
||||
std::size_t readingCount = 0;
|
||||
uint64_t submittedFrames = 0;
|
||||
uint64_t consumedFrames = 0;
|
||||
uint64_t droppedReadyFrames = 0;
|
||||
uint64_t submitMisses = 0;
|
||||
uint64_t consumeMisses = 0;
|
||||
uint64_t latestFrameIndex = 0;
|
||||
bool hasSubmittedFrame = false;
|
||||
double latestFrameAgeMilliseconds = 0.0;
|
||||
};
|
||||
|
||||
class InputFrameMailbox
|
||||
{
|
||||
public:
|
||||
InputFrameMailbox() = default;
|
||||
explicit InputFrameMailbox(const InputFrameMailboxConfig& config);
|
||||
|
||||
void Configure(const InputFrameMailboxConfig& config);
|
||||
InputFrameMailboxConfig Config() const;
|
||||
|
||||
bool SubmitFrame(const void* bytes, unsigned rowBytes, uint64_t frameIndex);
|
||||
bool TryAcquireOldest(InputFrame& frame);
|
||||
bool Release(const InputFrame& frame);
|
||||
void Clear();
|
||||
InputFrameMailboxMetrics Metrics() const;
|
||||
|
||||
private:
|
||||
struct Slot
|
||||
{
|
||||
std::vector<unsigned char> bytes;
|
||||
InputFrameSlotState state = InputFrameSlotState::Free;
|
||||
uint64_t frameIndex = 0;
|
||||
uint64_t generation = 0;
|
||||
};
|
||||
|
||||
bool IsValidLocked(const InputFrame& frame) const;
|
||||
void FillFrameLocked(std::size_t index, InputFrame& frame) const;
|
||||
bool DropOldestReadyLocked();
|
||||
void TrimReadyFramesLocked();
|
||||
std::size_t FrameByteCount() const;
|
||||
|
||||
mutable std::mutex mMutex;
|
||||
InputFrameMailboxConfig mConfig;
|
||||
std::vector<Slot> mSlots;
|
||||
std::deque<std::size_t> mReadyIndices;
|
||||
InputFrameMailboxMetrics mCounters;
|
||||
std::chrono::steady_clock::time_point mLatestSubmitTime;
|
||||
};
|
||||
@@ -47,12 +47,9 @@ bool SystemFrameExchange::AcquireForRender(SystemFrame& frame)
|
||||
std::lock_guard<std::mutex> lock(mMutex);
|
||||
if (!AcquireFreeLocked(frame))
|
||||
{
|
||||
if (!DropOldestCompletedLocked() || !AcquireFreeLocked(frame))
|
||||
{
|
||||
frame = SystemFrame();
|
||||
++mCounters.acquireMisses;
|
||||
return false;
|
||||
}
|
||||
frame = SystemFrame();
|
||||
++mCounters.acquireMisses;
|
||||
return false;
|
||||
}
|
||||
|
||||
++mCounters.acquiredFrames;
|
||||
@@ -72,6 +69,7 @@ bool SystemFrameExchange::PublishCompleted(const SystemFrame& frame)
|
||||
slot.state = SystemFrameSlotState::Completed;
|
||||
slot.frameIndex = frame.frameIndex;
|
||||
mCompletedIndices.push_back(frame.index);
|
||||
TrimCompletedLocked();
|
||||
++mCounters.completedFrames;
|
||||
mCondition.notify_all();
|
||||
return true;
|
||||
@@ -130,6 +128,51 @@ bool SystemFrameExchange::WaitForCompletedDepth(std::size_t targetDepth, std::ch
|
||||
});
|
||||
}
|
||||
|
||||
bool SystemFrameExchange::WaitForStableCompletedDepth(
|
||||
std::size_t targetDepth,
|
||||
std::chrono::milliseconds stableDuration,
|
||||
std::chrono::milliseconds timeout)
|
||||
{
|
||||
if (targetDepth == 0)
|
||||
return true;
|
||||
|
||||
const auto deadline = std::chrono::steady_clock::now() + timeout;
|
||||
std::unique_lock<std::mutex> lock(mMutex);
|
||||
bool stableWindowStarted = false;
|
||||
std::chrono::steady_clock::time_point stableSince;
|
||||
|
||||
while (true)
|
||||
{
|
||||
const auto now = std::chrono::steady_clock::now();
|
||||
if (now >= deadline)
|
||||
return false;
|
||||
|
||||
if (CompletedCountLocked() >= targetDepth)
|
||||
{
|
||||
if (stableDuration <= std::chrono::milliseconds::zero())
|
||||
return true;
|
||||
|
||||
if (!stableWindowStarted)
|
||||
{
|
||||
stableSince = now;
|
||||
stableWindowStarted = true;
|
||||
}
|
||||
|
||||
const auto stableDeadline = stableSince + stableDuration;
|
||||
if (now >= stableDeadline)
|
||||
return true;
|
||||
|
||||
mCondition.wait_until(lock, stableDeadline < deadline ? stableDeadline : deadline);
|
||||
continue;
|
||||
}
|
||||
|
||||
stableWindowStarted = false;
|
||||
mCondition.wait_until(lock, deadline, [&]() {
|
||||
return CompletedCountLocked() >= targetDepth;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void SystemFrameExchange::Clear()
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mMutex);
|
||||
@@ -210,6 +253,17 @@ bool SystemFrameExchange::DropOldestCompletedLocked()
|
||||
return false;
|
||||
}
|
||||
|
||||
void SystemFrameExchange::TrimCompletedLocked()
|
||||
{
|
||||
if (mConfig.maxCompletedFrames == 0)
|
||||
return;
|
||||
while (CompletedCountLocked() > mConfig.maxCompletedFrames)
|
||||
{
|
||||
if (!DropOldestCompletedLocked())
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
bool SystemFrameExchange::IsValidLocked(const SystemFrame& frame) const
|
||||
{
|
||||
return frame.index < mSlots.size() && mSlots[frame.index].generation == frame.generation;
|
||||
|
||||
@@ -22,6 +22,10 @@ public:
|
||||
bool ConsumeCompletedForSchedule(SystemFrame& frame);
|
||||
bool ReleaseScheduledByBytes(void* bytes);
|
||||
bool WaitForCompletedDepth(std::size_t targetDepth, std::chrono::milliseconds timeout);
|
||||
bool WaitForStableCompletedDepth(
|
||||
std::size_t targetDepth,
|
||||
std::chrono::milliseconds stableDuration,
|
||||
std::chrono::milliseconds timeout);
|
||||
void Clear();
|
||||
|
||||
SystemFrameExchangeMetrics Metrics() const;
|
||||
@@ -37,6 +41,7 @@ private:
|
||||
|
||||
bool AcquireFreeLocked(SystemFrame& frame);
|
||||
bool DropOldestCompletedLocked();
|
||||
void TrimCompletedLocked();
|
||||
bool IsValidLocked(const SystemFrame& frame) const;
|
||||
void FillFrameLocked(std::size_t index, SystemFrame& frame);
|
||||
std::size_t CompletedCountLocked() const;
|
||||
|
||||
@@ -20,6 +20,7 @@ struct SystemFrameExchangeConfig
|
||||
VideoIOPixelFormat pixelFormat = VideoIOPixelFormat::Bgra8;
|
||||
unsigned rowBytes = 0;
|
||||
std::size_t capacity = 0;
|
||||
std::size_t maxCompletedFrames = 0;
|
||||
};
|
||||
|
||||
struct SystemFrame
|
||||
|
||||
341
apps/RenderCadenceCompositor/render/InputFrameTexture.cpp
Normal file
341
apps/RenderCadenceCompositor/render/InputFrameTexture.cpp
Normal file
@@ -0,0 +1,341 @@
|
||||
#include "InputFrameTexture.h"
|
||||
|
||||
#include <chrono>
|
||||
|
||||
#ifndef GL_FRAMEBUFFER_BINDING
|
||||
#define GL_FRAMEBUFFER_BINDING 0x8CA6
|
||||
#endif
|
||||
|
||||
namespace
|
||||
{
|
||||
constexpr GLuint kUyvyTextureUnit = 0;
|
||||
|
||||
const char* kDecodeVertexShader = R"GLSL(
|
||||
#version 430 core
|
||||
out vec2 vTexCoord;
|
||||
void main()
|
||||
{
|
||||
vec2 positions[3] = vec2[3](
|
||||
vec2(-1.0, -1.0),
|
||||
vec2( 3.0, -1.0),
|
||||
vec2(-1.0, 3.0));
|
||||
vec2 texCoords[3] = vec2[3](
|
||||
vec2(0.0, 0.0),
|
||||
vec2(2.0, 0.0),
|
||||
vec2(0.0, 2.0));
|
||||
gl_Position = vec4(positions[gl_VertexID], 0.0, 1.0);
|
||||
vTexCoord = texCoords[gl_VertexID];
|
||||
}
|
||||
)GLSL";
|
||||
|
||||
const char* kUyvyDecodeFragmentShader = R"GLSL(
|
||||
#version 430 core
|
||||
layout(binding = 0) uniform sampler2D uPackedUyvy;
|
||||
uniform vec2 uDecodedSize;
|
||||
in vec2 vTexCoord;
|
||||
out vec4 fragColor;
|
||||
|
||||
vec4 rec709YCbCr2rgba(float yByte, float cbByte, float crByte)
|
||||
{
|
||||
float y = (yByte - 16.0) / 219.0;
|
||||
float cb = (cbByte - 16.0) / 224.0 - 0.5;
|
||||
float cr = (crByte - 16.0) / 224.0 - 0.5;
|
||||
return vec4(
|
||||
y + 1.5748 * cr,
|
||||
y - 0.1873 * cb - 0.4681 * cr,
|
||||
y + 1.8556 * cb,
|
||||
1.0);
|
||||
}
|
||||
|
||||
void main()
|
||||
{
|
||||
ivec2 decodedSize = ivec2(uDecodedSize);
|
||||
ivec2 outputCoord = ivec2(clamp(gl_FragCoord.xy, vec2(0.0), vec2(decodedSize - ivec2(1))));
|
||||
int sourceY = decodedSize.y - 1 - outputCoord.y;
|
||||
ivec2 packedCoord = ivec2(clamp(outputCoord.x / 2, 0, max(decodedSize.x / 2 - 1, 0)), sourceY);
|
||||
vec4 macroPixel = texelFetch(uPackedUyvy, packedCoord, 0) * 255.0;
|
||||
float ySample = (outputCoord.x & 1) != 0 ? macroPixel.a : macroPixel.g;
|
||||
fragColor = clamp(rec709YCbCr2rgba(ySample, macroPixel.r, macroPixel.b), vec4(0.0), vec4(1.0));
|
||||
}
|
||||
)GLSL";
|
||||
}
|
||||
|
||||
InputFrameTexture::~InputFrameTexture()
|
||||
{
|
||||
ShutdownGl();
|
||||
}
|
||||
|
||||
GLuint InputFrameTexture::PollAndUpload(InputFrameMailbox* mailbox)
|
||||
{
|
||||
if (mailbox == nullptr)
|
||||
return mTexture;
|
||||
|
||||
InputFrame frame;
|
||||
if (!mailbox->TryAcquireOldest(frame))
|
||||
{
|
||||
++mUploadMisses;
|
||||
mLastUploadMilliseconds = 0.0;
|
||||
return mTexture;
|
||||
}
|
||||
|
||||
if (frame.bytes != nullptr && frame.pixelFormat == VideoIOPixelFormat::Bgra8 && EnsureTexture(frame))
|
||||
{
|
||||
mLastFrameFormatSupported = true;
|
||||
const auto uploadStart = std::chrono::steady_clock::now();
|
||||
UploadBgra8FrameFlippedVertically(frame);
|
||||
const auto uploadEnd = std::chrono::steady_clock::now();
|
||||
mLastUploadMilliseconds = std::chrono::duration_cast<std::chrono::duration<double, std::milli>>(uploadEnd - uploadStart).count();
|
||||
++mUploadedFrames;
|
||||
}
|
||||
else if (frame.bytes != nullptr && frame.pixelFormat == VideoIOPixelFormat::Uyvy8 && EnsureTexture(frame) && EnsureRawUyvyTexture(frame) && EnsureDecodeProgram())
|
||||
{
|
||||
mLastFrameFormatSupported = true;
|
||||
const auto uploadStart = std::chrono::steady_clock::now();
|
||||
UploadUyvy8Frame(frame);
|
||||
DecodeUyvy8Frame(frame);
|
||||
const auto uploadEnd = std::chrono::steady_clock::now();
|
||||
mLastUploadMilliseconds = std::chrono::duration_cast<std::chrono::duration<double, std::milli>>(uploadEnd - uploadStart).count();
|
||||
++mUploadedFrames;
|
||||
}
|
||||
else
|
||||
{
|
||||
mLastFrameFormatSupported = frame.pixelFormat == VideoIOPixelFormat::Bgra8 || frame.pixelFormat == VideoIOPixelFormat::Uyvy8;
|
||||
mLastUploadMilliseconds = 0.0;
|
||||
}
|
||||
|
||||
mailbox->Release(frame);
|
||||
return mTexture;
|
||||
}
|
||||
|
||||
void InputFrameTexture::ShutdownGl()
|
||||
{
|
||||
if (mTexture != 0)
|
||||
glDeleteTextures(1, &mTexture);
|
||||
if (mRawTexture != 0)
|
||||
glDeleteTextures(1, &mRawTexture);
|
||||
mTexture = 0;
|
||||
mRawTexture = 0;
|
||||
mWidth = 0;
|
||||
mHeight = 0;
|
||||
mRawWidth = 0;
|
||||
mRawHeight = 0;
|
||||
DestroyDecodeResources();
|
||||
}
|
||||
|
||||
bool InputFrameTexture::EnsureTexture(const InputFrame& frame)
|
||||
{
|
||||
if (frame.width == 0 || frame.height == 0)
|
||||
return false;
|
||||
|
||||
if (mTexture != 0 && mWidth == frame.width && mHeight == frame.height)
|
||||
return true;
|
||||
|
||||
ShutdownGl();
|
||||
glGenTextures(1, &mTexture);
|
||||
glBindTexture(GL_TEXTURE_2D, mTexture);
|
||||
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>(frame.width),
|
||||
static_cast<GLsizei>(frame.height),
|
||||
0,
|
||||
GL_BGRA,
|
||||
GL_UNSIGNED_INT_8_8_8_8_REV,
|
||||
nullptr);
|
||||
glBindTexture(GL_TEXTURE_2D, 0);
|
||||
|
||||
mWidth = frame.width;
|
||||
mHeight = frame.height;
|
||||
return mTexture != 0;
|
||||
}
|
||||
|
||||
bool InputFrameTexture::EnsureRawUyvyTexture(const InputFrame& frame)
|
||||
{
|
||||
if (frame.width == 0 || frame.height == 0)
|
||||
return false;
|
||||
|
||||
const unsigned rawWidth = (frame.width + 1u) / 2u;
|
||||
if (mRawTexture != 0 && mRawWidth == rawWidth && mRawHeight == frame.height)
|
||||
return true;
|
||||
|
||||
if (mRawTexture != 0)
|
||||
glDeleteTextures(1, &mRawTexture);
|
||||
mRawTexture = 0;
|
||||
glGenTextures(1, &mRawTexture);
|
||||
glBindTexture(GL_TEXTURE_2D, mRawTexture);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
|
||||
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>(rawWidth),
|
||||
static_cast<GLsizei>(frame.height),
|
||||
0,
|
||||
GL_RGBA,
|
||||
GL_UNSIGNED_BYTE,
|
||||
nullptr);
|
||||
glBindTexture(GL_TEXTURE_2D, 0);
|
||||
|
||||
mRawWidth = rawWidth;
|
||||
mRawHeight = frame.height;
|
||||
return mRawTexture != 0;
|
||||
}
|
||||
|
||||
void InputFrameTexture::UploadBgra8FrameFlippedVertically(const InputFrame& frame)
|
||||
{
|
||||
glBindTexture(GL_TEXTURE_2D, mTexture);
|
||||
glPixelStorei(GL_UNPACK_ALIGNMENT, 4);
|
||||
glPixelStorei(GL_UNPACK_ROW_LENGTH, frame.rowBytes > 0 ? static_cast<GLint>(frame.rowBytes / 4) : 0);
|
||||
|
||||
const unsigned char* sourceBytes = static_cast<const unsigned char*>(frame.bytes);
|
||||
for (unsigned destinationY = 0; destinationY < frame.height; ++destinationY)
|
||||
{
|
||||
const unsigned sourceY = frame.height - 1u - destinationY;
|
||||
const unsigned char* sourceRow = sourceBytes + static_cast<std::size_t>(sourceY) * static_cast<std::size_t>(frame.rowBytes);
|
||||
glTexSubImage2D(
|
||||
GL_TEXTURE_2D,
|
||||
0,
|
||||
0,
|
||||
static_cast<GLint>(destinationY),
|
||||
static_cast<GLsizei>(frame.width),
|
||||
1,
|
||||
GL_BGRA,
|
||||
GL_UNSIGNED_INT_8_8_8_8_REV,
|
||||
sourceRow);
|
||||
}
|
||||
|
||||
glPixelStorei(GL_UNPACK_ROW_LENGTH, 0);
|
||||
glBindTexture(GL_TEXTURE_2D, 0);
|
||||
}
|
||||
|
||||
void InputFrameTexture::UploadUyvy8Frame(const InputFrame& frame)
|
||||
{
|
||||
glBindTexture(GL_TEXTURE_2D, mRawTexture);
|
||||
glPixelStorei(GL_UNPACK_ALIGNMENT, 4);
|
||||
glPixelStorei(GL_UNPACK_ROW_LENGTH, frame.rowBytes > 0 ? static_cast<GLint>(frame.rowBytes / 4) : 0);
|
||||
glTexSubImage2D(
|
||||
GL_TEXTURE_2D,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
static_cast<GLsizei>((frame.width + 1u) / 2u),
|
||||
static_cast<GLsizei>(frame.height),
|
||||
GL_RGBA,
|
||||
GL_UNSIGNED_BYTE,
|
||||
frame.bytes);
|
||||
glPixelStorei(GL_UNPACK_ROW_LENGTH, 0);
|
||||
glBindTexture(GL_TEXTURE_2D, 0);
|
||||
}
|
||||
|
||||
void InputFrameTexture::DecodeUyvy8Frame(const InputFrame& frame)
|
||||
{
|
||||
GLint previousFramebuffer = 0;
|
||||
glGetIntegerv(GL_FRAMEBUFFER_BINDING, &previousFramebuffer);
|
||||
|
||||
if (mDecodeFramebuffer == 0)
|
||||
glGenFramebuffers(1, &mDecodeFramebuffer);
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, mDecodeFramebuffer);
|
||||
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, mTexture, 0);
|
||||
if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE)
|
||||
{
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, static_cast<GLuint>(previousFramebuffer));
|
||||
return;
|
||||
}
|
||||
|
||||
glViewport(0, 0, static_cast<GLsizei>(frame.width), static_cast<GLsizei>(frame.height));
|
||||
glDisable(GL_SCISSOR_TEST);
|
||||
glDisable(GL_DEPTH_TEST);
|
||||
glDisable(GL_BLEND);
|
||||
glUseProgram(mDecodeProgram);
|
||||
const GLint decodedSizeLocation = glGetUniformLocation(mDecodeProgram, "uDecodedSize");
|
||||
if (decodedSizeLocation >= 0)
|
||||
glUniform2f(decodedSizeLocation, static_cast<GLfloat>(frame.width), static_cast<GLfloat>(frame.height));
|
||||
glActiveTexture(GL_TEXTURE0 + kUyvyTextureUnit);
|
||||
glBindTexture(GL_TEXTURE_2D, mRawTexture);
|
||||
glBindVertexArray(mDecodeVertexArray);
|
||||
glDrawArrays(GL_TRIANGLES, 0, 3);
|
||||
glBindVertexArray(0);
|
||||
glBindTexture(GL_TEXTURE_2D, 0);
|
||||
glActiveTexture(GL_TEXTURE0);
|
||||
glUseProgram(0);
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, static_cast<GLuint>(previousFramebuffer));
|
||||
}
|
||||
|
||||
bool InputFrameTexture::EnsureDecodeProgram()
|
||||
{
|
||||
if (mDecodeProgram != 0)
|
||||
return true;
|
||||
|
||||
if (!CompileShader(GL_VERTEX_SHADER, kDecodeVertexShader, mDecodeVertexShader))
|
||||
return false;
|
||||
if (!CompileShader(GL_FRAGMENT_SHADER, kUyvyDecodeFragmentShader, mDecodeFragmentShader))
|
||||
return false;
|
||||
if (!LinkProgram(mDecodeVertexShader, mDecodeFragmentShader, mDecodeProgram))
|
||||
return false;
|
||||
|
||||
glUseProgram(mDecodeProgram);
|
||||
const GLint samplerLocation = glGetUniformLocation(mDecodeProgram, "uPackedUyvy");
|
||||
if (samplerLocation >= 0)
|
||||
glUniform1i(samplerLocation, static_cast<GLint>(kUyvyTextureUnit));
|
||||
glUseProgram(0);
|
||||
|
||||
if (mDecodeVertexArray == 0)
|
||||
glGenVertexArrays(1, &mDecodeVertexArray);
|
||||
return mDecodeProgram != 0 && mDecodeVertexArray != 0;
|
||||
}
|
||||
|
||||
void InputFrameTexture::DestroyDecodeResources()
|
||||
{
|
||||
if (mDecodeFramebuffer != 0)
|
||||
glDeleteFramebuffers(1, &mDecodeFramebuffer);
|
||||
if (mDecodeVertexArray != 0)
|
||||
glDeleteVertexArrays(1, &mDecodeVertexArray);
|
||||
if (mDecodeProgram != 0)
|
||||
glDeleteProgram(mDecodeProgram);
|
||||
if (mDecodeVertexShader != 0)
|
||||
glDeleteShader(mDecodeVertexShader);
|
||||
if (mDecodeFragmentShader != 0)
|
||||
glDeleteShader(mDecodeFragmentShader);
|
||||
mDecodeFramebuffer = 0;
|
||||
mDecodeVertexArray = 0;
|
||||
mDecodeProgram = 0;
|
||||
mDecodeVertexShader = 0;
|
||||
mDecodeFragmentShader = 0;
|
||||
}
|
||||
|
||||
bool InputFrameTexture::CompileShader(GLenum shaderType, const char* source, GLuint& shader)
|
||||
{
|
||||
shader = glCreateShader(shaderType);
|
||||
glShaderSource(shader, 1, &source, nullptr);
|
||||
glCompileShader(shader);
|
||||
GLint compileResult = GL_FALSE;
|
||||
glGetShaderiv(shader, GL_COMPILE_STATUS, &compileResult);
|
||||
if (compileResult != GL_FALSE)
|
||||
return true;
|
||||
glDeleteShader(shader);
|
||||
shader = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
bool InputFrameTexture::LinkProgram(GLuint vertexShader, GLuint fragmentShader, GLuint& program)
|
||||
{
|
||||
program = glCreateProgram();
|
||||
glAttachShader(program, vertexShader);
|
||||
glAttachShader(program, fragmentShader);
|
||||
glLinkProgram(program);
|
||||
GLint linkResult = GL_FALSE;
|
||||
glGetProgramiv(program, GL_LINK_STATUS, &linkResult);
|
||||
if (linkResult != GL_FALSE)
|
||||
return true;
|
||||
glDeleteProgram(program);
|
||||
program = 0;
|
||||
return false;
|
||||
}
|
||||
50
apps/RenderCadenceCompositor/render/InputFrameTexture.h
Normal file
50
apps/RenderCadenceCompositor/render/InputFrameTexture.h
Normal file
@@ -0,0 +1,50 @@
|
||||
#pragma once
|
||||
|
||||
#include "../frames/InputFrameMailbox.h"
|
||||
#include "GLExtensions.h"
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
class InputFrameTexture
|
||||
{
|
||||
public:
|
||||
InputFrameTexture() = default;
|
||||
InputFrameTexture(const InputFrameTexture&) = delete;
|
||||
InputFrameTexture& operator=(const InputFrameTexture&) = delete;
|
||||
~InputFrameTexture();
|
||||
|
||||
GLuint PollAndUpload(InputFrameMailbox* mailbox);
|
||||
GLuint Texture() const { return mTexture; }
|
||||
uint64_t UploadedFrames() const { return mUploadedFrames; }
|
||||
uint64_t UploadMisses() const { return mUploadMisses; }
|
||||
double LastUploadMilliseconds() const { return mLastUploadMilliseconds; }
|
||||
bool LastFrameFormatSupported() const { return mLastFrameFormatSupported; }
|
||||
void ShutdownGl();
|
||||
|
||||
private:
|
||||
bool EnsureTexture(const InputFrame& frame);
|
||||
bool EnsureRawUyvyTexture(const InputFrame& frame);
|
||||
bool EnsureDecodeProgram();
|
||||
void UploadBgra8FrameFlippedVertically(const InputFrame& frame);
|
||||
void UploadUyvy8Frame(const InputFrame& frame);
|
||||
void DecodeUyvy8Frame(const InputFrame& frame);
|
||||
void DestroyDecodeResources();
|
||||
static bool CompileShader(GLenum shaderType, const char* source, GLuint& shader);
|
||||
static bool LinkProgram(GLuint vertexShader, GLuint fragmentShader, GLuint& program);
|
||||
|
||||
GLuint mTexture = 0;
|
||||
GLuint mRawTexture = 0;
|
||||
GLuint mDecodeFramebuffer = 0;
|
||||
GLuint mDecodeVertexArray = 0;
|
||||
GLuint mDecodeProgram = 0;
|
||||
GLuint mDecodeVertexShader = 0;
|
||||
GLuint mDecodeFragmentShader = 0;
|
||||
unsigned mWidth = 0;
|
||||
unsigned mHeight = 0;
|
||||
unsigned mRawWidth = 0;
|
||||
unsigned mRawHeight = 0;
|
||||
uint64_t mUploadedFrames = 0;
|
||||
uint64_t mUploadMisses = 0;
|
||||
double mLastUploadMilliseconds = 0.0;
|
||||
bool mLastFrameFormatSupported = true;
|
||||
};
|
||||
@@ -13,6 +13,7 @@ RenderCadenceClock::RenderCadenceClock(double frameDurationMilliseconds)
|
||||
void RenderCadenceClock::Reset(TimePoint now)
|
||||
{
|
||||
mNextRenderTime = now;
|
||||
mPendingFrameAdvance = 1;
|
||||
mOverrunCount = 0;
|
||||
mSkippedFrameCount = 0;
|
||||
}
|
||||
@@ -27,10 +28,12 @@ RenderCadenceClock::Tick RenderCadenceClock::Poll(TimePoint now)
|
||||
}
|
||||
|
||||
tick.due = true;
|
||||
mPendingFrameAdvance = 1;
|
||||
const Duration lateBy = now - mNextRenderTime;
|
||||
if (lateBy > mFrameDuration)
|
||||
{
|
||||
tick.skippedFrames = static_cast<uint64_t>(lateBy / mFrameDuration);
|
||||
mPendingFrameAdvance += tick.skippedFrames;
|
||||
++mOverrunCount;
|
||||
mSkippedFrameCount += tick.skippedFrames;
|
||||
}
|
||||
@@ -39,7 +42,8 @@ RenderCadenceClock::Tick RenderCadenceClock::Poll(TimePoint now)
|
||||
|
||||
void RenderCadenceClock::MarkRendered(TimePoint now)
|
||||
{
|
||||
mNextRenderTime += mFrameDuration;
|
||||
mNextRenderTime += mFrameDuration * mPendingFrameAdvance;
|
||||
mPendingFrameAdvance = 1;
|
||||
if (now - mNextRenderTime > mFrameDuration * 4)
|
||||
mNextRenderTime = now + mFrameDuration;
|
||||
}
|
||||
|
||||
@@ -31,6 +31,7 @@ public:
|
||||
private:
|
||||
Duration mFrameDuration;
|
||||
TimePoint mNextRenderTime = Clock::now();
|
||||
uint64_t mPendingFrameAdvance = 1;
|
||||
uint64_t mOverrunCount = 0;
|
||||
uint64_t mSkippedFrameCount = 0;
|
||||
};
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
#include "RenderThread.h"
|
||||
|
||||
#include "../frames/InputFrameMailbox.h"
|
||||
#include "../frames/SystemFrameExchange.h"
|
||||
#include "../frames/SystemFrameTypes.h"
|
||||
#include "../logging/Logger.h"
|
||||
#include "../platform/HiddenGlWindow.h"
|
||||
#include "InputFrameTexture.h"
|
||||
#include "readback/Bgra8ReadbackPipeline.h"
|
||||
#include "GLExtensions.h"
|
||||
#include "runtime/RuntimeRenderScene.h"
|
||||
@@ -20,6 +22,13 @@ RenderThread::RenderThread(SystemFrameExchange& frameExchange, Config config) :
|
||||
{
|
||||
}
|
||||
|
||||
RenderThread::RenderThread(SystemFrameExchange& frameExchange, InputFrameMailbox* inputMailbox, Config config) :
|
||||
mFrameExchange(frameExchange),
|
||||
mInputMailbox(inputMailbox),
|
||||
mConfig(config)
|
||||
{
|
||||
}
|
||||
|
||||
RenderThread::~RenderThread()
|
||||
{
|
||||
Stop();
|
||||
@@ -76,6 +85,21 @@ RenderThread::Metrics RenderThread::GetMetrics() const
|
||||
metrics.skippedFrames = mSkippedFrames.load(std::memory_order_relaxed);
|
||||
metrics.shaderBuildsCommitted = mShaderBuildsCommitted.load(std::memory_order_relaxed);
|
||||
metrics.shaderBuildFailures = mShaderBuildFailures.load(std::memory_order_relaxed);
|
||||
metrics.renderFrameMilliseconds = mRenderFrameMilliseconds.load(std::memory_order_relaxed);
|
||||
metrics.renderFrameBudgetUsedPercent = mRenderFrameBudgetUsedPercent.load(std::memory_order_relaxed);
|
||||
metrics.renderFrameMaxMilliseconds = mRenderFrameMaxMilliseconds.load(std::memory_order_relaxed);
|
||||
metrics.readbackQueueMilliseconds = mReadbackQueueMilliseconds.load(std::memory_order_relaxed);
|
||||
metrics.completedReadbackCopyMilliseconds = mCompletedReadbackCopyMilliseconds.load(std::memory_order_relaxed);
|
||||
metrics.inputFramesReceived = mInputFramesReceived.load(std::memory_order_relaxed);
|
||||
metrics.inputFramesDropped = mInputFramesDropped.load(std::memory_order_relaxed);
|
||||
metrics.inputConsumeMisses = mInputConsumeMisses.load(std::memory_order_relaxed);
|
||||
metrics.inputUploadMisses = mInputUploadMisses.load(std::memory_order_relaxed);
|
||||
metrics.inputReadyFrames = mInputReadyFrames.load(std::memory_order_relaxed);
|
||||
metrics.inputReadingFrames = mInputReadingFrames.load(std::memory_order_relaxed);
|
||||
metrics.inputLatestAgeMilliseconds = mInputLatestAgeMilliseconds.load(std::memory_order_relaxed);
|
||||
metrics.inputUploadMilliseconds = mInputUploadMilliseconds.load(std::memory_order_relaxed);
|
||||
metrics.inputFormatSupported = mInputFormatSupported.load(std::memory_order_relaxed);
|
||||
metrics.inputSignalPresent = mInputSignalPresent.load(std::memory_order_relaxed);
|
||||
return metrics;
|
||||
}
|
||||
|
||||
@@ -109,6 +133,7 @@ void RenderThread::ThreadMain()
|
||||
SimpleMotionRenderer renderer;
|
||||
RuntimeRenderScene runtimeRenderScene;
|
||||
Bgra8ReadbackPipeline readback;
|
||||
InputFrameTexture inputTexture;
|
||||
if (!runtimeRenderScene.StartPrepareWorker(std::move(prepareWindow), error))
|
||||
{
|
||||
SignalStartupFailure(error.empty() ? "Runtime shader prepare worker initialization failed." : error);
|
||||
@@ -134,6 +159,7 @@ void RenderThread::ThreadMain()
|
||||
CountAcquireMiss();
|
||||
},
|
||||
[this]() { CountCompleted(); });
|
||||
PublishReadbackMetrics(readback);
|
||||
|
||||
const auto now = RenderCadenceClock::Clock::now();
|
||||
const RenderCadenceClock::Tick tick = clock.Poll(now);
|
||||
@@ -145,15 +171,20 @@ void RenderThread::ThreadMain()
|
||||
}
|
||||
|
||||
TryCommitReadyRuntimeShader(runtimeRenderScene);
|
||||
if (!readback.RenderAndQueue(frameIndex, [this, &renderer, &runtimeRenderScene](uint64_t index) {
|
||||
const GLuint videoInputTexture = inputTexture.PollAndUpload(mInputMailbox);
|
||||
PublishInputMetrics(inputTexture);
|
||||
if (!readback.RenderAndQueue(frameIndex, [this, &renderer, &runtimeRenderScene, videoInputTexture](uint64_t index) {
|
||||
if (runtimeRenderScene.HasLayers())
|
||||
runtimeRenderScene.RenderFrame(index, mConfig.width, mConfig.height);
|
||||
runtimeRenderScene.RenderFrame(index, mConfig.width, mConfig.height, videoInputTexture);
|
||||
else if (videoInputTexture != 0)
|
||||
renderer.RenderTexture(videoInputTexture);
|
||||
else
|
||||
renderer.RenderFrame(index);
|
||||
}))
|
||||
{
|
||||
mPboQueueMisses.fetch_add(1, std::memory_order_relaxed);
|
||||
}
|
||||
PublishReadbackMetrics(readback);
|
||||
|
||||
CountRendered();
|
||||
++frameIndex;
|
||||
@@ -172,9 +203,11 @@ void RenderThread::ThreadMain()
|
||||
CountAcquireMiss();
|
||||
},
|
||||
[this]() { CountCompleted(); });
|
||||
PublishReadbackMetrics(readback);
|
||||
}
|
||||
|
||||
readback.Shutdown();
|
||||
inputTexture.ShutdownGl();
|
||||
runtimeRenderScene.ShutdownGl();
|
||||
renderer.ShutdownGl();
|
||||
window.ClearCurrent();
|
||||
@@ -212,6 +245,58 @@ void RenderThread::CountAcquireMiss()
|
||||
mAcquireMisses.fetch_add(1, std::memory_order_relaxed);
|
||||
}
|
||||
|
||||
void RenderThread::PublishReadbackMetrics(const Bgra8ReadbackPipeline& readback)
|
||||
{
|
||||
const double renderMilliseconds = readback.LastRenderFrameMilliseconds();
|
||||
mRenderFrameMilliseconds.store(renderMilliseconds, std::memory_order_relaxed);
|
||||
if (mConfig.frameDurationMilliseconds > 0.0)
|
||||
{
|
||||
mRenderFrameBudgetUsedPercent.store(
|
||||
(renderMilliseconds / mConfig.frameDurationMilliseconds) * 100.0,
|
||||
std::memory_order_relaxed);
|
||||
}
|
||||
else
|
||||
{
|
||||
mRenderFrameBudgetUsedPercent.store(0.0, std::memory_order_relaxed);
|
||||
}
|
||||
|
||||
const double previousMax = mRenderFrameMaxMilliseconds.load(std::memory_order_relaxed);
|
||||
if (renderMilliseconds > previousMax)
|
||||
mRenderFrameMaxMilliseconds.store(renderMilliseconds, std::memory_order_relaxed);
|
||||
|
||||
mReadbackQueueMilliseconds.store(readback.LastReadbackQueueMilliseconds(), std::memory_order_relaxed);
|
||||
mCompletedReadbackCopyMilliseconds.store(readback.LastCompletedReadbackCopyMilliseconds(), std::memory_order_relaxed);
|
||||
}
|
||||
|
||||
void RenderThread::PublishInputMetrics(const InputFrameTexture& inputTexture)
|
||||
{
|
||||
if (mInputMailbox != nullptr)
|
||||
{
|
||||
const InputFrameMailboxMetrics mailboxMetrics = mInputMailbox->Metrics();
|
||||
mInputFramesReceived.store(mailboxMetrics.submittedFrames, std::memory_order_relaxed);
|
||||
mInputFramesDropped.store(mailboxMetrics.droppedReadyFrames + mailboxMetrics.submitMisses, std::memory_order_relaxed);
|
||||
mInputConsumeMisses.store(mailboxMetrics.consumeMisses, std::memory_order_relaxed);
|
||||
mInputReadyFrames.store(mailboxMetrics.readyCount, std::memory_order_relaxed);
|
||||
mInputReadingFrames.store(mailboxMetrics.readingCount, std::memory_order_relaxed);
|
||||
mInputLatestAgeMilliseconds.store(mailboxMetrics.latestFrameAgeMilliseconds, std::memory_order_relaxed);
|
||||
mInputSignalPresent.store(mailboxMetrics.hasSubmittedFrame, std::memory_order_relaxed);
|
||||
}
|
||||
else
|
||||
{
|
||||
mInputFramesReceived.store(0, std::memory_order_relaxed);
|
||||
mInputFramesDropped.store(0, std::memory_order_relaxed);
|
||||
mInputConsumeMisses.store(0, std::memory_order_relaxed);
|
||||
mInputReadyFrames.store(0, std::memory_order_relaxed);
|
||||
mInputReadingFrames.store(0, std::memory_order_relaxed);
|
||||
mInputLatestAgeMilliseconds.store(0.0, std::memory_order_relaxed);
|
||||
mInputSignalPresent.store(false, std::memory_order_relaxed);
|
||||
}
|
||||
|
||||
mInputUploadMisses.store(inputTexture.UploadMisses(), std::memory_order_relaxed);
|
||||
mInputUploadMilliseconds.store(inputTexture.LastUploadMilliseconds(), std::memory_order_relaxed);
|
||||
mInputFormatSupported.store(inputTexture.LastFrameFormatSupported(), std::memory_order_relaxed);
|
||||
}
|
||||
|
||||
void RenderThread::SubmitRuntimeShaderArtifact(const RuntimeShaderArtifact& artifact)
|
||||
{
|
||||
if (artifact.fragmentShaderSource.empty())
|
||||
|
||||
@@ -14,6 +14,9 @@
|
||||
#include <thread>
|
||||
|
||||
class SystemFrameExchange;
|
||||
class InputFrameMailbox;
|
||||
class InputFrameTexture;
|
||||
class Bgra8ReadbackPipeline;
|
||||
|
||||
class RenderThread
|
||||
{
|
||||
@@ -36,9 +39,25 @@ public:
|
||||
uint64_t skippedFrames = 0;
|
||||
uint64_t shaderBuildsCommitted = 0;
|
||||
uint64_t shaderBuildFailures = 0;
|
||||
double renderFrameMilliseconds = 0.0;
|
||||
double renderFrameBudgetUsedPercent = 0.0;
|
||||
double renderFrameMaxMilliseconds = 0.0;
|
||||
double readbackQueueMilliseconds = 0.0;
|
||||
double completedReadbackCopyMilliseconds = 0.0;
|
||||
uint64_t inputFramesReceived = 0;
|
||||
uint64_t inputFramesDropped = 0;
|
||||
uint64_t inputConsumeMisses = 0;
|
||||
uint64_t inputUploadMisses = 0;
|
||||
std::size_t inputReadyFrames = 0;
|
||||
std::size_t inputReadingFrames = 0;
|
||||
double inputLatestAgeMilliseconds = 0.0;
|
||||
double inputUploadMilliseconds = 0.0;
|
||||
bool inputFormatSupported = true;
|
||||
bool inputSignalPresent = false;
|
||||
};
|
||||
|
||||
RenderThread(SystemFrameExchange& frameExchange, Config config);
|
||||
RenderThread(SystemFrameExchange& frameExchange, InputFrameMailbox* inputMailbox, Config config);
|
||||
RenderThread(const RenderThread&) = delete;
|
||||
RenderThread& operator=(const RenderThread&) = delete;
|
||||
~RenderThread();
|
||||
@@ -58,11 +77,14 @@ private:
|
||||
void CountRendered();
|
||||
void CountCompleted();
|
||||
void CountAcquireMiss();
|
||||
void PublishReadbackMetrics(const Bgra8ReadbackPipeline& readback);
|
||||
void PublishInputMetrics(const InputFrameTexture& inputTexture);
|
||||
void TryCommitReadyRuntimeShader(RuntimeRenderScene& runtimeRenderScene);
|
||||
bool TryTakePendingRuntimeShaderArtifact(RuntimeShaderArtifact& artifact);
|
||||
bool TryTakePendingRenderLayers(std::vector<RenderCadenceCompositor::RuntimeRenderLayerModel>& layers);
|
||||
|
||||
SystemFrameExchange& mFrameExchange;
|
||||
InputFrameMailbox* mInputMailbox = nullptr;
|
||||
Config mConfig;
|
||||
std::thread mThread;
|
||||
std::atomic<bool> mStopping{ false };
|
||||
@@ -81,6 +103,21 @@ private:
|
||||
std::atomic<uint64_t> mSkippedFrames{ 0 };
|
||||
std::atomic<uint64_t> mShaderBuildsCommitted{ 0 };
|
||||
std::atomic<uint64_t> mShaderBuildFailures{ 0 };
|
||||
std::atomic<double> mRenderFrameMilliseconds{ 0.0 };
|
||||
std::atomic<double> mRenderFrameBudgetUsedPercent{ 0.0 };
|
||||
std::atomic<double> mRenderFrameMaxMilliseconds{ 0.0 };
|
||||
std::atomic<double> mReadbackQueueMilliseconds{ 0.0 };
|
||||
std::atomic<double> mCompletedReadbackCopyMilliseconds{ 0.0 };
|
||||
std::atomic<uint64_t> mInputFramesReceived{ 0 };
|
||||
std::atomic<uint64_t> mInputFramesDropped{ 0 };
|
||||
std::atomic<uint64_t> mInputConsumeMisses{ 0 };
|
||||
std::atomic<uint64_t> mInputUploadMisses{ 0 };
|
||||
std::atomic<std::size_t> mInputReadyFrames{ 0 };
|
||||
std::atomic<std::size_t> mInputReadingFrames{ 0 };
|
||||
std::atomic<double> mInputLatestAgeMilliseconds{ 0.0 };
|
||||
std::atomic<double> mInputUploadMilliseconds{ 0.0 };
|
||||
std::atomic<bool> mInputFormatSupported{ true };
|
||||
std::atomic<bool> mInputSignalPresent{ false };
|
||||
|
||||
std::mutex mShaderArtifactMutex;
|
||||
bool mHasPendingShaderArtifact = false;
|
||||
|
||||
@@ -3,8 +3,135 @@
|
||||
#include "GLExtensions.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cmath>
|
||||
|
||||
namespace
|
||||
{
|
||||
constexpr GLuint kInputTextureUnit = 0;
|
||||
|
||||
const char* kTextureVertexShader = R"GLSL(
|
||||
#version 430 core
|
||||
out vec2 vTexCoord;
|
||||
void main()
|
||||
{
|
||||
vec2 positions[3] = vec2[3](
|
||||
vec2(-1.0, -1.0),
|
||||
vec2( 3.0, -1.0),
|
||||
vec2(-1.0, 3.0));
|
||||
vec2 texCoords[3] = vec2[3](
|
||||
vec2(0.0, 0.0),
|
||||
vec2(2.0, 0.0),
|
||||
vec2(0.0, 2.0));
|
||||
gl_Position = vec4(positions[gl_VertexID], 0.0, 1.0);
|
||||
vTexCoord = texCoords[gl_VertexID];
|
||||
}
|
||||
)GLSL";
|
||||
|
||||
const char* kTextureFragmentShader = R"GLSL(
|
||||
#version 430 core
|
||||
layout(binding = 0) uniform sampler2D uInputTexture;
|
||||
in vec2 vTexCoord;
|
||||
out vec4 fragColor;
|
||||
void main()
|
||||
{
|
||||
fragColor = texture(uInputTexture, clamp(vTexCoord, vec2(0.0), vec2(1.0)));
|
||||
}
|
||||
)GLSL";
|
||||
|
||||
const char* kPatternFragmentShader = R"GLSL(
|
||||
#version 430 core
|
||||
uniform vec2 uResolution;
|
||||
uniform float uFrameIndex;
|
||||
in vec2 vTexCoord;
|
||||
out vec4 fragColor;
|
||||
|
||||
vec3 hexColor(float r, float g, float b)
|
||||
{
|
||||
return vec3(r, g, b) / 255.0;
|
||||
}
|
||||
|
||||
vec3 smpteTop(float x)
|
||||
{
|
||||
if (x < 240.0) return hexColor(102.0, 102.0, 102.0);
|
||||
if (x < 445.0) return hexColor(191.0, 191.0, 191.0);
|
||||
if (x < 651.0) return hexColor(191.0, 191.0, 0.0);
|
||||
if (x < 857.0) return hexColor(0.0, 191.0, 191.0);
|
||||
if (x < 1063.0) return hexColor(0.0, 191.0, 0.0);
|
||||
if (x < 1269.0) return hexColor(191.0, 0.0, 191.0);
|
||||
if (x < 1475.0) return hexColor(191.0, 0.0, 0.0);
|
||||
if (x < 1680.0) return hexColor(0.0, 0.0, 191.0);
|
||||
return hexColor(102.0, 102.0, 102.0);
|
||||
}
|
||||
|
||||
vec3 smpteMiddleA(float x)
|
||||
{
|
||||
if (x < 240.0) return hexColor(0.0, 255.0, 255.0);
|
||||
if (x < 445.0) return hexColor(0.0, 63.0, 105.0);
|
||||
if (x < 1680.0) return hexColor(191.0, 191.0, 191.0);
|
||||
return hexColor(0.0, 0.0, 255.0);
|
||||
}
|
||||
|
||||
vec3 smpteMiddleB(float x)
|
||||
{
|
||||
if (x < 240.0) return hexColor(255.0, 255.0, 0.0);
|
||||
if (x < 445.0) return hexColor(65.0, 0.0, 119.0);
|
||||
if (x < 1475.0)
|
||||
{
|
||||
float ramp = clamp((x - 445.0) / (1475.0 - 445.0), 0.0, 1.0);
|
||||
return vec3(ramp);
|
||||
}
|
||||
if (x < 1680.0) return vec3(1.0);
|
||||
return hexColor(255.0, 0.0, 0.0);
|
||||
}
|
||||
|
||||
vec3 smpteBottom(float x)
|
||||
{
|
||||
if (x < 240.0) return hexColor(38.0, 38.0, 38.0);
|
||||
if (x < 549.0) return vec3(0.0);
|
||||
if (x < 960.0) return vec3(1.0);
|
||||
if (x < 1268.0) return vec3(0.0);
|
||||
if (x < 1337.0) return hexColor(5.0, 5.0, 5.0);
|
||||
if (x < 1405.0) return vec3(0.0);
|
||||
if (x < 1474.0) return hexColor(10.0, 10.0, 10.0);
|
||||
if (x < 1680.0) return vec3(0.0);
|
||||
return hexColor(38.0, 38.0, 38.0);
|
||||
}
|
||||
|
||||
vec3 smpteColor(vec2 uv)
|
||||
{
|
||||
vec2 pixel = vec2(clamp(uv.x, 0.0, 1.0), 1.0 - clamp(uv.y, 0.0, 1.0)) * vec2(1920.0, 1080.0);
|
||||
if (pixel.y < 630.0) return smpteTop(pixel.x);
|
||||
if (pixel.y < 720.0) return smpteMiddleA(pixel.x);
|
||||
if (pixel.y < 810.0) return smpteMiddleB(pixel.x);
|
||||
return smpteBottom(pixel.x);
|
||||
}
|
||||
|
||||
void main()
|
||||
{
|
||||
vec2 uv = clamp(vTexCoord, vec2(0.0), vec2(1.0));
|
||||
vec3 color = smpteColor(uv);
|
||||
|
||||
float t = uFrameIndex / 60.0;
|
||||
vec2 cubeSize = vec2(0.16, 0.20);
|
||||
vec2 cubeMin = vec2(
|
||||
(0.5 + 0.5 * sin(t * 1.7)) * (1.0 - cubeSize.x),
|
||||
(0.5 + 0.5 * sin(t * 1.1 + 0.8)) * (1.0 - cubeSize.y));
|
||||
vec2 cubeMax = cubeMin + cubeSize;
|
||||
bool insideCube = uv.x >= cubeMin.x && uv.x <= cubeMax.x && uv.y >= cubeMin.y && uv.y <= cubeMax.y;
|
||||
if (insideCube)
|
||||
{
|
||||
vec2 local = (uv - cubeMin) / cubeSize;
|
||||
vec3 cubeColor = vec3(1.0, 0.74 + 0.18 * sin(t * 2.1), 0.08);
|
||||
float edge = step(local.x, 0.04) + step(local.y, 0.04) + step(0.96, local.x) + step(0.96, local.y);
|
||||
color = edge > 0.0 ? vec3(1.0) : cubeColor;
|
||||
}
|
||||
|
||||
fragColor = vec4(color, 1.0);
|
||||
}
|
||||
)GLSL";
|
||||
}
|
||||
|
||||
bool SimpleMotionRenderer::InitializeGl(unsigned width, unsigned height)
|
||||
{
|
||||
mWidth = width;
|
||||
@@ -14,37 +141,173 @@ bool SimpleMotionRenderer::InitializeGl(unsigned width, unsigned height)
|
||||
|
||||
void SimpleMotionRenderer::RenderFrame(uint64_t frameIndex)
|
||||
{
|
||||
const float t = static_cast<float>(frameIndex) / 60.0f;
|
||||
const float red = 0.1f + 0.4f * (0.5f + 0.5f * std::sin(t));
|
||||
const float green = 0.1f + 0.4f * (0.5f + 0.5f * std::sin(t * 0.73f + 1.0f));
|
||||
const float blue = 0.15f + 0.3f * (0.5f + 0.5f * std::sin(t * 0.41f + 2.0f));
|
||||
if (!EnsurePatternProgram())
|
||||
{
|
||||
glViewport(0, 0, static_cast<GLsizei>(mWidth), static_cast<GLsizei>(mHeight));
|
||||
glDisable(GL_SCISSOR_TEST);
|
||||
glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
|
||||
glClear(GL_COLOR_BUFFER_BIT);
|
||||
return;
|
||||
}
|
||||
|
||||
glViewport(0, 0, static_cast<GLsizei>(mWidth), static_cast<GLsizei>(mHeight));
|
||||
glDisable(GL_SCISSOR_TEST);
|
||||
glClearColor(red, green, blue, 1.0f);
|
||||
glClear(GL_COLOR_BUFFER_BIT);
|
||||
glDisable(GL_DEPTH_TEST);
|
||||
glDisable(GL_BLEND);
|
||||
glUseProgram(mPatternProgram);
|
||||
const GLint resolutionLocation = glGetUniformLocation(mPatternProgram, "uResolution");
|
||||
if (resolutionLocation >= 0)
|
||||
glUniform2f(resolutionLocation, static_cast<float>(mWidth), static_cast<float>(mHeight));
|
||||
const GLint frameIndexLocation = glGetUniformLocation(mPatternProgram, "uFrameIndex");
|
||||
if (frameIndexLocation >= 0)
|
||||
glUniform1f(frameIndexLocation, static_cast<float>(frameIndex));
|
||||
glBindVertexArray(mPatternVertexArray);
|
||||
glDrawArrays(GL_TRIANGLES, 0, 3);
|
||||
glBindVertexArray(0);
|
||||
glUseProgram(0);
|
||||
}
|
||||
|
||||
const int boxWidth = (std::max)(1, static_cast<int>(mWidth / 6));
|
||||
const int boxHeight = (std::max)(1, static_cast<int>(mHeight / 5));
|
||||
const float phase = 0.5f + 0.5f * std::sin(t * 1.7f);
|
||||
const int x = static_cast<int>(phase * static_cast<float>(mWidth - static_cast<unsigned>(boxWidth)));
|
||||
const int y = static_cast<int>((0.5f + 0.5f * std::sin(t * 1.1f + 0.8f)) * static_cast<float>(mHeight - static_cast<unsigned>(boxHeight)));
|
||||
void SimpleMotionRenderer::RenderTexture(GLuint texture)
|
||||
{
|
||||
if (texture == 0 || !EnsureTextureProgram())
|
||||
{
|
||||
RenderFrame(0);
|
||||
return;
|
||||
}
|
||||
|
||||
glEnable(GL_SCISSOR_TEST);
|
||||
glScissor(x, y, boxWidth, boxHeight);
|
||||
glClearColor(1.0f - red, 0.85f, 0.15f + blue, 1.0f);
|
||||
glClear(GL_COLOR_BUFFER_BIT);
|
||||
|
||||
const int stripeWidth = (std::max)(1, static_cast<int>(mWidth / 80));
|
||||
const int stripeX = static_cast<int>((frameIndex % 120) * (mWidth - static_cast<unsigned>(stripeWidth)) / 119);
|
||||
glScissor(stripeX, 0, stripeWidth, static_cast<GLsizei>(mHeight));
|
||||
glClearColor(1.0f, 1.0f, 1.0f, 1.0f);
|
||||
glClear(GL_COLOR_BUFFER_BIT);
|
||||
glViewport(0, 0, static_cast<GLsizei>(mWidth), static_cast<GLsizei>(mHeight));
|
||||
glDisable(GL_SCISSOR_TEST);
|
||||
glDisable(GL_DEPTH_TEST);
|
||||
glDisable(GL_BLEND);
|
||||
glActiveTexture(GL_TEXTURE0 + kInputTextureUnit);
|
||||
glBindTexture(GL_TEXTURE_2D, texture);
|
||||
glUseProgram(mTextureProgram);
|
||||
glBindVertexArray(mTextureVertexArray);
|
||||
glDrawArrays(GL_TRIANGLES, 0, 3);
|
||||
glBindVertexArray(0);
|
||||
glUseProgram(0);
|
||||
glBindTexture(GL_TEXTURE_2D, 0);
|
||||
glActiveTexture(GL_TEXTURE0);
|
||||
}
|
||||
|
||||
void SimpleMotionRenderer::ShutdownGl()
|
||||
{
|
||||
DestroyPatternProgram();
|
||||
DestroyTextureProgram();
|
||||
mWidth = 0;
|
||||
mHeight = 0;
|
||||
}
|
||||
|
||||
bool SimpleMotionRenderer::EnsurePatternProgram()
|
||||
{
|
||||
if (mPatternProgram != 0)
|
||||
return true;
|
||||
|
||||
if (!CompileShader(GL_VERTEX_SHADER, kTextureVertexShader, mPatternVertexShader))
|
||||
return false;
|
||||
if (!CompileShader(GL_FRAGMENT_SHADER, kPatternFragmentShader, mPatternFragmentShader))
|
||||
{
|
||||
DestroyPatternProgram();
|
||||
return false;
|
||||
}
|
||||
|
||||
mPatternProgram = glCreateProgram();
|
||||
glAttachShader(mPatternProgram, mPatternVertexShader);
|
||||
glAttachShader(mPatternProgram, mPatternFragmentShader);
|
||||
glLinkProgram(mPatternProgram);
|
||||
|
||||
GLint linkResult = GL_FALSE;
|
||||
glGetProgramiv(mPatternProgram, GL_LINK_STATUS, &linkResult);
|
||||
if (linkResult == GL_FALSE)
|
||||
{
|
||||
DestroyPatternProgram();
|
||||
return false;
|
||||
}
|
||||
|
||||
glGenVertexArrays(1, &mPatternVertexArray);
|
||||
return mPatternVertexArray != 0;
|
||||
}
|
||||
|
||||
bool SimpleMotionRenderer::EnsureTextureProgram()
|
||||
{
|
||||
if (mTextureProgram != 0)
|
||||
return true;
|
||||
|
||||
if (!CompileShader(GL_VERTEX_SHADER, kTextureVertexShader, mTextureVertexShader))
|
||||
return false;
|
||||
if (!CompileShader(GL_FRAGMENT_SHADER, kTextureFragmentShader, mTextureFragmentShader))
|
||||
{
|
||||
DestroyTextureProgram();
|
||||
return false;
|
||||
}
|
||||
|
||||
mTextureProgram = glCreateProgram();
|
||||
glAttachShader(mTextureProgram, mTextureVertexShader);
|
||||
glAttachShader(mTextureProgram, mTextureFragmentShader);
|
||||
glLinkProgram(mTextureProgram);
|
||||
|
||||
GLint linkResult = GL_FALSE;
|
||||
glGetProgramiv(mTextureProgram, GL_LINK_STATUS, &linkResult);
|
||||
if (linkResult == GL_FALSE)
|
||||
{
|
||||
DestroyTextureProgram();
|
||||
return false;
|
||||
}
|
||||
|
||||
glUseProgram(mTextureProgram);
|
||||
const GLint inputLocation = glGetUniformLocation(mTextureProgram, "uInputTexture");
|
||||
if (inputLocation >= 0)
|
||||
glUniform1i(inputLocation, static_cast<GLint>(kInputTextureUnit));
|
||||
glUseProgram(0);
|
||||
|
||||
glGenVertexArrays(1, &mTextureVertexArray);
|
||||
return mTextureVertexArray != 0;
|
||||
}
|
||||
|
||||
void SimpleMotionRenderer::DestroyTextureProgram()
|
||||
{
|
||||
if (mTextureProgram != 0)
|
||||
glDeleteProgram(mTextureProgram);
|
||||
if (mTextureVertexShader != 0)
|
||||
glDeleteShader(mTextureVertexShader);
|
||||
if (mTextureFragmentShader != 0)
|
||||
glDeleteShader(mTextureFragmentShader);
|
||||
if (mTextureVertexArray != 0)
|
||||
glDeleteVertexArrays(1, &mTextureVertexArray);
|
||||
mTextureProgram = 0;
|
||||
mTextureVertexShader = 0;
|
||||
mTextureFragmentShader = 0;
|
||||
mTextureVertexArray = 0;
|
||||
}
|
||||
|
||||
void SimpleMotionRenderer::DestroyPatternProgram()
|
||||
{
|
||||
if (mPatternProgram != 0)
|
||||
glDeleteProgram(mPatternProgram);
|
||||
if (mPatternVertexShader != 0)
|
||||
glDeleteShader(mPatternVertexShader);
|
||||
if (mPatternFragmentShader != 0)
|
||||
glDeleteShader(mPatternFragmentShader);
|
||||
if (mPatternVertexArray != 0)
|
||||
glDeleteVertexArrays(1, &mPatternVertexArray);
|
||||
mPatternProgram = 0;
|
||||
mPatternVertexShader = 0;
|
||||
mPatternFragmentShader = 0;
|
||||
mPatternVertexArray = 0;
|
||||
}
|
||||
|
||||
bool SimpleMotionRenderer::CompileShader(GLenum shaderType, const char* source, GLuint& shader)
|
||||
{
|
||||
shader = glCreateShader(shaderType);
|
||||
glShaderSource(shader, 1, &source, nullptr);
|
||||
glCompileShader(shader);
|
||||
|
||||
GLint compileResult = GL_FALSE;
|
||||
glGetShaderiv(shader, GL_COMPILE_STATUS, &compileResult);
|
||||
if (compileResult != GL_FALSE)
|
||||
return true;
|
||||
|
||||
glDeleteShader(shader);
|
||||
shader = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
#pragma once
|
||||
|
||||
#include "GLExtensions.h"
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
class SimpleMotionRenderer
|
||||
@@ -9,12 +11,27 @@ public:
|
||||
|
||||
bool InitializeGl(unsigned width, unsigned height);
|
||||
void RenderFrame(uint64_t frameIndex);
|
||||
void RenderTexture(GLuint texture);
|
||||
void ShutdownGl();
|
||||
|
||||
unsigned Width() const { return mWidth; }
|
||||
unsigned Height() const { return mHeight; }
|
||||
|
||||
private:
|
||||
bool EnsureTextureProgram();
|
||||
bool EnsurePatternProgram();
|
||||
void DestroyTextureProgram();
|
||||
void DestroyPatternProgram();
|
||||
static bool CompileShader(GLenum shaderType, const char* source, GLuint& shader);
|
||||
|
||||
unsigned mWidth = 0;
|
||||
unsigned mHeight = 0;
|
||||
GLuint mPatternProgram = 0;
|
||||
GLuint mPatternVertexShader = 0;
|
||||
GLuint mPatternFragmentShader = 0;
|
||||
GLuint mPatternVertexArray = 0;
|
||||
GLuint mTextureProgram = 0;
|
||||
GLuint mTextureVertexShader = 0;
|
||||
GLuint mTextureFragmentShader = 0;
|
||||
GLuint mTextureVertexArray = 0;
|
||||
};
|
||||
|
||||
@@ -2,8 +2,18 @@
|
||||
|
||||
#include "../frames/SystemFrameTypes.h"
|
||||
|
||||
#include <chrono>
|
||||
#include <cstring>
|
||||
|
||||
namespace
|
||||
{
|
||||
double MillisecondsSince(std::chrono::steady_clock::time_point start)
|
||||
{
|
||||
return std::chrono::duration_cast<std::chrono::duration<double, std::milli>>(
|
||||
std::chrono::steady_clock::now() - start).count();
|
||||
}
|
||||
}
|
||||
|
||||
Bgra8ReadbackPipeline::~Bgra8ReadbackPipeline()
|
||||
{
|
||||
Shutdown();
|
||||
@@ -50,10 +60,15 @@ bool Bgra8ReadbackPipeline::RenderAndQueue(uint64_t frameIndex, const RenderCall
|
||||
return false;
|
||||
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, mFramebuffer);
|
||||
const auto renderStart = std::chrono::steady_clock::now();
|
||||
renderFrame(frameIndex);
|
||||
mLastRenderFrameMilliseconds = MillisecondsSince(renderStart);
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||
|
||||
return mPboRing.QueueReadback(mFramebuffer, mWidth, mHeight, frameIndex);
|
||||
const auto queueStart = std::chrono::steady_clock::now();
|
||||
const bool queued = mPboRing.QueueReadback(mFramebuffer, mWidth, mHeight, frameIndex);
|
||||
mLastReadbackQueueMilliseconds = MillisecondsSince(queueStart);
|
||||
return queued;
|
||||
}
|
||||
|
||||
void Bgra8ReadbackPipeline::ConsumeCompleted(
|
||||
@@ -68,12 +83,14 @@ void Bgra8ReadbackPipeline::ConsumeCompleted(
|
||||
PboReadbackRing::CompletedReadback readback;
|
||||
while (mPboRing.TryAcquireCompleted(readback))
|
||||
{
|
||||
const auto copyStart = std::chrono::steady_clock::now();
|
||||
glBindBuffer(GL_PIXEL_PACK_BUFFER, readback.pbo);
|
||||
void* mapped = glMapBuffer(GL_PIXEL_PACK_BUFFER, GL_READ_ONLY);
|
||||
if (!mapped)
|
||||
{
|
||||
glBindBuffer(GL_PIXEL_PACK_BUFFER, 0);
|
||||
mPboRing.ReleaseCompleted(readback);
|
||||
mLastCompletedReadbackCopyMilliseconds = MillisecondsSince(copyStart);
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -99,6 +116,7 @@ void Bgra8ReadbackPipeline::ConsumeCompleted(
|
||||
glUnmapBuffer(GL_PIXEL_PACK_BUFFER);
|
||||
glBindBuffer(GL_PIXEL_PACK_BUFFER, 0);
|
||||
mPboRing.ReleaseCompleted(readback);
|
||||
mLastCompletedReadbackCopyMilliseconds = MillisecondsSince(copyStart);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -38,6 +38,9 @@ public:
|
||||
unsigned RowBytes() const { return mRowBytes; }
|
||||
VideoIOPixelFormat PixelFormat() const { return VideoIOPixelFormat::Bgra8; }
|
||||
uint64_t PboQueueMisses() const { return mPboRing.QueueMisses(); }
|
||||
double LastRenderFrameMilliseconds() const { return mLastRenderFrameMilliseconds; }
|
||||
double LastReadbackQueueMilliseconds() const { return mLastReadbackQueueMilliseconds; }
|
||||
double LastCompletedReadbackCopyMilliseconds() const { return mLastCompletedReadbackCopyMilliseconds; }
|
||||
|
||||
private:
|
||||
bool CreateRenderTarget();
|
||||
@@ -48,5 +51,8 @@ private:
|
||||
unsigned mRowBytes = 0;
|
||||
GLuint mFramebuffer = 0;
|
||||
GLuint mTexture = 0;
|
||||
double mLastRenderFrameMilliseconds = 0.0;
|
||||
double mLastReadbackQueueMilliseconds = 0.0;
|
||||
double mLastCompletedReadbackCopyMilliseconds = 0.0;
|
||||
PboReadbackRing mPboRing;
|
||||
};
|
||||
|
||||
@@ -28,7 +28,10 @@ bool RuntimeRenderScene::CommitRenderLayers(const std::vector<RenderCadenceCompo
|
||||
std::vector<RenderCadenceCompositor::RuntimeRenderLayerModel> layersToPrepare;
|
||||
nextOrder.reserve(layers.size());
|
||||
for (const RenderCadenceCompositor::RuntimeRenderLayerModel& layer : layers)
|
||||
nextOrder.push_back(layer.id);
|
||||
{
|
||||
if (!layer.bypass)
|
||||
nextOrder.push_back(layer.id);
|
||||
}
|
||||
|
||||
for (auto layerIt = mLayers.begin(); layerIt != mLayers.end();)
|
||||
{
|
||||
@@ -39,14 +42,20 @@ bool RuntimeRenderScene::CommitRenderLayers(const std::vector<RenderCadenceCompo
|
||||
continue;
|
||||
}
|
||||
|
||||
if (layerIt->renderer)
|
||||
layerIt->renderer->ShutdownGl();
|
||||
for (LayerProgram::PassProgram& pass : layerIt->passes)
|
||||
{
|
||||
if (pass.renderer)
|
||||
pass.renderer->ShutdownGl();
|
||||
}
|
||||
ReleasePendingPrograms(*layerIt);
|
||||
layerIt = mLayers.erase(layerIt);
|
||||
}
|
||||
|
||||
for (const RenderCadenceCompositor::RuntimeRenderLayerModel& layer : layers)
|
||||
{
|
||||
if (layer.artifact.fragmentShaderSource.empty())
|
||||
if (layer.bypass)
|
||||
continue;
|
||||
if (layer.artifact.passes.empty() && layer.artifact.fragmentShaderSource.empty())
|
||||
continue;
|
||||
|
||||
const std::string fingerprint = Fingerprint(layer.artifact);
|
||||
@@ -55,16 +64,32 @@ bool RuntimeRenderScene::CommitRenderLayers(const std::vector<RenderCadenceCompo
|
||||
{
|
||||
LayerProgram next;
|
||||
next.layerId = layer.id;
|
||||
next.renderer = std::make_unique<RuntimeShaderRenderer>();
|
||||
mLayers.push_back(std::move(next));
|
||||
program = &mLayers.back();
|
||||
}
|
||||
|
||||
if (program->shaderId == layer.shaderId && program->sourceFingerprint == fingerprint && program->renderer && program->renderer->HasProgram())
|
||||
bool hasReadyPass = false;
|
||||
for (const LayerProgram::PassProgram& pass : program->passes)
|
||||
{
|
||||
if (pass.renderer && pass.renderer->HasProgram())
|
||||
{
|
||||
hasReadyPass = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (program->shaderId == layer.shaderId && program->sourceFingerprint == fingerprint && hasReadyPass)
|
||||
{
|
||||
for (LayerProgram::PassProgram& pass : program->passes)
|
||||
{
|
||||
if (pass.renderer)
|
||||
pass.renderer->UpdateArtifactState(layer.artifact);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (program->pendingFingerprint == fingerprint)
|
||||
continue;
|
||||
|
||||
ReleasePendingPrograms(*program);
|
||||
program->shaderId = layer.shaderId;
|
||||
program->pendingFingerprint = fingerprint;
|
||||
layersToPrepare.push_back(layer);
|
||||
@@ -77,77 +102,17 @@ bool RuntimeRenderScene::CommitRenderLayers(const std::vector<RenderCadenceCompo
|
||||
return true;
|
||||
}
|
||||
|
||||
bool RuntimeRenderScene::HasLayers()
|
||||
{
|
||||
ConsumePreparedPrograms();
|
||||
|
||||
for (const std::string& layerId : mLayerOrder)
|
||||
{
|
||||
const LayerProgram* layer = FindLayer(layerId);
|
||||
if (layer && layer->renderer && layer->renderer->HasProgram())
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void RuntimeRenderScene::RenderFrame(uint64_t frameIndex, unsigned width, unsigned height)
|
||||
{
|
||||
ConsumePreparedPrograms();
|
||||
|
||||
std::vector<LayerProgram*> readyLayers;
|
||||
for (const std::string& layerId : mLayerOrder)
|
||||
{
|
||||
LayerProgram* layer = FindLayer(layerId);
|
||||
if (!layer || !layer->renderer || !layer->renderer->HasProgram())
|
||||
continue;
|
||||
readyLayers.push_back(layer);
|
||||
}
|
||||
|
||||
if (readyLayers.empty())
|
||||
return;
|
||||
|
||||
GLint outputFramebuffer = 0;
|
||||
glGetIntegerv(GL_FRAMEBUFFER_BINDING, &outputFramebuffer);
|
||||
|
||||
if (readyLayers.size() == 1)
|
||||
{
|
||||
readyLayers.front()->renderer->RenderFrame(frameIndex, width, height);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!EnsureLayerTargets(width, height))
|
||||
{
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, static_cast<GLuint>(outputFramebuffer));
|
||||
readyLayers.back()->renderer->RenderFrame(frameIndex, width, height);
|
||||
return;
|
||||
}
|
||||
|
||||
GLuint layerInputTexture = 0;
|
||||
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));
|
||||
readyLayers[layerIndex]->renderer->RenderFrame(frameIndex, width, height, layerInputTexture, layerInputTexture);
|
||||
continue;
|
||||
}
|
||||
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, mLayerFramebuffers[nextTargetIndex]);
|
||||
readyLayers[layerIndex]->renderer->RenderFrame(frameIndex, width, height, layerInputTexture, layerInputTexture);
|
||||
layerInputTexture = mLayerTextures[nextTargetIndex];
|
||||
nextTargetIndex = 1 - nextTargetIndex;
|
||||
}
|
||||
}
|
||||
|
||||
void RuntimeRenderScene::ShutdownGl()
|
||||
{
|
||||
mPrepareWorker.Stop();
|
||||
for (LayerProgram& layer : mLayers)
|
||||
{
|
||||
if (layer.renderer)
|
||||
layer.renderer->ShutdownGl();
|
||||
for (LayerProgram::PassProgram& pass : layer.passes)
|
||||
{
|
||||
if (pass.renderer)
|
||||
pass.renderer->ShutdownGl();
|
||||
}
|
||||
ReleasePendingPrograms(layer);
|
||||
}
|
||||
mLayers.clear();
|
||||
mLayerOrder.clear();
|
||||
@@ -172,83 +137,96 @@ void RuntimeRenderScene::ConsumePreparedPrograms()
|
||||
continue;
|
||||
}
|
||||
|
||||
bool replacesExistingPendingPass = false;
|
||||
for (RuntimePreparedShaderProgram& existing : layer->pendingPreparedPrograms)
|
||||
{
|
||||
if (existing.passId != preparedProgram.passId)
|
||||
continue;
|
||||
existing.ReleaseGl();
|
||||
existing = std::move(preparedProgram);
|
||||
replacesExistingPendingPass = true;
|
||||
break;
|
||||
}
|
||||
if (!replacesExistingPendingPass)
|
||||
layer->pendingPreparedPrograms.push_back(std::move(preparedProgram));
|
||||
TryCommitPendingPrograms(*layer);
|
||||
}
|
||||
}
|
||||
|
||||
void RuntimeRenderScene::ReleasePendingPrograms(LayerProgram& layer)
|
||||
{
|
||||
for (RuntimePreparedShaderProgram& program : layer.pendingPreparedPrograms)
|
||||
program.ReleaseGl();
|
||||
layer.pendingPreparedPrograms.clear();
|
||||
}
|
||||
|
||||
void RuntimeRenderScene::TryCommitPendingPrograms(LayerProgram& layer)
|
||||
{
|
||||
if (layer.pendingPreparedPrograms.empty())
|
||||
return;
|
||||
|
||||
const RuntimeShaderArtifact& artifact = layer.pendingPreparedPrograms.front().artifact;
|
||||
const std::size_t expectedPassCount = artifact.passes.empty() ? 1 : artifact.passes.size();
|
||||
if (layer.pendingPreparedPrograms.size() < expectedPassCount)
|
||||
return;
|
||||
|
||||
std::vector<LayerProgram::PassProgram> nextPasses;
|
||||
nextPasses.reserve(expectedPassCount);
|
||||
for (const RuntimeShaderPassArtifact& passArtifact : artifact.passes)
|
||||
{
|
||||
auto preparedIt = std::find_if(
|
||||
layer.pendingPreparedPrograms.begin(),
|
||||
layer.pendingPreparedPrograms.end(),
|
||||
[&passArtifact](const RuntimePreparedShaderProgram& prepared) {
|
||||
return prepared.passId == passArtifact.passId;
|
||||
});
|
||||
if (preparedIt == layer.pendingPreparedPrograms.end())
|
||||
return;
|
||||
|
||||
std::unique_ptr<RuntimeShaderRenderer> nextRenderer = std::make_unique<RuntimeShaderRenderer>();
|
||||
std::string error;
|
||||
if (!nextRenderer->CommitPreparedProgram(preparedProgram, error))
|
||||
if (!nextRenderer->CommitPreparedProgram(*preparedIt, error))
|
||||
{
|
||||
preparedProgram.ReleaseGl();
|
||||
continue;
|
||||
ReleasePendingPrograms(layer);
|
||||
return;
|
||||
}
|
||||
|
||||
if (layer->renderer)
|
||||
layer->renderer->ShutdownGl();
|
||||
layer->renderer = std::move(nextRenderer);
|
||||
layer->shaderId = preparedProgram.shaderId;
|
||||
layer->sourceFingerprint = preparedProgram.sourceFingerprint;
|
||||
layer->pendingFingerprint.clear();
|
||||
LayerProgram::PassProgram nextPass;
|
||||
nextPass.passId = preparedIt->passId;
|
||||
nextPass.inputNames = preparedIt->inputNames;
|
||||
nextPass.outputName = preparedIt->outputName.empty() ? preparedIt->passId : preparedIt->outputName;
|
||||
nextPass.renderer = std::move(nextRenderer);
|
||||
nextPasses.push_back(std::move(nextPass));
|
||||
}
|
||||
}
|
||||
|
||||
bool RuntimeRenderScene::EnsureLayerTargets(unsigned width, unsigned height)
|
||||
{
|
||||
if (width == 0 || height == 0)
|
||||
return false;
|
||||
if (mLayerFramebuffers[0] != 0 && mLayerFramebuffers[1] != 0 && mLayerTextures[0] != 0 && mLayerTextures[1] != 0
|
||||
&& mLayerTargetWidth == width && mLayerTargetHeight == height)
|
||||
return true;
|
||||
|
||||
DestroyLayerTargets();
|
||||
mLayerTargetWidth = width;
|
||||
mLayerTargetHeight = height;
|
||||
|
||||
glGenFramebuffers(2, mLayerFramebuffers);
|
||||
glGenTextures(2, mLayerTextures);
|
||||
for (int index = 0; index < 2; ++index)
|
||||
if (artifact.passes.empty())
|
||||
{
|
||||
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)
|
||||
RuntimePreparedShaderProgram& prepared = layer.pendingPreparedPrograms.front();
|
||||
std::unique_ptr<RuntimeShaderRenderer> nextRenderer = std::make_unique<RuntimeShaderRenderer>();
|
||||
std::string error;
|
||||
if (!nextRenderer->CommitPreparedProgram(prepared, error))
|
||||
{
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||
glBindTexture(GL_TEXTURE_2D, 0);
|
||||
DestroyLayerTargets();
|
||||
return false;
|
||||
ReleasePendingPrograms(layer);
|
||||
return;
|
||||
}
|
||||
|
||||
LayerProgram::PassProgram nextPass;
|
||||
nextPass.passId = prepared.passId;
|
||||
nextPass.inputNames = prepared.inputNames;
|
||||
nextPass.outputName = prepared.outputName.empty() ? prepared.passId : prepared.outputName;
|
||||
nextPass.renderer = std::move(nextRenderer);
|
||||
nextPasses.push_back(std::move(nextPass));
|
||||
}
|
||||
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||
glBindTexture(GL_TEXTURE_2D, 0);
|
||||
return true;
|
||||
}
|
||||
|
||||
void RuntimeRenderScene::DestroyLayerTargets()
|
||||
{
|
||||
if (mLayerFramebuffers[0] != 0 || mLayerFramebuffers[1] != 0)
|
||||
glDeleteFramebuffers(2, mLayerFramebuffers);
|
||||
if (mLayerTextures[0] != 0 || mLayerTextures[1] != 0)
|
||||
glDeleteTextures(2, mLayerTextures);
|
||||
mLayerFramebuffers[0] = 0;
|
||||
mLayerFramebuffers[1] = 0;
|
||||
mLayerTextures[0] = 0;
|
||||
mLayerTextures[1] = 0;
|
||||
mLayerTargetWidth = 0;
|
||||
mLayerTargetHeight = 0;
|
||||
for (LayerProgram::PassProgram& pass : layer.passes)
|
||||
{
|
||||
if (pass.renderer)
|
||||
pass.renderer->ShutdownGl();
|
||||
}
|
||||
layer.passes = std::move(nextPasses);
|
||||
layer.shaderId = artifact.shaderId;
|
||||
layer.sourceFingerprint = layer.pendingPreparedPrograms.front().sourceFingerprint;
|
||||
layer.pendingFingerprint.clear();
|
||||
layer.pendingPreparedPrograms.clear();
|
||||
}
|
||||
|
||||
RuntimeRenderScene::LayerProgram* RuntimeRenderScene::FindLayer(const std::string& layerId)
|
||||
@@ -271,8 +249,23 @@ const RuntimeRenderScene::LayerProgram* RuntimeRenderScene::FindLayer(const std:
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
RuntimeRenderScene::LayerProgram::PassProgram* RuntimeRenderScene::FindPass(LayerProgram& layer, const std::string& passId)
|
||||
{
|
||||
for (LayerProgram::PassProgram& pass : layer.passes)
|
||||
{
|
||||
if (pass.passId == passId)
|
||||
return &pass;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
std::string RuntimeRenderScene::Fingerprint(const RuntimeShaderArtifact& artifact)
|
||||
{
|
||||
const std::hash<std::string> hasher;
|
||||
return artifact.shaderId + ":" + std::to_string(artifact.fragmentShaderSource.size()) + ":" + std::to_string(hasher(artifact.fragmentShaderSource));
|
||||
std::string source;
|
||||
for (const RuntimeShaderPassArtifact& pass : artifact.passes)
|
||||
source += pass.passId + ":" + pass.outputName + ":" + pass.fragmentShaderSource + "\n";
|
||||
if (source.empty())
|
||||
source = artifact.fragmentShaderSource;
|
||||
return artifact.shaderId + ":" + std::to_string(source.size()) + ":" + std::to_string(hasher(source));
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ public:
|
||||
bool StartPrepareWorker(std::unique_ptr<HiddenGlWindow> sharedWindow, std::string& error);
|
||||
bool CommitRenderLayers(const std::vector<RenderCadenceCompositor::RuntimeRenderLayerModel>& layers, std::string& error);
|
||||
bool HasLayers();
|
||||
void RenderFrame(uint64_t frameIndex, unsigned width, unsigned height);
|
||||
void RenderFrame(uint64_t frameIndex, unsigned width, unsigned height, GLuint videoInputTexture = 0);
|
||||
void ShutdownGl();
|
||||
|
||||
private:
|
||||
@@ -32,21 +32,33 @@ private:
|
||||
std::string shaderId;
|
||||
std::string sourceFingerprint;
|
||||
std::string pendingFingerprint;
|
||||
std::unique_ptr<RuntimeShaderRenderer> renderer;
|
||||
std::vector<RuntimePreparedShaderProgram> pendingPreparedPrograms;
|
||||
struct PassProgram
|
||||
{
|
||||
std::string passId;
|
||||
std::vector<std::string> inputNames;
|
||||
std::string outputName;
|
||||
std::unique_ptr<RuntimeShaderRenderer> renderer;
|
||||
};
|
||||
std::vector<PassProgram> passes;
|
||||
};
|
||||
|
||||
void ConsumePreparedPrograms();
|
||||
void ReleasePendingPrograms(LayerProgram& layer);
|
||||
void TryCommitPendingPrograms(LayerProgram& layer);
|
||||
bool EnsureLayerTargets(unsigned width, unsigned height);
|
||||
void DestroyLayerTargets();
|
||||
GLuint RenderLayer(LayerProgram& layer, uint64_t frameIndex, unsigned width, unsigned height, GLuint videoInputTexture, GLuint layerInputTexture, GLuint outputFramebuffer, bool renderToOutput);
|
||||
LayerProgram* FindLayer(const std::string& layerId);
|
||||
const LayerProgram* FindLayer(const std::string& layerId) const;
|
||||
LayerProgram::PassProgram* FindPass(LayerProgram& layer, const std::string& passId);
|
||||
static std::string Fingerprint(const RuntimeShaderArtifact& artifact);
|
||||
|
||||
RuntimeShaderPrepareWorker mPrepareWorker;
|
||||
std::vector<LayerProgram> mLayers;
|
||||
std::vector<std::string> mLayerOrder;
|
||||
GLuint mLayerFramebuffers[2] = {};
|
||||
GLuint mLayerTextures[2] = {};
|
||||
GLuint mLayerFramebuffers[4] = {};
|
||||
GLuint mLayerTextures[4] = {};
|
||||
unsigned mLayerTargetWidth = 0;
|
||||
unsigned mLayerTargetHeight = 0;
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -82,7 +82,10 @@ std::vector<unsigned char> BuildRuntimeShaderGlobalParamsStd140(
|
||||
|
||||
for (const ShaderParameterDefinition& definition : artifact.parameterDefinitions)
|
||||
{
|
||||
const ShaderParameterValue value = DefaultValueForDefinition(definition);
|
||||
const auto valueIt = artifact.parameterValues.find(definition.id);
|
||||
const ShaderParameterValue value = valueIt == artifact.parameterValues.end()
|
||||
? DefaultValueForDefinition(definition)
|
||||
: valueIt->second;
|
||||
switch (definition.type)
|
||||
{
|
||||
case ShaderParameterType::Float:
|
||||
@@ -109,8 +112,8 @@ std::vector<unsigned char> BuildRuntimeShaderGlobalParamsStd140(
|
||||
case ShaderParameterType::Text:
|
||||
break;
|
||||
case ShaderParameterType::Trigger:
|
||||
AppendStd140Int(buffer, 0);
|
||||
AppendStd140Float(buffer, -1000000.0f);
|
||||
AppendStd140Int(buffer, value.numberValues.empty() ? 0 : static_cast<int>(value.numberValues[0]));
|
||||
AppendStd140Float(buffer, value.numberValues.size() > 1 ? static_cast<float>(value.numberValues[1]) : -1000000.0f);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -77,20 +77,35 @@ void RuntimeShaderPrepareWorker::Submit(const std::vector<RenderCadenceComposito
|
||||
std::lock_guard<std::mutex> lock(mMutex);
|
||||
for (const RenderCadenceCompositor::RuntimeRenderLayerModel& layer : layers)
|
||||
{
|
||||
if (layer.artifact.fragmentShaderSource.empty())
|
||||
if (layer.artifact.passes.empty() && layer.artifact.fragmentShaderSource.empty())
|
||||
continue;
|
||||
|
||||
PrepareRequest request;
|
||||
request.layerId = layer.id;
|
||||
request.shaderId = layer.shaderId;
|
||||
request.sourceFingerprint = Fingerprint(layer.artifact);
|
||||
request.artifact = layer.artifact;
|
||||
std::vector<RuntimeShaderPassArtifact> passes = layer.artifact.passes;
|
||||
if (passes.empty())
|
||||
{
|
||||
RuntimeShaderPassArtifact pass;
|
||||
pass.passId = "main";
|
||||
pass.fragmentShaderSource = layer.artifact.fragmentShaderSource;
|
||||
pass.outputName = "layerOutput";
|
||||
passes.push_back(std::move(pass));
|
||||
}
|
||||
|
||||
auto sameLayer = [&request](const PrepareRequest& existing) {
|
||||
return existing.layerId == request.layerId;
|
||||
auto sameLayer = [&layer](const PrepareRequest& existing) {
|
||||
return existing.layerId == layer.id;
|
||||
};
|
||||
mRequests.erase(std::remove_if(mRequests.begin(), mRequests.end(), sameLayer), mRequests.end());
|
||||
mRequests.push_back(std::move(request));
|
||||
|
||||
for (const RuntimeShaderPassArtifact& pass : passes)
|
||||
{
|
||||
PrepareRequest request;
|
||||
request.layerId = layer.id;
|
||||
request.shaderId = layer.shaderId;
|
||||
request.passId = pass.passId;
|
||||
request.sourceFingerprint = Fingerprint(layer.artifact);
|
||||
request.artifact = layer.artifact;
|
||||
request.passArtifact = pass;
|
||||
mRequests.push_back(std::move(request));
|
||||
}
|
||||
}
|
||||
mCondition.notify_one();
|
||||
}
|
||||
@@ -137,10 +152,11 @@ void RuntimeShaderPrepareWorker::ThreadMain()
|
||||
}
|
||||
|
||||
RuntimePreparedShaderProgram preparedProgram;
|
||||
RuntimeShaderRenderer::BuildPreparedProgram(
|
||||
RuntimeShaderRenderer::BuildPreparedPassProgram(
|
||||
request.layerId,
|
||||
request.sourceFingerprint,
|
||||
request.artifact,
|
||||
request.passArtifact,
|
||||
preparedProgram);
|
||||
glFlush();
|
||||
|
||||
@@ -154,5 +170,10 @@ void RuntimeShaderPrepareWorker::ThreadMain()
|
||||
std::string RuntimeShaderPrepareWorker::Fingerprint(const RuntimeShaderArtifact& artifact)
|
||||
{
|
||||
const std::hash<std::string> hasher;
|
||||
return artifact.shaderId + ":" + std::to_string(artifact.fragmentShaderSource.size()) + ":" + std::to_string(hasher(artifact.fragmentShaderSource));
|
||||
std::string source;
|
||||
for (const RuntimeShaderPassArtifact& pass : artifact.passes)
|
||||
source += pass.passId + ":" + pass.outputName + ":" + pass.fragmentShaderSource + "\n";
|
||||
if (source.empty())
|
||||
source = artifact.fragmentShaderSource;
|
||||
return artifact.shaderId + ":" + std::to_string(source.size()) + ":" + std::to_string(hasher(source));
|
||||
}
|
||||
|
||||
@@ -35,8 +35,10 @@ private:
|
||||
{
|
||||
std::string layerId;
|
||||
std::string shaderId;
|
||||
std::string passId;
|
||||
std::string sourceFingerprint;
|
||||
RuntimeShaderArtifact artifact;
|
||||
RuntimeShaderPassArtifact passArtifact;
|
||||
};
|
||||
|
||||
void ThreadMain();
|
||||
|
||||
@@ -4,13 +4,18 @@
|
||||
#include "../../runtime/RuntimeShaderArtifact.h"
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
struct RuntimePreparedShaderProgram
|
||||
{
|
||||
std::string layerId;
|
||||
std::string shaderId;
|
||||
std::string passId;
|
||||
std::string sourceFingerprint;
|
||||
RuntimeShaderArtifact artifact;
|
||||
RuntimeShaderPassArtifact passArtifact;
|
||||
std::vector<std::string> inputNames;
|
||||
std::string outputName;
|
||||
GLuint program = 0;
|
||||
GLuint vertexShader = 0;
|
||||
GLuint fragmentShader = 0;
|
||||
|
||||
@@ -37,41 +37,6 @@ RuntimeShaderRenderer::~RuntimeShaderRenderer()
|
||||
ShutdownGl();
|
||||
}
|
||||
|
||||
bool RuntimeShaderRenderer::CommitFragmentShader(const std::string& fragmentShaderSource, std::string& error)
|
||||
{
|
||||
RuntimeShaderArtifact artifact;
|
||||
artifact.shaderId = "runtime-fragment";
|
||||
artifact.displayName = "Runtime Fragment";
|
||||
artifact.fragmentShaderSource = fragmentShaderSource;
|
||||
return CommitShaderArtifact(artifact, error);
|
||||
}
|
||||
|
||||
bool RuntimeShaderRenderer::CommitShaderArtifact(const RuntimeShaderArtifact& artifact, std::string& error)
|
||||
{
|
||||
if (artifact.fragmentShaderSource.empty())
|
||||
{
|
||||
error = "Cannot commit an empty fragment shader.";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!EnsureStaticGlResources(error))
|
||||
return false;
|
||||
|
||||
GLuint vertexShader = 0;
|
||||
GLuint fragmentShader = 0;
|
||||
GLuint program = 0;
|
||||
if (!BuildProgram(artifact.fragmentShaderSource, program, vertexShader, fragmentShader, error))
|
||||
return false;
|
||||
|
||||
DestroyProgram();
|
||||
mProgram = program;
|
||||
mVertexShader = vertexShader;
|
||||
mFragmentShader = fragmentShader;
|
||||
mArtifact = artifact;
|
||||
AssignSamplerUniforms(mProgram);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool RuntimeShaderRenderer::CommitPreparedProgram(RuntimePreparedShaderProgram& preparedProgram, std::string& error)
|
||||
{
|
||||
if (!preparedProgram.succeeded || preparedProgram.program == 0)
|
||||
@@ -94,26 +59,53 @@ bool RuntimeShaderRenderer::CommitPreparedProgram(RuntimePreparedShaderProgram&
|
||||
return true;
|
||||
}
|
||||
|
||||
void RuntimeShaderRenderer::UpdateArtifactState(const RuntimeShaderArtifact& artifact)
|
||||
{
|
||||
mArtifact.parameterDefinitions = artifact.parameterDefinitions;
|
||||
mArtifact.parameterValues = artifact.parameterValues;
|
||||
mArtifact.message = artifact.message;
|
||||
}
|
||||
|
||||
bool RuntimeShaderRenderer::BuildPreparedProgram(
|
||||
const std::string& layerId,
|
||||
const std::string& sourceFingerprint,
|
||||
const RuntimeShaderArtifact& artifact,
|
||||
RuntimePreparedShaderProgram& preparedProgram)
|
||||
{
|
||||
RuntimeShaderPassArtifact passArtifact;
|
||||
passArtifact.passId = "main";
|
||||
passArtifact.fragmentShaderSource = artifact.fragmentShaderSource;
|
||||
passArtifact.outputName = "layerOutput";
|
||||
if (!artifact.passes.empty())
|
||||
passArtifact = artifact.passes.front();
|
||||
return BuildPreparedPassProgram(layerId, sourceFingerprint, artifact, passArtifact, preparedProgram);
|
||||
}
|
||||
|
||||
bool RuntimeShaderRenderer::BuildPreparedPassProgram(
|
||||
const std::string& layerId,
|
||||
const std::string& sourceFingerprint,
|
||||
const RuntimeShaderArtifact& artifact,
|
||||
const RuntimeShaderPassArtifact& passArtifact,
|
||||
RuntimePreparedShaderProgram& preparedProgram)
|
||||
{
|
||||
preparedProgram = RuntimePreparedShaderProgram();
|
||||
preparedProgram.layerId = layerId;
|
||||
preparedProgram.shaderId = artifact.shaderId;
|
||||
preparedProgram.passId = passArtifact.passId;
|
||||
preparedProgram.sourceFingerprint = sourceFingerprint;
|
||||
preparedProgram.artifact = artifact;
|
||||
preparedProgram.passArtifact = passArtifact;
|
||||
preparedProgram.inputNames = passArtifact.inputNames;
|
||||
preparedProgram.outputName = passArtifact.outputName.empty() ? passArtifact.passId : passArtifact.outputName;
|
||||
|
||||
if (artifact.fragmentShaderSource.empty())
|
||||
if (passArtifact.fragmentShaderSource.empty())
|
||||
{
|
||||
preparedProgram.error = "Cannot prepare an empty fragment shader.";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!BuildProgram(
|
||||
artifact.fragmentShaderSource,
|
||||
passArtifact.fragmentShaderSource,
|
||||
preparedProgram.program,
|
||||
preparedProgram.vertexShader,
|
||||
preparedProgram.fragmentShader,
|
||||
|
||||
@@ -16,10 +16,9 @@ public:
|
||||
RuntimeShaderRenderer& operator=(const RuntimeShaderRenderer&) = delete;
|
||||
~RuntimeShaderRenderer();
|
||||
|
||||
bool CommitFragmentShader(const std::string& fragmentShaderSource, std::string& error);
|
||||
bool CommitShaderArtifact(const RuntimeShaderArtifact& artifact, std::string& error);
|
||||
bool CommitPreparedProgram(RuntimePreparedShaderProgram& preparedProgram, std::string& error);
|
||||
bool HasProgram() const { return mProgram != 0; }
|
||||
void UpdateArtifactState(const RuntimeShaderArtifact& artifact);
|
||||
void RenderFrame(uint64_t frameIndex, unsigned width, unsigned height, GLuint sourceTexture = 0, GLuint layerInputTexture = 0);
|
||||
void ShutdownGl();
|
||||
|
||||
@@ -28,6 +27,12 @@ public:
|
||||
const std::string& sourceFingerprint,
|
||||
const RuntimeShaderArtifact& artifact,
|
||||
RuntimePreparedShaderProgram& preparedProgram);
|
||||
static bool BuildPreparedPassProgram(
|
||||
const std::string& layerId,
|
||||
const std::string& sourceFingerprint,
|
||||
const RuntimeShaderArtifact& artifact,
|
||||
const RuntimeShaderPassArtifact& passArtifact,
|
||||
RuntimePreparedShaderProgram& preparedProgram);
|
||||
|
||||
private:
|
||||
bool EnsureStaticGlResources(std::string& error);
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
#include "RuntimeLayerModel.h"
|
||||
|
||||
#include "RuntimeParameterUtils.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <chrono>
|
||||
#include <utility>
|
||||
|
||||
namespace RenderCadenceCompositor
|
||||
@@ -26,6 +30,7 @@ bool RuntimeLayerModel::InitializeSingleLayer(const SupportedShaderCatalog& shad
|
||||
layer.shaderName = shaderPackage->displayName.empty() ? shaderPackage->id : shaderPackage->displayName;
|
||||
layer.buildState = RuntimeLayerBuildState::Pending;
|
||||
layer.message = "Runtime Slang build is waiting to start.";
|
||||
InitializeDefaultParameterValues(layer, *shaderPackage);
|
||||
mLayers.push_back(std::move(layer));
|
||||
error.clear();
|
||||
return true;
|
||||
@@ -46,6 +51,7 @@ bool RuntimeLayerModel::AddLayer(const SupportedShaderCatalog& shaderCatalog, co
|
||||
layer.shaderName = shaderPackage->displayName.empty() ? shaderPackage->id : shaderPackage->displayName;
|
||||
layer.buildState = RuntimeLayerBuildState::Pending;
|
||||
layer.message = "Runtime Slang build is waiting to start.";
|
||||
InitializeDefaultParameterValues(layer, *shaderPackage);
|
||||
layerId = layer.id;
|
||||
mLayers.push_back(std::move(layer));
|
||||
error.clear();
|
||||
@@ -68,6 +74,127 @@ bool RuntimeLayerModel::RemoveLayer(const std::string& layerId, std::string& err
|
||||
return false;
|
||||
}
|
||||
|
||||
bool RuntimeLayerModel::ReorderLayer(const std::string& layerId, int targetIndex, std::string& error)
|
||||
{
|
||||
auto layerIt = std::find_if(mLayers.begin(), mLayers.end(), [&layerId](const Layer& layer) {
|
||||
return layer.id == layerId;
|
||||
});
|
||||
if (layerIt == mLayers.end())
|
||||
{
|
||||
error = "Unknown runtime layer id: " + layerId;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (targetIndex < 0)
|
||||
targetIndex = 0;
|
||||
if (targetIndex >= static_cast<int>(mLayers.size()))
|
||||
targetIndex = static_cast<int>(mLayers.size()) - 1;
|
||||
|
||||
Layer layer = std::move(*layerIt);
|
||||
mLayers.erase(layerIt);
|
||||
std::size_t destinationIndex = static_cast<std::size_t>(targetIndex);
|
||||
if (destinationIndex > mLayers.size())
|
||||
destinationIndex = mLayers.size();
|
||||
mLayers.insert(mLayers.begin() + static_cast<std::ptrdiff_t>(destinationIndex), std::move(layer));
|
||||
error.clear();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool RuntimeLayerModel::SetLayerBypass(const std::string& layerId, bool bypass, std::string& error)
|
||||
{
|
||||
Layer* layer = FindLayer(layerId);
|
||||
if (!layer)
|
||||
{
|
||||
error = "Unknown runtime layer id: " + layerId;
|
||||
return false;
|
||||
}
|
||||
layer->bypass = bypass;
|
||||
error.clear();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool RuntimeLayerModel::SetLayerShader(const SupportedShaderCatalog& shaderCatalog, const std::string& layerId, const std::string& shaderId, std::string& error)
|
||||
{
|
||||
Layer* layer = FindLayer(layerId);
|
||||
if (!layer)
|
||||
{
|
||||
error = "Unknown runtime layer id: " + layerId;
|
||||
return false;
|
||||
}
|
||||
|
||||
const ShaderPackage* shaderPackage = shaderCatalog.FindPackage(shaderId);
|
||||
if (!shaderPackage)
|
||||
{
|
||||
error = "Shader '" + shaderId + "' is not in the supported shader catalog.";
|
||||
return false;
|
||||
}
|
||||
|
||||
layer->shaderId = shaderPackage->id;
|
||||
layer->shaderName = shaderPackage->displayName.empty() ? shaderPackage->id : shaderPackage->displayName;
|
||||
layer->buildState = RuntimeLayerBuildState::Pending;
|
||||
layer->message = "Runtime Slang build is waiting to start.";
|
||||
layer->renderReady = false;
|
||||
layer->artifact = RuntimeShaderArtifact();
|
||||
InitializeDefaultParameterValues(*layer, *shaderPackage);
|
||||
error.clear();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool RuntimeLayerModel::UpdateParameter(const std::string& layerId, const std::string& parameterId, const JsonValue& value, std::string& error)
|
||||
{
|
||||
Layer* layer = FindLayer(layerId);
|
||||
if (!layer)
|
||||
{
|
||||
error = "Unknown runtime layer id: " + layerId;
|
||||
return false;
|
||||
}
|
||||
|
||||
const ShaderParameterDefinition* definition = FindParameterDefinition(*layer, parameterId);
|
||||
if (!definition)
|
||||
{
|
||||
error = "Unknown parameter id '" + parameterId + "' for layer " + layerId + ".";
|
||||
return false;
|
||||
}
|
||||
|
||||
ShaderParameterValue normalizedValue;
|
||||
if (definition->type == ShaderParameterType::Trigger)
|
||||
{
|
||||
const auto currentIt = layer->parameterValues.find(parameterId);
|
||||
const double previousCount = currentIt == layer->parameterValues.end() || currentIt->second.numberValues.empty()
|
||||
? 0.0
|
||||
: currentIt->second.numberValues.front();
|
||||
normalizedValue.numberValues = { previousCount + 1.0, RuntimeElapsedSeconds() };
|
||||
}
|
||||
else if (!NormalizeAndValidateParameterValue(*definition, value, normalizedValue, error))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
layer->parameterValues[parameterId] = normalizedValue;
|
||||
if (layer->renderReady)
|
||||
layer->artifact.parameterValues = layer->parameterValues;
|
||||
error.clear();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool RuntimeLayerModel::ResetParameters(const std::string& layerId, std::string& error)
|
||||
{
|
||||
Layer* layer = FindLayer(layerId);
|
||||
if (!layer)
|
||||
{
|
||||
error = "Unknown runtime layer id: " + layerId;
|
||||
return false;
|
||||
}
|
||||
|
||||
layer->parameterValues.clear();
|
||||
for (const ShaderParameterDefinition& definition : layer->parameterDefinitions)
|
||||
layer->parameterValues[definition.id] = DefaultValueForDefinition(definition);
|
||||
if (layer->renderReady)
|
||||
layer->artifact.parameterValues = layer->parameterValues;
|
||||
error.clear();
|
||||
return true;
|
||||
}
|
||||
|
||||
void RuntimeLayerModel::Clear()
|
||||
{
|
||||
mLayers.clear();
|
||||
@@ -106,6 +233,7 @@ bool RuntimeLayerModel::MarkBuildReady(const RuntimeShaderArtifact& artifact, st
|
||||
layer->message = artifact.message;
|
||||
layer->renderReady = true;
|
||||
layer->artifact = artifact;
|
||||
layer->artifact.parameterValues = layer->parameterValues;
|
||||
error.clear();
|
||||
return true;
|
||||
}
|
||||
@@ -159,7 +287,9 @@ RuntimeLayerModelSnapshot RuntimeLayerModel::Snapshot() const
|
||||
RuntimeRenderLayerModel renderLayer;
|
||||
renderLayer.id = layer.id;
|
||||
renderLayer.shaderId = layer.shaderId;
|
||||
renderLayer.bypass = layer.bypass;
|
||||
renderLayer.artifact = layer.artifact;
|
||||
renderLayer.artifact.parameterValues = layer.parameterValues;
|
||||
snapshot.renderLayers.push_back(std::move(renderLayer));
|
||||
}
|
||||
}
|
||||
@@ -204,6 +334,24 @@ RuntimeLayerModel::Layer* RuntimeLayerModel::FindFirstLayerForShader(const std::
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
void RuntimeLayerModel::InitializeDefaultParameterValues(Layer& layer, const ShaderPackage& shaderPackage)
|
||||
{
|
||||
layer.parameterDefinitions = shaderPackage.parameters;
|
||||
layer.parameterValues.clear();
|
||||
for (const ShaderParameterDefinition& definition : layer.parameterDefinitions)
|
||||
layer.parameterValues[definition.id] = DefaultValueForDefinition(definition);
|
||||
}
|
||||
|
||||
const ShaderParameterDefinition* RuntimeLayerModel::FindParameterDefinition(const Layer& layer, const std::string& parameterId)
|
||||
{
|
||||
for (const ShaderParameterDefinition& definition : layer.parameterDefinitions)
|
||||
{
|
||||
if (definition.id == parameterId)
|
||||
return &definition;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
std::string RuntimeLayerModel::AllocateLayerId()
|
||||
{
|
||||
return "runtime-layer-" + std::to_string(mNextLayerNumber++);
|
||||
@@ -219,6 +367,13 @@ RuntimeLayerReadModel RuntimeLayerModel::ToReadModel(const Layer& layer)
|
||||
readModel.buildState = layer.buildState;
|
||||
readModel.message = layer.message;
|
||||
readModel.renderReady = layer.renderReady;
|
||||
readModel.parameterDefinitions = layer.parameterDefinitions;
|
||||
readModel.parameterValues = layer.parameterValues;
|
||||
return readModel;
|
||||
}
|
||||
|
||||
double RuntimeLayerModel::RuntimeElapsedSeconds() const
|
||||
{
|
||||
return std::chrono::duration_cast<std::chrono::duration<double>>(std::chrono::steady_clock::now() - mStartTime).count();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
#pragma once
|
||||
|
||||
#include "RuntimeJson.h"
|
||||
#include "RuntimeShaderArtifact.h"
|
||||
#include "SupportedShaderCatalog.h"
|
||||
|
||||
#include <chrono>
|
||||
#include <cstdint>
|
||||
#include <map>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
@@ -25,12 +28,15 @@ struct RuntimeLayerReadModel
|
||||
RuntimeLayerBuildState buildState = RuntimeLayerBuildState::Pending;
|
||||
std::string message;
|
||||
bool renderReady = false;
|
||||
std::vector<ShaderParameterDefinition> parameterDefinitions;
|
||||
std::map<std::string, ShaderParameterValue> parameterValues;
|
||||
};
|
||||
|
||||
struct RuntimeRenderLayerModel
|
||||
{
|
||||
std::string id;
|
||||
std::string shaderId;
|
||||
bool bypass = false;
|
||||
RuntimeShaderArtifact artifact;
|
||||
};
|
||||
|
||||
@@ -50,6 +56,11 @@ public:
|
||||
|
||||
bool AddLayer(const SupportedShaderCatalog& shaderCatalog, const std::string& shaderId, std::string& layerId, std::string& error);
|
||||
bool RemoveLayer(const std::string& layerId, std::string& error);
|
||||
bool ReorderLayer(const std::string& layerId, int targetIndex, std::string& error);
|
||||
bool SetLayerBypass(const std::string& layerId, bool bypass, std::string& error);
|
||||
bool SetLayerShader(const SupportedShaderCatalog& shaderCatalog, const std::string& layerId, const std::string& shaderId, std::string& error);
|
||||
bool UpdateParameter(const std::string& layerId, const std::string& parameterId, const JsonValue& value, std::string& error);
|
||||
bool ResetParameters(const std::string& layerId, std::string& error);
|
||||
bool MarkBuildStarted(const std::string& layerId, const std::string& message, std::string& error);
|
||||
bool MarkBuildReady(const RuntimeShaderArtifact& artifact, std::string& error);
|
||||
bool MarkBuildFailedForShader(const std::string& shaderId, const std::string& message);
|
||||
@@ -69,16 +80,22 @@ private:
|
||||
RuntimeLayerBuildState buildState = RuntimeLayerBuildState::Pending;
|
||||
std::string message;
|
||||
bool renderReady = false;
|
||||
std::vector<ShaderParameterDefinition> parameterDefinitions;
|
||||
std::map<std::string, ShaderParameterValue> parameterValues;
|
||||
RuntimeShaderArtifact artifact;
|
||||
};
|
||||
|
||||
Layer* FindLayer(const std::string& layerId);
|
||||
const Layer* FindLayer(const std::string& layerId) const;
|
||||
Layer* FindFirstLayerForShader(const std::string& shaderId);
|
||||
static void InitializeDefaultParameterValues(Layer& layer, const ShaderPackage& shaderPackage);
|
||||
static const ShaderParameterDefinition* FindParameterDefinition(const Layer& layer, const std::string& parameterId);
|
||||
std::string AllocateLayerId();
|
||||
static RuntimeLayerReadModel ToReadModel(const Layer& layer);
|
||||
double RuntimeElapsedSeconds() const;
|
||||
|
||||
std::vector<Layer> mLayers;
|
||||
uint64_t mNextLayerNumber = 1;
|
||||
std::chrono::steady_clock::time_point mStartTime = std::chrono::steady_clock::now();
|
||||
};
|
||||
}
|
||||
|
||||
@@ -2,15 +2,26 @@
|
||||
|
||||
#include "ShaderTypes.h"
|
||||
|
||||
#include <map>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
struct RuntimeShaderPassArtifact
|
||||
{
|
||||
std::string passId;
|
||||
std::string fragmentShaderSource;
|
||||
std::vector<std::string> inputNames;
|
||||
std::string outputName;
|
||||
};
|
||||
|
||||
struct RuntimeShaderArtifact
|
||||
{
|
||||
std::string layerId;
|
||||
std::string shaderId;
|
||||
std::string displayName;
|
||||
std::string fragmentShaderSource;
|
||||
std::vector<RuntimeShaderPassArtifact> passes;
|
||||
std::string message;
|
||||
std::vector<ShaderParameterDefinition> parameterDefinitions;
|
||||
std::map<std::string, ShaderParameterValue> parameterValues;
|
||||
};
|
||||
|
||||
@@ -112,8 +112,6 @@ RuntimeSlangShaderBuild RuntimeSlangShaderCompiler::BuildShader(const std::strin
|
||||
return build;
|
||||
}
|
||||
|
||||
const ShaderPassDefinition& pass = shaderPackage.passes.front();
|
||||
|
||||
ShaderCompiler compiler(
|
||||
repoRoot,
|
||||
runtimeBuildDir / (shaderId + ".wrapper.slang"),
|
||||
@@ -122,11 +120,22 @@ RuntimeSlangShaderBuild RuntimeSlangShaderCompiler::BuildShader(const std::strin
|
||||
0);
|
||||
|
||||
const auto start = std::chrono::steady_clock::now();
|
||||
if (!compiler.BuildPassFragmentShaderSource(shaderPackage, pass, build.artifact.fragmentShaderSource, error))
|
||||
for (const ShaderPassDefinition& pass : shaderPackage.passes)
|
||||
{
|
||||
build.succeeded = false;
|
||||
build.message = error.empty() ? "Slang compile failed." : error;
|
||||
return build;
|
||||
std::string fragmentShaderSource;
|
||||
if (!compiler.BuildPassFragmentShaderSource(shaderPackage, pass, fragmentShaderSource, error))
|
||||
{
|
||||
build.succeeded = false;
|
||||
build.message = error.empty() ? "Slang compile failed." : error;
|
||||
return build;
|
||||
}
|
||||
|
||||
RuntimeShaderPassArtifact passArtifact;
|
||||
passArtifact.passId = pass.id;
|
||||
passArtifact.fragmentShaderSource = std::move(fragmentShaderSource);
|
||||
passArtifact.inputNames = pass.inputNames;
|
||||
passArtifact.outputName = pass.outputName;
|
||||
build.artifact.passes.push_back(std::move(passArtifact));
|
||||
}
|
||||
|
||||
const auto end = std::chrono::steady_clock::now();
|
||||
@@ -135,6 +144,8 @@ RuntimeSlangShaderBuild RuntimeSlangShaderCompiler::BuildShader(const std::strin
|
||||
build.artifact.shaderId = shaderPackage.id;
|
||||
build.artifact.displayName = shaderPackage.displayName;
|
||||
build.artifact.parameterDefinitions = shaderPackage.parameters;
|
||||
if (!build.artifact.passes.empty())
|
||||
build.artifact.fragmentShaderSource = build.artifact.passes.front().fragmentShaderSource;
|
||||
build.artifact.message = shaderPackage.displayName + " Slang compile completed in " + std::to_string(milliseconds) + " ms.";
|
||||
build.message = build.artifact.message;
|
||||
return build;
|
||||
|
||||
@@ -9,8 +9,8 @@ namespace RenderCadenceCompositor
|
||||
{
|
||||
ShaderSupportResult CheckStatelessSinglePassShaderSupport(const ShaderPackage& shaderPackage)
|
||||
{
|
||||
if (shaderPackage.passes.size() != 1)
|
||||
return { false, "RenderCadenceCompositor currently supports only single-pass runtime shaders." };
|
||||
if (shaderPackage.passes.empty())
|
||||
return { false, "Shader package has no render passes." };
|
||||
|
||||
if (shaderPackage.temporal.enabled)
|
||||
return { false, "RenderCadenceCompositor currently supports only stateless shaders; temporal history is not enabled in this app." };
|
||||
@@ -30,6 +30,35 @@ ShaderSupportResult CheckStatelessSinglePassShaderSupport(const ShaderPackage& s
|
||||
return { false, "RenderCadenceCompositor currently skips text parameters because they require per-shader text texture storage." };
|
||||
}
|
||||
|
||||
bool writesLayerOutput = false;
|
||||
for (const ShaderPassDefinition& pass : shaderPackage.passes)
|
||||
{
|
||||
if (pass.sourcePath.empty())
|
||||
{
|
||||
return { false, "Shader pass '" + pass.id + "' has no source." };
|
||||
}
|
||||
if (pass.outputName == "layerOutput")
|
||||
writesLayerOutput = true;
|
||||
for (const std::string& inputName : pass.inputNames)
|
||||
{
|
||||
if (inputName == "videoInput" || inputName == "layerInput")
|
||||
continue;
|
||||
bool matchesNamedOutput = false;
|
||||
for (const ShaderPassDefinition& outputPass : shaderPackage.passes)
|
||||
{
|
||||
if (outputPass.outputName == inputName)
|
||||
{
|
||||
matchesNamedOutput = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!matchesNamedOutput)
|
||||
return { false, "Shader pass '" + pass.id + "' references unknown input '" + inputName + "'." };
|
||||
}
|
||||
}
|
||||
if (!writesLayerOutput)
|
||||
return { false, "Shader package must write a pass output named 'layerOutput'." };
|
||||
|
||||
return { true, std::string() };
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
#include <chrono>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
|
||||
namespace RenderCadenceCompositor
|
||||
{
|
||||
@@ -18,14 +19,46 @@ struct CadenceTelemetrySnapshot
|
||||
uint64_t scheduledTotal = 0;
|
||||
uint64_t completedPollMisses = 0;
|
||||
uint64_t scheduleFailures = 0;
|
||||
uint64_t completedDrops = 0;
|
||||
uint64_t acquireMisses = 0;
|
||||
uint64_t completions = 0;
|
||||
uint64_t displayedLate = 0;
|
||||
uint64_t dropped = 0;
|
||||
uint64_t clockOverruns = 0;
|
||||
uint64_t clockSkippedFrames = 0;
|
||||
uint64_t shaderBuildsCommitted = 0;
|
||||
uint64_t shaderBuildFailures = 0;
|
||||
double renderFrameMilliseconds = 0.0;
|
||||
double renderFrameBudgetUsedPercent = 0.0;
|
||||
double renderFrameMaxMilliseconds = 0.0;
|
||||
double readbackQueueMilliseconds = 0.0;
|
||||
double completedReadbackCopyMilliseconds = 0.0;
|
||||
uint64_t inputFramesReceived = 0;
|
||||
uint64_t inputFramesDropped = 0;
|
||||
uint64_t inputConsumeMisses = 0;
|
||||
uint64_t inputUploadMisses = 0;
|
||||
std::size_t inputReadyFrames = 0;
|
||||
std::size_t inputReadingFrames = 0;
|
||||
double inputLatestAgeMilliseconds = 0.0;
|
||||
double inputUploadMilliseconds = 0.0;
|
||||
bool inputFormatSupported = true;
|
||||
bool inputSignalPresent = false;
|
||||
double inputCaptureFps = 0.0;
|
||||
double inputConvertMilliseconds = 0.0;
|
||||
double inputSubmitMilliseconds = 0.0;
|
||||
uint64_t inputNoSignalFrames = 0;
|
||||
uint64_t inputUnsupportedFrames = 0;
|
||||
uint64_t inputSubmitMisses = 0;
|
||||
std::string inputCaptureFormat = "none";
|
||||
bool deckLinkBufferedAvailable = false;
|
||||
uint64_t deckLinkBuffered = 0;
|
||||
double deckLinkScheduleCallMilliseconds = 0.0;
|
||||
bool deckLinkScheduleLeadAvailable = false;
|
||||
int64_t deckLinkPlaybackStreamTime = 0;
|
||||
uint64_t deckLinkPlaybackFrameIndex = 0;
|
||||
uint64_t deckLinkNextScheduleFrameIndex = 0;
|
||||
int64_t deckLinkScheduleLeadFrames = 0;
|
||||
uint64_t deckLinkScheduleRealignments = 0;
|
||||
};
|
||||
|
||||
class CadenceTelemetry
|
||||
@@ -57,12 +90,20 @@ public:
|
||||
snapshot.scheduleFailures = outputMetrics.scheduleFailures > threadMetrics.scheduleFailures
|
||||
? outputMetrics.scheduleFailures
|
||||
: threadMetrics.scheduleFailures;
|
||||
snapshot.completedDrops = exchangeMetrics.completedDrops;
|
||||
snapshot.acquireMisses = exchangeMetrics.acquireMisses;
|
||||
snapshot.completions = outputMetrics.completions;
|
||||
snapshot.displayedLate = outputMetrics.displayedLate;
|
||||
snapshot.dropped = outputMetrics.dropped;
|
||||
snapshot.deckLinkBufferedAvailable = outputMetrics.actualBufferedFramesAvailable;
|
||||
snapshot.deckLinkBuffered = outputMetrics.actualBufferedFrames;
|
||||
snapshot.deckLinkScheduleCallMilliseconds = outputMetrics.scheduleCallMilliseconds;
|
||||
snapshot.deckLinkScheduleLeadAvailable = outputMetrics.scheduleLeadAvailable;
|
||||
snapshot.deckLinkPlaybackStreamTime = outputMetrics.playbackStreamTime;
|
||||
snapshot.deckLinkPlaybackFrameIndex = outputMetrics.playbackFrameIndex;
|
||||
snapshot.deckLinkNextScheduleFrameIndex = outputMetrics.nextScheduleFrameIndex;
|
||||
snapshot.deckLinkScheduleLeadFrames = outputMetrics.scheduleLeadFrames;
|
||||
snapshot.deckLinkScheduleRealignments = outputMetrics.scheduleRealignmentCount;
|
||||
|
||||
if (mHasLastSample && seconds > 0.0)
|
||||
{
|
||||
@@ -86,8 +127,47 @@ public:
|
||||
{
|
||||
CadenceTelemetrySnapshot snapshot = Sample(exchange, output, outputThread);
|
||||
const auto renderMetrics = renderThread.GetMetrics();
|
||||
snapshot.clockOverruns = renderMetrics.clockOverruns;
|
||||
snapshot.clockSkippedFrames = renderMetrics.skippedFrames;
|
||||
snapshot.shaderBuildsCommitted = renderMetrics.shaderBuildsCommitted;
|
||||
snapshot.shaderBuildFailures = renderMetrics.shaderBuildFailures;
|
||||
snapshot.renderFrameMilliseconds = renderMetrics.renderFrameMilliseconds;
|
||||
snapshot.renderFrameBudgetUsedPercent = renderMetrics.renderFrameBudgetUsedPercent;
|
||||
snapshot.renderFrameMaxMilliseconds = renderMetrics.renderFrameMaxMilliseconds;
|
||||
snapshot.readbackQueueMilliseconds = renderMetrics.readbackQueueMilliseconds;
|
||||
snapshot.completedReadbackCopyMilliseconds = renderMetrics.completedReadbackCopyMilliseconds;
|
||||
snapshot.inputFramesReceived = renderMetrics.inputFramesReceived;
|
||||
snapshot.inputFramesDropped = renderMetrics.inputFramesDropped;
|
||||
snapshot.inputConsumeMisses = renderMetrics.inputConsumeMisses;
|
||||
snapshot.inputUploadMisses = renderMetrics.inputUploadMisses;
|
||||
snapshot.inputReadyFrames = renderMetrics.inputReadyFrames;
|
||||
snapshot.inputReadingFrames = renderMetrics.inputReadingFrames;
|
||||
snapshot.inputLatestAgeMilliseconds = renderMetrics.inputLatestAgeMilliseconds;
|
||||
snapshot.inputUploadMilliseconds = renderMetrics.inputUploadMilliseconds;
|
||||
snapshot.inputFormatSupported = renderMetrics.inputFormatSupported;
|
||||
snapshot.inputSignalPresent = renderMetrics.inputSignalPresent;
|
||||
return snapshot;
|
||||
}
|
||||
|
||||
template <typename SystemFrameExchange, typename Output, typename OutputThread, typename RenderThread, typename InputEdge>
|
||||
CadenceTelemetrySnapshot Sample(
|
||||
const SystemFrameExchange& exchange,
|
||||
const Output& output,
|
||||
const OutputThread& outputThread,
|
||||
const RenderThread& renderThread,
|
||||
const InputEdge& inputEdge)
|
||||
{
|
||||
CadenceTelemetrySnapshot snapshot = Sample(exchange, output, outputThread, renderThread);
|
||||
const auto inputMetrics = inputEdge.Metrics();
|
||||
snapshot.inputConvertMilliseconds = inputMetrics.convertMilliseconds;
|
||||
snapshot.inputSubmitMilliseconds = inputMetrics.submitMilliseconds;
|
||||
snapshot.inputNoSignalFrames = inputMetrics.noInputSourceFrames;
|
||||
snapshot.inputUnsupportedFrames = inputMetrics.unsupportedFrames;
|
||||
snapshot.inputSubmitMisses = inputMetrics.submitMisses;
|
||||
snapshot.inputCaptureFormat = inputMetrics.captureFormat ? inputMetrics.captureFormat : "none";
|
||||
if (snapshot.sampleSeconds > 0.0)
|
||||
snapshot.inputCaptureFps = static_cast<double>(inputMetrics.capturedFrames - mLastInputCapturedFrames) / snapshot.sampleSeconds;
|
||||
mLastInputCapturedFrames = inputMetrics.capturedFrames;
|
||||
return snapshot;
|
||||
}
|
||||
|
||||
@@ -97,6 +177,7 @@ private:
|
||||
Clock::time_point mLastSampleTime = Clock::now();
|
||||
uint64_t mLastRenderedFrames = 0;
|
||||
uint64_t mLastScheduledFrames = 0;
|
||||
uint64_t mLastInputCapturedFrames = 0;
|
||||
bool mHasLastSample = false;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -21,11 +21,39 @@ inline void WriteCadenceTelemetryJson(JsonWriter& writer, const CadenceTelemetry
|
||||
writer.KeyUInt("scheduledTotal", snapshot.scheduledTotal);
|
||||
writer.KeyUInt("completedPollMisses", snapshot.completedPollMisses);
|
||||
writer.KeyUInt("scheduleFailures", snapshot.scheduleFailures);
|
||||
writer.KeyUInt("completedDrops", snapshot.completedDrops);
|
||||
writer.KeyUInt("acquireMisses", snapshot.acquireMisses);
|
||||
writer.KeyUInt("completions", snapshot.completions);
|
||||
writer.KeyUInt("late", snapshot.displayedLate);
|
||||
writer.KeyUInt("dropped", snapshot.dropped);
|
||||
writer.KeyUInt("clockOverruns", snapshot.clockOverruns);
|
||||
writer.KeyUInt("clockSkippedFrames", snapshot.clockSkippedFrames);
|
||||
writer.KeyUInt("clockOveruns", snapshot.clockOverruns);
|
||||
writer.KeyUInt("clockSkipped", snapshot.clockSkippedFrames);
|
||||
writer.KeyUInt("shaderCommitted", snapshot.shaderBuildsCommitted);
|
||||
writer.KeyUInt("shaderFailures", snapshot.shaderBuildFailures);
|
||||
writer.KeyDouble("renderFrameMs", snapshot.renderFrameMilliseconds);
|
||||
writer.KeyDouble("renderFrameBudgetUsedPercent", snapshot.renderFrameBudgetUsedPercent);
|
||||
writer.KeyDouble("renderFrameMaxMs", snapshot.renderFrameMaxMilliseconds);
|
||||
writer.KeyDouble("readbackQueueMs", snapshot.readbackQueueMilliseconds);
|
||||
writer.KeyDouble("completedReadbackCopyMs", snapshot.completedReadbackCopyMilliseconds);
|
||||
writer.KeyUInt("inputFramesReceived", snapshot.inputFramesReceived);
|
||||
writer.KeyUInt("inputFramesDropped", snapshot.inputFramesDropped);
|
||||
writer.KeyUInt("inputConsumeMisses", snapshot.inputConsumeMisses);
|
||||
writer.KeyUInt("inputUploadMisses", snapshot.inputUploadMisses);
|
||||
writer.KeyUInt("inputReadyFrames", static_cast<uint64_t>(snapshot.inputReadyFrames));
|
||||
writer.KeyUInt("inputReadingFrames", static_cast<uint64_t>(snapshot.inputReadingFrames));
|
||||
writer.KeyDouble("inputLatestAgeMs", snapshot.inputLatestAgeMilliseconds);
|
||||
writer.KeyDouble("inputUploadMs", snapshot.inputUploadMilliseconds);
|
||||
writer.KeyBool("inputFormatSupported", snapshot.inputFormatSupported);
|
||||
writer.KeyBool("inputSignalPresent", snapshot.inputSignalPresent);
|
||||
writer.KeyDouble("inputCaptureFps", snapshot.inputCaptureFps);
|
||||
writer.KeyDouble("inputConvertMs", snapshot.inputConvertMilliseconds);
|
||||
writer.KeyDouble("inputSubmitMs", snapshot.inputSubmitMilliseconds);
|
||||
writer.KeyUInt("inputNoSignalFrames", snapshot.inputNoSignalFrames);
|
||||
writer.KeyUInt("inputUnsupportedFrames", snapshot.inputUnsupportedFrames);
|
||||
writer.KeyUInt("inputSubmitMisses", snapshot.inputSubmitMisses);
|
||||
writer.KeyString("inputCaptureFormat", snapshot.inputCaptureFormat);
|
||||
writer.KeyBool("deckLinkBufferedAvailable", snapshot.deckLinkBufferedAvailable);
|
||||
writer.Key("deckLinkBuffered");
|
||||
if (snapshot.deckLinkBufferedAvailable)
|
||||
@@ -33,6 +61,16 @@ inline void WriteCadenceTelemetryJson(JsonWriter& writer, const CadenceTelemetry
|
||||
else
|
||||
writer.Null();
|
||||
writer.KeyDouble("scheduleCallMs", snapshot.deckLinkScheduleCallMilliseconds);
|
||||
writer.KeyBool("deckLinkScheduleLeadAvailable", snapshot.deckLinkScheduleLeadAvailable);
|
||||
writer.Key("deckLinkScheduleLeadFrames");
|
||||
if (snapshot.deckLinkScheduleLeadAvailable)
|
||||
writer.Int(snapshot.deckLinkScheduleLeadFrames);
|
||||
else
|
||||
writer.Null();
|
||||
writer.KeyUInt("deckLinkPlaybackFrameIndex", snapshot.deckLinkPlaybackFrameIndex);
|
||||
writer.KeyUInt("deckLinkNextScheduleFrameIndex", snapshot.deckLinkNextScheduleFrameIndex);
|
||||
writer.KeyInt("deckLinkPlaybackStreamTime", snapshot.deckLinkPlaybackStreamTime);
|
||||
writer.KeyUInt("deckLinkScheduleRealignments", snapshot.deckLinkScheduleRealignments);
|
||||
writer.EndObject();
|
||||
}
|
||||
|
||||
|
||||
@@ -78,7 +78,13 @@ private:
|
||||
message << "DeckLink reported frame timing issue: lateDelta=" << lateDelta
|
||||
<< " droppedDelta=" << droppedDelta
|
||||
<< " totalLate=" << snapshot.displayedLate
|
||||
<< " totalDropped=" << snapshot.dropped;
|
||||
<< " totalDropped=" << snapshot.dropped
|
||||
<< " scheduleLead=";
|
||||
if (snapshot.deckLinkScheduleLeadAvailable)
|
||||
message << snapshot.deckLinkScheduleLeadFrames;
|
||||
else
|
||||
message << "n/a";
|
||||
message << " realignments=" << snapshot.deckLinkScheduleRealignments;
|
||||
LogWarning("telemetry", message.str());
|
||||
}
|
||||
|
||||
|
||||
367
apps/RenderCadenceCompositor/video/DeckLinkInput.cpp
Normal file
367
apps/RenderCadenceCompositor/video/DeckLinkInput.cpp
Normal file
@@ -0,0 +1,367 @@
|
||||
#include "DeckLinkInput.h"
|
||||
|
||||
#include "DeckLinkVideoIOFormat.h"
|
||||
#include "../logging/Logger.h"
|
||||
|
||||
#include <chrono>
|
||||
#include <new>
|
||||
|
||||
namespace RenderCadenceCompositor
|
||||
{
|
||||
namespace
|
||||
{
|
||||
bool FindInputDisplayMode(IDeckLinkInput* input, BMDDisplayMode targetMode, IDeckLinkDisplayMode** foundMode)
|
||||
{
|
||||
if (input == nullptr || foundMode == nullptr)
|
||||
return false;
|
||||
|
||||
*foundMode = nullptr;
|
||||
CComPtr<IDeckLinkDisplayModeIterator> iterator;
|
||||
if (input->GetDisplayModeIterator(&iterator) != S_OK)
|
||||
return false;
|
||||
|
||||
return FindDeckLinkDisplayMode(iterator, targetMode, foundMode);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
DeckLinkInputCallback::DeckLinkInputCallback(DeckLinkInput& owner) :
|
||||
mOwner(owner)
|
||||
{
|
||||
}
|
||||
|
||||
HRESULT STDMETHODCALLTYPE DeckLinkInputCallback::QueryInterface(REFIID iid, LPVOID* ppv)
|
||||
{
|
||||
if (ppv == nullptr)
|
||||
return E_POINTER;
|
||||
if (iid == IID_IUnknown || iid == IID_IDeckLinkInputCallback)
|
||||
{
|
||||
*ppv = static_cast<IDeckLinkInputCallback*>(this);
|
||||
AddRef();
|
||||
return S_OK;
|
||||
}
|
||||
*ppv = nullptr;
|
||||
return E_NOINTERFACE;
|
||||
}
|
||||
|
||||
ULONG STDMETHODCALLTYPE DeckLinkInputCallback::AddRef()
|
||||
{
|
||||
return ++mRefCount;
|
||||
}
|
||||
|
||||
ULONG STDMETHODCALLTYPE DeckLinkInputCallback::Release()
|
||||
{
|
||||
const ULONG refCount = --mRefCount;
|
||||
if (refCount == 0)
|
||||
delete this;
|
||||
return refCount;
|
||||
}
|
||||
|
||||
HRESULT STDMETHODCALLTYPE DeckLinkInputCallback::VideoInputFrameArrived(IDeckLinkVideoInputFrame* videoFrame, IDeckLinkAudioInputPacket*)
|
||||
{
|
||||
if (videoFrame != nullptr)
|
||||
mOwner.HandleFrameArrived(videoFrame);
|
||||
return S_OK;
|
||||
}
|
||||
|
||||
HRESULT STDMETHODCALLTYPE DeckLinkInputCallback::VideoInputFormatChanged(BMDVideoInputFormatChangedEvents, IDeckLinkDisplayMode*, BMDDetectedVideoInputFormatFlags)
|
||||
{
|
||||
mOwner.HandleFormatChanged();
|
||||
return S_OK;
|
||||
}
|
||||
|
||||
DeckLinkInput::DeckLinkInput(InputFrameMailbox& mailbox) :
|
||||
mMailbox(mailbox)
|
||||
{
|
||||
}
|
||||
|
||||
DeckLinkInput::~DeckLinkInput()
|
||||
{
|
||||
ReleaseResources();
|
||||
}
|
||||
|
||||
bool DeckLinkInput::Initialize(const DeckLinkInputConfig& config, std::string& error)
|
||||
{
|
||||
ReleaseResources();
|
||||
mConfig = config;
|
||||
Log("decklink-input", "Initializing DeckLink input for " + config.videoFormat.displayName + ".");
|
||||
|
||||
if (!DiscoverInput(config, error))
|
||||
return false;
|
||||
|
||||
if (mInput->EnableVideoInput(config.videoFormat.displayMode, mCapturePixelFormat, bmdVideoInputFlagDefault) != S_OK)
|
||||
{
|
||||
error = "DeckLink input setup failed while enabling " +
|
||||
std::string(mCapturePixelFormat == bmdFormat8BitBGRA ? "BGRA8" : "UYVY8") +
|
||||
" input for " + config.videoFormat.displayName + ".";
|
||||
ReleaseResources();
|
||||
return false;
|
||||
}
|
||||
Log(
|
||||
"decklink-input",
|
||||
std::string("DeckLink input enabled in ") + (mCapturePixelFormat == bmdFormat8BitBGRA ? "BGRA8" : "UYVY8 raw capture") + " mode.");
|
||||
|
||||
mCallback.Attach(new (std::nothrow) DeckLinkInputCallback(*this));
|
||||
if (mCallback == nullptr)
|
||||
{
|
||||
error = "DeckLink input setup failed while creating the capture callback.";
|
||||
ReleaseResources();
|
||||
return false;
|
||||
}
|
||||
|
||||
if (mInput->SetCallback(mCallback) != S_OK)
|
||||
{
|
||||
error = "DeckLink input setup failed while installing the capture callback.";
|
||||
ReleaseResources();
|
||||
return false;
|
||||
}
|
||||
Log("decklink-input", "DeckLink input callback installed.");
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool DeckLinkInput::Start(std::string& error)
|
||||
{
|
||||
if (mInput == nullptr)
|
||||
{
|
||||
error = "DeckLink input has not been initialized.";
|
||||
return false;
|
||||
}
|
||||
if (mRunning.load(std::memory_order_acquire))
|
||||
return true;
|
||||
|
||||
if (mInput->StartStreams() != S_OK)
|
||||
{
|
||||
error = "DeckLink input stream failed to start.";
|
||||
return false;
|
||||
}
|
||||
mRunning.store(true, std::memory_order_release);
|
||||
Log("decklink-input", "DeckLink input stream started.");
|
||||
return true;
|
||||
}
|
||||
|
||||
void DeckLinkInput::Stop()
|
||||
{
|
||||
if (mInput != nullptr && mRunning.exchange(false, std::memory_order_acq_rel))
|
||||
{
|
||||
mInput->StopStreams();
|
||||
Log("decklink-input", "DeckLink input stream stopped.");
|
||||
}
|
||||
}
|
||||
|
||||
void DeckLinkInput::ReleaseResources()
|
||||
{
|
||||
Stop();
|
||||
if (mInput != nullptr)
|
||||
{
|
||||
mInput->SetCallback(nullptr);
|
||||
mInput->DisableVideoInput();
|
||||
}
|
||||
mCallback.Release();
|
||||
mInput.Release();
|
||||
}
|
||||
|
||||
DeckLinkInputMetrics DeckLinkInput::Metrics() const
|
||||
{
|
||||
DeckLinkInputMetrics metrics;
|
||||
metrics.capturedFrames = mCapturedFrames.load(std::memory_order_relaxed);
|
||||
metrics.noInputSourceFrames = mNoInputSourceFrames.load(std::memory_order_relaxed);
|
||||
metrics.unsupportedFrames = mUnsupportedFrames.load(std::memory_order_relaxed);
|
||||
metrics.submitMisses = mSubmitMisses.load(std::memory_order_relaxed);
|
||||
metrics.convertMilliseconds = mConvertMilliseconds.load(std::memory_order_relaxed);
|
||||
metrics.submitMilliseconds = mSubmitMilliseconds.load(std::memory_order_relaxed);
|
||||
metrics.captureFormat = CaptureFormatName();
|
||||
return metrics;
|
||||
}
|
||||
|
||||
VideoIOPixelFormat DeckLinkInput::CapturePixelFormat() const
|
||||
{
|
||||
return mCapturePixelFormat == bmdFormat8BitYUV ? VideoIOPixelFormat::Uyvy8 : VideoIOPixelFormat::Bgra8;
|
||||
}
|
||||
|
||||
void DeckLinkInput::HandleFrameArrived(IDeckLinkVideoInputFrame* inputFrame)
|
||||
{
|
||||
if (inputFrame == nullptr)
|
||||
return;
|
||||
|
||||
if ((inputFrame->GetFlags() & bmdFrameHasNoInputSource) == bmdFrameHasNoInputSource)
|
||||
{
|
||||
mNoInputSourceFrames.fetch_add(1, std::memory_order_relaxed);
|
||||
bool expected = false;
|
||||
if (mLoggedNoInputSource.compare_exchange_strong(expected, true, std::memory_order_relaxed))
|
||||
TryLog(LogLevel::Warning, "decklink-input", "DeckLink input callback reports no input source.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (inputFrame->GetWidth() != static_cast<long>(mMailbox.Config().width) ||
|
||||
inputFrame->GetHeight() != static_cast<long>(mMailbox.Config().height))
|
||||
{
|
||||
mUnsupportedFrames.fetch_add(1, std::memory_order_relaxed);
|
||||
bool expected = false;
|
||||
if (mLoggedUnsupportedFrame.compare_exchange_strong(expected, true, std::memory_order_relaxed))
|
||||
TryLog(LogLevel::Warning, "decklink-input", "DeckLink input frame dimensions do not match the configured mailbox.");
|
||||
return;
|
||||
}
|
||||
|
||||
CComPtr<IDeckLinkVideoBuffer> inputFrameBuffer;
|
||||
if (inputFrame->QueryInterface(IID_IDeckLinkVideoBuffer, reinterpret_cast<void**>(&inputFrameBuffer)) != S_OK)
|
||||
{
|
||||
mUnsupportedFrames.fetch_add(1, std::memory_order_relaxed);
|
||||
bool expected = false;
|
||||
if (mLoggedUnsupportedFrame.compare_exchange_strong(expected, true, std::memory_order_relaxed))
|
||||
TryLog(LogLevel::Warning, "decklink-input", "DeckLink input frame does not expose IDeckLinkVideoBuffer.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (inputFrameBuffer->StartAccess(bmdBufferAccessRead) != S_OK)
|
||||
{
|
||||
mUnsupportedFrames.fetch_add(1, std::memory_order_relaxed);
|
||||
bool expected = false;
|
||||
if (mLoggedUnsupportedFrame.compare_exchange_strong(expected, true, std::memory_order_relaxed))
|
||||
TryLog(LogLevel::Warning, "decklink-input", "DeckLink input frame buffer could not be opened for read access.");
|
||||
return;
|
||||
}
|
||||
|
||||
void* bytes = nullptr;
|
||||
inputFrameBuffer->GetBytes(&bytes);
|
||||
bool submitted = false;
|
||||
if (mCapturePixelFormat == bmdFormat8BitBGRA)
|
||||
submitted = SubmitBgra8Frame(inputFrame, bytes);
|
||||
else if (mCapturePixelFormat == bmdFormat8BitYUV)
|
||||
submitted = SubmitUyvy8Frame(inputFrame, bytes);
|
||||
else
|
||||
mUnsupportedFrames.fetch_add(1, std::memory_order_relaxed);
|
||||
|
||||
if (!submitted)
|
||||
{
|
||||
mSubmitMisses.fetch_add(1, std::memory_order_relaxed);
|
||||
bool expected = false;
|
||||
if (mLoggedSubmitMiss.compare_exchange_strong(expected, true, std::memory_order_relaxed))
|
||||
TryLog(LogLevel::Warning, "decklink-input", "DeckLink input frame could not be submitted to InputFrameMailbox.");
|
||||
}
|
||||
mCapturedFrames.fetch_add(1, std::memory_order_relaxed);
|
||||
bool expectedFirstFrame = false;
|
||||
if (mLoggedFirstFrame.compare_exchange_strong(expectedFirstFrame, true, std::memory_order_relaxed))
|
||||
{
|
||||
TryLog(
|
||||
LogLevel::Log,
|
||||
"decklink-input",
|
||||
std::string("First DeckLink ") + (mCapturePixelFormat == bmdFormat8BitBGRA ? "BGRA8" : "UYVY8 raw") + " input frame submitted to InputFrameMailbox.");
|
||||
}
|
||||
|
||||
inputFrameBuffer->EndAccess(bmdBufferAccessRead);
|
||||
}
|
||||
|
||||
void DeckLinkInput::HandleFormatChanged()
|
||||
{
|
||||
mUnsupportedFrames.fetch_add(1, std::memory_order_relaxed);
|
||||
TryLog(LogLevel::Warning, "decklink-input", "DeckLink input format changed; input edge does not auto-switch formats yet.");
|
||||
}
|
||||
|
||||
bool DeckLinkInput::DiscoverInput(const DeckLinkInputConfig& config, std::string& error)
|
||||
{
|
||||
CComPtr<IDeckLinkIterator> iterator;
|
||||
HRESULT result = CoCreateInstance(CLSID_CDeckLinkIterator, nullptr, CLSCTX_ALL, IID_IDeckLinkIterator, reinterpret_cast<void**>(&iterator));
|
||||
if (FAILED(result))
|
||||
{
|
||||
error = "DeckLink input discovery failed. Blackmagic DeckLink drivers may not be installed.";
|
||||
return false;
|
||||
}
|
||||
|
||||
CComPtr<IDeckLink> deckLink;
|
||||
while (iterator->Next(&deckLink) == S_OK)
|
||||
{
|
||||
CComPtr<IDeckLinkInput> candidateInput;
|
||||
if (deckLink->QueryInterface(IID_IDeckLinkInput, reinterpret_cast<void**>(&candidateInput)) == S_OK && candidateInput != nullptr)
|
||||
{
|
||||
CComPtr<IDeckLinkDisplayMode> displayMode;
|
||||
if (FindInputDisplayMode(candidateInput, config.videoFormat.displayMode, &displayMode) &&
|
||||
SupportsInputFormat(candidateInput, config.videoFormat.displayMode, bmdFormat8BitBGRA))
|
||||
{
|
||||
mInput = candidateInput;
|
||||
mCapturePixelFormat = bmdFormat8BitBGRA;
|
||||
Log("decklink-input", "DeckLink input device selected for BGRA8 capture.");
|
||||
return true;
|
||||
}
|
||||
if (displayMode != nullptr &&
|
||||
SupportsInputFormat(candidateInput, config.videoFormat.displayMode, bmdFormat8BitYUV))
|
||||
{
|
||||
mInput = candidateInput;
|
||||
mCapturePixelFormat = bmdFormat8BitYUV;
|
||||
Log("decklink-input", "DeckLink input device selected for UYVY8 raw capture with render-thread GPU decode.");
|
||||
return true;
|
||||
}
|
||||
}
|
||||
deckLink.Release();
|
||||
}
|
||||
|
||||
error = "No DeckLink input device supports BGRA8 or UYVY8 capture for " + config.videoFormat.displayName + ".";
|
||||
return false;
|
||||
}
|
||||
|
||||
bool DeckLinkInput::SupportsInputFormat(IDeckLinkInput* input, BMDDisplayMode displayMode, BMDPixelFormat pixelFormat) const
|
||||
{
|
||||
if (input == nullptr)
|
||||
return false;
|
||||
|
||||
BOOL supported = FALSE;
|
||||
BMDDisplayMode actualMode = bmdModeUnknown;
|
||||
const HRESULT result = input->DoesSupportVideoMode(
|
||||
bmdVideoConnectionUnspecified,
|
||||
displayMode,
|
||||
pixelFormat,
|
||||
bmdNoVideoInputConversion,
|
||||
bmdSupportedVideoModeDefault,
|
||||
&actualMode,
|
||||
&supported);
|
||||
return result == S_OK && supported != FALSE;
|
||||
}
|
||||
|
||||
bool DeckLinkInput::SubmitBgra8Frame(IDeckLinkVideoInputFrame* inputFrame, const void* bytes)
|
||||
{
|
||||
if (inputFrame == nullptr || bytes == nullptr)
|
||||
return false;
|
||||
const uint64_t frameIndex = mCapturedFrames.load(std::memory_order_relaxed);
|
||||
mConvertMilliseconds.store(0.0, std::memory_order_relaxed);
|
||||
const auto submitStart = std::chrono::steady_clock::now();
|
||||
const bool submitted = mMailbox.SubmitFrame(bytes, static_cast<unsigned>(inputFrame->GetRowBytes()), frameIndex);
|
||||
const auto submitEnd = std::chrono::steady_clock::now();
|
||||
mSubmitMilliseconds.store(
|
||||
std::chrono::duration_cast<std::chrono::duration<double, std::milli>>(submitEnd - submitStart).count(),
|
||||
std::memory_order_relaxed);
|
||||
return submitted;
|
||||
}
|
||||
|
||||
bool DeckLinkInput::SubmitUyvy8Frame(IDeckLinkVideoInputFrame* inputFrame, const void* bytes)
|
||||
{
|
||||
if (inputFrame == nullptr || bytes == nullptr)
|
||||
return false;
|
||||
|
||||
const unsigned width = static_cast<unsigned>(inputFrame->GetWidth());
|
||||
const unsigned height = static_cast<unsigned>(inputFrame->GetHeight());
|
||||
const long sourceRowBytes = inputFrame->GetRowBytes();
|
||||
if (width == 0 || height == 0 || sourceRowBytes < static_cast<long>(width * 2u))
|
||||
return false;
|
||||
|
||||
mConvertMilliseconds.store(0.0, std::memory_order_relaxed);
|
||||
const uint64_t frameIndex = mCapturedFrames.load(std::memory_order_relaxed);
|
||||
const auto submitStart = std::chrono::steady_clock::now();
|
||||
const bool submitted = mMailbox.SubmitFrame(bytes, static_cast<unsigned>(sourceRowBytes), frameIndex);
|
||||
const auto submitEnd = std::chrono::steady_clock::now();
|
||||
mSubmitMilliseconds.store(
|
||||
std::chrono::duration_cast<std::chrono::duration<double, std::milli>>(submitEnd - submitStart).count(),
|
||||
std::memory_order_relaxed);
|
||||
return submitted;
|
||||
}
|
||||
|
||||
const char* DeckLinkInput::CaptureFormatName() const
|
||||
{
|
||||
if (mInput == nullptr)
|
||||
return "none";
|
||||
if (mCapturePixelFormat == bmdFormat8BitBGRA)
|
||||
return "BGRA8";
|
||||
if (mCapturePixelFormat == bmdFormat8BitYUV)
|
||||
return "UYVY8";
|
||||
return "unsupported";
|
||||
}
|
||||
}
|
||||
96
apps/RenderCadenceCompositor/video/DeckLinkInput.h
Normal file
96
apps/RenderCadenceCompositor/video/DeckLinkInput.h
Normal file
@@ -0,0 +1,96 @@
|
||||
#pragma once
|
||||
|
||||
#include "../frames/InputFrameMailbox.h"
|
||||
#include "DeckLinkAPI_h.h"
|
||||
#include "DeckLinkDisplayMode.h"
|
||||
|
||||
#include <atlbase.h>
|
||||
|
||||
#include <atomic>
|
||||
#include <chrono>
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace RenderCadenceCompositor
|
||||
{
|
||||
struct DeckLinkInputConfig
|
||||
{
|
||||
VideoFormat videoFormat;
|
||||
};
|
||||
|
||||
struct DeckLinkInputMetrics
|
||||
{
|
||||
uint64_t capturedFrames = 0;
|
||||
uint64_t noInputSourceFrames = 0;
|
||||
uint64_t unsupportedFrames = 0;
|
||||
uint64_t submitMisses = 0;
|
||||
double convertMilliseconds = 0.0;
|
||||
double submitMilliseconds = 0.0;
|
||||
const char* captureFormat = "none";
|
||||
};
|
||||
|
||||
class DeckLinkInput;
|
||||
|
||||
class DeckLinkInputCallback final : public IDeckLinkInputCallback
|
||||
{
|
||||
public:
|
||||
explicit DeckLinkInputCallback(DeckLinkInput& owner);
|
||||
|
||||
HRESULT STDMETHODCALLTYPE QueryInterface(REFIID iid, LPVOID* ppv) override;
|
||||
ULONG STDMETHODCALLTYPE AddRef() override;
|
||||
ULONG STDMETHODCALLTYPE Release() override;
|
||||
HRESULT STDMETHODCALLTYPE VideoInputFrameArrived(IDeckLinkVideoInputFrame* videoFrame, IDeckLinkAudioInputPacket* audioPacket) override;
|
||||
HRESULT STDMETHODCALLTYPE VideoInputFormatChanged(BMDVideoInputFormatChangedEvents notificationEvents, IDeckLinkDisplayMode* newDisplayMode, BMDDetectedVideoInputFormatFlags detectedSignalFlags) override;
|
||||
|
||||
private:
|
||||
DeckLinkInput& mOwner;
|
||||
std::atomic<ULONG> mRefCount{ 1 };
|
||||
};
|
||||
|
||||
class DeckLinkInput
|
||||
{
|
||||
public:
|
||||
DeckLinkInput(InputFrameMailbox& mailbox);
|
||||
DeckLinkInput(const DeckLinkInput&) = delete;
|
||||
DeckLinkInput& operator=(const DeckLinkInput&) = delete;
|
||||
~DeckLinkInput();
|
||||
|
||||
bool Initialize(const DeckLinkInputConfig& config, std::string& error);
|
||||
bool Start(std::string& error);
|
||||
void Stop();
|
||||
void ReleaseResources();
|
||||
|
||||
bool IsInitialized() const { return mInput != nullptr; }
|
||||
bool IsRunning() const { return mRunning.load(std::memory_order_acquire); }
|
||||
VideoIOPixelFormat CapturePixelFormat() const;
|
||||
DeckLinkInputMetrics Metrics() const;
|
||||
|
||||
void HandleFrameArrived(IDeckLinkVideoInputFrame* inputFrame);
|
||||
void HandleFormatChanged();
|
||||
|
||||
private:
|
||||
bool DiscoverInput(const DeckLinkInputConfig& config, std::string& error);
|
||||
bool SupportsInputFormat(IDeckLinkInput* input, BMDDisplayMode displayMode, BMDPixelFormat pixelFormat) const;
|
||||
bool SubmitBgra8Frame(IDeckLinkVideoInputFrame* inputFrame, const void* bytes);
|
||||
bool SubmitUyvy8Frame(IDeckLinkVideoInputFrame* inputFrame, const void* bytes);
|
||||
const char* CaptureFormatName() const;
|
||||
|
||||
InputFrameMailbox& mMailbox;
|
||||
DeckLinkInputConfig mConfig;
|
||||
BMDPixelFormat mCapturePixelFormat = bmdFormat8BitBGRA;
|
||||
CComPtr<IDeckLinkInput> mInput;
|
||||
CComPtr<DeckLinkInputCallback> mCallback;
|
||||
std::atomic<bool> mRunning{ false };
|
||||
std::atomic<uint64_t> mCapturedFrames{ 0 };
|
||||
std::atomic<uint64_t> mNoInputSourceFrames{ 0 };
|
||||
std::atomic<uint64_t> mUnsupportedFrames{ 0 };
|
||||
std::atomic<uint64_t> mSubmitMisses{ 0 };
|
||||
std::atomic<double> mConvertMilliseconds{ 0.0 };
|
||||
std::atomic<double> mSubmitMilliseconds{ 0.0 };
|
||||
std::atomic<bool> mLoggedFirstFrame{ false };
|
||||
std::atomic<bool> mLoggedNoInputSource{ false };
|
||||
std::atomic<bool> mLoggedUnsupportedFrame{ false };
|
||||
std::atomic<bool> mLoggedSubmitMiss{ false };
|
||||
};
|
||||
}
|
||||
87
apps/RenderCadenceCompositor/video/DeckLinkInputThread.h
Normal file
87
apps/RenderCadenceCompositor/video/DeckLinkInputThread.h
Normal file
@@ -0,0 +1,87 @@
|
||||
#pragma once
|
||||
|
||||
#include "DeckLinkInput.h"
|
||||
|
||||
#include <atomic>
|
||||
#include <chrono>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
|
||||
namespace RenderCadenceCompositor
|
||||
{
|
||||
struct DeckLinkInputThreadConfig
|
||||
{
|
||||
std::chrono::milliseconds idleSleep = std::chrono::milliseconds(100);
|
||||
};
|
||||
|
||||
class DeckLinkInputThread
|
||||
{
|
||||
public:
|
||||
DeckLinkInputThread(DeckLinkInput& input, DeckLinkInputThreadConfig config = DeckLinkInputThreadConfig()) :
|
||||
mInput(input),
|
||||
mConfig(config)
|
||||
{
|
||||
}
|
||||
|
||||
DeckLinkInputThread(const DeckLinkInputThread&) = delete;
|
||||
DeckLinkInputThread& operator=(const DeckLinkInputThread&) = delete;
|
||||
|
||||
~DeckLinkInputThread()
|
||||
{
|
||||
Stop();
|
||||
}
|
||||
|
||||
bool Start(std::string& error)
|
||||
{
|
||||
if (mThread.joinable())
|
||||
return true;
|
||||
mStartSucceeded.store(false, std::memory_order_release);
|
||||
mStartCompleted.store(false, std::memory_order_release);
|
||||
mStopping.store(false, std::memory_order_release);
|
||||
mThread = std::thread([this]() { ThreadMain(); });
|
||||
|
||||
while (!mStartCompleted.load(std::memory_order_acquire))
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(1));
|
||||
|
||||
if (mStartSucceeded.load(std::memory_order_acquire))
|
||||
return true;
|
||||
|
||||
error = mStartError;
|
||||
Stop();
|
||||
return false;
|
||||
}
|
||||
|
||||
void Stop()
|
||||
{
|
||||
mStopping.store(true, std::memory_order_release);
|
||||
if (mThread.joinable())
|
||||
mThread.join();
|
||||
}
|
||||
|
||||
private:
|
||||
void ThreadMain()
|
||||
{
|
||||
std::string error;
|
||||
if (!mInput.Start(error))
|
||||
{
|
||||
mStartError = error;
|
||||
mStartCompleted.store(true, std::memory_order_release);
|
||||
return;
|
||||
}
|
||||
|
||||
mStartSucceeded.store(true, std::memory_order_release);
|
||||
mStartCompleted.store(true, std::memory_order_release);
|
||||
while (!mStopping.load(std::memory_order_acquire))
|
||||
std::this_thread::sleep_for(mConfig.idleSleep);
|
||||
mInput.Stop();
|
||||
}
|
||||
|
||||
DeckLinkInput& mInput;
|
||||
DeckLinkInputThreadConfig mConfig;
|
||||
std::thread mThread;
|
||||
std::atomic<bool> mStopping{ false };
|
||||
std::atomic<bool> mStartCompleted{ false };
|
||||
std::atomic<bool> mStartSucceeded{ false };
|
||||
std::string mStartError;
|
||||
};
|
||||
}
|
||||
@@ -15,6 +15,7 @@ bool DeckLinkOutput::Initialize(const DeckLinkOutputConfig& config, CompletionCa
|
||||
mCompletionCallback = completionCallback;
|
||||
|
||||
VideoFormatSelection formats;
|
||||
formats.output = config.outputVideoMode;
|
||||
if (!mSession.DiscoverDevicesAndModes(formats, error))
|
||||
return false;
|
||||
if (!mSession.SelectPreferredFormats(formats, config.outputAlphaRequired, error))
|
||||
@@ -76,6 +77,12 @@ DeckLinkOutputMetrics DeckLinkOutput::Metrics() const
|
||||
metrics.actualBufferedFramesAvailable = state.actualDeckLinkBufferedFramesAvailable;
|
||||
metrics.actualBufferedFrames = state.actualDeckLinkBufferedFrames;
|
||||
metrics.scheduleCallMilliseconds = state.deckLinkScheduleCallMilliseconds;
|
||||
metrics.scheduleLeadAvailable = state.deckLinkScheduleLeadAvailable;
|
||||
metrics.playbackStreamTime = state.deckLinkPlaybackStreamTime;
|
||||
metrics.playbackFrameIndex = state.deckLinkPlaybackFrameIndex;
|
||||
metrics.nextScheduleFrameIndex = state.deckLinkNextScheduleFrameIndex;
|
||||
metrics.scheduleLeadFrames = state.deckLinkScheduleLeadFrames;
|
||||
metrics.scheduleRealignmentCount = state.deckLinkScheduleRealignmentCount;
|
||||
return metrics;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#pragma once
|
||||
|
||||
#include "DeckLinkDisplayMode.h"
|
||||
#include "DeckLinkSession.h"
|
||||
#include "VideoIOTypes.h"
|
||||
|
||||
@@ -12,6 +13,7 @@ namespace RenderCadenceCompositor
|
||||
{
|
||||
struct DeckLinkOutputConfig
|
||||
{
|
||||
VideoFormat outputVideoMode;
|
||||
bool externalKeyingEnabled = false;
|
||||
bool outputAlphaRequired = false;
|
||||
};
|
||||
@@ -26,6 +28,12 @@ struct DeckLinkOutputMetrics
|
||||
bool actualBufferedFramesAvailable = false;
|
||||
uint64_t actualBufferedFrames = 0;
|
||||
double scheduleCallMilliseconds = 0.0;
|
||||
bool scheduleLeadAvailable = false;
|
||||
int64_t playbackStreamTime = 0;
|
||||
uint64_t playbackFrameIndex = 0;
|
||||
uint64_t nextScheduleFrameIndex = 0;
|
||||
int64_t scheduleLeadFrames = 0;
|
||||
uint64_t scheduleRealignmentCount = 0;
|
||||
};
|
||||
|
||||
class DeckLinkOutput
|
||||
|
||||
@@ -77,12 +77,15 @@ private:
|
||||
while (!mStopping)
|
||||
{
|
||||
const auto exchangeMetrics = mExchange.Metrics();
|
||||
if (exchangeMetrics.scheduledCount >= mConfig.targetBufferedFrames)
|
||||
const auto outputMetrics = mOutput.Metrics();
|
||||
const std::size_t bufferedFrames = outputMetrics.actualBufferedFramesAvailable
|
||||
? static_cast<std::size_t>(outputMetrics.actualBufferedFrames)
|
||||
: exchangeMetrics.scheduledCount;
|
||||
if (bufferedFrames >= mConfig.targetBufferedFrames)
|
||||
{
|
||||
std::this_thread::sleep_for(mConfig.idleSleep);
|
||||
continue;
|
||||
}
|
||||
|
||||
SystemFrame frame;
|
||||
if (!mExchange.ConsumeCompletedForSchedule(frame))
|
||||
{
|
||||
|
||||
@@ -10,7 +10,10 @@ The active plan for tightening render-thread ownership is:
|
||||
|
||||
The plan for building a fresh modular app around the proven probe architecture is:
|
||||
|
||||
- [New Render Cadence App Plan](NEW_RENDER_CADENCE_APP_PLAN.md)
|
||||
- [RenderCadenceCompositor README](../apps/RenderCadenceCompositor/README.md)
|
||||
- [Render Cadence Golden Rules](RENDER_CADENCE_GOLDEN_RULES.md)
|
||||
|
||||
`NEW_RENDER_CADENCE_APP_PLAN.md` remains as historical planning context, but the README and golden rules are the current contract for the new cadence-first app.
|
||||
|
||||
## Application Shape
|
||||
|
||||
@@ -287,7 +290,7 @@ Slots have four states:
|
||||
- `Completed`
|
||||
- `Scheduled`
|
||||
|
||||
Completed-but-unscheduled frames are treated as a latest-N cache. If render cadence needs space and old completed frames have not been scheduled, the oldest unscheduled completed frame can be recycled.
|
||||
In the current legacy app, completed-but-unscheduled frames are treated as a latest-N cache. The newer `RenderCadenceCompositor` uses a bounded FIFO completed reserve instead; see its README for the cadence-first contract.
|
||||
|
||||
Scheduled frames are protected until DeckLink reports completion.
|
||||
|
||||
@@ -295,7 +298,7 @@ Scheduled frames are protected until DeckLink reports completion.
|
||||
|
||||
`RenderOutputQueue` holds completed unscheduled output frames waiting to be scheduled.
|
||||
|
||||
It is bounded and latest-N:
|
||||
In the legacy app it is bounded and latest-N:
|
||||
|
||||
- pushing beyond capacity releases/drops the oldest ready frame
|
||||
- `DropOldestFrame()` is used when the frame pool needs to recycle old completed work
|
||||
@@ -363,7 +366,7 @@ The probe does not use the main runtime, shader system, preview path, input uplo
|
||||
- one OpenGL render thread with its own hidden GL context
|
||||
- simple BGRA8 motion rendering
|
||||
- async PBO readback
|
||||
- latest-N system-memory frame slots
|
||||
- legacy latest-N system-memory frame slots; bounded FIFO completed reserve in `RenderCadenceCompositor`
|
||||
- a playout thread that feeds DeckLink
|
||||
- real rendered warmup before scheduled playback
|
||||
|
||||
@@ -531,7 +534,7 @@ When `VST_DISABLE_INPUT_CAPTURE=1`, this flow is skipped.
|
||||
- Keep one owner for each kind of state.
|
||||
- Keep GL work on the render thread.
|
||||
- Keep DeckLink completion callbacks passive.
|
||||
- Treat completed unscheduled output frames as latest-N cache entries.
|
||||
- In the legacy app, treat completed unscheduled output frames as latest-N cache entries; in `RenderCadenceCompositor`, preserve completed frames as a bounded FIFO reserve.
|
||||
- Protect scheduled output frames until DeckLink completion.
|
||||
- Keep output timing more important than preview/screenshot.
|
||||
- Measure timing by domain instead of adding fallback branches blindly.
|
||||
|
||||
@@ -115,6 +115,24 @@ Lesson:
|
||||
- keep synthetic counters only as diagnostics
|
||||
- do not infer device health from internal stream indexes alone
|
||||
|
||||
### Schedule Cursor Recovery Must Be Conservative
|
||||
|
||||
The DeckLink schedule cursor should normally advance as a continuous stream timeline. Continuously realigning the next scheduled stream time to the sampled playback cursor can create its own timing fault: output may look like low FPS even when render and scheduling counters average 59.94/60 fps.
|
||||
|
||||
What worked better:
|
||||
|
||||
- use the exact DeckLink frame duration for the render cadence
|
||||
- keep healthy scheduling on a continuous stream cursor
|
||||
- measure schedule lead from DeckLink playback time versus the next schedule time
|
||||
- realign only after real pressure, such as a late/drop report or dangerously low measured lead
|
||||
- re-arm proactive realignment only after lead has recovered
|
||||
|
||||
Lesson:
|
||||
|
||||
- schedule recovery is an output-edge safety valve, not a per-frame timing policy
|
||||
- if recovery increments continuously, the recovery path has become the problem
|
||||
- include schedule lead and realignment count in telemetry/logs so drift is visible before guessing
|
||||
|
||||
### More Buffer Is Not Automatically Smoother
|
||||
|
||||
Increasing DeckLink scheduled frames sometimes made the reported device buffer look healthier while visible motion still stuttered.
|
||||
@@ -196,7 +214,7 @@ Lesson:
|
||||
|
||||
- system-memory slots are the contract between render and playout
|
||||
- scheduled slots must not be recycled early
|
||||
- completed-but-unscheduled slots can be latest-N cache entries
|
||||
- completed-but-unscheduled slots should form a bounded FIFO reserve for playout
|
||||
|
||||
### Startup Needs Real Preroll
|
||||
|
||||
@@ -222,18 +240,18 @@ Lesson:
|
||||
|
||||
The app has at least two important frame stores:
|
||||
|
||||
- system-memory completed/latest-N frames
|
||||
- system-memory completed FIFO reserve frames
|
||||
- DeckLink scheduled/device buffer
|
||||
|
||||
They have different ownership rules.
|
||||
|
||||
Completed-but-unscheduled frames are disposable if a newer frame is available and cadence needs the slot.
|
||||
Completed-but-unscheduled frames should be a bounded FIFO reserve for playout. If that reserve overflows, dropping the oldest completed frame is an app-side reserve policy and should be counted separately from DeckLink dropped frames.
|
||||
|
||||
Scheduled frames are not disposable because DeckLink may still read them.
|
||||
|
||||
Lesson:
|
||||
|
||||
- latest-N completed frames are a cache
|
||||
- completed frames waiting for playout are a bounded FIFO reserve
|
||||
- scheduled frames are owned by DeckLink until completion
|
||||
- keep metrics for both
|
||||
|
||||
@@ -246,7 +264,8 @@ That couples the clocks again.
|
||||
Lesson:
|
||||
|
||||
- render cadence should keep rendering at selected cadence
|
||||
- if completed cache is full, recycle/drop the oldest unscheduled completed frame
|
||||
- render acquire should not evict completed frames that are waiting for playout
|
||||
- if the completed reserve overflows, drop/count the oldest unscheduled completed frame
|
||||
- only scheduled/in-flight saturation should prevent rendering to a safe slot
|
||||
|
||||
## Render Thread Lessons
|
||||
@@ -282,6 +301,24 @@ Lesson:
|
||||
- test policies such as `one_before_output` or `skip_before_output`
|
||||
- prefer latest-input semantics over draining every pending upload
|
||||
|
||||
### CPU Input Conversion Can Be Worse Than Input Copy
|
||||
|
||||
When DeckLink input only exposed UYVY8 on the test machine, an initial CPU UYVY-to-BGRA conversion in the input callback measured around a full-frame budget on sampled runs and reduced input cadence dramatically.
|
||||
|
||||
Moving the input edge to raw UYVY8 capture changed the ownership:
|
||||
|
||||
- DeckLink callback copies raw supported input bytes into `InputFrameMailbox`
|
||||
- the mailbox keeps latest-frame semantics and uses a contiguous copy when row strides match
|
||||
- the render thread uploads/decodes UYVY8 into the shader-visible `gVideoInput` texture
|
||||
- runtime shaders continue to see decoded input, not packed capture bytes
|
||||
|
||||
Lesson:
|
||||
|
||||
- keep input callbacks as capture/copy edges
|
||||
- keep GL decode/upload in the render-owned path
|
||||
- measure input copy, upload, and decode separately
|
||||
- do not hide expensive format conversion inside the DeckLink callback
|
||||
|
||||
### Preview And Screenshot Must Stay Secondary
|
||||
|
||||
Preview is useful, but DeckLink output is the real-time path.
|
||||
@@ -322,7 +359,7 @@ The current direction is still sound:
|
||||
```text
|
||||
Render cadence loop
|
||||
renders at selected output cadence
|
||||
writes latest-N completed system-memory frames
|
||||
writes completed system-memory frames into a bounded FIFO reserve
|
||||
never sprints to refill DeckLink
|
||||
|
||||
Frame store
|
||||
@@ -369,7 +406,7 @@ A full rewrite becomes attractive only if the current GL ownership model cannot
|
||||
- Render cadence is time-driven, not completion-driven.
|
||||
- DeckLink scheduling is device-buffer-driven, not render-driven.
|
||||
- Completion callbacks release and report; they do not render.
|
||||
- System-memory completed frames are latest-N cache entries.
|
||||
- System-memory completed frames are a bounded FIFO reserve.
|
||||
- Scheduled frames are protected until DeckLink completion.
|
||||
- Startup uses real rendered warmup/preroll.
|
||||
- Black fallback is degraded/error behavior, not steady-state behavior.
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# New Render Cadence App Plan
|
||||
|
||||
Status: historical implementation plan. `apps/RenderCadenceCompositor` now exists; use [apps/RenderCadenceCompositor/README.md](../apps/RenderCadenceCompositor/README.md) and [Render Cadence Golden Rules](RENDER_CADENCE_GOLDEN_RULES.md) as the current implementation contract.
|
||||
|
||||
This plan describes a new application folder that rebuilds the output path from the proven `DeckLinkRenderCadenceProbe` architecture, but as a maintainable app foundation rather than a monolithic probe file.
|
||||
|
||||
The first goal is not to port the current compositor feature set. The first goal is to reproduce the probe's smooth 59.94/60 fps DeckLink output with clean module boundaries, tests where possible, and a structure that can later accept the shader/runtime/control systems without compromising timing.
|
||||
@@ -43,7 +45,7 @@ Render cadence thread
|
||||
|
||||
System frame exchange
|
||||
-> owns Free / Rendering / Completed / Scheduled slots
|
||||
-> latest-N semantics for completed unscheduled frames
|
||||
-> bounded FIFO reserve for completed unscheduled frames
|
||||
-> protects scheduled frames until DeckLink completion
|
||||
|
||||
DeckLink output thread
|
||||
@@ -63,7 +65,7 @@ Everything else must fit around that spine.
|
||||
- Completion callbacks never render.
|
||||
- No synchronous render request exists in the output path.
|
||||
- Preview, screenshot, input upload, shader rebuild, and runtime control cannot run ahead of a due output frame.
|
||||
- Completed unscheduled frames are latest-N and disposable.
|
||||
- Completed unscheduled frames are a bounded FIFO reserve; overflow drops are counted separately from DeckLink drops.
|
||||
- Scheduled frames are protected until DeckLink completion.
|
||||
- Startup warms up real rendered frames before scheduled playback starts.
|
||||
|
||||
@@ -77,7 +79,7 @@ Keep these behaviors from `DeckLinkRenderCadenceProbe`:
|
||||
- PBO ring readback
|
||||
- non-blocking fence polling with zero timeout
|
||||
- system-memory slots with `Free`, `Rendering`, `Completed`, `Scheduled`
|
||||
- drop oldest completed unscheduled frame if render needs space
|
||||
- preserve completed frames waiting for playout; drop/count the oldest completed frame only if the bounded reserve overflows
|
||||
- DeckLink playout thread only schedules completed frames
|
||||
- warmup completed frames before `StartScheduledPlayback()`
|
||||
- one-line-per-second timing telemetry
|
||||
@@ -430,7 +432,7 @@ Feature set:
|
||||
- simple motion renderer
|
||||
- BGRA8 only
|
||||
- PBO async readback
|
||||
- latest-N system-memory frame exchange
|
||||
- bounded FIFO system-memory frame exchange
|
||||
- warmup before playback
|
||||
- one-line telemetry
|
||||
|
||||
|
||||
@@ -48,6 +48,7 @@ The output/scheduling side may:
|
||||
- release frames after DeckLink completion
|
||||
- report late/dropped/schedule telemetry
|
||||
- record app-side poll misses
|
||||
- conservatively realign the DeckLink schedule cursor after measured timing pressure
|
||||
|
||||
It must not:
|
||||
|
||||
@@ -55,9 +56,12 @@ It must not:
|
||||
- invoke GL
|
||||
- compile shaders
|
||||
- block the render cadence waiting for DeckLink
|
||||
- continuously rewrite healthy scheduled timestamps
|
||||
|
||||
If no completed frame is available, record the miss and keep the ownership boundary intact.
|
||||
|
||||
DeckLink input is also an edge, not a renderer. It may capture/copy the latest supported CPU frame into an input mailbox and update input telemetry, but it must not call GL, schedule output, compile shaders, or drive render cadence. Format decode that requires GL belongs to the render-owned input upload path.
|
||||
|
||||
## 4. Runtime Build Work Produces Artifacts
|
||||
|
||||
Runtime shader work is split into two phases:
|
||||
@@ -91,9 +95,11 @@ Short mutex use for exchanging small already-prepared objects is acceptable. Hol
|
||||
|
||||
## 6. System Memory Frames Are A Handoff, Not A Render Driver
|
||||
|
||||
The system-memory frame exchange stores the latest rendered frames and protects frames scheduled to DeckLink.
|
||||
The system-memory frame exchange stores completed frames as a bounded FIFO reserve and protects frames scheduled to DeckLink.
|
||||
|
||||
It may drop old completed, unscheduled frames when the render thread needs a free slot. It must never force the render thread to wait for the output side to consume a frame.
|
||||
Render acquire must not evict completed frames that are waiting for playout, and it must never force the render thread to wait for the output side to consume a frame.
|
||||
|
||||
If the completed reserve overflows, the exchange may drop the oldest completed, unscheduled frame and record `completedDrops`. That is an app-side reserve drop, not a DeckLink dropped frame.
|
||||
|
||||
## 7. Startup Uses Warmup, Not Burst Rendering
|
||||
|
||||
@@ -112,6 +118,12 @@ Good examples:
|
||||
- `completedPollMisses`
|
||||
- `scheduleFailures`
|
||||
- `decklinkBuffered`
|
||||
- `deckLinkScheduleLeadFrames`
|
||||
- `deckLinkScheduleRealignments`
|
||||
- `inputCaptureFps`
|
||||
- `inputSubmitMs`
|
||||
- `inputUploadMs`
|
||||
- `inputConvertMs`
|
||||
- `shaderCommitted`
|
||||
- `shaderFailures`
|
||||
|
||||
|
||||
@@ -98,7 +98,7 @@ render cadence thread
|
||||
-> samples latest render input/state
|
||||
-> renders one frame
|
||||
-> queues async readback/copies completed readback into system-memory slot
|
||||
-> publishes completed frame to latest-N output buffer
|
||||
-> publishes completed frame to bounded FIFO output reserve
|
||||
|
||||
video output thread
|
||||
-> consumes completed system-memory frames
|
||||
|
||||
@@ -633,10 +633,12 @@ components:
|
||||
type: number
|
||||
renderMs:
|
||||
type: number
|
||||
description: Alias of cadence.renderFrameMs for clients that read the top-level performance object.
|
||||
smoothedRenderMs:
|
||||
type: number
|
||||
budgetUsedPercent:
|
||||
type: number
|
||||
description: Alias of cadence.renderFrameBudgetUsedPercent for clients that read the top-level performance object.
|
||||
completionIntervalMs:
|
||||
type: number
|
||||
smoothedCompletionIntervalMs:
|
||||
@@ -649,6 +651,93 @@ components:
|
||||
type: number
|
||||
flushedFrameCount:
|
||||
type: number
|
||||
cadence:
|
||||
$ref: "#/components/schemas/CadenceTelemetry"
|
||||
CadenceTelemetry:
|
||||
type: object
|
||||
properties:
|
||||
clockOverruns:
|
||||
type: number
|
||||
description: Render cadence overruns where the render thread was late enough to skip one or more frame intervals.
|
||||
clockSkippedFrames:
|
||||
type: number
|
||||
description: Total render cadence frame intervals skipped instead of catch-up rendering.
|
||||
clockOveruns:
|
||||
type: number
|
||||
deprecated: true
|
||||
description: Deprecated misspelled alias for clockOverruns.
|
||||
clockSkipped:
|
||||
type: number
|
||||
deprecated: true
|
||||
description: Deprecated alias for clockSkippedFrames.
|
||||
renderFrameMs:
|
||||
type: number
|
||||
description: Most recent render-thread frame draw duration in milliseconds, excluding completed-readback copy and readback queue work.
|
||||
renderFrameBudgetUsedPercent:
|
||||
type: number
|
||||
description: Most recent render-thread frame draw duration as a percentage of the selected frame budget.
|
||||
renderFrameMaxMs:
|
||||
type: number
|
||||
description: Maximum observed render-thread frame draw duration in milliseconds for this process.
|
||||
readbackQueueMs:
|
||||
type: number
|
||||
description: Most recent duration spent queueing BGRA8 async PBO readback after rendering.
|
||||
completedReadbackCopyMs:
|
||||
type: number
|
||||
description: Most recent duration spent mapping and copying a completed BGRA8 readback into system-memory frame storage.
|
||||
completedDrops:
|
||||
type: number
|
||||
description: Number of completed unscheduled system-memory frames dropped so render could reuse the slot.
|
||||
acquireMisses:
|
||||
type: number
|
||||
description: Number of times render/readback could not acquire a writable system-memory frame slot.
|
||||
inputFramesReceived:
|
||||
type: number
|
||||
inputFramesDropped:
|
||||
type: number
|
||||
inputConsumeMisses:
|
||||
type: number
|
||||
description: Render ticks where no ready input frame was available to upload.
|
||||
inputUploadMisses:
|
||||
type: number
|
||||
description: Input texture upload attempts that reused the previous GL input texture.
|
||||
inputReadyFrames:
|
||||
type: number
|
||||
description: Ready input frames currently queued in the input mailbox.
|
||||
inputReadingFrames:
|
||||
type: number
|
||||
description: Input frames currently protected while render uploads them.
|
||||
inputLatestAgeMs:
|
||||
type: number
|
||||
inputUploadMs:
|
||||
type: number
|
||||
inputCaptureFps:
|
||||
type: number
|
||||
inputConvertMs:
|
||||
type: number
|
||||
inputSubmitMs:
|
||||
type: number
|
||||
inputCaptureFormat:
|
||||
type: string
|
||||
deckLinkScheduleLeadAvailable:
|
||||
type: boolean
|
||||
description: Whether DeckLink playback stream-time lead telemetry is currently available.
|
||||
deckLinkScheduleLeadFrames:
|
||||
type: number
|
||||
nullable: true
|
||||
description: Estimated number of frame intervals between the next app schedule timestamp and the DeckLink playback frame index.
|
||||
deckLinkPlaybackFrameIndex:
|
||||
type: number
|
||||
description: DeckLink playback stream time converted to frame index at the configured output cadence.
|
||||
deckLinkNextScheduleFrameIndex:
|
||||
type: number
|
||||
description: Next frame index the app scheduler will assign to a DeckLink output frame.
|
||||
deckLinkPlaybackStreamTime:
|
||||
type: number
|
||||
description: Raw DeckLink scheduled playback stream time in the output mode time scale.
|
||||
deckLinkScheduleRealignments:
|
||||
type: number
|
||||
description: Count of schedule-cursor recovery realignments triggered by DeckLink late/drop pressure.
|
||||
BackendPlayoutStatus:
|
||||
type: object
|
||||
properties:
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#include "AppConfigProvider.h"
|
||||
|
||||
#include <chrono>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <iostream>
|
||||
@@ -104,6 +105,8 @@ void TestHelpers()
|
||||
|
||||
const double duration = FrameDurationMillisecondsFromRateString("50");
|
||||
Expect(duration > 19.9 && duration < 20.1, "frame duration parses numeric rate");
|
||||
const double deckLinkDuration = FrameDurationMillisecondsFromDisplayMode(bmdModeHD1080p5994, 0.0);
|
||||
Expect(deckLinkDuration > 16.6833 && deckLinkDuration < 16.6834, "DeckLink 59.94 display mode duration is exact");
|
||||
|
||||
const std::filesystem::path configPath = FindConfigFile();
|
||||
Expect(!configPath.empty(), "default config is discoverable from test working directory");
|
||||
|
||||
@@ -60,6 +60,27 @@ void TestLatePollRecordsSkippedFrames()
|
||||
Expect(cadence.SkippedFrameCount() == 3, "late poll accumulates skipped frames");
|
||||
}
|
||||
|
||||
void TestLatePollSkipsMissedIntervalsInsteadOfCatchingUp()
|
||||
{
|
||||
using Clock = RenderCadenceClock::Clock;
|
||||
RenderCadenceClock cadence(10.0);
|
||||
const auto start = Clock::now();
|
||||
cadence.Reset(start);
|
||||
|
||||
const auto late = start + std::chrono::milliseconds(35);
|
||||
const auto tick = cadence.Poll(late);
|
||||
Expect(tick.due, "late skipped-interval poll is due");
|
||||
Expect(tick.skippedFrames == 3, "late skipped-interval poll counts missed frames");
|
||||
|
||||
cadence.MarkRendered(late);
|
||||
Expect(cadence.NextRenderTime() > late, "late render schedules the next tick in the future");
|
||||
Expect(cadence.NextRenderTime() - late <= std::chrono::milliseconds(6), "late render does not leave catch-up frames due immediately");
|
||||
|
||||
const auto immediateFollowup = cadence.Poll(late);
|
||||
Expect(!immediateFollowup.due, "cadence does not allow an immediate catch-up render after a late frame");
|
||||
Expect(immediateFollowup.sleepFor > RenderCadenceClock::Duration::zero(), "cadence reports wait time after skipping missed intervals");
|
||||
}
|
||||
|
||||
void TestMarkRenderedRebasesAfterLargeStall()
|
||||
{
|
||||
using Clock = RenderCadenceClock::Clock;
|
||||
@@ -81,6 +102,7 @@ int main()
|
||||
TestEarlyPollWaitsWithoutAdvancing();
|
||||
TestDuePollRendersWithoutSkipping();
|
||||
TestLatePollRecordsSkippedFrames();
|
||||
TestLatePollSkipsMissedIntervalsInsteadOfCatchingUp();
|
||||
TestMarkRenderedRebasesAfterLargeStall();
|
||||
|
||||
if (gFailures != 0)
|
||||
|
||||
@@ -27,6 +27,13 @@ SystemFrameExchangeConfig MakeConfig(std::size_t capacity = 2)
|
||||
return config;
|
||||
}
|
||||
|
||||
SystemFrameExchangeConfig MakeBoundedCompletedConfig(std::size_t capacity = 4, std::size_t maxCompletedFrames = 2)
|
||||
{
|
||||
SystemFrameExchangeConfig config = MakeConfig(capacity);
|
||||
config.maxCompletedFrames = maxCompletedFrames;
|
||||
return config;
|
||||
}
|
||||
|
||||
void TestAcquirePublishesAndSchedules()
|
||||
{
|
||||
SystemFrameExchange exchange(MakeConfig(1));
|
||||
@@ -57,32 +64,54 @@ void TestAcquirePublishesAndSchedules()
|
||||
Expect(metrics.scheduledFrames == 1, "scheduled metric is counted");
|
||||
}
|
||||
|
||||
void TestAcquireDropsOldestCompletedUnscheduled()
|
||||
void TestAcquirePreservesCompletedFrames()
|
||||
{
|
||||
SystemFrameExchange exchange(MakeConfig(2));
|
||||
|
||||
SystemFrame first;
|
||||
SystemFrame second;
|
||||
SystemFrame third;
|
||||
Expect(exchange.AcquireForRender(first), "first frame can be acquired");
|
||||
Expect(exchange.AcquireForRender(first), "first preserving frame can be acquired");
|
||||
first.frameIndex = 1;
|
||||
Expect(exchange.PublishCompleted(first), "first frame can be completed");
|
||||
Expect(exchange.AcquireForRender(second), "second frame can be acquired");
|
||||
Expect(exchange.PublishCompleted(first), "first preserving frame can be completed");
|
||||
Expect(exchange.AcquireForRender(second), "second preserving frame can be acquired");
|
||||
second.frameIndex = 2;
|
||||
Expect(exchange.PublishCompleted(second), "second frame can be completed");
|
||||
Expect(exchange.PublishCompleted(second), "second preserving frame can be completed");
|
||||
|
||||
Expect(exchange.AcquireForRender(third), "third acquire drops the oldest completed frame");
|
||||
Expect(third.index == first.index, "oldest completed slot is reused");
|
||||
Expect(!exchange.AcquireForRender(third), "render acquire refuses to drop completed frames");
|
||||
|
||||
SystemFrame scheduled;
|
||||
Expect(exchange.ConsumeCompletedForSchedule(scheduled), "remaining completed frame can be scheduled");
|
||||
Expect(scheduled.index == second.index, "newer completed frame survives drop");
|
||||
Expect(scheduled.frameIndex == 2, "newer frame index survives drop");
|
||||
Expect(exchange.ConsumeCompletedForSchedule(scheduled), "oldest completed frame survives acquire miss");
|
||||
Expect(scheduled.frameIndex == 1, "preserving acquire keeps FIFO output continuity");
|
||||
|
||||
SystemFrameExchangeMetrics metrics = exchange.Metrics();
|
||||
Expect(metrics.completedDrops == 1, "drop metric is counted");
|
||||
Expect(metrics.renderingCount == 1, "reused slot is rendering");
|
||||
Expect(metrics.scheduledCount == 1, "consumed slot is scheduled");
|
||||
Expect(metrics.completedDrops == 0, "preserving acquire does not count completed drops");
|
||||
Expect(metrics.acquireMisses == 1, "preserving acquire miss is counted");
|
||||
}
|
||||
|
||||
void TestCompletedReserveIsBoundedFifo()
|
||||
{
|
||||
SystemFrameExchange exchange(MakeBoundedCompletedConfig(4, 2));
|
||||
|
||||
for (uint64_t frameIndex = 1; frameIndex <= 3; ++frameIndex)
|
||||
{
|
||||
SystemFrame frame;
|
||||
Expect(exchange.AcquireForRender(frame), "bounded reserve frame can be acquired");
|
||||
frame.frameIndex = frameIndex;
|
||||
Expect(exchange.PublishCompleted(frame), "bounded reserve frame can be completed");
|
||||
}
|
||||
|
||||
SystemFrame firstScheduled;
|
||||
Expect(exchange.ConsumeCompletedForSchedule(firstScheduled), "bounded reserve oldest retained frame can be scheduled");
|
||||
Expect(firstScheduled.frameIndex == 2, "bounded reserve drops oldest overflow and keeps FIFO order");
|
||||
|
||||
SystemFrame secondScheduled;
|
||||
Expect(exchange.ConsumeCompletedForSchedule(secondScheduled), "bounded reserve second retained frame can be scheduled");
|
||||
Expect(secondScheduled.frameIndex == 3, "bounded reserve schedules next retained frame");
|
||||
|
||||
SystemFrameExchangeMetrics metrics = exchange.Metrics();
|
||||
Expect(metrics.completedDrops == 1, "bounded completed reserve records oldest overflow drop");
|
||||
Expect(metrics.scheduledFrames == 2, "bounded reserve schedules retained frames");
|
||||
}
|
||||
|
||||
void TestScheduledFramesAreNotDropped()
|
||||
@@ -154,16 +183,39 @@ void TestCompletedPollMissIsCounted()
|
||||
SystemFrameExchangeMetrics metrics = exchange.Metrics();
|
||||
Expect(metrics.completedPollMisses == 1, "completed poll miss is counted");
|
||||
}
|
||||
|
||||
void TestStableCompletedDepthCanBeObserved()
|
||||
{
|
||||
SystemFrameExchange exchange(MakeConfig(1));
|
||||
SystemFrame frame;
|
||||
Expect(exchange.AcquireForRender(frame), "stable-depth frame can be acquired");
|
||||
Expect(exchange.PublishCompleted(frame), "stable-depth frame can be completed");
|
||||
|
||||
Expect(
|
||||
exchange.WaitForStableCompletedDepth(1, std::chrono::milliseconds(1), std::chrono::milliseconds(50)),
|
||||
"stable completed depth can be observed");
|
||||
}
|
||||
|
||||
void TestStableCompletedDepthTimesOut()
|
||||
{
|
||||
SystemFrameExchange exchange(MakeConfig(1));
|
||||
Expect(
|
||||
!exchange.WaitForStableCompletedDepth(1, std::chrono::milliseconds(1), std::chrono::milliseconds(1)),
|
||||
"missing stable completed depth times out");
|
||||
}
|
||||
}
|
||||
|
||||
int main()
|
||||
{
|
||||
TestAcquirePublishesAndSchedules();
|
||||
TestAcquireDropsOldestCompletedUnscheduled();
|
||||
TestAcquirePreservesCompletedFrames();
|
||||
TestCompletedReserveIsBoundedFifo();
|
||||
TestScheduledFramesAreNotDropped();
|
||||
TestGenerationValidationRejectsStaleFrames();
|
||||
TestPixelFormatAwareSizing();
|
||||
TestCompletedPollMissIsCounted();
|
||||
TestStableCompletedDepthCanBeObserved();
|
||||
TestStableCompletedDepthTimesOut();
|
||||
|
||||
if (gFailures != 0)
|
||||
{
|
||||
|
||||
@@ -147,6 +147,29 @@ void TestLayerPostEndpointsUseCallbacks()
|
||||
Expect(removeResponse.body.find("Unknown layer id.") != std::string::npos, "remove layer callback returns diagnostic");
|
||||
}
|
||||
|
||||
void TestGenericPostCallbackHandlesControlRoutes()
|
||||
{
|
||||
using namespace RenderCadenceCompositor;
|
||||
|
||||
HttpControlServer server;
|
||||
HttpControlServerCallbacks callbacks;
|
||||
callbacks.executePost = [](const std::string& path, const std::string& body) {
|
||||
ExpectEquals(path, "/api/layers/set-bypass", "generic callback receives route path");
|
||||
Expect(body.find("runtime-layer-1") != std::string::npos, "generic callback receives request body");
|
||||
return ControlActionResult{ true, std::string() };
|
||||
};
|
||||
server.SetCallbacksForTest(callbacks);
|
||||
|
||||
HttpControlServer::HttpRequest request;
|
||||
request.method = "POST";
|
||||
request.path = "/api/layers/set-bypass";
|
||||
request.body = "{\"layerId\":\"runtime-layer-1\",\"bypass\":true}";
|
||||
|
||||
const HttpControlServer::HttpResponse response = server.RouteRequestForTest(request);
|
||||
ExpectEquals(response.status, "200 OK", "generic control callback success returns 200");
|
||||
Expect(response.body.find("\"ok\":true") != std::string::npos, "generic control callback returns action success");
|
||||
}
|
||||
|
||||
void TestUnknownEndpointReturns404()
|
||||
{
|
||||
using namespace RenderCadenceCompositor;
|
||||
@@ -169,6 +192,7 @@ int main()
|
||||
TestRootServesUiIndex();
|
||||
TestKnownPostEndpointReturnsActionError();
|
||||
TestLayerPostEndpointsUseCallbacks();
|
||||
TestGenericPostCallbackHandlesControlRoutes();
|
||||
TestUnknownEndpointReturns404();
|
||||
|
||||
if (gFailures != 0)
|
||||
|
||||
137
tests/RenderCadenceCompositorInputFrameMailboxTests.cpp
Normal file
137
tests/RenderCadenceCompositorInputFrameMailboxTests.cpp
Normal file
@@ -0,0 +1,137 @@
|
||||
#include "InputFrameMailbox.h"
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <iostream>
|
||||
#include <vector>
|
||||
|
||||
namespace
|
||||
{
|
||||
int gFailures = 0;
|
||||
|
||||
void Expect(bool condition, const char* message)
|
||||
{
|
||||
if (condition)
|
||||
return;
|
||||
|
||||
++gFailures;
|
||||
std::cerr << "FAIL: " << message << "\n";
|
||||
}
|
||||
|
||||
InputFrameMailboxConfig MakeConfig(std::size_t capacity = 2)
|
||||
{
|
||||
InputFrameMailboxConfig config;
|
||||
config.width = 2;
|
||||
config.height = 2;
|
||||
config.pixelFormat = VideoIOPixelFormat::Bgra8;
|
||||
config.rowBytes = VideoIORowBytes(config.pixelFormat, config.width);
|
||||
config.capacity = capacity;
|
||||
return config;
|
||||
}
|
||||
|
||||
InputFrameMailboxConfig MakeBufferedConfig(std::size_t capacity = 4, std::size_t maxReadyFrames = 2)
|
||||
{
|
||||
InputFrameMailboxConfig config = MakeConfig(capacity);
|
||||
config.maxReadyFrames = maxReadyFrames;
|
||||
return config;
|
||||
}
|
||||
|
||||
std::vector<unsigned char> MakeFrame(unsigned char value)
|
||||
{
|
||||
return std::vector<unsigned char>(16, value);
|
||||
}
|
||||
|
||||
void TestSubmitDropsOldestWhenFull()
|
||||
{
|
||||
InputFrameMailbox mailbox(MakeConfig(2));
|
||||
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 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");
|
||||
|
||||
InputFrame oldest;
|
||||
Expect(mailbox.TryAcquireOldest(oldest), "oldest frame available after drop");
|
||||
Expect(oldest.frameIndex == 2, "bounded mailbox keeps FIFO order after trimming oldest overflow");
|
||||
Expect(mailbox.Release(oldest), "oldest frame releases");
|
||||
|
||||
const InputFrameMailboxMetrics metrics = mailbox.Metrics();
|
||||
Expect(metrics.droppedReadyFrames >= 1, "full mailbox records ready drop");
|
||||
Expect(metrics.submitMisses == 0, "full mailbox did not block producer when ready slots were disposable");
|
||||
}
|
||||
|
||||
void TestReadingFrameIsProtected()
|
||||
{
|
||||
InputFrameMailbox mailbox(MakeConfig(1));
|
||||
const std::vector<unsigned char> frame1 = MakeFrame(1);
|
||||
const std::vector<unsigned char> frame2 = MakeFrame(2);
|
||||
|
||||
Expect(mailbox.SubmitFrame(frame1.data(), 8, 1), "protected frame submits");
|
||||
InputFrame acquired;
|
||||
Expect(mailbox.TryAcquireOldest(acquired), "protected frame acquired");
|
||||
Expect(!mailbox.SubmitFrame(frame2.data(), 8, 2), "producer cannot overwrite frame currently being read");
|
||||
Expect(mailbox.Release(acquired), "protected frame releases");
|
||||
Expect(mailbox.SubmitFrame(frame2.data(), 8, 2), "producer can submit after release");
|
||||
}
|
||||
|
||||
void TestAcquireOldestConsumesFifoWithoutDroppingReadyFrames()
|
||||
{
|
||||
InputFrameMailbox mailbox(MakeConfig(3));
|
||||
const std::vector<unsigned char> frame1 = MakeFrame(1);
|
||||
const std::vector<unsigned char> frame2 = MakeFrame(2);
|
||||
|
||||
Expect(mailbox.SubmitFrame(frame1.data(), 8, 1), "fifo first frame submits");
|
||||
Expect(mailbox.SubmitFrame(frame2.data(), 8, 2), "fifo second frame submits");
|
||||
|
||||
InputFrame acquired;
|
||||
Expect(mailbox.TryAcquireOldest(acquired), "fifo oldest frame acquired");
|
||||
Expect(acquired.frameIndex == 1, "fifo acquire returns oldest frame");
|
||||
Expect(mailbox.Release(acquired), "fifo acquired frame releases");
|
||||
|
||||
const InputFrameMailboxMetrics metrics = mailbox.Metrics();
|
||||
Expect(metrics.readyCount == 1, "fifo acquire leaves newer frame ready");
|
||||
Expect(metrics.droppedReadyFrames == 0, "fifo acquire does not drop newer ready frame");
|
||||
}
|
||||
|
||||
void TestMaxReadyFramesKeepsConfiguredInputBuffer()
|
||||
{
|
||||
InputFrameMailbox mailbox(MakeBufferedConfig(4, 3));
|
||||
const std::vector<unsigned char> frame1 = MakeFrame(1);
|
||||
const std::vector<unsigned char> frame2 = MakeFrame(2);
|
||||
const std::vector<unsigned char> frame3 = MakeFrame(3);
|
||||
const std::vector<unsigned char> frame4 = MakeFrame(4);
|
||||
|
||||
Expect(mailbox.SubmitFrame(frame1.data(), 8, 1), "bounded first frame submits");
|
||||
Expect(mailbox.SubmitFrame(frame2.data(), 8, 2), "bounded second frame submits");
|
||||
Expect(mailbox.SubmitFrame(frame3.data(), 8, 3), "bounded third frame submits");
|
||||
Expect(mailbox.SubmitFrame(frame4.data(), 8, 4), "bounded fourth frame submits");
|
||||
|
||||
InputFrame acquired;
|
||||
Expect(mailbox.TryAcquireOldest(acquired), "bounded oldest available frame acquired");
|
||||
Expect(acquired.frameIndex == 2, "bounded buffer trims oldest beyond configured ready frame limit");
|
||||
Expect(mailbox.Release(acquired), "bounded acquired frame releases");
|
||||
|
||||
const InputFrameMailboxMetrics metrics = mailbox.Metrics();
|
||||
Expect(metrics.readyCount == 2, "bounded acquire leaves remaining configured ready frames");
|
||||
Expect(metrics.droppedReadyFrames == 1, "bounded buffer records trimmed frame");
|
||||
}
|
||||
}
|
||||
|
||||
int main()
|
||||
{
|
||||
TestSubmitDropsOldestWhenFull();
|
||||
TestReadingFrameIsProtected();
|
||||
TestAcquireOldestConsumesFifoWithoutDroppingReadyFrames();
|
||||
TestMaxReadyFramesKeepsConfiguredInputBuffer();
|
||||
|
||||
if (gFailures != 0)
|
||||
{
|
||||
std::cerr << gFailures << " RenderCadenceCompositorInputFrameMailbox test failure(s).\n";
|
||||
return 1;
|
||||
}
|
||||
|
||||
std::cout << "RenderCadenceCompositorInputFrameMailbox tests passed.\n";
|
||||
return 0;
|
||||
}
|
||||
@@ -45,7 +45,8 @@ RenderCadenceCompositor::SupportedShaderCatalog MakeCatalog(std::filesystem::pat
|
||||
"category": "Tests",
|
||||
"entryPoint": "shadeVideo",
|
||||
"parameters": [
|
||||
{ "id": "gain", "label": "Gain", "type": "float", "default": 0.5 }
|
||||
{ "id": "gain", "label": "Gain", "type": "float", "default": 0.5 },
|
||||
{ "id": "drop", "label": "Drop", "type": "trigger" }
|
||||
]
|
||||
})");
|
||||
|
||||
@@ -143,6 +144,58 @@ void TestAddAndRemoveLayers()
|
||||
|
||||
std::filesystem::remove_all(root);
|
||||
}
|
||||
|
||||
void TestLayerControlsUpdateDisplayAndRenderModels()
|
||||
{
|
||||
std::filesystem::path root;
|
||||
RenderCadenceCompositor::SupportedShaderCatalog catalog = MakeCatalog(root);
|
||||
|
||||
RenderCadenceCompositor::RuntimeLayerModel model;
|
||||
std::string error;
|
||||
std::string firstLayerId;
|
||||
std::string secondLayerId;
|
||||
Expect(model.AddLayer(catalog, "solid", firstLayerId, error), "first control layer can be added");
|
||||
Expect(model.AddLayer(catalog, "solid", secondLayerId, error), "second control layer can be added");
|
||||
|
||||
Expect(model.SetLayerBypass(firstLayerId, true, error), "bypass can be set");
|
||||
Expect(model.ReorderLayer(firstLayerId, 1, error), "layer can be reordered");
|
||||
RenderCadenceCompositor::RuntimeLayerModelSnapshot snapshot = model.Snapshot();
|
||||
Expect(snapshot.displayLayers[1].id == firstLayerId, "reordered layer moves to requested index");
|
||||
Expect(snapshot.displayLayers[1].bypass, "bypass state is visible in read model");
|
||||
|
||||
JsonValue gainValue(0.75);
|
||||
Expect(model.UpdateParameter(firstLayerId, "gain", gainValue, error), "parameter value can be updated");
|
||||
snapshot = model.Snapshot();
|
||||
Expect(snapshot.displayLayers[1].parameterValues.at("gain").numberValues.front() == 0.75, "updated parameter value is visible");
|
||||
JsonValue dropPulse(true);
|
||||
Expect(model.UpdateParameter(firstLayerId, "drop", dropPulse, error), "trigger parameter can be pulsed");
|
||||
snapshot = model.Snapshot();
|
||||
const std::vector<double> firstTrigger = snapshot.displayLayers[1].parameterValues.at("drop").numberValues;
|
||||
Expect(firstTrigger.size() == 2 && firstTrigger[0] == 1.0 && firstTrigger[1] >= 0.0, "trigger pulse increments count and records runtime time");
|
||||
Expect(model.UpdateParameter(firstLayerId, "drop", dropPulse, error), "trigger parameter can be pulsed again");
|
||||
snapshot = model.Snapshot();
|
||||
const std::vector<double> secondTrigger = snapshot.displayLayers[1].parameterValues.at("drop").numberValues;
|
||||
Expect(secondTrigger.size() == 2 && secondTrigger[0] == 2.0 && secondTrigger[1] >= firstTrigger[1], "second trigger pulse increments count again");
|
||||
|
||||
RuntimeShaderArtifact artifact;
|
||||
artifact.layerId = firstLayerId;
|
||||
artifact.shaderId = "solid";
|
||||
artifact.displayName = "Solid";
|
||||
artifact.fragmentShaderSource = "void main(){}";
|
||||
artifact.parameterDefinitions = snapshot.displayLayers[1].parameterDefinitions;
|
||||
artifact.message = "build ready";
|
||||
Expect(model.MarkBuildReady(artifact, error), "ready artifact keeps layer parameter state");
|
||||
snapshot = model.Snapshot();
|
||||
Expect(snapshot.renderLayers.size() == 1, "ready layer produces render model");
|
||||
Expect(snapshot.renderLayers[0].bypass, "render model carries bypass state");
|
||||
Expect(snapshot.renderLayers[0].artifact.parameterValues.at("gain").numberValues.front() == 0.75, "render artifact carries updated parameter value");
|
||||
|
||||
Expect(model.ResetParameters(firstLayerId, error), "parameters can reset to defaults");
|
||||
snapshot = model.Snapshot();
|
||||
Expect(snapshot.displayLayers[1].parameterValues.at("gain").numberValues.front() == 0.5, "reset restores default value");
|
||||
|
||||
std::filesystem::remove_all(root);
|
||||
}
|
||||
}
|
||||
|
||||
int main()
|
||||
@@ -151,6 +204,7 @@ int main()
|
||||
TestRejectsUnsupportedStartupShader();
|
||||
TestBuildFailureStaysDisplaySide();
|
||||
TestAddAndRemoveLayers();
|
||||
TestLayerControlsUpdateDisplayAndRenderModels();
|
||||
|
||||
if (gFailures != 0)
|
||||
{
|
||||
|
||||
@@ -80,6 +80,15 @@ ShaderParameterDefinition EnumParam()
|
||||
definition.enumOptions = { { "soft", "Soft" }, { "hard", "Hard" } };
|
||||
return definition;
|
||||
}
|
||||
|
||||
ShaderParameterDefinition TriggerParam()
|
||||
{
|
||||
ShaderParameterDefinition definition;
|
||||
definition.id = "drop";
|
||||
definition.label = "Drop";
|
||||
definition.type = ShaderParameterType::Trigger;
|
||||
return definition;
|
||||
}
|
||||
}
|
||||
|
||||
int main()
|
||||
@@ -90,6 +99,11 @@ int main()
|
||||
artifact.parameterDefinitions.push_back(ColorParam());
|
||||
artifact.parameterDefinitions.push_back(BoolParam());
|
||||
artifact.parameterDefinitions.push_back(EnumParam());
|
||||
artifact.parameterDefinitions.push_back(TriggerParam());
|
||||
|
||||
ShaderParameterValue triggerValue;
|
||||
triggerValue.numberValues = { 3.0, 1.25 };
|
||||
artifact.parameterValues["drop"] = triggerValue;
|
||||
|
||||
const std::vector<unsigned char> buffer = BuildRuntimeShaderGlobalParamsStd140(artifact, 120, 1920, 1080);
|
||||
|
||||
@@ -104,6 +118,8 @@ int main()
|
||||
Expect(ReadFloat(buffer, 92) == 1.0f, "color default alpha is packed");
|
||||
Expect(ReadInt(buffer, 96) == 1, "bool default is packed as int");
|
||||
Expect(ReadInt(buffer, 100) == 1, "enum default is packed as selected option index");
|
||||
Expect(ReadInt(buffer, 104) == 3, "trigger count is packed as int");
|
||||
Expect(ReadFloat(buffer, 108) == 1.25f, "trigger time is packed after trigger count");
|
||||
|
||||
std::cout << "RenderCadenceCompositorRuntimeShaderParams tests passed.\n";
|
||||
return 0;
|
||||
|
||||
@@ -43,6 +43,13 @@ int main()
|
||||
|
||||
RenderCadenceCompositor::CadenceTelemetrySnapshot telemetry;
|
||||
telemetry.renderFps = 59.94;
|
||||
telemetry.renderFrameMilliseconds = 2.5;
|
||||
telemetry.renderFrameBudgetUsedPercent = 15.0;
|
||||
telemetry.renderFrameMaxMilliseconds = 4.0;
|
||||
telemetry.readbackQueueMilliseconds = 0.6;
|
||||
telemetry.completedReadbackCopyMilliseconds = 1.2;
|
||||
telemetry.completedDrops = 3;
|
||||
telemetry.acquireMisses = 4;
|
||||
telemetry.shaderBuildsCommitted = 1;
|
||||
|
||||
const std::filesystem::path root = MakeTestRoot();
|
||||
@@ -98,6 +105,13 @@ int main()
|
||||
ExpectContains(json, "\"type\":\"color\"", "state JSON should serialize parameter types for the UI");
|
||||
ExpectContains(json, "\"width\":1920", "state JSON should expose output width");
|
||||
ExpectContains(json, "\"height\":1080", "state JSON should expose output height");
|
||||
ExpectContains(json, "\"renderMs\":2.5", "state JSON should expose top-level render timing");
|
||||
ExpectContains(json, "\"budgetUsedPercent\":15", "state JSON should expose top-level render budget percentage");
|
||||
ExpectContains(json, "\"renderFrameMs\":2.5", "state JSON should expose cadence render timing");
|
||||
ExpectContains(json, "\"readbackQueueMs\":0.6", "state JSON should expose readback queue timing");
|
||||
ExpectContains(json, "\"completedReadbackCopyMs\":1.2", "state JSON should expose completed readback copy timing");
|
||||
ExpectContains(json, "\"completedDrops\":3", "state JSON should expose completed drop count");
|
||||
ExpectContains(json, "\"acquireMisses\":4", "state JSON should expose acquire miss count");
|
||||
|
||||
std::filesystem::remove_all(root);
|
||||
|
||||
|
||||
@@ -23,6 +23,8 @@ ShaderPackage MakeSinglePassPackage()
|
||||
ShaderPassDefinition pass;
|
||||
pass.id = "main";
|
||||
pass.entryPoint = "mainImage";
|
||||
pass.sourcePath = "shader.slang";
|
||||
pass.outputName = "layerOutput";
|
||||
shaderPackage.passes.push_back(pass);
|
||||
return shaderPackage;
|
||||
}
|
||||
@@ -37,19 +39,35 @@ void SupportsSinglePassStatelessPackage()
|
||||
Expect(result.reason.empty(), "supported packages should not report a rejection reason");
|
||||
}
|
||||
|
||||
void RejectsMultipassPackage()
|
||||
void SupportsStatelessNamedPassPackage()
|
||||
{
|
||||
ShaderPackage shaderPackage = MakeSinglePassPackage();
|
||||
shaderPackage.passes.front().outputName = "generatedMask";
|
||||
ShaderPassDefinition secondPass;
|
||||
secondPass.id = "second";
|
||||
secondPass.entryPoint = "mainImage";
|
||||
secondPass.sourcePath = "shader.slang";
|
||||
secondPass.inputNames.push_back("generatedMask");
|
||||
secondPass.outputName = "layerOutput";
|
||||
shaderPackage.passes.push_back(secondPass);
|
||||
|
||||
const RenderCadenceCompositor::ShaderSupportResult result =
|
||||
RenderCadenceCompositor::CheckStatelessSinglePassShaderSupport(shaderPackage);
|
||||
|
||||
Expect(!result.supported, "multipass packages should be rejected");
|
||||
Expect(result.reason.find("single-pass") != std::string::npos, "multipass rejection should explain the single-pass limit");
|
||||
Expect(result.supported, "stateless named-pass packages should be supported");
|
||||
Expect(result.reason.empty(), "supported named-pass packages should not report a rejection reason");
|
||||
}
|
||||
|
||||
void RejectsUnknownPassInput()
|
||||
{
|
||||
ShaderPackage shaderPackage = MakeSinglePassPackage();
|
||||
shaderPackage.passes.front().inputNames.push_back("missingIntermediate");
|
||||
|
||||
const RenderCadenceCompositor::ShaderSupportResult result =
|
||||
RenderCadenceCompositor::CheckStatelessSinglePassShaderSupport(shaderPackage);
|
||||
|
||||
Expect(!result.supported, "packages with unknown pass inputs should be rejected");
|
||||
Expect(result.reason.find("unknown input") != std::string::npos, "unknown input rejection should explain the missing named output");
|
||||
}
|
||||
|
||||
void RejectsTemporalPackage()
|
||||
@@ -97,7 +115,8 @@ void RejectsTextParameters()
|
||||
int main()
|
||||
{
|
||||
SupportsSinglePassStatelessPackage();
|
||||
RejectsMultipassPackage();
|
||||
SupportsStatelessNamedPassPackage();
|
||||
RejectsUnknownPassInput();
|
||||
RejectsTemporalPackage();
|
||||
RejectsTextureAssets();
|
||||
RejectsTextParameters();
|
||||
|
||||
@@ -26,6 +26,8 @@ struct FakeExchangeMetrics
|
||||
std::size_t scheduledCount = 0;
|
||||
uint64_t completedFrames = 0;
|
||||
uint64_t scheduledFrames = 0;
|
||||
uint64_t completedDrops = 0;
|
||||
uint64_t acquireMisses = 0;
|
||||
};
|
||||
|
||||
struct FakeExchange
|
||||
@@ -55,6 +57,12 @@ struct FakeOutputMetrics
|
||||
bool actualBufferedFramesAvailable = false;
|
||||
uint64_t actualBufferedFrames = 0;
|
||||
double scheduleCallMilliseconds = 0.0;
|
||||
bool scheduleLeadAvailable = false;
|
||||
int64_t playbackStreamTime = 0;
|
||||
uint64_t playbackFrameIndex = 0;
|
||||
uint64_t nextScheduleFrameIndex = 0;
|
||||
int64_t scheduleLeadFrames = 0;
|
||||
uint64_t scheduleRealignmentCount = 0;
|
||||
};
|
||||
|
||||
struct FakeOutput
|
||||
@@ -65,8 +73,25 @@ struct FakeOutput
|
||||
|
||||
struct FakeRenderThreadMetrics
|
||||
{
|
||||
uint64_t clockOverruns = 0;
|
||||
uint64_t skippedFrames = 0;
|
||||
uint64_t shaderBuildsCommitted = 0;
|
||||
uint64_t shaderBuildFailures = 0;
|
||||
double renderFrameMilliseconds = 0.0;
|
||||
double renderFrameBudgetUsedPercent = 0.0;
|
||||
double renderFrameMaxMilliseconds = 0.0;
|
||||
double readbackQueueMilliseconds = 0.0;
|
||||
double completedReadbackCopyMilliseconds = 0.0;
|
||||
uint64_t inputFramesReceived = 0;
|
||||
uint64_t inputFramesDropped = 0;
|
||||
uint64_t inputConsumeMisses = 0;
|
||||
uint64_t inputUploadMisses = 0;
|
||||
std::size_t inputReadyFrames = 0;
|
||||
std::size_t inputReadingFrames = 0;
|
||||
double inputLatestAgeMilliseconds = 0.0;
|
||||
double inputUploadMilliseconds = 0.0;
|
||||
bool inputFormatSupported = true;
|
||||
bool inputSignalPresent = false;
|
||||
};
|
||||
|
||||
struct FakeRenderThread
|
||||
@@ -84,28 +109,78 @@ void TestTelemetrySamplesCompletedPollMissesAndShaderCounts()
|
||||
exchange.metrics.scheduledCount = 4;
|
||||
exchange.metrics.completedFrames = 100;
|
||||
exchange.metrics.scheduledFrames = 96;
|
||||
exchange.metrics.completedDrops = 2;
|
||||
exchange.metrics.acquireMisses = 3;
|
||||
|
||||
FakeOutput output;
|
||||
output.metrics.actualBufferedFramesAvailable = true;
|
||||
output.metrics.actualBufferedFrames = 4;
|
||||
output.metrics.scheduleLeadAvailable = true;
|
||||
output.metrics.playbackStreamTime = 10010;
|
||||
output.metrics.playbackFrameIndex = 10;
|
||||
output.metrics.nextScheduleFrameIndex = 14;
|
||||
output.metrics.scheduleLeadFrames = 4;
|
||||
output.metrics.scheduleRealignmentCount = 1;
|
||||
|
||||
FakeOutputThread outputThread;
|
||||
outputThread.metrics.completedPollMisses = 12;
|
||||
outputThread.metrics.scheduleFailures = 0;
|
||||
|
||||
FakeRenderThread renderThread;
|
||||
renderThread.metrics.clockOverruns = 5;
|
||||
renderThread.metrics.skippedFrames = 8;
|
||||
renderThread.metrics.shaderBuildsCommitted = 1;
|
||||
renderThread.metrics.shaderBuildFailures = 0;
|
||||
renderThread.metrics.renderFrameMilliseconds = 2.5;
|
||||
renderThread.metrics.renderFrameBudgetUsedPercent = 15.0;
|
||||
renderThread.metrics.renderFrameMaxMilliseconds = 4.0;
|
||||
renderThread.metrics.readbackQueueMilliseconds = 0.6;
|
||||
renderThread.metrics.completedReadbackCopyMilliseconds = 1.2;
|
||||
renderThread.metrics.inputFramesReceived = 9;
|
||||
renderThread.metrics.inputFramesDropped = 2;
|
||||
renderThread.metrics.inputConsumeMisses = 3;
|
||||
renderThread.metrics.inputUploadMisses = 4;
|
||||
renderThread.metrics.inputReadyFrames = 1;
|
||||
renderThread.metrics.inputReadingFrames = 0;
|
||||
renderThread.metrics.inputLatestAgeMilliseconds = 4.5;
|
||||
renderThread.metrics.inputUploadMilliseconds = 0.25;
|
||||
renderThread.metrics.inputFormatSupported = true;
|
||||
renderThread.metrics.inputSignalPresent = true;
|
||||
|
||||
const auto snapshot = telemetry.Sample(exchange, output, outputThread, renderThread);
|
||||
Expect(snapshot.freeFrames == 7, "free frame count is sampled");
|
||||
Expect(snapshot.completedFrames == 1, "completed frame count is sampled");
|
||||
Expect(snapshot.scheduledFrames == 4, "scheduled frame count is sampled");
|
||||
Expect(snapshot.completedPollMisses == 12, "completed poll misses are sampled");
|
||||
Expect(snapshot.completedDrops == 2, "completed drops are sampled");
|
||||
Expect(snapshot.acquireMisses == 3, "acquire misses are sampled");
|
||||
Expect(snapshot.clockOverruns == 5, "clock overrun count is sampled");
|
||||
Expect(snapshot.clockSkippedFrames == 8, "clock skipped frame count is sampled");
|
||||
Expect(snapshot.shaderBuildsCommitted == 1, "shader committed count is sampled");
|
||||
Expect(snapshot.shaderBuildFailures == 0, "shader failure count is sampled");
|
||||
Expect(snapshot.renderFrameMilliseconds == 2.5, "render frame timing is sampled");
|
||||
Expect(snapshot.renderFrameBudgetUsedPercent == 15.0, "render budget percentage is sampled");
|
||||
Expect(snapshot.renderFrameMaxMilliseconds == 4.0, "render frame max timing is sampled");
|
||||
Expect(snapshot.readbackQueueMilliseconds == 0.6, "readback queue timing is sampled");
|
||||
Expect(snapshot.completedReadbackCopyMilliseconds == 1.2, "completed readback copy timing is sampled");
|
||||
Expect(snapshot.inputFramesReceived == 9, "input received count is sampled");
|
||||
Expect(snapshot.inputFramesDropped == 2, "input dropped count is sampled");
|
||||
Expect(snapshot.inputConsumeMisses == 3, "input consume miss count is sampled");
|
||||
Expect(snapshot.inputUploadMisses == 4, "input upload miss count is sampled");
|
||||
Expect(snapshot.inputReadyFrames == 1, "input ready frame count is sampled");
|
||||
Expect(snapshot.inputReadingFrames == 0, "input reading frame count is sampled");
|
||||
Expect(snapshot.inputLatestAgeMilliseconds == 4.5, "input latest age is sampled");
|
||||
Expect(snapshot.inputUploadMilliseconds == 0.25, "input upload timing is sampled");
|
||||
Expect(snapshot.inputFormatSupported, "input format support is sampled");
|
||||
Expect(snapshot.inputSignalPresent, "input signal present is sampled");
|
||||
Expect(snapshot.deckLinkBufferedAvailable, "buffer telemetry availability is sampled");
|
||||
Expect(snapshot.deckLinkBuffered == 4, "buffer depth is sampled");
|
||||
Expect(snapshot.deckLinkScheduleLeadAvailable, "schedule lead availability is sampled");
|
||||
Expect(snapshot.deckLinkPlaybackStreamTime == 10010, "playback stream time is sampled");
|
||||
Expect(snapshot.deckLinkPlaybackFrameIndex == 10, "playback frame index is sampled");
|
||||
Expect(snapshot.deckLinkNextScheduleFrameIndex == 14, "next schedule frame index is sampled");
|
||||
Expect(snapshot.deckLinkScheduleLeadFrames == 4, "schedule lead frames are sampled");
|
||||
Expect(snapshot.deckLinkScheduleRealignments == 1, "schedule realignment count is sampled");
|
||||
}
|
||||
|
||||
void TestTelemetryComputesRatesFromDeltas()
|
||||
@@ -143,14 +218,46 @@ void TestTelemetrySerializesToJson()
|
||||
snapshot.scheduledTotal = 118;
|
||||
snapshot.completedPollMisses = 3;
|
||||
snapshot.scheduleFailures = 0;
|
||||
snapshot.completedDrops = 4;
|
||||
snapshot.acquireMisses = 5;
|
||||
snapshot.completions = 117;
|
||||
snapshot.displayedLate = 1;
|
||||
snapshot.dropped = 2;
|
||||
snapshot.clockOverruns = 3;
|
||||
snapshot.clockSkippedFrames = 5;
|
||||
snapshot.shaderBuildsCommitted = 1;
|
||||
snapshot.shaderBuildFailures = 0;
|
||||
snapshot.renderFrameMilliseconds = 2.5;
|
||||
snapshot.renderFrameBudgetUsedPercent = 15.0;
|
||||
snapshot.renderFrameMaxMilliseconds = 4.0;
|
||||
snapshot.readbackQueueMilliseconds = 0.6;
|
||||
snapshot.completedReadbackCopyMilliseconds = 1.2;
|
||||
snapshot.inputFramesReceived = 10;
|
||||
snapshot.inputFramesDropped = 1;
|
||||
snapshot.inputConsumeMisses = 2;
|
||||
snapshot.inputUploadMisses = 3;
|
||||
snapshot.inputReadyFrames = 1;
|
||||
snapshot.inputReadingFrames = 0;
|
||||
snapshot.inputLatestAgeMilliseconds = 3.5;
|
||||
snapshot.inputUploadMilliseconds = 0.75;
|
||||
snapshot.inputFormatSupported = true;
|
||||
snapshot.inputSignalPresent = true;
|
||||
snapshot.inputCaptureFps = 59.94;
|
||||
snapshot.inputConvertMilliseconds = 4.25;
|
||||
snapshot.inputSubmitMilliseconds = 0.35;
|
||||
snapshot.inputNoSignalFrames = 2;
|
||||
snapshot.inputUnsupportedFrames = 3;
|
||||
snapshot.inputSubmitMisses = 4;
|
||||
snapshot.inputCaptureFormat = "UYVY8";
|
||||
snapshot.deckLinkBufferedAvailable = true;
|
||||
snapshot.deckLinkBuffered = 4;
|
||||
snapshot.deckLinkScheduleCallMilliseconds = 1.25;
|
||||
snapshot.deckLinkScheduleLeadAvailable = true;
|
||||
snapshot.deckLinkScheduleLeadFrames = 4;
|
||||
snapshot.deckLinkPlaybackFrameIndex = 10;
|
||||
snapshot.deckLinkNextScheduleFrameIndex = 14;
|
||||
snapshot.deckLinkPlaybackStreamTime = 10010;
|
||||
snapshot.deckLinkScheduleRealignments = 1;
|
||||
|
||||
const std::string json = RenderCadenceCompositor::CadenceTelemetryToJson(snapshot);
|
||||
const std::string expected =
|
||||
@@ -158,10 +265,31 @@ void TestTelemetrySerializesToJson()
|
||||
"\"free\":7,\"completed\":1,\"scheduled\":4,"
|
||||
"\"renderedTotal\":120,\"scheduledTotal\":118,"
|
||||
"\"completedPollMisses\":3,\"scheduleFailures\":0,"
|
||||
"\"completedDrops\":4,\"acquireMisses\":5,"
|
||||
"\"completions\":117,\"late\":1,\"dropped\":2,"
|
||||
"\"clockOverruns\":3,\"clockSkippedFrames\":5,"
|
||||
"\"clockOveruns\":3,\"clockSkipped\":5,"
|
||||
"\"shaderCommitted\":1,\"shaderFailures\":0,"
|
||||
"\"renderFrameMs\":2.5,\"renderFrameBudgetUsedPercent\":15,"
|
||||
"\"renderFrameMaxMs\":4,\"readbackQueueMs\":0.6,"
|
||||
"\"completedReadbackCopyMs\":1.2,"
|
||||
"\"inputFramesReceived\":10,\"inputFramesDropped\":1,"
|
||||
"\"inputConsumeMisses\":2,\"inputUploadMisses\":3,"
|
||||
"\"inputReadyFrames\":1,\"inputReadingFrames\":0,"
|
||||
"\"inputLatestAgeMs\":3.5,\"inputUploadMs\":0.75,"
|
||||
"\"inputFormatSupported\":true,\"inputSignalPresent\":true,"
|
||||
"\"inputCaptureFps\":59.94,\"inputConvertMs\":4.25,"
|
||||
"\"inputSubmitMs\":0.35,\"inputNoSignalFrames\":2,"
|
||||
"\"inputUnsupportedFrames\":3,\"inputSubmitMisses\":4,"
|
||||
"\"inputCaptureFormat\":\"UYVY8\","
|
||||
"\"deckLinkBufferedAvailable\":true,\"deckLinkBuffered\":4,"
|
||||
"\"scheduleCallMs\":1.25}";
|
||||
"\"scheduleCallMs\":1.25,"
|
||||
"\"deckLinkScheduleLeadAvailable\":true,"
|
||||
"\"deckLinkScheduleLeadFrames\":4,"
|
||||
"\"deckLinkPlaybackFrameIndex\":10,"
|
||||
"\"deckLinkNextScheduleFrameIndex\":14,"
|
||||
"\"deckLinkPlaybackStreamTime\":10010,"
|
||||
"\"deckLinkScheduleRealignments\":1}";
|
||||
Expect(json == expected, "telemetry snapshot serializes to stable JSON");
|
||||
}
|
||||
|
||||
|
||||
@@ -70,6 +70,19 @@ void TestDefaultPolicyReportsLagWithoutSkippingScheduleTime()
|
||||
Expect(scheduler.NextScheduleTime().streamTime == 1000, "default recovery keeps stream time continuous");
|
||||
}
|
||||
|
||||
void TestScheduleCursorCanAlignToPlaybackClock()
|
||||
{
|
||||
VideoPlayoutScheduler scheduler;
|
||||
scheduler.Configure(1000, 50000);
|
||||
|
||||
(void)scheduler.NextScheduleTime();
|
||||
scheduler.AlignNextScheduleTimeToPlayback(10000, 4);
|
||||
Expect(scheduler.NextScheduleTime().streamTime == 14000, "schedule cursor skips stale stream time after underfeed");
|
||||
|
||||
scheduler.AlignNextScheduleTimeToPlayback(11000, 1);
|
||||
Expect(scheduler.NextScheduleTime().streamTime == 15000, "schedule cursor does not move backward");
|
||||
}
|
||||
|
||||
void TestMeasuredRecoveryIsCappedByPolicy()
|
||||
{
|
||||
VideoPlayoutPolicy policy;
|
||||
@@ -133,6 +146,7 @@ int main()
|
||||
TestScheduleAdvancesFromZero();
|
||||
TestLateAndDroppedRecoveryUsesMeasuredPressure();
|
||||
TestDefaultPolicyReportsLagWithoutSkippingScheduleTime();
|
||||
TestScheduleCursorCanAlignToPlaybackClock();
|
||||
TestMeasuredRecoveryIsCappedByPolicy();
|
||||
TestCleanCompletionTracksCompletedIndexAndClearsStreaks();
|
||||
TestPolicyNormalization();
|
||||
|
||||
Reference in New Issue
Block a user