Files
video-shader-toys/apps/LoopThroughWithOpenGLCompositing/runtime/RuntimeStore.cpp
Aiden b2369c418b
All checks were successful
CI / React UI Build (push) Successful in 10s
CI / Native Windows Build And Tests (push) Successful in 2m40s
CI / Windows Release Package (push) Successful in 2m39s
pass 2
2026-05-11 01:29:44 +10:00

1825 lines
59 KiB
C++

#include "RuntimeStore.h"
#include "RuntimeClock.h"
#include "ShaderPackageRegistry.h"
#include "RuntimeParameterUtils.h"
#include <algorithm>
#include <cctype>
#include <cmath>
#include <fstream>
#include <mutex>
#include <random>
#include <set>
#include <sstream>
#include <windows.h>
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);
}
double Clamp01(double value)
{
return (std::max)(0.0, (std::min)(1.0, value));
}
std::string ToLowerCopy(std::string text)
{
std::transform(text.begin(), text.end(), text.begin(),
[](unsigned char ch) { return static_cast<char>(std::tolower(ch)); });
return text;
}
bool LooksLikePackagedRuntimeRoot(const std::filesystem::path& candidate)
{
return std::filesystem::exists(candidate / "config" / "runtime-host.json") &&
std::filesystem::exists(candidate / "runtime" / "templates" / "shader_wrapper.slang.in") &&
std::filesystem::exists(candidate / "shaders");
}
bool LooksLikeRepoRoot(const std::filesystem::path& candidate)
{
return std::filesystem::exists(candidate / "CMakeLists.txt") &&
std::filesystem::exists(candidate / "apps" / "LoopThroughWithOpenGLCompositing");
}
std::filesystem::path FindRepoRootCandidate()
{
std::vector<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 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 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;
}
double GenerateStartupRandom()
{
std::random_device randomDevice;
std::uniform_real_distribution<double> distribution(0.0, 1.0);
return distribution(randomDevice);
}
bool TryParseLayerIdNumber(const std::string& layerId, uint64_t& number)
{
const std::string prefix = "layer-";
if (layerId.rfind(prefix, 0) != 0 || layerId.size() == prefix.size())
return false;
uint64_t parsed = 0;
for (std::size_t index = prefix.size(); index < layerId.size(); ++index)
{
const unsigned char ch = static_cast<unsigned char>(layerId[index]);
if (!std::isdigit(ch))
return false;
parsed = parsed * 10 + static_cast<uint64_t>(ch - '0');
}
number = parsed;
return true;
}
std::string ShaderParameterTypeToString(ShaderParameterType type)
{
switch (type)
{
case ShaderParameterType::Float: return "float";
case ShaderParameterType::Vec2: return "vec2";
case ShaderParameterType::Color: return "color";
case ShaderParameterType::Boolean: return "bool";
case ShaderParameterType::Enum: return "enum";
case ShaderParameterType::Text: return "text";
case ShaderParameterType::Trigger: return "trigger";
}
return "unknown";
}
}
RuntimeStore::RuntimeStore() :
mHealthTelemetry(),
mReloadRequested(false),
mCompileSucceeded(false),
mStartupRandom(GenerateStartupRandom()),
mServerPort(8080),
mAutoReloadEnabled(true),
mStartTime(std::chrono::steady_clock::now()),
mLastScanTime((std::chrono::steady_clock::time_point::min)()),
mFrameCounter(0),
mNextLayerId(0)
{
}
HealthTelemetry& RuntimeStore::GetHealthTelemetry()
{
return mHealthTelemetry;
}
const HealthTelemetry& RuntimeStore::GetHealthTelemetry() const
{
return mHealthTelemetry;
}
bool RuntimeStore::InitializeStore(std::string& error)
{
try
{
std::lock_guard<std::mutex> lock(mMutex);
if (!ResolvePaths(error))
return false;
if (!LoadConfig(error))
return false;
mShaderRoot = mRepoRoot / mConfig.shaderLibrary;
if (!LoadPersistentState(error))
return false;
if (!ScanShaderPackages(error))
return false;
NormalizePersistentLayerIdsLocked();
for (RuntimeStore::LayerPersistentState& layer : mPersistentState.layers)
{
auto shaderIt = mPackagesById.find(layer.shaderId);
if (shaderIt != mPackagesById.end())
EnsureLayerDefaultsLocked(layer, shaderIt->second);
}
if (mPersistentState.layers.empty() && !mPackageOrder.empty())
{
RuntimeStore::LayerPersistentState layer;
layer.id = GenerateLayerId();
layer.shaderId = mPackageOrder.front();
layer.bypass = false;
EnsureLayerDefaultsLocked(layer, mPackagesById[layer.shaderId]);
mPersistentState.layers.push_back(layer);
}
mServerPort = mConfig.serverPort;
mAutoReloadEnabled = mConfig.autoReload;
mReloadRequested = true;
mCompileMessage = "Waiting for shader compile.";
return true;
}
catch (const std::exception& exception)
{
error = std::string("RuntimeStore::InitializeStore exception: ") + exception.what();
return false;
}
catch (...)
{
error = "RuntimeStore::InitializeStore threw a non-standard exception.";
return false;
}
}
std::string RuntimeStore::BuildPersistentStateJson() const
{
return SerializeJson(BuildRuntimeStateValue(), true);
}
bool RuntimeStore::PollStoredFileChanges(bool& registryChanged, bool& reloadRequested, std::string& error)
{
try
{
std::lock_guard<std::mutex> lock(mMutex);
registryChanged = false;
reloadRequested = false;
if (!mAutoReloadEnabled)
{
reloadRequested = mReloadRequested;
return true;
}
const auto now = std::chrono::steady_clock::now();
if (mLastScanTime != (std::chrono::steady_clock::time_point::min)() &&
std::chrono::duration_cast<std::chrono::milliseconds>(now - mLastScanTime).count() < 250)
{
reloadRequested = mReloadRequested;
return true;
}
mLastScanTime = now;
std::string scanError;
std::map<std::string, ShaderPackage> previousPackages = mPackagesById;
std::vector<std::string> previousOrder = mPackageOrder;
std::map<std::string, std::pair<std::filesystem::file_time_type, std::filesystem::file_time_type>> previousLayerShaderTimes;
for (const RuntimeStore::LayerPersistentState& layer : mPersistentState.layers)
{
auto previous = previousPackages.find(layer.shaderId);
if (previous != previousPackages.end())
previousLayerShaderTimes[layer.id] = std::make_pair(previous->second.shaderWriteTime, previous->second.manifestWriteTime);
}
if (!ScanShaderPackages(scanError))
{
error = scanError;
return false;
}
registryChanged = previousOrder != mPackageOrder;
if (!registryChanged && previousPackages.size() == mPackagesById.size())
{
for (const auto& item : mPackagesById)
{
auto previous = previousPackages.find(item.first);
if (previous == previousPackages.end())
{
registryChanged = true;
break;
}
if (previous->second.shaderWriteTime != item.second.shaderWriteTime ||
previous->second.manifestWriteTime != item.second.manifestWriteTime ||
!TextureAssetsEqual(previous->second.textureAssets, item.second.textureAssets) ||
!FontAssetsEqual(previous->second.fontAssets, item.second.fontAssets))
{
registryChanged = true;
break;
}
}
}
for (RuntimeStore::LayerPersistentState& layer : mPersistentState.layers)
{
auto active = mPackagesById.find(layer.shaderId);
auto previous = previousLayerShaderTimes.find(layer.id);
if (active == mPackagesById.end())
continue;
EnsureLayerDefaultsLocked(layer, active->second);
if (previous != previousLayerShaderTimes.end())
{
auto previousPackage = previousPackages.find(layer.shaderId);
if (previous->second.first != active->second.shaderWriteTime ||
previous->second.second != active->second.manifestWriteTime ||
(previousPackage != previousPackages.end() &&
(!TextureAssetsEqual(previousPackage->second.textureAssets, active->second.textureAssets) ||
!FontAssetsEqual(previousPackage->second.fontAssets, active->second.fontAssets))))
{
mReloadRequested = true;
}
}
}
reloadRequested = mReloadRequested;
if (registryChanged || reloadRequested)
MarkRenderStateDirtyLocked();
return true;
}
catch (const std::exception& exception)
{
error = std::string("RuntimeStore::PollStoredFileChanges exception: ") + exception.what();
return false;
}
catch (...)
{
error = "RuntimeStore::PollStoredFileChanges threw a non-standard exception.";
return false;
}
}
bool RuntimeStore::CreateStoredLayer(const std::string& shaderId, std::string& error)
{
std::lock_guard<std::mutex> lock(mMutex);
auto shaderIt = mPackagesById.find(shaderId);
if (shaderIt == mPackagesById.end())
{
error = "Unknown shader id: " + shaderId;
return false;
}
RuntimeStore::LayerPersistentState layer;
layer.id = GenerateLayerId();
layer.shaderId = shaderId;
layer.bypass = false;
EnsureLayerDefaultsLocked(layer, shaderIt->second);
mPersistentState.layers.push_back(layer);
mReloadRequested = true;
MarkRenderStateDirtyLocked();
return SavePersistentState(error);
}
bool RuntimeStore::DeleteStoredLayer(const std::string& layerId, std::string& error)
{
std::lock_guard<std::mutex> lock(mMutex);
auto it = std::find_if(mPersistentState.layers.begin(), mPersistentState.layers.end(),
[&layerId](const RuntimeStore::LayerPersistentState& layer) { return layer.id == layerId; });
if (it == mPersistentState.layers.end())
{
error = "Unknown layer id: " + layerId;
return false;
}
mPersistentState.layers.erase(it);
mReloadRequested = true;
MarkRenderStateDirtyLocked();
return SavePersistentState(error);
}
bool RuntimeStore::MoveStoredLayer(const std::string& layerId, int direction, std::string& error)
{
std::lock_guard<std::mutex> lock(mMutex);
auto it = std::find_if(mPersistentState.layers.begin(), mPersistentState.layers.end(),
[&layerId](const RuntimeStore::LayerPersistentState& layer) { return layer.id == layerId; });
if (it == mPersistentState.layers.end())
{
error = "Unknown layer id: " + layerId;
return false;
}
const std::ptrdiff_t index = std::distance(mPersistentState.layers.begin(), it);
const std::ptrdiff_t newIndex = index + direction;
if (newIndex < 0 || newIndex >= static_cast<std::ptrdiff_t>(mPersistentState.layers.size()))
return true;
std::swap(mPersistentState.layers[index], mPersistentState.layers[newIndex]);
mReloadRequested = true;
MarkRenderStateDirtyLocked();
return SavePersistentState(error);
}
bool RuntimeStore::MoveStoredLayerToIndex(const std::string& layerId, std::size_t targetIndex, std::string& error)
{
std::lock_guard<std::mutex> lock(mMutex);
auto it = std::find_if(mPersistentState.layers.begin(), mPersistentState.layers.end(),
[&layerId](const RuntimeStore::LayerPersistentState& layer) { return layer.id == layerId; });
if (it == mPersistentState.layers.end())
{
error = "Unknown layer id: " + layerId;
return false;
}
if (mPersistentState.layers.empty())
return true;
if (targetIndex >= mPersistentState.layers.size())
targetIndex = mPersistentState.layers.size() - 1;
const std::size_t sourceIndex = static_cast<std::size_t>(std::distance(mPersistentState.layers.begin(), it));
if (sourceIndex == targetIndex)
return true;
RuntimeStore::LayerPersistentState movedLayer = *it;
mPersistentState.layers.erase(mPersistentState.layers.begin() + static_cast<std::ptrdiff_t>(sourceIndex));
mPersistentState.layers.insert(mPersistentState.layers.begin() + static_cast<std::ptrdiff_t>(targetIndex), movedLayer);
mReloadRequested = true;
MarkRenderStateDirtyLocked();
return SavePersistentState(error);
}
bool RuntimeStore::SetStoredLayerBypassState(const std::string& layerId, bool bypassed, std::string& error)
{
std::lock_guard<std::mutex> lock(mMutex);
RuntimeStore::LayerPersistentState* layer = FindLayerById(layerId);
if (!layer)
{
error = "Unknown layer id: " + layerId;
return false;
}
layer->bypass = bypassed;
mReloadRequested = true;
MarkParameterStateDirtyLocked();
return SavePersistentState(error);
}
bool RuntimeStore::SetStoredLayerShaderSelection(const std::string& layerId, const std::string& shaderId, std::string& error)
{
std::lock_guard<std::mutex> lock(mMutex);
RuntimeStore::LayerPersistentState* layer = FindLayerById(layerId);
if (!layer)
{
error = "Unknown layer id: " + layerId;
return false;
}
auto shaderIt = mPackagesById.find(shaderId);
if (shaderIt == mPackagesById.end())
{
error = "Unknown shader id: " + shaderId;
return false;
}
layer->shaderId = shaderId;
layer->parameterValues.clear();
EnsureLayerDefaultsLocked(*layer, shaderIt->second);
mReloadRequested = true;
MarkRenderStateDirtyLocked();
return SavePersistentState(error);
}
bool RuntimeStore::SetStoredParameterValue(const std::string& layerId, const std::string& parameterId, const ShaderParameterValue& value, bool persistState, std::string& error)
{
std::lock_guard<std::mutex> lock(mMutex);
RuntimeStore::LayerPersistentState* layer = FindLayerById(layerId);
if (!layer)
{
error = "Unknown layer id: " + layerId;
return false;
}
layer->parameterValues[parameterId] = value;
MarkParameterStateDirtyLocked();
return !persistState || SavePersistentState(error);
}
bool RuntimeStore::ResetStoredLayerParameterValues(const std::string& layerId, std::string& error)
{
std::lock_guard<std::mutex> lock(mMutex);
RuntimeStore::LayerPersistentState* layer = FindLayerById(layerId);
if (!layer)
{
error = "Unknown layer id: " + layerId;
return false;
}
auto shaderIt = mPackagesById.find(layer->shaderId);
if (shaderIt == mPackagesById.end())
{
error = "Unknown shader id: " + layer->shaderId;
return false;
}
layer->parameterValues.clear();
EnsureLayerDefaultsLocked(*layer, shaderIt->second);
MarkParameterStateDirtyLocked();
return SavePersistentState(error);
}
bool RuntimeStore::SaveStackPresetSnapshot(const std::string& presetName, std::string& error) const
{
std::lock_guard<std::mutex> lock(mMutex);
const std::string safeStem = MakeSafePresetFileStem(presetName);
if (safeStem.empty())
{
error = "Preset name must include at least one letter or number.";
return false;
}
JsonValue root = JsonValue::MakeObject();
root.set("version", JsonValue(1.0));
root.set("name", JsonValue(TrimCopy(presetName)));
root.set("layers", SerializeLayerStackLocked());
return WriteTextFile(mPresetRoot / (safeStem + ".json"), SerializeJson(root, true), error);
}
bool RuntimeStore::LoadStackPresetSnapshot(const std::string& presetName, std::string& error)
{
std::lock_guard<std::mutex> lock(mMutex);
const std::string safeStem = MakeSafePresetFileStem(presetName);
if (safeStem.empty())
{
error = "Preset name must include at least one letter or number.";
return false;
}
const std::filesystem::path presetPath = mPresetRoot / (safeStem + ".json");
std::string presetText = ReadTextFile(presetPath, error);
if (presetText.empty())
return false;
JsonValue root;
if (!ParseJson(presetText, root, error))
return false;
const JsonValue* layersValue = root.find("layers");
if (!layersValue || !layersValue->isArray())
{
error = "Preset file is missing a valid 'layers' array.";
return false;
}
std::vector<RuntimeStore::LayerPersistentState> nextLayers;
if (!DeserializeLayerStackLocked(*layersValue, nextLayers, error))
return false;
if (nextLayers.empty())
{
error = "Preset does not contain any valid layers.";
return false;
}
mPersistentState.layers = nextLayers;
mReloadRequested = true;
MarkRenderStateDirtyLocked();
return SavePersistentState(error);
}
bool RuntimeStore::HasStoredLayer(const std::string& layerId) const
{
std::lock_guard<std::mutex> lock(mMutex);
return FindLayerById(layerId) != nullptr;
}
bool RuntimeStore::HasStoredShader(const std::string& shaderId) const
{
std::lock_guard<std::mutex> lock(mMutex);
return mPackagesById.find(shaderId) != mPackagesById.end();
}
bool RuntimeStore::TryGetStoredParameterById(const std::string& layerId, const std::string& parameterId, StoredParameterSnapshot& snapshot, std::string& error) const
{
std::lock_guard<std::mutex> lock(mMutex);
const LayerPersistentState* layer = FindLayerById(layerId);
if (!layer)
{
error = "Unknown layer id: " + layerId;
return false;
}
auto shaderIt = mPackagesById.find(layer->shaderId);
if (shaderIt == mPackagesById.end())
{
error = "Unknown shader id: " + layer->shaderId;
return false;
}
auto parameterIt = std::find_if(shaderIt->second.parameters.begin(), shaderIt->second.parameters.end(),
[&parameterId](const ShaderParameterDefinition& definition) { return definition.id == parameterId; });
if (parameterIt == shaderIt->second.parameters.end())
{
error = "Unknown parameter id: " + parameterId;
return false;
}
snapshot = StoredParameterSnapshot();
snapshot.layerId = layer->id;
snapshot.definition = *parameterIt;
auto valueIt = layer->parameterValues.find(parameterIt->id);
if (valueIt != layer->parameterValues.end())
{
snapshot.currentValue = valueIt->second;
snapshot.hasCurrentValue = true;
}
return true;
}
bool RuntimeStore::TryGetStoredParameterByControlKey(const std::string& layerKey, const std::string& parameterKey, StoredParameterSnapshot& snapshot, std::string& error) const
{
std::lock_guard<std::mutex> lock(mMutex);
const LayerPersistentState* layer = nullptr;
const ShaderPackage* shaderPackage = nullptr;
std::vector<ShaderParameterDefinition>::const_iterator parameterIt;
if (!TryResolveStoredLayerAndParameterByControlKeyLocked(layerKey, parameterKey, layer, shaderPackage, parameterIt, error))
return false;
snapshot = StoredParameterSnapshot();
snapshot.layerId = layer->id;
snapshot.definition = *parameterIt;
auto valueIt = layer->parameterValues.find(parameterIt->id);
if (valueIt != layer->parameterValues.end())
{
snapshot.currentValue = valueIt->second;
snapshot.hasCurrentValue = true;
}
return true;
}
bool RuntimeStore::ResolveStoredLayerMove(const std::string& layerId, int direction, bool& shouldMove, std::string& error) const
{
std::lock_guard<std::mutex> lock(mMutex);
auto it = std::find_if(mPersistentState.layers.begin(), mPersistentState.layers.end(),
[&layerId](const RuntimeStore::LayerPersistentState& layer) { return layer.id == layerId; });
if (it == mPersistentState.layers.end())
{
error = "Unknown layer id: " + layerId;
return false;
}
const std::ptrdiff_t index = std::distance(mPersistentState.layers.begin(), it);
const std::ptrdiff_t newIndex = index + direction;
shouldMove = newIndex >= 0 && newIndex < static_cast<std::ptrdiff_t>(mPersistentState.layers.size()) && newIndex != index;
return true;
}
bool RuntimeStore::ResolveStoredLayerMoveToIndex(const std::string& layerId, std::size_t targetIndex, bool& shouldMove, std::string& error) const
{
std::lock_guard<std::mutex> lock(mMutex);
auto it = std::find_if(mPersistentState.layers.begin(), mPersistentState.layers.end(),
[&layerId](const RuntimeStore::LayerPersistentState& layer) { return layer.id == layerId; });
if (it == mPersistentState.layers.end())
{
error = "Unknown layer id: " + layerId;
return false;
}
if (mPersistentState.layers.empty())
{
shouldMove = false;
return true;
}
const std::size_t clampedTargetIndex = (std::min)(targetIndex, mPersistentState.layers.size() - 1);
const std::size_t sourceIndex = static_cast<std::size_t>(std::distance(mPersistentState.layers.begin(), it));
shouldMove = sourceIndex != clampedTargetIndex;
return true;
}
bool RuntimeStore::IsValidStackPresetName(const std::string& presetName) const
{
return !MakeSafePresetFileStem(presetName).empty();
}
double RuntimeStore::GetRuntimeElapsedSeconds() const
{
return std::chrono::duration_cast<std::chrono::duration<double>>(
std::chrono::steady_clock::now() - mStartTime).count();
}
const std::filesystem::path& RuntimeStore::GetRuntimeRepositoryRoot() const
{
return mRepoRoot;
}
const std::filesystem::path& RuntimeStore::GetRuntimeUiRoot() const
{
return mUiRoot;
}
const std::filesystem::path& RuntimeStore::GetRuntimeDocsRoot() const
{
return mDocsRoot;
}
const std::filesystem::path& RuntimeStore::GetRuntimeDataRoot() const
{
return mRuntimeRoot;
}
unsigned short RuntimeStore::GetConfiguredControlServerPort() const
{
return mServerPort;
}
unsigned short RuntimeStore::GetConfiguredOscPort() const
{
return mConfig.oscPort;
}
const std::string& RuntimeStore::GetConfiguredOscBindAddress() const
{
return mConfig.oscBindAddress;
}
double RuntimeStore::GetConfiguredOscSmoothing() const
{
return mConfig.oscSmoothing;
}
unsigned RuntimeStore::GetConfiguredMaxTemporalHistoryFrames() const
{
return mConfig.maxTemporalHistoryFrames;
}
unsigned RuntimeStore::GetConfiguredPreviewFps() const
{
return mConfig.previewFps;
}
bool RuntimeStore::IsExternalKeyingConfigured() const
{
return mConfig.enableExternalKeying;
}
const std::string& RuntimeStore::GetConfiguredInputVideoFormat() const
{
return mConfig.inputVideoFormat;
}
const std::string& RuntimeStore::GetConfiguredInputFrameRate() const
{
return mConfig.inputFrameRate;
}
const std::string& RuntimeStore::GetConfiguredOutputVideoFormat() const
{
return mConfig.outputVideoFormat;
}
const std::string& RuntimeStore::GetConfiguredOutputFrameRate() const
{
return mConfig.outputFrameRate;
}
void RuntimeStore::SetBoundControlServerPort(unsigned short port)
{
std::lock_guard<std::mutex> lock(mMutex);
mServerPort = port;
}
void RuntimeStore::SetCompileStatus(bool succeeded, const std::string& message)
{
std::lock_guard<std::mutex> lock(mMutex);
mCompileSucceeded = succeeded;
mCompileMessage = message;
}
void RuntimeStore::ClearReloadRequest()
{
std::lock_guard<std::mutex> lock(mMutex);
mReloadRequested = false;
}
JsonValue RuntimeStore::BuildRuntimeStateValue() const
{
const HealthTelemetry::Snapshot telemetrySnapshot = mHealthTelemetry.GetSnapshot();
std::lock_guard<std::mutex> lock(mMutex);
JsonValue root = JsonValue::MakeObject();
JsonValue app = JsonValue::MakeObject();
app.set("serverPort", JsonValue(static_cast<double>(mServerPort)));
app.set("oscPort", JsonValue(static_cast<double>(mConfig.oscPort)));
app.set("oscBindAddress", JsonValue(mConfig.oscBindAddress));
app.set("oscSmoothing", JsonValue(mConfig.oscSmoothing));
app.set("autoReload", JsonValue(mAutoReloadEnabled));
app.set("maxTemporalHistoryFrames", JsonValue(static_cast<double>(mConfig.maxTemporalHistoryFrames)));
app.set("previewFps", JsonValue(static_cast<double>(mConfig.previewFps)));
app.set("enableExternalKeying", JsonValue(mConfig.enableExternalKeying));
app.set("inputVideoFormat", JsonValue(mConfig.inputVideoFormat));
app.set("inputFrameRate", JsonValue(mConfig.inputFrameRate));
app.set("outputVideoFormat", JsonValue(mConfig.outputVideoFormat));
app.set("outputFrameRate", JsonValue(mConfig.outputFrameRate));
root.set("app", app);
JsonValue runtime = JsonValue::MakeObject();
runtime.set("layerCount", JsonValue(static_cast<double>(mPersistentState.layers.size())));
runtime.set("compileSucceeded", JsonValue(mCompileSucceeded));
runtime.set("compileMessage", JsonValue(mCompileMessage));
root.set("runtime", runtime);
JsonValue video = JsonValue::MakeObject();
video.set("hasSignal", JsonValue(telemetrySnapshot.signal.hasSignal));
video.set("width", JsonValue(static_cast<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 : mPackageStatuses)
{
JsonValue shader = JsonValue::MakeObject();
shader.set("id", JsonValue(status.id));
shader.set("name", JsonValue(status.displayName));
shader.set("description", JsonValue(status.description));
shader.set("category", JsonValue(status.category));
shader.set("available", JsonValue(status.available));
if (!status.available)
shader.set("error", JsonValue(status.error));
auto shaderIt = mPackagesById.find(status.id);
if (status.available && shaderIt != mPackagesById.end() && shaderIt->second.temporal.enabled)
{
JsonValue temporal = JsonValue::MakeObject();
temporal.set("enabled", JsonValue(true));
temporal.set("historySource", JsonValue(TemporalHistorySourceToString(shaderIt->second.temporal.historySource)));
temporal.set("requestedHistoryLength", JsonValue(static_cast<double>(shaderIt->second.temporal.requestedHistoryLength)));
temporal.set("effectiveHistoryLength", JsonValue(static_cast<double>(shaderIt->second.temporal.effectiveHistoryLength)));
shader.set("temporal", temporal);
}
if (status.available && shaderIt != mPackagesById.end() && shaderIt->second.feedback.enabled)
{
JsonValue feedback = JsonValue::MakeObject();
feedback.set("enabled", JsonValue(true));
feedback.set("writePass", JsonValue(shaderIt->second.feedback.writePassId));
shader.set("feedback", feedback);
}
shaderLibrary.pushBack(shader);
}
root.set("shaders", shaderLibrary);
JsonValue stackPresets = JsonValue::MakeArray();
for (const std::string& presetName : GetStackPresetNamesLocked())
stackPresets.pushBack(JsonValue(presetName));
root.set("stackPresets", stackPresets);
root.set("layers", SerializeLayerStack());
return root;
}
JsonValue RuntimeStore::SerializeLayerStack() const
{
return SerializeLayerStackLocked();
}
bool RuntimeStore::LoadConfig(std::string& error)
{
if (!std::filesystem::exists(mConfigPath))
return true;
std::string configText = ReadTextFile(mConfigPath, error);
if (configText.empty())
return false;
JsonValue configJson;
if (!ParseJson(configText, configJson, error))
return false;
if (const JsonValue* shaderLibraryValue = configJson.find("shaderLibrary"))
mConfig.shaderLibrary = shaderLibraryValue->asString();
if (const JsonValue* serverPortValue = configJson.find("serverPort"))
mConfig.serverPort = static_cast<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();
}
}
mAutoReloadEnabled = mConfig.autoReload;
return true;
}
bool RuntimeStore::LoadPersistentState(std::string& error)
{
if (!std::filesystem::exists(mRuntimeStatePath))
return true;
std::string stateText = ReadTextFile(mRuntimeStatePath, error);
if (stateText.empty())
return false;
JsonValue root;
if (!ParseJson(stateText, root, error))
return false;
if (const JsonValue* layersValue = root.find("layers"))
{
for (const JsonValue& layerValue : layersValue->asArray())
{
if (!layerValue.isObject())
continue;
RuntimeStore::LayerPersistentState layer;
if (const JsonValue* idValue = layerValue.find("id"))
layer.id = idValue->asString();
if (const JsonValue* shaderIdValue = layerValue.find("shaderId"))
layer.shaderId = shaderIdValue->asString();
if (const JsonValue* bypassValue = layerValue.find("bypass"))
layer.bypass = bypassValue->asBoolean(false);
else if (const JsonValue* enabledValue = layerValue.find("enabled"))
layer.bypass = !enabledValue->asBoolean(true);
if (const JsonValue* parameterValues = layerValue.find("parameterValues"))
{
for (const auto& parameterItem : parameterValues->asObject())
{
ShaderParameterValue value;
const JsonValue& jsonValue = parameterItem.second;
if (jsonValue.isBoolean())
value.booleanValue = jsonValue.asBoolean();
else if (jsonValue.isString())
value.enumValue = jsonValue.asString();
else if (jsonValue.isNumber())
value.numberValues.push_back(jsonValue.asNumber());
else if (jsonValue.isArray())
value.numberValues = JsonArrayToNumbers(jsonValue);
layer.parameterValues[parameterItem.first] = value;
}
}
if (!layer.shaderId.empty())
mPersistentState.layers.push_back(layer);
}
}
else
{
std::string activeShaderId;
if (const JsonValue* activeShaderValue = root.find("activeShaderId"))
activeShaderId = activeShaderValue->asString();
if (!activeShaderId.empty())
{
RuntimeStore::LayerPersistentState layer;
layer.id = GenerateLayerId();
layer.shaderId = activeShaderId;
layer.bypass = false;
if (const JsonValue* valuesByShader = root.find("parameterValuesByShader"))
{
const JsonValue* shaderValues = valuesByShader->find(activeShaderId);
if (shaderValues)
{
for (const auto& parameterItem : shaderValues->asObject())
{
ShaderParameterValue value;
const JsonValue& jsonValue = parameterItem.second;
if (jsonValue.isBoolean())
value.booleanValue = jsonValue.asBoolean();
else if (jsonValue.isString())
value.enumValue = jsonValue.asString();
else if (jsonValue.isNumber())
value.numberValues.push_back(jsonValue.asNumber());
else if (jsonValue.isArray())
value.numberValues = JsonArrayToNumbers(jsonValue);
layer.parameterValues[parameterItem.first] = value;
}
}
}
mPersistentState.layers.push_back(layer);
}
}
return true;
}
bool RuntimeStore::SavePersistentState(std::string& error) const
{
JsonValue root = JsonValue::MakeObject();
JsonValue layers = JsonValue::MakeArray();
for (const RuntimeStore::LayerPersistentState& layer : mPersistentState.layers)
{
JsonValue layerValue = JsonValue::MakeObject();
layerValue.set("id", JsonValue(layer.id));
layerValue.set("shaderId", JsonValue(layer.shaderId));
layerValue.set("bypass", JsonValue(layer.bypass));
JsonValue parameterValues = JsonValue::MakeObject();
auto packageIt = mPackagesById.find(layer.shaderId);
for (const auto& parameterItem : layer.parameterValues)
{
const ShaderParameterDefinition* definition = nullptr;
if (packageIt != mPackagesById.end())
{
for (const ShaderParameterDefinition& candidate : packageIt->second.parameters)
{
if (candidate.id == parameterItem.first)
{
definition = &candidate;
break;
}
}
}
if (definition)
parameterValues.set(parameterItem.first, SerializeParameterValue(*definition, parameterItem.second));
}
layerValue.set("parameterValues", parameterValues);
layers.pushBack(layerValue);
}
root.set("layers", layers);
return WriteTextFile(mRuntimeStatePath, SerializeJson(root, true), error);
}
bool RuntimeStore::ScanShaderPackages(std::string& error)
{
std::map<std::string, ShaderPackage> packagesById;
std::vector<std::string> packageOrder;
std::vector<ShaderPackageStatus> packageStatuses;
ShaderPackageRegistry registry(mConfig.maxTemporalHistoryFrames);
if (!registry.Scan(mShaderRoot, packagesById, packageOrder, packageStatuses, error))
return false;
mPackagesById.swap(packagesById);
mPackageOrder.swap(packageOrder);
mPackageStatuses.swap(packageStatuses);
for (auto it = mPersistentState.layers.begin(); it != mPersistentState.layers.end();)
{
if (mPackagesById.find(it->shaderId) == mPackagesById.end())
it = mPersistentState.layers.erase(it);
else
++it;
}
MarkRenderStateDirtyLocked();
return true;
}
std::string RuntimeStore::ReadTextFile(const std::filesystem::path& path, std::string& error) const
{
std::ifstream input(path, std::ios::binary);
if (!input)
{
error = "Could not open file: " + path.string();
return std::string();
}
std::ostringstream buffer;
buffer << input.rdbuf();
return buffer.str();
}
bool RuntimeStore::WriteTextFile(const std::filesystem::path& path, const std::string& contents, std::string& error) const
{
std::error_code fsError;
std::filesystem::create_directories(path.parent_path(), fsError);
const std::filesystem::path temporaryPath = path.string() + ".tmp";
std::ofstream output(temporaryPath, std::ios::binary | std::ios::trunc);
if (!output)
{
error = "Could not write file: " + temporaryPath.string();
return false;
}
output << contents;
output.close();
if (!output.good())
{
error = "Could not finish writing file: " + temporaryPath.string();
return false;
}
if (!MoveFileExA(temporaryPath.string().c_str(), path.string().c_str(), MOVEFILE_REPLACE_EXISTING | MOVEFILE_WRITE_THROUGH))
{
const DWORD lastError = GetLastError();
std::filesystem::remove(temporaryPath, fsError);
error = "Could not replace file: " + path.string() + " (Win32 error " + std::to_string(lastError) + ")";
return false;
}
return true;
}
bool RuntimeStore::ResolvePaths(std::string& error)
{
mRepoRoot = FindRepoRootCandidate();
if (mRepoRoot.empty())
{
error = "Could not locate the repository root from the current runtime path.";
return false;
}
const std::filesystem::path builtUiRoot = mRepoRoot / "ui" / "dist";
mUiRoot = std::filesystem::exists(builtUiRoot) ? builtUiRoot : (mRepoRoot / "ui");
mDocsRoot = mRepoRoot / "docs";
mConfigPath = mRepoRoot / "config" / "runtime-host.json";
mShaderRoot = mRepoRoot / mConfig.shaderLibrary;
mRuntimeRoot = mRepoRoot / "runtime";
mPresetRoot = mRuntimeRoot / "stack_presets";
mRuntimeStatePath = mRuntimeRoot / "runtime_state.json";
mWrapperPath = mRuntimeRoot / "shader_cache" / "active_shader_wrapper.slang";
mGeneratedGlslPath = mRuntimeRoot / "shader_cache" / "active_shader.raw.frag";
mPatchedGlslPath = mRuntimeRoot / "shader_cache" / "active_shader.frag";
std::error_code fsError;
std::filesystem::create_directories(mRuntimeRoot / "shader_cache", fsError);
std::filesystem::create_directories(mPresetRoot, fsError);
return true;
}
std::vector<std::string> RuntimeStore::GetStackPresetNamesLocked() const
{
std::vector<std::string> presetNames;
std::error_code fsError;
if (!std::filesystem::exists(mPresetRoot, fsError))
return presetNames;
for (const auto& entry : std::filesystem::directory_iterator(mPresetRoot, fsError))
{
if (!entry.is_regular_file())
continue;
if (ToLowerCopy(entry.path().extension().string()) != ".json")
continue;
presetNames.push_back(entry.path().stem().string());
}
std::sort(presetNames.begin(), presetNames.end());
return presetNames;
}
std::string RuntimeStore::MakeSafePresetFileStem(const std::string& presetName) const
{
return ::MakeSafePresetFileStem(presetName);
}
bool RuntimeStore::TryResolveStoredLayerAndParameterByControlKeyLocked(const std::string& layerKey, const std::string& parameterKey,
const RuntimeStore::LayerPersistentState*& matchedLayer, const ShaderPackage*& matchedPackage,
std::vector<ShaderParameterDefinition>::const_iterator& parameterIt, std::string& error) const
{
matchedLayer = nullptr;
matchedPackage = nullptr;
for (const RuntimeStore::LayerPersistentState& layer : mPersistentState.layers)
{
auto shaderIt = mPackagesById.find(layer.shaderId);
if (shaderIt == mPackagesById.end())
continue;
if (MatchesControlKey(layer.id, layerKey) || MatchesControlKey(shaderIt->second.id, layerKey) ||
MatchesControlKey(shaderIt->second.displayName, layerKey))
{
matchedLayer = &layer;
matchedPackage = &shaderIt->second;
break;
}
}
if (!matchedLayer || !matchedPackage)
{
error = "Unknown OSC layer key: " + layerKey;
return false;
}
parameterIt = std::find_if(matchedPackage->parameters.begin(), matchedPackage->parameters.end(),
[&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;
}
return true;
}
bool RuntimeStore::CopyShaderPackageForStoredLayer(const std::string& layerId, ShaderPackage& shaderPackage, std::string& error) const
{
std::lock_guard<std::mutex> lock(mMutex);
const RuntimeStore::LayerPersistentState* layer = FindLayerById(layerId);
if (!layer)
{
error = "Unknown layer id: " + layerId;
return false;
}
auto it = mPackagesById.find(layer->shaderId);
if (it == mPackagesById.end())
{
error = "Unknown shader id: " + layer->shaderId;
return false;
}
shaderPackage = it->second;
return true;
}
void RuntimeStore::GetShaderCompilerInputs(std::filesystem::path& repoRoot, std::filesystem::path& wrapperPath,
std::filesystem::path& generatedGlslPath, std::filesystem::path& patchedGlslPath, unsigned& maxTemporalHistoryFrames) const
{
std::lock_guard<std::mutex> lock(mMutex);
repoRoot = mRepoRoot;
wrapperPath = mWrapperPath;
generatedGlslPath = mGeneratedGlslPath;
patchedGlslPath = mPatchedGlslPath;
maxTemporalHistoryFrames = mConfig.maxTemporalHistoryFrames;
}
uint64_t RuntimeStore::GetRenderStateVersion() const
{
return mRenderStateVersion.load(std::memory_order_relaxed);
}
uint64_t RuntimeStore::GetParameterStateVersion() const
{
return mParameterStateVersion.load(std::memory_order_relaxed);
}
void RuntimeStore::AdvanceFrameCounter()
{
++mFrameCounter;
}
void RuntimeStore::BuildLayerRenderStates(unsigned outputWidth, unsigned outputHeight, std::vector<RuntimeRenderState>& states) const
{
std::lock_guard<std::mutex> lock(mMutex);
BuildLayerRenderStatesLocked(outputWidth, outputHeight, states);
}
bool RuntimeStore::TryBuildLayerRenderStates(unsigned outputWidth, unsigned outputHeight, std::vector<RuntimeRenderState>& states) const
{
std::unique_lock<std::mutex> lock(mMutex, std::try_to_lock);
if (!lock.owns_lock())
return false;
BuildLayerRenderStatesLocked(outputWidth, outputHeight, states);
return true;
}
bool RuntimeStore::TryRefreshLayerParameters(std::vector<RuntimeRenderState>& states) const
{
std::unique_lock<std::mutex> lock(mMutex, std::try_to_lock);
if (!lock.owns_lock())
return false;
RefreshLayerParametersLocked(states);
return true;
}
void RuntimeStore::RefreshDynamicRenderStateFields(std::vector<RuntimeRenderState>& states) const
{
std::lock_guard<std::mutex> lock(mMutex);
RefreshDynamicRenderStateFieldsLocked(states);
}
void RuntimeStore::BuildLayerRenderStatesLocked(unsigned outputWidth, unsigned outputHeight, std::vector<RuntimeRenderState>& states) const
{
states.clear();
const HealthTelemetry::SignalStatusSnapshot signalStatus = mHealthTelemetry.GetSignalStatusSnapshot();
for (const RuntimeStore::LayerPersistentState& layer : mPersistentState.layers)
{
auto shaderIt = mPackagesById.find(layer.shaderId);
if (shaderIt == mPackagesById.end())
continue;
RuntimeRenderState state;
state.layerId = layer.id;
state.shaderId = layer.shaderId;
state.shaderName = shaderIt->second.displayName;
state.mixAmount = 1.0;
state.bypass = layer.bypass ? 1.0 : 0.0;
state.inputWidth = signalStatus.width;
state.inputHeight = signalStatus.height;
state.outputWidth = outputWidth;
state.outputHeight = outputHeight;
state.parameterDefinitions = shaderIt->second.parameters;
state.textureAssets = shaderIt->second.textureAssets;
state.fontAssets = shaderIt->second.fontAssets;
state.isTemporal = shaderIt->second.temporal.enabled;
state.temporalHistorySource = shaderIt->second.temporal.historySource;
state.requestedTemporalHistoryLength = shaderIt->second.temporal.requestedHistoryLength;
state.effectiveTemporalHistoryLength = shaderIt->second.temporal.effectiveHistoryLength;
state.feedback = shaderIt->second.feedback;
for (const ShaderParameterDefinition& definition : shaderIt->second.parameters)
{
ShaderParameterValue value = DefaultValueForDefinition(definition);
auto valueIt = layer.parameterValues.find(definition.id);
if (valueIt != layer.parameterValues.end())
value = valueIt->second;
state.parameterValues[definition.id] = value;
}
states.push_back(state);
}
RefreshDynamicRenderStateFieldsLocked(states);
}
void RuntimeStore::RefreshLayerParametersLocked(std::vector<RuntimeRenderState>& states) const
{
for (RuntimeRenderState& state : states)
{
const auto layerIt = std::find_if(mPersistentState.layers.begin(), mPersistentState.layers.end(),
[&state](const RuntimeStore::LayerPersistentState& layer) { return layer.id == state.layerId; });
if (layerIt == mPersistentState.layers.end())
continue;
state.bypass = layerIt->bypass ? 1.0 : 0.0;
state.parameterValues.clear();
for (const ShaderParameterDefinition& definition : state.parameterDefinitions)
{
ShaderParameterValue value = DefaultValueForDefinition(definition);
auto valueIt = layerIt->parameterValues.find(definition.id);
if (valueIt != layerIt->parameterValues.end())
value = valueIt->second;
state.parameterValues[definition.id] = value;
}
}
}
void RuntimeStore::RefreshDynamicRenderStateFieldsLocked(std::vector<RuntimeRenderState>& states) const
{
const RuntimeClockSnapshot clock = GetRuntimeClockSnapshot();
const double timeSeconds = std::chrono::duration_cast<std::chrono::duration<double>>(std::chrono::steady_clock::now() - 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 = mStartupRandom;
state.frameCount = frameCount;
}
}
void RuntimeStore::MarkRenderStateDirtyLocked()
{
mRenderStateVersion.fetch_add(1, std::memory_order_relaxed);
mParameterStateVersion.fetch_add(1, std::memory_order_relaxed);
}
void RuntimeStore::MarkParameterStateDirtyLocked()
{
mParameterStateVersion.fetch_add(1, std::memory_order_relaxed);
}
bool RuntimeStore::NormalizeAndValidateValue(const ShaderParameterDefinition& definition, const JsonValue& value, ShaderParameterValue& normalizedValue, std::string& error) const
{
return NormalizeAndValidateParameterValue(definition, value, normalizedValue, error);
}
ShaderParameterValue RuntimeStore::DefaultValueForDefinition(const ShaderParameterDefinition& definition) const
{
return ::DefaultValueForDefinition(definition);
}
void RuntimeStore::EnsureLayerDefaultsLocked(LayerPersistentState& layerState, const ShaderPackage& shaderPackage) const
{
for (const ShaderParameterDefinition& definition : shaderPackage.parameters)
{
auto valueIt = layerState.parameterValues.find(definition.id);
if (valueIt == layerState.parameterValues.end())
{
layerState.parameterValues[definition.id] = DefaultValueForDefinition(definition);
continue;
}
JsonValue valueJson;
bool shouldNormalize = true;
switch (definition.type)
{
case ShaderParameterType::Float:
if (valueIt->second.numberValues.empty())
shouldNormalize = false;
else
valueJson = JsonValue(valueIt->second.numberValues.front());
break;
case ShaderParameterType::Vec2:
case ShaderParameterType::Color:
valueJson = JsonValue::MakeArray();
for (double number : valueIt->second.numberValues)
valueJson.pushBack(JsonValue(number));
break;
case ShaderParameterType::Boolean:
valueJson = JsonValue(valueIt->second.booleanValue);
break;
case ShaderParameterType::Enum:
valueJson = JsonValue(valueIt->second.enumValue);
break;
case ShaderParameterType::Text:
{
const std::string textValue = !valueIt->second.textValue.empty()
? valueIt->second.textValue
: valueIt->second.enumValue;
if (textValue.empty())
{
valueIt->second = DefaultValueForDefinition(definition);
shouldNormalize = false;
}
else
{
valueJson = JsonValue(textValue);
}
break;
}
case ShaderParameterType::Trigger:
if (valueIt->second.numberValues.empty())
valueJson = JsonValue(0.0);
else
valueJson = JsonValue((std::max)(0.0, std::floor(valueIt->second.numberValues.front())));
break;
}
if (!shouldNormalize)
continue;
ShaderParameterValue normalizedValue;
std::string normalizeError;
if (NormalizeAndValidateValue(definition, valueJson, normalizedValue, normalizeError))
valueIt->second = normalizedValue;
else
valueIt->second = DefaultValueForDefinition(definition);
}
}
JsonValue RuntimeStore::SerializeLayerStackLocked() const
{
JsonValue layers = JsonValue::MakeArray();
for (const LayerPersistentState& layer : mPersistentState.layers)
{
auto shaderIt = mPackagesById.find(layer.shaderId);
if (shaderIt == mPackagesById.end())
continue;
JsonValue layerValue = JsonValue::MakeObject();
layerValue.set("id", JsonValue(layer.id));
layerValue.set("shaderId", JsonValue(layer.shaderId));
layerValue.set("shaderName", JsonValue(shaderIt->second.displayName));
layerValue.set("bypass", JsonValue(layer.bypass));
if (shaderIt->second.temporal.enabled)
{
JsonValue temporal = JsonValue::MakeObject();
temporal.set("enabled", JsonValue(true));
temporal.set("historySource", JsonValue(TemporalHistorySourceToString(shaderIt->second.temporal.historySource)));
temporal.set("requestedHistoryLength", JsonValue(static_cast<double>(shaderIt->second.temporal.requestedHistoryLength)));
temporal.set("effectiveHistoryLength", JsonValue(static_cast<double>(shaderIt->second.temporal.effectiveHistoryLength)));
layerValue.set("temporal", temporal);
}
if (shaderIt->second.feedback.enabled)
{
JsonValue feedback = JsonValue::MakeObject();
feedback.set("enabled", JsonValue(true));
feedback.set("writePass", JsonValue(shaderIt->second.feedback.writePassId));
layerValue.set("feedback", feedback);
}
JsonValue parameters = JsonValue::MakeArray();
for (const ShaderParameterDefinition& definition : shaderIt->second.parameters)
{
JsonValue parameter = JsonValue::MakeObject();
parameter.set("id", JsonValue(definition.id));
parameter.set("label", JsonValue(definition.label));
if (!definition.description.empty())
parameter.set("description", JsonValue(definition.description));
parameter.set("type", JsonValue(ShaderParameterTypeToString(definition.type)));
parameter.set("defaultValue", SerializeParameterValue(definition, DefaultValueForDefinition(definition)));
if (!definition.minNumbers.empty())
{
JsonValue minValue = JsonValue::MakeArray();
for (double number : definition.minNumbers)
minValue.pushBack(JsonValue(number));
parameter.set("min", minValue);
}
if (!definition.maxNumbers.empty())
{
JsonValue maxValue = JsonValue::MakeArray();
for (double number : definition.maxNumbers)
maxValue.pushBack(JsonValue(number));
parameter.set("max", maxValue);
}
if (!definition.stepNumbers.empty())
{
JsonValue stepValue = JsonValue::MakeArray();
for (double number : definition.stepNumbers)
stepValue.pushBack(JsonValue(number));
parameter.set("step", stepValue);
}
if (definition.type == ShaderParameterType::Enum)
{
JsonValue options = JsonValue::MakeArray();
for (const ShaderParameterOption& option : definition.enumOptions)
{
JsonValue optionValue = JsonValue::MakeObject();
optionValue.set("value", JsonValue(option.value));
optionValue.set("label", JsonValue(option.label));
options.pushBack(optionValue);
}
parameter.set("options", options);
}
if (definition.type == ShaderParameterType::Text)
{
parameter.set("maxLength", JsonValue(static_cast<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;
}
bool RuntimeStore::DeserializeLayerStackLocked(const JsonValue& layersValue, std::vector<LayerPersistentState>& layers, std::string& error)
{
for (const JsonValue& layerValue : layersValue.asArray())
{
if (!layerValue.isObject())
continue;
const JsonValue* shaderIdValue = layerValue.find("shaderId");
if (!shaderIdValue)
continue;
const std::string shaderId = shaderIdValue->asString();
auto shaderIt = mPackagesById.find(shaderId);
if (shaderIt == mPackagesById.end())
{
error = "Preset references unknown shader id: " + shaderId;
return false;
}
LayerPersistentState layer;
layer.id = GenerateLayerId();
layer.shaderId = shaderId;
if (const JsonValue* bypassValue = layerValue.find("bypass"))
layer.bypass = bypassValue->asBoolean(false);
if (const JsonValue* parametersValue = layerValue.find("parameters"))
{
for (const JsonValue& parameterValue : parametersValue->asArray())
{
if (!parameterValue.isObject())
continue;
const JsonValue* parameterIdValue = parameterValue.find("id");
const JsonValue* valueValue = parameterValue.find("value");
if (!parameterIdValue || !valueValue)
continue;
const std::string parameterId = parameterIdValue->asString();
auto definitionIt = std::find_if(shaderIt->second.parameters.begin(), shaderIt->second.parameters.end(),
[&parameterId](const ShaderParameterDefinition& definition) { return definition.id == parameterId; });
if (definitionIt == shaderIt->second.parameters.end())
continue;
ShaderParameterValue normalizedValue;
if (!NormalizeAndValidateValue(*definitionIt, *valueValue, normalizedValue, error))
return false;
layer.parameterValues[parameterId] = normalizedValue;
}
}
EnsureLayerDefaultsLocked(layer, shaderIt->second);
layers.push_back(layer);
}
return true;
}
void RuntimeStore::NormalizePersistentLayerIdsLocked()
{
std::set<std::string> usedIds;
uint64_t maxLayerNumber = mNextLayerId;
for (LayerPersistentState& layer : mPersistentState.layers)
{
uint64_t layerNumber = 0;
const bool hasReusableId = !layer.id.empty() &&
usedIds.find(layer.id) == usedIds.end() &&
TryParseLayerIdNumber(layer.id, layerNumber);
if (hasReusableId)
{
usedIds.insert(layer.id);
maxLayerNumber = (std::max)(maxLayerNumber, layerNumber);
continue;
}
do
{
++maxLayerNumber;
layer.id = "layer-" + std::to_string(maxLayerNumber);
}
while (usedIds.find(layer.id) != usedIds.end());
usedIds.insert(layer.id);
}
mNextLayerId = maxLayerNumber;
}
JsonValue RuntimeStore::SerializeParameterValue(const ShaderParameterDefinition& definition, const ShaderParameterValue& value) const
{
switch (definition.type)
{
case ShaderParameterType::Boolean:
return JsonValue(value.booleanValue);
case ShaderParameterType::Enum:
return JsonValue(value.enumValue);
case ShaderParameterType::Text:
return JsonValue(value.textValue);
case ShaderParameterType::Trigger:
return JsonValue(value.numberValues.empty() ? 0.0 : value.numberValues.front());
case ShaderParameterType::Float:
return JsonValue(value.numberValues.empty() ? 0.0 : value.numberValues.front());
case ShaderParameterType::Vec2:
case ShaderParameterType::Color:
{
JsonValue array = JsonValue::MakeArray();
for (double number : value.numberValues)
array.pushBack(JsonValue(number));
return array;
}
}
return JsonValue();
}
std::string RuntimeStore::TemporalHistorySourceToString(TemporalHistorySource source) const
{
switch (source)
{
case TemporalHistorySource::Source:
return "source";
case TemporalHistorySource::PreLayerInput:
return "preLayerInput";
case TemporalHistorySource::None:
default:
return "none";
}
}
RuntimeStore::LayerPersistentState* RuntimeStore::FindLayerById(const std::string& layerId)
{
auto it = std::find_if(mPersistentState.layers.begin(), mPersistentState.layers.end(),
[&layerId](const LayerPersistentState& layer) { return layer.id == layerId; });
return it == mPersistentState.layers.end() ? nullptr : &*it;
}
const RuntimeStore::LayerPersistentState* RuntimeStore::FindLayerById(const std::string& layerId) const
{
auto it = std::find_if(mPersistentState.layers.begin(), mPersistentState.layers.end(),
[&layerId](const LayerPersistentState& layer) { return layer.id == layerId; });
return it == mPersistentState.layers.end() ? nullptr : &*it;
}
std::string RuntimeStore::GenerateLayerId()
{
while (true)
{
++mNextLayerId;
const std::string candidate = "layer-" + std::to_string(mNextLayerId);
if (!FindLayerById(candidate))
return candidate;
}
}