From b7e7452567abc3062e9bee1312c7e1828a3ae72d Mon Sep 17 00:00:00 2001 From: Aiden Date: Fri, 22 May 2026 14:58:42 +1000 Subject: [PATCH] videoIO seperation --- docs/CURRENT_SYSTEM_ARCHITECTURE.md | 10 +- src/README.md | 6 +- src/RenderCadenceCompositor.cpp | 2 +- src/app/AppConfig.h | 4 +- src/app/RenderCadenceApp.h | 31 +++--- src/video/DeckLinkInput.h | 28 ++---- src/video/DeckLinkOutput.h | 35 ++----- src/video/DeckLinkOutputThread.h | 123 +---------------------- src/video/VideoIOEdges.h | 69 +++++++++++++ src/video/VideoOutputThread.h | 127 ++++++++++++++++++++++++ tests/CMakeLists.txt | 5 + tests/VideoOutputThreadTests.cpp | 148 ++++++++++++++++++++++++++++ 12 files changed, 401 insertions(+), 187 deletions(-) create mode 100644 src/video/VideoIOEdges.h create mode 100644 src/video/VideoOutputThread.h create mode 100644 tests/VideoOutputThreadTests.cpp diff --git a/docs/CURRENT_SYSTEM_ARCHITECTURE.md b/docs/CURRENT_SYSTEM_ARCHITECTURE.md index a15ccd0..e2751ef 100644 --- a/docs/CURRENT_SYSTEM_ARCHITECTURE.md +++ b/docs/CURRENT_SYSTEM_ARCHITECTURE.md @@ -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. diff --git a/src/README.md b/src/README.md index e0a6887..fe7fa68 100644 --- a/src/README.md +++ b/src/README.md @@ -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 diff --git a/src/RenderCadenceCompositor.cpp b/src/RenderCadenceCompositor.cpp index 1c88320..b413590 100644 --- a/src/RenderCadenceCompositor.cpp +++ b/src/RenderCadenceCompositor.cpp @@ -203,7 +203,7 @@ int main(int argc, char** argv) RenderThread renderThread(frameExchange, &inputMailbox, renderConfig); RenderCadenceCompositor::RenderCadenceApp app(renderThread, frameExchange, appConfig); - app.SetDeckLinkInputMetricsProvider([&deckLinkInput]() { + app.SetVideoInputMetricsProvider([&deckLinkInput]() { return deckLinkInput.Metrics(); }); diff --git a/src/app/AppConfig.h b/src/app/AppConfig.h index 9d5ab8f..ebfc2a5 100644 --- a/src/app/AppConfig.h +++ b/src/app/AppConfig.h @@ -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 #include @@ -16,7 +16,7 @@ namespace RenderCadenceCompositor struct AppConfig { DeckLinkOutputConfig deckLink; - DeckLinkOutputThreadConfig outputThread; + VideoOutputThreadConfig outputThread; TelemetryHealthMonitorConfig telemetry; LoggerConfig logging; HttpControlServerConfig http; diff --git a/src/app/RenderCadenceApp.h b/src/app/RenderCadenceApp.h index fc3ea64..fb09189 100644 --- a/src/app/RenderCadenceApp.h +++ b/src/app/RenderCadenceApp.h @@ -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 #include @@ -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& layers) { mRenderThread.SubmitRuntimeRenderLayers(layers); @@ -120,10 +123,10 @@ public: } bool Started() const { return mStarted; } - const DeckLinkOutput& Output() const { return mOutput; } - void SetDeckLinkInputMetricsProvider(std::function provider) + const IVideoOutputEdge& Output() const { return mOutput; } + void SetVideoInputMetricsProvider(std::function 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 mOutputThread; + VideoOutputThread mOutputThread; TelemetryHealthMonitor mTelemetryHealth; CadenceTelemetry mHttpTelemetry; HttpControlServer mHttpServer; PreviewWindowThread mPreviewWindow; RuntimeLayerController mRuntimeLayers; - std::function mDeckLinkInputMetricsProvider; + std::function mVideoInputMetricsProvider; uint64_t mLastInputCapturedFrames = 0; bool mStarted = false; bool mVideoOutputEnabled = false; diff --git a/src/video/DeckLinkInput.h b/src/video/DeckLinkInput.h index 4b9d8fe..24fd084 100644 --- a/src/video/DeckLinkInput.h +++ b/src/video/DeckLinkInput.h @@ -3,6 +3,7 @@ #include "../frames/InputFrameMailbox.h" #include "DeckLinkAPI_h.h" #include "DeckLinkDisplayMode.h" +#include "VideoIOEdges.h" #include @@ -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 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(); diff --git a/src/video/DeckLinkOutput.h b/src/video/DeckLinkOutput.h index 14cf58e..066b9c6 100644 --- a/src/video/DeckLinkOutput.h +++ b/src/video/DeckLinkOutput.h @@ -2,6 +2,7 @@ #include "DeckLinkDisplayMode.h" #include "DeckLinkSession.h" +#include "VideoIOEdges.h" #include "VideoIOTypes.h" #include @@ -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; + 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); diff --git a/src/video/DeckLinkOutputThread.h b/src/video/DeckLinkOutputThread.h index 37fc567..8da08f7 100644 --- a/src/video/DeckLinkOutputThread.h +++ b/src/video/DeckLinkOutputThread.h @@ -1,127 +1,12 @@ #pragma once -#include "../frames/SystemFrameTypes.h" -#include "DeckLinkOutput.h" -#include "VideoIOTypes.h" - -#include -#include -#include -#include -#include +#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 -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(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 mStopping{ false }; - std::atomic mRunning{ false }; - std::atomic mScheduledFrames{ 0 }; - std::atomic mCompletedPollMisses{ 0 }; - std::atomic mScheduleFailures{ 0 }; -}; +using DeckLinkOutputThread = VideoOutputThread; } diff --git a/src/video/VideoIOEdges.h b/src/video/VideoIOEdges.h new file mode 100644 index 0000000..5f52336 --- /dev/null +++ b/src/video/VideoIOEdges.h @@ -0,0 +1,69 @@ +#pragma once + +#include "VideoIOFormat.h" +#include "VideoIOTypes.h" + +#include +#include +#include + +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; + + 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; +}; +} diff --git a/src/video/VideoOutputThread.h b/src/video/VideoOutputThread.h new file mode 100644 index 0000000..e39533a --- /dev/null +++ b/src/video/VideoOutputThread.h @@ -0,0 +1,127 @@ +#pragma once + +#include "../frames/SystemFrameTypes.h" +#include "VideoIOEdges.h" +#include "VideoIOTypes.h" + +#include +#include +#include +#include +#include + +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 +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(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 mStopping{ false }; + std::atomic mRunning{ false }; + std::atomic mScheduledFrames{ 0 }; + std::atomic mCompletedPollMisses{ 0 }; + std::atomic mScheduleFailures{ 0 }; +}; +} diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 5b2ee31..1ebdf33 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -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" diff --git a/tests/VideoOutputThreadTests.cpp b/tests/VideoOutputThreadTests.cpp new file mode 100644 index 0000000..f1df8b5 --- /dev/null +++ b/tests/VideoOutputThreadTests.cpp @@ -0,0 +1,148 @@ +#include "VideoOutputThread.h" + +#include +#include +#include +#include + +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 mScheduled{ 0 }; + std::atomic mReleased{ 0 }; + +private: + std::atomic 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 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 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 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; +}