Files
Aiden 5c1fc2a6cf
All checks were successful
CI / React UI Build (push) Successful in 10s
CI / Native Windows Build And Tests (push) Successful in 2m58s
CI / Windows Release Package (push) Has been skipped
telemetry and timing updates
2026-05-13 00:21:28 +10:00

292 lines
8.3 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);
if (!BuildSettledOutputReserve(error))
{
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);
Log(
"app",
"DeckLink output mode: " + mOutput.State().outputDisplayModeName +
", frame budget " + std::to_string(mOutput.State().frameBudgetMilliseconds) + " ms.");
}
bool BuildSettledOutputReserve(std::string& error)
{
const auto reserveTimeout = mConfig.warmupTimeout;
Log("app",
"Building output preroll reserve: waiting for " + std::to_string(mConfig.warmupCompletedFrames) +
" completed frame(s).");
if (mFrameExchange.WaitForCompletedDepth(mConfig.warmupCompletedFrames, reserveTimeout))
{
return true;
}
error = "Timed out waiting for output preroll reserve.";
return false;
}
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.";
};
}