#pragma once #include "AppConfig.h" #include "AppConfigProvider.h" #include "../logging/Logger.h" #include "../runtime/RuntimeLayerModel.h" #include "../runtime/RuntimeShaderBridge.h" #include "../runtime/SupportedShaderCatalog.h" #include "../control/RuntimeStateJson.h" #include "../telemetry/TelemetryHealthMonitor.h" #include "../video/DeckLinkOutput.h" #include "../video/DeckLinkOutputThread.h" #include "RuntimeJson.h" #include #include #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) { } RenderCadenceApp(const RenderCadenceApp&) = delete; RenderCadenceApp& operator=(const RenderCadenceApp&) = delete; ~RenderCadenceApp() { Stop(); } bool Start(std::string& error) { LoadSupportedShaderCatalog(); InitializeRuntimeLayerModel(); Log("app", "Starting render thread."); if (!detail::StartRenderThread(mRenderThread, error, 0)) { LogError("app", "Render thread start failed: " + error); Stop(); return false; } StartRuntimeShaderBuild(); 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(); StopRuntimeShaderBuild(); mRenderThread.Stop(); mOutput.ReleaseResources(); if (mStarted) Log("app", "RenderCadenceCompositor shutdown complete."); mStarted = false; } bool Started() const { return mStarted; } const DeckLinkOutput& Output() const { return mOutput; } 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 HandleAddLayer(body); }; callbacks.removeLayer = [this](const std::string& body) { return HandleRemoveLayer(body); }; 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); RuntimeLayerModelSnapshot layerSnapshot = CopyRuntimeLayerSnapshot(telemetry); return RuntimeStateToJson(RuntimeStateJsonInput{ mConfig, telemetry, mHttpServer.Port(), mVideoOutputEnabled, mVideoOutputStatus, mShaderCatalog, layerSnapshot }); } 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; } void StartRuntimeShaderBuild() { if (mConfig.runtimeShaderId.empty()) { Log("runtime-shader", "Runtime shader build disabled."); return; } Log("runtime-shader", "Starting background Slang build for shader '" + mConfig.runtimeShaderId + "'."); const std::string layerId = FirstRuntimeLayerId(); if (!layerId.empty()) StartLayerShaderBuild(layerId, mConfig.runtimeShaderId); } void LoadSupportedShaderCatalog() { const std::filesystem::path shaderRoot = FindRepoPath(mConfig.shaderLibrary); std::string error; if (!mShaderCatalog.Load(shaderRoot, static_cast(mConfig.maxTemporalHistoryFrames), error)) { LogWarning("runtime-shader", "Supported shader catalog is empty: " + error); return; } Log("runtime-shader", "Supported shader catalog loaded with " + std::to_string(mShaderCatalog.Shaders().size()) + " shader(s)."); } void InitializeRuntimeLayerModel() { std::lock_guard lock(mRuntimeLayerMutex); std::string error; if (!mRuntimeLayerModel.InitializeSingleLayer(mShaderCatalog, mConfig.runtimeShaderId, error)) { LogWarning("runtime-shader", error + " Runtime shader build disabled."); mConfig.runtimeShaderId.clear(); mRuntimeLayerModel.Clear(); } } void StopRuntimeShaderBuild() { StopAllRuntimeShaderBuilds(); } void MarkRuntimeBuildStarted(const std::string& message) { std::lock_guard lock(mRuntimeLayerMutex); std::string error; const std::string layerId = mRuntimeLayerModel.FirstLayerId(); if (!layerId.empty()) mRuntimeLayerModel.MarkBuildStarted(layerId, message, error); } bool MarkRuntimeBuildReady(const RuntimeShaderArtifact& artifact) { std::lock_guard lock(mRuntimeLayerMutex); std::string error; if (!mRuntimeLayerModel.MarkBuildReady(artifact, error)) { LogWarning("runtime-shader", error); return false; } return true; } void MarkRuntimeBuildFailed(const std::string& message) { std::lock_guard lock(mRuntimeLayerMutex); if (!mRuntimeLayerModel.MarkBuildFailedForShader(mConfig.runtimeShaderId, message)) LogWarning("runtime-shader", "Runtime shader failed without a matching display layer: " + message); } void MarkRuntimeBuildFailedForLayer(const std::string& layerId, const std::string& message) { std::lock_guard lock(mRuntimeLayerMutex); std::string error; if (!mRuntimeLayerModel.MarkBuildFailed(layerId, message, error)) LogWarning("runtime-shader", error); } std::string FirstRuntimeLayerId() const { std::lock_guard lock(mRuntimeLayerMutex); return mRuntimeLayerModel.FirstLayerId(); } void StartLayerShaderBuild(const std::string& layerId, const std::string& shaderId) { { std::lock_guard lock(mRuntimeLayerMutex); std::string error; mRuntimeLayerModel.MarkBuildStarted(layerId, "Runtime Slang build started for shader '" + shaderId + "'.", error); } auto bridge = std::make_unique(); RuntimeShaderBridge* bridgePtr = bridge.get(); { std::lock_guard lock(mShaderBuildMutex); auto existingIt = mShaderBuilds.find(layerId); if (existingIt != mShaderBuilds.end()) existingIt->second->Stop(); mShaderBuilds[layerId] = std::move(bridge); } bridgePtr->Start( layerId, shaderId, [this](const RuntimeShaderArtifact& artifact) { if (MarkRuntimeBuildReady(artifact)) PublishRuntimeRenderLayers(); }, [this, layerId](const std::string& message) { MarkRuntimeBuildFailedForLayer(layerId, message); LogError("runtime-shader", "Runtime Slang build failed: " + message); }); } void StopLayerShaderBuild(const std::string& layerId) { std::unique_ptr bridge; { std::lock_guard lock(mShaderBuildMutex); auto bridgeIt = mShaderBuilds.find(layerId); if (bridgeIt == mShaderBuilds.end()) return; bridge = std::move(bridgeIt->second); mShaderBuilds.erase(bridgeIt); } bridge->Stop(); } void StopAllRuntimeShaderBuilds() { std::map> builds; { std::lock_guard lock(mShaderBuildMutex); builds.swap(mShaderBuilds); } for (auto& entry : builds) entry.second->Stop(); } ControlActionResult HandleAddLayer(const std::string& body) { std::string shaderId; std::string error; if (!ExtractStringField(body, "shaderId", shaderId, error)) return { false, error }; std::string layerId; { std::lock_guard lock(mRuntimeLayerMutex); if (!mRuntimeLayerModel.AddLayer(mShaderCatalog, shaderId, layerId, error)) return { false, error }; } Log("runtime-shader", "Layer added: " + layerId + " shader=" + shaderId); StartLayerShaderBuild(layerId, shaderId); return { true, std::string() }; } ControlActionResult HandleRemoveLayer(const std::string& body) { std::string layerId; std::string error; if (!ExtractStringField(body, "layerId", layerId, error)) return { false, error }; { std::lock_guard lock(mRuntimeLayerMutex); if (!mRuntimeLayerModel.RemoveLayer(layerId, error)) return { false, error }; } Log("runtime-shader", "Layer removed: " + layerId); StopLayerShaderBuild(layerId); PublishRuntimeRenderLayers(); return { true, std::string() }; } void PublishRuntimeRenderLayers() { std::vector renderLayers; { std::lock_guard lock(mRuntimeLayerMutex); renderLayers = mRuntimeLayerModel.Snapshot().renderLayers; } mRenderThread.SubmitRuntimeRenderLayers(renderLayers); } static bool ExtractStringField(const std::string& body, const char* fieldName, std::string& value, std::string& error) { JsonValue root; std::string parseError; if (!ParseJson(body.empty() ? "{}" : body, root, parseError) || !root.isObject()) { error = parseError.empty() ? "Request body must be a JSON object." : parseError; return false; } const JsonValue* field = root.find(fieldName); if (!field || !field->isString() || field->asString().empty()) { error = std::string("Request field '") + fieldName + "' must be a non-empty string."; return false; } value = field->asString(); error.clear(); return true; } RuntimeLayerModelSnapshot CopyRuntimeLayerSnapshot(const CadenceTelemetrySnapshot& telemetry) const { std::lock_guard lock(mRuntimeLayerMutex); RuntimeLayerModelSnapshot snapshot = mRuntimeLayerModel.Snapshot(); if (telemetry.shaderBuildFailures > 0) { snapshot.compileSucceeded = false; snapshot.compileMessage = "Runtime shader GL commit failed; see logs for details."; } return snapshot; } RenderThread& mRenderThread; SystemFrameExchange& mFrameExchange; AppConfig mConfig; DeckLinkOutput mOutput; DeckLinkOutputThread mOutputThread; TelemetryHealthMonitor mTelemetryHealth; CadenceTelemetry mHttpTelemetry; HttpControlServer mHttpServer; SupportedShaderCatalog mShaderCatalog; mutable std::mutex mRuntimeLayerMutex; RuntimeLayerModel mRuntimeLayerModel; std::mutex mShaderBuildMutex; std::map> mShaderBuilds; bool mStarted = false; bool mVideoOutputEnabled = false; std::string mVideoOutputStatus = "DeckLink output not started."; }; }