diff --git a/.vscode/tasks.json b/.vscode/tasks.json index d3d6a2f..5ecd7e6 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -11,7 +11,8 @@ "--config", "Debug", "--target", - "LoopThroughWithOpenGLCompositing" + "LoopThroughWithOpenGLCompositing", + "--parallel" ], "group": { "kind": "build", @@ -29,7 +30,8 @@ "--config", "Release", "--target", - "LoopThroughWithOpenGLCompositing" + "LoopThroughWithOpenGLCompositing", + "--parallel" ], "group": "build", "problemMatcher": "$msCompile" diff --git a/CMakeLists.txt b/CMakeLists.txt index 976f5f6..a33afe4 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -104,8 +104,6 @@ set(APP_SOURCES "${APP_DIR}/resource.h" "${APP_DIR}/runtime/coordination/RuntimeCoordinator.cpp" "${APP_DIR}/runtime/coordination/RuntimeCoordinator.h" - "${APP_DIR}/runtime/legacy/RuntimeHost.cpp" - "${APP_DIR}/runtime/legacy/RuntimeHost.h" "${APP_DIR}/runtime/presentation/RuntimeStateJson.cpp" "${APP_DIR}/runtime/presentation/RuntimeStateJson.h" "${APP_DIR}/runtime/presentation/RuntimeStatePresenter.cpp" @@ -120,6 +118,7 @@ set(APP_SOURCES "${APP_DIR}/runtime/store/RuntimeConfigStore.h" "${APP_DIR}/runtime/store/RuntimeStore.cpp" "${APP_DIR}/runtime/store/RuntimeStore.h" + "${APP_DIR}/runtime/store/RuntimeStoreReadModels.h" "${APP_DIR}/runtime/store/ShaderPackageCatalog.cpp" "${APP_DIR}/runtime/store/ShaderPackageCatalog.h" "${APP_DIR}/runtime/support/RuntimeJson.cpp" @@ -159,7 +158,6 @@ target_include_directories(LoopThroughWithOpenGLCompositing PRIVATE "${APP_DIR}/platform" "${APP_DIR}/runtime" "${APP_DIR}/runtime/coordination" - "${APP_DIR}/runtime/legacy" "${APP_DIR}/runtime/presentation" "${APP_DIR}/runtime/snapshot" "${APP_DIR}/runtime/store" diff --git a/apps/LoopThroughWithOpenGLCompositing/runtime/legacy/RuntimeHost.cpp b/apps/LoopThroughWithOpenGLCompositing/runtime/legacy/RuntimeHost.cpp deleted file mode 100644 index 64df428..0000000 --- a/apps/LoopThroughWithOpenGLCompositing/runtime/legacy/RuntimeHost.cpp +++ /dev/null @@ -1,1017 +0,0 @@ -#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::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); -} - -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); - } -} - -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; -} - -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; - } -} diff --git a/apps/LoopThroughWithOpenGLCompositing/runtime/legacy/RuntimeHost.h b/apps/LoopThroughWithOpenGLCompositing/runtime/legacy/RuntimeHost.h deleted file mode 100644 index 5a0f385..0000000 --- a/apps/LoopThroughWithOpenGLCompositing/runtime/legacy/RuntimeHost.h +++ /dev/null @@ -1,103 +0,0 @@ -#pragma once - -#include "HealthTelemetry.h" -#include "RuntimeJson.h" -#include "ShaderTypes.h" - -#include -#include -#include -#include -#include -#include -#include -#include - -class RuntimeStore; -class RuntimeHost -{ -public: - RuntimeHost(); - HealthTelemetry& GetHealthTelemetry() { return mHealthTelemetry; } - const HealthTelemetry& GetHealthTelemetry() const { return mHealthTelemetry; } - bool AutoReloadEnabled() const { return mAutoReloadEnabled; } - -private: - struct AppConfig - { - std::string shaderLibrary = "shaders"; - unsigned short serverPort = 8080; - unsigned short oscPort = 9000; - std::string oscBindAddress = "127.0.0.1"; - double oscSmoothing = 0.18; - bool autoReload = true; - unsigned maxTemporalHistoryFrames = 4; - unsigned previewFps = 30; - bool enableExternalKeying = false; - std::string inputVideoFormat = "1080p"; - std::string inputFrameRate = "59.94"; - std::string outputVideoFormat = "1080p"; - std::string outputFrameRate = "59.94"; - }; - - struct LayerPersistentState - { - std::string id; - std::string shaderId; - bool bypass = false; - std::map parameterValues; - }; - - struct PersistentState - { - std::vector layers; - }; - - bool NormalizeAndValidateValue(const ShaderParameterDefinition& definition, const JsonValue& value, ShaderParameterValue& normalizedValue, std::string& error) const; - ShaderParameterValue DefaultValueForDefinition(const ShaderParameterDefinition& definition) const; - void EnsureLayerDefaultsLocked(LayerPersistentState& layerState, const ShaderPackage& shaderPackage) const; - JsonValue SerializeLayerStackLocked() const; - bool DeserializeLayerStackLocked(const JsonValue& layersValue, std::vector& layers, std::string& error); - void NormalizePersistentLayerIdsLocked(); - JsonValue SerializeParameterValue(const ShaderParameterDefinition& definition, const ShaderParameterValue& value) const; - std::string TemporalHistorySourceToString(TemporalHistorySource source) const; - LayerPersistentState* FindLayerById(const std::string& layerId); - const LayerPersistentState* FindLayerById(const std::string& layerId) const; - std::string GenerateLayerId(); - void MarkRenderStateDirtyLocked(); - void MarkParameterStateDirtyLocked(); - -private: - friend class RuntimeStore; - friend class RuntimeCoordinator; - HealthTelemetry mHealthTelemetry; - mutable std::mutex mMutex; - AppConfig mConfig; - PersistentState mPersistentState; - std::filesystem::path mRepoRoot; - std::filesystem::path mUiRoot; - std::filesystem::path mDocsRoot; - std::filesystem::path mShaderRoot; - std::filesystem::path mRuntimeRoot; - std::filesystem::path mPresetRoot; - std::filesystem::path mRuntimeStatePath; - std::filesystem::path mConfigPath; - std::filesystem::path mWrapperPath; - std::filesystem::path mGeneratedGlslPath; - std::filesystem::path mPatchedGlslPath; - std::map mPackagesById; - std::vector mPackageOrder; - std::vector mPackageStatuses; - bool mReloadRequested; - bool mCompileSucceeded; - std::string mCompileMessage; - double mStartupRandom; - unsigned short mServerPort; - bool mAutoReloadEnabled; - std::chrono::steady_clock::time_point mStartTime; - std::chrono::steady_clock::time_point mLastScanTime; - std::atomic mFrameCounter{ 0 }; - std::atomic mRenderStateVersion{ 0 }; - std::atomic mParameterStateVersion{ 0 }; - uint64_t mNextLayerId; -}; diff --git a/apps/LoopThroughWithOpenGLCompositing/runtime/presentation/RuntimeStateJson.cpp b/apps/LoopThroughWithOpenGLCompositing/runtime/presentation/RuntimeStateJson.cpp index d51954f..f947b9b 100644 --- a/apps/LoopThroughWithOpenGLCompositing/runtime/presentation/RuntimeStateJson.cpp +++ b/apps/LoopThroughWithOpenGLCompositing/runtime/presentation/RuntimeStateJson.cpp @@ -22,37 +22,50 @@ std::string ShaderParameterTypeToString(ShaderParameterType type) JsonValue RuntimeStateJson::SerializeLayerStack(const LayerStackStore& layerStack, const ShaderPackageCatalog& shaderCatalog) { - JsonValue layers = JsonValue::MakeArray(); - for (const LayerStackStore::LayerPersistentState& layer : layerStack.Layers()) + std::map packagesById; + for (const std::string& packageId : shaderCatalog.PackageOrder()) { - const ShaderPackage* shaderPackage = shaderCatalog.FindPackage(layer.shaderId); - if (!shaderPackage) + ShaderPackage shaderPackage; + if (shaderCatalog.CopyPackage(packageId, shaderPackage)) + packagesById[packageId] = shaderPackage; + } + return SerializeLayerStack(layerStack.Layers(), packagesById); +} + +JsonValue RuntimeStateJson::SerializeLayerStack(const std::vector& layerStates, const std::map& packagesById) +{ + JsonValue layersValue = JsonValue::MakeArray(); + for (const LayerStackStore::LayerPersistentState& layer : layerStates) + { + auto shaderIt = packagesById.find(layer.shaderId); + if (shaderIt == packagesById.end()) continue; + const ShaderPackage& shaderPackage = shaderIt->second; JsonValue layerValue = JsonValue::MakeObject(); layerValue.set("id", JsonValue(layer.id)); layerValue.set("shaderId", JsonValue(layer.shaderId)); - layerValue.set("shaderName", JsonValue(shaderPackage->displayName)); + layerValue.set("shaderName", JsonValue(shaderPackage.displayName)); layerValue.set("bypass", JsonValue(layer.bypass)); - if (shaderPackage->temporal.enabled) + if (shaderPackage.temporal.enabled) { JsonValue temporal = JsonValue::MakeObject(); temporal.set("enabled", JsonValue(true)); - temporal.set("historySource", JsonValue(TemporalHistorySourceToString(shaderPackage->temporal.historySource))); - temporal.set("requestedHistoryLength", JsonValue(static_cast(shaderPackage->temporal.requestedHistoryLength))); - temporal.set("effectiveHistoryLength", JsonValue(static_cast(shaderPackage->temporal.effectiveHistoryLength))); + temporal.set("historySource", JsonValue(TemporalHistorySourceToString(shaderPackage.temporal.historySource))); + temporal.set("requestedHistoryLength", JsonValue(static_cast(shaderPackage.temporal.requestedHistoryLength))); + temporal.set("effectiveHistoryLength", JsonValue(static_cast(shaderPackage.temporal.effectiveHistoryLength))); layerValue.set("temporal", temporal); } - if (shaderPackage->feedback.enabled) + if (shaderPackage.feedback.enabled) { JsonValue feedback = JsonValue::MakeObject(); feedback.set("enabled", JsonValue(true)); - feedback.set("writePass", JsonValue(shaderPackage->feedback.writePassId)); + feedback.set("writePass", JsonValue(shaderPackage.feedback.writePassId)); layerValue.set("feedback", feedback); } JsonValue parameters = JsonValue::MakeArray(); - for (const ShaderParameterDefinition& definition : shaderPackage->parameters) + for (const ShaderParameterDefinition& definition : shaderPackage.parameters) { JsonValue parameter = JsonValue::MakeObject(); parameter.set("id", JsonValue(definition.id)); @@ -111,9 +124,9 @@ JsonValue RuntimeStateJson::SerializeLayerStack(const LayerStackStore& layerStac } layerValue.set("parameters", parameters); - layers.pushBack(layerValue); + layersValue.pushBack(layerValue); } - return layers; + return layersValue; } JsonValue RuntimeStateJson::SerializeParameterValue(const ShaderParameterDefinition& definition, const ShaderParameterValue& value) diff --git a/apps/LoopThroughWithOpenGLCompositing/runtime/presentation/RuntimeStateJson.h b/apps/LoopThroughWithOpenGLCompositing/runtime/presentation/RuntimeStateJson.h index 333fb37..c6a4243 100644 --- a/apps/LoopThroughWithOpenGLCompositing/runtime/presentation/RuntimeStateJson.h +++ b/apps/LoopThroughWithOpenGLCompositing/runtime/presentation/RuntimeStateJson.h @@ -5,12 +5,15 @@ #include "ShaderPackageCatalog.h" #include "ShaderTypes.h" +#include #include +#include class RuntimeStateJson { public: static JsonValue SerializeLayerStack(const LayerStackStore& layerStack, const ShaderPackageCatalog& shaderCatalog); + static JsonValue SerializeLayerStack(const std::vector& layers, const std::map& packagesById); static JsonValue SerializeParameterValue(const ShaderParameterDefinition& definition, const ShaderParameterValue& value); static std::string TemporalHistorySourceToString(TemporalHistorySource source); }; diff --git a/apps/LoopThroughWithOpenGLCompositing/runtime/presentation/RuntimeStatePresenter.cpp b/apps/LoopThroughWithOpenGLCompositing/runtime/presentation/RuntimeStatePresenter.cpp index 2bdf887..431235d 100644 --- a/apps/LoopThroughWithOpenGLCompositing/runtime/presentation/RuntimeStatePresenter.cpp +++ b/apps/LoopThroughWithOpenGLCompositing/runtime/presentation/RuntimeStatePresenter.cpp @@ -3,8 +3,6 @@ #include "RuntimeStateJson.h" #include "RuntimeStore.h" -#include - std::string RuntimeStatePresenter::BuildRuntimeStateJson(const RuntimeStore& runtimeStore) { return SerializeJson(BuildRuntimeStateValue(runtimeStore), true); @@ -12,30 +10,30 @@ std::string RuntimeStatePresenter::BuildRuntimeStateJson(const RuntimeStore& run JsonValue RuntimeStatePresenter::BuildRuntimeStateValue(const RuntimeStore& runtimeStore) { - const HealthTelemetry::Snapshot telemetrySnapshot = runtimeStore.mHealthTelemetry.GetSnapshot(); - std::lock_guard lock(runtimeStore.mMutex); + const RuntimeStatePresentationReadModel model = runtimeStore.BuildRuntimeStatePresentationReadModel(); + const HealthTelemetry::Snapshot& telemetrySnapshot = model.telemetry; JsonValue root = JsonValue::MakeObject(); JsonValue app = JsonValue::MakeObject(); - app.set("serverPort", JsonValue(static_cast(runtimeStore.mServerPort))); - app.set("oscPort", JsonValue(static_cast(runtimeStore.mConfigStore.GetConfig().oscPort))); - app.set("oscBindAddress", JsonValue(runtimeStore.mConfigStore.GetConfig().oscBindAddress)); - app.set("oscSmoothing", JsonValue(runtimeStore.mConfigStore.GetConfig().oscSmoothing)); - app.set("autoReload", JsonValue(runtimeStore.mAutoReloadEnabled)); - app.set("maxTemporalHistoryFrames", JsonValue(static_cast(runtimeStore.mConfigStore.GetConfig().maxTemporalHistoryFrames))); - app.set("previewFps", JsonValue(static_cast(runtimeStore.mConfigStore.GetConfig().previewFps))); - app.set("enableExternalKeying", JsonValue(runtimeStore.mConfigStore.GetConfig().enableExternalKeying)); - app.set("inputVideoFormat", JsonValue(runtimeStore.mConfigStore.GetConfig().inputVideoFormat)); - app.set("inputFrameRate", JsonValue(runtimeStore.mConfigStore.GetConfig().inputFrameRate)); - app.set("outputVideoFormat", JsonValue(runtimeStore.mConfigStore.GetConfig().outputVideoFormat)); - app.set("outputFrameRate", JsonValue(runtimeStore.mConfigStore.GetConfig().outputFrameRate)); + app.set("serverPort", JsonValue(static_cast(model.serverPort))); + app.set("oscPort", JsonValue(static_cast(model.config.oscPort))); + app.set("oscBindAddress", JsonValue(model.config.oscBindAddress)); + app.set("oscSmoothing", JsonValue(model.config.oscSmoothing)); + app.set("autoReload", JsonValue(model.autoReloadEnabled)); + app.set("maxTemporalHistoryFrames", JsonValue(static_cast(model.config.maxTemporalHistoryFrames))); + app.set("previewFps", JsonValue(static_cast(model.config.previewFps))); + app.set("enableExternalKeying", JsonValue(model.config.enableExternalKeying)); + app.set("inputVideoFormat", JsonValue(model.config.inputVideoFormat)); + app.set("inputFrameRate", JsonValue(model.config.inputFrameRate)); + app.set("outputVideoFormat", JsonValue(model.config.outputVideoFormat)); + app.set("outputFrameRate", JsonValue(model.config.outputFrameRate)); root.set("app", app); JsonValue runtime = JsonValue::MakeObject(); - runtime.set("layerCount", JsonValue(static_cast(runtimeStore.mLayerStack.LayerCount()))); - runtime.set("compileSucceeded", JsonValue(runtimeStore.mCompileSucceeded)); - runtime.set("compileMessage", JsonValue(runtimeStore.mCompileMessage)); + runtime.set("layerCount", JsonValue(static_cast(model.layerStack.LayerCount()))); + runtime.set("compileSucceeded", JsonValue(model.compileSucceeded)); + runtime.set("compileMessage", JsonValue(model.compileMessage)); root.set("runtime", runtime); JsonValue video = JsonValue::MakeObject(); @@ -83,7 +81,7 @@ JsonValue RuntimeStatePresenter::BuildRuntimeStateValue(const RuntimeStore& runt root.set("performance", performance); JsonValue shaderLibrary = JsonValue::MakeArray(); - for (const ShaderPackageStatus& status : runtimeStore.mShaderCatalog.PackageStatuses()) + for (const ShaderPackageStatus& status : model.packageStatuses) { JsonValue shader = JsonValue::MakeObject(); shader.set("id", JsonValue(status.id)); @@ -94,21 +92,23 @@ JsonValue RuntimeStatePresenter::BuildRuntimeStateValue(const RuntimeStore& runt if (!status.available) shader.set("error", JsonValue(status.error)); - const ShaderPackage* shaderPackage = runtimeStore.mShaderCatalog.FindPackage(status.id); - if (status.available && shaderPackage && shaderPackage->temporal.enabled) + auto shaderIt = model.shaderCatalog.packagesById.find(status.id); + if (status.available && shaderIt != model.shaderCatalog.packagesById.end() && shaderIt->second.temporal.enabled) { + const ShaderPackage& shaderPackage = shaderIt->second; JsonValue temporal = JsonValue::MakeObject(); temporal.set("enabled", JsonValue(true)); - temporal.set("historySource", JsonValue(RuntimeStateJson::TemporalHistorySourceToString(shaderPackage->temporal.historySource))); - temporal.set("requestedHistoryLength", JsonValue(static_cast(shaderPackage->temporal.requestedHistoryLength))); - temporal.set("effectiveHistoryLength", JsonValue(static_cast(shaderPackage->temporal.effectiveHistoryLength))); + temporal.set("historySource", JsonValue(RuntimeStateJson::TemporalHistorySourceToString(shaderPackage.temporal.historySource))); + temporal.set("requestedHistoryLength", JsonValue(static_cast(shaderPackage.temporal.requestedHistoryLength))); + temporal.set("effectiveHistoryLength", JsonValue(static_cast(shaderPackage.temporal.effectiveHistoryLength))); shader.set("temporal", temporal); } - if (status.available && shaderPackage && shaderPackage->feedback.enabled) + if (status.available && shaderIt != model.shaderCatalog.packagesById.end() && shaderIt->second.feedback.enabled) { + const ShaderPackage& shaderPackage = shaderIt->second; JsonValue feedback = JsonValue::MakeObject(); feedback.set("enabled", JsonValue(true)); - feedback.set("writePass", JsonValue(shaderPackage->feedback.writePassId)); + feedback.set("writePass", JsonValue(shaderPackage.feedback.writePassId)); shader.set("feedback", feedback); } shaderLibrary.pushBack(shader); @@ -116,10 +116,10 @@ JsonValue RuntimeStatePresenter::BuildRuntimeStateValue(const RuntimeStore& runt root.set("shaders", shaderLibrary); JsonValue stackPresets = JsonValue::MakeArray(); - for (const std::string& presetName : runtimeStore.GetStackPresetNamesLocked()) + for (const std::string& presetName : model.stackPresetNames) stackPresets.pushBack(JsonValue(presetName)); root.set("stackPresets", stackPresets); - root.set("layers", RuntimeStateJson::SerializeLayerStack(runtimeStore.mLayerStack, runtimeStore.mShaderCatalog)); + root.set("layers", RuntimeStateJson::SerializeLayerStack(model.layerStack.Layers(), model.shaderCatalog.packagesById)); return root; } diff --git a/apps/LoopThroughWithOpenGLCompositing/runtime/snapshot/RenderSnapshotBuilder.cpp b/apps/LoopThroughWithOpenGLCompositing/runtime/snapshot/RenderSnapshotBuilder.cpp index 7d0d999..a8fe6c5 100644 --- a/apps/LoopThroughWithOpenGLCompositing/runtime/snapshot/RenderSnapshotBuilder.cpp +++ b/apps/LoopThroughWithOpenGLCompositing/runtime/snapshot/RenderSnapshotBuilder.cpp @@ -24,19 +24,14 @@ bool RenderSnapshotBuilder::BuildLayerPassFragmentShaderSources(const std::strin if (!mRuntimeStore.CopyShaderPackageForStoredLayer(layerId, shaderPackage, error)) return false; - std::filesystem::path repoRoot; - std::filesystem::path wrapperPath; - std::filesystem::path generatedGlslPath; - std::filesystem::path patchedGlslPath; - unsigned maxTemporalHistoryFrames = 0; - mRuntimeStore.GetShaderCompilerInputs(repoRoot, wrapperPath, generatedGlslPath, patchedGlslPath, maxTemporalHistoryFrames); + const ShaderCompilerInputs inputs = mRuntimeStore.GetShaderCompilerInputs(); ShaderCompiler compiler( - repoRoot, - wrapperPath, - generatedGlslPath, - patchedGlslPath, - maxTemporalHistoryFrames); + inputs.repoRoot, + inputs.wrapperPath, + inputs.generatedGlslPath, + inputs.patchedGlslPath, + inputs.maxTemporalHistoryFrames); passSources.clear(); passSources.reserve(shaderPackage.passes.size()); for (const ShaderPassDefinition& pass : shaderPackage.passes) @@ -83,34 +78,24 @@ void RenderSnapshotBuilder::AdvanceFrame() void RenderSnapshotBuilder::BuildLayerRenderStates(unsigned outputWidth, unsigned outputHeight, std::vector& states) const { - std::lock_guard lock(mRuntimeStore.mMutex); - BuildLayerRenderStatesLocked(outputWidth, outputHeight, states); + BuildLayerRenderStates(outputWidth, outputHeight, mRuntimeStore.BuildRenderSnapshotReadModel(), states); } bool RenderSnapshotBuilder::TryBuildLayerRenderStates(unsigned outputWidth, unsigned outputHeight, std::vector& states) const { - std::unique_lock lock(mRuntimeStore.mMutex, std::try_to_lock); - if (!lock.owns_lock()) - return false; - - BuildLayerRenderStatesLocked(outputWidth, outputHeight, states); + BuildLayerRenderStates(outputWidth, outputHeight, mRuntimeStore.BuildRenderSnapshotReadModel(), states); return true; } bool RenderSnapshotBuilder::TryRefreshLayerParameters(std::vector& states) const { - std::unique_lock lock(mRuntimeStore.mMutex, std::try_to_lock); - if (!lock.owns_lock()) - return false; - - RefreshLayerParametersLocked(states); + RefreshLayerParameters(mRuntimeStore.CopyLayerStates(), states); return true; } void RenderSnapshotBuilder::RefreshDynamicRenderStateFields(std::vector& states) const { - std::lock_guard lock(mRuntimeStore.mMutex); - RefreshDynamicRenderStateFieldsLocked(states); + RefreshDynamicRenderStateFields(mRuntimeStore.GetRenderTimingSnapshot(), states); } void RenderSnapshotBuilder::MarkRenderStateDirty() @@ -124,37 +109,37 @@ void RenderSnapshotBuilder::MarkParameterStateDirty() mParameterStateVersion.fetch_add(1, std::memory_order_relaxed); } -void RenderSnapshotBuilder::BuildLayerRenderStatesLocked(unsigned outputWidth, unsigned outputHeight, std::vector& states) const +void RenderSnapshotBuilder::BuildLayerRenderStates(unsigned outputWidth, unsigned outputHeight, const RenderSnapshotReadModel& readModel, std::vector& states) const { states.clear(); - const HealthTelemetry::SignalStatusSnapshot signalStatus = mRuntimeStore.mHealthTelemetry.GetSignalStatusSnapshot(); - for (const RuntimeStore::LayerPersistentState& layer : mRuntimeStore.mLayerStack.Layers()) + for (const LayerStackStore::LayerPersistentState& layer : readModel.layers) { - const ShaderPackage* shaderPackage = mRuntimeStore.mShaderCatalog.FindPackage(layer.shaderId); - if (!shaderPackage) + auto shaderIt = readModel.packagesById.find(layer.shaderId); + if (shaderIt == readModel.packagesById.end()) continue; + const ShaderPackage& shaderPackage = shaderIt->second; RuntimeRenderState state; state.layerId = layer.id; state.shaderId = layer.shaderId; - state.shaderName = shaderPackage->displayName; + state.shaderName = shaderPackage.displayName; state.mixAmount = 1.0; state.bypass = layer.bypass ? 1.0 : 0.0; - state.inputWidth = signalStatus.width; - state.inputHeight = signalStatus.height; + state.inputWidth = readModel.signalStatus.width; + state.inputHeight = readModel.signalStatus.height; state.outputWidth = outputWidth; state.outputHeight = outputHeight; - state.parameterDefinitions = shaderPackage->parameters; - state.textureAssets = shaderPackage->textureAssets; - state.fontAssets = shaderPackage->fontAssets; - state.isTemporal = shaderPackage->temporal.enabled; - state.temporalHistorySource = shaderPackage->temporal.historySource; - state.requestedTemporalHistoryLength = shaderPackage->temporal.requestedHistoryLength; - state.effectiveTemporalHistoryLength = shaderPackage->temporal.effectiveHistoryLength; - state.feedback = shaderPackage->feedback; + state.parameterDefinitions = shaderPackage.parameters; + state.textureAssets = shaderPackage.textureAssets; + state.fontAssets = shaderPackage.fontAssets; + state.isTemporal = shaderPackage.temporal.enabled; + state.temporalHistorySource = shaderPackage.temporal.historySource; + state.requestedTemporalHistoryLength = shaderPackage.temporal.requestedHistoryLength; + state.effectiveTemporalHistoryLength = shaderPackage.temporal.effectiveHistoryLength; + state.feedback = shaderPackage.feedback; - for (const ShaderParameterDefinition& definition : shaderPackage->parameters) + for (const ShaderParameterDefinition& definition : shaderPackage.parameters) { ShaderParameterValue value = DefaultValueForDefinition(definition); auto valueIt = layer.parameterValues.find(definition.id); @@ -166,16 +151,16 @@ void RenderSnapshotBuilder::BuildLayerRenderStatesLocked(unsigned outputWidth, u states.push_back(state); } - RefreshDynamicRenderStateFieldsLocked(states); + RefreshDynamicRenderStateFields(readModel.timing, states); } -void RenderSnapshotBuilder::RefreshLayerParametersLocked(std::vector& states) const +void RenderSnapshotBuilder::RefreshLayerParameters(const std::vector& layers, std::vector& states) const { for (RuntimeRenderState& state : states) { - const auto layerIt = std::find_if(mRuntimeStore.mLayerStack.Layers().begin(), mRuntimeStore.mLayerStack.Layers().end(), - [&state](const RuntimeStore::LayerPersistentState& layer) { return layer.id == state.layerId; }); - if (layerIt == mRuntimeStore.mLayerStack.Layers().end()) + const auto layerIt = std::find_if(layers.begin(), layers.end(), + [&state](const LayerStackStore::LayerPersistentState& layer) { return layer.id == state.layerId; }); + if (layerIt == layers.end()) continue; state.bypass = layerIt->bypass ? 1.0 : 0.0; @@ -191,10 +176,10 @@ void RenderSnapshotBuilder::RefreshLayerParametersLocked(std::vector& states) const +void RenderSnapshotBuilder::RefreshDynamicRenderStateFields(const RenderTimingSnapshot& timing, std::vector& states) const { const RuntimeClockSnapshot clock = GetRuntimeClockSnapshot(); - const double timeSeconds = std::chrono::duration_cast>(std::chrono::steady_clock::now() - mRuntimeStore.mStartTime).count(); + const double timeSeconds = std::chrono::duration_cast>(std::chrono::steady_clock::now() - timing.startTime).count(); const double frameCount = static_cast(mFrameCounter.load(std::memory_order_relaxed)); for (RuntimeRenderState& state : states) @@ -202,7 +187,7 @@ void RenderSnapshotBuilder::RefreshDynamicRenderStateFieldsLocked(std::vector @@ -32,9 +33,9 @@ public: void MarkParameterStateDirty(); private: - void BuildLayerRenderStatesLocked(unsigned outputWidth, unsigned outputHeight, std::vector& states) const; - void RefreshLayerParametersLocked(std::vector& states) const; - void RefreshDynamicRenderStateFieldsLocked(std::vector& states) const; + void BuildLayerRenderStates(unsigned outputWidth, unsigned outputHeight, const RenderSnapshotReadModel& readModel, std::vector& states) const; + void RefreshLayerParameters(const std::vector& layers, std::vector& states) const; + void RefreshDynamicRenderStateFields(const RenderTimingSnapshot& timing, std::vector& states) const; RuntimeStore& mRuntimeStore; std::atomic mFrameCounter{ 0 }; diff --git a/apps/LoopThroughWithOpenGLCompositing/runtime/store/RuntimeStore.cpp b/apps/LoopThroughWithOpenGLCompositing/runtime/store/RuntimeStore.cpp index 8d15d88..12eab05 100644 --- a/apps/LoopThroughWithOpenGLCompositing/runtime/store/RuntimeStore.cpp +++ b/apps/LoopThroughWithOpenGLCompositing/runtime/store/RuntimeStore.cpp @@ -564,15 +564,60 @@ bool RuntimeStore::CopyShaderPackageForStoredLayer(const std::string& layerId, S return true; } -void RuntimeStore::GetShaderCompilerInputs(std::filesystem::path& repoRoot, std::filesystem::path& wrapperPath, - std::filesystem::path& generatedGlslPath, std::filesystem::path& patchedGlslPath, unsigned& maxTemporalHistoryFrames) const +ShaderCompilerInputs RuntimeStore::GetShaderCompilerInputs() const { std::lock_guard lock(mMutex); - repoRoot = mConfigStore.GetRepoRoot(); - wrapperPath = mConfigStore.GetWrapperPath(); - generatedGlslPath = mConfigStore.GetGeneratedGlslPath(); - patchedGlslPath = mConfigStore.GetPatchedGlslPath(); - maxTemporalHistoryFrames = mConfigStore.GetConfig().maxTemporalHistoryFrames; + ShaderCompilerInputs inputs; + inputs.repoRoot = mConfigStore.GetRepoRoot(); + inputs.wrapperPath = mConfigStore.GetWrapperPath(); + inputs.generatedGlslPath = mConfigStore.GetGeneratedGlslPath(); + inputs.patchedGlslPath = mConfigStore.GetPatchedGlslPath(); + inputs.maxTemporalHistoryFrames = mConfigStore.GetConfig().maxTemporalHistoryFrames; + return inputs; +} + +RenderSnapshotReadModel RuntimeStore::BuildRenderSnapshotReadModel() const +{ + RenderSnapshotReadModel model; + model.signalStatus = mHealthTelemetry.GetSignalStatusSnapshot(); + std::lock_guard lock(mMutex); + model.layers = mLayerStack.Layers(); + model.packagesById = mShaderCatalog.CaptureSnapshot().packagesById; + model.timing.startTime = mStartTime; + model.timing.startupRandom = mStartupRandom; + return model; +} + +std::vector RuntimeStore::CopyLayerStates() const +{ + std::lock_guard lock(mMutex); + return mLayerStack.Layers(); +} + +RenderTimingSnapshot RuntimeStore::GetRenderTimingSnapshot() const +{ + std::lock_guard lock(mMutex); + RenderTimingSnapshot snapshot; + snapshot.startTime = mStartTime; + snapshot.startupRandom = mStartupRandom; + return snapshot; +} + +RuntimeStatePresentationReadModel RuntimeStore::BuildRuntimeStatePresentationReadModel() const +{ + RuntimeStatePresentationReadModel model; + model.telemetry = mHealthTelemetry.GetSnapshot(); + std::lock_guard lock(mMutex); + model.config = mConfigStore.GetConfig(); + model.layerStack = mLayerStack; + model.shaderCatalog = mShaderCatalog.CaptureSnapshot(); + model.packageStatuses = mShaderCatalog.PackageStatuses(); + model.stackPresetNames = GetStackPresetNamesLocked(); + model.serverPort = mServerPort; + model.autoReloadEnabled = mAutoReloadEnabled; + model.compileSucceeded = mCompileSucceeded; + model.compileMessage = mCompileMessage; + return model; } void RuntimeStore::MarkRenderStateDirtyLocked() diff --git a/apps/LoopThroughWithOpenGLCompositing/runtime/store/RuntimeStore.h b/apps/LoopThroughWithOpenGLCompositing/runtime/store/RuntimeStore.h index d023c07..4ebcea7 100644 --- a/apps/LoopThroughWithOpenGLCompositing/runtime/store/RuntimeStore.h +++ b/apps/LoopThroughWithOpenGLCompositing/runtime/store/RuntimeStore.h @@ -5,6 +5,7 @@ #include "RenderSnapshotBuilder.h" #include "RuntimeConfigStore.h" #include "RuntimeJson.h" +#include "RuntimeStoreReadModels.h" #include "ShaderPackageCatalog.h" #include "ShaderTypes.h" @@ -14,8 +15,6 @@ #include #include -class RuntimeStatePresenter; - class RuntimeStore { public: @@ -70,19 +69,20 @@ public: void SetCompileStatus(bool succeeded, const std::string& message); void ClearReloadRequest(); + bool CopyShaderPackageForStoredLayer(const std::string& layerId, ShaderPackage& shaderPackage, std::string& error) const; + ::ShaderCompilerInputs GetShaderCompilerInputs() const; + ::RenderSnapshotReadModel BuildRenderSnapshotReadModel() const; + std::vector CopyLayerStates() const; + ::RenderTimingSnapshot GetRenderTimingSnapshot() const; + ::RuntimeStatePresentationReadModel BuildRuntimeStatePresentationReadModel() const; private: - friend class RenderSnapshotBuilder; - friend class RuntimeStatePresenter; bool LoadPersistentState(std::string& error); bool SavePersistentState(std::string& error) const; bool ScanShaderPackages(std::string& error); std::string ReadTextFile(const std::filesystem::path& path, std::string& error) const; bool WriteTextFile(const std::filesystem::path& path, const std::string& contents, std::string& error) const; std::vector GetStackPresetNamesLocked() const; - bool CopyShaderPackageForStoredLayer(const std::string& layerId, ShaderPackage& shaderPackage, std::string& error) const; - void GetShaderCompilerInputs(std::filesystem::path& repoRoot, std::filesystem::path& wrapperPath, - std::filesystem::path& generatedGlslPath, std::filesystem::path& patchedGlslPath, unsigned& maxTemporalHistoryFrames) const; void MarkRenderStateDirtyLocked(); void MarkParameterStateDirtyLocked(); diff --git a/apps/LoopThroughWithOpenGLCompositing/runtime/store/RuntimeStoreReadModels.h b/apps/LoopThroughWithOpenGLCompositing/runtime/store/RuntimeStoreReadModels.h new file mode 100644 index 0000000..26ba8d2 --- /dev/null +++ b/apps/LoopThroughWithOpenGLCompositing/runtime/store/RuntimeStoreReadModels.h @@ -0,0 +1,50 @@ +#pragma once + +#include "HealthTelemetry.h" +#include "LayerStackStore.h" +#include "RuntimeConfigStore.h" +#include "ShaderPackageCatalog.h" +#include "ShaderTypes.h" + +#include +#include +#include +#include +#include + +struct ShaderCompilerInputs +{ + std::filesystem::path repoRoot; + std::filesystem::path wrapperPath; + std::filesystem::path generatedGlslPath; + std::filesystem::path patchedGlslPath; + unsigned maxTemporalHistoryFrames = 0; +}; + +struct RenderTimingSnapshot +{ + std::chrono::steady_clock::time_point startTime; + double startupRandom = 0.0; +}; + +struct RenderSnapshotReadModel +{ + std::vector layers; + std::map packagesById; + HealthTelemetry::SignalStatusSnapshot signalStatus; + RenderTimingSnapshot timing; +}; + +struct RuntimeStatePresentationReadModel +{ + RuntimeConfigStore::AppConfig config; + HealthTelemetry::Snapshot telemetry; + LayerStackStore layerStack; + ShaderPackageCatalog::Snapshot shaderCatalog; + std::vector packageStatuses; + std::vector stackPresetNames; + unsigned short serverPort = 0; + bool autoReloadEnabled = false; + bool compileSucceeded = false; + std::string compileMessage; +}; diff --git a/apps/LoopThroughWithOpenGLCompositing/runtime/telemetry/HealthTelemetry.h b/apps/LoopThroughWithOpenGLCompositing/runtime/telemetry/HealthTelemetry.h index 325237a..b0319f6 100644 --- a/apps/LoopThroughWithOpenGLCompositing/runtime/telemetry/HealthTelemetry.h +++ b/apps/LoopThroughWithOpenGLCompositing/runtime/telemetry/HealthTelemetry.h @@ -5,8 +5,8 @@ #include // Phase 1 compatibility seam for status and timing reporting. HealthTelemetry -// now owns the current operational status snapshot directly, so callers can -// target it without flowing through RuntimeHost-owned backing fields. +// owns the current operational status snapshot directly, so callers can report +// health without sharing runtime-store state. class HealthTelemetry { public: diff --git a/docs/ARCHITECTURE_RESILIENCE_REVIEW.md b/docs/ARCHITECTURE_RESILIENCE_REVIEW.md index 7c7928a..7f25011 100644 --- a/docs/ARCHITECTURE_RESILIENCE_REVIEW.md +++ b/docs/ARCHITECTURE_RESILIENCE_REVIEW.md @@ -6,7 +6,7 @@ Phase checklist: - [x] Define subsystem boundaries and target architecture - [ ] Introduce an internal event model -- [ ] Split `RuntimeHost` +- [x] Split `RuntimeHost` - [ ] Make the render thread the sole GL owner - [ ] Refactor live state layering into an explicit composition model - [ ] Move persistence onto a background snapshot writer @@ -32,9 +32,9 @@ Those points are important because they affect not just average performance, but ## Key Findings -### 1. `RuntimeHost` is carrying too many responsibilities +### 1. The original runtime host carried too many responsibilities -`RuntimeHost` currently acts as: +The original `RuntimeHost` acted as: - config store - persistent state store @@ -47,7 +47,7 @@ That makes it a single contention and failure domain. It is also why OSC and ren Relevant code: -- [RuntimeHost.h](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/runtime/legacy/RuntimeHost.h:15) +- `RuntimeHost.h` Recommended direction: @@ -258,7 +258,7 @@ Recommended direction: Relevant code: -- [RuntimeHost.cpp](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/runtime/legacy/RuntimeHost.cpp:1841) +- `RuntimeHost.cpp` Recent OSC work already reduced this problem for live automation, but the broader architecture would still benefit from: @@ -365,7 +365,7 @@ Status: - Design deliverable: complete. - Compatibility seams in code: partially complete and expanding. -- Target boundary extraction: not complete; remaining work is tracked by later phases, especially the event model, `RuntimeHost` split, render ownership, and persistence work. +- Target boundary extraction: not complete across the whole app; remaining work is tracked by later phases, especially the event model, render ownership, and persistence work. Target split: @@ -453,9 +453,9 @@ Suggested outcome: - the app stops relying on “shared object plus mutex plus polling” as the default coordination pattern -### Phase 3. Split `RuntimeHost` into persistent state, render snapshot state, and service-facing coordination +### Phase 3. Finish live-state and service-facing coordination -After the event model exists, break apart `RuntimeHost`. +After the event model exists, finish separating live committed state and service-facing coordination from the runtime facades. Recommended split: @@ -628,7 +628,7 @@ If this is approached as a serious architecture program rather than opportunisti 1. Define subsystem boundaries and target architecture. 2. Introduce the internal event model. -3. Split `RuntimeHost`. +3. Finish runtime live-state/service coordination. 4. Make the render thread the sole GL owner. 5. Formalize live state layering and composition. 6. Move persistence to a background snapshot writer. @@ -647,7 +647,7 @@ This order tries to avoid doing foundational work twice. ## Short Version -The app is in a much better place than it was before the OSC timing work, but the main remaining architectural risk is still shared ownership. Too many responsibilities converge on `RuntimeHost` and the shared GL path. The most sensible path forward is: +The app is in a much better place than it was before the OSC timing work, but the main remaining architectural risk is still shared ownership around the shared GL path. The most sensible path forward is: 1. define boundaries 2. establish an event model diff --git a/docs/PHASE_1_SUBSYSTEM_BOUNDARIES_DESIGN.md b/docs/PHASE_1_SUBSYSTEM_BOUNDARIES_DESIGN.md index b2422e6..f5d6ea4 100644 --- a/docs/PHASE_1_SUBSYSTEM_BOUNDARIES_DESIGN.md +++ b/docs/PHASE_1_SUBSYSTEM_BOUNDARIES_DESIGN.md @@ -1,6 +1,6 @@ # Phase 1 Design: Subsystem Boundaries and Target Architecture -This document expands Phase 1 of [ARCHITECTURE_RESILIENCE_REVIEW.md](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/docs/ARCHITECTURE_RESILIENCE_REVIEW.md) into a concrete target design. Its purpose is to define the long-term subsystem split before later phases introduce a full event model, split `RuntimeHost`, and move rendering onto a sole-owner render thread. +This document expands Phase 1 of [ARCHITECTURE_RESILIENCE_REVIEW.md](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/docs/ARCHITECTURE_RESILIENCE_REVIEW.md) into a concrete target design. Its purpose is to define the long-term subsystem split before later phases introduce a full event model and move rendering onto a sole-owner render thread. The main goal of Phase 1 is not to immediately rewrite the app. It is to establish clear ownership boundaries so later refactors all move toward the same architecture instead of solving local problems in conflicting ways. @@ -9,15 +9,15 @@ The main goal of Phase 1 is not to immediately rewrite the app. It is to establi Phase 1 has two different meanings in this repo, and they should not be collapsed: - Phase 1 design package: complete. -- Phase 1 target extraction in code: in progress. +- Phase 1 runtime target extraction in code: largely complete. -The completed design package includes the agreed subsystem names, responsibilities, dependency rules, state categories, and current-to-target migration map. The codebase also has concrete compatibility seams for those subsystems. That is different from saying every target boundary is fully extracted: several subsystems still delegate through compatibility helpers, and later roadmap phases are still responsible for the event model, remaining `RuntimeHost` split, sole-owner render thread, explicit live-state layering, background persistence, backend state machine, and fuller telemetry. +The completed design package includes the agreed subsystem names, responsibilities, dependency rules, state categories, and current-to-target migration map. The runtime code now has concrete subsystem folders and collaborators for those boundaries. That is different from saying every target boundary is fully extracted across the whole app: later roadmap phases are still responsible for the event model, sole-owner render thread, explicit live-state layering, background persistence, backend state machine, and fuller telemetry. ## Why Phase 1 Exists -Today the app works, but too many responsibilities still converge in a few places: +At the start of this phase the app worked, but too many responsibilities converged in a few places: -- `RuntimeHost` owns persistence, live layer state, shader package access, status reporting, and mutation entrypoints. +- `RuntimeHost` owned persistence, live layer state, shader package access, status reporting, and mutation entrypoints. - `OpenGLComposite` coordinates runtime setup, render state retrieval, shader rebuild handling, transient OSC overlay behavior, and video backend integration. - DeckLink callback-driven playout still reaches directly into render-facing work. - Background services rely on polling and shared mutable state more than explicit subsystem contracts. @@ -54,9 +54,9 @@ This phase is the target design and the dependency rules. Later phases perform t The following current code paths are the strongest evidence for the split proposed here: -- `RuntimeHost` is both store and live authority: - - [RuntimeHost.h](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/runtime/legacy/RuntimeHost.h:15) - - [RuntimeHost.cpp](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/runtime/legacy/RuntimeHost.cpp:726) +- `RuntimeHost` was both store and live authority: + - `RuntimeHost.h` + - `RuntimeHost.cpp` - `OpenGLComposite` is both app orchestrator and render/runtime coordinator: - [OpenGLComposite.cpp](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/gl/OpenGLComposite.cpp:106) - [OpenGLComposite.cpp](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/gl/OpenGLComposite.cpp:283) @@ -575,7 +575,7 @@ Core responsibilities: This is not a one-to-one rename plan. It is a responsibility migration map. -### Current `RuntimeHost` +### Previous `RuntimeHost` Should eventually split across: @@ -635,7 +635,7 @@ Likely examples: As later phases begin, these rules should be treated as guardrails. -### 1. No new cross-cutting state should be added to `RuntimeHost` +### 1. No new cross-cutting runtime object should be introduced If a new feature needs durable state, place it conceptually under `RuntimeStore`. If it needs render-local transient state, place it conceptually under `RenderEngine`. @@ -664,7 +664,7 @@ Phase 1 is a design phase, but it should support incremental migration. Recommended order after this document: 1. Introduce names and interfaces before moving logic. -2. Create compatibility adapters around `RuntimeHost` rather than forcing a flag day. +2. Create compatibility adapters around the subsystem facades rather than forcing a flag day. 3. Move read-only render snapshot publication out before moving all mutation logic. 4. Move service ingress boundaries out before removing the old polling shell. 5. Isolate timing/health setters from the core store as early as practical. @@ -679,7 +679,7 @@ Phase 1 can reasonably be considered complete once the project has: - agreed subsystem names and responsibilities - agreed allowed dependency directions - explicit state categories: persisted, committed live, transient overlay, health/timing -- a current-to-target responsibility map for `RuntimeHost`, `RuntimeServices`, `OpenGLComposite`, and backend/render bridge code +- a current-to-target responsibility map for runtime services, `OpenGLComposite`, and backend/render bridge code - a decision that later phases will build against this target rather than inventing new boundaries ad hoc By that definition, the Phase 1 design deliverable is complete. The implementation should still be described as partially extracted until the compatibility backing objects and cross-subsystem shims are removed. diff --git a/docs/subsystems/HealthTelemetry.md b/docs/subsystems/HealthTelemetry.md index 8a323da..310ef8d 100644 --- a/docs/subsystems/HealthTelemetry.md +++ b/docs/subsystems/HealthTelemetry.md @@ -19,10 +19,10 @@ Today, those responsibilities are fragmented across `RuntimeHost` status setters The current code already contains meaningful health and timing signals, but they are spread through unrelated ownership domains: - `RuntimeHost` stores signal and timing status: - - [RuntimeHost.h](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/runtime/legacy/RuntimeHost.h:41) - - [RuntimeHost.cpp](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/runtime/legacy/RuntimeHost.cpp:1353) - - [RuntimeHost.cpp](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/runtime/legacy/RuntimeHost.cpp:1415) - - [RuntimeHost.cpp](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/runtime/legacy/RuntimeHost.cpp:1441) + - `RuntimeHost.h` + - `RuntimeHost.cpp` + - `RuntimeHost.cpp` + - `RuntimeHost.cpp` - render and bridge code report timing by writing back into `RuntimeHost`: - [OpenGLRenderPipeline.cpp](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/gl/pipeline/OpenGLRenderPipeline.cpp:50) - [OpenGLVideoIOBridge.cpp](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/gl/pipeline/OpenGLVideoIOBridge.cpp:49) @@ -386,10 +386,10 @@ These are the clearest existing candidates: See: -- [RuntimeHost.h](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/runtime/legacy/RuntimeHost.h:41) -- [RuntimeHost.cpp](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/runtime/legacy/RuntimeHost.cpp:1353) -- [RuntimeHost.cpp](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/runtime/legacy/RuntimeHost.cpp:1415) -- [RuntimeHost.cpp](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/runtime/legacy/RuntimeHost.cpp:1441) +- `RuntimeHost.h` +- `RuntimeHost.cpp` +- `RuntimeHost.cpp` +- `RuntimeHost.cpp` In the target architecture, this kind of state should no longer sit on the same object that owns persistent layer truth. diff --git a/docs/subsystems/RuntimeCoordinator.md b/docs/subsystems/RuntimeCoordinator.md index 35ef773..243a8e4 100644 --- a/docs/subsystems/RuntimeCoordinator.md +++ b/docs/subsystems/RuntimeCoordinator.md @@ -9,8 +9,8 @@ This document defines the target design for the `RuntimeCoordinator` subsystem i Today the app's mutation path is split across several places: - `RuntimeHost` performs validation, mutation, persistence, render-state invalidation, and some status updates: - - [RuntimeHost.h](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/runtime/legacy/RuntimeHost.h:15) - - [RuntimeHost.cpp](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/runtime/legacy/RuntimeHost.cpp:891) + - `RuntimeHost.h` + - `RuntimeHost.cpp` - `OpenGLComposite` currently acts like an orchestration shell and a mutation coordinator at the same time: - [OpenGLCompositeRuntimeControls.cpp](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/gl/OpenGLCompositeRuntimeControls.cpp:1) - `RuntimeServices` still owns some deferred control flow around OSC commit and polling: @@ -67,7 +67,7 @@ This is the main policy surface that is currently spread between `RuntimeHost` m - `ApplyOscTargetByControlKey(...)` - `ResetLayerParameters(...)` -See [RuntimeHost.h](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/runtime/legacy/RuntimeHost.h:15). +See `RuntimeHost.h`. ### 3. State classification @@ -375,7 +375,7 @@ That "call host, then decide reload/broadcast policy" logic is a direct candidat - persistence writes - render-state dirty marking -Examples in [RuntimeHost.cpp](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/runtime/legacy/RuntimeHost.cpp:891): +Examples in `RuntimeHost.cpp`: - `AddLayer(...)` - `SetLayerShader(...)` diff --git a/docs/subsystems/RuntimeSnapshotProvider.md b/docs/subsystems/RuntimeSnapshotProvider.md index 108272f..d48ec63 100644 --- a/docs/subsystems/RuntimeSnapshotProvider.md +++ b/docs/subsystems/RuntimeSnapshotProvider.md @@ -16,11 +16,11 @@ It exists to solve three current problems: Today the closest current behavior lives in: -- [RuntimeHost::GetLayerRenderStates(...)](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/runtime/legacy/RuntimeHost.cpp:1535) -- [RuntimeHost::TryGetLayerRenderStates(...)](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/runtime/legacy/RuntimeHost.cpp:1543) -- [RuntimeHost::TryRefreshCachedLayerStates(...)](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/runtime/legacy/RuntimeHost.cpp:1554) -- [RuntimeHost::RefreshDynamicRenderStateFields(...)](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/runtime/legacy/RuntimeHost.cpp:1582) -- [RuntimeHost::BuildLayerRenderStatesLocked(...)](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/runtime/legacy/RuntimeHost.cpp:1598) +- `RuntimeHost::GetLayerRenderStates(...)` +- `RuntimeHost::TryGetLayerRenderStates(...)` +- `RuntimeHost::TryRefreshCachedLayerStates(...)` +- `RuntimeHost::RefreshDynamicRenderStateFields(...)` +- `RuntimeHost::BuildLayerRenderStatesLocked(...)` - the render-side cache usage in [OpenGLComposite.cpp](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/gl/OpenGLComposite.cpp:589) `RuntimeSnapshotProvider` should absorb that responsibility, but in a cleaner and more publish-oriented way. diff --git a/docs/subsystems/RuntimeStore.md b/docs/subsystems/RuntimeStore.md index 633b4b5..048c19d 100644 --- a/docs/subsystems/RuntimeStore.md +++ b/docs/subsystems/RuntimeStore.md @@ -301,22 +301,22 @@ Today, `RuntimeHost` contains most of the responsibilities that should move into Key current code paths: - config load: - - [RuntimeHost.cpp](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/runtime/legacy/RuntimeHost.cpp:1651) + - `RuntimeHost.cpp` - persistent state load: - - [RuntimeHost.cpp](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/runtime/legacy/RuntimeHost.cpp:1748) + - `RuntimeHost.cpp` - persistent state save: - - [RuntimeHost.cpp](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/runtime/legacy/RuntimeHost.cpp:1842) + - `RuntimeHost.cpp` - preset save/load: - - [RuntimeHost.cpp](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/runtime/legacy/RuntimeHost.cpp:1286) - - [RuntimeHost.cpp](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/runtime/legacy/RuntimeHost.cpp:1304) + - `RuntimeHost.cpp` + - `RuntimeHost.cpp` - state serialization helpers: - - [RuntimeHost.cpp](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/runtime/legacy/RuntimeHost.cpp:2061) - - [RuntimeHost.cpp](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/runtime/legacy/RuntimeHost.cpp:2172) - - [RuntimeHost.cpp](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/runtime/legacy/RuntimeHost.cpp:2268) + - `RuntimeHost.cpp` + - `RuntimeHost.cpp` + - `RuntimeHost.cpp` - path and file helpers: - - [RuntimeHost.cpp](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/runtime/legacy/RuntimeHost.cpp:1988) - - [RuntimeHost.cpp](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/runtime/legacy/RuntimeHost.cpp:2002) - - [RuntimeHost.cpp](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/runtime/legacy/RuntimeHost.cpp:2034) + - `RuntimeHost.cpp` + - `RuntimeHost.cpp` + - `RuntimeHost.cpp` Durable-state mutation entrypoints that currently live on `RuntimeHost` but conceptually split between coordinator and store: diff --git a/docs/subsystems/VideoBackend.md b/docs/subsystems/VideoBackend.md index c95a417..3346d2f 100644 --- a/docs/subsystems/VideoBackend.md +++ b/docs/subsystems/VideoBackend.md @@ -570,7 +570,7 @@ The most important migration is: - remove render work from `PlayoutFrameCompleted()` -### Current `RuntimeHost` Status Updates +### Previous Runtime Status Updates Frame pacing and signal status setters currently called from the bridge should ultimately be routed through: