#include "RuntimeStore.h" namespace { std::string TrimCopy(const std::string& text) { std::size_t start = 0; while (start < text.size() && std::isspace(static_cast(text[start]))) ++start; std::size_t end = text.size(); while (end > start && std::isspace(static_cast(text[end - 1]))) --end; return text.substr(start, end - start); } } RuntimeStore::RuntimeStore(RuntimeHost& runtimeHost) : mRuntimeHost(runtimeHost) { } bool RuntimeStore::InitializeStore(std::string& error) { try { std::lock_guard lock(mRuntimeHost.mMutex); if (!mRuntimeHost.ResolvePaths(error)) return false; if (!mRuntimeHost.LoadConfig(error)) return false; mRuntimeHost.mShaderRoot = mRuntimeHost.mRepoRoot / mRuntimeHost.mConfig.shaderLibrary; if (!mRuntimeHost.LoadPersistentState(error)) return false; if (!mRuntimeHost.ScanShaderPackages(error)) return false; mRuntimeHost.NormalizePersistentLayerIdsLocked(); for (RuntimeHost::LayerPersistentState& layer : mRuntimeHost.mPersistentState.layers) { auto shaderIt = mRuntimeHost.mPackagesById.find(layer.shaderId); if (shaderIt != mRuntimeHost.mPackagesById.end()) mRuntimeHost.EnsureLayerDefaultsLocked(layer, shaderIt->second); } if (mRuntimeHost.mPersistentState.layers.empty() && !mRuntimeHost.mPackageOrder.empty()) { RuntimeHost::LayerPersistentState layer; layer.id = mRuntimeHost.GenerateLayerId(); layer.shaderId = mRuntimeHost.mPackageOrder.front(); layer.bypass = false; mRuntimeHost.EnsureLayerDefaultsLocked(layer, mRuntimeHost.mPackagesById[layer.shaderId]); mRuntimeHost.mPersistentState.layers.push_back(layer); } mRuntimeHost.mServerPort = mRuntimeHost.mConfig.serverPort; mRuntimeHost.mAutoReloadEnabled = mRuntimeHost.mConfig.autoReload; mRuntimeHost.mReloadRequested = true; mRuntimeHost.mCompileMessage = "Waiting for shader compile."; return true; } catch (const std::exception& exception) { error = std::string("RuntimeStore::InitializeStore exception: ") + exception.what(); return false; } catch (...) { error = "RuntimeStore::InitializeStore threw a non-standard exception."; return false; } } std::string RuntimeStore::BuildPersistentStateJson() const { return SerializeJson(BuildRuntimeStateValue(), true); } bool RuntimeStore::CreateStoredLayer(const std::string& shaderId, std::string& error) { std::lock_guard lock(mRuntimeHost.mMutex); auto shaderIt = mRuntimeHost.mPackagesById.find(shaderId); if (shaderIt == mRuntimeHost.mPackagesById.end()) { error = "Unknown shader id: " + shaderId; return false; } RuntimeHost::LayerPersistentState layer; layer.id = mRuntimeHost.GenerateLayerId(); layer.shaderId = shaderId; layer.bypass = false; mRuntimeHost.EnsureLayerDefaultsLocked(layer, shaderIt->second); mRuntimeHost.mPersistentState.layers.push_back(layer); mRuntimeHost.mReloadRequested = true; mRuntimeHost.MarkRenderStateDirtyLocked(); return mRuntimeHost.SavePersistentState(error); } bool RuntimeStore::DeleteStoredLayer(const std::string& layerId, std::string& error) { std::lock_guard lock(mRuntimeHost.mMutex); auto it = std::find_if(mRuntimeHost.mPersistentState.layers.begin(), mRuntimeHost.mPersistentState.layers.end(), [&layerId](const RuntimeHost::LayerPersistentState& layer) { return layer.id == layerId; }); if (it == mRuntimeHost.mPersistentState.layers.end()) { error = "Unknown layer id: " + layerId; return false; } mRuntimeHost.mPersistentState.layers.erase(it); mRuntimeHost.mReloadRequested = true; mRuntimeHost.MarkRenderStateDirtyLocked(); return mRuntimeHost.SavePersistentState(error); } bool RuntimeStore::MoveStoredLayer(const std::string& layerId, int direction, std::string& error) { std::lock_guard lock(mRuntimeHost.mMutex); auto it = std::find_if(mRuntimeHost.mPersistentState.layers.begin(), mRuntimeHost.mPersistentState.layers.end(), [&layerId](const RuntimeHost::LayerPersistentState& layer) { return layer.id == layerId; }); if (it == mRuntimeHost.mPersistentState.layers.end()) { error = "Unknown layer id: " + layerId; return false; } const std::ptrdiff_t index = std::distance(mRuntimeHost.mPersistentState.layers.begin(), it); const std::ptrdiff_t newIndex = index + direction; if (newIndex < 0 || newIndex >= static_cast(mRuntimeHost.mPersistentState.layers.size())) return true; std::swap(mRuntimeHost.mPersistentState.layers[index], mRuntimeHost.mPersistentState.layers[newIndex]); mRuntimeHost.mReloadRequested = true; mRuntimeHost.MarkRenderStateDirtyLocked(); return mRuntimeHost.SavePersistentState(error); } bool RuntimeStore::MoveStoredLayerToIndex(const std::string& layerId, std::size_t targetIndex, std::string& error) { std::lock_guard lock(mRuntimeHost.mMutex); auto it = std::find_if(mRuntimeHost.mPersistentState.layers.begin(), mRuntimeHost.mPersistentState.layers.end(), [&layerId](const RuntimeHost::LayerPersistentState& layer) { return layer.id == layerId; }); if (it == mRuntimeHost.mPersistentState.layers.end()) { error = "Unknown layer id: " + layerId; return false; } if (mRuntimeHost.mPersistentState.layers.empty()) return true; if (targetIndex >= mRuntimeHost.mPersistentState.layers.size()) targetIndex = mRuntimeHost.mPersistentState.layers.size() - 1; const std::size_t sourceIndex = static_cast(std::distance(mRuntimeHost.mPersistentState.layers.begin(), it)); if (sourceIndex == targetIndex) return true; RuntimeHost::LayerPersistentState movedLayer = *it; mRuntimeHost.mPersistentState.layers.erase(mRuntimeHost.mPersistentState.layers.begin() + static_cast(sourceIndex)); mRuntimeHost.mPersistentState.layers.insert(mRuntimeHost.mPersistentState.layers.begin() + static_cast(targetIndex), movedLayer); mRuntimeHost.mReloadRequested = true; mRuntimeHost.MarkRenderStateDirtyLocked(); return mRuntimeHost.SavePersistentState(error); } bool RuntimeStore::SetStoredLayerBypassState(const std::string& layerId, bool bypassed, std::string& error) { std::lock_guard lock(mRuntimeHost.mMutex); RuntimeHost::LayerPersistentState* layer = mRuntimeHost.FindLayerById(layerId); if (!layer) { error = "Unknown layer id: " + layerId; return false; } layer->bypass = bypassed; mRuntimeHost.mReloadRequested = true; mRuntimeHost.MarkParameterStateDirtyLocked(); return mRuntimeHost.SavePersistentState(error); } bool RuntimeStore::SetStoredLayerShaderSelection(const std::string& layerId, const std::string& shaderId, std::string& error) { std::lock_guard lock(mRuntimeHost.mMutex); RuntimeHost::LayerPersistentState* layer = mRuntimeHost.FindLayerById(layerId); if (!layer) { error = "Unknown layer id: " + layerId; return false; } auto shaderIt = mRuntimeHost.mPackagesById.find(shaderId); if (shaderIt == mRuntimeHost.mPackagesById.end()) { error = "Unknown shader id: " + shaderId; return false; } layer->shaderId = shaderId; layer->parameterValues.clear(); mRuntimeHost.EnsureLayerDefaultsLocked(*layer, shaderIt->second); mRuntimeHost.mReloadRequested = true; mRuntimeHost.MarkRenderStateDirtyLocked(); return mRuntimeHost.SavePersistentState(error); } bool RuntimeStore::SetStoredParameterValue(const std::string& layerId, const std::string& parameterId, const JsonValue& newValue, std::string& error) { std::lock_guard lock(mRuntimeHost.mMutex); RuntimeHost::LayerPersistentState* layer = mRuntimeHost.FindLayerById(layerId); if (!layer) { error = "Unknown layer id: " + layerId; return false; } auto shaderIt = mRuntimeHost.mPackagesById.find(layer->shaderId); if (shaderIt == mRuntimeHost.mPackagesById.end()) { error = "Unknown shader id: " + layer->shaderId; return false; } const ShaderPackage& shaderPackage = shaderIt->second; auto parameterIt = std::find_if(shaderPackage.parameters.begin(), shaderPackage.parameters.end(), [¶meterId](const ShaderParameterDefinition& definition) { return definition.id == parameterId; }); if (parameterIt == shaderPackage.parameters.end()) { error = "Unknown parameter id: " + parameterId; return false; } if (parameterIt->type == ShaderParameterType::Trigger) { ShaderParameterValue& value = layer->parameterValues[parameterId]; const double previousCount = value.numberValues.empty() ? 0.0 : value.numberValues[0]; const double triggerTime = std::chrono::duration_cast>(std::chrono::steady_clock::now() - mRuntimeHost.mStartTime).count(); value.numberValues = { previousCount + 1.0, triggerTime }; mRuntimeHost.MarkParameterStateDirtyLocked(); return true; } ShaderParameterValue normalized; if (!mRuntimeHost.NormalizeAndValidateValue(*parameterIt, newValue, normalized, error)) return false; layer->parameterValues[parameterId] = normalized; mRuntimeHost.MarkParameterStateDirtyLocked(); return mRuntimeHost.SavePersistentState(error); } bool RuntimeStore::SetStoredParameterValueByControlKey(const std::string& layerKey, const std::string& parameterKey, const JsonValue& newValue, std::string& error) { return mRuntimeHost.UpdateLayerParameterByControlKey(layerKey, parameterKey, newValue, error); } bool RuntimeStore::SetStoredParameterValueByControlKey(const std::string& layerKey, const std::string& parameterKey, const JsonValue& newValue, bool persistState, std::string& error) { return mRuntimeHost.UpdateLayerParameterByControlKey(layerKey, parameterKey, newValue, persistState, error); } bool RuntimeStore::ResetStoredLayerParameterValues(const std::string& layerId, std::string& error) { std::lock_guard lock(mRuntimeHost.mMutex); RuntimeHost::LayerPersistentState* layer = mRuntimeHost.FindLayerById(layerId); if (!layer) { error = "Unknown layer id: " + layerId; return false; } auto shaderIt = mRuntimeHost.mPackagesById.find(layer->shaderId); if (shaderIt == mRuntimeHost.mPackagesById.end()) { error = "Unknown shader id: " + layer->shaderId; return false; } layer->parameterValues.clear(); mRuntimeHost.EnsureLayerDefaultsLocked(*layer, shaderIt->second); mRuntimeHost.MarkParameterStateDirtyLocked(); return mRuntimeHost.SavePersistentState(error); } bool RuntimeStore::SaveStackPresetSnapshot(const std::string& presetName, std::string& error) const { std::lock_guard lock(mRuntimeHost.mMutex); const std::string safeStem = mRuntimeHost.MakeSafePresetFileStem(presetName); if (safeStem.empty()) { error = "Preset name must include at least one letter or number."; return false; } JsonValue root = JsonValue::MakeObject(); root.set("version", JsonValue(1.0)); root.set("name", JsonValue(TrimCopy(presetName))); root.set("layers", mRuntimeHost.SerializeLayerStackLocked()); return mRuntimeHost.WriteTextFile(mRuntimeHost.mPresetRoot / (safeStem + ".json"), SerializeJson(root, true), error); } bool RuntimeStore::LoadStackPresetSnapshot(const std::string& presetName, std::string& error) { std::lock_guard lock(mRuntimeHost.mMutex); const std::string safeStem = mRuntimeHost.MakeSafePresetFileStem(presetName); if (safeStem.empty()) { error = "Preset name must include at least one letter or number."; return false; } const std::filesystem::path presetPath = mRuntimeHost.mPresetRoot / (safeStem + ".json"); std::string presetText = mRuntimeHost.ReadTextFile(presetPath, error); if (presetText.empty()) return false; JsonValue root; if (!ParseJson(presetText, root, error)) return false; const JsonValue* layersValue = root.find("layers"); if (!layersValue || !layersValue->isArray()) { error = "Preset file is missing a valid 'layers' array."; return false; } std::vector nextLayers; if (!mRuntimeHost.DeserializeLayerStackLocked(*layersValue, nextLayers, error)) return false; if (nextLayers.empty()) { error = "Preset does not contain any valid layers."; return false; } mRuntimeHost.mPersistentState.layers = nextLayers; mRuntimeHost.mReloadRequested = true; mRuntimeHost.MarkRenderStateDirtyLocked(); return mRuntimeHost.SavePersistentState(error); } const std::filesystem::path& RuntimeStore::GetRuntimeRepositoryRoot() const { return mRuntimeHost.GetRepoRoot(); } const std::filesystem::path& RuntimeStore::GetRuntimeUiRoot() const { return mRuntimeHost.GetUiRoot(); } const std::filesystem::path& RuntimeStore::GetRuntimeDocsRoot() const { return mRuntimeHost.GetDocsRoot(); } const std::filesystem::path& RuntimeStore::GetRuntimeDataRoot() const { return mRuntimeHost.GetRuntimeRoot(); } unsigned short RuntimeStore::GetConfiguredControlServerPort() const { return mRuntimeHost.GetServerPort(); } unsigned short RuntimeStore::GetConfiguredOscPort() const { return mRuntimeHost.GetOscPort(); } const std::string& RuntimeStore::GetConfiguredOscBindAddress() const { return mRuntimeHost.GetOscBindAddress(); } double RuntimeStore::GetConfiguredOscSmoothing() const { return mRuntimeHost.GetOscSmoothing(); } unsigned RuntimeStore::GetConfiguredMaxTemporalHistoryFrames() const { return mRuntimeHost.GetMaxTemporalHistoryFrames(); } unsigned RuntimeStore::GetConfiguredPreviewFps() const { return mRuntimeHost.GetPreviewFps(); } bool RuntimeStore::IsExternalKeyingConfigured() const { return mRuntimeHost.ExternalKeyingEnabled(); } const std::string& RuntimeStore::GetConfiguredInputVideoFormat() const { return mRuntimeHost.GetInputVideoFormat(); } const std::string& RuntimeStore::GetConfiguredInputFrameRate() const { return mRuntimeHost.GetInputFrameRate(); } const std::string& RuntimeStore::GetConfiguredOutputVideoFormat() const { return mRuntimeHost.GetOutputVideoFormat(); } const std::string& RuntimeStore::GetConfiguredOutputFrameRate() const { return mRuntimeHost.GetOutputFrameRate(); } void RuntimeStore::SetCompileStatus(bool succeeded, const std::string& message) { mRuntimeHost.SetCompileStatus(succeeded, message); } void RuntimeStore::ClearReloadRequest() { mRuntimeHost.ClearReloadRequest(); } JsonValue RuntimeStore::BuildRuntimeStateValue() const { const HealthTelemetry::Snapshot telemetrySnapshot = mRuntimeHost.mHealthTelemetry.GetSnapshot(); std::lock_guard lock(mRuntimeHost.mMutex); JsonValue root = JsonValue::MakeObject(); JsonValue app = JsonValue::MakeObject(); app.set("serverPort", JsonValue(static_cast(mRuntimeHost.mServerPort))); app.set("oscPort", JsonValue(static_cast(mRuntimeHost.mConfig.oscPort))); app.set("oscBindAddress", JsonValue(mRuntimeHost.mConfig.oscBindAddress)); app.set("oscSmoothing", JsonValue(mRuntimeHost.mConfig.oscSmoothing)); app.set("autoReload", JsonValue(mRuntimeHost.mAutoReloadEnabled)); app.set("maxTemporalHistoryFrames", JsonValue(static_cast(mRuntimeHost.mConfig.maxTemporalHistoryFrames))); app.set("previewFps", JsonValue(static_cast(mRuntimeHost.mConfig.previewFps))); app.set("enableExternalKeying", JsonValue(mRuntimeHost.mConfig.enableExternalKeying)); app.set("inputVideoFormat", JsonValue(mRuntimeHost.mConfig.inputVideoFormat)); app.set("inputFrameRate", JsonValue(mRuntimeHost.mConfig.inputFrameRate)); app.set("outputVideoFormat", JsonValue(mRuntimeHost.mConfig.outputVideoFormat)); app.set("outputFrameRate", JsonValue(mRuntimeHost.mConfig.outputFrameRate)); root.set("app", app); JsonValue runtime = JsonValue::MakeObject(); runtime.set("layerCount", JsonValue(static_cast(mRuntimeHost.mPersistentState.layers.size()))); runtime.set("compileSucceeded", JsonValue(mRuntimeHost.mCompileSucceeded)); runtime.set("compileMessage", JsonValue(mRuntimeHost.mCompileMessage)); root.set("runtime", runtime); JsonValue video = JsonValue::MakeObject(); video.set("hasSignal", JsonValue(telemetrySnapshot.signal.hasSignal)); video.set("width", JsonValue(static_cast(telemetrySnapshot.signal.width))); video.set("height", JsonValue(static_cast(telemetrySnapshot.signal.height))); video.set("modeName", JsonValue(telemetrySnapshot.signal.modeName)); root.set("video", video); JsonValue deckLink = JsonValue::MakeObject(); deckLink.set("modelName", JsonValue(telemetrySnapshot.videoIO.modelName)); deckLink.set("supportsInternalKeying", JsonValue(telemetrySnapshot.videoIO.supportsInternalKeying)); deckLink.set("supportsExternalKeying", JsonValue(telemetrySnapshot.videoIO.supportsExternalKeying)); deckLink.set("keyerInterfaceAvailable", JsonValue(telemetrySnapshot.videoIO.keyerInterfaceAvailable)); deckLink.set("externalKeyingRequested", JsonValue(telemetrySnapshot.videoIO.externalKeyingRequested)); deckLink.set("externalKeyingActive", JsonValue(telemetrySnapshot.videoIO.externalKeyingActive)); deckLink.set("statusMessage", JsonValue(telemetrySnapshot.videoIO.statusMessage)); root.set("decklink", deckLink); JsonValue videoIO = JsonValue::MakeObject(); videoIO.set("backend", JsonValue(telemetrySnapshot.videoIO.backendName)); videoIO.set("modelName", JsonValue(telemetrySnapshot.videoIO.modelName)); videoIO.set("supportsInternalKeying", JsonValue(telemetrySnapshot.videoIO.supportsInternalKeying)); videoIO.set("supportsExternalKeying", JsonValue(telemetrySnapshot.videoIO.supportsExternalKeying)); videoIO.set("keyerInterfaceAvailable", JsonValue(telemetrySnapshot.videoIO.keyerInterfaceAvailable)); videoIO.set("externalKeyingRequested", JsonValue(telemetrySnapshot.videoIO.externalKeyingRequested)); videoIO.set("externalKeyingActive", JsonValue(telemetrySnapshot.videoIO.externalKeyingActive)); videoIO.set("statusMessage", JsonValue(telemetrySnapshot.videoIO.statusMessage)); root.set("videoIO", videoIO); JsonValue performance = JsonValue::MakeObject(); performance.set("frameBudgetMs", JsonValue(telemetrySnapshot.performance.frameBudgetMilliseconds)); performance.set("renderMs", JsonValue(telemetrySnapshot.performance.renderMilliseconds)); performance.set("smoothedRenderMs", JsonValue(telemetrySnapshot.performance.smoothedRenderMilliseconds)); performance.set("budgetUsedPercent", JsonValue( telemetrySnapshot.performance.frameBudgetMilliseconds > 0.0 ? (telemetrySnapshot.performance.smoothedRenderMilliseconds / telemetrySnapshot.performance.frameBudgetMilliseconds) * 100.0 : 0.0)); performance.set("completionIntervalMs", JsonValue(telemetrySnapshot.performance.completionIntervalMilliseconds)); performance.set("smoothedCompletionIntervalMs", JsonValue(telemetrySnapshot.performance.smoothedCompletionIntervalMilliseconds)); performance.set("maxCompletionIntervalMs", JsonValue(telemetrySnapshot.performance.maxCompletionIntervalMilliseconds)); performance.set("lateFrameCount", JsonValue(static_cast(telemetrySnapshot.performance.lateFrameCount))); performance.set("droppedFrameCount", JsonValue(static_cast(telemetrySnapshot.performance.droppedFrameCount))); performance.set("flushedFrameCount", JsonValue(static_cast(telemetrySnapshot.performance.flushedFrameCount))); root.set("performance", performance); JsonValue shaderLibrary = JsonValue::MakeArray(); for (const ShaderPackageStatus& status : mRuntimeHost.mPackageStatuses) { JsonValue shader = JsonValue::MakeObject(); shader.set("id", JsonValue(status.id)); shader.set("name", JsonValue(status.displayName)); shader.set("description", JsonValue(status.description)); shader.set("category", JsonValue(status.category)); shader.set("available", JsonValue(status.available)); if (!status.available) shader.set("error", JsonValue(status.error)); auto shaderIt = mRuntimeHost.mPackagesById.find(status.id); if (status.available && shaderIt != mRuntimeHost.mPackagesById.end() && shaderIt->second.temporal.enabled) { JsonValue temporal = JsonValue::MakeObject(); temporal.set("enabled", JsonValue(true)); temporal.set("historySource", JsonValue(mRuntimeHost.TemporalHistorySourceToString(shaderIt->second.temporal.historySource))); temporal.set("requestedHistoryLength", JsonValue(static_cast(shaderIt->second.temporal.requestedHistoryLength))); temporal.set("effectiveHistoryLength", JsonValue(static_cast(shaderIt->second.temporal.effectiveHistoryLength))); shader.set("temporal", temporal); } if (status.available && shaderIt != mRuntimeHost.mPackagesById.end() && shaderIt->second.feedback.enabled) { JsonValue feedback = JsonValue::MakeObject(); feedback.set("enabled", JsonValue(true)); feedback.set("writePass", JsonValue(shaderIt->second.feedback.writePassId)); shader.set("feedback", feedback); } shaderLibrary.pushBack(shader); } root.set("shaders", shaderLibrary); JsonValue stackPresets = JsonValue::MakeArray(); for (const std::string& presetName : mRuntimeHost.GetStackPresetNamesLocked()) stackPresets.pushBack(JsonValue(presetName)); root.set("stackPresets", stackPresets); root.set("layers", SerializeLayerStack()); return root; } JsonValue RuntimeStore::SerializeLayerStack() const { return mRuntimeHost.SerializeLayerStackLocked(); }