289 lines
8.4 KiB
C++
289 lines
8.4 KiB
C++
#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 <chrono>
|
|
#include <filesystem>
|
|
#include <functional>
|
|
#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),
|
|
mRuntimeLayers([this](const std::vector<RuntimeRenderLayerModel>& 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<unsigned>(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;
|
|
}
|
|
|
|
if (mConfig.startupSettle > std::chrono::milliseconds::zero())
|
|
{
|
|
Log("app", "Settling render cadence before DeckLink output for " + std::to_string(mConfig.startupSettle.count()) + " ms.");
|
|
std::this_thread::sleep_for(mConfig.startupSettle);
|
|
Log("app", "Waiting for rendered reserve after startup settle.");
|
|
if (!mFrameExchange.WaitForCompletedDepth(mConfig.warmupCompletedFrames, mConfig.warmupTimeout))
|
|
{
|
|
error = "Timed out waiting for rendered reserve after startup settle.";
|
|
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<DeckLinkInputMetrics()> 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<double>(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<SystemFrameExchange> mOutputThread;
|
|
TelemetryHealthMonitor mTelemetryHealth;
|
|
CadenceTelemetry mHttpTelemetry;
|
|
HttpControlServer mHttpServer;
|
|
RuntimeLayerController mRuntimeLayers;
|
|
std::function<DeckLinkInputMetrics()> mDeckLinkInputMetricsProvider;
|
|
uint64_t mLastInputCapturedFrames = 0;
|
|
bool mStarted = false;
|
|
bool mVideoOutputEnabled = false;
|
|
std::string mVideoOutputStatus = "DeckLink output not started.";
|
|
};
|
|
}
|