videoIO seperation

This commit is contained in:
2026-05-22 14:58:42 +10:00
parent 35801601a5
commit b7e7452567
12 changed files with 401 additions and 187 deletions

View File

@@ -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.

View File

@@ -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

View File

@@ -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();
});

View File

@@ -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;

View File

@@ -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;

View File

@@ -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();

View File

@@ -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);

View File

@@ -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
View 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;
};
}

View 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 };
};
}

View File

@@ -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"

View 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;
}