#include "stdafx.h" #include "RuntimeHost.h" #include #include #include #include #include #include #include namespace { std::string Trim(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 ReplaceAll(std::string text, const std::string& from, const std::string& to) { std::size_t offset = 0; while ((offset = text.find(from, offset)) != std::string::npos) { text.replace(offset, from.length(), to); offset += to.length(); } return text; } bool IsFiniteNumber(double value) { return std::isfinite(value) != 0; } 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; } std::vector JsonArrayToNumbers(const JsonValue& value) { std::vector numbers; for (const JsonValue& item : value.asArray()) { if (item.isNumber()) numbers.push_back(item.asNumber()); } return numbers; } 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 (std::filesystem::exists(candidate / "CMakeLists.txt") && std::filesystem::exists(candidate / "apps" / "LoopThroughWithOpenGLCompositing")) { return candidate; } candidate = candidate.parent_path(); } } return std::filesystem::path(); } std::string ShaderParameterTypeToString(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"; } return "unknown"; } std::string SlangTypeForParameter(ShaderParameterType type) { switch (type) { case ShaderParameterType::Float: return "uniform float"; case ShaderParameterType::Vec2: return "uniform float2"; case ShaderParameterType::Color: return "uniform float4"; case ShaderParameterType::Boolean: return "uniform bool"; case ShaderParameterType::Enum: return "uniform int"; } return "uniform float"; } bool ParseTemporalHistorySource(const std::string& sourceName, TemporalHistorySource& source) { if (sourceName == "source") { source = TemporalHistorySource::Source; return true; } if (sourceName == "preLayerInput") { source = TemporalHistorySource::PreLayerInput; return true; } return false; } bool ParseShaderParameterType(const std::string& typeName, ShaderParameterType& type) { if (typeName == "float") { type = ShaderParameterType::Float; return true; } if (typeName == "vec2") { type = ShaderParameterType::Vec2; return true; } if (typeName == "color") { type = ShaderParameterType::Color; return true; } if (typeName == "bool") { type = ShaderParameterType::Boolean; return true; } if (typeName == "enum") { type = ShaderParameterType::Enum; return true; } return false; } 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; } } RuntimeHost::RuntimeHost() : mReloadRequested(false), mCompileSucceeded(false), mHasSignal(false), mSignalWidth(0), mSignalHeight(0), mFrameBudgetMilliseconds(0.0), mRenderMilliseconds(0.0), mSmoothedRenderMilliseconds(0.0), mServerPort(8080), mAutoReloadEnabled(true), mStartTime(std::chrono::steady_clock::now()), mLastScanTime(std::chrono::steady_clock::time_point::min()), mFrameCounter(0), mNextLayerId(0) { } bool RuntimeHost::Initialize(std::string& error) { try { std::lock_guard lock(mMutex); if (!ResolvePaths(error)) return false; if (!LoadConfig(error)) return false; mShaderRoot = mRepoRoot / mConfig.shaderLibrary; if (!LoadPersistentState(error)) return false; if (!ScanShaderPackages(error)) return false; for (LayerPersistentState& layer : mPersistentState.layers) { auto shaderIt = mPackagesById.find(layer.shaderId); if (shaderIt != mPackagesById.end()) EnsureLayerDefaultsLocked(layer, shaderIt->second); } if (mPersistentState.layers.empty() && !mPackageOrder.empty()) { LayerPersistentState layer; layer.id = GenerateLayerId(); layer.shaderId = mPackageOrder.front(); layer.bypass = false; EnsureLayerDefaultsLocked(layer, mPackagesById[layer.shaderId]); mPersistentState.layers.push_back(layer); } mServerPort = mConfig.serverPort; mAutoReloadEnabled = mConfig.autoReload; mReloadRequested = true; mCompileMessage = "Waiting for shader compile."; return true; } catch (const std::exception& exception) { error = std::string("RuntimeHost::Initialize exception: ") + exception.what(); return false; } catch (...) { error = "RuntimeHost::Initialize threw a non-standard exception."; return false; } } bool RuntimeHost::PollFileChanges(bool& registryChanged, bool& reloadRequested, std::string& error) { try { std::lock_guard lock(mMutex); registryChanged = false; reloadRequested = false; if (!mAutoReloadEnabled) { reloadRequested = mReloadRequested; return true; } const auto now = std::chrono::steady_clock::now(); if (mLastScanTime != std::chrono::steady_clock::time_point::min() && std::chrono::duration_cast(now - mLastScanTime).count() < 250) { reloadRequested = mReloadRequested; return true; } mLastScanTime = now; std::string scanError; std::map previousPackages = mPackagesById; std::vector previousOrder = mPackageOrder; std::map> previousLayerShaderTimes; for (const LayerPersistentState& layer : 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 != mPackageOrder; if (!registryChanged && previousPackages.size() == mPackagesById.size()) { for (const auto& item : 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)) { registryChanged = true; break; } } } for (LayerPersistentState& layer : mPersistentState.layers) { auto active = mPackagesById.find(layer.shaderId); auto previous = previousLayerShaderTimes.find(layer.id); if (active == mPackagesById.end()) continue; 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))) { mReloadRequested = true; } } } reloadRequested = mReloadRequested; return true; } catch (const std::exception& exception) { error = std::string("RuntimeHost::PollFileChanges exception: ") + exception.what(); return false; } catch (...) { error = "RuntimeHost::PollFileChanges threw a non-standard exception."; return false; } } bool RuntimeHost::ManualReloadRequested() { std::lock_guard lock(mMutex); return mReloadRequested; } void RuntimeHost::ClearReloadRequest() { std::lock_guard lock(mMutex); mReloadRequested = false; } bool RuntimeHost::AddLayer(const std::string& shaderId, std::string& error) { std::lock_guard lock(mMutex); auto shaderIt = mPackagesById.find(shaderId); if (shaderIt == mPackagesById.end()) { error = "Unknown shader id: " + shaderId; return false; } LayerPersistentState layer; layer.id = GenerateLayerId(); layer.shaderId = shaderId; layer.bypass = false; EnsureLayerDefaultsLocked(layer, shaderIt->second); mPersistentState.layers.push_back(layer); mReloadRequested = true; return SavePersistentState(error); } bool RuntimeHost::RemoveLayer(const std::string& layerId, std::string& error) { std::lock_guard lock(mMutex); auto it = std::find_if(mPersistentState.layers.begin(), mPersistentState.layers.end(), [&layerId](const LayerPersistentState& layer) { return layer.id == layerId; }); if (it == mPersistentState.layers.end()) { error = "Unknown layer id: " + layerId; return false; } mPersistentState.layers.erase(it); mReloadRequested = true; return SavePersistentState(error); } bool RuntimeHost::MoveLayer(const std::string& layerId, int direction, std::string& error) { std::lock_guard lock(mMutex); auto it = std::find_if(mPersistentState.layers.begin(), mPersistentState.layers.end(), [&layerId](const LayerPersistentState& layer) { return layer.id == layerId; }); if (it == mPersistentState.layers.end()) { error = "Unknown layer id: " + layerId; return false; } const std::ptrdiff_t index = std::distance(mPersistentState.layers.begin(), it); const std::ptrdiff_t newIndex = index + direction; if (newIndex < 0 || newIndex >= static_cast(mPersistentState.layers.size())) return true; std::swap(mPersistentState.layers[index], mPersistentState.layers[newIndex]); mReloadRequested = true; return SavePersistentState(error); } bool RuntimeHost::MoveLayerToIndex(const std::string& layerId, std::size_t targetIndex, std::string& error) { std::lock_guard lock(mMutex); auto it = std::find_if(mPersistentState.layers.begin(), mPersistentState.layers.end(), [&layerId](const LayerPersistentState& layer) { return layer.id == layerId; }); if (it == mPersistentState.layers.end()) { error = "Unknown layer id: " + layerId; return false; } if (mPersistentState.layers.empty()) return true; if (targetIndex >= mPersistentState.layers.size()) targetIndex = mPersistentState.layers.size() - 1; const std::size_t sourceIndex = static_cast(std::distance(mPersistentState.layers.begin(), it)); if (sourceIndex == targetIndex) return true; LayerPersistentState movedLayer = *it; mPersistentState.layers.erase(mPersistentState.layers.begin() + static_cast(sourceIndex)); mPersistentState.layers.insert(mPersistentState.layers.begin() + static_cast(targetIndex), movedLayer); mReloadRequested = true; return SavePersistentState(error); } bool RuntimeHost::SetLayerBypass(const std::string& layerId, bool bypassed, std::string& error) { std::lock_guard lock(mMutex); LayerPersistentState* layer = FindLayerById(layerId); if (!layer) { error = "Unknown layer id: " + layerId; return false; } layer->bypass = bypassed; mReloadRequested = true; return SavePersistentState(error); } bool RuntimeHost::SetLayerShader(const std::string& layerId, const std::string& shaderId, std::string& error) { std::lock_guard lock(mMutex); LayerPersistentState* layer = FindLayerById(layerId); if (!layer) { error = "Unknown layer id: " + layerId; return false; } auto shaderIt = mPackagesById.find(shaderId); if (shaderIt == mPackagesById.end()) { error = "Unknown shader id: " + shaderId; return false; } layer->shaderId = shaderId; layer->parameterValues.clear(); EnsureLayerDefaultsLocked(*layer, shaderIt->second); mReloadRequested = true; return SavePersistentState(error); } bool RuntimeHost::UpdateLayerParameter(const std::string& layerId, const std::string& parameterId, const JsonValue& newValue, std::string& error) { std::lock_guard lock(mMutex); LayerPersistentState* layer = FindLayerById(layerId); if (!layer) { error = "Unknown layer id: " + layerId; return false; } auto shaderIt = mPackagesById.find(layer->shaderId); if (shaderIt == 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; } ShaderParameterValue normalized; if (!NormalizeAndValidateValue(*parameterIt, newValue, normalized, error)) return false; layer->parameterValues[parameterId] = normalized; return SavePersistentState(error); } bool RuntimeHost::ResetLayerParameters(const std::string& layerId, std::string& error) { std::lock_guard lock(mMutex); LayerPersistentState* layer = FindLayerById(layerId); if (!layer) { error = "Unknown layer id: " + layerId; return false; } auto shaderIt = mPackagesById.find(layer->shaderId); if (shaderIt == mPackagesById.end()) { error = "Unknown shader id: " + layer->shaderId; return false; } layer->parameterValues.clear(); EnsureLayerDefaultsLocked(*layer, shaderIt->second); return SavePersistentState(error); } bool RuntimeHost::SaveStackPreset(const std::string& presetName, std::string& error) const { std::lock_guard lock(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(Trim(presetName))); root.set("layers", SerializeLayerStackLocked()); return WriteTextFile(mPresetRoot / (safeStem + ".json"), SerializeJson(root, true), error); } bool RuntimeHost::LoadStackPreset(const std::string& presetName, std::string& error) { std::lock_guard lock(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 = 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 (!DeserializeLayerStackLocked(*layersValue, nextLayers, error)) return false; if (nextLayers.empty()) { error = "Preset does not contain any valid layers."; return false; } mPersistentState.layers = nextLayers; mReloadRequested = true; return SavePersistentState(error); } void RuntimeHost::SetCompileStatus(bool succeeded, const std::string& message) { std::lock_guard lock(mMutex); mCompileSucceeded = succeeded; mCompileMessage = message; } void RuntimeHost::SetSignalStatus(bool hasSignal, unsigned width, unsigned height, const std::string& modeName) { std::lock_guard lock(mMutex); mHasSignal = hasSignal; mSignalWidth = width; mSignalHeight = height; mSignalModeName = modeName; } void RuntimeHost::SetDeckLinkOutputStatus(const std::string& modelName, bool supportsInternalKeying, bool supportsExternalKeying, bool keyerInterfaceAvailable, bool externalKeyingRequested, bool externalKeyingActive, const std::string& statusMessage) { std::lock_guard lock(mMutex); mDeckLinkOutputStatus.modelName = modelName; mDeckLinkOutputStatus.supportsInternalKeying = supportsInternalKeying; mDeckLinkOutputStatus.supportsExternalKeying = supportsExternalKeying; mDeckLinkOutputStatus.keyerInterfaceAvailable = keyerInterfaceAvailable; mDeckLinkOutputStatus.externalKeyingRequested = externalKeyingRequested; mDeckLinkOutputStatus.externalKeyingActive = externalKeyingActive; mDeckLinkOutputStatus.statusMessage = statusMessage; } void RuntimeHost::SetPerformanceStats(double frameBudgetMilliseconds, double renderMilliseconds) { std::lock_guard lock(mMutex); mFrameBudgetMilliseconds = std::max(frameBudgetMilliseconds, 0.0); mRenderMilliseconds = std::max(renderMilliseconds, 0.0); if (mSmoothedRenderMilliseconds <= 0.0) mSmoothedRenderMilliseconds = mRenderMilliseconds; else mSmoothedRenderMilliseconds = mSmoothedRenderMilliseconds * 0.9 + mRenderMilliseconds * 0.1; } void RuntimeHost::AdvanceFrame() { std::lock_guard lock(mMutex); ++mFrameCounter; } bool RuntimeHost::BuildLayerFragmentShaderSource(const std::string& layerId, std::string& fragmentShaderSource, std::string& error) { try { ShaderPackage shaderPackage; { std::lock_guard lock(mMutex); const LayerPersistentState* layer = FindLayerById(layerId); if (!layer) { error = "Unknown layer id: " + layerId; return false; } auto it = mPackagesById.find(layer->shaderId); if (it == mPackagesById.end()) { error = "Unknown shader id: " + layer->shaderId; return false; } shaderPackage = it->second; } const std::string wrapperSource = BuildWrapperSlangSource(shaderPackage); if (!WriteTextFile(mWrapperPath, wrapperSource, error)) return false; if (!RunSlangCompiler(mWrapperPath, mGeneratedGlslPath, error)) return false; fragmentShaderSource = ReadTextFile(mGeneratedGlslPath, error); if (fragmentShaderSource.empty()) return false; if (!PatchGeneratedGlsl(fragmentShaderSource, error)) return false; if (!WriteTextFile(mPatchedGlslPath, fragmentShaderSource, error)) return false; return true; } catch (const std::exception& exception) { error = std::string("RuntimeHost::BuildLayerFragmentShaderSource exception: ") + exception.what(); return false; } catch (...) { error = "RuntimeHost::BuildLayerFragmentShaderSource threw a non-standard exception."; return false; } } std::vector RuntimeHost::GetLayerRenderStates(unsigned outputWidth, unsigned outputHeight) const { std::lock_guard lock(mMutex); std::vector states; for (const LayerPersistentState& layer : mPersistentState.layers) { auto shaderIt = mPackagesById.find(layer.shaderId); if (shaderIt == mPackagesById.end()) continue; RuntimeRenderState state; state.layerId = layer.id; state.shaderId = layer.shaderId; state.timeSeconds = std::chrono::duration_cast>(std::chrono::steady_clock::now() - mStartTime).count(); state.frameCount = static_cast(mFrameCounter); state.mixAmount = 1.0; state.bypass = layer.bypass ? 1.0 : 0.0; state.inputWidth = mSignalWidth; state.inputHeight = mSignalHeight; state.outputWidth = outputWidth; state.outputHeight = outputHeight; state.parameterDefinitions = shaderIt->second.parameters; state.textureAssets = shaderIt->second.textureAssets; state.isTemporal = shaderIt->second.temporal.enabled; state.temporalHistorySource = shaderIt->second.temporal.historySource; state.requestedTemporalHistoryLength = shaderIt->second.temporal.requestedHistoryLength; state.effectiveTemporalHistoryLength = shaderIt->second.temporal.effectiveHistoryLength; for (const ShaderParameterDefinition& definition : shaderIt->second.parameters) { ShaderParameterValue value = 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); } return states; } std::string RuntimeHost::BuildStateJson() const { return SerializeJson(BuildStateValue(), true); } void RuntimeHost::SetServerPort(unsigned short port) { std::lock_guard lock(mMutex); mServerPort = port; } bool RuntimeHost::LoadConfig(std::string& error) { if (!std::filesystem::exists(mConfigPath)) return true; std::string configText = ReadTextFile(mConfigPath, error); if (configText.empty()) return false; JsonValue configJson; if (!ParseJson(configText, configJson, error)) return false; if (const JsonValue* shaderLibraryValue = configJson.find("shaderLibrary")) mConfig.shaderLibrary = shaderLibraryValue->asString(); if (const JsonValue* serverPortValue = configJson.find("serverPort")) mConfig.serverPort = static_cast(serverPortValue->asNumber(mConfig.serverPort)); if (const JsonValue* autoReloadValue = configJson.find("autoReload")) mConfig.autoReload = autoReloadValue->asBoolean(mConfig.autoReload); if (const JsonValue* maxTemporalHistoryFramesValue = configJson.find("maxTemporalHistoryFrames")) { const double configuredValue = maxTemporalHistoryFramesValue->asNumber(static_cast(mConfig.maxTemporalHistoryFrames)); mConfig.maxTemporalHistoryFrames = configuredValue <= 0.0 ? 0u : static_cast(configuredValue); } if (const JsonValue* enableExternalKeyingValue = configJson.find("enableExternalKeying")) mConfig.enableExternalKeying = enableExternalKeyingValue->asBoolean(mConfig.enableExternalKeying); mAutoReloadEnabled = mConfig.autoReload; return true; } bool RuntimeHost::LoadPersistentState(std::string& error) { if (!std::filesystem::exists(mRuntimeStatePath)) return true; std::string stateText = ReadTextFile(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; 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()) mPersistentState.layers.push_back(layer); } } else { // Migrate from the older single-shader state shape. std::string activeShaderId; if (const JsonValue* activeShaderValue = root.find("activeShaderId")) activeShaderId = activeShaderValue->asString(); if (!activeShaderId.empty()) { LayerPersistentState layer; layer.id = 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; } } } mPersistentState.layers.push_back(layer); } } return true; } bool RuntimeHost::SavePersistentState(std::string& error) const { JsonValue root = JsonValue::MakeObject(); JsonValue layers = JsonValue::MakeArray(); for (const LayerPersistentState& layer : 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 = mPackagesById.find(layer.shaderId); for (const auto& parameterItem : layer.parameterValues) { const ShaderParameterDefinition* definition = nullptr; if (packageIt != mPackagesById.end()) { for (const ShaderParameterDefinition& candidate : packageIt->second.parameters) { if (candidate.id == parameterItem.first) { definition = &candidate; break; } } } if (definition) parameterValues.set(parameterItem.first, SerializeParameterValue(*definition, parameterItem.second)); } layerValue.set("parameterValues", parameterValues); layers.pushBack(layerValue); } root.set("layers", layers); return WriteTextFile(mRuntimeStatePath, SerializeJson(root, true), error); } bool RuntimeHost::ScanShaderPackages(std::string& error) { std::map packagesById; std::vector packageOrder; if (!std::filesystem::exists(mShaderRoot)) { error = "Shader library directory does not exist: " + mShaderRoot.string(); return false; } for (const auto& entry : std::filesystem::directory_iterator(mShaderRoot)) { if (!entry.is_directory()) continue; std::filesystem::path manifestPath = entry.path() / "shader.json"; if (!std::filesystem::exists(manifestPath)) continue; ShaderPackage shaderPackage; if (!ParseShaderManifest(manifestPath, shaderPackage, error)) return false; if (packagesById.find(shaderPackage.id) != packagesById.end()) { error = "Duplicate shader id found: " + shaderPackage.id; return false; } packageOrder.push_back(shaderPackage.id); packagesById[shaderPackage.id] = shaderPackage; } std::sort(packageOrder.begin(), packageOrder.end()); mPackagesById.swap(packagesById); mPackageOrder.swap(packageOrder); for (auto it = mPersistentState.layers.begin(); it != mPersistentState.layers.end();) { if (mPackagesById.find(it->shaderId) == mPackagesById.end()) it = mPersistentState.layers.erase(it); else ++it; } return true; } bool RuntimeHost::ParseShaderManifest(const std::filesystem::path& manifestPath, ShaderPackage& shaderPackage, std::string& error) const { const std::string manifestText = ReadTextFile(manifestPath, error); if (manifestText.empty()) return false; JsonValue manifestJson; if (!ParseJson(manifestText, manifestJson, error)) return false; const JsonValue* idValue = manifestJson.find("id"); const JsonValue* nameValue = manifestJson.find("name"); if (!idValue || !nameValue) { error = "Shader manifest is missing required 'id' or 'name' field: " + manifestPath.string(); return false; } shaderPackage.id = idValue->asString(); shaderPackage.displayName = nameValue->asString(); shaderPackage.description = manifestJson.find("description") ? manifestJson.find("description")->asString() : ""; shaderPackage.category = manifestJson.find("category") ? manifestJson.find("category")->asString() : ""; shaderPackage.entryPoint = manifestJson.find("entryPoint") ? manifestJson.find("entryPoint")->asString() : "shadeVideo"; shaderPackage.directoryPath = manifestPath.parent_path(); shaderPackage.shaderPath = shaderPackage.directoryPath / "shader.slang"; shaderPackage.manifestPath = manifestPath; if (!std::filesystem::exists(shaderPackage.shaderPath)) { error = "Shader source not found for package " + shaderPackage.id + ": " + shaderPackage.shaderPath.string(); return false; } shaderPackage.shaderWriteTime = std::filesystem::last_write_time(shaderPackage.shaderPath); shaderPackage.manifestWriteTime = std::filesystem::last_write_time(shaderPackage.manifestPath); const JsonValue* texturesValue = manifestJson.find("textures"); if (texturesValue) { if (!texturesValue->isArray()) { error = "Shader manifest 'textures' field must be an array in: " + manifestPath.string(); return false; } for (const JsonValue& textureJson : texturesValue->asArray()) { if (!textureJson.isObject()) { error = "Shader texture entry must be an object in: " + manifestPath.string(); return false; } const JsonValue* textureIdValue = textureJson.find("id"); const JsonValue* texturePathValue = textureJson.find("path"); if (!textureIdValue || Trim(textureIdValue->asString()).empty() || !texturePathValue || Trim(texturePathValue->asString()).empty()) { error = "Shader texture is missing required 'id' or 'path' in: " + manifestPath.string(); return false; } ShaderTextureAsset textureAsset; textureAsset.id = textureIdValue->asString(); textureAsset.path = shaderPackage.directoryPath / texturePathValue->asString(); if (!std::filesystem::exists(textureAsset.path)) { error = "Shader texture asset not found for package " + shaderPackage.id + ": " + textureAsset.path.string(); return false; } textureAsset.writeTime = std::filesystem::last_write_time(textureAsset.path); shaderPackage.textureAssets.push_back(textureAsset); } } if (const JsonValue* temporalValue = manifestJson.find("temporal")) { if (!temporalValue->isObject()) { error = "Shader manifest 'temporal' field must be an object in: " + manifestPath.string(); return false; } const JsonValue* enabledValue = temporalValue->find("enabled"); if (enabledValue && enabledValue->asBoolean(false)) { const JsonValue* historySourceValue = temporalValue->find("historySource"); const JsonValue* historyLengthValue = temporalValue->find("historyLength"); if (!historySourceValue || Trim(historySourceValue->asString()).empty()) { error = "Temporal shader is missing required 'historySource' in: " + manifestPath.string(); return false; } if (!historyLengthValue || !historyLengthValue->isNumber()) { error = "Temporal shader is missing required numeric 'historyLength' in: " + manifestPath.string(); return false; } TemporalHistorySource historySource = TemporalHistorySource::None; if (!ParseTemporalHistorySource(historySourceValue->asString(), historySource)) { error = "Unsupported temporal historySource '" + historySourceValue->asString() + "' in: " + manifestPath.string(); return false; } const double requestedHistoryLength = historyLengthValue->asNumber(); if (!IsFiniteNumber(requestedHistoryLength) || requestedHistoryLength <= 0.0 || std::floor(requestedHistoryLength) != requestedHistoryLength) { error = "Temporal shader 'historyLength' must be a positive integer in: " + manifestPath.string(); return false; } shaderPackage.temporal.enabled = true; shaderPackage.temporal.historySource = historySource; shaderPackage.temporal.requestedHistoryLength = static_cast(requestedHistoryLength); shaderPackage.temporal.effectiveHistoryLength = std::min(shaderPackage.temporal.requestedHistoryLength, mConfig.maxTemporalHistoryFrames); } } const JsonValue* parametersValue = manifestJson.find("parameters"); if (parametersValue && parametersValue->isArray()) { for (const JsonValue& parameterJson : parametersValue->asArray()) { const JsonValue* parameterIdValue = parameterJson.find("id"); const JsonValue* parameterLabelValue = parameterJson.find("label"); const JsonValue* parameterTypeValue = parameterJson.find("type"); if (!parameterIdValue || !parameterLabelValue || !parameterTypeValue) { error = "Shader parameter is missing required fields in: " + manifestPath.string(); return false; } ShaderParameterDefinition definition; definition.id = parameterIdValue->asString(); definition.label = parameterLabelValue->asString(); if (!ParseShaderParameterType(parameterTypeValue->asString(), definition.type)) { error = "Unsupported parameter type '" + parameterTypeValue->asString() + "' in: " + manifestPath.string(); return false; } if (const JsonValue* defaultValue = parameterJson.find("default")) { if (definition.type == ShaderParameterType::Boolean) definition.defaultBoolean = defaultValue->asBoolean(false); else if (definition.type == ShaderParameterType::Enum) definition.defaultEnumValue = defaultValue->asString(); else if (defaultValue->isNumber()) definition.defaultNumbers.push_back(defaultValue->asNumber()); else if (defaultValue->isArray()) definition.defaultNumbers = JsonArrayToNumbers(*defaultValue); } if (const JsonValue* minValue = parameterJson.find("min")) { if (minValue->isNumber()) definition.minNumbers.push_back(minValue->asNumber()); else if (minValue->isArray()) definition.minNumbers = JsonArrayToNumbers(*minValue); } if (const JsonValue* maxValue = parameterJson.find("max")) { if (maxValue->isNumber()) definition.maxNumbers.push_back(maxValue->asNumber()); else if (maxValue->isArray()) definition.maxNumbers = JsonArrayToNumbers(*maxValue); } if (const JsonValue* stepValue = parameterJson.find("step")) { if (stepValue->isNumber()) definition.stepNumbers.push_back(stepValue->asNumber()); else if (stepValue->isArray()) definition.stepNumbers = JsonArrayToNumbers(*stepValue); } if (definition.type == ShaderParameterType::Enum) { const JsonValue* optionsValue = parameterJson.find("options"); if (!optionsValue || !optionsValue->isArray()) { error = "Enum parameter is missing 'options' in: " + manifestPath.string(); return false; } for (const JsonValue& optionJson : optionsValue->asArray()) { const JsonValue* value = optionJson.find("value"); const JsonValue* label = optionJson.find("label"); if (!value || !label) { error = "Enum parameter option is missing 'value' or 'label' in: " + manifestPath.string(); return false; } ShaderParameterOption option; option.value = value->asString(); option.label = label->asString(); definition.enumOptions.push_back(option); } bool defaultFound = definition.defaultEnumValue.empty(); for (const ShaderParameterOption& option : definition.enumOptions) { if (option.value == definition.defaultEnumValue) { defaultFound = true; break; } } if (!defaultFound) { error = "Enum parameter default is not present in its option list for: " + definition.id; return false; } } shaderPackage.parameters.push_back(definition); } } return true; } bool RuntimeHost::NormalizeAndValidateValue(const ShaderParameterDefinition& definition, const JsonValue& value, ShaderParameterValue& normalizedValue, std::string& error) const { normalizedValue = DefaultValueForDefinition(definition); switch (definition.type) { case ShaderParameterType::Float: { if (!value.isNumber()) { error = "Expected numeric value for float parameter '" + definition.id + "'."; return false; } double number = value.asNumber(); if (!IsFiniteNumber(number)) { error = "Float parameter '" + definition.id + "' must be finite."; return false; } if (!definition.minNumbers.empty()) number = std::max(number, definition.minNumbers.front()); if (!definition.maxNumbers.empty()) number = std::min(number, definition.maxNumbers.front()); normalizedValue.numberValues = { number }; return true; } case ShaderParameterType::Vec2: case ShaderParameterType::Color: { std::vector numbers = JsonArrayToNumbers(value); const std::size_t expectedSize = definition.type == ShaderParameterType::Vec2 ? 2 : 4; if (numbers.size() != expectedSize) { error = "Expected array value of size " + std::to_string(expectedSize) + " for parameter '" + definition.id + "'."; return false; } for (std::size_t index = 0; index < numbers.size(); ++index) { if (!IsFiniteNumber(numbers[index])) { error = "Parameter '" + definition.id + "' contains a non-finite value."; return false; } if (index < definition.minNumbers.size()) numbers[index] = std::max(numbers[index], definition.minNumbers[index]); if (index < definition.maxNumbers.size()) numbers[index] = std::min(numbers[index], definition.maxNumbers[index]); } normalizedValue.numberValues = numbers; return true; } case ShaderParameterType::Boolean: if (!value.isBoolean()) { error = "Expected boolean value for parameter '" + definition.id + "'."; return false; } normalizedValue.booleanValue = value.asBoolean(); return true; case ShaderParameterType::Enum: { if (!value.isString()) { error = "Expected string value for enum parameter '" + definition.id + "'."; return false; } const std::string selectedValue = value.asString(); for (const ShaderParameterOption& option : definition.enumOptions) { if (option.value == selectedValue) { normalizedValue.enumValue = selectedValue; return true; } } error = "Enum parameter '" + definition.id + "' received unsupported option '" + selectedValue + "'."; return false; } } return false; } ShaderParameterValue RuntimeHost::DefaultValueForDefinition(const ShaderParameterDefinition& definition) const { ShaderParameterValue value; switch (definition.type) { case ShaderParameterType::Float: value.numberValues = definition.defaultNumbers.empty() ? std::vector{ 0.0 } : definition.defaultNumbers; break; case ShaderParameterType::Vec2: value.numberValues = definition.defaultNumbers.size() == 2 ? definition.defaultNumbers : std::vector{ 0.0, 0.0 }; break; case ShaderParameterType::Color: value.numberValues = definition.defaultNumbers.size() == 4 ? definition.defaultNumbers : std::vector{ 1.0, 1.0, 1.0, 1.0 }; break; case ShaderParameterType::Boolean: value.booleanValue = definition.defaultBoolean; break; case ShaderParameterType::Enum: value.enumValue = definition.defaultEnumValue; break; } return value; } void RuntimeHost::EnsureLayerDefaultsLocked(LayerPersistentState& layerState, const ShaderPackage& shaderPackage) const { for (const ShaderParameterDefinition& definition : shaderPackage.parameters) { if (layerState.parameterValues.find(definition.id) == layerState.parameterValues.end()) layerState.parameterValues[definition.id] = DefaultValueForDefinition(definition); } } std::string RuntimeHost::BuildWrapperSlangSource(const ShaderPackage& shaderPackage) const { std::ostringstream source; source << "struct FragmentInput\n"; source << "{\n"; source << "\tfloat4 position : SV_Position;\n"; source << "\tfloat2 texCoord : TEXCOORD0;\n"; source << "};\n\n"; source << "struct ShaderContext\n"; source << "{\n"; source << "\tfloat2 uv;\n"; source << "\tfloat4 sourceColor;\n"; source << "\tfloat2 inputResolution;\n"; source << "\tfloat2 outputResolution;\n"; source << "\tfloat time;\n"; source << "\tfloat frameCount;\n"; source << "\tfloat mixAmount;\n"; source << "\tfloat bypass;\n"; source << "\tint sourceHistoryLength;\n"; source << "\tint temporalHistoryLength;\n"; source << "};\n\n"; source << "cbuffer GlobalParams\n"; source << "{\n"; source << "\tfloat gTime;\n"; source << "\tfloat2 gInputResolution;\n"; source << "\tfloat2 gOutputResolution;\n"; source << "\tfloat gFrameCount;\n"; source << "\tfloat gMixAmount;\n"; source << "\tfloat gBypass;\n"; source << "\tint gSourceHistoryLength;\n"; source << "\tint gTemporalHistoryLength;\n"; for (const ShaderParameterDefinition& definition : shaderPackage.parameters) source << "\t" << SlangTypeForParameter(definition.type).substr(strlen("uniform ")) << " " << definition.id << ";\n"; source << "};\n\n"; source << "Sampler2D gVideoInput;\n"; for (unsigned index = 0; index < mConfig.maxTemporalHistoryFrames; ++index) source << "Sampler2D gSourceHistory" << index << ";\n"; for (unsigned index = 0; index < mConfig.maxTemporalHistoryFrames; ++index) source << "Sampler2D gTemporalHistory" << index << ";\n"; for (const ShaderTextureAsset& textureAsset : shaderPackage.textureAssets) source << "Sampler2D " << textureAsset.id << ";\n"; source << "\n"; source << "float4 sampleVideo(float2 tc)\n"; source << "{\n"; source << "\treturn gVideoInput.Sample(tc);\n"; source << "}\n\n"; source << "float4 sampleSourceHistory(int framesAgo, float2 tc)\n"; source << "{\n"; source << "\tif (gSourceHistoryLength <= 0)\n"; source << "\t\treturn sampleVideo(tc);\n"; source << "\tint clampedIndex = framesAgo;\n"; source << "\tif (clampedIndex < 0)\n"; source << "\t\tclampedIndex = 0;\n"; source << "\tif (clampedIndex >= gSourceHistoryLength)\n"; source << "\t\tclampedIndex = gSourceHistoryLength - 1;\n"; source << "\tswitch (clampedIndex)\n"; source << "\t{\n"; for (unsigned index = 0; index < mConfig.maxTemporalHistoryFrames; ++index) source << "\tcase " << index << ": return gSourceHistory" << index << ".Sample(tc);\n"; source << "\tdefault: return sampleVideo(tc);\n"; source << "\t}\n"; source << "}\n\n"; source << "float4 sampleTemporalHistory(int framesAgo, float2 tc)\n"; source << "{\n"; source << "\tif (gTemporalHistoryLength <= 0)\n"; source << "\t\treturn sampleVideo(tc);\n"; source << "\tint clampedIndex = framesAgo;\n"; source << "\tif (clampedIndex < 0)\n"; source << "\t\tclampedIndex = 0;\n"; source << "\tif (clampedIndex >= gTemporalHistoryLength)\n"; source << "\t\tclampedIndex = gTemporalHistoryLength - 1;\n"; source << "\tswitch (clampedIndex)\n"; source << "\t{\n"; for (unsigned index = 0; index < mConfig.maxTemporalHistoryFrames; ++index) source << "\tcase " << index << ": return gTemporalHistory" << index << ".Sample(tc);\n"; source << "\tdefault: return sampleVideo(tc);\n"; source << "\t}\n"; source << "}\n\n"; source << "#include \"" << shaderPackage.shaderPath.generic_string() << "\"\n\n"; source << "[shader(\"fragment\")]\n"; source << "float4 fragmentMain(FragmentInput input) : SV_Target\n"; source << "{\n"; source << "\tShaderContext context;\n"; source << "\tcontext.uv = input.texCoord;\n"; source << "\tcontext.sourceColor = sampleVideo(context.uv);\n"; source << "\tcontext.inputResolution = gInputResolution;\n"; source << "\tcontext.outputResolution = gOutputResolution;\n"; source << "\tcontext.time = gTime;\n"; source << "\tcontext.frameCount = gFrameCount;\n"; source << "\tcontext.mixAmount = gMixAmount;\n"; source << "\tcontext.bypass = gBypass;\n"; source << "\tcontext.sourceHistoryLength = gSourceHistoryLength;\n"; source << "\tcontext.temporalHistoryLength = gTemporalHistoryLength;\n"; source << "\tfloat4 effectedColor = " << shaderPackage.entryPoint << "(context);\n"; source << "\tfloat mixValue = clamp(gBypass > 0.5 ? 0.0 : gMixAmount, 0.0, 1.0);\n"; source << "\treturn lerp(context.sourceColor, effectedColor, mixValue);\n"; source << "}\n"; return source.str(); } bool RuntimeHost::FindSlangCompiler(std::filesystem::path& compilerPath, std::string& error) const { std::filesystem::path thirdPartyRoot = mRepoRoot / "3rdParty"; if (!std::filesystem::exists(thirdPartyRoot)) { error = "3rdParty directory was not found under the repository root."; return false; } for (const auto& entry : std::filesystem::directory_iterator(thirdPartyRoot)) { if (!entry.is_directory()) continue; std::filesystem::path candidate = entry.path() / "bin" / "slangc.exe"; if (std::filesystem::exists(candidate)) { compilerPath = candidate; return true; } } error = "Could not find slangc.exe under 3rdParty."; return false; } bool RuntimeHost::RunSlangCompiler(const std::filesystem::path& wrapperPath, const std::filesystem::path& outputPath, std::string& error) const { std::filesystem::path compilerPath; if (!FindSlangCompiler(compilerPath, error)) return false; std::string commandLine = "\"" + compilerPath.string() + "\" \"" + wrapperPath.string() + "\" -target glsl -profile glsl_430 -entry fragmentMain -stage fragment -o \"" + outputPath.string() + "\""; STARTUPINFOA startupInfo = {}; PROCESS_INFORMATION processInfo = {}; startupInfo.cb = sizeof(startupInfo); std::vector mutableCommandLine(commandLine.begin(), commandLine.end()); mutableCommandLine.push_back('\0'); if (!CreateProcessA(NULL, mutableCommandLine.data(), NULL, NULL, FALSE, CREATE_NO_WINDOW, NULL, mRepoRoot.string().c_str(), &startupInfo, &processInfo)) { error = "Failed to launch slangc.exe."; return false; } WaitForSingleObject(processInfo.hProcess, INFINITE); DWORD exitCode = 0; GetExitCodeProcess(processInfo.hProcess, &exitCode); CloseHandle(processInfo.hThread); CloseHandle(processInfo.hProcess); if (exitCode != 0) { error = "slangc.exe returned a non-zero exit code while compiling the active shader package."; return false; } return true; } bool RuntimeHost::PatchGeneratedGlsl(std::string& shaderText, std::string& error) const { if (shaderText.find("#version 450") == std::string::npos) { error = "Generated GLSL did not include the expected version header."; return false; } shaderText = ReplaceAll(shaderText, "#version 450", "#version 430 core"); shaderText = std::regex_replace(shaderText, std::regex(R"(#extension GL_EXT_samplerless_texture_functions : require\r?\n)"), ""); shaderText = std::regex_replace(shaderText, std::regex(R"(layout\(row_major\) uniform;\r?\n)"), ""); shaderText = std::regex_replace(shaderText, std::regex(R"(layout\(row_major\) buffer;\r?\n)"), ""); shaderText = std::regex_replace(shaderText, std::regex(R"(layout\(location = 0\)\s*in vec2 ([A-Za-z0-9_]+);)"), "in vec2 vTexCoord;"); shaderText = ReplaceAll(shaderText, "input_texCoord_0", "vTexCoord"); std::smatch match; std::regex outRegex(R"(layout\(location = 0\)\s*out vec4 ([A-Za-z0-9_]+);)"); if (std::regex_search(shaderText, match, outRegex)) { const std::string outputName = match[1].str(); shaderText = std::regex_replace(shaderText, outRegex, "layout(location = 0) out vec4 fragColor;"); shaderText = ReplaceAll(shaderText, outputName + " =", "fragColor ="); } return true; } std::string RuntimeHost::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 RuntimeHost::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); std::ofstream output(path, std::ios::binary); if (!output) { error = "Could not write file: " + path.string(); return false; } output << contents; return output.good(); } bool RuntimeHost::ResolvePaths(std::string& error) { mRepoRoot = FindRepoRootCandidate(); if (mRepoRoot.empty()) { error = "Could not locate the repository root from the current runtime path."; return false; } const std::filesystem::path builtUiRoot = mRepoRoot / "ui" / "dist"; mUiRoot = std::filesystem::exists(builtUiRoot) ? builtUiRoot : (mRepoRoot / "ui"); mConfigPath = mRepoRoot / "config" / "runtime-host.json"; mShaderRoot = mRepoRoot / mConfig.shaderLibrary; mRuntimeRoot = mRepoRoot / "runtime"; mPresetRoot = mRuntimeRoot / "stack_presets"; mRuntimeStatePath = mRuntimeRoot / "runtime_state.json"; mWrapperPath = mRuntimeRoot / "shader_cache" / "active_shader_wrapper.slang"; mGeneratedGlslPath = mRuntimeRoot / "shader_cache" / "active_shader.raw.frag"; mPatchedGlslPath = mRuntimeRoot / "shader_cache" / "active_shader.frag"; std::error_code fsError; std::filesystem::create_directories(mRuntimeRoot / "shader_cache", fsError); std::filesystem::create_directories(mPresetRoot, fsError); return true; } JsonValue RuntimeHost::BuildStateValue() const { std::lock_guard lock(mMutex); JsonValue root = JsonValue::MakeObject(); JsonValue app = JsonValue::MakeObject(); app.set("serverPort", JsonValue(static_cast(mServerPort))); app.set("autoReload", JsonValue(mAutoReloadEnabled)); app.set("maxTemporalHistoryFrames", JsonValue(static_cast(mConfig.maxTemporalHistoryFrames))); app.set("enableExternalKeying", JsonValue(mConfig.enableExternalKeying)); root.set("app", app); JsonValue runtime = JsonValue::MakeObject(); runtime.set("layerCount", JsonValue(static_cast(mPersistentState.layers.size()))); runtime.set("compileSucceeded", JsonValue(mCompileSucceeded)); runtime.set("compileMessage", JsonValue(mCompileMessage)); root.set("runtime", runtime); JsonValue video = JsonValue::MakeObject(); video.set("hasSignal", JsonValue(mHasSignal)); video.set("width", JsonValue(static_cast(mSignalWidth))); video.set("height", JsonValue(static_cast(mSignalHeight))); video.set("modeName", JsonValue(mSignalModeName)); root.set("video", video); JsonValue deckLink = JsonValue::MakeObject(); deckLink.set("modelName", JsonValue(mDeckLinkOutputStatus.modelName)); deckLink.set("supportsInternalKeying", JsonValue(mDeckLinkOutputStatus.supportsInternalKeying)); deckLink.set("supportsExternalKeying", JsonValue(mDeckLinkOutputStatus.supportsExternalKeying)); deckLink.set("keyerInterfaceAvailable", JsonValue(mDeckLinkOutputStatus.keyerInterfaceAvailable)); deckLink.set("externalKeyingRequested", JsonValue(mDeckLinkOutputStatus.externalKeyingRequested)); deckLink.set("externalKeyingActive", JsonValue(mDeckLinkOutputStatus.externalKeyingActive)); deckLink.set("statusMessage", JsonValue(mDeckLinkOutputStatus.statusMessage)); root.set("decklink", deckLink); JsonValue performance = JsonValue::MakeObject(); performance.set("frameBudgetMs", JsonValue(mFrameBudgetMilliseconds)); performance.set("renderMs", JsonValue(mRenderMilliseconds)); performance.set("smoothedRenderMs", JsonValue(mSmoothedRenderMilliseconds)); performance.set("budgetUsedPercent", JsonValue(mFrameBudgetMilliseconds > 0.0 ? (mSmoothedRenderMilliseconds / mFrameBudgetMilliseconds) * 100.0 : 0.0)); root.set("performance", performance); JsonValue shaderLibrary = JsonValue::MakeArray(); for (const std::string& shaderId : mPackageOrder) { auto shaderIt = mPackagesById.find(shaderId); if (shaderIt == mPackagesById.end()) continue; JsonValue shader = JsonValue::MakeObject(); shader.set("id", JsonValue(shaderIt->second.id)); shader.set("name", JsonValue(shaderIt->second.displayName)); shader.set("description", JsonValue(shaderIt->second.description)); shader.set("category", JsonValue(shaderIt->second.category)); if (shaderIt->second.temporal.enabled) { JsonValue temporal = JsonValue::MakeObject(); temporal.set("enabled", JsonValue(true)); temporal.set("historySource", JsonValue(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); } 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", SerializeLayerStackLocked()); return root; } JsonValue RuntimeHost::SerializeLayerStackLocked() const { JsonValue layers = JsonValue::MakeArray(); for (const LayerPersistentState& layer : mPersistentState.layers) { auto shaderIt = mPackagesById.find(layer.shaderId); if (shaderIt == mPackagesById.end()) continue; JsonValue layerValue = JsonValue::MakeObject(); layerValue.set("id", JsonValue(layer.id)); layerValue.set("shaderId", JsonValue(layer.shaderId)); layerValue.set("shaderName", JsonValue(shaderIt->second.displayName)); layerValue.set("bypass", JsonValue(layer.bypass)); if (shaderIt->second.temporal.enabled) { JsonValue temporal = JsonValue::MakeObject(); temporal.set("enabled", JsonValue(true)); temporal.set("historySource", JsonValue(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))); layerValue.set("temporal", temporal); } JsonValue parameters = JsonValue::MakeArray(); for (const ShaderParameterDefinition& definition : shaderIt->second.parameters) { JsonValue parameter = JsonValue::MakeObject(); parameter.set("id", JsonValue(definition.id)); parameter.set("label", JsonValue(definition.label)); parameter.set("type", JsonValue(ShaderParameterTypeToString(definition.type))); if (!definition.minNumbers.empty()) { JsonValue minValue = JsonValue::MakeArray(); for (double number : definition.minNumbers) minValue.pushBack(JsonValue(number)); parameter.set("min", minValue); } if (!definition.maxNumbers.empty()) { JsonValue maxValue = JsonValue::MakeArray(); for (double number : definition.maxNumbers) maxValue.pushBack(JsonValue(number)); parameter.set("max", maxValue); } if (!definition.stepNumbers.empty()) { JsonValue stepValue = JsonValue::MakeArray(); for (double number : definition.stepNumbers) stepValue.pushBack(JsonValue(number)); parameter.set("step", stepValue); } if (definition.type == ShaderParameterType::Enum) { JsonValue options = JsonValue::MakeArray(); for (const ShaderParameterOption& option : definition.enumOptions) { JsonValue optionValue = JsonValue::MakeObject(); optionValue.set("value", JsonValue(option.value)); optionValue.set("label", JsonValue(option.label)); options.pushBack(optionValue); } parameter.set("options", options); } ShaderParameterValue value = DefaultValueForDefinition(definition); auto valueIt = layer.parameterValues.find(definition.id); if (valueIt != layer.parameterValues.end()) value = valueIt->second; parameter.set("value", SerializeParameterValue(definition, value)); parameters.pushBack(parameter); } layerValue.set("parameters", parameters); layers.pushBack(layerValue); } return layers; } bool RuntimeHost::DeserializeLayerStackLocked(const JsonValue& layersValue, std::vector& layers, std::string& error) { for (const JsonValue& layerValue : layersValue.asArray()) { if (!layerValue.isObject()) continue; const JsonValue* shaderIdValue = layerValue.find("shaderId"); if (!shaderIdValue) continue; const std::string shaderId = shaderIdValue->asString(); auto shaderIt = mPackagesById.find(shaderId); if (shaderIt == mPackagesById.end()) { error = "Preset references unknown shader id: " + shaderId; return false; } LayerPersistentState layer; layer.id = GenerateLayerId(); layer.shaderId = shaderId; if (const JsonValue* bypassValue = layerValue.find("bypass")) layer.bypass = bypassValue->asBoolean(false); if (const JsonValue* parametersValue = layerValue.find("parameters")) { for (const JsonValue& parameterValue : parametersValue->asArray()) { if (!parameterValue.isObject()) continue; const JsonValue* parameterIdValue = parameterValue.find("id"); const JsonValue* valueValue = parameterValue.find("value"); if (!parameterIdValue || !valueValue) continue; const std::string parameterId = parameterIdValue->asString(); auto definitionIt = std::find_if(shaderIt->second.parameters.begin(), shaderIt->second.parameters.end(), [¶meterId](const ShaderParameterDefinition& definition) { return definition.id == parameterId; }); if (definitionIt == shaderIt->second.parameters.end()) continue; ShaderParameterValue normalizedValue; if (!NormalizeAndValidateValue(*definitionIt, *valueValue, normalizedValue, error)) return false; layer.parameterValues[parameterId] = normalizedValue; } } EnsureLayerDefaultsLocked(layer, shaderIt->second); layers.push_back(layer); } return true; } std::vector RuntimeHost::GetStackPresetNamesLocked() const { std::vector presetNames; std::error_code fsError; if (!std::filesystem::exists(mPresetRoot, fsError)) return presetNames; for (const auto& entry : std::filesystem::directory_iterator(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 RuntimeHost::MakeSafePresetFileStem(const std::string& presetName) const { std::string trimmed = Trim(presetName); std::string safe; safe.reserve(trimmed.size()); for (unsigned char ch : trimmed) { if (std::isalnum(ch)) safe.push_back(static_cast(std::tolower(ch))); else if (ch == ' ' || ch == '-' || ch == '_') { if (safe.empty() || safe.back() == '-') continue; safe.push_back('-'); } } while (!safe.empty() && safe.back() == '-') safe.pop_back(); return safe; } JsonValue RuntimeHost::SerializeParameterValue(const ShaderParameterDefinition& definition, const ShaderParameterValue& value) const { switch (definition.type) { case ShaderParameterType::Boolean: return JsonValue(value.booleanValue); case ShaderParameterType::Enum: return JsonValue(value.enumValue); case ShaderParameterType::Float: return JsonValue(value.numberValues.empty() ? 0.0 : value.numberValues.front()); case ShaderParameterType::Vec2: case ShaderParameterType::Color: { JsonValue array = JsonValue::MakeArray(); for (double number : value.numberValues) array.pushBack(JsonValue(number)); return array; } } return JsonValue(); } std::string RuntimeHost::TemporalHistorySourceToString(TemporalHistorySource source) const { switch (source) { case TemporalHistorySource::Source: return "source"; case TemporalHistorySource::PreLayerInput: return "preLayerInput"; case TemporalHistorySource::None: default: return "none"; } } RuntimeHost::LayerPersistentState* RuntimeHost::FindLayerById(const std::string& layerId) { auto it = std::find_if(mPersistentState.layers.begin(), mPersistentState.layers.end(), [&layerId](const LayerPersistentState& layer) { return layer.id == layerId; }); return it == mPersistentState.layers.end() ? nullptr : &*it; } const RuntimeHost::LayerPersistentState* RuntimeHost::FindLayerById(const std::string& layerId) const { auto it = std::find_if(mPersistentState.layers.begin(), mPersistentState.layers.end(), [&layerId](const LayerPersistentState& layer) { return layer.id == layerId; }); return it == mPersistentState.layers.end() ? nullptr : &*it; } std::string RuntimeHost::GenerateLayerId() { while (true) { ++mNextLayerId; const std::string candidate = "layer-" + std::to_string(mNextLayerId); if (!FindLayerById(candidate)) return candidate; } }