1306 lines
46 KiB
C++
1306 lines
46 KiB
C++
#include "RuntimeStore.h"
|
|
|
|
#include "RuntimeClock.h"
|
|
#include "ShaderPackageRegistry.h"
|
|
#include "RuntimeParameterUtils.h"
|
|
|
|
#include <fstream>
|
|
#include <mutex>
|
|
#include <random>
|
|
#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;
|
|
}
|
|
}
|
|
|
|
RuntimeStore::RuntimeStore()
|
|
{
|
|
}
|
|
|
|
HealthTelemetry& RuntimeStore::GetHealthTelemetry()
|
|
{
|
|
return mRuntimeHost.GetHealthTelemetry();
|
|
}
|
|
|
|
const HealthTelemetry& RuntimeStore::GetHealthTelemetry() const
|
|
{
|
|
return mRuntimeHost.GetHealthTelemetry();
|
|
}
|
|
|
|
bool RuntimeStore::InitializeStore(std::string& error)
|
|
{
|
|
try
|
|
{
|
|
std::lock_guard<std::mutex> lock(mRuntimeHost.mMutex);
|
|
|
|
if (!ResolvePaths(error))
|
|
return false;
|
|
if (!LoadConfig(error))
|
|
return false;
|
|
mRuntimeHost.mShaderRoot = mRuntimeHost.mRepoRoot / mRuntimeHost.mConfig.shaderLibrary;
|
|
if (!LoadPersistentState(error))
|
|
return false;
|
|
if (!ScanShaderPackages(error))
|
|
return false;
|
|
mRuntimeHost.NormalizePersistentLayerIdsLocked();
|
|
|
|
for (RuntimeHost::LayerPersistentState& layer : mRuntimeHost.mPersistentState.layers)
|
|
{
|
|
auto shaderIt = mRuntimeHost.mPackagesById.find(layer.shaderId);
|
|
if (shaderIt != mRuntimeHost.mPackagesById.end())
|
|
mRuntimeHost.EnsureLayerDefaultsLocked(layer, shaderIt->second);
|
|
}
|
|
|
|
if (mRuntimeHost.mPersistentState.layers.empty() && !mRuntimeHost.mPackageOrder.empty())
|
|
{
|
|
RuntimeHost::LayerPersistentState layer;
|
|
layer.id = mRuntimeHost.GenerateLayerId();
|
|
layer.shaderId = mRuntimeHost.mPackageOrder.front();
|
|
layer.bypass = false;
|
|
mRuntimeHost.EnsureLayerDefaultsLocked(layer, mRuntimeHost.mPackagesById[layer.shaderId]);
|
|
mRuntimeHost.mPersistentState.layers.push_back(layer);
|
|
}
|
|
|
|
mRuntimeHost.mServerPort = mRuntimeHost.mConfig.serverPort;
|
|
mRuntimeHost.mAutoReloadEnabled = mRuntimeHost.mConfig.autoReload;
|
|
mRuntimeHost.mReloadRequested = true;
|
|
mRuntimeHost.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(mRuntimeHost.mMutex);
|
|
registryChanged = false;
|
|
reloadRequested = false;
|
|
|
|
if (!mRuntimeHost.mAutoReloadEnabled)
|
|
{
|
|
reloadRequested = mRuntimeHost.mReloadRequested;
|
|
return true;
|
|
}
|
|
|
|
const auto now = std::chrono::steady_clock::now();
|
|
if (mRuntimeHost.mLastScanTime != (std::chrono::steady_clock::time_point::min)() &&
|
|
std::chrono::duration_cast<std::chrono::milliseconds>(now - mRuntimeHost.mLastScanTime).count() < 250)
|
|
{
|
|
reloadRequested = mRuntimeHost.mReloadRequested;
|
|
return true;
|
|
}
|
|
|
|
mRuntimeHost.mLastScanTime = now;
|
|
|
|
std::string scanError;
|
|
std::map<std::string, ShaderPackage> previousPackages = mRuntimeHost.mPackagesById;
|
|
std::vector<std::string> previousOrder = mRuntimeHost.mPackageOrder;
|
|
std::map<std::string, std::pair<std::filesystem::file_time_type, std::filesystem::file_time_type>> previousLayerShaderTimes;
|
|
for (const RuntimeHost::LayerPersistentState& layer : mRuntimeHost.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 != mRuntimeHost.mPackageOrder;
|
|
if (!registryChanged && previousPackages.size() == mRuntimeHost.mPackagesById.size())
|
|
{
|
|
for (const auto& item : mRuntimeHost.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 (RuntimeHost::LayerPersistentState& layer : mRuntimeHost.mPersistentState.layers)
|
|
{
|
|
auto active = mRuntimeHost.mPackagesById.find(layer.shaderId);
|
|
auto previous = previousLayerShaderTimes.find(layer.id);
|
|
if (active == mRuntimeHost.mPackagesById.end())
|
|
continue;
|
|
mRuntimeHost.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))))
|
|
{
|
|
mRuntimeHost.mReloadRequested = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
reloadRequested = mRuntimeHost.mReloadRequested;
|
|
if (registryChanged || reloadRequested)
|
|
mRuntimeHost.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(mRuntimeHost.mMutex);
|
|
auto shaderIt = mRuntimeHost.mPackagesById.find(shaderId);
|
|
if (shaderIt == mRuntimeHost.mPackagesById.end())
|
|
{
|
|
error = "Unknown shader id: " + shaderId;
|
|
return false;
|
|
}
|
|
|
|
RuntimeHost::LayerPersistentState layer;
|
|
layer.id = mRuntimeHost.GenerateLayerId();
|
|
layer.shaderId = shaderId;
|
|
layer.bypass = false;
|
|
mRuntimeHost.EnsureLayerDefaultsLocked(layer, shaderIt->second);
|
|
mRuntimeHost.mPersistentState.layers.push_back(layer);
|
|
mRuntimeHost.mReloadRequested = true;
|
|
mRuntimeHost.MarkRenderStateDirtyLocked();
|
|
return SavePersistentState(error);
|
|
}
|
|
|
|
bool RuntimeStore::DeleteStoredLayer(const std::string& layerId, std::string& error)
|
|
{
|
|
std::lock_guard<std::mutex> lock(mRuntimeHost.mMutex);
|
|
auto it = std::find_if(mRuntimeHost.mPersistentState.layers.begin(), mRuntimeHost.mPersistentState.layers.end(),
|
|
[&layerId](const RuntimeHost::LayerPersistentState& layer) { return layer.id == layerId; });
|
|
if (it == mRuntimeHost.mPersistentState.layers.end())
|
|
{
|
|
error = "Unknown layer id: " + layerId;
|
|
return false;
|
|
}
|
|
|
|
mRuntimeHost.mPersistentState.layers.erase(it);
|
|
mRuntimeHost.mReloadRequested = true;
|
|
mRuntimeHost.MarkRenderStateDirtyLocked();
|
|
return SavePersistentState(error);
|
|
}
|
|
|
|
bool RuntimeStore::MoveStoredLayer(const std::string& layerId, int direction, std::string& error)
|
|
{
|
|
std::lock_guard<std::mutex> lock(mRuntimeHost.mMutex);
|
|
auto it = std::find_if(mRuntimeHost.mPersistentState.layers.begin(), mRuntimeHost.mPersistentState.layers.end(),
|
|
[&layerId](const RuntimeHost::LayerPersistentState& layer) { return layer.id == layerId; });
|
|
if (it == mRuntimeHost.mPersistentState.layers.end())
|
|
{
|
|
error = "Unknown layer id: " + layerId;
|
|
return false;
|
|
}
|
|
|
|
const std::ptrdiff_t index = std::distance(mRuntimeHost.mPersistentState.layers.begin(), it);
|
|
const std::ptrdiff_t newIndex = index + direction;
|
|
if (newIndex < 0 || newIndex >= static_cast<std::ptrdiff_t>(mRuntimeHost.mPersistentState.layers.size()))
|
|
return true;
|
|
|
|
std::swap(mRuntimeHost.mPersistentState.layers[index], mRuntimeHost.mPersistentState.layers[newIndex]);
|
|
mRuntimeHost.mReloadRequested = true;
|
|
mRuntimeHost.MarkRenderStateDirtyLocked();
|
|
return SavePersistentState(error);
|
|
}
|
|
|
|
bool RuntimeStore::MoveStoredLayerToIndex(const std::string& layerId, std::size_t targetIndex, std::string& error)
|
|
{
|
|
std::lock_guard<std::mutex> lock(mRuntimeHost.mMutex);
|
|
auto it = std::find_if(mRuntimeHost.mPersistentState.layers.begin(), mRuntimeHost.mPersistentState.layers.end(),
|
|
[&layerId](const RuntimeHost::LayerPersistentState& layer) { return layer.id == layerId; });
|
|
if (it == mRuntimeHost.mPersistentState.layers.end())
|
|
{
|
|
error = "Unknown layer id: " + layerId;
|
|
return false;
|
|
}
|
|
|
|
if (mRuntimeHost.mPersistentState.layers.empty())
|
|
return true;
|
|
|
|
if (targetIndex >= mRuntimeHost.mPersistentState.layers.size())
|
|
targetIndex = mRuntimeHost.mPersistentState.layers.size() - 1;
|
|
|
|
const std::size_t sourceIndex = static_cast<std::size_t>(std::distance(mRuntimeHost.mPersistentState.layers.begin(), it));
|
|
if (sourceIndex == targetIndex)
|
|
return true;
|
|
|
|
RuntimeHost::LayerPersistentState movedLayer = *it;
|
|
mRuntimeHost.mPersistentState.layers.erase(mRuntimeHost.mPersistentState.layers.begin() + static_cast<std::ptrdiff_t>(sourceIndex));
|
|
mRuntimeHost.mPersistentState.layers.insert(mRuntimeHost.mPersistentState.layers.begin() + static_cast<std::ptrdiff_t>(targetIndex), movedLayer);
|
|
mRuntimeHost.mReloadRequested = true;
|
|
mRuntimeHost.MarkRenderStateDirtyLocked();
|
|
return SavePersistentState(error);
|
|
}
|
|
|
|
bool RuntimeStore::SetStoredLayerBypassState(const std::string& layerId, bool bypassed, std::string& error)
|
|
{
|
|
std::lock_guard<std::mutex> lock(mRuntimeHost.mMutex);
|
|
RuntimeHost::LayerPersistentState* layer = mRuntimeHost.FindLayerById(layerId);
|
|
if (!layer)
|
|
{
|
|
error = "Unknown layer id: " + layerId;
|
|
return false;
|
|
}
|
|
|
|
layer->bypass = bypassed;
|
|
mRuntimeHost.mReloadRequested = true;
|
|
mRuntimeHost.MarkParameterStateDirtyLocked();
|
|
return SavePersistentState(error);
|
|
}
|
|
|
|
bool RuntimeStore::SetStoredLayerShaderSelection(const std::string& layerId, const std::string& shaderId, std::string& error)
|
|
{
|
|
std::lock_guard<std::mutex> lock(mRuntimeHost.mMutex);
|
|
RuntimeHost::LayerPersistentState* layer = mRuntimeHost.FindLayerById(layerId);
|
|
if (!layer)
|
|
{
|
|
error = "Unknown layer id: " + layerId;
|
|
return false;
|
|
}
|
|
|
|
auto shaderIt = mRuntimeHost.mPackagesById.find(shaderId);
|
|
if (shaderIt == mRuntimeHost.mPackagesById.end())
|
|
{
|
|
error = "Unknown shader id: " + shaderId;
|
|
return false;
|
|
}
|
|
|
|
layer->shaderId = shaderId;
|
|
layer->parameterValues.clear();
|
|
mRuntimeHost.EnsureLayerDefaultsLocked(*layer, shaderIt->second);
|
|
mRuntimeHost.mReloadRequested = true;
|
|
mRuntimeHost.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(mRuntimeHost.mMutex);
|
|
|
|
RuntimeHost::LayerPersistentState* layer = mRuntimeHost.FindLayerById(layerId);
|
|
if (!layer)
|
|
{
|
|
error = "Unknown layer id: " + layerId;
|
|
return false;
|
|
}
|
|
|
|
layer->parameterValues[parameterId] = value;
|
|
mRuntimeHost.MarkParameterStateDirtyLocked();
|
|
return !persistState || SavePersistentState(error);
|
|
}
|
|
|
|
bool RuntimeStore::ResetStoredLayerParameterValues(const std::string& layerId, std::string& error)
|
|
{
|
|
std::lock_guard<std::mutex> lock(mRuntimeHost.mMutex);
|
|
|
|
RuntimeHost::LayerPersistentState* layer = mRuntimeHost.FindLayerById(layerId);
|
|
if (!layer)
|
|
{
|
|
error = "Unknown layer id: " + layerId;
|
|
return false;
|
|
}
|
|
|
|
auto shaderIt = mRuntimeHost.mPackagesById.find(layer->shaderId);
|
|
if (shaderIt == mRuntimeHost.mPackagesById.end())
|
|
{
|
|
error = "Unknown shader id: " + layer->shaderId;
|
|
return false;
|
|
}
|
|
|
|
layer->parameterValues.clear();
|
|
mRuntimeHost.EnsureLayerDefaultsLocked(*layer, shaderIt->second);
|
|
mRuntimeHost.MarkParameterStateDirtyLocked();
|
|
return SavePersistentState(error);
|
|
}
|
|
|
|
bool RuntimeStore::SaveStackPresetSnapshot(const std::string& presetName, std::string& error) const
|
|
{
|
|
std::lock_guard<std::mutex> lock(mRuntimeHost.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", mRuntimeHost.SerializeLayerStackLocked());
|
|
|
|
return WriteTextFile(mRuntimeHost.mPresetRoot / (safeStem + ".json"), SerializeJson(root, true), error);
|
|
}
|
|
|
|
bool RuntimeStore::LoadStackPresetSnapshot(const std::string& presetName, std::string& error)
|
|
{
|
|
std::lock_guard<std::mutex> lock(mRuntimeHost.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 = mRuntimeHost.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<RuntimeHost::LayerPersistentState> nextLayers;
|
|
if (!mRuntimeHost.DeserializeLayerStackLocked(*layersValue, nextLayers, error))
|
|
return false;
|
|
|
|
if (nextLayers.empty())
|
|
{
|
|
error = "Preset does not contain any valid layers.";
|
|
return false;
|
|
}
|
|
|
|
mRuntimeHost.mPersistentState.layers = nextLayers;
|
|
mRuntimeHost.mReloadRequested = true;
|
|
mRuntimeHost.MarkRenderStateDirtyLocked();
|
|
return SavePersistentState(error);
|
|
}
|
|
|
|
const std::filesystem::path& RuntimeStore::GetRuntimeRepositoryRoot() const
|
|
{
|
|
return mRuntimeHost.mRepoRoot;
|
|
}
|
|
|
|
const std::filesystem::path& RuntimeStore::GetRuntimeUiRoot() const
|
|
{
|
|
return mRuntimeHost.mUiRoot;
|
|
}
|
|
|
|
const std::filesystem::path& RuntimeStore::GetRuntimeDocsRoot() const
|
|
{
|
|
return mRuntimeHost.mDocsRoot;
|
|
}
|
|
|
|
const std::filesystem::path& RuntimeStore::GetRuntimeDataRoot() const
|
|
{
|
|
return mRuntimeHost.mRuntimeRoot;
|
|
}
|
|
|
|
unsigned short RuntimeStore::GetConfiguredControlServerPort() const
|
|
{
|
|
return mRuntimeHost.mServerPort;
|
|
}
|
|
|
|
unsigned short RuntimeStore::GetConfiguredOscPort() const
|
|
{
|
|
return mRuntimeHost.mConfig.oscPort;
|
|
}
|
|
|
|
const std::string& RuntimeStore::GetConfiguredOscBindAddress() const
|
|
{
|
|
return mRuntimeHost.mConfig.oscBindAddress;
|
|
}
|
|
|
|
double RuntimeStore::GetConfiguredOscSmoothing() const
|
|
{
|
|
return mRuntimeHost.mConfig.oscSmoothing;
|
|
}
|
|
|
|
unsigned RuntimeStore::GetConfiguredMaxTemporalHistoryFrames() const
|
|
{
|
|
return mRuntimeHost.mConfig.maxTemporalHistoryFrames;
|
|
}
|
|
|
|
unsigned RuntimeStore::GetConfiguredPreviewFps() const
|
|
{
|
|
return mRuntimeHost.mConfig.previewFps;
|
|
}
|
|
|
|
bool RuntimeStore::IsExternalKeyingConfigured() const
|
|
{
|
|
return mRuntimeHost.mConfig.enableExternalKeying;
|
|
}
|
|
|
|
const std::string& RuntimeStore::GetConfiguredInputVideoFormat() const
|
|
{
|
|
return mRuntimeHost.mConfig.inputVideoFormat;
|
|
}
|
|
|
|
const std::string& RuntimeStore::GetConfiguredInputFrameRate() const
|
|
{
|
|
return mRuntimeHost.mConfig.inputFrameRate;
|
|
}
|
|
|
|
const std::string& RuntimeStore::GetConfiguredOutputVideoFormat() const
|
|
{
|
|
return mRuntimeHost.mConfig.outputVideoFormat;
|
|
}
|
|
|
|
const std::string& RuntimeStore::GetConfiguredOutputFrameRate() const
|
|
{
|
|
return mRuntimeHost.mConfig.outputFrameRate;
|
|
}
|
|
|
|
void RuntimeStore::SetBoundControlServerPort(unsigned short port)
|
|
{
|
|
std::lock_guard<std::mutex> lock(mRuntimeHost.mMutex);
|
|
mRuntimeHost.mServerPort = port;
|
|
}
|
|
|
|
void RuntimeStore::SetCompileStatus(bool succeeded, const std::string& message)
|
|
{
|
|
std::lock_guard<std::mutex> lock(mRuntimeHost.mMutex);
|
|
mRuntimeHost.mCompileSucceeded = succeeded;
|
|
mRuntimeHost.mCompileMessage = message;
|
|
}
|
|
|
|
void RuntimeStore::ClearReloadRequest()
|
|
{
|
|
std::lock_guard<std::mutex> lock(mRuntimeHost.mMutex);
|
|
mRuntimeHost.mReloadRequested = false;
|
|
}
|
|
|
|
JsonValue RuntimeStore::BuildRuntimeStateValue() const
|
|
{
|
|
const HealthTelemetry::Snapshot telemetrySnapshot = mRuntimeHost.mHealthTelemetry.GetSnapshot();
|
|
std::lock_guard<std::mutex> lock(mRuntimeHost.mMutex);
|
|
|
|
JsonValue root = JsonValue::MakeObject();
|
|
|
|
JsonValue app = JsonValue::MakeObject();
|
|
app.set("serverPort", JsonValue(static_cast<double>(mRuntimeHost.mServerPort)));
|
|
app.set("oscPort", JsonValue(static_cast<double>(mRuntimeHost.mConfig.oscPort)));
|
|
app.set("oscBindAddress", JsonValue(mRuntimeHost.mConfig.oscBindAddress));
|
|
app.set("oscSmoothing", JsonValue(mRuntimeHost.mConfig.oscSmoothing));
|
|
app.set("autoReload", JsonValue(mRuntimeHost.mAutoReloadEnabled));
|
|
app.set("maxTemporalHistoryFrames", JsonValue(static_cast<double>(mRuntimeHost.mConfig.maxTemporalHistoryFrames)));
|
|
app.set("previewFps", JsonValue(static_cast<double>(mRuntimeHost.mConfig.previewFps)));
|
|
app.set("enableExternalKeying", JsonValue(mRuntimeHost.mConfig.enableExternalKeying));
|
|
app.set("inputVideoFormat", JsonValue(mRuntimeHost.mConfig.inputVideoFormat));
|
|
app.set("inputFrameRate", JsonValue(mRuntimeHost.mConfig.inputFrameRate));
|
|
app.set("outputVideoFormat", JsonValue(mRuntimeHost.mConfig.outputVideoFormat));
|
|
app.set("outputFrameRate", JsonValue(mRuntimeHost.mConfig.outputFrameRate));
|
|
root.set("app", app);
|
|
|
|
JsonValue runtime = JsonValue::MakeObject();
|
|
runtime.set("layerCount", JsonValue(static_cast<double>(mRuntimeHost.mPersistentState.layers.size())));
|
|
runtime.set("compileSucceeded", JsonValue(mRuntimeHost.mCompileSucceeded));
|
|
runtime.set("compileMessage", JsonValue(mRuntimeHost.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 : mRuntimeHost.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 = mRuntimeHost.mPackagesById.find(status.id);
|
|
if (status.available && shaderIt != mRuntimeHost.mPackagesById.end() && shaderIt->second.temporal.enabled)
|
|
{
|
|
JsonValue temporal = JsonValue::MakeObject();
|
|
temporal.set("enabled", JsonValue(true));
|
|
temporal.set("historySource", JsonValue(mRuntimeHost.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 != mRuntimeHost.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 mRuntimeHost.SerializeLayerStackLocked();
|
|
}
|
|
|
|
bool RuntimeStore::LoadConfig(std::string& error)
|
|
{
|
|
if (!std::filesystem::exists(mRuntimeHost.mConfigPath))
|
|
return true;
|
|
|
|
std::string configText = ReadTextFile(mRuntimeHost.mConfigPath, error);
|
|
if (configText.empty())
|
|
return false;
|
|
|
|
JsonValue configJson;
|
|
if (!ParseJson(configText, configJson, error))
|
|
return false;
|
|
|
|
if (const JsonValue* shaderLibraryValue = configJson.find("shaderLibrary"))
|
|
mRuntimeHost.mConfig.shaderLibrary = shaderLibraryValue->asString();
|
|
if (const JsonValue* serverPortValue = configJson.find("serverPort"))
|
|
mRuntimeHost.mConfig.serverPort = static_cast<unsigned short>(serverPortValue->asNumber(mRuntimeHost.mConfig.serverPort));
|
|
if (const JsonValue* oscPortValue = configJson.find("oscPort"))
|
|
mRuntimeHost.mConfig.oscPort = static_cast<unsigned short>(oscPortValue->asNumber(mRuntimeHost.mConfig.oscPort));
|
|
if (const JsonValue* oscBindAddressValue = configJson.find("oscBindAddress"))
|
|
mRuntimeHost.mConfig.oscBindAddress = oscBindAddressValue->asString();
|
|
if (const JsonValue* oscSmoothingValue = configJson.find("oscSmoothing"))
|
|
mRuntimeHost.mConfig.oscSmoothing = Clamp01(oscSmoothingValue->asNumber(mRuntimeHost.mConfig.oscSmoothing));
|
|
if (const JsonValue* autoReloadValue = configJson.find("autoReload"))
|
|
mRuntimeHost.mConfig.autoReload = autoReloadValue->asBoolean(mRuntimeHost.mConfig.autoReload);
|
|
if (const JsonValue* maxTemporalHistoryFramesValue = configJson.find("maxTemporalHistoryFrames"))
|
|
{
|
|
const double configuredValue = maxTemporalHistoryFramesValue->asNumber(static_cast<double>(mRuntimeHost.mConfig.maxTemporalHistoryFrames));
|
|
mRuntimeHost.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>(mRuntimeHost.mConfig.previewFps));
|
|
mRuntimeHost.mConfig.previewFps = configuredValue <= 0.0 ? 0u : static_cast<unsigned>(configuredValue);
|
|
}
|
|
if (const JsonValue* enableExternalKeyingValue = configJson.find("enableExternalKeying"))
|
|
mRuntimeHost.mConfig.enableExternalKeying = enableExternalKeyingValue->asBoolean(mRuntimeHost.mConfig.enableExternalKeying);
|
|
if (const JsonValue* videoFormatValue = configJson.find("videoFormat"))
|
|
{
|
|
if (videoFormatValue->isString() && !videoFormatValue->asString().empty())
|
|
{
|
|
mRuntimeHost.mConfig.inputVideoFormat = videoFormatValue->asString();
|
|
mRuntimeHost.mConfig.outputVideoFormat = videoFormatValue->asString();
|
|
}
|
|
}
|
|
if (const JsonValue* frameRateValue = configJson.find("frameRate"))
|
|
{
|
|
if (frameRateValue->isString() && !frameRateValue->asString().empty())
|
|
{
|
|
mRuntimeHost.mConfig.inputFrameRate = frameRateValue->asString();
|
|
mRuntimeHost.mConfig.outputFrameRate = frameRateValue->asString();
|
|
}
|
|
else if (frameRateValue->isNumber())
|
|
{
|
|
std::ostringstream stream;
|
|
stream << frameRateValue->asNumber();
|
|
mRuntimeHost.mConfig.inputFrameRate = stream.str();
|
|
mRuntimeHost.mConfig.outputFrameRate = stream.str();
|
|
}
|
|
}
|
|
if (const JsonValue* inputVideoFormatValue = configJson.find("inputVideoFormat"))
|
|
{
|
|
if (inputVideoFormatValue->isString() && !inputVideoFormatValue->asString().empty())
|
|
mRuntimeHost.mConfig.inputVideoFormat = inputVideoFormatValue->asString();
|
|
}
|
|
if (const JsonValue* inputFrameRateValue = configJson.find("inputFrameRate"))
|
|
{
|
|
if (inputFrameRateValue->isString() && !inputFrameRateValue->asString().empty())
|
|
mRuntimeHost.mConfig.inputFrameRate = inputFrameRateValue->asString();
|
|
else if (inputFrameRateValue->isNumber())
|
|
{
|
|
std::ostringstream stream;
|
|
stream << inputFrameRateValue->asNumber();
|
|
mRuntimeHost.mConfig.inputFrameRate = stream.str();
|
|
}
|
|
}
|
|
if (const JsonValue* outputVideoFormatValue = configJson.find("outputVideoFormat"))
|
|
{
|
|
if (outputVideoFormatValue->isString() && !outputVideoFormatValue->asString().empty())
|
|
mRuntimeHost.mConfig.outputVideoFormat = outputVideoFormatValue->asString();
|
|
}
|
|
if (const JsonValue* outputFrameRateValue = configJson.find("outputFrameRate"))
|
|
{
|
|
if (outputFrameRateValue->isString() && !outputFrameRateValue->asString().empty())
|
|
mRuntimeHost.mConfig.outputFrameRate = outputFrameRateValue->asString();
|
|
else if (outputFrameRateValue->isNumber())
|
|
{
|
|
std::ostringstream stream;
|
|
stream << outputFrameRateValue->asNumber();
|
|
mRuntimeHost.mConfig.outputFrameRate = stream.str();
|
|
}
|
|
}
|
|
|
|
mRuntimeHost.mAutoReloadEnabled = mRuntimeHost.mConfig.autoReload;
|
|
return true;
|
|
}
|
|
|
|
bool RuntimeStore::LoadPersistentState(std::string& error)
|
|
{
|
|
if (!std::filesystem::exists(mRuntimeHost.mRuntimeStatePath))
|
|
return true;
|
|
|
|
std::string stateText = ReadTextFile(mRuntimeHost.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;
|
|
RuntimeHost::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())
|
|
mRuntimeHost.mPersistentState.layers.push_back(layer);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
std::string activeShaderId;
|
|
if (const JsonValue* activeShaderValue = root.find("activeShaderId"))
|
|
activeShaderId = activeShaderValue->asString();
|
|
|
|
if (!activeShaderId.empty())
|
|
{
|
|
RuntimeHost::LayerPersistentState layer;
|
|
layer.id = mRuntimeHost.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;
|
|
}
|
|
}
|
|
}
|
|
|
|
mRuntimeHost.mPersistentState.layers.push_back(layer);
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
bool RuntimeStore::SavePersistentState(std::string& error) const
|
|
{
|
|
JsonValue root = JsonValue::MakeObject();
|
|
|
|
JsonValue layers = JsonValue::MakeArray();
|
|
for (const RuntimeHost::LayerPersistentState& layer : mRuntimeHost.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 = mRuntimeHost.mPackagesById.find(layer.shaderId);
|
|
for (const auto& parameterItem : layer.parameterValues)
|
|
{
|
|
const ShaderParameterDefinition* definition = nullptr;
|
|
if (packageIt != mRuntimeHost.mPackagesById.end())
|
|
{
|
|
for (const ShaderParameterDefinition& candidate : packageIt->second.parameters)
|
|
{
|
|
if (candidate.id == parameterItem.first)
|
|
{
|
|
definition = &candidate;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (definition)
|
|
parameterValues.set(parameterItem.first, mRuntimeHost.SerializeParameterValue(*definition, parameterItem.second));
|
|
}
|
|
|
|
layerValue.set("parameterValues", parameterValues);
|
|
layers.pushBack(layerValue);
|
|
}
|
|
root.set("layers", layers);
|
|
|
|
return WriteTextFile(mRuntimeHost.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(mRuntimeHost.mConfig.maxTemporalHistoryFrames);
|
|
if (!registry.Scan(mRuntimeHost.mShaderRoot, packagesById, packageOrder, packageStatuses, error))
|
|
return false;
|
|
|
|
mRuntimeHost.mPackagesById.swap(packagesById);
|
|
mRuntimeHost.mPackageOrder.swap(packageOrder);
|
|
mRuntimeHost.mPackageStatuses.swap(packageStatuses);
|
|
|
|
for (auto it = mRuntimeHost.mPersistentState.layers.begin(); it != mRuntimeHost.mPersistentState.layers.end();)
|
|
{
|
|
if (mRuntimeHost.mPackagesById.find(it->shaderId) == mRuntimeHost.mPackagesById.end())
|
|
it = mRuntimeHost.mPersistentState.layers.erase(it);
|
|
else
|
|
++it;
|
|
}
|
|
|
|
mRuntimeHost.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)
|
|
{
|
|
mRuntimeHost.mRepoRoot = FindRepoRootCandidate();
|
|
if (mRuntimeHost.mRepoRoot.empty())
|
|
{
|
|
error = "Could not locate the repository root from the current runtime path.";
|
|
return false;
|
|
}
|
|
|
|
const std::filesystem::path builtUiRoot = mRuntimeHost.mRepoRoot / "ui" / "dist";
|
|
mRuntimeHost.mUiRoot = std::filesystem::exists(builtUiRoot) ? builtUiRoot : (mRuntimeHost.mRepoRoot / "ui");
|
|
mRuntimeHost.mDocsRoot = mRuntimeHost.mRepoRoot / "docs";
|
|
mRuntimeHost.mConfigPath = mRuntimeHost.mRepoRoot / "config" / "runtime-host.json";
|
|
mRuntimeHost.mShaderRoot = mRuntimeHost.mRepoRoot / mRuntimeHost.mConfig.shaderLibrary;
|
|
mRuntimeHost.mRuntimeRoot = mRuntimeHost.mRepoRoot / "runtime";
|
|
mRuntimeHost.mPresetRoot = mRuntimeHost.mRuntimeRoot / "stack_presets";
|
|
mRuntimeHost.mRuntimeStatePath = mRuntimeHost.mRuntimeRoot / "runtime_state.json";
|
|
mRuntimeHost.mWrapperPath = mRuntimeHost.mRuntimeRoot / "shader_cache" / "active_shader_wrapper.slang";
|
|
mRuntimeHost.mGeneratedGlslPath = mRuntimeHost.mRuntimeRoot / "shader_cache" / "active_shader.raw.frag";
|
|
mRuntimeHost.mPatchedGlslPath = mRuntimeHost.mRuntimeRoot / "shader_cache" / "active_shader.frag";
|
|
|
|
std::error_code fsError;
|
|
std::filesystem::create_directories(mRuntimeHost.mRuntimeRoot / "shader_cache", fsError);
|
|
std::filesystem::create_directories(mRuntimeHost.mPresetRoot, fsError);
|
|
return true;
|
|
}
|
|
|
|
std::vector<std::string> RuntimeStore::GetStackPresetNamesLocked() const
|
|
{
|
|
std::vector<std::string> presetNames;
|
|
std::error_code fsError;
|
|
if (!std::filesystem::exists(mRuntimeHost.mPresetRoot, fsError))
|
|
return presetNames;
|
|
|
|
for (const auto& entry : std::filesystem::directory_iterator(mRuntimeHost.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,
|
|
RuntimeHost::LayerPersistentState*& matchedLayer, const ShaderPackage*& matchedPackage,
|
|
std::vector<ShaderParameterDefinition>::const_iterator& parameterIt, std::string& error) const
|
|
{
|
|
matchedLayer = nullptr;
|
|
matchedPackage = nullptr;
|
|
|
|
for (RuntimeHost::LayerPersistentState& layer : mRuntimeHost.mPersistentState.layers)
|
|
{
|
|
auto shaderIt = mRuntimeHost.mPackagesById.find(layer.shaderId);
|
|
if (shaderIt == mRuntimeHost.mPackagesById.end())
|
|
continue;
|
|
|
|
if (MatchesControlKey(layer.id, layerKey) || MatchesControlKey(shaderIt->second.id, layerKey) ||
|
|
MatchesControlKey(shaderIt->second.displayName, layerKey))
|
|
{
|
|
matchedLayer = &layer;
|
|
matchedPackage = &shaderIt->second;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!matchedLayer || !matchedPackage)
|
|
{
|
|
error = "Unknown OSC layer key: " + layerKey;
|
|
return false;
|
|
}
|
|
|
|
parameterIt = std::find_if(matchedPackage->parameters.begin(), matchedPackage->parameters.end(),
|
|
[¶meterKey](const ShaderParameterDefinition& definition)
|
|
{
|
|
return MatchesControlKey(definition.id, parameterKey) || MatchesControlKey(definition.label, parameterKey);
|
|
});
|
|
if (parameterIt == matchedPackage->parameters.end())
|
|
{
|
|
error = "Unknown OSC parameter key: " + parameterKey;
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
bool RuntimeStore::CopyShaderPackageForStoredLayer(const std::string& layerId, ShaderPackage& shaderPackage, std::string& error) const
|
|
{
|
|
std::lock_guard<std::mutex> lock(mRuntimeHost.mMutex);
|
|
const RuntimeHost::LayerPersistentState* layer = mRuntimeHost.FindLayerById(layerId);
|
|
if (!layer)
|
|
{
|
|
error = "Unknown layer id: " + layerId;
|
|
return false;
|
|
}
|
|
|
|
auto it = mRuntimeHost.mPackagesById.find(layer->shaderId);
|
|
if (it == mRuntimeHost.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(mRuntimeHost.mMutex);
|
|
repoRoot = mRuntimeHost.mRepoRoot;
|
|
wrapperPath = mRuntimeHost.mWrapperPath;
|
|
generatedGlslPath = mRuntimeHost.mGeneratedGlslPath;
|
|
patchedGlslPath = mRuntimeHost.mPatchedGlslPath;
|
|
maxTemporalHistoryFrames = mRuntimeHost.mConfig.maxTemporalHistoryFrames;
|
|
}
|
|
|
|
uint64_t RuntimeStore::GetRenderStateVersion() const
|
|
{
|
|
return mRuntimeHost.mRenderStateVersion.load(std::memory_order_relaxed);
|
|
}
|
|
|
|
uint64_t RuntimeStore::GetParameterStateVersion() const
|
|
{
|
|
return mRuntimeHost.mParameterStateVersion.load(std::memory_order_relaxed);
|
|
}
|
|
|
|
void RuntimeStore::AdvanceFrameCounter()
|
|
{
|
|
++mRuntimeHost.mFrameCounter;
|
|
}
|
|
|
|
void RuntimeStore::BuildLayerRenderStates(unsigned outputWidth, unsigned outputHeight, std::vector<RuntimeRenderState>& states) const
|
|
{
|
|
std::lock_guard<std::mutex> lock(mRuntimeHost.mMutex);
|
|
BuildLayerRenderStatesLocked(outputWidth, outputHeight, states);
|
|
}
|
|
|
|
bool RuntimeStore::TryBuildLayerRenderStates(unsigned outputWidth, unsigned outputHeight, std::vector<RuntimeRenderState>& states) const
|
|
{
|
|
std::unique_lock<std::mutex> lock(mRuntimeHost.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(mRuntimeHost.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(mRuntimeHost.mMutex);
|
|
RefreshDynamicRenderStateFieldsLocked(states);
|
|
}
|
|
|
|
void RuntimeStore::BuildLayerRenderStatesLocked(unsigned outputWidth, unsigned outputHeight, std::vector<RuntimeRenderState>& states) const
|
|
{
|
|
states.clear();
|
|
const HealthTelemetry::SignalStatusSnapshot signalStatus = mRuntimeHost.mHealthTelemetry.GetSignalStatusSnapshot();
|
|
|
|
for (const RuntimeHost::LayerPersistentState& layer : mRuntimeHost.mPersistentState.layers)
|
|
{
|
|
auto shaderIt = mRuntimeHost.mPackagesById.find(layer.shaderId);
|
|
if (shaderIt == mRuntimeHost.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 = mRuntimeHost.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(mRuntimeHost.mPersistentState.layers.begin(), mRuntimeHost.mPersistentState.layers.end(),
|
|
[&state](const RuntimeHost::LayerPersistentState& layer) { return layer.id == state.layerId; });
|
|
if (layerIt == mRuntimeHost.mPersistentState.layers.end())
|
|
continue;
|
|
|
|
state.bypass = layerIt->bypass ? 1.0 : 0.0;
|
|
state.parameterValues.clear();
|
|
for (const ShaderParameterDefinition& definition : state.parameterDefinitions)
|
|
{
|
|
ShaderParameterValue value = mRuntimeHost.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() - mRuntimeHost.mStartTime).count();
|
|
const double frameCount = static_cast<double>(mRuntimeHost.mFrameCounter.load(std::memory_order_relaxed));
|
|
|
|
for (RuntimeRenderState& state : states)
|
|
{
|
|
state.timeSeconds = timeSeconds;
|
|
state.utcTimeSeconds = clock.utcTimeSeconds;
|
|
state.utcOffsetSeconds = clock.utcOffsetSeconds;
|
|
state.startupRandom = mRuntimeHost.mStartupRandom;
|
|
state.frameCount = frameCount;
|
|
}
|
|
}
|