#pragma once #include "AppConfig.h" #include "AppConfigProvider.h" #include "RuntimeLayerController.h" #include "../logging/Logger.h" #include "../control/RuntimeStateJson.h" #include "../telemetry/TelemetryHealthMonitor.h" #include "../video/DeckLinkInput.h" #include "../video/DeckLinkOutput.h" #include "../video/DeckLinkOutputThread.h" #include #include #include #include #include #include namespace RenderCadenceCompositor { namespace detail { template auto StartRenderThread(RenderThread& renderThread, std::string& error, int) -> decltype(renderThread.Start(error), bool()) { return renderThread.Start(error); } template bool StartRenderThreadWithoutError(RenderThread& renderThread, std::true_type) { return renderThread.Start(); } template bool StartRenderThreadWithoutError(RenderThread& renderThread, std::false_type) { renderThread.Start(); return true; } template auto StartRenderThread(RenderThread& renderThread, std::string&, long) -> decltype(renderThread.Start(), bool()) { return StartRenderThreadWithoutError(renderThread, std::is_same()); } } template class RenderCadenceApp { public: RenderCadenceApp(RenderThread& renderThread, SystemFrameExchange& frameExchange, AppConfig config = DefaultAppConfig()) : mRenderThread(renderThread), mFrameExchange(frameExchange), mConfig(config), mOutputThread(mOutput, mFrameExchange, mConfig.outputThread), mTelemetryHealth(mConfig.telemetry), mRuntimeLayers([this](const std::vector& layers) { mRenderThread.SubmitRuntimeRenderLayers(layers); }) { } RenderCadenceApp(const RenderCadenceApp&) = delete; RenderCadenceApp& operator=(const RenderCadenceApp&) = delete; ~RenderCadenceApp() { Stop(); } bool Start(std::string& error) { mRuntimeLayers.Initialize( mConfig.shaderLibrary, static_cast(mConfig.maxTemporalHistoryFrames), mConfig.runtimeShaderId); Log("app", "Starting render thread."); if (!detail::StartRenderThread(mRenderThread, error, 0)) { LogError("app", "Render thread start failed: " + error); Stop(); return false; } mRuntimeLayers.StartStartupBuild(mConfig.runtimeShaderId); Log("app", "Waiting for rendered warmup frames."); if (!mFrameExchange.WaitForCompletedDepth(mConfig.warmupCompletedFrames, mConfig.warmupTimeout)) { error = "Timed out waiting for rendered warmup frames."; LogError("app", error); Stop(); return false; } StartOptionalVideoOutput(); mTelemetryHealth.Start(mFrameExchange, mOutput, mOutputThread, mRenderThread); StartHttpServer(); Log("app", "RenderCadenceCompositor started."); mStarted = true; return true; } void Stop() { mHttpServer.Stop(); mTelemetryHealth.Stop(); mOutputThread.Stop(); mOutput.Stop(); mRuntimeLayers.Stop(); mRenderThread.Stop(); mOutput.ReleaseResources(); if (mStarted) Log("app", "RenderCadenceCompositor shutdown complete."); mStarted = false; } bool Started() const { return mStarted; } const DeckLinkOutput& Output() const { return mOutput; } void SetDeckLinkInputMetricsProvider(std::function provider) { mDeckLinkInputMetricsProvider = std::move(provider); } private: void StartOptionalVideoOutput() { std::string outputError; Log("app", "Initializing optional DeckLink output."); if (!mOutput.Initialize( mConfig.deckLink, [this](const VideoIOCompletion& completion) { mFrameExchange.ReleaseScheduledByBytes(completion.outputFrameBuffer); }, outputError)) { DisableVideoOutput("DeckLink output unavailable: " + outputError); return; } Log("app", "Starting DeckLink output thread."); if (!mOutputThread.Start()) { DisableVideoOutput("DeckLink output thread failed to start."); return; } Log("app", "Waiting for DeckLink preroll frames."); if (!WaitForPreroll()) { DisableVideoOutput("Timed out waiting for DeckLink preroll frames."); return; } Log("app", "Starting DeckLink scheduled playback."); if (!mOutput.StartScheduledPlayback(outputError)) { DisableVideoOutput("DeckLink scheduled playback failed: " + outputError); return; } mVideoOutputEnabled = true; mVideoOutputStatus = "DeckLink scheduled output running."; Log("app", mVideoOutputStatus); } void DisableVideoOutput(const std::string& reason) { mOutputThread.Stop(); mOutput.Stop(); mOutput.ReleaseResources(); mFrameExchange.Clear(); mVideoOutputEnabled = false; mVideoOutputStatus = reason; LogWarning("app", reason + " Continuing without video output."); } void StartHttpServer() { HttpControlServerCallbacks callbacks; callbacks.getStateJson = [this]() { return BuildStateJson(); }; callbacks.addLayer = [this](const std::string& body) { return mRuntimeLayers.HandleAddLayer(body); }; callbacks.removeLayer = [this](const std::string& body) { return mRuntimeLayers.HandleRemoveLayer(body); }; callbacks.executePost = [this](const std::string& path, const std::string& body) { RuntimeControlCommand command; std::string error; if (!ParseRuntimeControlCommand(path, body, command, error)) return ControlActionResult{ false, error }; return mRuntimeLayers.HandleControlCommand(command); }; std::string error; if (!mHttpServer.Start( FindRepoPath("ui/dist"), FindRepoPath("docs"), mConfig.http, callbacks, error)) { LogWarning("http", "HTTP control server did not start: " + error); return; } } std::string BuildStateJson() { CadenceTelemetrySnapshot telemetry = mHttpTelemetry.Sample(mFrameExchange, mOutput, mOutputThread, mRenderThread); ApplyDeckLinkInputMetrics(telemetry); RuntimeLayerModelSnapshot layerSnapshot = mRuntimeLayers.Snapshot(telemetry); return RuntimeStateToJson(RuntimeStateJsonInput{ mConfig, telemetry, mHttpServer.Port(), mVideoOutputEnabled, mVideoOutputStatus, mRuntimeLayers.ShaderCatalog(), layerSnapshot }); } void ApplyDeckLinkInputMetrics(CadenceTelemetrySnapshot& telemetry) { if (!mDeckLinkInputMetricsProvider) return; const DeckLinkInputMetrics inputMetrics = mDeckLinkInputMetricsProvider(); telemetry.inputConvertMilliseconds = inputMetrics.convertMilliseconds; telemetry.inputSubmitMilliseconds = inputMetrics.submitMilliseconds; telemetry.inputNoSignalFrames = inputMetrics.noInputSourceFrames; telemetry.inputUnsupportedFrames = inputMetrics.unsupportedFrames; telemetry.inputSubmitMisses = inputMetrics.submitMisses; telemetry.inputCaptureFormat = inputMetrics.captureFormat ? inputMetrics.captureFormat : "none"; if (telemetry.sampleSeconds > 0.0) telemetry.inputCaptureFps = static_cast(inputMetrics.capturedFrames - mLastInputCapturedFrames) / telemetry.sampleSeconds; mLastInputCapturedFrames = inputMetrics.capturedFrames; } bool WaitForPreroll() const { const auto deadline = std::chrono::steady_clock::now() + mConfig.prerollTimeout; while (std::chrono::steady_clock::now() < deadline) { if (mFrameExchange.Metrics().scheduledCount >= mConfig.outputThread.targetBufferedFrames) return true; std::this_thread::sleep_for(mConfig.prerollPoll); } return false; } RenderThread& mRenderThread; SystemFrameExchange& mFrameExchange; AppConfig mConfig; DeckLinkOutput mOutput; DeckLinkOutputThread mOutputThread; TelemetryHealthMonitor mTelemetryHealth; CadenceTelemetry mHttpTelemetry; HttpControlServer mHttpServer; RuntimeLayerController mRuntimeLayers; std::function mDeckLinkInputMetricsProvider; uint64_t mLastInputCapturedFrames = 0; bool mStarted = false; bool mVideoOutputEnabled = false; std::string mVideoOutputStatus = "DeckLink output not started."; }; }