Clean up
This commit is contained in:
291
src/app/RenderCadenceApp.h
Normal file
291
src/app/RenderCadenceApp.h
Normal file
@@ -0,0 +1,291 @@
|
||||
#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.";
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user