videoIO seperation
This commit is contained in:
@@ -25,7 +25,7 @@ Primary source areas:
|
|||||||
- `src/render/thread`: render thread lifecycle, cadence loop, metrics, and runtime shader commit mailbox
|
- `src/render/thread`: render thread lifecycle, cadence loop, metrics, and runtime shader commit mailbox
|
||||||
- `src/render/runtime`: render-thread-owned runtime shader scene, renderer, text texture upload cache, and shared-context shader prepare worker
|
- `src/render/runtime`: render-thread-owned runtime shader scene, renderer, text texture upload cache, and shared-context shader prepare worker
|
||||||
- `src/frames`: system-memory frame exchange
|
- `src/frames`: system-memory frame exchange
|
||||||
- `src/video`: DeckLink input/output edges and scheduling
|
- `src/video`: generic video IO edge contracts, DeckLink input/output edges, and scheduling
|
||||||
- `src/runtime/catalog`: supported shader catalog and package filtering
|
- `src/runtime/catalog`: supported shader catalog and package filtering
|
||||||
- `src/runtime/layers`: app-side runtime layer model, restore, reload, and render snapshot construction
|
- `src/runtime/layers`: app-side runtime layer model, restore, reload, and render snapshot construction
|
||||||
- `src/runtime/shader`: background Slang build bridge and prepared shader artifact types
|
- `src/runtime/shader`: background Slang build bridge and prepared shader artifact types
|
||||||
@@ -47,7 +47,7 @@ Startup broadly proceeds as:
|
|||||||
6. Start the render thread.
|
6. Start the render thread.
|
||||||
7. Queue background Slang builds for every pending active layer.
|
7. Queue background Slang builds for every pending active layer.
|
||||||
8. Build a small completed-frame reserve.
|
8. Build a small completed-frame reserve.
|
||||||
9. Start optional preview, optional DeckLink output, telemetry, and HTTP control.
|
9. Start optional preview, optional video output, telemetry, and HTTP control.
|
||||||
|
|
||||||
The runtime-state restore is intentionally app/control side work. The render thread does not read JSON, inspect the shader library, or decide what to compile.
|
The runtime-state restore is intentionally app/control side work. The render thread does not read JSON, inspect the shader library, or decide what to compile.
|
||||||
|
|
||||||
@@ -130,11 +130,11 @@ When a runtime shader build completes, the app publishes a render-layer artifact
|
|||||||
|
|
||||||
## Video And Preview
|
## Video And Preview
|
||||||
|
|
||||||
DeckLink input and output are optional edges.
|
Video input and output are optional edges. DeckLink is the current concrete backend.
|
||||||
|
|
||||||
Input captures BGRA8 directly where possible, or raw UYVY8 into `InputFrameMailbox` for render-thread GPU decode. The input edge does not call GL, render, preview, screenshot, shader, or output scheduling code.
|
The input edge writes CPU frames into `InputFrameMailbox`. The current DeckLink backend captures BGRA8 directly where possible, or raw UYVY8 for render-thread GPU decode. The input edge does not call GL, render, preview, screenshot, shader, or output scheduling code.
|
||||||
|
|
||||||
Output consumes completed system-memory frames from `SystemFrameExchange` and schedules them to DeckLink. If DeckLink output is unavailable, the app continues running render cadence, control, preview, telemetry, and logging.
|
The output edge consumes completed system-memory frames from `SystemFrameExchange`. The current DeckLink backend schedules those frames to DeckLink. If video output is unavailable, the app continues running render cadence, control, preview, telemetry, and logging.
|
||||||
|
|
||||||
`PreviewWindowThread` is optional and uses a non-consuming system-memory tap. It paints with Win32/GDI on its own thread and skips preview ticks instead of blocking the frame exchange.
|
`PreviewWindowThread` is optional and uses a non-consuming system-memory tap. It paints with Win32/GDI on its own thread and skips preview ticks instead of blocking the frame exchange.
|
||||||
|
|
||||||
|
|||||||
@@ -29,9 +29,9 @@ SystemFrameExchange
|
|||||||
preserves completed output frames as a bounded FIFO reserve once they are waiting for playout
|
preserves completed output frames as a bounded FIFO reserve once they are waiting for playout
|
||||||
protects scheduled frames until DeckLink completion
|
protects scheduled frames until DeckLink completion
|
||||||
|
|
||||||
DeckLinkOutputThread
|
VideoOutputThread
|
||||||
consumes completed system-memory frames
|
consumes completed system-memory frames
|
||||||
schedules them into DeckLink up to target depth
|
schedules them into the active video output edge up to target depth
|
||||||
never renders
|
never renders
|
||||||
|
|
||||||
PreviewWindowThread
|
PreviewWindowThread
|
||||||
@@ -47,7 +47,7 @@ Startup builds a small output preroll reserve before DeckLink scheduled playback
|
|||||||
|
|
||||||
Included now:
|
Included now:
|
||||||
|
|
||||||
- output-only DeckLink
|
- generic video output edge contract with DeckLink as the current implementation
|
||||||
- optional DeckLink input edge with BGRA8 capture or raw UYVY8 capture decoded on the render thread
|
- optional DeckLink input edge with BGRA8 capture or raw UYVY8 capture decoded on the render thread
|
||||||
- non-blocking startup when DeckLink output is unavailable
|
- non-blocking startup when DeckLink output is unavailable
|
||||||
- hidden render-thread-owned OpenGL context
|
- hidden render-thread-owned OpenGL context
|
||||||
|
|||||||
@@ -203,7 +203,7 @@ int main(int argc, char** argv)
|
|||||||
RenderThread renderThread(frameExchange, &inputMailbox, renderConfig);
|
RenderThread renderThread(frameExchange, &inputMailbox, renderConfig);
|
||||||
|
|
||||||
RenderCadenceCompositor::RenderCadenceApp<RenderThread, SystemFrameExchange> app(renderThread, frameExchange, appConfig);
|
RenderCadenceCompositor::RenderCadenceApp<RenderThread, SystemFrameExchange> app(renderThread, frameExchange, appConfig);
|
||||||
app.SetDeckLinkInputMetricsProvider([&deckLinkInput]() {
|
app.SetVideoInputMetricsProvider([&deckLinkInput]() {
|
||||||
return deckLinkInput.Metrics();
|
return deckLinkInput.Metrics();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
#include "../preview/PreviewConfig.h"
|
#include "../preview/PreviewConfig.h"
|
||||||
#include "../telemetry/TelemetryHealthMonitor.h"
|
#include "../telemetry/TelemetryHealthMonitor.h"
|
||||||
#include "../video/DeckLinkOutput.h"
|
#include "../video/DeckLinkOutput.h"
|
||||||
#include "../video/DeckLinkOutputThread.h"
|
#include "../video/VideoOutputThread.h"
|
||||||
|
|
||||||
#include <chrono>
|
#include <chrono>
|
||||||
#include <cstddef>
|
#include <cstddef>
|
||||||
@@ -16,7 +16,7 @@ namespace RenderCadenceCompositor
|
|||||||
struct AppConfig
|
struct AppConfig
|
||||||
{
|
{
|
||||||
DeckLinkOutputConfig deckLink;
|
DeckLinkOutputConfig deckLink;
|
||||||
DeckLinkOutputThreadConfig outputThread;
|
VideoOutputThreadConfig outputThread;
|
||||||
TelemetryHealthMonitorConfig telemetry;
|
TelemetryHealthMonitorConfig telemetry;
|
||||||
LoggerConfig logging;
|
LoggerConfig logging;
|
||||||
HttpControlServerConfig http;
|
HttpControlServerConfig http;
|
||||||
|
|||||||
@@ -7,9 +7,9 @@
|
|||||||
#include "../control/RuntimeStateJson.h"
|
#include "../control/RuntimeStateJson.h"
|
||||||
#include "../preview/PreviewWindowThread.h"
|
#include "../preview/PreviewWindowThread.h"
|
||||||
#include "../telemetry/TelemetryHealthMonitor.h"
|
#include "../telemetry/TelemetryHealthMonitor.h"
|
||||||
#include "../video/DeckLinkInput.h"
|
|
||||||
#include "../video/DeckLinkOutput.h"
|
#include "../video/DeckLinkOutput.h"
|
||||||
#include "../video/DeckLinkOutputThread.h"
|
#include "../video/VideoIOEdges.h"
|
||||||
|
#include "../video/VideoOutputThread.h"
|
||||||
|
|
||||||
#include <chrono>
|
#include <chrono>
|
||||||
#include <filesystem>
|
#include <filesystem>
|
||||||
@@ -56,7 +56,10 @@ public:
|
|||||||
mRenderThread(renderThread),
|
mRenderThread(renderThread),
|
||||||
mFrameExchange(frameExchange),
|
mFrameExchange(frameExchange),
|
||||||
mConfig(config),
|
mConfig(config),
|
||||||
mOutputThread(mOutput, mFrameExchange, mConfig.outputThread),
|
mOutputThread(mOutput, mFrameExchange, VideoOutputThreadConfig{
|
||||||
|
mConfig.outputThread.targetBufferedFrames,
|
||||||
|
mConfig.outputThread.idleSleep
|
||||||
|
}),
|
||||||
mTelemetryHealth(mConfig.telemetry),
|
mTelemetryHealth(mConfig.telemetry),
|
||||||
mRuntimeLayers([this](const std::vector<RuntimeRenderLayerModel>& layers) {
|
mRuntimeLayers([this](const std::vector<RuntimeRenderLayerModel>& layers) {
|
||||||
mRenderThread.SubmitRuntimeRenderLayers(layers);
|
mRenderThread.SubmitRuntimeRenderLayers(layers);
|
||||||
@@ -120,10 +123,10 @@ public:
|
|||||||
}
|
}
|
||||||
|
|
||||||
bool Started() const { return mStarted; }
|
bool Started() const { return mStarted; }
|
||||||
const DeckLinkOutput& Output() const { return mOutput; }
|
const IVideoOutputEdge& Output() const { return mOutput; }
|
||||||
void SetDeckLinkInputMetricsProvider(std::function<DeckLinkInputMetrics()> provider)
|
void SetVideoInputMetricsProvider(std::function<VideoInputEdgeMetrics()> provider)
|
||||||
{
|
{
|
||||||
mDeckLinkInputMetricsProvider = std::move(provider);
|
mVideoInputMetricsProvider = std::move(provider);
|
||||||
}
|
}
|
||||||
|
|
||||||
private:
|
private:
|
||||||
@@ -142,10 +145,10 @@ private:
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
Log("app", "Starting DeckLink output thread.");
|
Log("app", "Starting video output thread.");
|
||||||
if (!mOutputThread.Start())
|
if (!mOutputThread.Start())
|
||||||
{
|
{
|
||||||
DisableVideoOutput("DeckLink output thread failed to start.");
|
DisableVideoOutput("Video output thread failed to start.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -252,7 +255,7 @@ private:
|
|||||||
std::string BuildStateJson()
|
std::string BuildStateJson()
|
||||||
{
|
{
|
||||||
CadenceTelemetrySnapshot telemetry = mHttpTelemetry.Sample(mFrameExchange, mOutput, mOutputThread, mRenderThread);
|
CadenceTelemetrySnapshot telemetry = mHttpTelemetry.Sample(mFrameExchange, mOutput, mOutputThread, mRenderThread);
|
||||||
ApplyDeckLinkInputMetrics(telemetry);
|
ApplyVideoInputMetrics(telemetry);
|
||||||
RuntimeLayerModelSnapshot layerSnapshot = mRuntimeLayers.Snapshot(telemetry);
|
RuntimeLayerModelSnapshot layerSnapshot = mRuntimeLayers.Snapshot(telemetry);
|
||||||
return RuntimeStateToJson(RuntimeStateJsonInput{
|
return RuntimeStateToJson(RuntimeStateJsonInput{
|
||||||
mConfig,
|
mConfig,
|
||||||
@@ -265,12 +268,12 @@ private:
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
void ApplyDeckLinkInputMetrics(CadenceTelemetrySnapshot& telemetry)
|
void ApplyVideoInputMetrics(CadenceTelemetrySnapshot& telemetry)
|
||||||
{
|
{
|
||||||
if (!mDeckLinkInputMetricsProvider)
|
if (!mVideoInputMetricsProvider)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
const DeckLinkInputMetrics inputMetrics = mDeckLinkInputMetricsProvider();
|
const VideoInputEdgeMetrics inputMetrics = mVideoInputMetricsProvider();
|
||||||
telemetry.inputConvertMilliseconds = inputMetrics.convertMilliseconds;
|
telemetry.inputConvertMilliseconds = inputMetrics.convertMilliseconds;
|
||||||
telemetry.inputSubmitMilliseconds = inputMetrics.submitMilliseconds;
|
telemetry.inputSubmitMilliseconds = inputMetrics.submitMilliseconds;
|
||||||
telemetry.inputNoSignalFrames = inputMetrics.noInputSourceFrames;
|
telemetry.inputNoSignalFrames = inputMetrics.noInputSourceFrames;
|
||||||
@@ -298,13 +301,13 @@ private:
|
|||||||
SystemFrameExchange& mFrameExchange;
|
SystemFrameExchange& mFrameExchange;
|
||||||
AppConfig mConfig;
|
AppConfig mConfig;
|
||||||
DeckLinkOutput mOutput;
|
DeckLinkOutput mOutput;
|
||||||
DeckLinkOutputThread<SystemFrameExchange> mOutputThread;
|
VideoOutputThread<SystemFrameExchange> mOutputThread;
|
||||||
TelemetryHealthMonitor mTelemetryHealth;
|
TelemetryHealthMonitor mTelemetryHealth;
|
||||||
CadenceTelemetry mHttpTelemetry;
|
CadenceTelemetry mHttpTelemetry;
|
||||||
HttpControlServer mHttpServer;
|
HttpControlServer mHttpServer;
|
||||||
PreviewWindowThread mPreviewWindow;
|
PreviewWindowThread mPreviewWindow;
|
||||||
RuntimeLayerController mRuntimeLayers;
|
RuntimeLayerController mRuntimeLayers;
|
||||||
std::function<DeckLinkInputMetrics()> mDeckLinkInputMetricsProvider;
|
std::function<VideoInputEdgeMetrics()> mVideoInputMetricsProvider;
|
||||||
uint64_t mLastInputCapturedFrames = 0;
|
uint64_t mLastInputCapturedFrames = 0;
|
||||||
bool mStarted = false;
|
bool mStarted = false;
|
||||||
bool mVideoOutputEnabled = false;
|
bool mVideoOutputEnabled = false;
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
#include "../frames/InputFrameMailbox.h"
|
#include "../frames/InputFrameMailbox.h"
|
||||||
#include "DeckLinkAPI_h.h"
|
#include "DeckLinkAPI_h.h"
|
||||||
#include "DeckLinkDisplayMode.h"
|
#include "DeckLinkDisplayMode.h"
|
||||||
|
#include "VideoIOEdges.h"
|
||||||
|
|
||||||
#include <atlbase.h>
|
#include <atlbase.h>
|
||||||
|
|
||||||
@@ -19,16 +20,7 @@ struct DeckLinkInputConfig
|
|||||||
VideoFormat videoFormat;
|
VideoFormat videoFormat;
|
||||||
};
|
};
|
||||||
|
|
||||||
struct DeckLinkInputMetrics
|
using DeckLinkInputMetrics = VideoInputEdgeMetrics;
|
||||||
{
|
|
||||||
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 DeckLinkInput;
|
||||||
|
|
||||||
@@ -48,7 +40,7 @@ private:
|
|||||||
std::atomic<ULONG> mRefCount{ 1 };
|
std::atomic<ULONG> mRefCount{ 1 };
|
||||||
};
|
};
|
||||||
|
|
||||||
class DeckLinkInput
|
class DeckLinkInput : public IVideoInputEdge
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
DeckLinkInput(InputFrameMailbox& mailbox);
|
DeckLinkInput(InputFrameMailbox& mailbox);
|
||||||
@@ -57,14 +49,14 @@ public:
|
|||||||
~DeckLinkInput();
|
~DeckLinkInput();
|
||||||
|
|
||||||
bool Initialize(const DeckLinkInputConfig& config, std::string& error);
|
bool Initialize(const DeckLinkInputConfig& config, std::string& error);
|
||||||
bool Start(std::string& error);
|
bool Start(std::string& error) override;
|
||||||
void Stop();
|
void Stop() override;
|
||||||
void ReleaseResources();
|
void ReleaseResources() override;
|
||||||
|
|
||||||
bool IsInitialized() const { return mInput != nullptr; }
|
bool IsInitialized() const override { return mInput != nullptr; }
|
||||||
bool IsRunning() const { return mRunning.load(std::memory_order_acquire); }
|
bool IsRunning() const override { return mRunning.load(std::memory_order_acquire); }
|
||||||
VideoIOPixelFormat CapturePixelFormat() const;
|
VideoIOPixelFormat CapturePixelFormat() const override;
|
||||||
DeckLinkInputMetrics Metrics() const;
|
DeckLinkInputMetrics Metrics() const override;
|
||||||
|
|
||||||
void HandleFrameArrived(IDeckLinkVideoInputFrame* inputFrame);
|
void HandleFrameArrived(IDeckLinkVideoInputFrame* inputFrame);
|
||||||
void HandleFormatChanged();
|
void HandleFormatChanged();
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
#include "DeckLinkDisplayMode.h"
|
#include "DeckLinkDisplayMode.h"
|
||||||
#include "DeckLinkSession.h"
|
#include "DeckLinkSession.h"
|
||||||
|
#include "VideoIOEdges.h"
|
||||||
#include "VideoIOTypes.h"
|
#include "VideoIOTypes.h"
|
||||||
|
|
||||||
#include <atomic>
|
#include <atomic>
|
||||||
@@ -18,28 +19,12 @@ struct DeckLinkOutputConfig
|
|||||||
bool outputAlphaRequired = false;
|
bool outputAlphaRequired = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
struct DeckLinkOutputMetrics
|
using DeckLinkOutputMetrics = VideoOutputEdgeMetrics;
|
||||||
{
|
|
||||||
uint64_t completions = 0;
|
|
||||||
uint64_t displayedLate = 0;
|
|
||||||
uint64_t dropped = 0;
|
|
||||||
uint64_t flushed = 0;
|
|
||||||
uint64_t scheduleFailures = 0;
|
|
||||||
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
|
class DeckLinkOutput : public IVideoOutputEdge
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
using CompletionCallback = std::function<void(const VideoIOCompletion&)>;
|
using CompletionCallback = IVideoOutputEdge::CompletionCallback;
|
||||||
|
|
||||||
DeckLinkOutput() = default;
|
DeckLinkOutput() = default;
|
||||||
DeckLinkOutput(const DeckLinkOutput&) = delete;
|
DeckLinkOutput(const DeckLinkOutput&) = delete;
|
||||||
@@ -47,13 +32,13 @@ public:
|
|||||||
~DeckLinkOutput();
|
~DeckLinkOutput();
|
||||||
|
|
||||||
bool Initialize(const DeckLinkOutputConfig& config, CompletionCallback completionCallback, std::string& error);
|
bool Initialize(const DeckLinkOutputConfig& config, CompletionCallback completionCallback, std::string& error);
|
||||||
bool StartScheduledPlayback(std::string& error);
|
bool StartScheduledPlayback(std::string& error) override;
|
||||||
bool ScheduleFrame(const VideoIOOutputFrame& frame);
|
bool ScheduleFrame(const VideoIOOutputFrame& frame) override;
|
||||||
void Stop();
|
void Stop() override;
|
||||||
void ReleaseResources();
|
void ReleaseResources() override;
|
||||||
|
|
||||||
const VideoIOState& State() const;
|
const VideoIOState& State() const override;
|
||||||
DeckLinkOutputMetrics Metrics() const;
|
DeckLinkOutputMetrics Metrics() const override;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void HandleCompletion(const VideoIOCompletion& completion);
|
void HandleCompletion(const VideoIOCompletion& completion);
|
||||||
|
|||||||
@@ -1,127 +1,12 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include "../frames/SystemFrameTypes.h"
|
#include "VideoOutputThread.h"
|
||||||
#include "DeckLinkOutput.h"
|
|
||||||
#include "VideoIOTypes.h"
|
|
||||||
|
|
||||||
#include <atomic>
|
|
||||||
#include <chrono>
|
|
||||||
#include <cstddef>
|
|
||||||
#include <cstdint>
|
|
||||||
#include <thread>
|
|
||||||
|
|
||||||
namespace RenderCadenceCompositor
|
namespace RenderCadenceCompositor
|
||||||
{
|
{
|
||||||
struct DeckLinkOutputThreadConfig
|
using DeckLinkOutputThreadConfig = VideoOutputThreadConfig;
|
||||||
{
|
using DeckLinkOutputThreadMetrics = VideoOutputThreadMetrics;
|
||||||
std::size_t targetBufferedFrames = 4;
|
|
||||||
std::chrono::milliseconds idleSleep = std::chrono::milliseconds(1);
|
|
||||||
};
|
|
||||||
|
|
||||||
struct DeckLinkOutputThreadMetrics
|
|
||||||
{
|
|
||||||
uint64_t scheduledFrames = 0;
|
|
||||||
uint64_t completedPollMisses = 0;
|
|
||||||
uint64_t scheduleFailures = 0;
|
|
||||||
};
|
|
||||||
|
|
||||||
template <typename SystemFrameExchange>
|
template <typename SystemFrameExchange>
|
||||||
class DeckLinkOutputThread
|
using DeckLinkOutputThread = VideoOutputThread<SystemFrameExchange>;
|
||||||
{
|
|
||||||
public:
|
|
||||||
DeckLinkOutputThread(DeckLinkOutput& output, SystemFrameExchange& exchange, DeckLinkOutputThreadConfig config = DeckLinkOutputThreadConfig()) :
|
|
||||||
mOutput(output),
|
|
||||||
mExchange(exchange),
|
|
||||||
mConfig(config)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
DeckLinkOutputThread(const DeckLinkOutputThread&) = delete;
|
|
||||||
DeckLinkOutputThread& operator=(const DeckLinkOutputThread&) = delete;
|
|
||||||
|
|
||||||
~DeckLinkOutputThread()
|
|
||||||
{
|
|
||||||
Stop();
|
|
||||||
}
|
|
||||||
|
|
||||||
bool Start()
|
|
||||||
{
|
|
||||||
if (mRunning)
|
|
||||||
return true;
|
|
||||||
mStopping = false;
|
|
||||||
mThread = std::thread([this]() { ThreadMain(); });
|
|
||||||
mRunning = true;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
void Stop()
|
|
||||||
{
|
|
||||||
mStopping = true;
|
|
||||||
if (mThread.joinable())
|
|
||||||
mThread.join();
|
|
||||||
mRunning = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
DeckLinkOutputThreadMetrics Metrics() const
|
|
||||||
{
|
|
||||||
DeckLinkOutputThreadMetrics metrics;
|
|
||||||
metrics.scheduledFrames = mScheduledFrames.load();
|
|
||||||
metrics.completedPollMisses = mCompletedPollMisses.load();
|
|
||||||
metrics.scheduleFailures = mScheduleFailures.load();
|
|
||||||
return metrics;
|
|
||||||
}
|
|
||||||
|
|
||||||
private:
|
|
||||||
void ThreadMain()
|
|
||||||
{
|
|
||||||
while (!mStopping)
|
|
||||||
{
|
|
||||||
const auto exchangeMetrics = mExchange.Metrics();
|
|
||||||
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))
|
|
||||||
{
|
|
||||||
++mCompletedPollMisses;
|
|
||||||
std::this_thread::sleep_for(mConfig.idleSleep);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
VideoIOOutputFrame outputFrame;
|
|
||||||
outputFrame.bytes = frame.bytes;
|
|
||||||
outputFrame.nativeBuffer = frame.bytes;
|
|
||||||
outputFrame.rowBytes = frame.rowBytes;
|
|
||||||
outputFrame.width = frame.width;
|
|
||||||
outputFrame.height = frame.height;
|
|
||||||
outputFrame.pixelFormat = frame.pixelFormat;
|
|
||||||
|
|
||||||
if (!mOutput.ScheduleFrame(outputFrame))
|
|
||||||
{
|
|
||||||
++mScheduleFailures;
|
|
||||||
mExchange.ReleaseScheduledByBytes(frame.bytes);
|
|
||||||
std::this_thread::sleep_for(mConfig.idleSleep);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
++mScheduledFrames;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
DeckLinkOutput& mOutput;
|
|
||||||
SystemFrameExchange& mExchange;
|
|
||||||
DeckLinkOutputThreadConfig mConfig;
|
|
||||||
std::thread mThread;
|
|
||||||
std::atomic<bool> mStopping{ false };
|
|
||||||
std::atomic<bool> mRunning{ false };
|
|
||||||
std::atomic<uint64_t> mScheduledFrames{ 0 };
|
|
||||||
std::atomic<uint64_t> mCompletedPollMisses{ 0 };
|
|
||||||
std::atomic<uint64_t> mScheduleFailures{ 0 };
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|||||||
69
src/video/VideoIOEdges.h
Normal file
69
src/video/VideoIOEdges.h
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "VideoIOFormat.h"
|
||||||
|
#include "VideoIOTypes.h"
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
#include <functional>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
namespace RenderCadenceCompositor
|
||||||
|
{
|
||||||
|
struct VideoInputEdgeMetrics
|
||||||
|
{
|
||||||
|
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 IVideoInputEdge
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
virtual ~IVideoInputEdge() = default;
|
||||||
|
|
||||||
|
virtual bool Start(std::string& error) = 0;
|
||||||
|
virtual void Stop() = 0;
|
||||||
|
virtual void ReleaseResources() = 0;
|
||||||
|
virtual bool IsInitialized() const = 0;
|
||||||
|
virtual bool IsRunning() const = 0;
|
||||||
|
virtual VideoIOPixelFormat CapturePixelFormat() const = 0;
|
||||||
|
virtual VideoInputEdgeMetrics Metrics() const = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct VideoOutputEdgeMetrics
|
||||||
|
{
|
||||||
|
uint64_t completions = 0;
|
||||||
|
uint64_t displayedLate = 0;
|
||||||
|
uint64_t dropped = 0;
|
||||||
|
uint64_t flushed = 0;
|
||||||
|
uint64_t scheduleFailures = 0;
|
||||||
|
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 IVideoOutputEdge
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
using CompletionCallback = std::function<void(const VideoIOCompletion&)>;
|
||||||
|
|
||||||
|
virtual ~IVideoOutputEdge() = default;
|
||||||
|
|
||||||
|
virtual bool StartScheduledPlayback(std::string& error) = 0;
|
||||||
|
virtual bool ScheduleFrame(const VideoIOOutputFrame& frame) = 0;
|
||||||
|
virtual void Stop() = 0;
|
||||||
|
virtual void ReleaseResources() = 0;
|
||||||
|
virtual const VideoIOState& State() const = 0;
|
||||||
|
virtual VideoOutputEdgeMetrics Metrics() const = 0;
|
||||||
|
};
|
||||||
|
}
|
||||||
127
src/video/VideoOutputThread.h
Normal file
127
src/video/VideoOutputThread.h
Normal file
@@ -0,0 +1,127 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "../frames/SystemFrameTypes.h"
|
||||||
|
#include "VideoIOEdges.h"
|
||||||
|
#include "VideoIOTypes.h"
|
||||||
|
|
||||||
|
#include <atomic>
|
||||||
|
#include <chrono>
|
||||||
|
#include <cstddef>
|
||||||
|
#include <cstdint>
|
||||||
|
#include <thread>
|
||||||
|
|
||||||
|
namespace RenderCadenceCompositor
|
||||||
|
{
|
||||||
|
struct VideoOutputThreadConfig
|
||||||
|
{
|
||||||
|
std::size_t targetBufferedFrames = 4;
|
||||||
|
std::chrono::milliseconds idleSleep = std::chrono::milliseconds(1);
|
||||||
|
};
|
||||||
|
|
||||||
|
struct VideoOutputThreadMetrics
|
||||||
|
{
|
||||||
|
uint64_t scheduledFrames = 0;
|
||||||
|
uint64_t completedPollMisses = 0;
|
||||||
|
uint64_t scheduleFailures = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
template <typename SystemFrameExchange>
|
||||||
|
class VideoOutputThread
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
VideoOutputThread(IVideoOutputEdge& output, SystemFrameExchange& exchange, VideoOutputThreadConfig config = VideoOutputThreadConfig()) :
|
||||||
|
mOutput(output),
|
||||||
|
mExchange(exchange),
|
||||||
|
mConfig(config)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
VideoOutputThread(const VideoOutputThread&) = delete;
|
||||||
|
VideoOutputThread& operator=(const VideoOutputThread&) = delete;
|
||||||
|
|
||||||
|
~VideoOutputThread()
|
||||||
|
{
|
||||||
|
Stop();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Start()
|
||||||
|
{
|
||||||
|
if (mRunning)
|
||||||
|
return true;
|
||||||
|
mStopping = false;
|
||||||
|
mThread = std::thread([this]() { ThreadMain(); });
|
||||||
|
mRunning = true;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Stop()
|
||||||
|
{
|
||||||
|
mStopping = true;
|
||||||
|
if (mThread.joinable())
|
||||||
|
mThread.join();
|
||||||
|
mRunning = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
VideoOutputThreadMetrics Metrics() const
|
||||||
|
{
|
||||||
|
VideoOutputThreadMetrics metrics;
|
||||||
|
metrics.scheduledFrames = mScheduledFrames.load();
|
||||||
|
metrics.completedPollMisses = mCompletedPollMisses.load();
|
||||||
|
metrics.scheduleFailures = mScheduleFailures.load();
|
||||||
|
return metrics;
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
void ThreadMain()
|
||||||
|
{
|
||||||
|
while (!mStopping)
|
||||||
|
{
|
||||||
|
const auto exchangeMetrics = mExchange.Metrics();
|
||||||
|
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))
|
||||||
|
{
|
||||||
|
++mCompletedPollMisses;
|
||||||
|
std::this_thread::sleep_for(mConfig.idleSleep);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
VideoIOOutputFrame outputFrame;
|
||||||
|
outputFrame.bytes = frame.bytes;
|
||||||
|
outputFrame.nativeBuffer = frame.bytes;
|
||||||
|
outputFrame.rowBytes = frame.rowBytes;
|
||||||
|
outputFrame.width = frame.width;
|
||||||
|
outputFrame.height = frame.height;
|
||||||
|
outputFrame.pixelFormat = frame.pixelFormat;
|
||||||
|
|
||||||
|
if (!mOutput.ScheduleFrame(outputFrame))
|
||||||
|
{
|
||||||
|
++mScheduleFailures;
|
||||||
|
mExchange.ReleaseScheduledByBytes(frame.bytes);
|
||||||
|
std::this_thread::sleep_for(mConfig.idleSleep);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
++mScheduledFrames;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
IVideoOutputEdge& mOutput;
|
||||||
|
SystemFrameExchange& mExchange;
|
||||||
|
VideoOutputThreadConfig mConfig;
|
||||||
|
std::thread mThread;
|
||||||
|
std::atomic<bool> mStopping{ false };
|
||||||
|
std::atomic<bool> mRunning{ false };
|
||||||
|
std::atomic<uint64_t> mScheduledFrames{ 0 };
|
||||||
|
std::atomic<uint64_t> mCompletedPollMisses{ 0 };
|
||||||
|
std::atomic<uint64_t> mScheduleFailures{ 0 };
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -138,6 +138,11 @@ add_video_shader_test(VideoPlayoutSchedulerTests
|
|||||||
"${TEST_DIR}/VideoPlayoutSchedulerTests.cpp"
|
"${TEST_DIR}/VideoPlayoutSchedulerTests.cpp"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
add_video_shader_test(VideoOutputThreadTests
|
||||||
|
"${SRC_DIR}/video/VideoIOFormat.cpp"
|
||||||
|
"${TEST_DIR}/VideoOutputThreadTests.cpp"
|
||||||
|
)
|
||||||
|
|
||||||
add_video_shader_test(OutputProductionControllerTests
|
add_video_shader_test(OutputProductionControllerTests
|
||||||
"${SRC_DIR}/video/OutputProductionController.cpp"
|
"${SRC_DIR}/video/OutputProductionController.cpp"
|
||||||
"${TEST_DIR}/OutputProductionControllerTests.cpp"
|
"${TEST_DIR}/OutputProductionControllerTests.cpp"
|
||||||
|
|||||||
148
tests/VideoOutputThreadTests.cpp
Normal file
148
tests/VideoOutputThreadTests.cpp
Normal file
@@ -0,0 +1,148 @@
|
|||||||
|
#include "VideoOutputThread.h"
|
||||||
|
|
||||||
|
#include <atomic>
|
||||||
|
#include <chrono>
|
||||||
|
#include <iostream>
|
||||||
|
#include <thread>
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
int gFailures = 0;
|
||||||
|
|
||||||
|
void Expect(bool condition, const char* message)
|
||||||
|
{
|
||||||
|
if (condition)
|
||||||
|
return;
|
||||||
|
std::cerr << "FAIL: " << message << "\n";
|
||||||
|
++gFailures;
|
||||||
|
}
|
||||||
|
|
||||||
|
struct FakeExchangeMetrics
|
||||||
|
{
|
||||||
|
std::size_t scheduledCount = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
class FakeExchange
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
FakeExchange()
|
||||||
|
{
|
||||||
|
mFrame.bytes = mBytes;
|
||||||
|
mFrame.rowBytes = 8;
|
||||||
|
mFrame.width = 2;
|
||||||
|
mFrame.height = 2;
|
||||||
|
mFrame.pixelFormat = VideoIOPixelFormat::Bgra8;
|
||||||
|
}
|
||||||
|
|
||||||
|
FakeExchangeMetrics Metrics() const
|
||||||
|
{
|
||||||
|
FakeExchangeMetrics metrics;
|
||||||
|
metrics.scheduledCount = mScheduled.load();
|
||||||
|
return metrics;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ConsumeCompletedForSchedule(SystemFrame& frame)
|
||||||
|
{
|
||||||
|
bool expected = true;
|
||||||
|
if (!mHasFrame.compare_exchange_strong(expected, false))
|
||||||
|
return false;
|
||||||
|
frame = mFrame;
|
||||||
|
mScheduled.fetch_add(1);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ReleaseScheduledByBytes(void*)
|
||||||
|
{
|
||||||
|
mReleased.fetch_add(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::atomic<std::size_t> mScheduled{ 0 };
|
||||||
|
std::atomic<std::size_t> mReleased{ 0 };
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::atomic<bool> mHasFrame{ true };
|
||||||
|
unsigned char mBytes[16] = {};
|
||||||
|
SystemFrame mFrame;
|
||||||
|
};
|
||||||
|
|
||||||
|
class FakeOutputEdge final : public RenderCadenceCompositor::IVideoOutputEdge
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
bool StartScheduledPlayback(std::string&) override { return true; }
|
||||||
|
bool ScheduleFrame(const VideoIOOutputFrame& frame) override
|
||||||
|
{
|
||||||
|
mLastWidth = frame.width;
|
||||||
|
mScheduled.fetch_add(1);
|
||||||
|
return mScheduleResult;
|
||||||
|
}
|
||||||
|
void Stop() override {}
|
||||||
|
void ReleaseResources() override {}
|
||||||
|
const VideoIOState& State() const override { return mState; }
|
||||||
|
RenderCadenceCompositor::VideoOutputEdgeMetrics Metrics() const override { return mMetrics; }
|
||||||
|
|
||||||
|
bool mScheduleResult = true;
|
||||||
|
std::atomic<std::size_t> mScheduled{ 0 };
|
||||||
|
unsigned mLastWidth = 0;
|
||||||
|
|
||||||
|
private:
|
||||||
|
VideoIOState mState;
|
||||||
|
RenderCadenceCompositor::VideoOutputEdgeMetrics mMetrics;
|
||||||
|
};
|
||||||
|
|
||||||
|
void TestSchedulesCompletedFrameThroughGenericEdge()
|
||||||
|
{
|
||||||
|
FakeExchange exchange;
|
||||||
|
FakeOutputEdge output;
|
||||||
|
RenderCadenceCompositor::VideoOutputThreadConfig config;
|
||||||
|
config.targetBufferedFrames = 1;
|
||||||
|
config.idleSleep = std::chrono::milliseconds(1);
|
||||||
|
|
||||||
|
RenderCadenceCompositor::VideoOutputThread<FakeExchange> thread(output, exchange, config);
|
||||||
|
Expect(thread.Start(), "video output thread starts");
|
||||||
|
std::this_thread::sleep_for(std::chrono::milliseconds(20));
|
||||||
|
thread.Stop();
|
||||||
|
|
||||||
|
const auto metrics = thread.Metrics();
|
||||||
|
Expect(output.mScheduled.load() == 1, "generic output edge receives one scheduled frame");
|
||||||
|
Expect(output.mLastWidth == 2, "scheduled frame data comes from exchange");
|
||||||
|
Expect(metrics.scheduledFrames == 1, "thread scheduled frame metric increments");
|
||||||
|
Expect(metrics.scheduleFailures == 0, "no schedule failure is reported");
|
||||||
|
Expect(exchange.mReleased.load() == 0, "successful schedule keeps frame scheduled");
|
||||||
|
}
|
||||||
|
|
||||||
|
void TestReleasesFrameWhenGenericEdgeRejectsSchedule()
|
||||||
|
{
|
||||||
|
FakeExchange exchange;
|
||||||
|
FakeOutputEdge output;
|
||||||
|
output.mScheduleResult = false;
|
||||||
|
RenderCadenceCompositor::VideoOutputThreadConfig config;
|
||||||
|
config.targetBufferedFrames = 1;
|
||||||
|
config.idleSleep = std::chrono::milliseconds(1);
|
||||||
|
|
||||||
|
RenderCadenceCompositor::VideoOutputThread<FakeExchange> thread(output, exchange, config);
|
||||||
|
Expect(thread.Start(), "video output thread starts for failure path");
|
||||||
|
std::this_thread::sleep_for(std::chrono::milliseconds(20));
|
||||||
|
thread.Stop();
|
||||||
|
|
||||||
|
const auto metrics = thread.Metrics();
|
||||||
|
Expect(output.mScheduled.load() == 1, "generic output edge receives failed schedule attempt");
|
||||||
|
Expect(metrics.scheduledFrames == 0, "failed schedule does not increment scheduled frames");
|
||||||
|
Expect(metrics.scheduleFailures == 1, "schedule failure metric increments");
|
||||||
|
Expect(exchange.mReleased.load() == 1, "failed schedule releases the frame back to exchange");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int main()
|
||||||
|
{
|
||||||
|
TestSchedulesCompletedFrameThroughGenericEdge();
|
||||||
|
TestReleasesFrameWhenGenericEdgeRejectsSchedule();
|
||||||
|
|
||||||
|
if (gFailures != 0)
|
||||||
|
{
|
||||||
|
std::cerr << gFailures << " VideoOutputThread test failure(s).\n";
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::cout << "VideoOutputThread tests passed.\n";
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user