#include "stdafx.h" #include "RuntimeHost.h" #include "RuntimeClock.h" #include "RuntimeParameterUtils.h" #include "ShaderPackageRegistry.h" #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); } bool IsFiniteNumber(double value) { return std::isfinite(value) != 0; } 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; } 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); } bool JsonValueContainsOnlyNumbers(const JsonValue& value) { if (!value.isArray()) return false; for (const JsonValue& item : value.asArray()) { if (!item.isNumber() || !IsFiniteNumber(item.asNumber())) return false; } return true; } double GenerateStartupRandom() { std::random_device randomDevice; std::uniform_real_distribution distribution(0.0, 1.0); return distribution(randomDevice); } bool TryParseLayerIdNumber(const std::string& layerId, uint64_t& number) { const std::string prefix = "layer-"; if (layerId.rfind(prefix, 0) != 0 || layerId.size() == prefix.size()) return false; uint64_t parsed = 0; for (std::size_t index = prefix.size(); index < layerId.size(); ++index) { const unsigned char ch = static_cast(layerId[index]); if (!std::isdigit(ch)) return false; parsed = parsed * 10 + static_cast(ch - '0'); } number = parsed; return true; } 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(); } 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"; case ShaderParameterType::Text: return "text"; case ShaderParameterType::Trigger: return "trigger"; } return "unknown"; } 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; } if (typeName == "text") { type = ShaderParameterType::Text; return true; } if (typeName == "trigger") { type = ShaderParameterType::Trigger; return true; } return false; } std::string ManifestPathMessage(const std::filesystem::path& manifestPath) { return manifestPath.string(); } bool RequireStringField(const JsonValue& object, const char* fieldName, std::string& value, const std::filesystem::path& manifestPath, std::string& error) { const JsonValue* fieldValue = object.find(fieldName); if (!fieldValue || !fieldValue->isString()) { error = "Shader manifest is missing required string '" + std::string(fieldName) + "' field: " + ManifestPathMessage(manifestPath); return false; } value = fieldValue->asString(); return true; } bool RequireNonEmptyStringField(const JsonValue& object, const char* fieldName, std::string& value, const std::filesystem::path& manifestPath, std::string& error) { if (!RequireStringField(object, fieldName, value, manifestPath, error)) return false; if (Trim(value).empty()) { error = "Shader manifest string '" + std::string(fieldName) + "' must not be empty: " + ManifestPathMessage(manifestPath); return false; } return true; } bool OptionalStringField(const JsonValue& object, const char* fieldName, std::string& value, const std::string& fallback, const std::filesystem::path& manifestPath, std::string& error) { const JsonValue* fieldValue = object.find(fieldName); if (!fieldValue) { value = fallback; return true; } if (!fieldValue->isString()) { error = "Shader manifest field '" + std::string(fieldName) + "' must be a string in: " + ManifestPathMessage(manifestPath); return false; } value = fieldValue->asString(); return true; } bool OptionalArrayField(const JsonValue& object, const char* fieldName, const JsonValue*& value, const std::filesystem::path& manifestPath, std::string& error) { value = object.find(fieldName); if (!value) return true; if (!value->isArray()) { error = "Shader manifest '" + std::string(fieldName) + "' field must be an array in: " + ManifestPathMessage(manifestPath); return false; } return true; } bool OptionalObjectField(const JsonValue& object, const char* fieldName, const JsonValue*& value, const std::filesystem::path& manifestPath, std::string& error) { value = object.find(fieldName); if (!value) return true; if (!value->isObject()) { error = "Shader manifest '" + std::string(fieldName) + "' field must be an object in: " + ManifestPathMessage(manifestPath); return false; } return true; } bool NumberListFromJsonValue(const JsonValue& value, std::vector& numbers, const std::string& fieldName, const std::filesystem::path& manifestPath, std::string& error) { if (value.isNumber()) { numbers.push_back(value.asNumber()); return true; } if (value.isArray()) { numbers = JsonArrayToNumbers(value); if (numbers.size() != value.asArray().size()) { error = "Shader parameter field '" + fieldName + "' must contain only numbers in: " + ManifestPathMessage(manifestPath); return false; } return true; } error = "Shader parameter field '" + fieldName + "' must be a number or array of numbers in: " + ManifestPathMessage(manifestPath); return false; } bool ValidateShaderIdentifier(const std::string& identifier, const std::string& fieldName, const std::filesystem::path& manifestPath, std::string& error) { if (identifier.empty() || !(std::isalpha(static_cast(identifier.front())) || identifier.front() == '_')) { error = "Shader manifest field '" + fieldName + "' must be a valid shader identifier in: " + ManifestPathMessage(manifestPath); return false; } for (char ch : identifier) { const unsigned char unsignedCh = static_cast(ch); if (!(std::isalnum(unsignedCh) || ch == '_')) { error = "Shader manifest field '" + fieldName + "' must be a valid shader identifier in: " + ManifestPathMessage(manifestPath); return false; } } return true; } bool ParseShaderMetadata(const JsonValue& manifestJson, ShaderPackage& shaderPackage, const std::filesystem::path& manifestPath, std::string& error) { if (!RequireStringField(manifestJson, "id", shaderPackage.id, manifestPath, error) || !RequireStringField(manifestJson, "name", shaderPackage.displayName, manifestPath, error) || !OptionalStringField(manifestJson, "description", shaderPackage.description, "", manifestPath, error) || !OptionalStringField(manifestJson, "category", shaderPackage.category, "", manifestPath, error) || !OptionalStringField(manifestJson, "entryPoint", shaderPackage.entryPoint, "shadeVideo", manifestPath, error)) { return false; } if (!ValidateShaderIdentifier(shaderPackage.entryPoint, "entryPoint", manifestPath, error)) return false; shaderPackage.directoryPath = manifestPath.parent_path(); shaderPackage.shaderPath = shaderPackage.directoryPath / "shader.slang"; shaderPackage.manifestPath = manifestPath; return true; } bool ParseTextureAssets(const JsonValue& manifestJson, ShaderPackage& shaderPackage, const std::filesystem::path& manifestPath, std::string& error) { const JsonValue* texturesValue = nullptr; if (!OptionalArrayField(manifestJson, "textures", texturesValue, manifestPath, error)) return false; if (!texturesValue) return true; for (const JsonValue& textureJson : texturesValue->asArray()) { if (!textureJson.isObject()) { error = "Shader texture entry must be an object in: " + ManifestPathMessage(manifestPath); return false; } std::string textureId; std::string texturePath; if (!RequireNonEmptyStringField(textureJson, "id", textureId, manifestPath, error) || !RequireNonEmptyStringField(textureJson, "path", texturePath, manifestPath, error)) { error = "Shader texture is missing required 'id' or 'path' in: " + ManifestPathMessage(manifestPath); return false; } if (!ValidateShaderIdentifier(textureId, "textures[].id", manifestPath, error)) return false; ShaderTextureAsset textureAsset; textureAsset.id = textureId; textureAsset.path = shaderPackage.directoryPath / texturePath; 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); } return true; } bool ParseFontAssets(const JsonValue& manifestJson, ShaderPackage& shaderPackage, const std::filesystem::path& manifestPath, std::string& error) { const JsonValue* fontsValue = nullptr; if (!OptionalArrayField(manifestJson, "fonts", fontsValue, manifestPath, error)) return false; if (!fontsValue) return true; for (const JsonValue& fontJson : fontsValue->asArray()) { if (!fontJson.isObject()) { error = "Shader font entry must be an object in: " + ManifestPathMessage(manifestPath); return false; } std::string fontId; std::string fontPath; if (!RequireNonEmptyStringField(fontJson, "id", fontId, manifestPath, error) || !RequireNonEmptyStringField(fontJson, "path", fontPath, manifestPath, error)) { error = "Shader font is missing required 'id' or 'path' in: " + ManifestPathMessage(manifestPath); return false; } if (!ValidateShaderIdentifier(fontId, "fonts[].id", manifestPath, error)) return false; ShaderFontAsset fontAsset; fontAsset.id = fontId; fontAsset.path = shaderPackage.directoryPath / fontPath; if (!std::filesystem::exists(fontAsset.path)) { error = "Shader font asset not found for package " + shaderPackage.id + ": " + fontAsset.path.string(); return false; } fontAsset.writeTime = std::filesystem::last_write_time(fontAsset.path); shaderPackage.fontAssets.push_back(fontAsset); } return true; } bool ParseTemporalSettings(const JsonValue& manifestJson, ShaderPackage& shaderPackage, unsigned maxTemporalHistoryFrames, const std::filesystem::path& manifestPath, std::string& error) { const JsonValue* temporalValue = nullptr; if (!OptionalObjectField(manifestJson, "temporal", temporalValue, manifestPath, error)) return false; if (!temporalValue) return true; const JsonValue* enabledValue = temporalValue->find("enabled"); if (!enabledValue || !enabledValue->asBoolean(false)) return true; std::string historySourceName; if (!RequireNonEmptyStringField(*temporalValue, "historySource", historySourceName, manifestPath, error)) { error = "Temporal shader is missing required 'historySource' in: " + ManifestPathMessage(manifestPath); return false; } const JsonValue* historyLengthValue = temporalValue->find("historyLength"); if (!historyLengthValue || !historyLengthValue->isNumber()) { error = "Temporal shader is missing required numeric 'historyLength' in: " + ManifestPathMessage(manifestPath); return false; } TemporalHistorySource historySource = TemporalHistorySource::None; if (!ParseTemporalHistorySource(historySourceName, historySource)) { error = "Unsupported temporal historySource '" + historySourceName + "' in: " + ManifestPathMessage(manifestPath); 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: " + ManifestPathMessage(manifestPath); return false; } shaderPackage.temporal.enabled = true; shaderPackage.temporal.historySource = historySource; shaderPackage.temporal.requestedHistoryLength = static_cast(requestedHistoryLength); shaderPackage.temporal.effectiveHistoryLength = std::min(shaderPackage.temporal.requestedHistoryLength, maxTemporalHistoryFrames); return true; } bool ParseParameterNumberField(const JsonValue& parameterJson, const char* fieldName, std::vector& values, const std::filesystem::path& manifestPath, std::string& error) { if (const JsonValue* fieldValue = parameterJson.find(fieldName)) return NumberListFromJsonValue(*fieldValue, values, fieldName, manifestPath, error); return true; } bool ParseParameterDefault(const JsonValue& parameterJson, ShaderParameterDefinition& definition, const std::filesystem::path& manifestPath, std::string& error) { const JsonValue* defaultValue = parameterJson.find("default"); if (!defaultValue) return true; if (definition.type == ShaderParameterType::Boolean) { if (!defaultValue->isBoolean()) { error = "Boolean parameter default must be a boolean for: " + definition.id; return false; } definition.defaultBoolean = defaultValue->asBoolean(false); return true; } if (definition.type == ShaderParameterType::Enum) { if (!defaultValue->isString()) { error = "Enum parameter default must be a string for: " + definition.id; return false; } definition.defaultEnumValue = defaultValue->asString(); return true; } if (definition.type == ShaderParameterType::Text) { if (!defaultValue->isString()) { error = "Text parameter default must be a string for: " + definition.id; return false; } definition.defaultTextValue = defaultValue->asString(); return true; } return NumberListFromJsonValue(*defaultValue, definition.defaultNumbers, "default", manifestPath, error); } bool ParseParameterOptions(const JsonValue& parameterJson, ShaderParameterDefinition& definition, const std::filesystem::path& manifestPath, std::string& error) { const JsonValue* optionsValue = nullptr; if (!OptionalArrayField(parameterJson, "options", optionsValue, manifestPath, error) || !optionsValue) { error = "Enum parameter is missing 'options' in: " + ManifestPathMessage(manifestPath); return false; } for (const JsonValue& optionJson : optionsValue->asArray()) { if (!optionJson.isObject()) { error = "Enum parameter option must be an object in: " + ManifestPathMessage(manifestPath); return false; } ShaderParameterOption option; if (!RequireStringField(optionJson, "value", option.value, manifestPath, error) || !RequireStringField(optionJson, "label", option.label, manifestPath, error)) { error = "Enum parameter option is missing 'value' or 'label' in: " + ManifestPathMessage(manifestPath); return false; } 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; } return true; } bool ParseParameterDefinition(const JsonValue& parameterJson, ShaderParameterDefinition& definition, const std::filesystem::path& manifestPath, std::string& error) { if (!parameterJson.isObject()) { error = "Shader parameter entry must be an object in: " + ManifestPathMessage(manifestPath); return false; } std::string typeName; if (!RequireStringField(parameterJson, "id", definition.id, manifestPath, error) || !RequireStringField(parameterJson, "label", definition.label, manifestPath, error) || !RequireStringField(parameterJson, "type", typeName, manifestPath, error)) { error = "Shader parameter is missing required fields in: " + ManifestPathMessage(manifestPath); return false; } if (!ParseShaderParameterType(typeName, definition.type)) { error = "Unsupported parameter type '" + typeName + "' in: " + ManifestPathMessage(manifestPath); return false; } if (!ValidateShaderIdentifier(definition.id, "parameters[].id", manifestPath, error)) return false; if (!OptionalStringField(parameterJson, "description", definition.description, "", manifestPath, error)) return false; if (!ParseParameterDefault(parameterJson, definition, manifestPath, error) || !ParseParameterNumberField(parameterJson, "min", definition.minNumbers, manifestPath, error) || !ParseParameterNumberField(parameterJson, "max", definition.maxNumbers, manifestPath, error) || !ParseParameterNumberField(parameterJson, "step", definition.stepNumbers, manifestPath, error)) { return false; } if (definition.type == ShaderParameterType::Text) { if (const JsonValue* fontValue = parameterJson.find("font")) { if (!fontValue->isString()) { error = "Text parameter 'font' must be a string for: " + definition.id; return false; } definition.fontId = fontValue->asString(); if (!definition.fontId.empty() && !ValidateShaderIdentifier(definition.fontId, "parameters[].font", manifestPath, error)) return false; } if (const JsonValue* maxLengthValue = parameterJson.find("maxLength")) { if (!maxLengthValue->isNumber() || maxLengthValue->asNumber() < 1.0 || maxLengthValue->asNumber() > 256.0) { error = "Text parameter 'maxLength' must be a number from 1 to 256 for: " + definition.id; return false; } definition.maxLength = static_cast(maxLengthValue->asNumber()); } } if (definition.type == ShaderParameterType::Enum) return ParseParameterOptions(parameterJson, definition, manifestPath, error); return true; } bool ParseParameterDefinitions(const JsonValue& manifestJson, ShaderPackage& shaderPackage, const std::filesystem::path& manifestPath, std::string& error) { const JsonValue* parametersValue = nullptr; if (!OptionalArrayField(manifestJson, "parameters", parametersValue, manifestPath, error)) return false; if (!parametersValue) return true; for (const JsonValue& parameterJson : parametersValue->asArray()) { ShaderParameterDefinition definition; if (!ParseParameterDefinition(parameterJson, definition, manifestPath, error)) return false; shaderPackage.parameters.push_back(definition); } return true; } } RuntimeHost::RuntimeHost() : mHealthTelemetry(), mReloadRequested(false), mCompileSucceeded(false), mStartupRandom(GenerateStartupRandom()), mServerPort(8080), mAutoReloadEnabled(true), mStartTime(std::chrono::steady_clock::now()), mLastScanTime(std::chrono::steady_clock::time_point::min()), mFrameCounter(0), mNextLayerId(0) { } void RuntimeHost::SetSignalStatus(bool hasSignal, unsigned width, unsigned height, const std::string& modeName) { const HealthTelemetry::SignalStatusSnapshot previousStatus = mHealthTelemetry.GetSignalStatusSnapshot(); mHealthTelemetry.ReportSignalStatus(hasSignal, width, height, modeName); if (previousStatus.hasSignal != hasSignal || previousStatus.width != width || previousStatus.height != height || previousStatus.modeName != modeName) { std::lock_guard lock(mMutex); MarkRenderStateDirtyLocked(); } } bool RuntimeHost::TrySetSignalStatus(bool hasSignal, unsigned width, unsigned height, const std::string& modeName) { const HealthTelemetry::SignalStatusSnapshot previousStatus = mHealthTelemetry.GetSignalStatusSnapshot(); if (!mHealthTelemetry.TryReportSignalStatus(hasSignal, width, height, modeName)) return false; if (previousStatus.hasSignal != hasSignal || previousStatus.width != width || previousStatus.height != height || previousStatus.modeName != modeName) { std::lock_guard lock(mMutex); MarkRenderStateDirtyLocked(); } return true; } void RuntimeHost::MarkRenderStateDirtyLocked() { mRenderStateVersion.fetch_add(1, std::memory_order_relaxed); mParameterStateVersion.fetch_add(1, std::memory_order_relaxed); } void RuntimeHost::MarkParameterStateDirtyLocked() { mParameterStateVersion.fetch_add(1, std::memory_order_relaxed); } void RuntimeHost::SetDeckLinkOutputStatus(const std::string& modelName, bool supportsInternalKeying, bool supportsExternalKeying, bool keyerInterfaceAvailable, bool externalKeyingRequested, bool externalKeyingActive, const std::string& statusMessage) { SetVideoIOStatus("decklink", modelName, supportsInternalKeying, supportsExternalKeying, keyerInterfaceAvailable, externalKeyingRequested, externalKeyingActive, statusMessage); } void RuntimeHost::SetVideoIOStatus(const std::string& backendName, const std::string& modelName, bool supportsInternalKeying, bool supportsExternalKeying, bool keyerInterfaceAvailable, bool externalKeyingRequested, bool externalKeyingActive, const std::string& statusMessage) { mHealthTelemetry.ReportVideoIOStatus( backendName, modelName, supportsInternalKeying, supportsExternalKeying, keyerInterfaceAvailable, externalKeyingRequested, externalKeyingActive, statusMessage); } void RuntimeHost::SetPerformanceStats(double frameBudgetMilliseconds, double renderMilliseconds) { mHealthTelemetry.RecordPerformanceStats(frameBudgetMilliseconds, renderMilliseconds); } bool RuntimeHost::TrySetPerformanceStats(double frameBudgetMilliseconds, double renderMilliseconds) { return mHealthTelemetry.TryRecordPerformanceStats(frameBudgetMilliseconds, renderMilliseconds); } void RuntimeHost::SetFramePacingStats(double completionIntervalMilliseconds, double smoothedCompletionIntervalMilliseconds, double maxCompletionIntervalMilliseconds, uint64_t lateFrameCount, uint64_t droppedFrameCount, uint64_t flushedFrameCount) { mHealthTelemetry.RecordFramePacingStats(completionIntervalMilliseconds, smoothedCompletionIntervalMilliseconds, maxCompletionIntervalMilliseconds, lateFrameCount, droppedFrameCount, flushedFrameCount); } bool RuntimeHost::TrySetFramePacingStats(double completionIntervalMilliseconds, double smoothedCompletionIntervalMilliseconds, double maxCompletionIntervalMilliseconds, uint64_t lateFrameCount, uint64_t droppedFrameCount, uint64_t flushedFrameCount) { return mHealthTelemetry.TryRecordFramePacingStats(completionIntervalMilliseconds, smoothedCompletionIntervalMilliseconds, maxCompletionIntervalMilliseconds, lateFrameCount, droppedFrameCount, flushedFrameCount); } 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* oscPortValue = configJson.find("oscPort")) mConfig.oscPort = static_cast(oscPortValue->asNumber(mConfig.oscPort)); if (const JsonValue* oscBindAddressValue = configJson.find("oscBindAddress")) mConfig.oscBindAddress = oscBindAddressValue->asString(); if (const JsonValue* oscSmoothingValue = configJson.find("oscSmoothing")) mConfig.oscSmoothing = Clamp01(oscSmoothingValue->asNumber(mConfig.oscSmoothing)); 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* previewFpsValue = configJson.find("previewFps")) { const double configuredValue = previewFpsValue->asNumber(static_cast(mConfig.previewFps)); mConfig.previewFps = configuredValue <= 0.0 ? 0u : static_cast(configuredValue); } if (const JsonValue* enableExternalKeyingValue = configJson.find("enableExternalKeying")) mConfig.enableExternalKeying = enableExternalKeyingValue->asBoolean(mConfig.enableExternalKeying); if (const JsonValue* videoFormatValue = configJson.find("videoFormat")) { if (videoFormatValue->isString() && !videoFormatValue->asString().empty()) { mConfig.inputVideoFormat = videoFormatValue->asString(); mConfig.outputVideoFormat = videoFormatValue->asString(); } } if (const JsonValue* frameRateValue = configJson.find("frameRate")) { if (frameRateValue->isString() && !frameRateValue->asString().empty()) { mConfig.inputFrameRate = frameRateValue->asString(); mConfig.outputFrameRate = frameRateValue->asString(); } else if (frameRateValue->isNumber()) { std::ostringstream stream; stream << frameRateValue->asNumber(); mConfig.inputFrameRate = stream.str(); mConfig.outputFrameRate = stream.str(); } } if (const JsonValue* inputVideoFormatValue = configJson.find("inputVideoFormat")) { if (inputVideoFormatValue->isString() && !inputVideoFormatValue->asString().empty()) mConfig.inputVideoFormat = inputVideoFormatValue->asString(); } if (const JsonValue* inputFrameRateValue = configJson.find("inputFrameRate")) { if (inputFrameRateValue->isString() && !inputFrameRateValue->asString().empty()) mConfig.inputFrameRate = inputFrameRateValue->asString(); else if (inputFrameRateValue->isNumber()) { std::ostringstream stream; stream << inputFrameRateValue->asNumber(); mConfig.inputFrameRate = stream.str(); } } if (const JsonValue* outputVideoFormatValue = configJson.find("outputVideoFormat")) { if (outputVideoFormatValue->isString() && !outputVideoFormatValue->asString().empty()) mConfig.outputVideoFormat = outputVideoFormatValue->asString(); } if (const JsonValue* outputFrameRateValue = configJson.find("outputFrameRate")) { if (outputFrameRateValue->isString() && !outputFrameRateValue->asString().empty()) mConfig.outputFrameRate = outputFrameRateValue->asString(); else if (outputFrameRateValue->isNumber()) { std::ostringstream stream; stream << outputFrameRateValue->asNumber(); mConfig.outputFrameRate = stream.str(); } } 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; std::vector packageStatuses; ShaderPackageRegistry registry(mConfig.maxTemporalHistoryFrames); if (!registry.Scan(mShaderRoot, packagesById, packageOrder, packageStatuses, error)) return false; mPackagesById.swap(packagesById); mPackageOrder.swap(packageOrder); mPackageStatuses.swap(packageStatuses); for (auto it = mPersistentState.layers.begin(); it != mPersistentState.layers.end();) { if (mPackagesById.find(it->shaderId) == mPackagesById.end()) it = mPersistentState.layers.erase(it); else ++it; } MarkRenderStateDirtyLocked(); return true; } bool RuntimeHost::NormalizeAndValidateValue(const ShaderParameterDefinition& definition, const JsonValue& value, ShaderParameterValue& normalizedValue, std::string& error) const { return NormalizeAndValidateParameterValue(definition, value, normalizedValue, error); } ShaderParameterValue RuntimeHost::DefaultValueForDefinition(const ShaderParameterDefinition& definition) const { return ::DefaultValueForDefinition(definition); } void RuntimeHost::EnsureLayerDefaultsLocked(LayerPersistentState& layerState, const ShaderPackage& shaderPackage) const { for (const ShaderParameterDefinition& definition : shaderPackage.parameters) { auto valueIt = layerState.parameterValues.find(definition.id); if (valueIt == layerState.parameterValues.end()) { layerState.parameterValues[definition.id] = DefaultValueForDefinition(definition); continue; } JsonValue valueJson; bool shouldNormalize = true; switch (definition.type) { case ShaderParameterType::Float: if (valueIt->second.numberValues.empty()) shouldNormalize = false; else valueJson = JsonValue(valueIt->second.numberValues.front()); break; case ShaderParameterType::Vec2: case ShaderParameterType::Color: valueJson = JsonValue::MakeArray(); for (double number : valueIt->second.numberValues) valueJson.pushBack(JsonValue(number)); break; case ShaderParameterType::Boolean: valueJson = JsonValue(valueIt->second.booleanValue); break; case ShaderParameterType::Enum: valueJson = JsonValue(valueIt->second.enumValue); break; case ShaderParameterType::Text: { const std::string textValue = !valueIt->second.textValue.empty() ? valueIt->second.textValue : valueIt->second.enumValue; if (textValue.empty()) { valueIt->second = DefaultValueForDefinition(definition); shouldNormalize = false; } else { valueJson = JsonValue(textValue); } break; } case ShaderParameterType::Trigger: if (valueIt->second.numberValues.empty()) valueJson = JsonValue(0.0); else valueJson = JsonValue(std::max(0.0, std::floor(valueIt->second.numberValues.front()))); break; } if (!shouldNormalize) continue; ShaderParameterValue normalizedValue; std::string normalizeError; if (NormalizeAndValidateValue(definition, valueJson, normalizedValue, normalizeError)) valueIt->second = normalizedValue; else valueIt->second = DefaultValueForDefinition(definition); } } 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); 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 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"); mDocsRoot = mRepoRoot / "docs"; 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::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); } if (shaderIt->second.feedback.enabled) { JsonValue feedback = JsonValue::MakeObject(); feedback.set("enabled", JsonValue(true)); feedback.set("writePass", JsonValue(shaderIt->second.feedback.writePassId)); layerValue.set("feedback", feedback); } 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)); if (!definition.description.empty()) parameter.set("description", JsonValue(definition.description)); parameter.set("type", JsonValue(ShaderParameterTypeToString(definition.type))); parameter.set("defaultValue", SerializeParameterValue(definition, DefaultValueForDefinition(definition))); 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); } if (definition.type == ShaderParameterType::Text) { parameter.set("maxLength", JsonValue(static_cast(definition.maxLength))); if (!definition.fontId.empty()) parameter.set("font", JsonValue(definition.fontId)); } 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; } void RuntimeHost::NormalizePersistentLayerIdsLocked() { std::set usedIds; uint64_t maxLayerNumber = mNextLayerId; for (LayerPersistentState& layer : mPersistentState.layers) { uint64_t layerNumber = 0; const bool hasReusableId = !layer.id.empty() && usedIds.find(layer.id) == usedIds.end() && TryParseLayerIdNumber(layer.id, layerNumber); if (hasReusableId) { usedIds.insert(layer.id); maxLayerNumber = std::max(maxLayerNumber, layerNumber); continue; } do { ++maxLayerNumber; layer.id = "layer-" + std::to_string(maxLayerNumber); } while (usedIds.find(layer.id) != usedIds.end()); usedIds.insert(layer.id); } mNextLayerId = maxLayerNumber; } 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 { return ::MakeSafePresetFileStem(presetName); } 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::Text: return JsonValue(value.textValue); case ShaderParameterType::Trigger: return JsonValue(value.numberValues.empty() ? 0.0 : value.numberValues.front()); 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; } }