1018 lines
32 KiB
C++
1018 lines
32 KiB
C++
#include "stdafx.h"
|
|
#include "RuntimeHost.h"
|
|
|
|
#include "RuntimeClock.h"
|
|
#include "RuntimeParameterUtils.h"
|
|
#include "ShaderPackageRegistry.h"
|
|
|
|
#include <algorithm>
|
|
#include <cmath>
|
|
#include <fstream>
|
|
#include <random>
|
|
#include <set>
|
|
#include <sstream>
|
|
|
|
namespace
|
|
{
|
|
std::string Trim(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);
|
|
}
|
|
|
|
bool IsFiniteNumber(double value)
|
|
{
|
|
return std::isfinite(value) != 0;
|
|
}
|
|
|
|
double Clamp01(double value)
|
|
{
|
|
return std::max(0.0, std::min(1.0, value));
|
|
}
|
|
|
|
std::string ToLowerCopy(std::string text)
|
|
{
|
|
std::transform(text.begin(), text.end(), text.begin(),
|
|
[](unsigned char ch) { return static_cast<char>(std::tolower(ch)); });
|
|
return text;
|
|
}
|
|
|
|
std::string SimplifyControlKey(const std::string& text)
|
|
{
|
|
std::string simplified;
|
|
for (unsigned char ch : text)
|
|
{
|
|
if (std::isalnum(ch))
|
|
simplified.push_back(static_cast<char>(std::tolower(ch)));
|
|
}
|
|
return simplified;
|
|
}
|
|
|
|
bool MatchesControlKey(const std::string& candidate, const std::string& key)
|
|
{
|
|
return candidate == key || SimplifyControlKey(candidate) == SimplifyControlKey(key);
|
|
}
|
|
|
|
bool JsonValueContainsOnlyNumbers(const JsonValue& value)
|
|
{
|
|
if (!value.isArray())
|
|
return false;
|
|
|
|
for (const JsonValue& item : value.asArray())
|
|
{
|
|
if (!item.isNumber() || !IsFiniteNumber(item.asNumber()))
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
double GenerateStartupRandom()
|
|
{
|
|
std::random_device randomDevice;
|
|
std::uniform_real_distribution<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;
|
|
}
|
|
|
|
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();
|
|
}
|
|
|
|
std::string ShaderParameterTypeToString(ShaderParameterType type)
|
|
{
|
|
switch (type)
|
|
{
|
|
case ShaderParameterType::Float: return "float";
|
|
case ShaderParameterType::Vec2: return "vec2";
|
|
case ShaderParameterType::Color: return "color";
|
|
case ShaderParameterType::Boolean: return "bool";
|
|
case ShaderParameterType::Enum: return "enum";
|
|
case ShaderParameterType::Text: return "text";
|
|
case ShaderParameterType::Trigger: return "trigger";
|
|
}
|
|
return "unknown";
|
|
}
|
|
|
|
bool ParseTemporalHistorySource(const std::string& sourceName, TemporalHistorySource& source)
|
|
{
|
|
if (sourceName == "source")
|
|
{
|
|
source = TemporalHistorySource::Source;
|
|
return true;
|
|
}
|
|
if (sourceName == "preLayerInput")
|
|
{
|
|
source = TemporalHistorySource::PreLayerInput;
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
bool ParseShaderParameterType(const std::string& typeName, ShaderParameterType& type)
|
|
{
|
|
if (typeName == "float")
|
|
{
|
|
type = ShaderParameterType::Float;
|
|
return true;
|
|
}
|
|
if (typeName == "vec2")
|
|
{
|
|
type = ShaderParameterType::Vec2;
|
|
return true;
|
|
}
|
|
if (typeName == "color")
|
|
{
|
|
type = ShaderParameterType::Color;
|
|
return true;
|
|
}
|
|
if (typeName == "bool")
|
|
{
|
|
type = ShaderParameterType::Boolean;
|
|
return true;
|
|
}
|
|
if (typeName == "enum")
|
|
{
|
|
type = ShaderParameterType::Enum;
|
|
return true;
|
|
}
|
|
if (typeName == "text")
|
|
{
|
|
type = ShaderParameterType::Text;
|
|
return true;
|
|
}
|
|
if (typeName == "trigger")
|
|
{
|
|
type = ShaderParameterType::Trigger;
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
std::string ManifestPathMessage(const std::filesystem::path& manifestPath)
|
|
{
|
|
return manifestPath.string();
|
|
}
|
|
|
|
bool RequireStringField(const JsonValue& object, const char* fieldName, std::string& value, const std::filesystem::path& manifestPath, std::string& error)
|
|
{
|
|
const JsonValue* fieldValue = object.find(fieldName);
|
|
if (!fieldValue || !fieldValue->isString())
|
|
{
|
|
error = "Shader manifest is missing required string '" + std::string(fieldName) + "' field: " + ManifestPathMessage(manifestPath);
|
|
return false;
|
|
}
|
|
|
|
value = fieldValue->asString();
|
|
return true;
|
|
}
|
|
|
|
bool RequireNonEmptyStringField(const JsonValue& object, const char* fieldName, std::string& value, const std::filesystem::path& manifestPath, std::string& error)
|
|
{
|
|
if (!RequireStringField(object, fieldName, value, manifestPath, error))
|
|
return false;
|
|
if (Trim(value).empty())
|
|
{
|
|
error = "Shader manifest string '" + std::string(fieldName) + "' must not be empty: " + ManifestPathMessage(manifestPath);
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool OptionalStringField(const JsonValue& object, const char* fieldName, std::string& value, const std::string& fallback, const std::filesystem::path& manifestPath, std::string& error)
|
|
{
|
|
const JsonValue* fieldValue = object.find(fieldName);
|
|
if (!fieldValue)
|
|
{
|
|
value = fallback;
|
|
return true;
|
|
}
|
|
if (!fieldValue->isString())
|
|
{
|
|
error = "Shader manifest field '" + std::string(fieldName) + "' must be a string in: " + ManifestPathMessage(manifestPath);
|
|
return false;
|
|
}
|
|
value = fieldValue->asString();
|
|
return true;
|
|
}
|
|
|
|
bool OptionalArrayField(const JsonValue& object, const char* fieldName, const JsonValue*& value, const std::filesystem::path& manifestPath, std::string& error)
|
|
{
|
|
value = object.find(fieldName);
|
|
if (!value)
|
|
return true;
|
|
if (!value->isArray())
|
|
{
|
|
error = "Shader manifest '" + std::string(fieldName) + "' field must be an array in: " + ManifestPathMessage(manifestPath);
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool OptionalObjectField(const JsonValue& object, const char* fieldName, const JsonValue*& value, const std::filesystem::path& manifestPath, std::string& error)
|
|
{
|
|
value = object.find(fieldName);
|
|
if (!value)
|
|
return true;
|
|
if (!value->isObject())
|
|
{
|
|
error = "Shader manifest '" + std::string(fieldName) + "' field must be an object in: " + ManifestPathMessage(manifestPath);
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool NumberListFromJsonValue(const JsonValue& value, std::vector<double>& numbers, const std::string& fieldName, const std::filesystem::path& manifestPath, std::string& error)
|
|
{
|
|
if (value.isNumber())
|
|
{
|
|
numbers.push_back(value.asNumber());
|
|
return true;
|
|
}
|
|
if (value.isArray())
|
|
{
|
|
numbers = JsonArrayToNumbers(value);
|
|
if (numbers.size() != value.asArray().size())
|
|
{
|
|
error = "Shader parameter field '" + fieldName + "' must contain only numbers in: " + ManifestPathMessage(manifestPath);
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
error = "Shader parameter field '" + fieldName + "' must be a number or array of numbers in: " + ManifestPathMessage(manifestPath);
|
|
return false;
|
|
}
|
|
|
|
bool ValidateShaderIdentifier(const std::string& identifier, const std::string& fieldName, const std::filesystem::path& manifestPath, std::string& error)
|
|
{
|
|
if (identifier.empty() || !(std::isalpha(static_cast<unsigned char>(identifier.front())) || identifier.front() == '_'))
|
|
{
|
|
error = "Shader manifest field '" + fieldName + "' must be a valid shader identifier in: " + ManifestPathMessage(manifestPath);
|
|
return false;
|
|
}
|
|
|
|
for (char ch : identifier)
|
|
{
|
|
const unsigned char unsignedCh = static_cast<unsigned char>(ch);
|
|
if (!(std::isalnum(unsignedCh) || ch == '_'))
|
|
{
|
|
error = "Shader manifest field '" + fieldName + "' must be a valid shader identifier in: " + ManifestPathMessage(manifestPath);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
bool ParseShaderMetadata(const JsonValue& manifestJson, ShaderPackage& shaderPackage, const std::filesystem::path& manifestPath, std::string& error)
|
|
{
|
|
if (!RequireStringField(manifestJson, "id", shaderPackage.id, manifestPath, error) ||
|
|
!RequireStringField(manifestJson, "name", shaderPackage.displayName, manifestPath, error) ||
|
|
!OptionalStringField(manifestJson, "description", shaderPackage.description, "", manifestPath, error) ||
|
|
!OptionalStringField(manifestJson, "category", shaderPackage.category, "", manifestPath, error) ||
|
|
!OptionalStringField(manifestJson, "entryPoint", shaderPackage.entryPoint, "shadeVideo", manifestPath, error))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (!ValidateShaderIdentifier(shaderPackage.entryPoint, "entryPoint", manifestPath, error))
|
|
return false;
|
|
|
|
shaderPackage.directoryPath = manifestPath.parent_path();
|
|
shaderPackage.shaderPath = shaderPackage.directoryPath / "shader.slang";
|
|
shaderPackage.manifestPath = manifestPath;
|
|
return true;
|
|
}
|
|
|
|
bool ParseTextureAssets(const JsonValue& manifestJson, ShaderPackage& shaderPackage, const std::filesystem::path& manifestPath, std::string& error)
|
|
{
|
|
const JsonValue* texturesValue = nullptr;
|
|
if (!OptionalArrayField(manifestJson, "textures", texturesValue, manifestPath, error))
|
|
return false;
|
|
if (!texturesValue)
|
|
return true;
|
|
|
|
for (const JsonValue& textureJson : texturesValue->asArray())
|
|
{
|
|
if (!textureJson.isObject())
|
|
{
|
|
error = "Shader texture entry must be an object in: " + ManifestPathMessage(manifestPath);
|
|
return false;
|
|
}
|
|
|
|
std::string textureId;
|
|
std::string texturePath;
|
|
if (!RequireNonEmptyStringField(textureJson, "id", textureId, manifestPath, error) ||
|
|
!RequireNonEmptyStringField(textureJson, "path", texturePath, manifestPath, error))
|
|
{
|
|
error = "Shader texture is missing required 'id' or 'path' in: " + ManifestPathMessage(manifestPath);
|
|
return false;
|
|
}
|
|
if (!ValidateShaderIdentifier(textureId, "textures[].id", manifestPath, error))
|
|
return false;
|
|
|
|
ShaderTextureAsset textureAsset;
|
|
textureAsset.id = textureId;
|
|
textureAsset.path = shaderPackage.directoryPath / texturePath;
|
|
if (!std::filesystem::exists(textureAsset.path))
|
|
{
|
|
error = "Shader texture asset not found for package " + shaderPackage.id + ": " + textureAsset.path.string();
|
|
return false;
|
|
}
|
|
|
|
textureAsset.writeTime = std::filesystem::last_write_time(textureAsset.path);
|
|
shaderPackage.textureAssets.push_back(textureAsset);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
bool ParseFontAssets(const JsonValue& manifestJson, ShaderPackage& shaderPackage, const std::filesystem::path& manifestPath, std::string& error)
|
|
{
|
|
const JsonValue* fontsValue = nullptr;
|
|
if (!OptionalArrayField(manifestJson, "fonts", fontsValue, manifestPath, error))
|
|
return false;
|
|
if (!fontsValue)
|
|
return true;
|
|
|
|
for (const JsonValue& fontJson : fontsValue->asArray())
|
|
{
|
|
if (!fontJson.isObject())
|
|
{
|
|
error = "Shader font entry must be an object in: " + ManifestPathMessage(manifestPath);
|
|
return false;
|
|
}
|
|
|
|
std::string fontId;
|
|
std::string fontPath;
|
|
if (!RequireNonEmptyStringField(fontJson, "id", fontId, manifestPath, error) ||
|
|
!RequireNonEmptyStringField(fontJson, "path", fontPath, manifestPath, error))
|
|
{
|
|
error = "Shader font is missing required 'id' or 'path' in: " + ManifestPathMessage(manifestPath);
|
|
return false;
|
|
}
|
|
if (!ValidateShaderIdentifier(fontId, "fonts[].id", manifestPath, error))
|
|
return false;
|
|
|
|
ShaderFontAsset fontAsset;
|
|
fontAsset.id = fontId;
|
|
fontAsset.path = shaderPackage.directoryPath / fontPath;
|
|
if (!std::filesystem::exists(fontAsset.path))
|
|
{
|
|
error = "Shader font asset not found for package " + shaderPackage.id + ": " + fontAsset.path.string();
|
|
return false;
|
|
}
|
|
|
|
fontAsset.writeTime = std::filesystem::last_write_time(fontAsset.path);
|
|
shaderPackage.fontAssets.push_back(fontAsset);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
bool ParseTemporalSettings(const JsonValue& manifestJson, ShaderPackage& shaderPackage, unsigned maxTemporalHistoryFrames, const std::filesystem::path& manifestPath, std::string& error)
|
|
{
|
|
const JsonValue* temporalValue = nullptr;
|
|
if (!OptionalObjectField(manifestJson, "temporal", temporalValue, manifestPath, error))
|
|
return false;
|
|
if (!temporalValue)
|
|
return true;
|
|
|
|
const JsonValue* enabledValue = temporalValue->find("enabled");
|
|
if (!enabledValue || !enabledValue->asBoolean(false))
|
|
return true;
|
|
|
|
std::string historySourceName;
|
|
if (!RequireNonEmptyStringField(*temporalValue, "historySource", historySourceName, manifestPath, error))
|
|
{
|
|
error = "Temporal shader is missing required 'historySource' in: " + ManifestPathMessage(manifestPath);
|
|
return false;
|
|
}
|
|
|
|
const JsonValue* historyLengthValue = temporalValue->find("historyLength");
|
|
if (!historyLengthValue || !historyLengthValue->isNumber())
|
|
{
|
|
error = "Temporal shader is missing required numeric 'historyLength' in: " + ManifestPathMessage(manifestPath);
|
|
return false;
|
|
}
|
|
|
|
TemporalHistorySource historySource = TemporalHistorySource::None;
|
|
if (!ParseTemporalHistorySource(historySourceName, historySource))
|
|
{
|
|
error = "Unsupported temporal historySource '" + historySourceName + "' in: " + ManifestPathMessage(manifestPath);
|
|
return false;
|
|
}
|
|
|
|
const double requestedHistoryLength = historyLengthValue->asNumber();
|
|
if (!IsFiniteNumber(requestedHistoryLength) || requestedHistoryLength <= 0.0 || std::floor(requestedHistoryLength) != requestedHistoryLength)
|
|
{
|
|
error = "Temporal shader 'historyLength' must be a positive integer in: " + ManifestPathMessage(manifestPath);
|
|
return false;
|
|
}
|
|
|
|
shaderPackage.temporal.enabled = true;
|
|
shaderPackage.temporal.historySource = historySource;
|
|
shaderPackage.temporal.requestedHistoryLength = static_cast<unsigned>(requestedHistoryLength);
|
|
shaderPackage.temporal.effectiveHistoryLength = std::min(shaderPackage.temporal.requestedHistoryLength, maxTemporalHistoryFrames);
|
|
return true;
|
|
}
|
|
|
|
bool ParseParameterNumberField(const JsonValue& parameterJson, const char* fieldName, std::vector<double>& values, const std::filesystem::path& manifestPath, std::string& error)
|
|
{
|
|
if (const JsonValue* fieldValue = parameterJson.find(fieldName))
|
|
return NumberListFromJsonValue(*fieldValue, values, fieldName, manifestPath, error);
|
|
return true;
|
|
}
|
|
|
|
bool ParseParameterDefault(const JsonValue& parameterJson, ShaderParameterDefinition& definition, const std::filesystem::path& manifestPath, std::string& error)
|
|
{
|
|
const JsonValue* defaultValue = parameterJson.find("default");
|
|
if (!defaultValue)
|
|
return true;
|
|
|
|
if (definition.type == ShaderParameterType::Boolean)
|
|
{
|
|
if (!defaultValue->isBoolean())
|
|
{
|
|
error = "Boolean parameter default must be a boolean for: " + definition.id;
|
|
return false;
|
|
}
|
|
definition.defaultBoolean = defaultValue->asBoolean(false);
|
|
return true;
|
|
}
|
|
|
|
if (definition.type == ShaderParameterType::Enum)
|
|
{
|
|
if (!defaultValue->isString())
|
|
{
|
|
error = "Enum parameter default must be a string for: " + definition.id;
|
|
return false;
|
|
}
|
|
definition.defaultEnumValue = defaultValue->asString();
|
|
return true;
|
|
}
|
|
|
|
if (definition.type == ShaderParameterType::Text)
|
|
{
|
|
if (!defaultValue->isString())
|
|
{
|
|
error = "Text parameter default must be a string for: " + definition.id;
|
|
return false;
|
|
}
|
|
definition.defaultTextValue = defaultValue->asString();
|
|
return true;
|
|
}
|
|
|
|
return NumberListFromJsonValue(*defaultValue, definition.defaultNumbers, "default", manifestPath, error);
|
|
}
|
|
|
|
bool ParseParameterOptions(const JsonValue& parameterJson, ShaderParameterDefinition& definition, const std::filesystem::path& manifestPath, std::string& error)
|
|
{
|
|
const JsonValue* optionsValue = nullptr;
|
|
if (!OptionalArrayField(parameterJson, "options", optionsValue, manifestPath, error) || !optionsValue)
|
|
{
|
|
error = "Enum parameter is missing 'options' in: " + ManifestPathMessage(manifestPath);
|
|
return false;
|
|
}
|
|
|
|
for (const JsonValue& optionJson : optionsValue->asArray())
|
|
{
|
|
if (!optionJson.isObject())
|
|
{
|
|
error = "Enum parameter option must be an object in: " + ManifestPathMessage(manifestPath);
|
|
return false;
|
|
}
|
|
|
|
ShaderParameterOption option;
|
|
if (!RequireStringField(optionJson, "value", option.value, manifestPath, error) ||
|
|
!RequireStringField(optionJson, "label", option.label, manifestPath, error))
|
|
{
|
|
error = "Enum parameter option is missing 'value' or 'label' in: " + ManifestPathMessage(manifestPath);
|
|
return false;
|
|
}
|
|
definition.enumOptions.push_back(option);
|
|
}
|
|
|
|
bool defaultFound = definition.defaultEnumValue.empty();
|
|
for (const ShaderParameterOption& option : definition.enumOptions)
|
|
{
|
|
if (option.value == definition.defaultEnumValue)
|
|
{
|
|
defaultFound = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!defaultFound)
|
|
{
|
|
error = "Enum parameter default is not present in its option list for: " + definition.id;
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
bool ParseParameterDefinition(const JsonValue& parameterJson, ShaderParameterDefinition& definition, const std::filesystem::path& manifestPath, std::string& error)
|
|
{
|
|
if (!parameterJson.isObject())
|
|
{
|
|
error = "Shader parameter entry must be an object in: " + ManifestPathMessage(manifestPath);
|
|
return false;
|
|
}
|
|
|
|
std::string typeName;
|
|
if (!RequireStringField(parameterJson, "id", definition.id, manifestPath, error) ||
|
|
!RequireStringField(parameterJson, "label", definition.label, manifestPath, error) ||
|
|
!RequireStringField(parameterJson, "type", typeName, manifestPath, error))
|
|
{
|
|
error = "Shader parameter is missing required fields in: " + ManifestPathMessage(manifestPath);
|
|
return false;
|
|
}
|
|
|
|
if (!ParseShaderParameterType(typeName, definition.type))
|
|
{
|
|
error = "Unsupported parameter type '" + typeName + "' in: " + ManifestPathMessage(manifestPath);
|
|
return false;
|
|
}
|
|
if (!ValidateShaderIdentifier(definition.id, "parameters[].id", manifestPath, error))
|
|
return false;
|
|
|
|
if (!OptionalStringField(parameterJson, "description", definition.description, "", manifestPath, error))
|
|
return false;
|
|
|
|
if (!ParseParameterDefault(parameterJson, definition, manifestPath, error) ||
|
|
!ParseParameterNumberField(parameterJson, "min", definition.minNumbers, manifestPath, error) ||
|
|
!ParseParameterNumberField(parameterJson, "max", definition.maxNumbers, manifestPath, error) ||
|
|
!ParseParameterNumberField(parameterJson, "step", definition.stepNumbers, manifestPath, error))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (definition.type == ShaderParameterType::Text)
|
|
{
|
|
if (const JsonValue* fontValue = parameterJson.find("font"))
|
|
{
|
|
if (!fontValue->isString())
|
|
{
|
|
error = "Text parameter 'font' must be a string for: " + definition.id;
|
|
return false;
|
|
}
|
|
definition.fontId = fontValue->asString();
|
|
if (!definition.fontId.empty() && !ValidateShaderIdentifier(definition.fontId, "parameters[].font", manifestPath, error))
|
|
return false;
|
|
}
|
|
if (const JsonValue* maxLengthValue = parameterJson.find("maxLength"))
|
|
{
|
|
if (!maxLengthValue->isNumber() || maxLengthValue->asNumber() < 1.0 || maxLengthValue->asNumber() > 256.0)
|
|
{
|
|
error = "Text parameter 'maxLength' must be a number from 1 to 256 for: " + definition.id;
|
|
return false;
|
|
}
|
|
definition.maxLength = static_cast<unsigned>(maxLengthValue->asNumber());
|
|
}
|
|
}
|
|
|
|
if (definition.type == ShaderParameterType::Enum)
|
|
return ParseParameterOptions(parameterJson, definition, manifestPath, error);
|
|
|
|
return true;
|
|
}
|
|
|
|
bool ParseParameterDefinitions(const JsonValue& manifestJson, ShaderPackage& shaderPackage, const std::filesystem::path& manifestPath, std::string& error)
|
|
{
|
|
const JsonValue* parametersValue = nullptr;
|
|
if (!OptionalArrayField(manifestJson, "parameters", parametersValue, manifestPath, error))
|
|
return false;
|
|
if (!parametersValue)
|
|
return true;
|
|
|
|
for (const JsonValue& parameterJson : parametersValue->asArray())
|
|
{
|
|
ShaderParameterDefinition definition;
|
|
if (!ParseParameterDefinition(parameterJson, definition, manifestPath, error))
|
|
return false;
|
|
shaderPackage.parameters.push_back(definition);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
}
|
|
|
|
RuntimeHost::RuntimeHost()
|
|
: mHealthTelemetry(),
|
|
mReloadRequested(false),
|
|
mCompileSucceeded(false),
|
|
mStartupRandom(GenerateStartupRandom()),
|
|
mServerPort(8080),
|
|
mAutoReloadEnabled(true),
|
|
mStartTime(std::chrono::steady_clock::now()),
|
|
mLastScanTime(std::chrono::steady_clock::time_point::min()),
|
|
mFrameCounter(0),
|
|
mNextLayerId(0)
|
|
{
|
|
}
|
|
|
|
void RuntimeHost::MarkRenderStateDirtyLocked()
|
|
{
|
|
mRenderStateVersion.fetch_add(1, std::memory_order_relaxed);
|
|
mParameterStateVersion.fetch_add(1, std::memory_order_relaxed);
|
|
}
|
|
|
|
void RuntimeHost::MarkParameterStateDirtyLocked()
|
|
{
|
|
mParameterStateVersion.fetch_add(1, std::memory_order_relaxed);
|
|
}
|
|
|
|
bool RuntimeHost::NormalizeAndValidateValue(const ShaderParameterDefinition& definition, const JsonValue& value, ShaderParameterValue& normalizedValue, std::string& error) const
|
|
{
|
|
return NormalizeAndValidateParameterValue(definition, value, normalizedValue, error);
|
|
}
|
|
|
|
ShaderParameterValue RuntimeHost::DefaultValueForDefinition(const ShaderParameterDefinition& definition) const
|
|
{
|
|
return ::DefaultValueForDefinition(definition);
|
|
}
|
|
|
|
void RuntimeHost::EnsureLayerDefaultsLocked(LayerPersistentState& layerState, const ShaderPackage& shaderPackage) const
|
|
{
|
|
for (const ShaderParameterDefinition& definition : shaderPackage.parameters)
|
|
{
|
|
auto valueIt = layerState.parameterValues.find(definition.id);
|
|
if (valueIt == layerState.parameterValues.end())
|
|
{
|
|
layerState.parameterValues[definition.id] = DefaultValueForDefinition(definition);
|
|
continue;
|
|
}
|
|
|
|
JsonValue valueJson;
|
|
bool shouldNormalize = true;
|
|
switch (definition.type)
|
|
{
|
|
case ShaderParameterType::Float:
|
|
if (valueIt->second.numberValues.empty())
|
|
shouldNormalize = false;
|
|
else
|
|
valueJson = JsonValue(valueIt->second.numberValues.front());
|
|
break;
|
|
case ShaderParameterType::Vec2:
|
|
case ShaderParameterType::Color:
|
|
valueJson = JsonValue::MakeArray();
|
|
for (double number : valueIt->second.numberValues)
|
|
valueJson.pushBack(JsonValue(number));
|
|
break;
|
|
case ShaderParameterType::Boolean:
|
|
valueJson = JsonValue(valueIt->second.booleanValue);
|
|
break;
|
|
case ShaderParameterType::Enum:
|
|
valueJson = JsonValue(valueIt->second.enumValue);
|
|
break;
|
|
case ShaderParameterType::Text:
|
|
{
|
|
const std::string textValue = !valueIt->second.textValue.empty()
|
|
? valueIt->second.textValue
|
|
: valueIt->second.enumValue;
|
|
if (textValue.empty())
|
|
{
|
|
valueIt->second = DefaultValueForDefinition(definition);
|
|
shouldNormalize = false;
|
|
}
|
|
else
|
|
{
|
|
valueJson = JsonValue(textValue);
|
|
}
|
|
break;
|
|
}
|
|
case ShaderParameterType::Trigger:
|
|
if (valueIt->second.numberValues.empty())
|
|
valueJson = JsonValue(0.0);
|
|
else
|
|
valueJson = JsonValue(std::max(0.0, std::floor(valueIt->second.numberValues.front())));
|
|
break;
|
|
}
|
|
|
|
if (!shouldNormalize)
|
|
continue;
|
|
|
|
ShaderParameterValue normalizedValue;
|
|
std::string normalizeError;
|
|
if (NormalizeAndValidateValue(definition, valueJson, normalizedValue, normalizeError))
|
|
valueIt->second = normalizedValue;
|
|
else
|
|
valueIt->second = DefaultValueForDefinition(definition);
|
|
}
|
|
}
|
|
|
|
JsonValue RuntimeHost::SerializeLayerStackLocked() const
|
|
{
|
|
JsonValue layers = JsonValue::MakeArray();
|
|
for (const LayerPersistentState& layer : mPersistentState.layers)
|
|
{
|
|
auto shaderIt = mPackagesById.find(layer.shaderId);
|
|
if (shaderIt == mPackagesById.end())
|
|
continue;
|
|
|
|
JsonValue layerValue = JsonValue::MakeObject();
|
|
layerValue.set("id", JsonValue(layer.id));
|
|
layerValue.set("shaderId", JsonValue(layer.shaderId));
|
|
layerValue.set("shaderName", JsonValue(shaderIt->second.displayName));
|
|
layerValue.set("bypass", JsonValue(layer.bypass));
|
|
if (shaderIt->second.temporal.enabled)
|
|
{
|
|
JsonValue temporal = JsonValue::MakeObject();
|
|
temporal.set("enabled", JsonValue(true));
|
|
temporal.set("historySource", JsonValue(TemporalHistorySourceToString(shaderIt->second.temporal.historySource)));
|
|
temporal.set("requestedHistoryLength", JsonValue(static_cast<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 RuntimeHost::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(),
|
|
[¶meterId](const ShaderParameterDefinition& definition) { return definition.id == parameterId; });
|
|
if (definitionIt == shaderIt->second.parameters.end())
|
|
continue;
|
|
|
|
ShaderParameterValue normalizedValue;
|
|
if (!NormalizeAndValidateValue(*definitionIt, *valueValue, normalizedValue, error))
|
|
return false;
|
|
|
|
layer.parameterValues[parameterId] = normalizedValue;
|
|
}
|
|
}
|
|
|
|
EnsureLayerDefaultsLocked(layer, shaderIt->second);
|
|
layers.push_back(layer);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
void RuntimeHost::NormalizePersistentLayerIdsLocked()
|
|
{
|
|
std::set<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 RuntimeHost::SerializeParameterValue(const ShaderParameterDefinition& definition, const ShaderParameterValue& value) const
|
|
{
|
|
switch (definition.type)
|
|
{
|
|
case ShaderParameterType::Boolean:
|
|
return JsonValue(value.booleanValue);
|
|
case ShaderParameterType::Enum:
|
|
return JsonValue(value.enumValue);
|
|
case ShaderParameterType::Text:
|
|
return JsonValue(value.textValue);
|
|
case ShaderParameterType::Trigger:
|
|
return JsonValue(value.numberValues.empty() ? 0.0 : value.numberValues.front());
|
|
case ShaderParameterType::Float:
|
|
return JsonValue(value.numberValues.empty() ? 0.0 : value.numberValues.front());
|
|
case ShaderParameterType::Vec2:
|
|
case ShaderParameterType::Color:
|
|
{
|
|
JsonValue array = JsonValue::MakeArray();
|
|
for (double number : value.numberValues)
|
|
array.pushBack(JsonValue(number));
|
|
return array;
|
|
}
|
|
}
|
|
return JsonValue();
|
|
}
|
|
|
|
std::string RuntimeHost::TemporalHistorySourceToString(TemporalHistorySource source) const
|
|
{
|
|
switch (source)
|
|
{
|
|
case TemporalHistorySource::Source:
|
|
return "source";
|
|
case TemporalHistorySource::PreLayerInput:
|
|
return "preLayerInput";
|
|
case TemporalHistorySource::None:
|
|
default:
|
|
return "none";
|
|
}
|
|
}
|
|
|
|
RuntimeHost::LayerPersistentState* RuntimeHost::FindLayerById(const std::string& layerId)
|
|
{
|
|
auto it = std::find_if(mPersistentState.layers.begin(), mPersistentState.layers.end(),
|
|
[&layerId](const LayerPersistentState& layer) { return layer.id == layerId; });
|
|
return it == mPersistentState.layers.end() ? nullptr : &*it;
|
|
}
|
|
|
|
const RuntimeHost::LayerPersistentState* RuntimeHost::FindLayerById(const std::string& layerId) const
|
|
{
|
|
auto it = std::find_if(mPersistentState.layers.begin(), mPersistentState.layers.end(),
|
|
[&layerId](const LayerPersistentState& layer) { return layer.id == layerId; });
|
|
return it == mPersistentState.layers.end() ? nullptr : &*it;
|
|
}
|
|
|
|
std::string RuntimeHost::GenerateLayerId()
|
|
{
|
|
while (true)
|
|
{
|
|
++mNextLayerId;
|
|
const std::string candidate = "layer-" + std::to_string(mNextLayerId);
|
|
if (!FindLayerById(candidate))
|
|
return candidate;
|
|
}
|
|
}
|