311 lines
8.7 KiB
C++
311 lines
8.7 KiB
C++
#pragma once
|
|
|
|
#include "AppConfig.h"
|
|
#include "AppConfigProvider.h"
|
|
#include "../logging/Logger.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 <chrono>
|
|
#include <filesystem>
|
|
#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();
|
|
|
|
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();
|
|
};
|
|
|
|
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);
|
|
RuntimeDisplayState runtimeState = CopyRuntimeDisplayState(telemetry);
|
|
return RuntimeStateToJson(RuntimeStateJsonInput{
|
|
mConfig,
|
|
telemetry,
|
|
mHttpServer.Port(),
|
|
mVideoOutputEnabled,
|
|
mVideoOutputStatus,
|
|
mShaderCatalog.Shaders(),
|
|
runtimeState.compileSucceeded,
|
|
runtimeState.compileMessage,
|
|
runtimeState.activeShaderPackage
|
|
});
|
|
}
|
|
|
|
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 + "'.");
|
|
SetRuntimeDisplayState(true, "Runtime Slang build started for shader '" + mConfig.runtimeShaderId + "'.", mConfig.runtimeShaderId);
|
|
mShaderBridge.Start(
|
|
mConfig.runtimeShaderId,
|
|
[this](const RuntimeShaderArtifact& artifact) {
|
|
SetRuntimeDisplayState(true, artifact.message.empty() ? "Runtime shader artifact is ready." : artifact.message, artifact.shaderId);
|
|
mRenderThread.SubmitRuntimeShaderArtifact(artifact);
|
|
},
|
|
[this](const std::string& message) {
|
|
SetRuntimeDisplayState(false, message);
|
|
LogError("runtime-shader", "Runtime Slang build failed: " + message);
|
|
});
|
|
}
|
|
|
|
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 StopRuntimeShaderBuild()
|
|
{
|
|
mShaderBridge.Stop();
|
|
}
|
|
|
|
struct RuntimeDisplayState
|
|
{
|
|
bool compileSucceeded = true;
|
|
std::string compileMessage;
|
|
const ShaderPackage* activeShaderPackage = nullptr;
|
|
};
|
|
|
|
void SetRuntimeDisplayState(bool compileSucceeded, const std::string& compileMessage, const std::string& activeShaderId = std::string())
|
|
{
|
|
std::lock_guard<std::mutex> lock(mRuntimeDisplayMutex);
|
|
mRuntimeCompileSucceeded = compileSucceeded;
|
|
mRuntimeCompileMessage = compileMessage;
|
|
if (!activeShaderId.empty())
|
|
mActiveShaderId = activeShaderId;
|
|
}
|
|
|
|
RuntimeDisplayState CopyRuntimeDisplayState(const CadenceTelemetrySnapshot& telemetry) const
|
|
{
|
|
std::lock_guard<std::mutex> lock(mRuntimeDisplayMutex);
|
|
RuntimeDisplayState state;
|
|
state.compileSucceeded = mRuntimeCompileSucceeded && telemetry.shaderBuildFailures == 0;
|
|
state.compileMessage = mRuntimeCompileMessage;
|
|
if (telemetry.shaderBuildFailures > 0)
|
|
state.compileMessage = "Runtime shader GL commit failed; see logs for details.";
|
|
if (state.compileMessage.empty())
|
|
state.compileMessage = mConfig.runtimeShaderId.empty()
|
|
? "Runtime shader build disabled."
|
|
: "Runtime shader build has not completed yet.";
|
|
state.activeShaderPackage = mShaderCatalog.FindPackage(mActiveShaderId);
|
|
return state;
|
|
}
|
|
|
|
RenderThread& mRenderThread;
|
|
SystemFrameExchange& mFrameExchange;
|
|
AppConfig mConfig;
|
|
DeckLinkOutput mOutput;
|
|
DeckLinkOutputThread<SystemFrameExchange> mOutputThread;
|
|
TelemetryHealthMonitor mTelemetryHealth;
|
|
CadenceTelemetry mHttpTelemetry;
|
|
HttpControlServer mHttpServer;
|
|
RuntimeShaderBridge mShaderBridge;
|
|
SupportedShaderCatalog mShaderCatalog;
|
|
mutable std::mutex mRuntimeDisplayMutex;
|
|
bool mRuntimeCompileSucceeded = true;
|
|
std::string mRuntimeCompileMessage;
|
|
std::string mActiveShaderId;
|
|
bool mStarted = false;
|
|
bool mVideoOutputEnabled = false;
|
|
std::string mVideoOutputStatus = "DeckLink output not started.";
|
|
};
|
|
}
|