471 lines
13 KiB
C++
471 lines
13 KiB
C++
#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 <chrono>
|
|
#include <filesystem>
|
|
#include <map>
|
|
#include <memory>
|
|
#include <mutex>
|
|
#include <string>
|
|
#include <thread>
|
|
#include <type_traits>
|
|
|
|
namespace RenderCadenceCompositor
|
|
{
|
|
namespace detail
|
|
{
|
|
template <typename RenderThread>
|
|
auto StartRenderThread(RenderThread& renderThread, std::string& error, int) -> decltype(renderThread.Start(error), bool())
|
|
{
|
|
return renderThread.Start(error);
|
|
}
|
|
|
|
template <typename RenderThread>
|
|
bool StartRenderThreadWithoutError(RenderThread& renderThread, std::true_type)
|
|
{
|
|
return renderThread.Start();
|
|
}
|
|
|
|
template <typename RenderThread>
|
|
bool StartRenderThreadWithoutError(RenderThread& renderThread, std::false_type)
|
|
{
|
|
renderThread.Start();
|
|
return true;
|
|
}
|
|
|
|
template <typename RenderThread>
|
|
auto StartRenderThread(RenderThread& renderThread, std::string&, long) -> decltype(renderThread.Start(), bool())
|
|
{
|
|
return StartRenderThreadWithoutError(renderThread, std::is_same<decltype(renderThread.Start()), bool>());
|
|
}
|
|
}
|
|
|
|
template <typename RenderThread, typename SystemFrameExchange>
|
|
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<unsigned>(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<std::mutex> 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<std::mutex> 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<std::mutex> 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<std::mutex> 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<std::mutex> lock(mRuntimeLayerMutex);
|
|
std::string error;
|
|
if (!mRuntimeLayerModel.MarkBuildFailed(layerId, message, error))
|
|
LogWarning("runtime-shader", error);
|
|
}
|
|
|
|
std::string FirstRuntimeLayerId() const
|
|
{
|
|
std::lock_guard<std::mutex> lock(mRuntimeLayerMutex);
|
|
return mRuntimeLayerModel.FirstLayerId();
|
|
}
|
|
|
|
void StartLayerShaderBuild(const std::string& layerId, const std::string& shaderId)
|
|
{
|
|
{
|
|
std::lock_guard<std::mutex> lock(mRuntimeLayerMutex);
|
|
std::string error;
|
|
mRuntimeLayerModel.MarkBuildStarted(layerId, "Runtime Slang build started for shader '" + shaderId + "'.", error);
|
|
}
|
|
|
|
auto bridge = std::make_unique<RuntimeShaderBridge>();
|
|
RuntimeShaderBridge* bridgePtr = bridge.get();
|
|
{
|
|
std::lock_guard<std::mutex> 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<RuntimeShaderBridge> bridge;
|
|
{
|
|
std::lock_guard<std::mutex> 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<std::string, std::unique_ptr<RuntimeShaderBridge>> builds;
|
|
{
|
|
std::lock_guard<std::mutex> 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<std::mutex> 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<std::mutex> 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<RuntimeRenderLayerModel> renderLayers;
|
|
{
|
|
std::lock_guard<std::mutex> 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<std::mutex> 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<SystemFrameExchange> mOutputThread;
|
|
TelemetryHealthMonitor mTelemetryHealth;
|
|
CadenceTelemetry mHttpTelemetry;
|
|
HttpControlServer mHttpServer;
|
|
SupportedShaderCatalog mShaderCatalog;
|
|
mutable std::mutex mRuntimeLayerMutex;
|
|
RuntimeLayerModel mRuntimeLayerModel;
|
|
std::mutex mShaderBuildMutex;
|
|
std::map<std::string, std::unique_ptr<RuntimeShaderBridge>> mShaderBuilds;
|
|
bool mStarted = false;
|
|
bool mVideoOutputEnabled = false;
|
|
std::string mVideoOutputStatus = "DeckLink output not started.";
|
|
};
|
|
}
|