Files
video-shader-toys/apps/RenderCadenceCompositor/app/RenderCadenceApp.h
Aiden d07ea1f63a
All checks were successful
CI / React UI Build (push) Successful in 11s
CI / Native Windows Build And Tests (push) Successful in 2m59s
CI / Windows Release Package (push) Has been skipped
Render changes
2026-05-12 14:36:36 +10:00

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.";
};
}