#include "RuntimeStore.h" #include "RuntimeClock.h" #include "ShaderPackageRegistry.h" #include "RuntimeParameterUtils.h" #include #include #include #include #include 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); } std::string SimplifyControlKey(const std::string& text) { std::string simplified; for (unsigned char ch : text) { if (std::isalnum(ch)) simplified.push_back(static_cast(std::tolower(ch))); } return simplified; } bool MatchesControlKey(const std::string& candidate, const std::string& key) { return candidate == key || SimplifyControlKey(candidate) == SimplifyControlKey(key); } double Clamp01(double value) { return (std::max)(0.0, (std::min)(1.0, value)); } std::string ToLowerCopy(std::string text) { std::transform(text.begin(), text.end(), text.begin(), [](unsigned char ch) { return static_cast(std::tolower(ch)); }); return text; } bool LooksLikePackagedRuntimeRoot(const std::filesystem::path& candidate) { return std::filesystem::exists(candidate / "config" / "runtime-host.json") && std::filesystem::exists(candidate / "runtime" / "templates" / "shader_wrapper.slang.in") && std::filesystem::exists(candidate / "shaders"); } bool LooksLikeRepoRoot(const std::filesystem::path& candidate) { return std::filesystem::exists(candidate / "CMakeLists.txt") && std::filesystem::exists(candidate / "apps" / "LoopThroughWithOpenGLCompositing"); } std::filesystem::path FindRepoRootCandidate() { std::vector rootsToTry; char currentDirectory[MAX_PATH] = {}; if (GetCurrentDirectoryA(MAX_PATH, currentDirectory) > 0) rootsToTry.push_back(std::filesystem::path(currentDirectory)); char modulePath[MAX_PATH] = {}; DWORD moduleLength = GetModuleFileNameA(NULL, modulePath, MAX_PATH); if (moduleLength > 0 && moduleLength < MAX_PATH) rootsToTry.push_back(std::filesystem::path(modulePath).parent_path()); for (const std::filesystem::path& startPath : rootsToTry) { std::filesystem::path candidate = startPath; for (int depth = 0; depth < 10 && !candidate.empty(); ++depth) { if (LooksLikePackagedRuntimeRoot(candidate) || LooksLikeRepoRoot(candidate)) return candidate; candidate = candidate.parent_path(); } } return std::filesystem::path(); } bool TextureAssetsEqual(const std::vector& left, const std::vector& right) { if (left.size() != right.size()) return false; for (std::size_t index = 0; index < left.size(); ++index) { if (left[index].id != right[index].id || left[index].path != right[index].path || left[index].writeTime != right[index].writeTime) { return false; } } return true; } bool FontAssetsEqual(const std::vector& left, const std::vector& right) { if (left.size() != right.size()) return false; for (std::size_t index = 0; index < left.size(); ++index) { if (left[index].id != right[index].id || left[index].path != right[index].path || left[index].writeTime != right[index].writeTime) { return false; } } return true; } } RuntimeStore::RuntimeStore() { } HealthTelemetry& RuntimeStore::GetHealthTelemetry() { return mRuntimeHost.GetHealthTelemetry(); } const HealthTelemetry& RuntimeStore::GetHealthTelemetry() const { return mRuntimeHost.GetHealthTelemetry(); } bool RuntimeStore::InitializeStore(std::string& error) { try { std::lock_guard lock(mRuntimeHost.mMutex); if (!ResolvePaths(error)) return false; if (!LoadConfig(error)) return false; mRuntimeHost.mShaderRoot = mRuntimeHost.mRepoRoot / mRuntimeHost.mConfig.shaderLibrary; if (!LoadPersistentState(error)) return false; if (!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::PollStoredFileChanges(bool& registryChanged, bool& reloadRequested, std::string& error) { try { std::lock_guard lock(mRuntimeHost.mMutex); registryChanged = false; reloadRequested = false; if (!mRuntimeHost.mAutoReloadEnabled) { reloadRequested = mRuntimeHost.mReloadRequested; return true; } const auto now = std::chrono::steady_clock::now(); if (mRuntimeHost.mLastScanTime != (std::chrono::steady_clock::time_point::min)() && std::chrono::duration_cast(now - mRuntimeHost.mLastScanTime).count() < 250) { reloadRequested = mRuntimeHost.mReloadRequested; return true; } mRuntimeHost.mLastScanTime = now; std::string scanError; std::map previousPackages = mRuntimeHost.mPackagesById; std::vector previousOrder = mRuntimeHost.mPackageOrder; std::map> previousLayerShaderTimes; for (const RuntimeHost::LayerPersistentState& layer : mRuntimeHost.mPersistentState.layers) { auto previous = previousPackages.find(layer.shaderId); if (previous != previousPackages.end()) previousLayerShaderTimes[layer.id] = std::make_pair(previous->second.shaderWriteTime, previous->second.manifestWriteTime); } if (!ScanShaderPackages(scanError)) { error = scanError; return false; } registryChanged = previousOrder != mRuntimeHost.mPackageOrder; if (!registryChanged && previousPackages.size() == mRuntimeHost.mPackagesById.size()) { for (const auto& item : mRuntimeHost.mPackagesById) { auto previous = previousPackages.find(item.first); if (previous == previousPackages.end()) { registryChanged = true; break; } if (previous->second.shaderWriteTime != item.second.shaderWriteTime || previous->second.manifestWriteTime != item.second.manifestWriteTime || !TextureAssetsEqual(previous->second.textureAssets, item.second.textureAssets) || !FontAssetsEqual(previous->second.fontAssets, item.second.fontAssets)) { registryChanged = true; break; } } } for (RuntimeHost::LayerPersistentState& layer : mRuntimeHost.mPersistentState.layers) { auto active = mRuntimeHost.mPackagesById.find(layer.shaderId); auto previous = previousLayerShaderTimes.find(layer.id); if (active == mRuntimeHost.mPackagesById.end()) continue; mRuntimeHost.EnsureLayerDefaultsLocked(layer, active->second); if (previous != previousLayerShaderTimes.end()) { auto previousPackage = previousPackages.find(layer.shaderId); if (previous->second.first != active->second.shaderWriteTime || previous->second.second != active->second.manifestWriteTime || (previousPackage != previousPackages.end() && (!TextureAssetsEqual(previousPackage->second.textureAssets, active->second.textureAssets) || !FontAssetsEqual(previousPackage->second.fontAssets, active->second.fontAssets)))) { mRuntimeHost.mReloadRequested = true; } } } reloadRequested = mRuntimeHost.mReloadRequested; if (registryChanged || reloadRequested) mRuntimeHost.MarkRenderStateDirtyLocked(); return true; } catch (const std::exception& exception) { error = std::string("RuntimeStore::PollStoredFileChanges exception: ") + exception.what(); return false; } catch (...) { error = "RuntimeStore::PollStoredFileChanges threw a non-standard exception."; return false; } } 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 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 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 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 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 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 SavePersistentState(error); } bool RuntimeStore::SetStoredParameterValue(const std::string& layerId, const std::string& parameterId, const ShaderParameterValue& value, bool persistState, 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->parameterValues[parameterId] = value; mRuntimeHost.MarkParameterStateDirtyLocked(); return !persistState || SavePersistentState(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 SavePersistentState(error); } bool RuntimeStore::SaveStackPresetSnapshot(const std::string& presetName, std::string& error) const { std::lock_guard lock(mRuntimeHost.mMutex); const std::string safeStem = 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 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 = 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 = 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 SavePersistentState(error); } const std::filesystem::path& RuntimeStore::GetRuntimeRepositoryRoot() const { return mRuntimeHost.mRepoRoot; } const std::filesystem::path& RuntimeStore::GetRuntimeUiRoot() const { return mRuntimeHost.mUiRoot; } const std::filesystem::path& RuntimeStore::GetRuntimeDocsRoot() const { return mRuntimeHost.mDocsRoot; } const std::filesystem::path& RuntimeStore::GetRuntimeDataRoot() const { return mRuntimeHost.mRuntimeRoot; } unsigned short RuntimeStore::GetConfiguredControlServerPort() const { return mRuntimeHost.mServerPort; } unsigned short RuntimeStore::GetConfiguredOscPort() const { return mRuntimeHost.mConfig.oscPort; } const std::string& RuntimeStore::GetConfiguredOscBindAddress() const { return mRuntimeHost.mConfig.oscBindAddress; } double RuntimeStore::GetConfiguredOscSmoothing() const { return mRuntimeHost.mConfig.oscSmoothing; } unsigned RuntimeStore::GetConfiguredMaxTemporalHistoryFrames() const { return mRuntimeHost.mConfig.maxTemporalHistoryFrames; } unsigned RuntimeStore::GetConfiguredPreviewFps() const { return mRuntimeHost.mConfig.previewFps; } bool RuntimeStore::IsExternalKeyingConfigured() const { return mRuntimeHost.mConfig.enableExternalKeying; } const std::string& RuntimeStore::GetConfiguredInputVideoFormat() const { return mRuntimeHost.mConfig.inputVideoFormat; } const std::string& RuntimeStore::GetConfiguredInputFrameRate() const { return mRuntimeHost.mConfig.inputFrameRate; } const std::string& RuntimeStore::GetConfiguredOutputVideoFormat() const { return mRuntimeHost.mConfig.outputVideoFormat; } const std::string& RuntimeStore::GetConfiguredOutputFrameRate() const { return mRuntimeHost.mConfig.outputFrameRate; } void RuntimeStore::SetBoundControlServerPort(unsigned short port) { std::lock_guard lock(mRuntimeHost.mMutex); mRuntimeHost.mServerPort = port; } void RuntimeStore::SetCompileStatus(bool succeeded, const std::string& message) { std::lock_guard lock(mRuntimeHost.mMutex); mRuntimeHost.mCompileSucceeded = succeeded; mRuntimeHost.mCompileMessage = message; } void RuntimeStore::ClearReloadRequest() { std::lock_guard lock(mRuntimeHost.mMutex); mRuntimeHost.mReloadRequested = false; } 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 : GetStackPresetNamesLocked()) stackPresets.pushBack(JsonValue(presetName)); root.set("stackPresets", stackPresets); root.set("layers", SerializeLayerStack()); return root; } JsonValue RuntimeStore::SerializeLayerStack() const { return mRuntimeHost.SerializeLayerStackLocked(); } bool RuntimeStore::LoadConfig(std::string& error) { if (!std::filesystem::exists(mRuntimeHost.mConfigPath)) return true; std::string configText = ReadTextFile(mRuntimeHost.mConfigPath, error); if (configText.empty()) return false; JsonValue configJson; if (!ParseJson(configText, configJson, error)) return false; if (const JsonValue* shaderLibraryValue = configJson.find("shaderLibrary")) mRuntimeHost.mConfig.shaderLibrary = shaderLibraryValue->asString(); if (const JsonValue* serverPortValue = configJson.find("serverPort")) mRuntimeHost.mConfig.serverPort = static_cast(serverPortValue->asNumber(mRuntimeHost.mConfig.serverPort)); if (const JsonValue* oscPortValue = configJson.find("oscPort")) mRuntimeHost.mConfig.oscPort = static_cast(oscPortValue->asNumber(mRuntimeHost.mConfig.oscPort)); if (const JsonValue* oscBindAddressValue = configJson.find("oscBindAddress")) mRuntimeHost.mConfig.oscBindAddress = oscBindAddressValue->asString(); if (const JsonValue* oscSmoothingValue = configJson.find("oscSmoothing")) mRuntimeHost.mConfig.oscSmoothing = Clamp01(oscSmoothingValue->asNumber(mRuntimeHost.mConfig.oscSmoothing)); if (const JsonValue* autoReloadValue = configJson.find("autoReload")) mRuntimeHost.mConfig.autoReload = autoReloadValue->asBoolean(mRuntimeHost.mConfig.autoReload); if (const JsonValue* maxTemporalHistoryFramesValue = configJson.find("maxTemporalHistoryFrames")) { const double configuredValue = maxTemporalHistoryFramesValue->asNumber(static_cast(mRuntimeHost.mConfig.maxTemporalHistoryFrames)); mRuntimeHost.mConfig.maxTemporalHistoryFrames = configuredValue <= 0.0 ? 0u : static_cast(configuredValue); } if (const JsonValue* previewFpsValue = configJson.find("previewFps")) { const double configuredValue = previewFpsValue->asNumber(static_cast(mRuntimeHost.mConfig.previewFps)); mRuntimeHost.mConfig.previewFps = configuredValue <= 0.0 ? 0u : static_cast(configuredValue); } if (const JsonValue* enableExternalKeyingValue = configJson.find("enableExternalKeying")) mRuntimeHost.mConfig.enableExternalKeying = enableExternalKeyingValue->asBoolean(mRuntimeHost.mConfig.enableExternalKeying); if (const JsonValue* videoFormatValue = configJson.find("videoFormat")) { if (videoFormatValue->isString() && !videoFormatValue->asString().empty()) { mRuntimeHost.mConfig.inputVideoFormat = videoFormatValue->asString(); mRuntimeHost.mConfig.outputVideoFormat = videoFormatValue->asString(); } } if (const JsonValue* frameRateValue = configJson.find("frameRate")) { if (frameRateValue->isString() && !frameRateValue->asString().empty()) { mRuntimeHost.mConfig.inputFrameRate = frameRateValue->asString(); mRuntimeHost.mConfig.outputFrameRate = frameRateValue->asString(); } else if (frameRateValue->isNumber()) { std::ostringstream stream; stream << frameRateValue->asNumber(); mRuntimeHost.mConfig.inputFrameRate = stream.str(); mRuntimeHost.mConfig.outputFrameRate = stream.str(); } } if (const JsonValue* inputVideoFormatValue = configJson.find("inputVideoFormat")) { if (inputVideoFormatValue->isString() && !inputVideoFormatValue->asString().empty()) mRuntimeHost.mConfig.inputVideoFormat = inputVideoFormatValue->asString(); } if (const JsonValue* inputFrameRateValue = configJson.find("inputFrameRate")) { if (inputFrameRateValue->isString() && !inputFrameRateValue->asString().empty()) mRuntimeHost.mConfig.inputFrameRate = inputFrameRateValue->asString(); else if (inputFrameRateValue->isNumber()) { std::ostringstream stream; stream << inputFrameRateValue->asNumber(); mRuntimeHost.mConfig.inputFrameRate = stream.str(); } } if (const JsonValue* outputVideoFormatValue = configJson.find("outputVideoFormat")) { if (outputVideoFormatValue->isString() && !outputVideoFormatValue->asString().empty()) mRuntimeHost.mConfig.outputVideoFormat = outputVideoFormatValue->asString(); } if (const JsonValue* outputFrameRateValue = configJson.find("outputFrameRate")) { if (outputFrameRateValue->isString() && !outputFrameRateValue->asString().empty()) mRuntimeHost.mConfig.outputFrameRate = outputFrameRateValue->asString(); else if (outputFrameRateValue->isNumber()) { std::ostringstream stream; stream << outputFrameRateValue->asNumber(); mRuntimeHost.mConfig.outputFrameRate = stream.str(); } } mRuntimeHost.mAutoReloadEnabled = mRuntimeHost.mConfig.autoReload; return true; } bool RuntimeStore::LoadPersistentState(std::string& error) { if (!std::filesystem::exists(mRuntimeHost.mRuntimeStatePath)) return true; std::string stateText = ReadTextFile(mRuntimeHost.mRuntimeStatePath, error); if (stateText.empty()) return false; JsonValue root; if (!ParseJson(stateText, root, error)) return false; if (const JsonValue* layersValue = root.find("layers")) { for (const JsonValue& layerValue : layersValue->asArray()) { if (!layerValue.isObject()) continue; RuntimeHost::LayerPersistentState layer; if (const JsonValue* idValue = layerValue.find("id")) layer.id = idValue->asString(); if (const JsonValue* shaderIdValue = layerValue.find("shaderId")) layer.shaderId = shaderIdValue->asString(); if (const JsonValue* bypassValue = layerValue.find("bypass")) layer.bypass = bypassValue->asBoolean(false); else if (const JsonValue* enabledValue = layerValue.find("enabled")) layer.bypass = !enabledValue->asBoolean(true); if (const JsonValue* parameterValues = layerValue.find("parameterValues")) { for (const auto& parameterItem : parameterValues->asObject()) { ShaderParameterValue value; const JsonValue& jsonValue = parameterItem.second; if (jsonValue.isBoolean()) value.booleanValue = jsonValue.asBoolean(); else if (jsonValue.isString()) value.enumValue = jsonValue.asString(); else if (jsonValue.isNumber()) value.numberValues.push_back(jsonValue.asNumber()); else if (jsonValue.isArray()) value.numberValues = JsonArrayToNumbers(jsonValue); layer.parameterValues[parameterItem.first] = value; } } if (!layer.shaderId.empty()) mRuntimeHost.mPersistentState.layers.push_back(layer); } } else { std::string activeShaderId; if (const JsonValue* activeShaderValue = root.find("activeShaderId")) activeShaderId = activeShaderValue->asString(); if (!activeShaderId.empty()) { RuntimeHost::LayerPersistentState layer; layer.id = mRuntimeHost.GenerateLayerId(); layer.shaderId = activeShaderId; layer.bypass = false; if (const JsonValue* valuesByShader = root.find("parameterValuesByShader")) { const JsonValue* shaderValues = valuesByShader->find(activeShaderId); if (shaderValues) { for (const auto& parameterItem : shaderValues->asObject()) { ShaderParameterValue value; const JsonValue& jsonValue = parameterItem.second; if (jsonValue.isBoolean()) value.booleanValue = jsonValue.asBoolean(); else if (jsonValue.isString()) value.enumValue = jsonValue.asString(); else if (jsonValue.isNumber()) value.numberValues.push_back(jsonValue.asNumber()); else if (jsonValue.isArray()) value.numberValues = JsonArrayToNumbers(jsonValue); layer.parameterValues[parameterItem.first] = value; } } } mRuntimeHost.mPersistentState.layers.push_back(layer); } } return true; } bool RuntimeStore::SavePersistentState(std::string& error) const { JsonValue root = JsonValue::MakeObject(); JsonValue layers = JsonValue::MakeArray(); for (const RuntimeHost::LayerPersistentState& layer : mRuntimeHost.mPersistentState.layers) { JsonValue layerValue = JsonValue::MakeObject(); layerValue.set("id", JsonValue(layer.id)); layerValue.set("shaderId", JsonValue(layer.shaderId)); layerValue.set("bypass", JsonValue(layer.bypass)); JsonValue parameterValues = JsonValue::MakeObject(); auto packageIt = mRuntimeHost.mPackagesById.find(layer.shaderId); for (const auto& parameterItem : layer.parameterValues) { const ShaderParameterDefinition* definition = nullptr; if (packageIt != mRuntimeHost.mPackagesById.end()) { for (const ShaderParameterDefinition& candidate : packageIt->second.parameters) { if (candidate.id == parameterItem.first) { definition = &candidate; break; } } } if (definition) parameterValues.set(parameterItem.first, mRuntimeHost.SerializeParameterValue(*definition, parameterItem.second)); } layerValue.set("parameterValues", parameterValues); layers.pushBack(layerValue); } root.set("layers", layers); return WriteTextFile(mRuntimeHost.mRuntimeStatePath, SerializeJson(root, true), error); } bool RuntimeStore::ScanShaderPackages(std::string& error) { std::map packagesById; std::vector packageOrder; std::vector packageStatuses; ShaderPackageRegistry registry(mRuntimeHost.mConfig.maxTemporalHistoryFrames); if (!registry.Scan(mRuntimeHost.mShaderRoot, packagesById, packageOrder, packageStatuses, error)) return false; mRuntimeHost.mPackagesById.swap(packagesById); mRuntimeHost.mPackageOrder.swap(packageOrder); mRuntimeHost.mPackageStatuses.swap(packageStatuses); for (auto it = mRuntimeHost.mPersistentState.layers.begin(); it != mRuntimeHost.mPersistentState.layers.end();) { if (mRuntimeHost.mPackagesById.find(it->shaderId) == mRuntimeHost.mPackagesById.end()) it = mRuntimeHost.mPersistentState.layers.erase(it); else ++it; } mRuntimeHost.MarkRenderStateDirtyLocked(); return true; } std::string RuntimeStore::ReadTextFile(const std::filesystem::path& path, std::string& error) const { std::ifstream input(path, std::ios::binary); if (!input) { error = "Could not open file: " + path.string(); return std::string(); } std::ostringstream buffer; buffer << input.rdbuf(); return buffer.str(); } bool RuntimeStore::WriteTextFile(const std::filesystem::path& path, const std::string& contents, std::string& error) const { std::error_code fsError; std::filesystem::create_directories(path.parent_path(), fsError); const std::filesystem::path temporaryPath = path.string() + ".tmp"; std::ofstream output(temporaryPath, std::ios::binary | std::ios::trunc); if (!output) { error = "Could not write file: " + temporaryPath.string(); return false; } output << contents; output.close(); if (!output.good()) { error = "Could not finish writing file: " + temporaryPath.string(); return false; } if (!MoveFileExA(temporaryPath.string().c_str(), path.string().c_str(), MOVEFILE_REPLACE_EXISTING | MOVEFILE_WRITE_THROUGH)) { const DWORD lastError = GetLastError(); std::filesystem::remove(temporaryPath, fsError); error = "Could not replace file: " + path.string() + " (Win32 error " + std::to_string(lastError) + ")"; return false; } return true; } bool RuntimeStore::ResolvePaths(std::string& error) { mRuntimeHost.mRepoRoot = FindRepoRootCandidate(); if (mRuntimeHost.mRepoRoot.empty()) { error = "Could not locate the repository root from the current runtime path."; return false; } const std::filesystem::path builtUiRoot = mRuntimeHost.mRepoRoot / "ui" / "dist"; mRuntimeHost.mUiRoot = std::filesystem::exists(builtUiRoot) ? builtUiRoot : (mRuntimeHost.mRepoRoot / "ui"); mRuntimeHost.mDocsRoot = mRuntimeHost.mRepoRoot / "docs"; mRuntimeHost.mConfigPath = mRuntimeHost.mRepoRoot / "config" / "runtime-host.json"; mRuntimeHost.mShaderRoot = mRuntimeHost.mRepoRoot / mRuntimeHost.mConfig.shaderLibrary; mRuntimeHost.mRuntimeRoot = mRuntimeHost.mRepoRoot / "runtime"; mRuntimeHost.mPresetRoot = mRuntimeHost.mRuntimeRoot / "stack_presets"; mRuntimeHost.mRuntimeStatePath = mRuntimeHost.mRuntimeRoot / "runtime_state.json"; mRuntimeHost.mWrapperPath = mRuntimeHost.mRuntimeRoot / "shader_cache" / "active_shader_wrapper.slang"; mRuntimeHost.mGeneratedGlslPath = mRuntimeHost.mRuntimeRoot / "shader_cache" / "active_shader.raw.frag"; mRuntimeHost.mPatchedGlslPath = mRuntimeHost.mRuntimeRoot / "shader_cache" / "active_shader.frag"; std::error_code fsError; std::filesystem::create_directories(mRuntimeHost.mRuntimeRoot / "shader_cache", fsError); std::filesystem::create_directories(mRuntimeHost.mPresetRoot, fsError); return true; } std::vector RuntimeStore::GetStackPresetNamesLocked() const { std::vector presetNames; std::error_code fsError; if (!std::filesystem::exists(mRuntimeHost.mPresetRoot, fsError)) return presetNames; for (const auto& entry : std::filesystem::directory_iterator(mRuntimeHost.mPresetRoot, fsError)) { if (!entry.is_regular_file()) continue; if (ToLowerCopy(entry.path().extension().string()) != ".json") continue; presetNames.push_back(entry.path().stem().string()); } std::sort(presetNames.begin(), presetNames.end()); return presetNames; } std::string RuntimeStore::MakeSafePresetFileStem(const std::string& presetName) const { return ::MakeSafePresetFileStem(presetName); } bool RuntimeStore::TryResolveStoredLayerAndParameterByControlKeyLocked(const std::string& layerKey, const std::string& parameterKey, RuntimeHost::LayerPersistentState*& matchedLayer, const ShaderPackage*& matchedPackage, std::vector::const_iterator& parameterIt, std::string& error) const { matchedLayer = nullptr; matchedPackage = nullptr; for (RuntimeHost::LayerPersistentState& layer : mRuntimeHost.mPersistentState.layers) { auto shaderIt = mRuntimeHost.mPackagesById.find(layer.shaderId); if (shaderIt == mRuntimeHost.mPackagesById.end()) continue; if (MatchesControlKey(layer.id, layerKey) || MatchesControlKey(shaderIt->second.id, layerKey) || MatchesControlKey(shaderIt->second.displayName, layerKey)) { matchedLayer = &layer; matchedPackage = &shaderIt->second; break; } } if (!matchedLayer || !matchedPackage) { error = "Unknown OSC layer key: " + layerKey; return false; } parameterIt = std::find_if(matchedPackage->parameters.begin(), matchedPackage->parameters.end(), [¶meterKey](const ShaderParameterDefinition& definition) { return MatchesControlKey(definition.id, parameterKey) || MatchesControlKey(definition.label, parameterKey); }); if (parameterIt == matchedPackage->parameters.end()) { error = "Unknown OSC parameter key: " + parameterKey; return false; } return true; } bool RuntimeStore::CopyShaderPackageForStoredLayer(const std::string& layerId, ShaderPackage& shaderPackage, std::string& error) const { std::lock_guard lock(mRuntimeHost.mMutex); const RuntimeHost::LayerPersistentState* layer = mRuntimeHost.FindLayerById(layerId); if (!layer) { error = "Unknown layer id: " + layerId; return false; } auto it = mRuntimeHost.mPackagesById.find(layer->shaderId); if (it == mRuntimeHost.mPackagesById.end()) { error = "Unknown shader id: " + layer->shaderId; return false; } shaderPackage = it->second; return true; } void RuntimeStore::GetShaderCompilerInputs(std::filesystem::path& repoRoot, std::filesystem::path& wrapperPath, std::filesystem::path& generatedGlslPath, std::filesystem::path& patchedGlslPath, unsigned& maxTemporalHistoryFrames) const { std::lock_guard lock(mRuntimeHost.mMutex); repoRoot = mRuntimeHost.mRepoRoot; wrapperPath = mRuntimeHost.mWrapperPath; generatedGlslPath = mRuntimeHost.mGeneratedGlslPath; patchedGlslPath = mRuntimeHost.mPatchedGlslPath; maxTemporalHistoryFrames = mRuntimeHost.mConfig.maxTemporalHistoryFrames; } uint64_t RuntimeStore::GetRenderStateVersion() const { return mRuntimeHost.mRenderStateVersion.load(std::memory_order_relaxed); } uint64_t RuntimeStore::GetParameterStateVersion() const { return mRuntimeHost.mParameterStateVersion.load(std::memory_order_relaxed); } void RuntimeStore::AdvanceFrameCounter() { ++mRuntimeHost.mFrameCounter; } void RuntimeStore::BuildLayerRenderStates(unsigned outputWidth, unsigned outputHeight, std::vector& states) const { std::lock_guard lock(mRuntimeHost.mMutex); BuildLayerRenderStatesLocked(outputWidth, outputHeight, states); } bool RuntimeStore::TryBuildLayerRenderStates(unsigned outputWidth, unsigned outputHeight, std::vector& states) const { std::unique_lock lock(mRuntimeHost.mMutex, std::try_to_lock); if (!lock.owns_lock()) return false; BuildLayerRenderStatesLocked(outputWidth, outputHeight, states); return true; } bool RuntimeStore::TryRefreshLayerParameters(std::vector& states) const { std::unique_lock lock(mRuntimeHost.mMutex, std::try_to_lock); if (!lock.owns_lock()) return false; RefreshLayerParametersLocked(states); return true; } void RuntimeStore::RefreshDynamicRenderStateFields(std::vector& states) const { std::lock_guard lock(mRuntimeHost.mMutex); RefreshDynamicRenderStateFieldsLocked(states); } void RuntimeStore::BuildLayerRenderStatesLocked(unsigned outputWidth, unsigned outputHeight, std::vector& states) const { states.clear(); const HealthTelemetry::SignalStatusSnapshot signalStatus = mRuntimeHost.mHealthTelemetry.GetSignalStatusSnapshot(); for (const RuntimeHost::LayerPersistentState& layer : mRuntimeHost.mPersistentState.layers) { auto shaderIt = mRuntimeHost.mPackagesById.find(layer.shaderId); if (shaderIt == mRuntimeHost.mPackagesById.end()) continue; RuntimeRenderState state; state.layerId = layer.id; state.shaderId = layer.shaderId; state.shaderName = shaderIt->second.displayName; state.mixAmount = 1.0; state.bypass = layer.bypass ? 1.0 : 0.0; state.inputWidth = signalStatus.width; state.inputHeight = signalStatus.height; state.outputWidth = outputWidth; state.outputHeight = outputHeight; state.parameterDefinitions = shaderIt->second.parameters; state.textureAssets = shaderIt->second.textureAssets; state.fontAssets = shaderIt->second.fontAssets; state.isTemporal = shaderIt->second.temporal.enabled; state.temporalHistorySource = shaderIt->second.temporal.historySource; state.requestedTemporalHistoryLength = shaderIt->second.temporal.requestedHistoryLength; state.effectiveTemporalHistoryLength = shaderIt->second.temporal.effectiveHistoryLength; state.feedback = shaderIt->second.feedback; for (const ShaderParameterDefinition& definition : shaderIt->second.parameters) { ShaderParameterValue value = mRuntimeHost.DefaultValueForDefinition(definition); auto valueIt = layer.parameterValues.find(definition.id); if (valueIt != layer.parameterValues.end()) value = valueIt->second; state.parameterValues[definition.id] = value; } states.push_back(state); } RefreshDynamicRenderStateFieldsLocked(states); } void RuntimeStore::RefreshLayerParametersLocked(std::vector& states) const { for (RuntimeRenderState& state : states) { const auto layerIt = std::find_if(mRuntimeHost.mPersistentState.layers.begin(), mRuntimeHost.mPersistentState.layers.end(), [&state](const RuntimeHost::LayerPersistentState& layer) { return layer.id == state.layerId; }); if (layerIt == mRuntimeHost.mPersistentState.layers.end()) continue; state.bypass = layerIt->bypass ? 1.0 : 0.0; state.parameterValues.clear(); for (const ShaderParameterDefinition& definition : state.parameterDefinitions) { ShaderParameterValue value = mRuntimeHost.DefaultValueForDefinition(definition); auto valueIt = layerIt->parameterValues.find(definition.id); if (valueIt != layerIt->parameterValues.end()) value = valueIt->second; state.parameterValues[definition.id] = value; } } } void RuntimeStore::RefreshDynamicRenderStateFieldsLocked(std::vector& states) const { const RuntimeClockSnapshot clock = GetRuntimeClockSnapshot(); const double timeSeconds = std::chrono::duration_cast>(std::chrono::steady_clock::now() - mRuntimeHost.mStartTime).count(); const double frameCount = static_cast(mRuntimeHost.mFrameCounter.load(std::memory_order_relaxed)); for (RuntimeRenderState& state : states) { state.timeSeconds = timeSeconds; state.utcTimeSeconds = clock.utcTimeSeconds; state.utcOffsetSeconds = clock.utcOffsetSeconds; state.startupRandom = mRuntimeHost.mStartupRandom; state.frameCount = frameCount; } }