485 lines
16 KiB
C++
485 lines
16 KiB
C++
#pragma once
|
|
|
|
#include "../app/AppConfig.h"
|
|
#include "../app/AppConfigProvider.h"
|
|
#include "../control/osc/OscControlServer.h"
|
|
#include "../json/JsonWriter.h"
|
|
#include "RuntimeLayerModel.h"
|
|
#include "SupportedShaderCatalog.h"
|
|
#include "../telemetry/CadenceTelemetryJson.h"
|
|
|
|
#include <cstdint>
|
|
#include <functional>
|
|
#include <string>
|
|
#include <vector>
|
|
|
|
namespace RenderCadenceCompositor
|
|
{
|
|
struct RuntimeStateJsonInput
|
|
{
|
|
const AppConfig& config;
|
|
const CadenceTelemetrySnapshot& telemetry;
|
|
unsigned short serverPort = 0;
|
|
bool videoOutputEnabled = false;
|
|
std::string videoOutputStatus;
|
|
const OscControlServerState* osc = nullptr;
|
|
std::function<void(JsonWriter&)> writeRuntimeJson;
|
|
std::function<void(JsonWriter&)> writeCatalogJson;
|
|
std::function<void(JsonWriter&)> writeLayersJson;
|
|
};
|
|
|
|
inline void WriteVideoIoStatusJson(JsonWriter& writer, const RuntimeStateJsonInput& input)
|
|
{
|
|
writer.BeginObject();
|
|
writer.KeyString("backend", input.config.output.backend);
|
|
writer.KeyNull("modelName");
|
|
writer.KeyBool("supportsInternalKeying", false);
|
|
writer.KeyBool("supportsExternalKeying", false);
|
|
writer.KeyBool("keyerInterfaceAvailable", false);
|
|
writer.KeyBool("externalKeyingRequested", input.config.output.externalKeyingEnabled);
|
|
writer.KeyBool("externalKeyingActive", input.videoOutputEnabled && input.config.output.externalKeyingEnabled);
|
|
writer.KeyString("statusMessage", input.videoOutputStatus);
|
|
writer.EndObject();
|
|
}
|
|
|
|
inline void WriteVideoOutputBackendMetricsJson(JsonWriter& writer, const RuntimeStateJsonInput& input)
|
|
{
|
|
writer.BeginObject();
|
|
if (input.config.output.backend == "decklink")
|
|
{
|
|
writer.KeyBool("bufferedAvailable", input.telemetry.deckLinkBufferedAvailable);
|
|
writer.Key("buffered");
|
|
if (input.telemetry.deckLinkBufferedAvailable)
|
|
writer.UInt(input.telemetry.deckLinkBuffered);
|
|
else
|
|
writer.Null();
|
|
writer.KeyDouble("scheduleCallMs", input.telemetry.deckLinkScheduleCallMilliseconds);
|
|
writer.KeyBool("scheduleLeadAvailable", input.telemetry.deckLinkScheduleLeadAvailable);
|
|
writer.Key("scheduleLeadFrames");
|
|
if (input.telemetry.deckLinkScheduleLeadAvailable)
|
|
writer.Int(input.telemetry.deckLinkScheduleLeadFrames);
|
|
else
|
|
writer.Null();
|
|
writer.KeyUInt("playbackFrameIndex", input.telemetry.deckLinkPlaybackFrameIndex);
|
|
writer.KeyUInt("nextScheduleFrameIndex", input.telemetry.deckLinkNextScheduleFrameIndex);
|
|
writer.KeyInt("playbackStreamTime", input.telemetry.deckLinkPlaybackStreamTime);
|
|
writer.KeyUInt("scheduleRealignments", input.telemetry.deckLinkScheduleRealignments);
|
|
}
|
|
writer.EndObject();
|
|
}
|
|
|
|
inline void WriteVideoOutputTelemetryJson(JsonWriter& writer, const RuntimeStateJsonInput& input)
|
|
{
|
|
writer.BeginObject();
|
|
writer.KeyBool("enabled", input.videoOutputEnabled);
|
|
writer.KeyString("backend", input.config.output.backend);
|
|
writer.KeyString("statusMessage", input.videoOutputStatus);
|
|
writer.KeyUInt("scheduleFailures", input.telemetry.scheduleFailures);
|
|
writer.KeyUInt("completions", input.telemetry.completions);
|
|
writer.KeyUInt("late", input.telemetry.displayedLate);
|
|
writer.KeyUInt("dropped", input.telemetry.dropped);
|
|
writer.Key("backendMetrics");
|
|
WriteVideoOutputBackendMetricsJson(writer, input);
|
|
writer.EndObject();
|
|
}
|
|
|
|
inline void OutputDimensions(const RuntimeStateJsonInput& input, unsigned& width, unsigned& height)
|
|
{
|
|
VideoFormatDimensions(input.config.output.resolution, width, height);
|
|
}
|
|
|
|
inline const char* ShaderParameterTypeName(ShaderParameterType type)
|
|
{
|
|
switch (type)
|
|
{
|
|
case ShaderParameterType::Float: return "float";
|
|
case ShaderParameterType::Vec2: return "vec2";
|
|
case ShaderParameterType::Color: return "color";
|
|
case ShaderParameterType::Boolean: return "bool";
|
|
case ShaderParameterType::Enum: return "enum";
|
|
case ShaderParameterType::Text: return "text";
|
|
case ShaderParameterType::Trigger: return "trigger";
|
|
}
|
|
return "unknown";
|
|
}
|
|
|
|
inline void WriteNumberArray(JsonWriter& writer, const std::vector<double>& values)
|
|
{
|
|
writer.BeginArray();
|
|
for (double value : values)
|
|
writer.Double(value);
|
|
writer.EndArray();
|
|
}
|
|
|
|
inline void WriteDefaultParameterValue(JsonWriter& writer, const ShaderParameterDefinition& parameter)
|
|
{
|
|
switch (parameter.type)
|
|
{
|
|
case ShaderParameterType::Boolean:
|
|
writer.Bool(parameter.defaultBoolean);
|
|
return;
|
|
case ShaderParameterType::Enum:
|
|
writer.String(parameter.defaultEnumValue);
|
|
return;
|
|
case ShaderParameterType::Text:
|
|
writer.String(parameter.defaultTextValue);
|
|
return;
|
|
case ShaderParameterType::Trigger:
|
|
writer.Double(0.0);
|
|
return;
|
|
case ShaderParameterType::Float:
|
|
writer.Double(parameter.defaultNumbers.empty() ? 0.0 : parameter.defaultNumbers.front());
|
|
return;
|
|
case ShaderParameterType::Vec2:
|
|
case ShaderParameterType::Color:
|
|
WriteNumberArray(writer, parameter.defaultNumbers);
|
|
return;
|
|
}
|
|
writer.Null();
|
|
}
|
|
|
|
inline void WriteParameterValue(JsonWriter& writer, const ShaderParameterDefinition& parameter, const ShaderParameterValue& value)
|
|
{
|
|
switch (parameter.type)
|
|
{
|
|
case ShaderParameterType::Boolean:
|
|
writer.Bool(value.booleanValue);
|
|
return;
|
|
case ShaderParameterType::Enum:
|
|
writer.String(value.enumValue);
|
|
return;
|
|
case ShaderParameterType::Text:
|
|
writer.String(value.textValue);
|
|
return;
|
|
case ShaderParameterType::Trigger:
|
|
case ShaderParameterType::Float:
|
|
writer.Double(value.numberValues.empty() ? 0.0 : value.numberValues.front());
|
|
return;
|
|
case ShaderParameterType::Vec2:
|
|
case ShaderParameterType::Color:
|
|
WriteNumberArray(writer, value.numberValues);
|
|
return;
|
|
}
|
|
writer.Null();
|
|
}
|
|
|
|
inline void WriteTemporalJson(JsonWriter& writer, const TemporalSettings& temporal)
|
|
{
|
|
writer.BeginObject();
|
|
writer.KeyBool("enabled", temporal.enabled);
|
|
writer.KeyString("historySource", "none");
|
|
writer.KeyUInt("requestedHistoryLength", temporal.requestedHistoryLength);
|
|
writer.KeyUInt("effectiveHistoryLength", temporal.effectiveHistoryLength);
|
|
writer.EndObject();
|
|
}
|
|
|
|
inline void WriteFeedbackJson(JsonWriter& writer, const FeedbackSettings& feedback)
|
|
{
|
|
writer.BeginObject();
|
|
writer.KeyBool("enabled", feedback.enabled);
|
|
writer.KeyString("writePass", feedback.writePassId);
|
|
writer.EndObject();
|
|
}
|
|
|
|
inline std::string ShaderAssetUrl(const std::string& shaderId, const std::string& assetPath)
|
|
{
|
|
return "/shader-assets/" + shaderId + "/" + assetPath;
|
|
}
|
|
|
|
inline void WriteShaderUiJson(JsonWriter& writer, const std::string& shaderId, const ShaderUiDefinition& ui)
|
|
{
|
|
if (!ui.enabled)
|
|
{
|
|
writer.Null();
|
|
return;
|
|
}
|
|
|
|
writer.BeginObject();
|
|
writer.KeyString("type", ui.type);
|
|
writer.KeyString("entry", ui.entryPath);
|
|
writer.KeyString("tag", ui.customElementTag);
|
|
writer.KeyString("assetUrl", ShaderAssetUrl(shaderId, ui.entryPath));
|
|
writer.EndObject();
|
|
}
|
|
|
|
inline const char* RuntimeLayerBuildStateName(RuntimeLayerBuildState state)
|
|
{
|
|
switch (state)
|
|
{
|
|
case RuntimeLayerBuildState::Pending: return "pending";
|
|
case RuntimeLayerBuildState::Ready: return "ready";
|
|
case RuntimeLayerBuildState::Failed: return "failed";
|
|
}
|
|
return "unknown";
|
|
}
|
|
|
|
inline void WriteParameterDefinitionJson(JsonWriter& writer, const ShaderParameterDefinition& parameter, const ShaderParameterValue* value)
|
|
{
|
|
writer.BeginObject();
|
|
writer.KeyString("id", parameter.id);
|
|
writer.KeyString("label", parameter.label.empty() ? parameter.id : parameter.label);
|
|
writer.KeyString("description", parameter.description);
|
|
writer.KeyString("type", ShaderParameterTypeName(parameter.type));
|
|
writer.Key("defaultValue");
|
|
WriteDefaultParameterValue(writer, parameter);
|
|
writer.Key("value");
|
|
if (value)
|
|
WriteParameterValue(writer, parameter, *value);
|
|
else
|
|
WriteDefaultParameterValue(writer, parameter);
|
|
|
|
if (!parameter.minNumbers.empty())
|
|
{
|
|
writer.Key("min");
|
|
WriteNumberArray(writer, parameter.minNumbers);
|
|
}
|
|
if (!parameter.maxNumbers.empty())
|
|
{
|
|
writer.Key("max");
|
|
WriteNumberArray(writer, parameter.maxNumbers);
|
|
}
|
|
if (!parameter.stepNumbers.empty())
|
|
{
|
|
writer.Key("step");
|
|
WriteNumberArray(writer, parameter.stepNumbers);
|
|
}
|
|
if (parameter.type == ShaderParameterType::Enum)
|
|
{
|
|
writer.Key("options");
|
|
writer.BeginArray();
|
|
for (const ShaderParameterOption& option : parameter.enumOptions)
|
|
{
|
|
writer.BeginObject();
|
|
writer.KeyString("value", option.value);
|
|
writer.KeyString("label", option.label.empty() ? option.value : option.label);
|
|
writer.EndObject();
|
|
}
|
|
writer.EndArray();
|
|
}
|
|
if (parameter.type == ShaderParameterType::Text)
|
|
{
|
|
writer.KeyUInt("maxLength", parameter.maxLength);
|
|
if (!parameter.fontId.empty())
|
|
writer.KeyString("font", parameter.fontId);
|
|
if (!parameter.fontParameterId.empty())
|
|
writer.KeyString("fontParameter", parameter.fontParameterId);
|
|
}
|
|
writer.EndObject();
|
|
}
|
|
|
|
inline void WriteShaderRuntimeJson(JsonWriter& writer, const RuntimeLayerModelSnapshot& runtimeLayers)
|
|
{
|
|
writer.BeginObject();
|
|
writer.KeyUInt("layerCount", static_cast<uint64_t>(runtimeLayers.displayLayers.size()));
|
|
writer.KeyBool("compileSucceeded", runtimeLayers.compileSucceeded);
|
|
writer.KeyString("compileMessage", runtimeLayers.compileMessage);
|
|
writer.EndObject();
|
|
}
|
|
|
|
inline void WriteEmptyRuntimeJson(JsonWriter& writer)
|
|
{
|
|
writer.BeginObject();
|
|
writer.KeyUInt("layerCount", 0);
|
|
writer.KeyBool("compileSucceeded", true);
|
|
writer.KeyString("compileMessage", "No runtime content controller is active.");
|
|
writer.EndObject();
|
|
}
|
|
|
|
inline void WriteShaderCatalogJson(JsonWriter& writer, const SupportedShaderCatalog& shaderCatalog)
|
|
{
|
|
writer.BeginArray();
|
|
for (const SupportedShaderSummary& shader : shaderCatalog.Shaders())
|
|
{
|
|
writer.BeginObject();
|
|
writer.KeyString("id", shader.id);
|
|
writer.KeyString("name", shader.name);
|
|
writer.KeyString("description", shader.description);
|
|
writer.KeyString("category", shader.category);
|
|
writer.KeyBool("available", true);
|
|
writer.KeyNull("error");
|
|
writer.Key("ui");
|
|
WriteShaderUiJson(writer, shader.id, shader.ui);
|
|
writer.EndObject();
|
|
}
|
|
writer.EndArray();
|
|
}
|
|
|
|
inline void WriteEmptyCatalogJson(JsonWriter& writer)
|
|
{
|
|
writer.BeginArray();
|
|
writer.EndArray();
|
|
}
|
|
|
|
inline void WriteRuntimeShaderLayersJson(
|
|
JsonWriter& writer,
|
|
const SupportedShaderCatalog& shaderCatalog,
|
|
const RuntimeLayerModelSnapshot& runtimeLayers)
|
|
{
|
|
writer.BeginArray();
|
|
for (const RuntimeLayerReadModel& layer : runtimeLayers.displayLayers)
|
|
{
|
|
const ShaderPackage* shaderPackage = shaderCatalog.FindPackage(layer.shaderId);
|
|
writer.BeginObject();
|
|
writer.KeyString("id", layer.id);
|
|
writer.KeyString("shaderId", layer.shaderId);
|
|
writer.KeyString("shaderName", layer.shaderName);
|
|
writer.KeyBool("bypass", layer.bypass);
|
|
writer.KeyString("buildState", RuntimeLayerBuildStateName(layer.buildState));
|
|
writer.KeyBool("renderReady", layer.renderReady);
|
|
writer.KeyString("message", layer.message);
|
|
writer.Key("ui");
|
|
if (shaderPackage)
|
|
WriteShaderUiJson(writer, shaderPackage->id, shaderPackage->ui);
|
|
else
|
|
writer.Null();
|
|
writer.Key("temporal");
|
|
if (shaderPackage)
|
|
WriteTemporalJson(writer, shaderPackage->temporal);
|
|
else
|
|
WriteTemporalJson(writer, TemporalSettings());
|
|
writer.Key("feedback");
|
|
if (shaderPackage)
|
|
WriteFeedbackJson(writer, shaderPackage->feedback);
|
|
else
|
|
WriteFeedbackJson(writer, FeedbackSettings());
|
|
writer.Key("parameters");
|
|
writer.BeginArray();
|
|
for (const ShaderParameterDefinition& parameter : layer.parameterDefinitions)
|
|
{
|
|
const auto valueIt = layer.parameterValues.find(parameter.id);
|
|
WriteParameterDefinitionJson(writer, parameter, valueIt == layer.parameterValues.end() ? nullptr : &valueIt->second);
|
|
}
|
|
writer.EndArray();
|
|
writer.EndObject();
|
|
}
|
|
writer.EndArray();
|
|
}
|
|
|
|
inline void WriteEmptyLayersJson(JsonWriter& writer)
|
|
{
|
|
writer.BeginArray();
|
|
writer.EndArray();
|
|
}
|
|
|
|
inline void WriteOscJson(JsonWriter& writer, const RuntimeStateJsonInput& input)
|
|
{
|
|
const bool configured = input.osc ? input.osc->configured : input.config.oscPort != 0;
|
|
const bool listening = input.osc ? input.osc->listening : false;
|
|
const std::string bindAddress = input.osc ? input.osc->bindAddress : input.config.oscBindAddress;
|
|
const unsigned short port = input.osc ? input.osc->port : input.config.oscPort;
|
|
const double smoothing = input.osc ? input.osc->smoothing : input.config.oscSmoothing;
|
|
const std::string status = input.osc
|
|
? input.osc->statusMessage
|
|
: (configured ? "OSC ingress is not implemented yet." : "OSC ingress disabled by config.");
|
|
|
|
writer.BeginObject();
|
|
writer.KeyBool("configured", configured);
|
|
writer.KeyBool("listening", listening);
|
|
writer.KeyString("bindAddress", bindAddress);
|
|
writer.KeyUInt("port", port);
|
|
writer.KeyDouble("smoothing", smoothing);
|
|
writer.KeyString("statusMessage", status);
|
|
writer.EndObject();
|
|
}
|
|
|
|
inline std::string RuntimeStateToJson(const RuntimeStateJsonInput& input)
|
|
{
|
|
JsonWriter writer;
|
|
writer.BeginObject();
|
|
|
|
writer.Key("app");
|
|
writer.BeginObject();
|
|
writer.KeyUInt("serverPort", input.serverPort);
|
|
writer.KeyUInt("oscPort", input.config.oscPort);
|
|
writer.KeyString("oscBindAddress", input.config.oscBindAddress);
|
|
writer.KeyDouble("oscSmoothing", input.config.oscSmoothing);
|
|
writer.KeyBool("autoReload", input.config.autoReload);
|
|
writer.KeyUInt("maxTemporalHistoryFrames", static_cast<uint64_t>(input.config.maxTemporalHistoryFrames));
|
|
writer.KeyDouble("previewFps", input.config.previewFps);
|
|
writer.Key("osc");
|
|
WriteOscJson(writer, input);
|
|
writer.Key("input");
|
|
writer.BeginObject();
|
|
writer.KeyString("backend", input.config.input.backend);
|
|
writer.KeyString("device", input.config.input.device);
|
|
writer.KeyString("resolution", input.config.input.resolution);
|
|
writer.KeyString("frameRate", input.config.input.frameRate);
|
|
writer.EndObject();
|
|
writer.Key("output");
|
|
writer.BeginObject();
|
|
writer.KeyString("backend", input.config.output.backend);
|
|
writer.KeyString("device", input.config.output.device);
|
|
writer.KeyString("resolution", input.config.output.resolution);
|
|
writer.KeyString("frameRate", input.config.output.frameRate);
|
|
writer.KeyString("pixelFormat", input.config.output.pixelFormat);
|
|
writer.KeyString("systemFramePixelFormat", VideoIOPixelFormatName(input.config.output.systemFramePixelFormat));
|
|
writer.Key("keying");
|
|
writer.BeginObject();
|
|
writer.KeyBool("outputAlpha", input.config.output.outputAlphaRequired || input.config.output.externalKeyingEnabled);
|
|
writer.KeyBool("external", input.config.output.externalKeyingEnabled);
|
|
writer.KeyBool("alphaRequired", input.config.output.outputAlphaRequired);
|
|
writer.EndObject();
|
|
writer.EndObject();
|
|
writer.EndObject();
|
|
|
|
writer.Key("runtime");
|
|
if (input.writeRuntimeJson)
|
|
input.writeRuntimeJson(writer);
|
|
else
|
|
WriteEmptyRuntimeJson(writer);
|
|
|
|
writer.Key("video");
|
|
writer.BeginObject();
|
|
unsigned outputWidth = 0;
|
|
unsigned outputHeight = 0;
|
|
OutputDimensions(input, outputWidth, outputHeight);
|
|
writer.KeyBool("hasSignal", input.videoOutputEnabled);
|
|
writer.KeyUInt("width", outputWidth);
|
|
writer.KeyUInt("height", outputHeight);
|
|
writer.KeyString("modeName", input.config.output.resolution + " output-only");
|
|
writer.EndObject();
|
|
|
|
writer.Key("videoOutput");
|
|
WriteVideoOutputTelemetryJson(writer, input);
|
|
writer.Key("decklink");
|
|
WriteVideoIoStatusJson(writer, input);
|
|
writer.Key("videoIO");
|
|
WriteVideoIoStatusJson(writer, input);
|
|
|
|
writer.Key("performance");
|
|
writer.BeginObject();
|
|
writer.KeyDouble("frameBudgetMs", FrameDurationMillisecondsFromRateString(input.config.output.frameRate));
|
|
writer.KeyDouble("renderMs", input.telemetry.renderFrameMilliseconds);
|
|
writer.KeyNull("smoothedRenderMs");
|
|
writer.KeyDouble("budgetUsedPercent", input.telemetry.renderFrameBudgetUsedPercent);
|
|
writer.KeyNull("completionIntervalMs");
|
|
writer.KeyNull("smoothedCompletionIntervalMs");
|
|
writer.KeyNull("maxCompletionIntervalMs");
|
|
writer.KeyUInt("lateFrameCount", input.telemetry.displayedLate);
|
|
writer.KeyUInt("droppedFrameCount", input.telemetry.dropped);
|
|
writer.KeyNull("flushedFrameCount");
|
|
writer.Key("cadence");
|
|
WriteCadenceTelemetryJson(writer, input.telemetry);
|
|
writer.EndObject();
|
|
|
|
writer.KeyNull("backendPlayout");
|
|
writer.KeyNull("runtimeEvents");
|
|
writer.Key("shaders");
|
|
if (input.writeCatalogJson)
|
|
input.writeCatalogJson(writer);
|
|
else
|
|
WriteEmptyCatalogJson(writer);
|
|
writer.Key("stackPresets");
|
|
writer.BeginArray();
|
|
writer.EndArray();
|
|
writer.Key("layers");
|
|
if (input.writeLayersJson)
|
|
input.writeLayersJson(writer);
|
|
else
|
|
WriteEmptyLayersJson(writer);
|
|
|
|
writer.EndObject();
|
|
return writer.StringValue();
|
|
}
|
|
}
|