From 5cbdbd6813f9654a016de312de55b3aad2b6bbc8 Mon Sep 17 00:00:00 2001 From: Aiden <68633820+awils27@users.noreply.github.com> Date: Mon, 11 May 2026 02:06:17 +1000 Subject: [PATCH] Pass 3 --- CMakeLists.txt | 12 + .../gl/OpenGLComposite.cpp | 2 +- .../runtime/LayerStackStore.cpp | 738 +++++++++ .../runtime/LayerStackStore.h | 72 + .../runtime/RenderSnapshotBuilder.cpp | 208 +++ .../runtime/RenderSnapshotBuilder.h | 43 + .../runtime/RuntimeConfigStore.cpp | 270 ++++ .../runtime/RuntimeConfigStore.h | 59 + .../runtime/RuntimeSnapshotProvider.cpp | 55 +- .../runtime/RuntimeSnapshotProvider.h | 13 +- .../runtime/RuntimeStateJson.cpp | 157 ++ .../runtime/RuntimeStateJson.h | 16 + .../runtime/RuntimeStatePresenter.cpp | 125 ++ .../runtime/RuntimeStatePresenter.h | 14 + .../runtime/RuntimeStore.cpp | 1418 ++--------------- .../runtime/RuntimeStore.h | 106 +- .../runtime/ShaderPackageCatalog.cpp | 127 ++ .../runtime/ShaderPackageCatalog.h | 37 + docs/PHASE_1_SUBSYSTEM_BOUNDARIES_DESIGN.md | 29 +- docs/subsystems/README.md | 4 +- docs/subsystems/RuntimeSnapshotProvider.md | 39 +- docs/subsystems/RuntimeStore.md | 29 +- 22 files changed, 2069 insertions(+), 1504 deletions(-) create mode 100644 apps/LoopThroughWithOpenGLCompositing/runtime/LayerStackStore.cpp create mode 100644 apps/LoopThroughWithOpenGLCompositing/runtime/LayerStackStore.h create mode 100644 apps/LoopThroughWithOpenGLCompositing/runtime/RenderSnapshotBuilder.cpp create mode 100644 apps/LoopThroughWithOpenGLCompositing/runtime/RenderSnapshotBuilder.h create mode 100644 apps/LoopThroughWithOpenGLCompositing/runtime/RuntimeConfigStore.cpp create mode 100644 apps/LoopThroughWithOpenGLCompositing/runtime/RuntimeConfigStore.h create mode 100644 apps/LoopThroughWithOpenGLCompositing/runtime/RuntimeStateJson.cpp create mode 100644 apps/LoopThroughWithOpenGLCompositing/runtime/RuntimeStateJson.h create mode 100644 apps/LoopThroughWithOpenGLCompositing/runtime/RuntimeStatePresenter.cpp create mode 100644 apps/LoopThroughWithOpenGLCompositing/runtime/RuntimeStatePresenter.h create mode 100644 apps/LoopThroughWithOpenGLCompositing/runtime/ShaderPackageCatalog.cpp create mode 100644 apps/LoopThroughWithOpenGLCompositing/runtime/ShaderPackageCatalog.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 12ebbb7..8384697 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -104,10 +104,20 @@ set(APP_SOURCES "${APP_DIR}/resource.h" "${APP_DIR}/runtime/HealthTelemetry.cpp" "${APP_DIR}/runtime/HealthTelemetry.h" + "${APP_DIR}/runtime/LayerStackStore.cpp" + "${APP_DIR}/runtime/LayerStackStore.h" + "${APP_DIR}/runtime/RenderSnapshotBuilder.cpp" + "${APP_DIR}/runtime/RenderSnapshotBuilder.h" + "${APP_DIR}/runtime/RuntimeConfigStore.cpp" + "${APP_DIR}/runtime/RuntimeConfigStore.h" "${APP_DIR}/runtime/RuntimeCoordinator.cpp" "${APP_DIR}/runtime/RuntimeCoordinator.h" "${APP_DIR}/runtime/RuntimeSnapshotProvider.cpp" "${APP_DIR}/runtime/RuntimeSnapshotProvider.h" + "${APP_DIR}/runtime/RuntimeStateJson.cpp" + "${APP_DIR}/runtime/RuntimeStateJson.h" + "${APP_DIR}/runtime/RuntimeStatePresenter.cpp" + "${APP_DIR}/runtime/RuntimeStatePresenter.h" "${APP_DIR}/runtime/RuntimeClock.cpp" "${APP_DIR}/runtime/RuntimeClock.h" "${APP_DIR}/runtime/RuntimeJson.cpp" @@ -116,6 +126,8 @@ set(APP_SOURCES "${APP_DIR}/runtime/RuntimeParameterUtils.h" "${APP_DIR}/runtime/RuntimeStore.cpp" "${APP_DIR}/runtime/RuntimeStore.h" + "${APP_DIR}/runtime/ShaderPackageCatalog.cpp" + "${APP_DIR}/runtime/ShaderPackageCatalog.h" "${APP_DIR}/shader/ShaderCompiler.cpp" "${APP_DIR}/shader/ShaderCompiler.h" "${APP_DIR}/shader/ShaderPackageRegistry.cpp" diff --git a/apps/LoopThroughWithOpenGLCompositing/gl/OpenGLComposite.cpp b/apps/LoopThroughWithOpenGLCompositing/gl/OpenGLComposite.cpp index a9ce8da..38dd8bd 100644 --- a/apps/LoopThroughWithOpenGLCompositing/gl/OpenGLComposite.cpp +++ b/apps/LoopThroughWithOpenGLCompositing/gl/OpenGLComposite.cpp @@ -36,7 +36,7 @@ OpenGLComposite::OpenGLComposite(HWND hWnd, HDC hDC, HGLRC hRC) : { InitializeCriticalSection(&pMutex); mRuntimeStore = std::make_unique(); - mRuntimeSnapshotProvider = std::make_unique(*mRuntimeStore); + mRuntimeSnapshotProvider = std::make_unique(mRuntimeStore->GetRenderSnapshotBuilder()); mRuntimeCoordinator = std::make_unique(*mRuntimeStore); mRenderEngine = std::make_unique( *mRuntimeSnapshotProvider, diff --git a/apps/LoopThroughWithOpenGLCompositing/runtime/LayerStackStore.cpp b/apps/LoopThroughWithOpenGLCompositing/runtime/LayerStackStore.cpp new file mode 100644 index 0000000..491fcb6 --- /dev/null +++ b/apps/LoopThroughWithOpenGLCompositing/runtime/LayerStackStore.cpp @@ -0,0 +1,738 @@ +#include "LayerStackStore.h" + +#include "RuntimeParameterUtils.h" +#include "RuntimeStateJson.h" + +#include +#include +#include +#include +#include +#include + +namespace +{ +std::string TrimCopy(const std::string& text) +{ + std::size_t start = 0; + while (start < text.size() && std::isspace(static_cast(text[start]))) + ++start; + + std::size_t end = text.size(); + while (end > start && std::isspace(static_cast(text[end - 1]))) + --end; + + return text.substr(start, end - start); +} + +std::string SimplifyControlKey(const std::string& text) +{ + std::string simplified; + for (unsigned char ch : text) + { + if (std::isalnum(ch)) + simplified.push_back(static_cast(std::tolower(ch))); + } + return simplified; +} + +bool MatchesControlKey(const std::string& candidate, const std::string& key) +{ + return candidate == key || SimplifyControlKey(candidate) == SimplifyControlKey(key); +} + +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 LayerStackStore::LoadPersistentStateValue(const JsonValue& root) +{ + 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()) + mLayers.push_back(layer); + } + } + else + { + std::string activeShaderId; + if (const JsonValue* activeShaderValue = root.find("activeShaderId")) + activeShaderId = activeShaderValue->asString(); + + if (!activeShaderId.empty()) + { + LayerPersistentState layer; + layer.id = GenerateLayerId(mLayers, mNextLayerId); + 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; + } + } + } + + mLayers.push_back(layer); + } + } + + return true; +} + +JsonValue LayerStackStore::BuildPersistentStateValue(const ShaderPackageCatalog& shaderCatalog) const +{ + JsonValue root = JsonValue::MakeObject(); + JsonValue layers = JsonValue::MakeArray(); + for (const LayerPersistentState& layer : mLayers) + { + 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(); + const ShaderPackage* shaderPackage = shaderCatalog.FindPackage(layer.shaderId); + for (const auto& parameterItem : layer.parameterValues) + { + const ShaderParameterDefinition* definition = nullptr; + if (shaderPackage) + { + for (const ShaderParameterDefinition& candidate : shaderPackage->parameters) + { + if (candidate.id == parameterItem.first) + { + definition = &candidate; + break; + } + } + } + + if (definition) + parameterValues.set(parameterItem.first, RuntimeStateJson::SerializeParameterValue(*definition, parameterItem.second)); + } + + layerValue.set("parameterValues", parameterValues); + layers.pushBack(layerValue); + } + root.set("layers", layers); + return root; +} + +void LayerStackStore::NormalizeLayerIds() +{ + std::set usedIds; + uint64_t maxLayerNumber = mNextLayerId; + + for (LayerPersistentState& layer : mLayers) + { + 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; +} + +void LayerStackStore::EnsureDefaultsForAllLayers(const ShaderPackageCatalog& shaderCatalog) +{ + for (LayerPersistentState& layer : mLayers) + { + const ShaderPackage* shaderPackage = shaderCatalog.FindPackage(layer.shaderId); + if (shaderPackage) + EnsureLayerDefaults(layer, *shaderPackage); + } +} + +void LayerStackStore::EnsureDefaultLayer(const ShaderPackageCatalog& shaderCatalog) +{ + if (!mLayers.empty() || shaderCatalog.PackageOrder().empty()) + return; + + LayerPersistentState layer; + layer.id = GenerateLayerId(mLayers, mNextLayerId); + layer.shaderId = shaderCatalog.PackageOrder().front(); + layer.bypass = false; + if (const ShaderPackage* shaderPackage = shaderCatalog.FindPackage(layer.shaderId)) + EnsureLayerDefaults(layer, *shaderPackage); + mLayers.push_back(layer); +} + +void LayerStackStore::RemoveLayersWithMissingPackages(const ShaderPackageCatalog& shaderCatalog) +{ + for (auto it = mLayers.begin(); it != mLayers.end();) + { + if (!shaderCatalog.HasPackage(it->shaderId)) + it = mLayers.erase(it); + else + ++it; + } +} + +bool LayerStackStore::CreateLayer(const ShaderPackageCatalog& shaderCatalog, const std::string& shaderId, std::string& error) +{ + const ShaderPackage* shaderPackage = shaderCatalog.FindPackage(shaderId); + if (!shaderPackage) + { + error = "Unknown shader id: " + shaderId; + return false; + } + + LayerPersistentState layer; + layer.id = GenerateLayerId(mLayers, mNextLayerId); + layer.shaderId = shaderId; + layer.bypass = false; + EnsureLayerDefaults(layer, *shaderPackage); + mLayers.push_back(layer); + return true; +} + +bool LayerStackStore::DeleteLayer(const std::string& layerId, std::string& error) +{ + auto it = std::find_if(mLayers.begin(), mLayers.end(), + [&layerId](const LayerPersistentState& layer) { return layer.id == layerId; }); + if (it == mLayers.end()) + { + error = "Unknown layer id: " + layerId; + return false; + } + + mLayers.erase(it); + return true; +} + +bool LayerStackStore::MoveLayer(const std::string& layerId, int direction, std::string& error) +{ + auto it = std::find_if(mLayers.begin(), mLayers.end(), + [&layerId](const LayerPersistentState& layer) { return layer.id == layerId; }); + if (it == mLayers.end()) + { + error = "Unknown layer id: " + layerId; + return false; + } + + const std::ptrdiff_t index = std::distance(mLayers.begin(), it); + const std::ptrdiff_t newIndex = index + direction; + if (newIndex < 0 || newIndex >= static_cast(mLayers.size())) + return true; + + std::swap(mLayers[index], mLayers[newIndex]); + return true; +} + +bool LayerStackStore::MoveLayerToIndex(const std::string& layerId, std::size_t targetIndex, std::string& error) +{ + auto it = std::find_if(mLayers.begin(), mLayers.end(), + [&layerId](const LayerPersistentState& layer) { return layer.id == layerId; }); + if (it == mLayers.end()) + { + error = "Unknown layer id: " + layerId; + return false; + } + + if (mLayers.empty()) + return true; + + if (targetIndex >= mLayers.size()) + targetIndex = mLayers.size() - 1; + + const std::size_t sourceIndex = static_cast(std::distance(mLayers.begin(), it)); + if (sourceIndex == targetIndex) + return true; + + LayerPersistentState movedLayer = *it; + mLayers.erase(mLayers.begin() + static_cast(sourceIndex)); + mLayers.insert(mLayers.begin() + static_cast(targetIndex), movedLayer); + return true; +} + +bool LayerStackStore::SetLayerBypassState(const std::string& layerId, bool bypassed, std::string& error) +{ + LayerPersistentState* layer = FindLayerById(layerId); + if (!layer) + { + error = "Unknown layer id: " + layerId; + return false; + } + + layer->bypass = bypassed; + return true; +} + +bool LayerStackStore::SetLayerShaderSelection(const ShaderPackageCatalog& shaderCatalog, const std::string& layerId, const std::string& shaderId, std::string& error) +{ + LayerPersistentState* layer = FindLayerById(layerId); + if (!layer) + { + error = "Unknown layer id: " + layerId; + return false; + } + + const ShaderPackage* shaderPackage = shaderCatalog.FindPackage(shaderId); + if (!shaderPackage) + { + error = "Unknown shader id: " + shaderId; + return false; + } + + layer->shaderId = shaderId; + layer->parameterValues.clear(); + EnsureLayerDefaults(*layer, *shaderPackage); + return true; +} + +bool LayerStackStore::SetParameterValue(const std::string& layerId, const std::string& parameterId, const ShaderParameterValue& value, std::string& error) +{ + LayerPersistentState* layer = FindLayerById(layerId); + if (!layer) + { + error = "Unknown layer id: " + layerId; + return false; + } + + layer->parameterValues[parameterId] = value; + return true; +} + +bool LayerStackStore::ResetLayerParameterValues(const ShaderPackageCatalog& shaderCatalog, const std::string& layerId, std::string& error) +{ + LayerPersistentState* layer = FindLayerById(layerId); + if (!layer) + { + error = "Unknown layer id: " + layerId; + return false; + } + + const ShaderPackage* shaderPackage = shaderCatalog.FindPackage(layer->shaderId); + if (!shaderPackage) + { + error = "Unknown shader id: " + layer->shaderId; + return false; + } + + layer->parameterValues.clear(); + EnsureLayerDefaults(*layer, *shaderPackage); + return true; +} + +bool LayerStackStore::HasLayer(const std::string& layerId) const +{ + return FindLayerById(layerId) != nullptr; +} + +bool LayerStackStore::TryGetParameterById(const ShaderPackageCatalog& shaderCatalog, const std::string& layerId, const std::string& parameterId, StoredParameterSnapshot& snapshot, std::string& error) const +{ + const LayerPersistentState* layer = FindLayerById(layerId); + if (!layer) + { + error = "Unknown layer id: " + layerId; + return false; + } + + const ShaderPackage* shaderPackage = shaderCatalog.FindPackage(layer->shaderId); + if (!shaderPackage) + { + error = "Unknown shader id: " + layer->shaderId; + return false; + } + + auto parameterIt = std::find_if(shaderPackage->parameters.begin(), shaderPackage->parameters.end(), + [¶meterId](const ShaderParameterDefinition& definition) { return definition.id == parameterId; }); + if (parameterIt == shaderPackage->parameters.end()) + { + error = "Unknown parameter id: " + parameterId; + return false; + } + + snapshot = StoredParameterSnapshot(); + snapshot.layerId = layer->id; + snapshot.definition = *parameterIt; + auto valueIt = layer->parameterValues.find(parameterIt->id); + if (valueIt != layer->parameterValues.end()) + { + snapshot.currentValue = valueIt->second; + snapshot.hasCurrentValue = true; + } + return true; +} + +bool LayerStackStore::TryGetParameterByControlKey(const ShaderPackageCatalog& shaderCatalog, const std::string& layerKey, const std::string& parameterKey, StoredParameterSnapshot& snapshot, std::string& error) const +{ + const LayerPersistentState* matchedLayer = nullptr; + const ShaderPackage* matchedPackage = nullptr; + + for (const LayerPersistentState& layer : mLayers) + { + const ShaderPackage* shaderPackage = shaderCatalog.FindPackage(layer.shaderId); + if (!shaderPackage) + continue; + + if (MatchesControlKey(layer.id, layerKey) || MatchesControlKey(shaderPackage->id, layerKey) || + MatchesControlKey(shaderPackage->displayName, layerKey)) + { + matchedLayer = &layer; + matchedPackage = shaderPackage; + break; + } + } + + if (!matchedLayer || !matchedPackage) + { + error = "Unknown OSC layer key: " + layerKey; + return false; + } + + auto parameterIt = std::find_if(matchedPackage->parameters.begin(), matchedPackage->parameters.end(), + [¶meterKey](const ShaderParameterDefinition& definition) + { + return MatchesControlKey(definition.id, parameterKey) || MatchesControlKey(definition.label, parameterKey); + }); + if (parameterIt == matchedPackage->parameters.end()) + { + error = "Unknown OSC parameter key: " + parameterKey; + return false; + } + + snapshot = StoredParameterSnapshot(); + snapshot.layerId = matchedLayer->id; + snapshot.definition = *parameterIt; + auto valueIt = matchedLayer->parameterValues.find(parameterIt->id); + if (valueIt != matchedLayer->parameterValues.end()) + { + snapshot.currentValue = valueIt->second; + snapshot.hasCurrentValue = true; + } + return true; +} + +bool LayerStackStore::ResolveLayerMove(const std::string& layerId, int direction, bool& shouldMove, std::string& error) const +{ + auto it = std::find_if(mLayers.begin(), mLayers.end(), + [&layerId](const LayerPersistentState& layer) { return layer.id == layerId; }); + if (it == mLayers.end()) + { + error = "Unknown layer id: " + layerId; + return false; + } + + const std::ptrdiff_t index = std::distance(mLayers.begin(), it); + const std::ptrdiff_t newIndex = index + direction; + shouldMove = newIndex >= 0 && newIndex < static_cast(mLayers.size()) && newIndex != index; + return true; +} + +bool LayerStackStore::ResolveLayerMoveToIndex(const std::string& layerId, std::size_t targetIndex, bool& shouldMove, std::string& error) const +{ + auto it = std::find_if(mLayers.begin(), mLayers.end(), + [&layerId](const LayerPersistentState& layer) { return layer.id == layerId; }); + if (it == mLayers.end()) + { + error = "Unknown layer id: " + layerId; + return false; + } + + if (mLayers.empty()) + { + shouldMove = false; + return true; + } + + const std::size_t clampedTargetIndex = (std::min)(targetIndex, mLayers.size() - 1); + const std::size_t sourceIndex = static_cast(std::distance(mLayers.begin(), it)); + shouldMove = sourceIndex != clampedTargetIndex; + return true; +} + +JsonValue LayerStackStore::BuildStackPresetValue(const ShaderPackageCatalog& shaderCatalog, const std::string& presetName) const +{ + JsonValue root = JsonValue::MakeObject(); + root.set("version", JsonValue(1.0)); + root.set("name", JsonValue(TrimCopy(presetName))); + root.set("layers", RuntimeStateJson::SerializeLayerStack(*this, shaderCatalog)); + return root; +} + +bool LayerStackStore::LoadStackPresetValue(const ShaderPackageCatalog& shaderCatalog, const JsonValue& root, std::string& error) +{ + const JsonValue* layersValue = root.find("layers"); + if (!layersValue || !layersValue->isArray()) + { + error = "Preset file is missing a valid 'layers' array."; + return false; + } + + std::vector nextLayers; + uint64_t nextLayerId = mNextLayerId; + if (!DeserializeLayerStack(shaderCatalog, *layersValue, nextLayers, nextLayerId, error)) + return false; + + if (nextLayers.empty()) + { + error = "Preset does not contain any valid layers."; + return false; + } + + mLayers = std::move(nextLayers); + mNextLayerId = nextLayerId; + return true; +} + +std::string LayerStackStore::MakeSafePresetFileStem(const std::string& presetName) +{ + return ::MakeSafePresetFileStem(presetName); +} + +const std::vector& LayerStackStore::Layers() const +{ + return mLayers; +} + +std::vector& LayerStackStore::Layers() +{ + return mLayers; +} + +std::size_t LayerStackStore::LayerCount() const +{ + return mLayers.size(); +} + +const LayerStackStore::LayerPersistentState* LayerStackStore::FindLayerById(const std::string& layerId) const +{ + auto it = std::find_if(mLayers.begin(), mLayers.end(), + [&layerId](const LayerPersistentState& layer) { return layer.id == layerId; }); + return it == mLayers.end() ? nullptr : &*it; +} + +LayerStackStore::LayerPersistentState* LayerStackStore::FindLayerById(const std::string& layerId) +{ + auto it = std::find_if(mLayers.begin(), mLayers.end(), + [&layerId](const LayerPersistentState& layer) { return layer.id == layerId; }); + return it == mLayers.end() ? nullptr : &*it; +} + +ShaderParameterValue LayerStackStore::DefaultValueForDefinition(const ShaderParameterDefinition& definition) +{ + return ::DefaultValueForDefinition(definition); +} + +void LayerStackStore::EnsureLayerDefaults(LayerPersistentState& layerState, const ShaderPackage& shaderPackage) +{ + 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 (NormalizeAndValidateParameterValue(definition, valueJson, normalizedValue, normalizeError)) + valueIt->second = normalizedValue; + else + valueIt->second = DefaultValueForDefinition(definition); + } +} + +bool LayerStackStore::DeserializeLayerStack(const ShaderPackageCatalog& shaderCatalog, const JsonValue& layersValue, std::vector& layers, uint64_t& nextLayerId, 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(); + const ShaderPackage* shaderPackage = shaderCatalog.FindPackage(shaderId); + if (!shaderPackage) + { + error = "Preset references unknown shader id: " + shaderId; + return false; + } + + LayerPersistentState layer; + layer.id = GenerateLayerId(layers, nextLayerId); + 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(shaderPackage->parameters.begin(), shaderPackage->parameters.end(), + [¶meterId](const ShaderParameterDefinition& definition) { return definition.id == parameterId; }); + if (definitionIt == shaderPackage->parameters.end()) + continue; + + ShaderParameterValue normalizedValue; + if (!NormalizeAndValidateParameterValue(*definitionIt, *valueValue, normalizedValue, error)) + return false; + + layer.parameterValues[parameterId] = normalizedValue; + } + } + + EnsureLayerDefaults(layer, *shaderPackage); + layers.push_back(layer); + } + + return true; +} + +std::string LayerStackStore::GenerateLayerId(std::vector& layers, uint64_t& nextLayerId) +{ + while (true) + { + ++nextLayerId; + const std::string candidate = "layer-" + std::to_string(nextLayerId); + auto it = std::find_if(layers.begin(), layers.end(), + [&candidate](const LayerPersistentState& layer) { return layer.id == candidate; }); + if (it == layers.end()) + return candidate; + } +} diff --git a/apps/LoopThroughWithOpenGLCompositing/runtime/LayerStackStore.h b/apps/LoopThroughWithOpenGLCompositing/runtime/LayerStackStore.h new file mode 100644 index 0000000..f291b15 --- /dev/null +++ b/apps/LoopThroughWithOpenGLCompositing/runtime/LayerStackStore.h @@ -0,0 +1,72 @@ +#pragma once + +#include "RuntimeJson.h" +#include "ShaderPackageCatalog.h" +#include "ShaderTypes.h" + +#include +#include +#include +#include +#include + +class LayerStackStore +{ +public: + struct LayerPersistentState + { + std::string id; + std::string shaderId; + bool bypass = false; + std::map parameterValues; + }; + + struct StoredParameterSnapshot + { + std::string layerId; + ShaderParameterDefinition definition; + ShaderParameterValue currentValue; + bool hasCurrentValue = false; + }; + + bool LoadPersistentStateValue(const JsonValue& root); + JsonValue BuildPersistentStateValue(const ShaderPackageCatalog& shaderCatalog) const; + void NormalizeLayerIds(); + void EnsureDefaultsForAllLayers(const ShaderPackageCatalog& shaderCatalog); + void EnsureDefaultLayer(const ShaderPackageCatalog& shaderCatalog); + void RemoveLayersWithMissingPackages(const ShaderPackageCatalog& shaderCatalog); + + bool CreateLayer(const ShaderPackageCatalog& shaderCatalog, const std::string& shaderId, std::string& error); + bool DeleteLayer(const std::string& layerId, std::string& error); + bool MoveLayer(const std::string& layerId, int direction, std::string& error); + bool MoveLayerToIndex(const std::string& layerId, std::size_t targetIndex, std::string& error); + bool SetLayerBypassState(const std::string& layerId, bool bypassed, std::string& error); + bool SetLayerShaderSelection(const ShaderPackageCatalog& shaderCatalog, const std::string& layerId, const std::string& shaderId, std::string& error); + bool SetParameterValue(const std::string& layerId, const std::string& parameterId, const ShaderParameterValue& value, std::string& error); + bool ResetLayerParameterValues(const ShaderPackageCatalog& shaderCatalog, const std::string& layerId, std::string& error); + + bool HasLayer(const std::string& layerId) const; + bool TryGetParameterById(const ShaderPackageCatalog& shaderCatalog, const std::string& layerId, const std::string& parameterId, StoredParameterSnapshot& snapshot, std::string& error) const; + bool TryGetParameterByControlKey(const ShaderPackageCatalog& shaderCatalog, const std::string& layerKey, const std::string& parameterKey, StoredParameterSnapshot& snapshot, std::string& error) const; + bool ResolveLayerMove(const std::string& layerId, int direction, bool& shouldMove, std::string& error) const; + bool ResolveLayerMoveToIndex(const std::string& layerId, std::size_t targetIndex, bool& shouldMove, std::string& error) const; + + JsonValue BuildStackPresetValue(const ShaderPackageCatalog& shaderCatalog, const std::string& presetName) const; + bool LoadStackPresetValue(const ShaderPackageCatalog& shaderCatalog, const JsonValue& root, std::string& error); + static std::string MakeSafePresetFileStem(const std::string& presetName); + + const std::vector& Layers() const; + std::vector& Layers(); + std::size_t LayerCount() const; + const LayerPersistentState* FindLayerById(const std::string& layerId) const; + LayerPersistentState* FindLayerById(const std::string& layerId); + +private: + static ShaderParameterValue DefaultValueForDefinition(const ShaderParameterDefinition& definition); + static void EnsureLayerDefaults(LayerPersistentState& layerState, const ShaderPackage& shaderPackage); + static bool DeserializeLayerStack(const ShaderPackageCatalog& shaderCatalog, const JsonValue& layersValue, std::vector& layers, uint64_t& nextLayerId, std::string& error); + static std::string GenerateLayerId(std::vector& layers, uint64_t& nextLayerId); + + std::vector mLayers; + uint64_t mNextLayerId = 0; +}; diff --git a/apps/LoopThroughWithOpenGLCompositing/runtime/RenderSnapshotBuilder.cpp b/apps/LoopThroughWithOpenGLCompositing/runtime/RenderSnapshotBuilder.cpp new file mode 100644 index 0000000..7d0d999 --- /dev/null +++ b/apps/LoopThroughWithOpenGLCompositing/runtime/RenderSnapshotBuilder.cpp @@ -0,0 +1,208 @@ +#include "RenderSnapshotBuilder.h" + +#include "RuntimeClock.h" +#include "RuntimeParameterUtils.h" +#include "RuntimeStore.h" +#include "ShaderCompiler.h" + +#include +#include +#include +#include +#include + +RenderSnapshotBuilder::RenderSnapshotBuilder(RuntimeStore& runtimeStore) : + mRuntimeStore(runtimeStore) +{ +} + +bool RenderSnapshotBuilder::BuildLayerPassFragmentShaderSources(const std::string& layerId, std::vector& passSources, std::string& error) const +{ + try + { + ShaderPackage shaderPackage; + 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); + + ShaderCompiler compiler( + repoRoot, + wrapperPath, + generatedGlslPath, + patchedGlslPath, + maxTemporalHistoryFrames); + passSources.clear(); + passSources.reserve(shaderPackage.passes.size()); + for (const ShaderPassDefinition& pass : shaderPackage.passes) + { + ShaderPassBuildSource passSource; + passSource.passId = pass.id; + passSource.inputNames = pass.inputNames; + passSource.outputName = pass.outputName; + if (!compiler.BuildPassFragmentShaderSource(shaderPackage, pass, passSource.fragmentShaderSource, error)) + return false; + passSources.push_back(std::move(passSource)); + } + return true; + } + catch (const std::exception& exception) + { + error = std::string("RenderSnapshotBuilder::BuildLayerPassFragmentShaderSources exception: ") + exception.what(); + return false; + } + catch (...) + { + error = "RenderSnapshotBuilder::BuildLayerPassFragmentShaderSources threw a non-standard exception."; + return false; + } +} + +unsigned RenderSnapshotBuilder::GetMaxTemporalHistoryFrames() const +{ + return mRuntimeStore.GetConfiguredMaxTemporalHistoryFrames(); +} + +RuntimeSnapshotVersions RenderSnapshotBuilder::GetVersions() const +{ + RuntimeSnapshotVersions versions; + versions.renderStateVersion = mRenderStateVersion.load(std::memory_order_relaxed); + versions.parameterStateVersion = mParameterStateVersion.load(std::memory_order_relaxed); + return versions; +} + +void RenderSnapshotBuilder::AdvanceFrame() +{ + ++mFrameCounter; +} + +void RenderSnapshotBuilder::BuildLayerRenderStates(unsigned outputWidth, unsigned outputHeight, std::vector& states) const +{ + std::lock_guard lock(mRuntimeStore.mMutex); + BuildLayerRenderStatesLocked(outputWidth, outputHeight, 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); + 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); + return true; +} + +void RenderSnapshotBuilder::RefreshDynamicRenderStateFields(std::vector& states) const +{ + std::lock_guard lock(mRuntimeStore.mMutex); + RefreshDynamicRenderStateFieldsLocked(states); +} + +void RenderSnapshotBuilder::MarkRenderStateDirty() +{ + mRenderStateVersion.fetch_add(1, std::memory_order_relaxed); + mParameterStateVersion.fetch_add(1, std::memory_order_relaxed); +} + +void RenderSnapshotBuilder::MarkParameterStateDirty() +{ + mParameterStateVersion.fetch_add(1, std::memory_order_relaxed); +} + +void RenderSnapshotBuilder::BuildLayerRenderStatesLocked(unsigned outputWidth, unsigned outputHeight, std::vector& states) const +{ + states.clear(); + const HealthTelemetry::SignalStatusSnapshot signalStatus = mRuntimeStore.mHealthTelemetry.GetSignalStatusSnapshot(); + + for (const RuntimeStore::LayerPersistentState& layer : mRuntimeStore.mLayerStack.Layers()) + { + const ShaderPackage* shaderPackage = mRuntimeStore.mShaderCatalog.FindPackage(layer.shaderId); + if (!shaderPackage) + continue; + + RuntimeRenderState state; + state.layerId = layer.id; + state.shaderId = layer.shaderId; + 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.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; + + for (const ShaderParameterDefinition& definition : shaderPackage->parameters) + { + ShaderParameterValue value = DefaultValueForDefinition(definition); + auto valueIt = layer.parameterValues.find(definition.id); + if (valueIt != layer.parameterValues.end()) + value = valueIt->second; + state.parameterValues[definition.id] = value; + } + + states.push_back(state); + } + + RefreshDynamicRenderStateFieldsLocked(states); +} + +void RenderSnapshotBuilder::RefreshLayerParametersLocked(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()) + continue; + + state.bypass = layerIt->bypass ? 1.0 : 0.0; + state.parameterValues.clear(); + for (const ShaderParameterDefinition& definition : state.parameterDefinitions) + { + ShaderParameterValue value = DefaultValueForDefinition(definition); + auto valueIt = layerIt->parameterValues.find(definition.id); + if (valueIt != layerIt->parameterValues.end()) + value = valueIt->second; + state.parameterValues[definition.id] = value; + } + } +} + +void RenderSnapshotBuilder::RefreshDynamicRenderStateFieldsLocked(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 frameCount = static_cast(mFrameCounter.load(std::memory_order_relaxed)); + + for (RuntimeRenderState& state : states) + { + state.timeSeconds = timeSeconds; + state.utcTimeSeconds = clock.utcTimeSeconds; + state.utcOffsetSeconds = clock.utcOffsetSeconds; + state.startupRandom = mRuntimeStore.mStartupRandom; + state.frameCount = frameCount; + } +} diff --git a/apps/LoopThroughWithOpenGLCompositing/runtime/RenderSnapshotBuilder.h b/apps/LoopThroughWithOpenGLCompositing/runtime/RenderSnapshotBuilder.h new file mode 100644 index 0000000..ac87f33 --- /dev/null +++ b/apps/LoopThroughWithOpenGLCompositing/runtime/RenderSnapshotBuilder.h @@ -0,0 +1,43 @@ +#pragma once + +#include "ShaderTypes.h" + +#include +#include +#include +#include + +class RuntimeStore; + +struct RuntimeSnapshotVersions +{ + uint64_t renderStateVersion = 0; + uint64_t parameterStateVersion = 0; +}; + +class RenderSnapshotBuilder +{ +public: + explicit RenderSnapshotBuilder(RuntimeStore& runtimeStore); + + bool BuildLayerPassFragmentShaderSources(const std::string& layerId, std::vector& passSources, std::string& error) const; + unsigned GetMaxTemporalHistoryFrames() const; + RuntimeSnapshotVersions GetVersions() const; + void AdvanceFrame(); + void BuildLayerRenderStates(unsigned outputWidth, unsigned outputHeight, std::vector& states) const; + bool TryBuildLayerRenderStates(unsigned outputWidth, unsigned outputHeight, std::vector& states) const; + bool TryRefreshLayerParameters(std::vector& states) const; + void RefreshDynamicRenderStateFields(std::vector& states) const; + void MarkRenderStateDirty(); + 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; + + RuntimeStore& mRuntimeStore; + std::atomic mFrameCounter{ 0 }; + std::atomic mRenderStateVersion{ 0 }; + std::atomic mParameterStateVersion{ 0 }; +}; diff --git a/apps/LoopThroughWithOpenGLCompositing/runtime/RuntimeConfigStore.cpp b/apps/LoopThroughWithOpenGLCompositing/runtime/RuntimeConfigStore.cpp new file mode 100644 index 0000000..1d70151 --- /dev/null +++ b/apps/LoopThroughWithOpenGLCompositing/runtime/RuntimeConfigStore.cpp @@ -0,0 +1,270 @@ +#include "RuntimeConfigStore.h" + +#include "RuntimeJson.h" + +#include +#include +#include +#include +#include + +namespace +{ +double Clamp01(double value) +{ + return (std::max)(0.0, (std::min)(1.0, value)); +} + +bool LooksLikePackagedRuntimeRoot(const std::filesystem::path& candidate) +{ + return std::filesystem::exists(candidate / "config" / "runtime-host.json") && + std::filesystem::exists(candidate / "runtime" / "templates" / "shader_wrapper.slang.in") && + std::filesystem::exists(candidate / "shaders"); +} + +bool LooksLikeRepoRoot(const std::filesystem::path& candidate) +{ + return std::filesystem::exists(candidate / "CMakeLists.txt") && + std::filesystem::exists(candidate / "apps" / "LoopThroughWithOpenGLCompositing"); +} + +std::filesystem::path FindRepoRootCandidate() +{ + std::vector rootsToTry; + + char currentDirectory[MAX_PATH] = {}; + if (GetCurrentDirectoryA(MAX_PATH, currentDirectory) > 0) + rootsToTry.push_back(std::filesystem::path(currentDirectory)); + + char modulePath[MAX_PATH] = {}; + DWORD moduleLength = GetModuleFileNameA(NULL, modulePath, MAX_PATH); + if (moduleLength > 0 && moduleLength < MAX_PATH) + rootsToTry.push_back(std::filesystem::path(modulePath).parent_path()); + + for (const std::filesystem::path& startPath : rootsToTry) + { + std::filesystem::path candidate = startPath; + for (int depth = 0; depth < 10 && !candidate.empty(); ++depth) + { + if (LooksLikePackagedRuntimeRoot(candidate) || LooksLikeRepoRoot(candidate)) + return candidate; + + candidate = candidate.parent_path(); + } + } + + return std::filesystem::path(); +} +} + +bool RuntimeConfigStore::Initialize(std::string& error) +{ + if (!ResolvePaths(error)) + return false; + if (!LoadConfig(error)) + return false; + RefreshConfigDependentPaths(); + return true; +} + +const RuntimeConfigStore::AppConfig& RuntimeConfigStore::GetConfig() const +{ + return mConfig; +} + +const std::filesystem::path& RuntimeConfigStore::GetRepoRoot() const +{ + return mRepoRoot; +} + +const std::filesystem::path& RuntimeConfigStore::GetUiRoot() const +{ + return mUiRoot; +} + +const std::filesystem::path& RuntimeConfigStore::GetDocsRoot() const +{ + return mDocsRoot; +} + +const std::filesystem::path& RuntimeConfigStore::GetShaderRoot() const +{ + return mShaderRoot; +} + +const std::filesystem::path& RuntimeConfigStore::GetRuntimeRoot() const +{ + return mRuntimeRoot; +} + +const std::filesystem::path& RuntimeConfigStore::GetPresetRoot() const +{ + return mPresetRoot; +} + +const std::filesystem::path& RuntimeConfigStore::GetRuntimeStatePath() const +{ + return mRuntimeStatePath; +} + +const std::filesystem::path& RuntimeConfigStore::GetWrapperPath() const +{ + return mWrapperPath; +} + +const std::filesystem::path& RuntimeConfigStore::GetGeneratedGlslPath() const +{ + return mGeneratedGlslPath; +} + +const std::filesystem::path& RuntimeConfigStore::GetPatchedGlslPath() const +{ + return mPatchedGlslPath; +} + +void RuntimeConfigStore::SetBoundControlServerPort(unsigned short port) +{ + mConfig.serverPort = port; +} + +bool RuntimeConfigStore::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"; + mRuntimeRoot = mRepoRoot / "runtime"; + mPresetRoot = mRuntimeRoot / "stack_presets"; + mRuntimeStatePath = mRuntimeRoot / "runtime_state.json"; + RefreshConfigDependentPaths(); + + std::error_code fsError; + std::filesystem::create_directories(mRuntimeRoot / "shader_cache", fsError); + std::filesystem::create_directories(mPresetRoot, fsError); + return true; +} + +bool RuntimeConfigStore::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(); + } + } + + return true; +} + +std::string RuntimeConfigStore::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(); +} + +void RuntimeConfigStore::RefreshConfigDependentPaths() +{ + mShaderRoot = mRepoRoot / mConfig.shaderLibrary; + mWrapperPath = mRuntimeRoot / "shader_cache" / "active_shader_wrapper.slang"; + mGeneratedGlslPath = mRuntimeRoot / "shader_cache" / "active_shader.raw.frag"; + mPatchedGlslPath = mRuntimeRoot / "shader_cache" / "active_shader.frag"; +} diff --git a/apps/LoopThroughWithOpenGLCompositing/runtime/RuntimeConfigStore.h b/apps/LoopThroughWithOpenGLCompositing/runtime/RuntimeConfigStore.h new file mode 100644 index 0000000..399b234 --- /dev/null +++ b/apps/LoopThroughWithOpenGLCompositing/runtime/RuntimeConfigStore.h @@ -0,0 +1,59 @@ +#pragma once + +#include +#include + +class RuntimeConfigStore +{ +public: + 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"; + }; + + bool Initialize(std::string& error); + + const AppConfig& GetConfig() const; + const std::filesystem::path& GetRepoRoot() const; + const std::filesystem::path& GetUiRoot() const; + const std::filesystem::path& GetDocsRoot() const; + const std::filesystem::path& GetShaderRoot() const; + const std::filesystem::path& GetRuntimeRoot() const; + const std::filesystem::path& GetPresetRoot() const; + const std::filesystem::path& GetRuntimeStatePath() const; + const std::filesystem::path& GetWrapperPath() const; + const std::filesystem::path& GetGeneratedGlslPath() const; + const std::filesystem::path& GetPatchedGlslPath() const; + void SetBoundControlServerPort(unsigned short port); + +private: + bool ResolvePaths(std::string& error); + bool LoadConfig(std::string& error); + std::string ReadTextFile(const std::filesystem::path& path, std::string& error) const; + void RefreshConfigDependentPaths(); + + AppConfig mConfig; + 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; +}; diff --git a/apps/LoopThroughWithOpenGLCompositing/runtime/RuntimeSnapshotProvider.cpp b/apps/LoopThroughWithOpenGLCompositing/runtime/RuntimeSnapshotProvider.cpp index 710157c..541cb61 100644 --- a/apps/LoopThroughWithOpenGLCompositing/runtime/RuntimeSnapshotProvider.cpp +++ b/apps/LoopThroughWithOpenGLCompositing/runtime/RuntimeSnapshotProvider.cpp @@ -1,12 +1,9 @@ #include "RuntimeSnapshotProvider.h" -#include "ShaderCompiler.h" - -#include #include -RuntimeSnapshotProvider::RuntimeSnapshotProvider(RuntimeStore& runtimeStore) : - mRuntimeStore(runtimeStore) +RuntimeSnapshotProvider::RuntimeSnapshotProvider(RenderSnapshotBuilder& renderSnapshotBuilder) : + mRenderSnapshotBuilder(renderSnapshotBuilder) { } @@ -14,36 +11,7 @@ bool RuntimeSnapshotProvider::BuildLayerPassFragmentShaderSources(const std::str { try { - ShaderPackage shaderPackage; - 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); - - ShaderCompiler compiler( - repoRoot, - wrapperPath, - generatedGlslPath, - patchedGlslPath, - maxTemporalHistoryFrames); - passSources.clear(); - passSources.reserve(shaderPackage.passes.size()); - for (const ShaderPassDefinition& pass : shaderPackage.passes) - { - ShaderPassBuildSource passSource; - passSource.passId = pass.id; - passSource.inputNames = pass.inputNames; - passSource.outputName = pass.outputName; - if (!compiler.BuildPassFragmentShaderSource(shaderPackage, pass, passSource.fragmentShaderSource, error)) - return false; - passSources.push_back(std::move(passSource)); - } - return true; + return mRenderSnapshotBuilder.BuildLayerPassFragmentShaderSources(layerId, passSources, error); } catch (const std::exception& exception) { @@ -59,20 +27,17 @@ bool RuntimeSnapshotProvider::BuildLayerPassFragmentShaderSources(const std::str unsigned RuntimeSnapshotProvider::GetMaxTemporalHistoryFrames() const { - return mRuntimeStore.GetConfiguredMaxTemporalHistoryFrames(); + return mRenderSnapshotBuilder.GetMaxTemporalHistoryFrames(); } RuntimeSnapshotVersions RuntimeSnapshotProvider::GetVersions() const { - RuntimeSnapshotVersions versions; - versions.renderStateVersion = mRuntimeStore.GetRenderStateVersion(); - versions.parameterStateVersion = mRuntimeStore.GetParameterStateVersion(); - return versions; + return mRenderSnapshotBuilder.GetVersions(); } void RuntimeSnapshotProvider::AdvanceFrame() { - mRuntimeStore.AdvanceFrameCounter(); + mRenderSnapshotBuilder.AdvanceFrame(); } RuntimeRenderStateSnapshot RuntimeSnapshotProvider::PublishRenderStateSnapshot(unsigned outputWidth, unsigned outputHeight) const @@ -87,7 +52,7 @@ RuntimeRenderStateSnapshot RuntimeSnapshotProvider::PublishRenderStateSnapshot(u RuntimeRenderStateSnapshot snapshot; snapshot.outputWidth = outputWidth; snapshot.outputHeight = outputHeight; - mRuntimeStore.BuildLayerRenderStates(outputWidth, outputHeight, snapshot.states); + mRenderSnapshotBuilder.BuildLayerRenderStates(outputWidth, outputHeight, snapshot.states); const RuntimeSnapshotVersions versionsAfter = GetVersions(); if (versionsBefore.renderStateVersion == versionsAfter.renderStateVersion && @@ -107,7 +72,7 @@ bool RuntimeSnapshotProvider::TryPublishRenderStateSnapshot(unsigned outputWidth return true; std::vector states; - if (!mRuntimeStore.TryBuildLayerRenderStates(outputWidth, outputHeight, states)) + if (!mRenderSnapshotBuilder.TryBuildLayerRenderStates(outputWidth, outputHeight, states)) return false; const RuntimeSnapshotVersions versionsAfter = GetVersions(); @@ -128,7 +93,7 @@ bool RuntimeSnapshotProvider::TryPublishRenderStateSnapshot(unsigned outputWidth bool RuntimeSnapshotProvider::TryRefreshPublishedSnapshotParameters(RuntimeRenderStateSnapshot& snapshot) const { const uint64_t expectedRenderStateVersion = snapshot.versions.renderStateVersion; - if (!mRuntimeStore.TryRefreshLayerParameters(snapshot.states)) + if (!mRenderSnapshotBuilder.TryRefreshLayerParameters(snapshot.states)) return false; const RuntimeSnapshotVersions versions = GetVersions(); @@ -142,7 +107,7 @@ bool RuntimeSnapshotProvider::TryRefreshPublishedSnapshotParameters(RuntimeRende void RuntimeSnapshotProvider::RefreshDynamicRenderStateFields(std::vector& states) const { - mRuntimeStore.RefreshDynamicRenderStateFields(states); + mRenderSnapshotBuilder.RefreshDynamicRenderStateFields(states); } bool RuntimeSnapshotProvider::TryGetPublishedRenderStateSnapshot(unsigned outputWidth, unsigned outputHeight, diff --git a/apps/LoopThroughWithOpenGLCompositing/runtime/RuntimeSnapshotProvider.h b/apps/LoopThroughWithOpenGLCompositing/runtime/RuntimeSnapshotProvider.h index 4d6a622..f014720 100644 --- a/apps/LoopThroughWithOpenGLCompositing/runtime/RuntimeSnapshotProvider.h +++ b/apps/LoopThroughWithOpenGLCompositing/runtime/RuntimeSnapshotProvider.h @@ -1,18 +1,11 @@ #pragma once -#include "RuntimeStore.h" +#include "RenderSnapshotBuilder.h" -#include #include #include #include -struct RuntimeSnapshotVersions -{ - uint64_t renderStateVersion = 0; - uint64_t parameterStateVersion = 0; -}; - struct RuntimeRenderStateSnapshot { RuntimeSnapshotVersions versions; @@ -24,7 +17,7 @@ struct RuntimeRenderStateSnapshot class RuntimeSnapshotProvider { public: - explicit RuntimeSnapshotProvider(RuntimeStore& runtimeStore); + explicit RuntimeSnapshotProvider(RenderSnapshotBuilder& renderSnapshotBuilder); bool BuildLayerPassFragmentShaderSources(const std::string& layerId, std::vector& passSources, std::string& error) const; unsigned GetMaxTemporalHistoryFrames() const; @@ -42,7 +35,7 @@ private: static bool SnapshotMatches(const RuntimeRenderStateSnapshot& snapshot, unsigned outputWidth, unsigned outputHeight, const RuntimeSnapshotVersions& versions); - RuntimeStore& mRuntimeStore; + RenderSnapshotBuilder& mRenderSnapshotBuilder; mutable std::mutex mPublishedSnapshotMutex; mutable bool mHasPublishedRenderStateSnapshot = false; mutable RuntimeRenderStateSnapshot mPublishedRenderStateSnapshot; diff --git a/apps/LoopThroughWithOpenGLCompositing/runtime/RuntimeStateJson.cpp b/apps/LoopThroughWithOpenGLCompositing/runtime/RuntimeStateJson.cpp new file mode 100644 index 0000000..d51954f --- /dev/null +++ b/apps/LoopThroughWithOpenGLCompositing/runtime/RuntimeStateJson.cpp @@ -0,0 +1,157 @@ +#include "RuntimeStateJson.h" + +#include "RuntimeParameterUtils.h" + +namespace +{ +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"; +} +} + +JsonValue RuntimeStateJson::SerializeLayerStack(const LayerStackStore& layerStack, const ShaderPackageCatalog& shaderCatalog) +{ + JsonValue layers = JsonValue::MakeArray(); + for (const LayerStackStore::LayerPersistentState& layer : layerStack.Layers()) + { + const ShaderPackage* shaderPackage = shaderCatalog.FindPackage(layer.shaderId); + if (!shaderPackage) + continue; + + JsonValue layerValue = JsonValue::MakeObject(); + layerValue.set("id", JsonValue(layer.id)); + layerValue.set("shaderId", JsonValue(layer.shaderId)); + layerValue.set("shaderName", JsonValue(shaderPackage->displayName)); + layerValue.set("bypass", JsonValue(layer.bypass)); + 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))); + layerValue.set("temporal", temporal); + } + if (shaderPackage->feedback.enabled) + { + JsonValue feedback = JsonValue::MakeObject(); + feedback.set("enabled", JsonValue(true)); + feedback.set("writePass", JsonValue(shaderPackage->feedback.writePassId)); + layerValue.set("feedback", feedback); + } + + JsonValue parameters = JsonValue::MakeArray(); + for (const ShaderParameterDefinition& definition : shaderPackage->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; +} + +JsonValue RuntimeStateJson::SerializeParameterValue(const ShaderParameterDefinition& definition, const ShaderParameterValue& value) +{ + 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 RuntimeStateJson::TemporalHistorySourceToString(TemporalHistorySource source) +{ + switch (source) + { + case TemporalHistorySource::Source: + return "source"; + case TemporalHistorySource::PreLayerInput: + return "preLayerInput"; + case TemporalHistorySource::None: + default: + return "none"; + } +} diff --git a/apps/LoopThroughWithOpenGLCompositing/runtime/RuntimeStateJson.h b/apps/LoopThroughWithOpenGLCompositing/runtime/RuntimeStateJson.h new file mode 100644 index 0000000..333fb37 --- /dev/null +++ b/apps/LoopThroughWithOpenGLCompositing/runtime/RuntimeStateJson.h @@ -0,0 +1,16 @@ +#pragma once + +#include "LayerStackStore.h" +#include "RuntimeJson.h" +#include "ShaderPackageCatalog.h" +#include "ShaderTypes.h" + +#include + +class RuntimeStateJson +{ +public: + static JsonValue SerializeLayerStack(const LayerStackStore& layerStack, const ShaderPackageCatalog& shaderCatalog); + static JsonValue SerializeParameterValue(const ShaderParameterDefinition& definition, const ShaderParameterValue& value); + static std::string TemporalHistorySourceToString(TemporalHistorySource source); +}; diff --git a/apps/LoopThroughWithOpenGLCompositing/runtime/RuntimeStatePresenter.cpp b/apps/LoopThroughWithOpenGLCompositing/runtime/RuntimeStatePresenter.cpp new file mode 100644 index 0000000..2bdf887 --- /dev/null +++ b/apps/LoopThroughWithOpenGLCompositing/runtime/RuntimeStatePresenter.cpp @@ -0,0 +1,125 @@ +#include "RuntimeStatePresenter.h" + +#include "RuntimeStateJson.h" +#include "RuntimeStore.h" + +#include + +std::string RuntimeStatePresenter::BuildRuntimeStateJson(const RuntimeStore& runtimeStore) +{ + return SerializeJson(BuildRuntimeStateValue(runtimeStore), true); +} + +JsonValue RuntimeStatePresenter::BuildRuntimeStateValue(const RuntimeStore& runtimeStore) +{ + const HealthTelemetry::Snapshot telemetrySnapshot = runtimeStore.mHealthTelemetry.GetSnapshot(); + std::lock_guard lock(runtimeStore.mMutex); + + 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)); + 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)); + root.set("runtime", runtime); + + JsonValue video = JsonValue::MakeObject(); + video.set("hasSignal", JsonValue(telemetrySnapshot.signal.hasSignal)); + video.set("width", JsonValue(static_cast(telemetrySnapshot.signal.width))); + video.set("height", JsonValue(static_cast(telemetrySnapshot.signal.height))); + video.set("modeName", JsonValue(telemetrySnapshot.signal.modeName)); + root.set("video", video); + + JsonValue deckLink = JsonValue::MakeObject(); + deckLink.set("modelName", JsonValue(telemetrySnapshot.videoIO.modelName)); + deckLink.set("supportsInternalKeying", JsonValue(telemetrySnapshot.videoIO.supportsInternalKeying)); + deckLink.set("supportsExternalKeying", JsonValue(telemetrySnapshot.videoIO.supportsExternalKeying)); + deckLink.set("keyerInterfaceAvailable", JsonValue(telemetrySnapshot.videoIO.keyerInterfaceAvailable)); + deckLink.set("externalKeyingRequested", JsonValue(telemetrySnapshot.videoIO.externalKeyingRequested)); + deckLink.set("externalKeyingActive", JsonValue(telemetrySnapshot.videoIO.externalKeyingActive)); + deckLink.set("statusMessage", JsonValue(telemetrySnapshot.videoIO.statusMessage)); + root.set("decklink", deckLink); + + JsonValue videoIO = JsonValue::MakeObject(); + videoIO.set("backend", JsonValue(telemetrySnapshot.videoIO.backendName)); + videoIO.set("modelName", JsonValue(telemetrySnapshot.videoIO.modelName)); + videoIO.set("supportsInternalKeying", JsonValue(telemetrySnapshot.videoIO.supportsInternalKeying)); + videoIO.set("supportsExternalKeying", JsonValue(telemetrySnapshot.videoIO.supportsExternalKeying)); + videoIO.set("keyerInterfaceAvailable", JsonValue(telemetrySnapshot.videoIO.keyerInterfaceAvailable)); + videoIO.set("externalKeyingRequested", JsonValue(telemetrySnapshot.videoIO.externalKeyingRequested)); + videoIO.set("externalKeyingActive", JsonValue(telemetrySnapshot.videoIO.externalKeyingActive)); + videoIO.set("statusMessage", JsonValue(telemetrySnapshot.videoIO.statusMessage)); + root.set("videoIO", videoIO); + + JsonValue performance = JsonValue::MakeObject(); + performance.set("frameBudgetMs", JsonValue(telemetrySnapshot.performance.frameBudgetMilliseconds)); + performance.set("renderMs", JsonValue(telemetrySnapshot.performance.renderMilliseconds)); + performance.set("smoothedRenderMs", JsonValue(telemetrySnapshot.performance.smoothedRenderMilliseconds)); + performance.set("budgetUsedPercent", JsonValue( + telemetrySnapshot.performance.frameBudgetMilliseconds > 0.0 + ? (telemetrySnapshot.performance.smoothedRenderMilliseconds / telemetrySnapshot.performance.frameBudgetMilliseconds) * 100.0 + : 0.0)); + performance.set("completionIntervalMs", JsonValue(telemetrySnapshot.performance.completionIntervalMilliseconds)); + performance.set("smoothedCompletionIntervalMs", JsonValue(telemetrySnapshot.performance.smoothedCompletionIntervalMilliseconds)); + performance.set("maxCompletionIntervalMs", JsonValue(telemetrySnapshot.performance.maxCompletionIntervalMilliseconds)); + performance.set("lateFrameCount", JsonValue(static_cast(telemetrySnapshot.performance.lateFrameCount))); + performance.set("droppedFrameCount", JsonValue(static_cast(telemetrySnapshot.performance.droppedFrameCount))); + performance.set("flushedFrameCount", JsonValue(static_cast(telemetrySnapshot.performance.flushedFrameCount))); + root.set("performance", performance); + + JsonValue shaderLibrary = JsonValue::MakeArray(); + for (const ShaderPackageStatus& status : runtimeStore.mShaderCatalog.PackageStatuses()) + { + JsonValue shader = JsonValue::MakeObject(); + shader.set("id", JsonValue(status.id)); + shader.set("name", JsonValue(status.displayName)); + shader.set("description", JsonValue(status.description)); + shader.set("category", JsonValue(status.category)); + shader.set("available", JsonValue(status.available)); + if (!status.available) + shader.set("error", JsonValue(status.error)); + + const ShaderPackage* shaderPackage = runtimeStore.mShaderCatalog.FindPackage(status.id); + if (status.available && shaderPackage && shaderPackage->temporal.enabled) + { + 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))); + shader.set("temporal", temporal); + } + if (status.available && shaderPackage && shaderPackage->feedback.enabled) + { + JsonValue feedback = JsonValue::MakeObject(); + feedback.set("enabled", JsonValue(true)); + feedback.set("writePass", JsonValue(shaderPackage->feedback.writePassId)); + shader.set("feedback", feedback); + } + shaderLibrary.pushBack(shader); + } + root.set("shaders", shaderLibrary); + + JsonValue stackPresets = JsonValue::MakeArray(); + for (const std::string& presetName : runtimeStore.GetStackPresetNamesLocked()) + stackPresets.pushBack(JsonValue(presetName)); + root.set("stackPresets", stackPresets); + + root.set("layers", RuntimeStateJson::SerializeLayerStack(runtimeStore.mLayerStack, runtimeStore.mShaderCatalog)); + return root; +} diff --git a/apps/LoopThroughWithOpenGLCompositing/runtime/RuntimeStatePresenter.h b/apps/LoopThroughWithOpenGLCompositing/runtime/RuntimeStatePresenter.h new file mode 100644 index 0000000..8022b15 --- /dev/null +++ b/apps/LoopThroughWithOpenGLCompositing/runtime/RuntimeStatePresenter.h @@ -0,0 +1,14 @@ +#pragma once + +#include "RuntimeJson.h" + +#include + +class RuntimeStore; + +class RuntimeStatePresenter +{ +public: + static std::string BuildRuntimeStateJson(const RuntimeStore& runtimeStore); + static JsonValue BuildRuntimeStateValue(const RuntimeStore& runtimeStore); +}; diff --git a/apps/LoopThroughWithOpenGLCompositing/runtime/RuntimeStore.cpp b/apps/LoopThroughWithOpenGLCompositing/runtime/RuntimeStore.cpp index 7c6a8a5..8d15d88 100644 --- a/apps/LoopThroughWithOpenGLCompositing/runtime/RuntimeStore.cpp +++ b/apps/LoopThroughWithOpenGLCompositing/runtime/RuntimeStore.cpp @@ -1,55 +1,16 @@ #include "RuntimeStore.h" -#include "RuntimeClock.h" -#include "ShaderPackageRegistry.h" -#include "RuntimeParameterUtils.h" +#include "RuntimeStatePresenter.h" -#include #include -#include #include #include #include -#include #include #include namespace { -std::string TrimCopy(const std::string& text) -{ - std::size_t start = 0; - while (start < text.size() && std::isspace(static_cast(text[start]))) - ++start; - - std::size_t end = text.size(); - while (end > start && std::isspace(static_cast(text[end - 1]))) - --end; - - return text.substr(start, end - start); -} - -std::string SimplifyControlKey(const std::string& text) -{ - std::string simplified; - for (unsigned char ch : text) - { - if (std::isalnum(ch)) - simplified.push_back(static_cast(std::tolower(ch))); - } - return simplified; -} - -bool MatchesControlKey(const std::string& candidate, const std::string& key) -{ - return candidate == key || SimplifyControlKey(candidate) == SimplifyControlKey(key); -} - -double Clamp01(double value) -{ - return (std::max)(0.0, (std::min)(1.0, value)); -} - std::string ToLowerCopy(std::string text) { std::transform(text.begin(), text.end(), text.begin(), @@ -57,83 +18,6 @@ std::string ToLowerCopy(std::string text) return text; } -bool LooksLikePackagedRuntimeRoot(const std::filesystem::path& candidate) -{ - return std::filesystem::exists(candidate / "config" / "runtime-host.json") && - std::filesystem::exists(candidate / "runtime" / "templates" / "shader_wrapper.slang.in") && - std::filesystem::exists(candidate / "shaders"); -} - -bool LooksLikeRepoRoot(const std::filesystem::path& candidate) -{ - return std::filesystem::exists(candidate / "CMakeLists.txt") && - std::filesystem::exists(candidate / "apps" / "LoopThroughWithOpenGLCompositing"); -} - -std::filesystem::path FindRepoRootCandidate() -{ - std::vector rootsToTry; - - char currentDirectory[MAX_PATH] = {}; - if (GetCurrentDirectoryA(MAX_PATH, currentDirectory) > 0) - rootsToTry.push_back(std::filesystem::path(currentDirectory)); - - char modulePath[MAX_PATH] = {}; - DWORD moduleLength = GetModuleFileNameA(NULL, modulePath, MAX_PATH); - if (moduleLength > 0 && moduleLength < MAX_PATH) - rootsToTry.push_back(std::filesystem::path(modulePath).parent_path()); - - for (const std::filesystem::path& startPath : rootsToTry) - { - std::filesystem::path candidate = startPath; - for (int depth = 0; depth < 10 && !candidate.empty(); ++depth) - { - if (LooksLikePackagedRuntimeRoot(candidate) || LooksLikeRepoRoot(candidate)) - return candidate; - - candidate = candidate.parent_path(); - } - } - - return std::filesystem::path(); -} - -bool TextureAssetsEqual(const std::vector& left, const std::vector& right) -{ - if (left.size() != right.size()) - return false; - - for (std::size_t index = 0; index < left.size(); ++index) - { - if (left[index].id != right[index].id || - left[index].path != right[index].path || - left[index].writeTime != right[index].writeTime) - { - return false; - } - } - - return true; -} - -bool FontAssetsEqual(const std::vector& left, const std::vector& right) -{ - if (left.size() != right.size()) - return false; - - for (std::size_t index = 0; index < left.size(); ++index) - { - if (left[index].id != right[index].id || - left[index].path != right[index].path || - left[index].writeTime != right[index].writeTime) - { - return false; - } - } - - return true; -} - double GenerateStartupRandom() { std::random_device randomDevice; @@ -141,42 +25,10 @@ double GenerateStartupRandom() 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; -} - -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"; -} } RuntimeStore::RuntimeStore() : + mRenderSnapshotBuilder(*this), mHealthTelemetry(), mReloadRequested(false), mCompileSucceeded(false), @@ -184,9 +36,7 @@ RuntimeStore::RuntimeStore() : mServerPort(8080), mAutoReloadEnabled(true), mStartTime(std::chrono::steady_clock::now()), - mLastScanTime((std::chrono::steady_clock::time_point::min)()), - mFrameCounter(0), - mNextLayerId(0) + mLastScanTime((std::chrono::steady_clock::time_point::min)()) { } @@ -200,42 +50,34 @@ const HealthTelemetry& RuntimeStore::GetHealthTelemetry() const return mHealthTelemetry; } +RenderSnapshotBuilder& RuntimeStore::GetRenderSnapshotBuilder() +{ + return mRenderSnapshotBuilder; +} + +const RenderSnapshotBuilder& RuntimeStore::GetRenderSnapshotBuilder() const +{ + return mRenderSnapshotBuilder; +} + bool RuntimeStore::InitializeStore(std::string& error) { try { std::lock_guard lock(mMutex); - if (!ResolvePaths(error)) + if (!mConfigStore.Initialize(error)) return false; - if (!LoadConfig(error)) - return false; - mShaderRoot = mRepoRoot / mConfig.shaderLibrary; if (!LoadPersistentState(error)) return false; if (!ScanShaderPackages(error)) return false; - NormalizePersistentLayerIdsLocked(); + mLayerStack.NormalizeLayerIds(); + mLayerStack.EnsureDefaultsForAllLayers(mShaderCatalog); + mLayerStack.EnsureDefaultLayer(mShaderCatalog); - for (RuntimeStore::LayerPersistentState& layer : mPersistentState.layers) - { - auto shaderIt = mPackagesById.find(layer.shaderId); - if (shaderIt != mPackagesById.end()) - EnsureLayerDefaultsLocked(layer, shaderIt->second); - } - - if (mPersistentState.layers.empty() && !mPackageOrder.empty()) - { - RuntimeStore::LayerPersistentState layer; - layer.id = GenerateLayerId(); - layer.shaderId = mPackageOrder.front(); - layer.bypass = false; - EnsureLayerDefaultsLocked(layer, mPackagesById[layer.shaderId]); - mPersistentState.layers.push_back(layer); - } - - mServerPort = mConfig.serverPort; - mAutoReloadEnabled = mConfig.autoReload; + mServerPort = mConfigStore.GetConfig().serverPort; + mAutoReloadEnabled = mConfigStore.GetConfig().autoReload; mReloadRequested = true; mCompileMessage = "Waiting for shader compile."; return true; @@ -254,7 +96,7 @@ bool RuntimeStore::InitializeStore(std::string& error) std::string RuntimeStore::BuildPersistentStateJson() const { - return SerializeJson(BuildRuntimeStateValue(), true); + return RuntimeStatePresenter::BuildRuntimeStateJson(*this); } bool RuntimeStore::PollStoredFileChanges(bool& registryChanged, bool& reloadRequested, std::string& error) @@ -282,63 +124,23 @@ bool RuntimeStore::PollStoredFileChanges(bool& registryChanged, bool& reloadRequ mLastScanTime = now; std::string scanError; - std::map previousPackages = mPackagesById; - std::vector previousOrder = mPackageOrder; - std::map> previousLayerShaderTimes; - for (const RuntimeStore::LayerPersistentState& layer : mPersistentState.layers) - { - auto previous = previousPackages.find(layer.shaderId); - if (previous != previousPackages.end()) - previousLayerShaderTimes[layer.id] = std::make_pair(previous->second.shaderWriteTime, previous->second.manifestWriteTime); - } - + const ShaderPackageCatalog::Snapshot previousCatalog = mShaderCatalog.CaptureSnapshot(); if (!ScanShaderPackages(scanError)) { error = scanError; return false; } - registryChanged = previousOrder != mPackageOrder; - if (!registryChanged && previousPackages.size() == mPackagesById.size()) - { - for (const auto& item : mPackagesById) - { - auto previous = previousPackages.find(item.first); - if (previous == previousPackages.end()) - { - registryChanged = true; - break; - } - if (previous->second.shaderWriteTime != item.second.shaderWriteTime || - previous->second.manifestWriteTime != item.second.manifestWriteTime || - !TextureAssetsEqual(previous->second.textureAssets, item.second.textureAssets) || - !FontAssetsEqual(previous->second.fontAssets, item.second.fontAssets)) - { - registryChanged = true; - break; - } - } - } + registryChanged = mShaderCatalog.HasCatalogChangedSince(previousCatalog); - for (RuntimeStore::LayerPersistentState& layer : mPersistentState.layers) + mLayerStack.EnsureDefaultsForAllLayers(mShaderCatalog); + for (RuntimeStore::LayerPersistentState& layer : mLayerStack.Layers()) { - auto active = mPackagesById.find(layer.shaderId); - auto previous = previousLayerShaderTimes.find(layer.id); - if (active == mPackagesById.end()) + const ShaderPackage* active = mShaderCatalog.FindPackage(layer.shaderId); + if (!active) continue; - EnsureLayerDefaultsLocked(layer, active->second); - if (previous != previousLayerShaderTimes.end()) - { - auto previousPackage = previousPackages.find(layer.shaderId); - if (previous->second.first != active->second.shaderWriteTime || - previous->second.second != active->second.manifestWriteTime || - (previousPackage != previousPackages.end() && - (!TextureAssetsEqual(previousPackage->second.textureAssets, active->second.textureAssets) || - !FontAssetsEqual(previousPackage->second.fontAssets, active->second.fontAssets)))) - { - mReloadRequested = true; - } - } + if (mShaderCatalog.HasPackageChangedSince(previousCatalog, layer.shaderId)) + mReloadRequested = true; } reloadRequested = mReloadRequested; @@ -361,19 +163,9 @@ bool RuntimeStore::PollStoredFileChanges(bool& registryChanged, bool& reloadRequ bool RuntimeStore::CreateStoredLayer(const std::string& shaderId, std::string& error) { std::lock_guard lock(mMutex); - auto shaderIt = mPackagesById.find(shaderId); - if (shaderIt == mPackagesById.end()) - { - error = "Unknown shader id: " + shaderId; + if (!mLayerStack.CreateLayer(mShaderCatalog, shaderId, error)) return false; - } - RuntimeStore::LayerPersistentState layer; - layer.id = GenerateLayerId(); - layer.shaderId = shaderId; - layer.bypass = false; - EnsureLayerDefaultsLocked(layer, shaderIt->second); - mPersistentState.layers.push_back(layer); mReloadRequested = true; MarkRenderStateDirtyLocked(); return SavePersistentState(error); @@ -382,15 +174,9 @@ bool RuntimeStore::CreateStoredLayer(const std::string& shaderId, std::string& e bool RuntimeStore::DeleteStoredLayer(const std::string& layerId, std::string& error) { std::lock_guard lock(mMutex); - auto it = std::find_if(mPersistentState.layers.begin(), mPersistentState.layers.end(), - [&layerId](const RuntimeStore::LayerPersistentState& layer) { return layer.id == layerId; }); - if (it == mPersistentState.layers.end()) - { - error = "Unknown layer id: " + layerId; + if (!mLayerStack.DeleteLayer(layerId, error)) return false; - } - mPersistentState.layers.erase(it); mReloadRequested = true; MarkRenderStateDirtyLocked(); return SavePersistentState(error); @@ -399,20 +185,15 @@ bool RuntimeStore::DeleteStoredLayer(const std::string& layerId, std::string& er bool RuntimeStore::MoveStoredLayer(const std::string& layerId, int direction, std::string& error) { std::lock_guard lock(mMutex); - auto it = std::find_if(mPersistentState.layers.begin(), mPersistentState.layers.end(), - [&layerId](const RuntimeStore::LayerPersistentState& layer) { return layer.id == layerId; }); - if (it == mPersistentState.layers.end()) - { - error = "Unknown layer id: " + layerId; + bool shouldMove = false; + if (!mLayerStack.ResolveLayerMove(layerId, direction, shouldMove, error)) return false; - } - - const std::ptrdiff_t index = std::distance(mPersistentState.layers.begin(), it); - const std::ptrdiff_t newIndex = index + direction; - if (newIndex < 0 || newIndex >= static_cast(mPersistentState.layers.size())) + if (!shouldMove) return true; - std::swap(mPersistentState.layers[index], mPersistentState.layers[newIndex]); + if (!mLayerStack.MoveLayer(layerId, direction, error)) + return false; + mReloadRequested = true; MarkRenderStateDirtyLocked(); return SavePersistentState(error); @@ -421,27 +202,15 @@ bool RuntimeStore::MoveStoredLayer(const std::string& layerId, int direction, st bool RuntimeStore::MoveStoredLayerToIndex(const std::string& layerId, std::size_t targetIndex, std::string& error) { std::lock_guard lock(mMutex); - auto it = std::find_if(mPersistentState.layers.begin(), mPersistentState.layers.end(), - [&layerId](const RuntimeStore::LayerPersistentState& layer) { return layer.id == layerId; }); - if (it == mPersistentState.layers.end()) - { - error = "Unknown layer id: " + layerId; + bool shouldMove = false; + if (!mLayerStack.ResolveLayerMoveToIndex(layerId, targetIndex, shouldMove, error)) return false; - } - - if (mPersistentState.layers.empty()) + if (!shouldMove) return true; - if (targetIndex >= mPersistentState.layers.size()) - targetIndex = mPersistentState.layers.size() - 1; + if (!mLayerStack.MoveLayerToIndex(layerId, targetIndex, error)) + return false; - const std::size_t sourceIndex = static_cast(std::distance(mPersistentState.layers.begin(), it)); - if (sourceIndex == targetIndex) - return true; - - RuntimeStore::LayerPersistentState movedLayer = *it; - mPersistentState.layers.erase(mPersistentState.layers.begin() + static_cast(sourceIndex)); - mPersistentState.layers.insert(mPersistentState.layers.begin() + static_cast(targetIndex), movedLayer); mReloadRequested = true; MarkRenderStateDirtyLocked(); return SavePersistentState(error); @@ -450,14 +219,9 @@ bool RuntimeStore::MoveStoredLayerToIndex(const std::string& layerId, std::size_ bool RuntimeStore::SetStoredLayerBypassState(const std::string& layerId, bool bypassed, std::string& error) { std::lock_guard lock(mMutex); - RuntimeStore::LayerPersistentState* layer = FindLayerById(layerId); - if (!layer) - { - error = "Unknown layer id: " + layerId; + if (!mLayerStack.SetLayerBypassState(layerId, bypassed, error)) return false; - } - layer->bypass = bypassed; mReloadRequested = true; MarkParameterStateDirtyLocked(); return SavePersistentState(error); @@ -466,23 +230,9 @@ bool RuntimeStore::SetStoredLayerBypassState(const std::string& layerId, bool by bool RuntimeStore::SetStoredLayerShaderSelection(const std::string& layerId, const std::string& shaderId, std::string& error) { std::lock_guard lock(mMutex); - RuntimeStore::LayerPersistentState* layer = FindLayerById(layerId); - if (!layer) - { - error = "Unknown layer id: " + layerId; + if (!mLayerStack.SetLayerShaderSelection(mShaderCatalog, layerId, shaderId, error)) return false; - } - auto shaderIt = mPackagesById.find(shaderId); - if (shaderIt == mPackagesById.end()) - { - error = "Unknown shader id: " + shaderId; - return false; - } - - layer->shaderId = shaderId; - layer->parameterValues.clear(); - EnsureLayerDefaultsLocked(*layer, shaderIt->second); mReloadRequested = true; MarkRenderStateDirtyLocked(); return SavePersistentState(error); @@ -492,14 +242,9 @@ bool RuntimeStore::SetStoredParameterValue(const std::string& layerId, const std { std::lock_guard lock(mMutex); - RuntimeStore::LayerPersistentState* layer = FindLayerById(layerId); - if (!layer) - { - error = "Unknown layer id: " + layerId; + if (!mLayerStack.SetParameterValue(layerId, parameterId, value, error)) return false; - } - layer->parameterValues[parameterId] = value; MarkParameterStateDirtyLocked(); return !persistState || SavePersistentState(error); } @@ -508,22 +253,9 @@ bool RuntimeStore::ResetStoredLayerParameterValues(const std::string& layerId, s { std::lock_guard lock(mMutex); - RuntimeStore::LayerPersistentState* layer = FindLayerById(layerId); - if (!layer) - { - error = "Unknown layer id: " + layerId; + if (!mLayerStack.ResetLayerParameterValues(mShaderCatalog, layerId, error)) return false; - } - auto shaderIt = mPackagesById.find(layer->shaderId); - if (shaderIt == mPackagesById.end()) - { - error = "Unknown shader id: " + layer->shaderId; - return false; - } - - layer->parameterValues.clear(); - EnsureLayerDefaultsLocked(*layer, shaderIt->second); MarkParameterStateDirtyLocked(); return SavePersistentState(error); } @@ -531,7 +263,7 @@ bool RuntimeStore::ResetStoredLayerParameterValues(const std::string& layerId, s bool RuntimeStore::SaveStackPresetSnapshot(const std::string& presetName, std::string& error) const { std::lock_guard lock(mMutex); - const std::string safeStem = MakeSafePresetFileStem(presetName); + const std::string safeStem = LayerStackStore::MakeSafePresetFileStem(presetName); if (safeStem.empty()) { error = "Preset name must include at least one letter or number."; @@ -539,24 +271,22 @@ bool RuntimeStore::SaveStackPresetSnapshot(const std::string& presetName, std::s } JsonValue root = JsonValue::MakeObject(); - root.set("version", JsonValue(1.0)); - root.set("name", JsonValue(TrimCopy(presetName))); - root.set("layers", SerializeLayerStackLocked()); + root = mLayerStack.BuildStackPresetValue(mShaderCatalog, presetName); - return WriteTextFile(mPresetRoot / (safeStem + ".json"), SerializeJson(root, true), error); + return WriteTextFile(mConfigStore.GetPresetRoot() / (safeStem + ".json"), SerializeJson(root, true), error); } bool RuntimeStore::LoadStackPresetSnapshot(const std::string& presetName, std::string& error) { std::lock_guard lock(mMutex); - const std::string safeStem = MakeSafePresetFileStem(presetName); + const std::string safeStem = LayerStackStore::MakeSafePresetFileStem(presetName); if (safeStem.empty()) { error = "Preset name must include at least one letter or number."; return false; } - const std::filesystem::path presetPath = mPresetRoot / (safeStem + ".json"); + const std::filesystem::path presetPath = mConfigStore.GetPresetRoot() / (safeStem + ".json"); std::string presetText = ReadTextFile(presetPath, error); if (presetText.empty()) return false; @@ -565,24 +295,9 @@ bool RuntimeStore::LoadStackPresetSnapshot(const std::string& presetName, std::s if (!ParseJson(presetText, root, error)) return false; - const JsonValue* layersValue = root.find("layers"); - if (!layersValue || !layersValue->isArray()) - { - error = "Preset file is missing a valid 'layers' array."; - return false; - } - - std::vector nextLayers; - if (!DeserializeLayerStackLocked(*layersValue, nextLayers, error)) + if (!mLayerStack.LoadStackPresetValue(mShaderCatalog, root, error)) return false; - if (nextLayers.empty()) - { - error = "Preset does not contain any valid layers."; - return false; - } - - mPersistentState.layers = nextLayers; mReloadRequested = true; MarkRenderStateDirtyLocked(); return SavePersistentState(error); @@ -591,118 +306,44 @@ bool RuntimeStore::LoadStackPresetSnapshot(const std::string& presetName, std::s bool RuntimeStore::HasStoredLayer(const std::string& layerId) const { std::lock_guard lock(mMutex); - return FindLayerById(layerId) != nullptr; + return mLayerStack.HasLayer(layerId); } bool RuntimeStore::HasStoredShader(const std::string& shaderId) const { std::lock_guard lock(mMutex); - return mPackagesById.find(shaderId) != mPackagesById.end(); + return mShaderCatalog.HasPackage(shaderId); } bool RuntimeStore::TryGetStoredParameterById(const std::string& layerId, const std::string& parameterId, StoredParameterSnapshot& snapshot, std::string& error) const { std::lock_guard lock(mMutex); - const LayerPersistentState* layer = FindLayerById(layerId); - if (!layer) - { - error = "Unknown layer id: " + layerId; - return false; - } - - auto shaderIt = mPackagesById.find(layer->shaderId); - if (shaderIt == mPackagesById.end()) - { - error = "Unknown shader id: " + layer->shaderId; - return false; - } - - auto parameterIt = std::find_if(shaderIt->second.parameters.begin(), shaderIt->second.parameters.end(), - [¶meterId](const ShaderParameterDefinition& definition) { return definition.id == parameterId; }); - if (parameterIt == shaderIt->second.parameters.end()) - { - error = "Unknown parameter id: " + parameterId; - return false; - } - - snapshot = StoredParameterSnapshot(); - snapshot.layerId = layer->id; - snapshot.definition = *parameterIt; - auto valueIt = layer->parameterValues.find(parameterIt->id); - if (valueIt != layer->parameterValues.end()) - { - snapshot.currentValue = valueIt->second; - snapshot.hasCurrentValue = true; - } - return true; + return mLayerStack.TryGetParameterById(mShaderCatalog, layerId, parameterId, snapshot, error); } bool RuntimeStore::TryGetStoredParameterByControlKey(const std::string& layerKey, const std::string& parameterKey, StoredParameterSnapshot& snapshot, std::string& error) const { std::lock_guard lock(mMutex); - const LayerPersistentState* layer = nullptr; - const ShaderPackage* shaderPackage = nullptr; - std::vector::const_iterator parameterIt; - if (!TryResolveStoredLayerAndParameterByControlKeyLocked(layerKey, parameterKey, layer, shaderPackage, parameterIt, error)) - return false; - - snapshot = StoredParameterSnapshot(); - snapshot.layerId = layer->id; - snapshot.definition = *parameterIt; - auto valueIt = layer->parameterValues.find(parameterIt->id); - if (valueIt != layer->parameterValues.end()) - { - snapshot.currentValue = valueIt->second; - snapshot.hasCurrentValue = true; - } - return true; + return mLayerStack.TryGetParameterByControlKey(mShaderCatalog, layerKey, parameterKey, snapshot, error); } bool RuntimeStore::ResolveStoredLayerMove(const std::string& layerId, int direction, bool& shouldMove, std::string& error) const { std::lock_guard lock(mMutex); - auto it = std::find_if(mPersistentState.layers.begin(), mPersistentState.layers.end(), - [&layerId](const RuntimeStore::LayerPersistentState& layer) { return layer.id == layerId; }); - if (it == mPersistentState.layers.end()) - { - error = "Unknown layer id: " + layerId; - return false; - } - - const std::ptrdiff_t index = std::distance(mPersistentState.layers.begin(), it); - const std::ptrdiff_t newIndex = index + direction; - shouldMove = newIndex >= 0 && newIndex < static_cast(mPersistentState.layers.size()) && newIndex != index; - return true; + return mLayerStack.ResolveLayerMove(layerId, direction, shouldMove, error); } bool RuntimeStore::ResolveStoredLayerMoveToIndex(const std::string& layerId, std::size_t targetIndex, bool& shouldMove, std::string& error) const { std::lock_guard lock(mMutex); - auto it = std::find_if(mPersistentState.layers.begin(), mPersistentState.layers.end(), - [&layerId](const RuntimeStore::LayerPersistentState& layer) { return layer.id == layerId; }); - if (it == mPersistentState.layers.end()) - { - error = "Unknown layer id: " + layerId; - return false; - } - - if (mPersistentState.layers.empty()) - { - shouldMove = false; - return true; - } - - const std::size_t clampedTargetIndex = (std::min)(targetIndex, mPersistentState.layers.size() - 1); - const std::size_t sourceIndex = static_cast(std::distance(mPersistentState.layers.begin(), it)); - shouldMove = sourceIndex != clampedTargetIndex; - return true; + return mLayerStack.ResolveLayerMoveToIndex(layerId, targetIndex, shouldMove, error); } bool RuntimeStore::IsValidStackPresetName(const std::string& presetName) const { - return !MakeSafePresetFileStem(presetName).empty(); + return !LayerStackStore::MakeSafePresetFileStem(presetName).empty(); } double RuntimeStore::GetRuntimeElapsedSeconds() const @@ -713,22 +354,22 @@ double RuntimeStore::GetRuntimeElapsedSeconds() const const std::filesystem::path& RuntimeStore::GetRuntimeRepositoryRoot() const { - return mRepoRoot; + return mConfigStore.GetRepoRoot(); } const std::filesystem::path& RuntimeStore::GetRuntimeUiRoot() const { - return mUiRoot; + return mConfigStore.GetUiRoot(); } const std::filesystem::path& RuntimeStore::GetRuntimeDocsRoot() const { - return mDocsRoot; + return mConfigStore.GetDocsRoot(); } const std::filesystem::path& RuntimeStore::GetRuntimeDataRoot() const { - return mRuntimeRoot; + return mConfigStore.GetRuntimeRoot(); } unsigned short RuntimeStore::GetConfiguredControlServerPort() const @@ -738,58 +379,59 @@ unsigned short RuntimeStore::GetConfiguredControlServerPort() const unsigned short RuntimeStore::GetConfiguredOscPort() const { - return mConfig.oscPort; + return mConfigStore.GetConfig().oscPort; } const std::string& RuntimeStore::GetConfiguredOscBindAddress() const { - return mConfig.oscBindAddress; + return mConfigStore.GetConfig().oscBindAddress; } double RuntimeStore::GetConfiguredOscSmoothing() const { - return mConfig.oscSmoothing; + return mConfigStore.GetConfig().oscSmoothing; } unsigned RuntimeStore::GetConfiguredMaxTemporalHistoryFrames() const { - return mConfig.maxTemporalHistoryFrames; + return mConfigStore.GetConfig().maxTemporalHistoryFrames; } unsigned RuntimeStore::GetConfiguredPreviewFps() const { - return mConfig.previewFps; + return mConfigStore.GetConfig().previewFps; } bool RuntimeStore::IsExternalKeyingConfigured() const { - return mConfig.enableExternalKeying; + return mConfigStore.GetConfig().enableExternalKeying; } const std::string& RuntimeStore::GetConfiguredInputVideoFormat() const { - return mConfig.inputVideoFormat; + return mConfigStore.GetConfig().inputVideoFormat; } const std::string& RuntimeStore::GetConfiguredInputFrameRate() const { - return mConfig.inputFrameRate; + return mConfigStore.GetConfig().inputFrameRate; } const std::string& RuntimeStore::GetConfiguredOutputVideoFormat() const { - return mConfig.outputVideoFormat; + return mConfigStore.GetConfig().outputVideoFormat; } const std::string& RuntimeStore::GetConfiguredOutputFrameRate() const { - return mConfig.outputFrameRate; + return mConfigStore.GetConfig().outputFrameRate; } void RuntimeStore::SetBoundControlServerPort(unsigned short port) { std::lock_guard lock(mMutex); mServerPort = port; + mConfigStore.SetBoundControlServerPort(port); } void RuntimeStore::SetCompileStatus(bool succeeded, const std::string& message) @@ -805,228 +447,12 @@ void RuntimeStore::ClearReloadRequest() mReloadRequested = false; } -JsonValue RuntimeStore::BuildRuntimeStateValue() const -{ - const HealthTelemetry::Snapshot telemetrySnapshot = mHealthTelemetry.GetSnapshot(); - std::lock_guard lock(mMutex); - - JsonValue root = JsonValue::MakeObject(); - - JsonValue app = JsonValue::MakeObject(); - app.set("serverPort", JsonValue(static_cast(mServerPort))); - app.set("oscPort", JsonValue(static_cast(mConfig.oscPort))); - app.set("oscBindAddress", JsonValue(mConfig.oscBindAddress)); - app.set("oscSmoothing", JsonValue(mConfig.oscSmoothing)); - app.set("autoReload", JsonValue(mAutoReloadEnabled)); - app.set("maxTemporalHistoryFrames", JsonValue(static_cast(mConfig.maxTemporalHistoryFrames))); - app.set("previewFps", JsonValue(static_cast(mConfig.previewFps))); - app.set("enableExternalKeying", JsonValue(mConfig.enableExternalKeying)); - app.set("inputVideoFormat", JsonValue(mConfig.inputVideoFormat)); - app.set("inputFrameRate", JsonValue(mConfig.inputFrameRate)); - app.set("outputVideoFormat", JsonValue(mConfig.outputVideoFormat)); - app.set("outputFrameRate", JsonValue(mConfig.outputFrameRate)); - root.set("app", app); - - JsonValue runtime = JsonValue::MakeObject(); - runtime.set("layerCount", JsonValue(static_cast(mPersistentState.layers.size()))); - runtime.set("compileSucceeded", JsonValue(mCompileSucceeded)); - runtime.set("compileMessage", JsonValue(mCompileMessage)); - root.set("runtime", runtime); - - JsonValue video = JsonValue::MakeObject(); - video.set("hasSignal", JsonValue(telemetrySnapshot.signal.hasSignal)); - video.set("width", JsonValue(static_cast(telemetrySnapshot.signal.width))); - video.set("height", JsonValue(static_cast(telemetrySnapshot.signal.height))); - video.set("modeName", JsonValue(telemetrySnapshot.signal.modeName)); - root.set("video", video); - - JsonValue deckLink = JsonValue::MakeObject(); - deckLink.set("modelName", JsonValue(telemetrySnapshot.videoIO.modelName)); - deckLink.set("supportsInternalKeying", JsonValue(telemetrySnapshot.videoIO.supportsInternalKeying)); - deckLink.set("supportsExternalKeying", JsonValue(telemetrySnapshot.videoIO.supportsExternalKeying)); - deckLink.set("keyerInterfaceAvailable", JsonValue(telemetrySnapshot.videoIO.keyerInterfaceAvailable)); - deckLink.set("externalKeyingRequested", JsonValue(telemetrySnapshot.videoIO.externalKeyingRequested)); - deckLink.set("externalKeyingActive", JsonValue(telemetrySnapshot.videoIO.externalKeyingActive)); - deckLink.set("statusMessage", JsonValue(telemetrySnapshot.videoIO.statusMessage)); - root.set("decklink", deckLink); - - JsonValue videoIO = JsonValue::MakeObject(); - videoIO.set("backend", JsonValue(telemetrySnapshot.videoIO.backendName)); - videoIO.set("modelName", JsonValue(telemetrySnapshot.videoIO.modelName)); - videoIO.set("supportsInternalKeying", JsonValue(telemetrySnapshot.videoIO.supportsInternalKeying)); - videoIO.set("supportsExternalKeying", JsonValue(telemetrySnapshot.videoIO.supportsExternalKeying)); - videoIO.set("keyerInterfaceAvailable", JsonValue(telemetrySnapshot.videoIO.keyerInterfaceAvailable)); - videoIO.set("externalKeyingRequested", JsonValue(telemetrySnapshot.videoIO.externalKeyingRequested)); - videoIO.set("externalKeyingActive", JsonValue(telemetrySnapshot.videoIO.externalKeyingActive)); - videoIO.set("statusMessage", JsonValue(telemetrySnapshot.videoIO.statusMessage)); - root.set("videoIO", videoIO); - - JsonValue performance = JsonValue::MakeObject(); - performance.set("frameBudgetMs", JsonValue(telemetrySnapshot.performance.frameBudgetMilliseconds)); - performance.set("renderMs", JsonValue(telemetrySnapshot.performance.renderMilliseconds)); - performance.set("smoothedRenderMs", JsonValue(telemetrySnapshot.performance.smoothedRenderMilliseconds)); - performance.set("budgetUsedPercent", JsonValue( - telemetrySnapshot.performance.frameBudgetMilliseconds > 0.0 - ? (telemetrySnapshot.performance.smoothedRenderMilliseconds / telemetrySnapshot.performance.frameBudgetMilliseconds) * 100.0 - : 0.0)); - performance.set("completionIntervalMs", JsonValue(telemetrySnapshot.performance.completionIntervalMilliseconds)); - performance.set("smoothedCompletionIntervalMs", JsonValue(telemetrySnapshot.performance.smoothedCompletionIntervalMilliseconds)); - performance.set("maxCompletionIntervalMs", JsonValue(telemetrySnapshot.performance.maxCompletionIntervalMilliseconds)); - performance.set("lateFrameCount", JsonValue(static_cast(telemetrySnapshot.performance.lateFrameCount))); - performance.set("droppedFrameCount", JsonValue(static_cast(telemetrySnapshot.performance.droppedFrameCount))); - performance.set("flushedFrameCount", JsonValue(static_cast(telemetrySnapshot.performance.flushedFrameCount))); - root.set("performance", performance); - - JsonValue shaderLibrary = JsonValue::MakeArray(); - for (const ShaderPackageStatus& status : mPackageStatuses) - { - JsonValue shader = JsonValue::MakeObject(); - shader.set("id", JsonValue(status.id)); - shader.set("name", JsonValue(status.displayName)); - shader.set("description", JsonValue(status.description)); - shader.set("category", JsonValue(status.category)); - shader.set("available", JsonValue(status.available)); - if (!status.available) - shader.set("error", JsonValue(status.error)); - - auto shaderIt = mPackagesById.find(status.id); - if (status.available && shaderIt != mPackagesById.end() && shaderIt->second.temporal.enabled) - { - JsonValue temporal = JsonValue::MakeObject(); - temporal.set("enabled", JsonValue(true)); - temporal.set("historySource", JsonValue(TemporalHistorySourceToString(shaderIt->second.temporal.historySource))); - temporal.set("requestedHistoryLength", JsonValue(static_cast(shaderIt->second.temporal.requestedHistoryLength))); - temporal.set("effectiveHistoryLength", JsonValue(static_cast(shaderIt->second.temporal.effectiveHistoryLength))); - shader.set("temporal", temporal); - } - if (status.available && shaderIt != mPackagesById.end() && shaderIt->second.feedback.enabled) - { - JsonValue feedback = JsonValue::MakeObject(); - feedback.set("enabled", JsonValue(true)); - feedback.set("writePass", JsonValue(shaderIt->second.feedback.writePassId)); - shader.set("feedback", feedback); - } - shaderLibrary.pushBack(shader); - } - root.set("shaders", shaderLibrary); - - JsonValue stackPresets = JsonValue::MakeArray(); - for (const std::string& presetName : GetStackPresetNamesLocked()) - stackPresets.pushBack(JsonValue(presetName)); - root.set("stackPresets", stackPresets); - - root.set("layers", SerializeLayerStack()); - return root; -} - -JsonValue RuntimeStore::SerializeLayerStack() const -{ - return SerializeLayerStackLocked(); -} - -bool RuntimeStore::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 RuntimeStore::LoadPersistentState(std::string& error) { - if (!std::filesystem::exists(mRuntimeStatePath)) + if (!std::filesystem::exists(mConfigStore.GetRuntimeStatePath())) return true; - std::string stateText = ReadTextFile(mRuntimeStatePath, error); + std::string stateText = ReadTextFile(mConfigStore.GetRuntimeStatePath(), error); if (stateText.empty()) return false; @@ -1034,147 +460,20 @@ bool RuntimeStore::LoadPersistentState(std::string& error) if (!ParseJson(stateText, root, error)) return false; - if (const JsonValue* layersValue = root.find("layers")) - { - for (const JsonValue& layerValue : layersValue->asArray()) - { - if (!layerValue.isObject()) - continue; - RuntimeStore::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 - { - std::string activeShaderId; - if (const JsonValue* activeShaderValue = root.find("activeShaderId")) - activeShaderId = activeShaderValue->asString(); - - if (!activeShaderId.empty()) - { - RuntimeStore::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; + return mLayerStack.LoadPersistentStateValue(root); } bool RuntimeStore::SavePersistentState(std::string& error) const { - JsonValue root = JsonValue::MakeObject(); - - JsonValue layers = JsonValue::MakeArray(); - for (const RuntimeStore::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); + return WriteTextFile(mConfigStore.GetRuntimeStatePath(), SerializeJson(mLayerStack.BuildPersistentStateValue(mShaderCatalog), true), error); } bool RuntimeStore::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)) + if (!mShaderCatalog.Scan(mConfigStore.GetShaderRoot(), mConfigStore.GetConfig().maxTemporalHistoryFrames, 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; - } + mLayerStack.RemoveLayersWithMissingPackages(mShaderCatalog); MarkRenderStateDirtyLocked(); return true; @@ -1226,41 +525,14 @@ bool RuntimeStore::WriteTextFile(const std::filesystem::path& path, const std::s return true; } -bool RuntimeStore::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; -} - std::vector RuntimeStore::GetStackPresetNamesLocked() const { std::vector presetNames; std::error_code fsError; - if (!std::filesystem::exists(mPresetRoot, fsError)) + if (!std::filesystem::exists(mConfigStore.GetPresetRoot(), fsError)) return presetNames; - for (const auto& entry : std::filesystem::directory_iterator(mPresetRoot, fsError)) + for (const auto& entry : std::filesystem::directory_iterator(mConfigStore.GetPresetRoot(), fsError)) { if (!entry.is_regular_file()) continue; @@ -1273,71 +545,22 @@ std::vector RuntimeStore::GetStackPresetNamesLocked() const return presetNames; } -std::string RuntimeStore::MakeSafePresetFileStem(const std::string& presetName) const -{ - return ::MakeSafePresetFileStem(presetName); -} - -bool RuntimeStore::TryResolveStoredLayerAndParameterByControlKeyLocked(const std::string& layerKey, const std::string& parameterKey, - const RuntimeStore::LayerPersistentState*& matchedLayer, const ShaderPackage*& matchedPackage, - std::vector::const_iterator& parameterIt, std::string& error) const -{ - matchedLayer = nullptr; - matchedPackage = nullptr; - - for (const RuntimeStore::LayerPersistentState& layer : mPersistentState.layers) - { - auto shaderIt = mPackagesById.find(layer.shaderId); - if (shaderIt == mPackagesById.end()) - continue; - - if (MatchesControlKey(layer.id, layerKey) || MatchesControlKey(shaderIt->second.id, layerKey) || - MatchesControlKey(shaderIt->second.displayName, layerKey)) - { - matchedLayer = &layer; - matchedPackage = &shaderIt->second; - break; - } - } - - if (!matchedLayer || !matchedPackage) - { - error = "Unknown OSC layer key: " + layerKey; - return false; - } - - parameterIt = std::find_if(matchedPackage->parameters.begin(), matchedPackage->parameters.end(), - [¶meterKey](const ShaderParameterDefinition& definition) - { - return MatchesControlKey(definition.id, parameterKey) || MatchesControlKey(definition.label, parameterKey); - }); - if (parameterIt == matchedPackage->parameters.end()) - { - error = "Unknown OSC parameter key: " + parameterKey; - return false; - } - - return true; -} - bool RuntimeStore::CopyShaderPackageForStoredLayer(const std::string& layerId, ShaderPackage& shaderPackage, std::string& error) const { std::lock_guard lock(mMutex); - const RuntimeStore::LayerPersistentState* layer = FindLayerById(layerId); + const RuntimeStore::LayerPersistentState* layer = mLayerStack.FindLayerById(layerId); if (!layer) { error = "Unknown layer id: " + layerId; return false; } - auto it = mPackagesById.find(layer->shaderId); - if (it == mPackagesById.end()) + if (!mShaderCatalog.CopyPackage(layer->shaderId, shaderPackage)) { error = "Unknown shader id: " + layer->shaderId; return false; } - shaderPackage = it->second; return true; } @@ -1345,480 +568,19 @@ void RuntimeStore::GetShaderCompilerInputs(std::filesystem::path& repoRoot, std: std::filesystem::path& generatedGlslPath, std::filesystem::path& patchedGlslPath, unsigned& maxTemporalHistoryFrames) const { std::lock_guard lock(mMutex); - repoRoot = mRepoRoot; - wrapperPath = mWrapperPath; - generatedGlslPath = mGeneratedGlslPath; - patchedGlslPath = mPatchedGlslPath; - maxTemporalHistoryFrames = mConfig.maxTemporalHistoryFrames; -} - -uint64_t RuntimeStore::GetRenderStateVersion() const -{ - return mRenderStateVersion.load(std::memory_order_relaxed); -} - -uint64_t RuntimeStore::GetParameterStateVersion() const -{ - return mParameterStateVersion.load(std::memory_order_relaxed); -} - -void RuntimeStore::AdvanceFrameCounter() -{ - ++mFrameCounter; -} - -void RuntimeStore::BuildLayerRenderStates(unsigned outputWidth, unsigned outputHeight, std::vector& states) const -{ - std::lock_guard lock(mMutex); - BuildLayerRenderStatesLocked(outputWidth, outputHeight, states); -} - -bool RuntimeStore::TryBuildLayerRenderStates(unsigned outputWidth, unsigned outputHeight, std::vector& states) const -{ - std::unique_lock lock(mMutex, std::try_to_lock); - if (!lock.owns_lock()) - return false; - - BuildLayerRenderStatesLocked(outputWidth, outputHeight, states); - return true; -} - -bool RuntimeStore::TryRefreshLayerParameters(std::vector& states) const -{ - std::unique_lock lock(mMutex, std::try_to_lock); - if (!lock.owns_lock()) - return false; - - RefreshLayerParametersLocked(states); - return true; -} - -void RuntimeStore::RefreshDynamicRenderStateFields(std::vector& states) const -{ - std::lock_guard lock(mMutex); - RefreshDynamicRenderStateFieldsLocked(states); -} - -void RuntimeStore::BuildLayerRenderStatesLocked(unsigned outputWidth, unsigned outputHeight, std::vector& states) const -{ - states.clear(); - const HealthTelemetry::SignalStatusSnapshot signalStatus = mHealthTelemetry.GetSignalStatusSnapshot(); - - for (const RuntimeStore::LayerPersistentState& layer : mPersistentState.layers) - { - auto shaderIt = mPackagesById.find(layer.shaderId); - if (shaderIt == mPackagesById.end()) - continue; - - RuntimeRenderState state; - state.layerId = layer.id; - state.shaderId = layer.shaderId; - state.shaderName = shaderIt->second.displayName; - state.mixAmount = 1.0; - state.bypass = layer.bypass ? 1.0 : 0.0; - state.inputWidth = signalStatus.width; - state.inputHeight = signalStatus.height; - state.outputWidth = outputWidth; - state.outputHeight = outputHeight; - state.parameterDefinitions = shaderIt->second.parameters; - state.textureAssets = shaderIt->second.textureAssets; - state.fontAssets = shaderIt->second.fontAssets; - state.isTemporal = shaderIt->second.temporal.enabled; - state.temporalHistorySource = shaderIt->second.temporal.historySource; - state.requestedTemporalHistoryLength = shaderIt->second.temporal.requestedHistoryLength; - state.effectiveTemporalHistoryLength = shaderIt->second.temporal.effectiveHistoryLength; - state.feedback = shaderIt->second.feedback; - - for (const ShaderParameterDefinition& definition : shaderIt->second.parameters) - { - ShaderParameterValue value = DefaultValueForDefinition(definition); - auto valueIt = layer.parameterValues.find(definition.id); - if (valueIt != layer.parameterValues.end()) - value = valueIt->second; - state.parameterValues[definition.id] = value; - } - - states.push_back(state); - } - - RefreshDynamicRenderStateFieldsLocked(states); -} - -void RuntimeStore::RefreshLayerParametersLocked(std::vector& states) const -{ - for (RuntimeRenderState& state : states) - { - const auto layerIt = std::find_if(mPersistentState.layers.begin(), mPersistentState.layers.end(), - [&state](const RuntimeStore::LayerPersistentState& layer) { return layer.id == state.layerId; }); - if (layerIt == mPersistentState.layers.end()) - continue; - - state.bypass = layerIt->bypass ? 1.0 : 0.0; - state.parameterValues.clear(); - for (const ShaderParameterDefinition& definition : state.parameterDefinitions) - { - ShaderParameterValue value = DefaultValueForDefinition(definition); - auto valueIt = layerIt->parameterValues.find(definition.id); - if (valueIt != layerIt->parameterValues.end()) - value = valueIt->second; - state.parameterValues[definition.id] = value; - } - } -} - -void RuntimeStore::RefreshDynamicRenderStateFieldsLocked(std::vector& states) const -{ - const RuntimeClockSnapshot clock = GetRuntimeClockSnapshot(); - const double timeSeconds = std::chrono::duration_cast>(std::chrono::steady_clock::now() - mStartTime).count(); - const double frameCount = static_cast(mFrameCounter.load(std::memory_order_relaxed)); - - for (RuntimeRenderState& state : states) - { - state.timeSeconds = timeSeconds; - state.utcTimeSeconds = clock.utcTimeSeconds; - state.utcOffsetSeconds = clock.utcOffsetSeconds; - state.startupRandom = mStartupRandom; - state.frameCount = frameCount; - } + repoRoot = mConfigStore.GetRepoRoot(); + wrapperPath = mConfigStore.GetWrapperPath(); + generatedGlslPath = mConfigStore.GetGeneratedGlslPath(); + patchedGlslPath = mConfigStore.GetPatchedGlslPath(); + maxTemporalHistoryFrames = mConfigStore.GetConfig().maxTemporalHistoryFrames; } void RuntimeStore::MarkRenderStateDirtyLocked() { - mRenderStateVersion.fetch_add(1, std::memory_order_relaxed); - mParameterStateVersion.fetch_add(1, std::memory_order_relaxed); + mRenderSnapshotBuilder.MarkRenderStateDirty(); } void RuntimeStore::MarkParameterStateDirtyLocked() { - mParameterStateVersion.fetch_add(1, std::memory_order_relaxed); -} - -bool RuntimeStore::NormalizeAndValidateValue(const ShaderParameterDefinition& definition, const JsonValue& value, ShaderParameterValue& normalizedValue, std::string& error) const -{ - return NormalizeAndValidateParameterValue(definition, value, normalizedValue, error); -} - -ShaderParameterValue RuntimeStore::DefaultValueForDefinition(const ShaderParameterDefinition& definition) const -{ - return ::DefaultValueForDefinition(definition); -} - -void RuntimeStore::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 RuntimeStore::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 RuntimeStore::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 RuntimeStore::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 RuntimeStore::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 RuntimeStore::TemporalHistorySourceToString(TemporalHistorySource source) const -{ - switch (source) - { - case TemporalHistorySource::Source: - return "source"; - case TemporalHistorySource::PreLayerInput: - return "preLayerInput"; - case TemporalHistorySource::None: - default: - return "none"; - } -} - -RuntimeStore::LayerPersistentState* RuntimeStore::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 RuntimeStore::LayerPersistentState* RuntimeStore::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 RuntimeStore::GenerateLayerId() -{ - while (true) - { - ++mNextLayerId; - const std::string candidate = "layer-" + std::to_string(mNextLayerId); - if (!FindLayerById(candidate)) - return candidate; - } + mRenderSnapshotBuilder.MarkParameterStateDirty(); } diff --git a/apps/LoopThroughWithOpenGLCompositing/runtime/RuntimeStore.h b/apps/LoopThroughWithOpenGLCompositing/runtime/RuntimeStore.h index 6945304..d023c07 100644 --- a/apps/LoopThroughWithOpenGLCompositing/runtime/RuntimeStore.h +++ b/apps/LoopThroughWithOpenGLCompositing/runtime/RuntimeStore.h @@ -1,64 +1,32 @@ #pragma once #include "HealthTelemetry.h" +#include "LayerStackStore.h" +#include "RenderSnapshotBuilder.h" +#include "RuntimeConfigStore.h" #include "RuntimeJson.h" +#include "ShaderPackageCatalog.h" #include "ShaderTypes.h" -#include #include -#include #include -#include #include #include #include -class RuntimeSnapshotProvider; +class RuntimeStatePresenter; class RuntimeStore { public: - struct StoredParameterSnapshot - { - std::string layerId; - ShaderParameterDefinition definition; - ShaderParameterValue currentValue; - bool hasCurrentValue = false; - }; - - 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; - }; + using StoredParameterSnapshot = LayerStackStore::StoredParameterSnapshot; + using LayerPersistentState = LayerStackStore::LayerPersistentState; RuntimeStore(); HealthTelemetry& GetHealthTelemetry(); const HealthTelemetry& GetHealthTelemetry() const; + RenderSnapshotBuilder& GetRenderSnapshotBuilder(); + const RenderSnapshotBuilder& GetRenderSnapshotBuilder() const; bool InitializeStore(std::string& error); std::string BuildPersistentStateJson() const; @@ -104,66 +72,26 @@ public: void ClearReloadRequest(); private: - friend class RuntimeSnapshotProvider; - bool LoadConfig(std::string& error); + 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; - bool ResolvePaths(std::string& error); std::vector GetStackPresetNamesLocked() const; - std::string MakeSafePresetFileStem(const std::string& presetName) const; - bool TryResolveStoredLayerAndParameterByControlKeyLocked(const std::string& layerKey, const std::string& parameterKey, - const LayerPersistentState*& matchedLayer, const ShaderPackage*& matchedPackage, - std::vector::const_iterator& parameterIt, std::string& error) 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; - uint64_t GetRenderStateVersion() const; - uint64_t GetParameterStateVersion() const; - void AdvanceFrameCounter(); - void BuildLayerRenderStates(unsigned outputWidth, unsigned outputHeight, std::vector& states) const; - bool TryBuildLayerRenderStates(unsigned outputWidth, unsigned outputHeight, std::vector& states) const; - bool TryRefreshLayerParameters(std::vector& states) const; - void RefreshDynamicRenderStateFields(std::vector& states) const; - void BuildLayerRenderStatesLocked(unsigned outputWidth, unsigned outputHeight, std::vector& states) const; - void RefreshLayerParametersLocked(std::vector& states) const; - void RefreshDynamicRenderStateFieldsLocked(std::vector& states) const; - JsonValue BuildRuntimeStateValue() const; - JsonValue SerializeLayerStack() const; - 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(); + RenderSnapshotBuilder mRenderSnapshotBuilder; + RuntimeConfigStore mConfigStore; + ShaderPackageCatalog mShaderCatalog; + LayerStackStore mLayerStack; 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; @@ -172,8 +100,4 @@ private: 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/ShaderPackageCatalog.cpp b/apps/LoopThroughWithOpenGLCompositing/runtime/ShaderPackageCatalog.cpp new file mode 100644 index 0000000..b7f3153 --- /dev/null +++ b/apps/LoopThroughWithOpenGLCompositing/runtime/ShaderPackageCatalog.cpp @@ -0,0 +1,127 @@ +#include "ShaderPackageCatalog.h" + +#include "ShaderPackageRegistry.h" + +bool ShaderPackageCatalog::Scan(const std::filesystem::path& shaderRoot, unsigned maxTemporalHistoryFrames, std::string& error) +{ + std::map packagesById; + std::vector packageOrder; + std::vector packageStatuses; + + ShaderPackageRegistry registry(maxTemporalHistoryFrames); + if (!registry.Scan(shaderRoot, packagesById, packageOrder, packageStatuses, error)) + return false; + + mPackagesById.swap(packagesById); + mPackageOrder.swap(packageOrder); + mPackageStatuses.swap(packageStatuses); + return true; +} + +ShaderPackageCatalog::Snapshot ShaderPackageCatalog::CaptureSnapshot() const +{ + Snapshot snapshot; + snapshot.packagesById = mPackagesById; + snapshot.packageOrder = mPackageOrder; + return snapshot; +} + +bool ShaderPackageCatalog::HasCatalogChangedSince(const Snapshot& snapshot) const +{ + if (snapshot.packageOrder != mPackageOrder || snapshot.packagesById.size() != mPackagesById.size()) + return true; + + for (const auto& item : mPackagesById) + { + auto previous = snapshot.packagesById.find(item.first); + if (previous == snapshot.packagesById.end() || !PackagesEquivalent(previous->second, item.second)) + return true; + } + + return false; +} + +bool ShaderPackageCatalog::HasPackageChangedSince(const Snapshot& snapshot, const std::string& shaderId) const +{ + auto previous = snapshot.packagesById.find(shaderId); + auto current = mPackagesById.find(shaderId); + if (previous == snapshot.packagesById.end() || current == mPackagesById.end()) + return previous != snapshot.packagesById.end() || current != mPackagesById.end(); + + return !PackagesEquivalent(previous->second, current->second); +} + +bool ShaderPackageCatalog::HasPackage(const std::string& shaderId) const +{ + return mPackagesById.find(shaderId) != mPackagesById.end(); +} + +const ShaderPackage* ShaderPackageCatalog::FindPackage(const std::string& shaderId) const +{ + auto it = mPackagesById.find(shaderId); + return it == mPackagesById.end() ? nullptr : &it->second; +} + +bool ShaderPackageCatalog::CopyPackage(const std::string& shaderId, ShaderPackage& shaderPackage) const +{ + const ShaderPackage* package = FindPackage(shaderId); + if (!package) + return false; + + shaderPackage = *package; + return true; +} + +const std::vector& ShaderPackageCatalog::PackageOrder() const +{ + return mPackageOrder; +} + +const std::vector& ShaderPackageCatalog::PackageStatuses() const +{ + return mPackageStatuses; +} + +bool ShaderPackageCatalog::PackagesEquivalent(const ShaderPackage& left, const ShaderPackage& right) +{ + return left.shaderWriteTime == right.shaderWriteTime && + left.manifestWriteTime == right.manifestWriteTime && + TextureAssetsEqual(left.textureAssets, right.textureAssets) && + FontAssetsEqual(left.fontAssets, right.fontAssets); +} + +bool ShaderPackageCatalog::TextureAssetsEqual(const std::vector& left, const std::vector& right) +{ + if (left.size() != right.size()) + return false; + + for (std::size_t index = 0; index < left.size(); ++index) + { + if (left[index].id != right[index].id || + left[index].path != right[index].path || + left[index].writeTime != right[index].writeTime) + { + return false; + } + } + + return true; +} + +bool ShaderPackageCatalog::FontAssetsEqual(const std::vector& left, const std::vector& right) +{ + if (left.size() != right.size()) + return false; + + for (std::size_t index = 0; index < left.size(); ++index) + { + if (left[index].id != right[index].id || + left[index].path != right[index].path || + left[index].writeTime != right[index].writeTime) + { + return false; + } + } + + return true; +} diff --git a/apps/LoopThroughWithOpenGLCompositing/runtime/ShaderPackageCatalog.h b/apps/LoopThroughWithOpenGLCompositing/runtime/ShaderPackageCatalog.h new file mode 100644 index 0000000..d2cfade --- /dev/null +++ b/apps/LoopThroughWithOpenGLCompositing/runtime/ShaderPackageCatalog.h @@ -0,0 +1,37 @@ +#pragma once + +#include "ShaderTypes.h" + +#include +#include +#include +#include + +class ShaderPackageCatalog +{ +public: + struct Snapshot + { + std::map packagesById; + std::vector packageOrder; + }; + + bool Scan(const std::filesystem::path& shaderRoot, unsigned maxTemporalHistoryFrames, std::string& error); + Snapshot CaptureSnapshot() const; + bool HasCatalogChangedSince(const Snapshot& snapshot) const; + bool HasPackageChangedSince(const Snapshot& snapshot, const std::string& shaderId) const; + bool HasPackage(const std::string& shaderId) const; + const ShaderPackage* FindPackage(const std::string& shaderId) const; + bool CopyPackage(const std::string& shaderId, ShaderPackage& shaderPackage) const; + const std::vector& PackageOrder() const; + const std::vector& PackageStatuses() const; + +private: + static bool PackagesEquivalent(const ShaderPackage& left, const ShaderPackage& right); + static bool TextureAssetsEqual(const std::vector& left, const std::vector& right); + static bool FontAssetsEqual(const std::vector& left, const std::vector& right); + + std::map mPackagesById; + std::vector mPackageOrder; + std::vector mPackageStatuses; +}; diff --git a/docs/PHASE_1_SUBSYSTEM_BOUNDARIES_DESIGN.md b/docs/PHASE_1_SUBSYSTEM_BOUNDARIES_DESIGN.md index ca8d4a5..5c47fac 100644 --- a/docs/PHASE_1_SUBSYSTEM_BOUNDARIES_DESIGN.md +++ b/docs/PHASE_1_SUBSYSTEM_BOUNDARIES_DESIGN.md @@ -102,12 +102,21 @@ The codebase now has an initial Phase 1 compatibility split in place: - `RuntimeStore` - [RuntimeStore.h](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/runtime/RuntimeStore.h) - [RuntimeStore.cpp](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/runtime/RuntimeStore.cpp) +- `RuntimeConfigStore` + - [RuntimeConfigStore.h](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/runtime/RuntimeConfigStore.h) + - [RuntimeConfigStore.cpp](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/runtime/RuntimeConfigStore.cpp) +- `ShaderPackageCatalog` + - [ShaderPackageCatalog.h](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/runtime/ShaderPackageCatalog.h) + - [ShaderPackageCatalog.cpp](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/runtime/ShaderPackageCatalog.cpp) - `RuntimeCoordinator` - [RuntimeCoordinator.h](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/runtime/RuntimeCoordinator.h) - [RuntimeCoordinator.cpp](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/runtime/RuntimeCoordinator.cpp) - `RuntimeSnapshotProvider` - [RuntimeSnapshotProvider.h](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/runtime/RuntimeSnapshotProvider.h) - [RuntimeSnapshotProvider.cpp](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/runtime/RuntimeSnapshotProvider.cpp) +- `RenderSnapshotBuilder` + - [RenderSnapshotBuilder.h](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/runtime/RenderSnapshotBuilder.h) + - [RenderSnapshotBuilder.cpp](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/runtime/RenderSnapshotBuilder.cpp) - `ControlServices` - [ControlServices.h](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/control/ControlServices.h) - [ControlServices.cpp](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/control/ControlServices.cpp) @@ -124,15 +133,15 @@ The codebase now has an initial Phase 1 compatibility split in place: These are still compatibility seams, not a completed subsystem extraction. Some responsibilities have moved behind the new boundaries, while other paths still delegate through compatibility helpers, `OpenGLComposite`, `DeckLinkSession`, and the existing bridge/pipeline classes. Their purpose is to give later work real code boundaries that can be expanded without first inventing the names: - UI/runtime control calls in `OpenGLCompositeRuntimeControls.cpp` now route through `RuntimeCoordinator` -- runtime startup for path resolution, config load, persistent state load, and shader package scan now initializes through `RuntimeStore` -- runtime/UI state JSON composition now lives in `RuntimeStore` instead of `RuntimeHost` -- regular stored layer mutations and stack preset save/load now live in `RuntimeStore` instead of `RuntimeHost` public APIs +- runtime startup now initializes path resolution and config loading through `RuntimeConfigStore`, with shader package scan and lookup delegated to `ShaderPackageCatalog` +- runtime/UI state JSON composition now routes through `RuntimeStatePresenter` and `RuntimeStateJson` instead of living in `RuntimeHost` or `RuntimeStore` +- regular stored layer mutations and stack preset save/load now route through `RuntimeStore` into `LayerStackStore` instead of `RuntimeHost` public APIs - persisted OSC-by-control-key commits now route through `RuntimeCoordinator` before applying store changes - mutation and reload policy now routes through `RuntimeCoordinator` - parameter target resolution, value normalization, trigger classification, and move no-op classification now live under `RuntimeCoordinator` - render-state and shader-build reads in `OpenGLComposite.cpp`, `OpenGLShaderPrograms.cpp`, and `ShaderBuildQueue.cpp` now route through `RuntimeSnapshotProvider` -- `RuntimeSnapshotProvider` now depends on `RuntimeStore` rather than sharing `RuntimeHost` directly -- render-state assembly, cached parameter refresh, and frame-context application now flow through `RuntimeSnapshotProvider` and store-owned snapshot helpers instead of `RuntimeHost` public APIs +- `RuntimeSnapshotProvider` now depends on `RenderSnapshotBuilder` rather than on `RuntimeStore` friendship or shared `RuntimeHost` access +- render-state assembly, cached parameter refresh, dynamic frame-field application, and render snapshot versions now live in `RenderSnapshotBuilder` instead of `RuntimeStore` - `RuntimeSnapshotProvider` now publishes versioned render snapshot objects and serves matching consumers from the last published snapshot - service ingress and polling coordination now route through `ControlServices` - `ControlServices` now queues coordinator results for OSC commit and file-poll outcomes instead of directly deciding runtime/store policy @@ -140,6 +149,10 @@ These are still compatibility seams, not a completed subsystem extraction. Some - `HealthTelemetry` now owns the live signal, video-I/O, and performance snapshots directly instead of `RuntimeHost` keeping those backing fields - render-side frame advancement and render-performance reporting now flow through `RuntimeSnapshotProvider` and `HealthTelemetry` instead of directly through `RuntimeHost` - `RuntimeStore` now owns its durable/session backing fields directly instead of wrapping a compatibility `RuntimeHost` object +- `RuntimeConfigStore` now owns runtime config parsing, path resolution, configured ports/formats, runtime roots, and shader compiler paths instead of leaving those responsibilities inside `RuntimeStore` +- `ShaderPackageCatalog` now owns shader package scanning, package status/order/lookup, and package asset/source change comparison instead of leaving those responsibilities inside `RuntimeStore` +- `LayerStackStore` now owns durable layer state, layer CRUD/reorder, parameter persistence, and stack preset value serialization/load instead of leaving those responsibilities inside `RuntimeStore` +- `RuntimeStatePresenter` and `RuntimeStateJson` now own runtime-state JSON assembly and layer-stack presentation serialization instead of leaving those responsibilities inside storage classes - `RuntimeCoordinator` now uses explicit `RuntimeStore` query APIs/read models instead of friendship or direct store-internal access - live OSC overlay state and smoothing/commit decisions now live under `RenderEngine` instead of `OpenGLComposite` - coordinator result application, shader-build requests, ready-build application, and runtime-state broadcasts now route through `RuntimeUpdateController` instead of being interpreted directly by `OpenGLComposite` @@ -346,7 +359,8 @@ Allowed dependency directions: - `RuntimeCoordinator -> RuntimeStore` - `RuntimeCoordinator -> RuntimeSnapshotProvider` - `RuntimeCoordinator -> HealthTelemetry` -- `RuntimeSnapshotProvider -> RuntimeStore` +- `RuntimeSnapshotProvider -> RenderSnapshotBuilder` +- `RenderSnapshotBuilder -> RuntimeStore` - `RenderEngine -> RuntimeSnapshotProvider` - `RenderEngine -> HealthTelemetry` - `VideoBackend -> RenderEngine` @@ -574,7 +588,8 @@ Likely examples: - config loading/saving -> `RuntimeStore` - layer stack mutation validation -> `RuntimeCoordinator` -- render state building/versioning -> `RuntimeSnapshotProvider` +- render state building/versioning -> `RenderSnapshotBuilder` +- render snapshot publication/cache -> `RuntimeSnapshotProvider` - timing/status setters -> `HealthTelemetry` ### Current `RuntimeServices` diff --git a/docs/subsystems/README.md b/docs/subsystems/README.md index 1d41ce7..00fb601 100644 --- a/docs/subsystems/README.md +++ b/docs/subsystems/README.md @@ -42,11 +42,11 @@ That order mirrors the intended dependency story: ## Subsystem Notes - [RuntimeStore.md](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/docs/subsystems/RuntimeStore.md) - Durable runtime config, persisted layer state, presets, and package metadata ownership. + Durable runtime-state facade over layer-stack, config, package-catalog, presentation, and persistence boundaries. - [RuntimeCoordinator.md](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/docs/subsystems/RuntimeCoordinator.md) Mutation validation, state classification, reset/reload policy, and publication/persistence requests. - [RuntimeSnapshotProvider.md](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/docs/subsystems/RuntimeSnapshotProvider.md) - Render-facing snapshot build, publication, and versioning boundaries. + Render-facing snapshot publication boundary backed by explicit render snapshot building/versioning. - [ControlServices.md](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/docs/subsystems/ControlServices.md) OSC, HTTP/WebSocket, and file-watch ingress plus normalization and service-local buffering. - [RenderEngine.md](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/docs/subsystems/RenderEngine.md) diff --git a/docs/subsystems/RuntimeSnapshotProvider.md b/docs/subsystems/RuntimeSnapshotProvider.md index b32e46e..e39469f 100644 --- a/docs/subsystems/RuntimeSnapshotProvider.md +++ b/docs/subsystems/RuntimeSnapshotProvider.md @@ -12,7 +12,7 @@ It exists to solve three current problems: - render state is still built directly out of `RuntimeHost` under a shared mutex - render reads and refreshes partially mutable cached layer state in more than one place -- state publication, state versioning, and dynamic frame-field refresh are not yet explicit subsystems +- state publication, state versioning, and dynamic frame-field refresh need explicit ownership Today the closest current behavior lives in: @@ -29,13 +29,17 @@ Today the closest current behavior lives in: `RuntimeSnapshotProvider` is responsible for: -- building render-facing snapshots from durable store state plus whatever committed-live state view the Phase 3 split ultimately exposes - publishing stable, versioned snapshots that can be consumed without large shared mutable locks +- giving `RenderEngine` a cheap read path for the latest committed snapshot +- making snapshot invalidation and publication rules explicit + +`RenderSnapshotBuilder` is responsible for: + +- building render-facing snapshots from durable store state plus whatever committed-live state view the Phase 3 split ultimately exposes - separating structural snapshot changes from dynamic frame fields - translating runtime layer state into render-ready layer descriptors - attaching immutable or near-immutable shader/package-derived data needed by render -- giving `RenderEngine` a cheap read path for the latest committed snapshot -- making snapshot invalidation and publication rules explicit +- maintaining render snapshot version counters and frame advancement It is not responsible for: @@ -75,7 +79,7 @@ The shape of render-facing layer state should remain consistent across phases ev ## Snapshot Inputs -`RuntimeSnapshotProvider` should build from a read-oriented runtime view, not from direct mutation calls. +`RenderSnapshotBuilder` should build from a read-oriented runtime view, not from direct mutation calls. `RuntimeSnapshotProvider` should consume the builder's output and own publication/cache behavior. That view will likely include: @@ -87,7 +91,8 @@ That view will likely include: The important Phase 1 rule is not "the provider always reads one specific object." It is: -- the provider consumes read-oriented committed runtime state +- the builder consumes read-oriented committed runtime state +- the provider consumes builder-published render snapshot data - the provider does not own mutation policy - render consumes the provider's published output instead of reaching back into whichever runtime object currently stores the truth @@ -292,11 +297,11 @@ Notes: ### `RuntimeStore` -`RuntimeSnapshotProvider` depends on store-owned durable data and package metadata through a read-oriented interface or view. +`RenderSnapshotBuilder` depends on store-owned durable data and package metadata through a read-oriented interface or view. `RuntimeSnapshotProvider` depends on the builder rather than reaching into store internals directly. -If committed live state remains physically co-located with the store during early migration, the provider may read it through the same view. If committed live state moves behind a coordinator-owned live-session model later, the provider should consume that through a similarly read-oriented view. +If committed live state remains physically co-located with the store during early migration, the builder may read it through the same view. If committed live state moves behind a coordinator-owned live-session model later, the builder should consume that through a similarly read-oriented view. -It should not mutate the store directly. +Neither the builder nor provider should mutate the store directly. ### `RuntimeCoordinator` @@ -337,7 +342,7 @@ This is especially important while migrating away from the current lock/fallback The current code suggests the following migration map. -### Move into `RuntimeSnapshotProvider` +### Move into `RenderSnapshotBuilder` From `RuntimeHost`: @@ -346,6 +351,12 @@ From `RuntimeHost`: - explicit version composition for render-visible state - dynamic frame-context construction currently done in `RefreshDynamicRenderStateFields(...)` +### Move into `RuntimeSnapshotProvider` + +- published snapshot cache ownership +- version matching for already-published snapshots +- render-facing compatibility API while render callers migrate + ### Stop exposing directly from the host/store boundary Current methods that should become compatibility shims and later disappear: @@ -357,9 +368,9 @@ Current methods that should become compatibility shims and later disappear: ### Render-side compatibility during migration -The current `OpenGLComposite` cache path: +The previous `OpenGLComposite` cache path: -- reads versions from `RuntimeHost` +- reads versions from `RuntimeHost`/store-owned counters - conditionally calls `TryRefreshCachedLayerStates(...)` - conditionally rebuilds full layer state - then reapplies render-local OSC overlay state @@ -367,8 +378,8 @@ The current `OpenGLComposite` cache path: During migration, that should become: 1. get latest published snapshot from provider -2. compare snapshot versions against render-local cache -3. rebuild only if needed +2. compare snapshot versions produced by `RenderSnapshotBuilder` +3. rebuild through `RenderSnapshotBuilder` only if needed 4. apply render-local overlay state 5. attach frame context diff --git a/docs/subsystems/RuntimeStore.md b/docs/subsystems/RuntimeStore.md index 9f65373..d8a8f82 100644 --- a/docs/subsystems/RuntimeStore.md +++ b/docs/subsystems/RuntimeStore.md @@ -276,7 +276,7 @@ Per the Phase 1 subsystem design, `RuntimeStore` should sit low in the dependenc Allowed inbound dependencies: - `RuntimeCoordinator -> RuntimeStore` -- `RuntimeSnapshotProvider -> RuntimeStore` +- `RenderSnapshotBuilder -> RuntimeStore` - temporary migration shims from `ControlServices` only where explicitly tolerated Allowed outbound dependencies: @@ -359,16 +359,33 @@ Those belong under other target subsystems. `RuntimeStore` does not need to be one monolithic class forever. A practical internal shape would be: - `RuntimeConfigStore` - - runtime host config load/save and resolved paths -- `PersistentLayerStore` + - runtime host config load and resolved paths + +The current codebase has begun this split: `RuntimeConfigStore` owns config parsing, path resolution, configured ports/formats, runtime roots, and shader compiler paths, while `RuntimeStore` keeps compatibility delegates for existing callers. +- `LayerStackStore` - durable layer stack and parameter values -- `StackPresetStore` - - preset enumeration/load/save -- `ShaderPackageCatalogStore` + - layer CRUD/reorder and shader selection + - stack preset value serialization/load +- `RuntimeStatePresenter` / `RuntimeStateJson` + - runtime-state JSON assembly + - layer-stack presentation serialization +- `RenderSnapshotBuilder` + - render-state assembly and parameter refresh + - dynamic frame-field refresh and render snapshot version counters +- `ShaderPackageCatalog` - durable manifest/package metadata + - shader package scanning, status/order/lookup, and asset/source change comparison - `PersistenceWriter` helper - synchronous at first, async/debounced later +The current codebase has begun the layer split: `LayerStackStore` owns durable layer state, layer CRUD/reorder, parameter persistence, and stack preset value serialization/load. `RuntimeStore` still owns locking, file IO, and compatibility delegates for existing callers. + +The current codebase has begun the render snapshot split: `RenderSnapshotBuilder` owns render-state assembly, cached parameter refresh, dynamic frame-field refresh, and render snapshot versions. `RuntimeSnapshotProvider` depends on this builder rather than on `RuntimeStore` friendship. + +The current codebase has also begun the presentation split: `RuntimeStatePresenter` owns top-level runtime-state JSON assembly, while `RuntimeStateJson` owns the layer-stack and parameter presentation shape used by runtime state clients. + +The current codebase has also begun the package split: `ShaderPackageCatalog` owns package scanning and registry comparison, while `RuntimeStore` uses it to keep layer state valid and to build compatibility read models. + These can still be presented through one subsystem façade during migration. ## Persistence Model