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/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/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/layers`: app-side runtime layer model, restore, reload, and render snapshot construction
|
||||
- `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.
|
||||
7. Queue background Slang builds for every pending active layer.
|
||||
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.
|
||||
|
||||
@@ -130,11 +130,11 @@ When a runtime shader build completes, the app publishes a render-layer artifact
|
||||
|
||||
## 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.
|
||||
|
||||
|
||||
@@ -29,9 +29,9 @@ SystemFrameExchange
|
||||
preserves completed output frames as a bounded FIFO reserve once they are waiting for playout
|
||||
protects scheduled frames until DeckLink completion
|
||||
|
||||
DeckLinkOutputThread
|
||||
VideoOutputThread
|
||||
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
|
||||
|
||||
PreviewWindowThread
|
||||
@@ -47,7 +47,7 @@ Startup builds a small output preroll reserve before DeckLink scheduled playback
|
||||
|
||||
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
|
||||
- non-blocking startup when DeckLink output is unavailable
|
||||
- hidden render-thread-owned OpenGL context
|
||||
|
||||
@@ -203,7 +203,7 @@ int main(int argc, char** argv)
|
||||
RenderThread renderThread(frameExchange, &inputMailbox, renderConfig);
|
||||
|
||||
RenderCadenceCompositor::RenderCadenceApp<RenderThread, SystemFrameExchange> app(renderThread, frameExchange, appConfig);
|
||||
app.SetDeckLinkInputMetricsProvider([&deckLinkInput]() {
|
||||
app.SetVideoInputMetricsProvider([&deckLinkInput]() {
|
||||
return deckLinkInput.Metrics();
|
||||
});
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
#include "../preview/PreviewConfig.h"
|
||||
#include "../telemetry/TelemetryHealthMonitor.h"
|
||||
#include "../video/DeckLinkOutput.h"
|
||||
#include "../video/DeckLinkOutputThread.h"
|
||||
#include "../video/VideoOutputThread.h"
|
||||
|
||||
#include <chrono>
|
||||
#include <cstddef>
|
||||
@@ -16,7 +16,7 @@ namespace RenderCadenceCompositor
|
||||
struct AppConfig
|
||||
{
|
||||
DeckLinkOutputConfig deckLink;
|
||||
DeckLinkOutputThreadConfig outputThread;
|
||||
VideoOutputThreadConfig outputThread;
|
||||
TelemetryHealthMonitorConfig telemetry;
|
||||
LoggerConfig logging;
|
||||
HttpControlServerConfig http;
|
||||
|
||||
@@ -7,9 +7,9 @@
|
||||
#include "../control/RuntimeStateJson.h"
|
||||
#include "../preview/PreviewWindowThread.h"
|
||||
#include "../telemetry/TelemetryHealthMonitor.h"
|
||||
#include "../video/DeckLinkInput.h"
|
||||
#include "../video/DeckLinkOutput.h"
|
||||
#include "../video/DeckLinkOutputThread.h"
|
||||
#include "../video/VideoIOEdges.h"
|
||||
#include "../video/VideoOutputThread.h"
|
||||
|
||||
#include <chrono>
|
||||
#include <filesystem>
|
||||
@@ -56,7 +56,10 @@ public:
|
||||
mRenderThread(renderThread),
|
||||
mFrameExchange(frameExchange),
|
||||
mConfig(config),
|
||||
mOutputThread(mOutput, mFrameExchange, mConfig.outputThread),
|
||||
mOutputThread(mOutput, mFrameExchange, VideoOutputThreadConfig{
|
||||
mConfig.outputThread.targetBufferedFrames,
|
||||
mConfig.outputThread.idleSleep
|
||||
}),
|
||||
mTelemetryHealth(mConfig.telemetry),
|
||||
mRuntimeLayers([this](const std::vector<RuntimeRenderLayerModel>& layers) {
|
||||
mRenderThread.SubmitRuntimeRenderLayers(layers);
|
||||
@@ -120,10 +123,10 @@ public:
|
||||
}
|
||||
|
||||
bool Started() const { return mStarted; }
|
||||
const DeckLinkOutput& Output() const { return mOutput; }
|
||||
void SetDeckLinkInputMetricsProvider(std::function<DeckLinkInputMetrics()> provider)
|
||||
const IVideoOutputEdge& Output() const { return mOutput; }
|
||||
void SetVideoInputMetricsProvider(std::function<VideoInputEdgeMetrics()> provider)
|
||||
{
|
||||
mDeckLinkInputMetricsProvider = std::move(provider);
|
||||
mVideoInputMetricsProvider = std::move(provider);
|
||||
}
|
||||
|
||||
private:
|
||||
@@ -142,10 +145,10 @@ private:
|
||||
return;
|
||||
}
|
||||
|
||||
Log("app", "Starting DeckLink output thread.");
|
||||
Log("app", "Starting video output thread.");
|
||||
if (!mOutputThread.Start())
|
||||
{
|
||||
DisableVideoOutput("DeckLink output thread failed to start.");
|
||||
DisableVideoOutput("Video output thread failed to start.");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -252,7 +255,7 @@ private:
|
||||
std::string BuildStateJson()
|
||||
{
|
||||
CadenceTelemetrySnapshot telemetry = mHttpTelemetry.Sample(mFrameExchange, mOutput, mOutputThread, mRenderThread);
|
||||
ApplyDeckLinkInputMetrics(telemetry);
|
||||
ApplyVideoInputMetrics(telemetry);
|
||||
RuntimeLayerModelSnapshot layerSnapshot = mRuntimeLayers.Snapshot(telemetry);
|
||||
return RuntimeStateToJson(RuntimeStateJsonInput{
|
||||
mConfig,
|
||||
@@ -265,12 +268,12 @@ private:
|
||||
});
|
||||
}
|
||||
|
||||
void ApplyDeckLinkInputMetrics(CadenceTelemetrySnapshot& telemetry)
|
||||
void ApplyVideoInputMetrics(CadenceTelemetrySnapshot& telemetry)
|
||||
{
|
||||
if (!mDeckLinkInputMetricsProvider)
|
||||
if (!mVideoInputMetricsProvider)
|
||||
return;
|
||||
|
||||
const DeckLinkInputMetrics inputMetrics = mDeckLinkInputMetricsProvider();
|
||||
const VideoInputEdgeMetrics inputMetrics = mVideoInputMetricsProvider();
|
||||
telemetry.inputConvertMilliseconds = inputMetrics.convertMilliseconds;
|
||||
telemetry.inputSubmitMilliseconds = inputMetrics.submitMilliseconds;
|
||||
telemetry.inputNoSignalFrames = inputMetrics.noInputSourceFrames;
|
||||
@@ -298,13 +301,13 @@ private:
|
||||
SystemFrameExchange& mFrameExchange;
|
||||
AppConfig mConfig;
|
||||
DeckLinkOutput mOutput;
|
||||
DeckLinkOutputThread<SystemFrameExchange> mOutputThread;
|
||||
VideoOutputThread<SystemFrameExchange> mOutputThread;
|
||||
TelemetryHealthMonitor mTelemetryHealth;
|
||||
CadenceTelemetry mHttpTelemetry;
|
||||
HttpControlServer mHttpServer;
|
||||
PreviewWindowThread mPreviewWindow;
|
||||
RuntimeLayerController mRuntimeLayers;
|
||||
std::function<DeckLinkInputMetrics()> mDeckLinkInputMetricsProvider;
|
||||
std::function<VideoInputEdgeMetrics()> mVideoInputMetricsProvider;
|
||||
uint64_t mLastInputCapturedFrames = 0;
|
||||
bool mStarted = false;
|
||||
bool mVideoOutputEnabled = false;
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
#include "../frames/InputFrameMailbox.h"
|
||||
#include "DeckLinkAPI_h.h"
|
||||
#include "DeckLinkDisplayMode.h"
|
||||
#include "VideoIOEdges.h"
|
||||
|
||||
#include <atlbase.h>
|
||||
|
||||
@@ -19,16 +20,7 @@ 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";
|
||||
};
|
||||
using DeckLinkInputMetrics = VideoInputEdgeMetrics;
|
||||
|
||||
class DeckLinkInput;
|
||||
|
||||
@@ -48,7 +40,7 @@ private:
|
||||
std::atomic<ULONG> mRefCount{ 1 };
|
||||
};
|
||||
|
||||
class DeckLinkInput
|
||||
class DeckLinkInput : public IVideoInputEdge
|
||||
{
|
||||
public:
|
||||
DeckLinkInput(InputFrameMailbox& mailbox);
|
||||
@@ -57,14 +49,14 @@ public:
|
||||
~DeckLinkInput();
|
||||
|
||||
bool Initialize(const DeckLinkInputConfig& config, std::string& error);
|
||||
bool Start(std::string& error);
|
||||
void Stop();
|
||||
void ReleaseResources();
|
||||
bool Start(std::string& error) override;
|
||||
void Stop() override;
|
||||
void ReleaseResources() override;
|
||||
|
||||
bool IsInitialized() const { return mInput != nullptr; }
|
||||
bool IsRunning() const { return mRunning.load(std::memory_order_acquire); }
|
||||
VideoIOPixelFormat CapturePixelFormat() const;
|
||||
DeckLinkInputMetrics Metrics() const;
|
||||
bool IsInitialized() const override { return mInput != nullptr; }
|
||||
bool IsRunning() const override { return mRunning.load(std::memory_order_acquire); }
|
||||
VideoIOPixelFormat CapturePixelFormat() const override;
|
||||
DeckLinkInputMetrics Metrics() const override;
|
||||
|
||||
void HandleFrameArrived(IDeckLinkVideoInputFrame* inputFrame);
|
||||
void HandleFormatChanged();
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
#include "DeckLinkDisplayMode.h"
|
||||
#include "DeckLinkSession.h"
|
||||
#include "VideoIOEdges.h"
|
||||
#include "VideoIOTypes.h"
|
||||
|
||||
#include <atomic>
|
||||
@@ -18,28 +19,12 @@ struct DeckLinkOutputConfig
|
||||
bool outputAlphaRequired = false;
|
||||
};
|
||||
|
||||
struct DeckLinkOutputMetrics
|
||||
{
|
||||
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;
|
||||
};
|
||||
using DeckLinkOutputMetrics = VideoOutputEdgeMetrics;
|
||||
|
||||
class DeckLinkOutput
|
||||
class DeckLinkOutput : public IVideoOutputEdge
|
||||
{
|
||||
public:
|
||||
using CompletionCallback = std::function<void(const VideoIOCompletion&)>;
|
||||
using CompletionCallback = IVideoOutputEdge::CompletionCallback;
|
||||
|
||||
DeckLinkOutput() = default;
|
||||
DeckLinkOutput(const DeckLinkOutput&) = delete;
|
||||
@@ -47,13 +32,13 @@ public:
|
||||
~DeckLinkOutput();
|
||||
|
||||
bool Initialize(const DeckLinkOutputConfig& config, CompletionCallback completionCallback, std::string& error);
|
||||
bool StartScheduledPlayback(std::string& error);
|
||||
bool ScheduleFrame(const VideoIOOutputFrame& frame);
|
||||
void Stop();
|
||||
void ReleaseResources();
|
||||
bool StartScheduledPlayback(std::string& error) override;
|
||||
bool ScheduleFrame(const VideoIOOutputFrame& frame) override;
|
||||
void Stop() override;
|
||||
void ReleaseResources() override;
|
||||
|
||||
const VideoIOState& State() const;
|
||||
DeckLinkOutputMetrics Metrics() const;
|
||||
const VideoIOState& State() const override;
|
||||
DeckLinkOutputMetrics Metrics() const override;
|
||||
|
||||
private:
|
||||
void HandleCompletion(const VideoIOCompletion& completion);
|
||||
|
||||
@@ -1,127 +1,12 @@
|
||||
#pragma once
|
||||
|
||||
#include "../frames/SystemFrameTypes.h"
|
||||
#include "DeckLinkOutput.h"
|
||||
#include "VideoIOTypes.h"
|
||||
|
||||
#include <atomic>
|
||||
#include <chrono>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <thread>
|
||||
#include "VideoOutputThread.h"
|
||||
|
||||
namespace RenderCadenceCompositor
|
||||
{
|
||||
struct DeckLinkOutputThreadConfig
|
||||
{
|
||||
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;
|
||||
};
|
||||
using DeckLinkOutputThreadConfig = VideoOutputThreadConfig;
|
||||
using DeckLinkOutputThreadMetrics = VideoOutputThreadMetrics;
|
||||
|
||||
template <typename SystemFrameExchange>
|
||||
class DeckLinkOutputThread
|
||||
{
|
||||
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 };
|
||||
};
|
||||
using DeckLinkOutputThread = VideoOutputThread<SystemFrameExchange>;
|
||||
}
|
||||
|
||||
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"
|
||||
)
|
||||
|
||||
add_video_shader_test(VideoOutputThreadTests
|
||||
"${SRC_DIR}/video/VideoIOFormat.cpp"
|
||||
"${TEST_DIR}/VideoOutputThreadTests.cpp"
|
||||
)
|
||||
|
||||
add_video_shader_test(OutputProductionControllerTests
|
||||
"${SRC_DIR}/video/OutputProductionController.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