Pass 3
Some checks failed
CI / React UI Build (push) Successful in 11s
CI / Native Windows Build And Tests (push) Successful in 2m48s
CI / Windows Release Package (push) Has been cancelled

This commit is contained in:
Aiden
2026-05-11 02:06:17 +10:00
parent b2369c418b
commit 5cbdbd6813
22 changed files with 2069 additions and 1504 deletions

View File

@@ -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"

View File

@@ -36,7 +36,7 @@ OpenGLComposite::OpenGLComposite(HWND hWnd, HDC hDC, HGLRC hRC) :
{
InitializeCriticalSection(&pMutex);
mRuntimeStore = std::make_unique<RuntimeStore>();
mRuntimeSnapshotProvider = std::make_unique<RuntimeSnapshotProvider>(*mRuntimeStore);
mRuntimeSnapshotProvider = std::make_unique<RuntimeSnapshotProvider>(mRuntimeStore->GetRenderSnapshotBuilder());
mRuntimeCoordinator = std::make_unique<RuntimeCoordinator>(*mRuntimeStore);
mRenderEngine = std::make_unique<RenderEngine>(
*mRuntimeSnapshotProvider,

View File

@@ -0,0 +1,738 @@
#include "LayerStackStore.h"
#include "RuntimeParameterUtils.h"
#include "RuntimeStateJson.h"
#include <algorithm>
#include <cctype>
#include <cmath>
#include <set>
#include <sstream>
#include <utility>
namespace
{
std::string TrimCopy(const std::string& text)
{
std::size_t start = 0;
while (start < text.size() && std::isspace(static_cast<unsigned char>(text[start])))
++start;
std::size_t end = text.size();
while (end > start && std::isspace(static_cast<unsigned char>(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<char>(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<unsigned char>(layerId[index]);
if (!std::isdigit(ch))
return false;
parsed = parsed * 10 + static_cast<uint64_t>(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<std::string> 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<std::ptrdiff_t>(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::size_t>(std::distance(mLayers.begin(), it));
if (sourceIndex == targetIndex)
return true;
LayerPersistentState movedLayer = *it;
mLayers.erase(mLayers.begin() + static_cast<std::ptrdiff_t>(sourceIndex));
mLayers.insert(mLayers.begin() + static_cast<std::ptrdiff_t>(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(),
[&parameterId](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(),
[&parameterKey](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<std::ptrdiff_t>(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::size_t>(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<LayerPersistentState> 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::LayerPersistentState>& LayerStackStore::Layers() const
{
return mLayers;
}
std::vector<LayerStackStore::LayerPersistentState>& 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<LayerPersistentState>& 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(),
[&parameterId](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<LayerPersistentState>& 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;
}
}

View File

@@ -0,0 +1,72 @@
#pragma once
#include "RuntimeJson.h"
#include "ShaderPackageCatalog.h"
#include "ShaderTypes.h"
#include <cstddef>
#include <cstdint>
#include <map>
#include <string>
#include <vector>
class LayerStackStore
{
public:
struct LayerPersistentState
{
std::string id;
std::string shaderId;
bool bypass = false;
std::map<std::string, ShaderParameterValue> 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<LayerPersistentState>& Layers() const;
std::vector<LayerPersistentState>& 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<LayerPersistentState>& layers, uint64_t& nextLayerId, std::string& error);
static std::string GenerateLayerId(std::vector<LayerPersistentState>& layers, uint64_t& nextLayerId);
std::vector<LayerPersistentState> mLayers;
uint64_t mNextLayerId = 0;
};

View File

@@ -0,0 +1,208 @@
#include "RenderSnapshotBuilder.h"
#include "RuntimeClock.h"
#include "RuntimeParameterUtils.h"
#include "RuntimeStore.h"
#include "ShaderCompiler.h"
#include <algorithm>
#include <chrono>
#include <filesystem>
#include <mutex>
#include <utility>
RenderSnapshotBuilder::RenderSnapshotBuilder(RuntimeStore& runtimeStore) :
mRuntimeStore(runtimeStore)
{
}
bool RenderSnapshotBuilder::BuildLayerPassFragmentShaderSources(const std::string& layerId, std::vector<ShaderPassBuildSource>& 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<RuntimeRenderState>& states) const
{
std::lock_guard<std::mutex> lock(mRuntimeStore.mMutex);
BuildLayerRenderStatesLocked(outputWidth, outputHeight, states);
}
bool RenderSnapshotBuilder::TryBuildLayerRenderStates(unsigned outputWidth, unsigned outputHeight, std::vector<RuntimeRenderState>& states) const
{
std::unique_lock<std::mutex> lock(mRuntimeStore.mMutex, std::try_to_lock);
if (!lock.owns_lock())
return false;
BuildLayerRenderStatesLocked(outputWidth, outputHeight, states);
return true;
}
bool RenderSnapshotBuilder::TryRefreshLayerParameters(std::vector<RuntimeRenderState>& states) const
{
std::unique_lock<std::mutex> lock(mRuntimeStore.mMutex, std::try_to_lock);
if (!lock.owns_lock())
return false;
RefreshLayerParametersLocked(states);
return true;
}
void RenderSnapshotBuilder::RefreshDynamicRenderStateFields(std::vector<RuntimeRenderState>& states) const
{
std::lock_guard<std::mutex> 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<RuntimeRenderState>& 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<RuntimeRenderState>& 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<RuntimeRenderState>& states) const
{
const RuntimeClockSnapshot clock = GetRuntimeClockSnapshot();
const double timeSeconds = std::chrono::duration_cast<std::chrono::duration<double>>(std::chrono::steady_clock::now() - mRuntimeStore.mStartTime).count();
const double frameCount = static_cast<double>(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;
}
}

View File

@@ -0,0 +1,43 @@
#pragma once
#include "ShaderTypes.h"
#include <atomic>
#include <cstdint>
#include <string>
#include <vector>
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<ShaderPassBuildSource>& passSources, std::string& error) const;
unsigned GetMaxTemporalHistoryFrames() const;
RuntimeSnapshotVersions GetVersions() const;
void AdvanceFrame();
void BuildLayerRenderStates(unsigned outputWidth, unsigned outputHeight, std::vector<RuntimeRenderState>& states) const;
bool TryBuildLayerRenderStates(unsigned outputWidth, unsigned outputHeight, std::vector<RuntimeRenderState>& states) const;
bool TryRefreshLayerParameters(std::vector<RuntimeRenderState>& states) const;
void RefreshDynamicRenderStateFields(std::vector<RuntimeRenderState>& states) const;
void MarkRenderStateDirty();
void MarkParameterStateDirty();
private:
void BuildLayerRenderStatesLocked(unsigned outputWidth, unsigned outputHeight, std::vector<RuntimeRenderState>& states) const;
void RefreshLayerParametersLocked(std::vector<RuntimeRenderState>& states) const;
void RefreshDynamicRenderStateFieldsLocked(std::vector<RuntimeRenderState>& states) const;
RuntimeStore& mRuntimeStore;
std::atomic<uint64_t> mFrameCounter{ 0 };
std::atomic<uint64_t> mRenderStateVersion{ 0 };
std::atomic<uint64_t> mParameterStateVersion{ 0 };
};

View File

@@ -0,0 +1,270 @@
#include "RuntimeConfigStore.h"
#include "RuntimeJson.h"
#include <algorithm>
#include <fstream>
#include <sstream>
#include <vector>
#include <windows.h>
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<std::filesystem::path> 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<unsigned short>(serverPortValue->asNumber(mConfig.serverPort));
if (const JsonValue* oscPortValue = configJson.find("oscPort"))
mConfig.oscPort = static_cast<unsigned short>(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<double>(mConfig.maxTemporalHistoryFrames));
mConfig.maxTemporalHistoryFrames = configuredValue <= 0.0 ? 0u : static_cast<unsigned>(configuredValue);
}
if (const JsonValue* previewFpsValue = configJson.find("previewFps"))
{
const double configuredValue = previewFpsValue->asNumber(static_cast<double>(mConfig.previewFps));
mConfig.previewFps = configuredValue <= 0.0 ? 0u : static_cast<unsigned>(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";
}

View File

@@ -0,0 +1,59 @@
#pragma once
#include <filesystem>
#include <string>
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;
};

View File

@@ -1,12 +1,9 @@
#include "RuntimeSnapshotProvider.h"
#include "ShaderCompiler.h"
#include <filesystem>
#include <utility>
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<RuntimeRenderState> 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<RuntimeRenderState>& states) const
{
mRuntimeStore.RefreshDynamicRenderStateFields(states);
mRenderSnapshotBuilder.RefreshDynamicRenderStateFields(states);
}
bool RuntimeSnapshotProvider::TryGetPublishedRenderStateSnapshot(unsigned outputWidth, unsigned outputHeight,

View File

@@ -1,18 +1,11 @@
#pragma once
#include "RuntimeStore.h"
#include "RenderSnapshotBuilder.h"
#include <cstdint>
#include <mutex>
#include <string>
#include <vector>
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<ShaderPassBuildSource>& 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;

View File

@@ -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<double>(shaderPackage->temporal.requestedHistoryLength)));
temporal.set("effectiveHistoryLength", JsonValue(static_cast<double>(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<double>(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";
}
}

View File

@@ -0,0 +1,16 @@
#pragma once
#include "LayerStackStore.h"
#include "RuntimeJson.h"
#include "ShaderPackageCatalog.h"
#include "ShaderTypes.h"
#include <string>
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);
};

View File

@@ -0,0 +1,125 @@
#include "RuntimeStatePresenter.h"
#include "RuntimeStateJson.h"
#include "RuntimeStore.h"
#include <mutex>
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<std::mutex> lock(runtimeStore.mMutex);
JsonValue root = JsonValue::MakeObject();
JsonValue app = JsonValue::MakeObject();
app.set("serverPort", JsonValue(static_cast<double>(runtimeStore.mServerPort)));
app.set("oscPort", JsonValue(static_cast<double>(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<double>(runtimeStore.mConfigStore.GetConfig().maxTemporalHistoryFrames)));
app.set("previewFps", JsonValue(static_cast<double>(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<double>(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<double>(telemetrySnapshot.signal.width)));
video.set("height", JsonValue(static_cast<double>(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<double>(telemetrySnapshot.performance.lateFrameCount)));
performance.set("droppedFrameCount", JsonValue(static_cast<double>(telemetrySnapshot.performance.droppedFrameCount)));
performance.set("flushedFrameCount", JsonValue(static_cast<double>(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<double>(shaderPackage->temporal.requestedHistoryLength)));
temporal.set("effectiveHistoryLength", JsonValue(static_cast<double>(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;
}

View File

@@ -0,0 +1,14 @@
#pragma once
#include "RuntimeJson.h"
#include <string>
class RuntimeStore;
class RuntimeStatePresenter
{
public:
static std::string BuildRuntimeStateJson(const RuntimeStore& runtimeStore);
static JsonValue BuildRuntimeStateValue(const RuntimeStore& runtimeStore);
};

File diff suppressed because it is too large Load Diff

View File

@@ -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 <atomic>
#include <chrono>
#include <cstdint>
#include <filesystem>
#include <map>
#include <mutex>
#include <string>
#include <vector>
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<std::string, ShaderParameterValue> parameterValues;
};
struct PersistentState
{
std::vector<LayerPersistentState> 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<std::string> 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<ShaderParameterDefinition>::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<RuntimeRenderState>& states) const;
bool TryBuildLayerRenderStates(unsigned outputWidth, unsigned outputHeight, std::vector<RuntimeRenderState>& states) const;
bool TryRefreshLayerParameters(std::vector<RuntimeRenderState>& states) const;
void RefreshDynamicRenderStateFields(std::vector<RuntimeRenderState>& states) const;
void BuildLayerRenderStatesLocked(unsigned outputWidth, unsigned outputHeight, std::vector<RuntimeRenderState>& states) const;
void RefreshLayerParametersLocked(std::vector<RuntimeRenderState>& states) const;
void RefreshDynamicRenderStateFieldsLocked(std::vector<RuntimeRenderState>& 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<LayerPersistentState>& 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<std::string, ShaderPackage> mPackagesById;
std::vector<std::string> mPackageOrder;
std::vector<ShaderPackageStatus> 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<uint64_t> mFrameCounter{ 0 };
std::atomic<uint64_t> mRenderStateVersion{ 0 };
std::atomic<uint64_t> mParameterStateVersion{ 0 };
uint64_t mNextLayerId;
};

View File

@@ -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<std::string, ShaderPackage> packagesById;
std::vector<std::string> packageOrder;
std::vector<ShaderPackageStatus> 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<std::string>& ShaderPackageCatalog::PackageOrder() const
{
return mPackageOrder;
}
const std::vector<ShaderPackageStatus>& 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<ShaderTextureAsset>& left, const std::vector<ShaderTextureAsset>& 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<ShaderFontAsset>& left, const std::vector<ShaderFontAsset>& 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;
}

View File

@@ -0,0 +1,37 @@
#pragma once
#include "ShaderTypes.h"
#include <filesystem>
#include <map>
#include <string>
#include <vector>
class ShaderPackageCatalog
{
public:
struct Snapshot
{
std::map<std::string, ShaderPackage> packagesById;
std::vector<std::string> 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<std::string>& PackageOrder() const;
const std::vector<ShaderPackageStatus>& PackageStatuses() const;
private:
static bool PackagesEquivalent(const ShaderPackage& left, const ShaderPackage& right);
static bool TextureAssetsEqual(const std::vector<ShaderTextureAsset>& left, const std::vector<ShaderTextureAsset>& right);
static bool FontAssetsEqual(const std::vector<ShaderFontAsset>& left, const std::vector<ShaderFontAsset>& right);
std::map<std::string, ShaderPackage> mPackagesById;
std::vector<std::string> mPackageOrder;
std::vector<ShaderPackageStatus> mPackageStatuses;
};

View File

@@ -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`

View File

@@ -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)

View File

@@ -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

View File

@@ -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