Files
video-shader-toys/src/shader/ShaderManifestParser.cpp
Aiden b7ce079a26
All checks were successful
CI / React UI Build (push) Successful in 39s
CI / Native Windows Build And Tests (push) Successful in 2m23s
CI / Windows Release Package (push) Successful in 3m6s
Fixed duplication
2026-06-01 06:52:56 -04:00

442 lines
14 KiB
C++

#include "stdafx.h"
#include "ShaderManifestParser.h"
#include "ShaderUiPath.h"
#include <algorithm>
#include <cmath>
#include <cctype>
namespace ShaderManifestParsing
{
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;
}
namespace
{
bool ParseTemporalHistorySource(const std::string& sourceName, TemporalHistorySource& source)
{
if (sourceName == "source")
{
source = TemporalHistorySource::Source;
return true;
}
if (sourceName == "preLayerInput")
{
source = TemporalHistorySource::PreLayerInput;
return true;
}
if (sourceName == "none")
{
source = TemporalHistorySource::None;
return true;
}
return false;
}
bool IsValidCustomElementTag(const std::string& tag)
{
if (tag.empty() || tag.find('-') == std::string::npos || tag.front() == '-' || tag.back() == '-')
return false;
const unsigned char first = static_cast<unsigned char>(tag.front());
if (first < 'a' || first > 'z')
return false;
for (char ch : tag)
{
const unsigned char value = static_cast<unsigned char>(ch);
if ((value >= 'a' && value <= 'z') || (value >= '0' && value <= '9') || value == '-')
continue;
return false;
}
return true;
}
}
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 ParsePassDefinitions(const JsonValue& manifestJson, ShaderPackage& shaderPackage, const std::filesystem::path& manifestPath, std::string& error)
{
const JsonValue* passesValue = nullptr;
if (!OptionalArrayField(manifestJson, "passes", passesValue, manifestPath, error))
return false;
if (!passesValue)
{
ShaderPassDefinition pass;
pass.id = "main";
pass.entryPoint = shaderPackage.entryPoint;
pass.sourcePath = shaderPackage.shaderPath;
pass.outputName = "layerOutput";
if (!std::filesystem::exists(pass.sourcePath))
{
error = "Shader source not found for package " + shaderPackage.id + ": " + pass.sourcePath.string();
return false;
}
pass.sourceWriteTime = std::filesystem::last_write_time(pass.sourcePath);
shaderPackage.passes.push_back(pass);
return true;
}
if (passesValue->asArray().empty())
{
error = "Shader manifest 'passes' field must not be empty in: " + ManifestPathMessage(manifestPath);
return false;
}
for (const JsonValue& passJson : passesValue->asArray())
{
if (!passJson.isObject())
{
error = "Shader pass entry must be an object in: " + ManifestPathMessage(manifestPath);
return false;
}
std::string passId;
std::string sourcePath;
if (!RequireNonEmptyStringField(passJson, "id", passId, manifestPath, error) ||
!RequireNonEmptyStringField(passJson, "source", sourcePath, manifestPath, error))
{
error = "Shader pass is missing required 'id' or 'source' in: " + ManifestPathMessage(manifestPath);
return false;
}
if (!ValidateShaderIdentifier(passId, "passes[].id", manifestPath, error))
return false;
for (const ShaderPassDefinition& existingPass : shaderPackage.passes)
{
if (existingPass.id == passId)
{
error = "Duplicate shader pass id '" + passId + "' in: " + ManifestPathMessage(manifestPath);
return false;
}
}
ShaderPassDefinition pass;
pass.id = passId;
pass.sourcePath = shaderPackage.directoryPath / sourcePath;
if (!OptionalStringField(passJson, "entryPoint", pass.entryPoint, shaderPackage.entryPoint, manifestPath, error) ||
!OptionalStringField(passJson, "output", pass.outputName, passId, manifestPath, error))
{
return false;
}
if (!ValidateShaderIdentifier(pass.entryPoint, "passes[].entryPoint", manifestPath, error))
return false;
const JsonValue* inputsValue = nullptr;
if (!OptionalArrayField(passJson, "inputs", inputsValue, manifestPath, error))
return false;
if (inputsValue)
{
for (const JsonValue& inputValue : inputsValue->asArray())
{
if (!inputValue.isString())
{
error = "Shader pass inputs must be strings in: " + ManifestPathMessage(manifestPath);
return false;
}
pass.inputNames.push_back(inputValue.asString());
}
}
if (!std::filesystem::exists(pass.sourcePath))
{
error = "Shader pass source not found for package " + shaderPackage.id + ": " + pass.sourcePath.string();
return false;
}
pass.sourceWriteTime = std::filesystem::last_write_time(pass.sourcePath);
shaderPackage.passes.push_back(pass);
}
shaderPackage.shaderPath = shaderPackage.passes.front().sourcePath;
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 ParseFeedbackSettings(const JsonValue& manifestJson, ShaderPackage& shaderPackage, const std::filesystem::path& manifestPath, std::string& error)
{
const JsonValue* feedbackValue = nullptr;
if (!OptionalObjectField(manifestJson, "feedback", feedbackValue, manifestPath, error))
return false;
if (!feedbackValue)
return true;
const JsonValue* enabledValue = feedbackValue->find("enabled");
if (!enabledValue || !enabledValue->asBoolean(false))
return true;
shaderPackage.feedback.enabled = true;
if (!OptionalStringField(*feedbackValue, "writePass", shaderPackage.feedback.writePassId, "", manifestPath, error))
return false;
if (shaderPackage.feedback.writePassId.empty())
{
if (shaderPackage.passes.empty())
{
error = "Feedback-enabled shader has no passes to target in: " + ManifestPathMessage(manifestPath);
return false;
}
shaderPackage.feedback.writePassId = shaderPackage.passes.back().id;
}
if (!ValidateShaderIdentifier(shaderPackage.feedback.writePassId, "feedback.writePass", manifestPath, error))
return false;
const auto passIt = std::find_if(shaderPackage.passes.begin(), shaderPackage.passes.end(),
[&shaderPackage](const ShaderPassDefinition& pass) { return pass.id == shaderPackage.feedback.writePassId; });
if (passIt == shaderPackage.passes.end())
{
error = "Feedback writePass '" + shaderPackage.feedback.writePassId + "' does not match any declared pass in: " + ManifestPathMessage(manifestPath);
return false;
}
return true;
}
bool ParseUiDefinition(const JsonValue& manifestJson, ShaderPackage& shaderPackage, const std::filesystem::path& manifestPath, std::string& error)
{
const JsonValue* uiValue = nullptr;
if (!OptionalObjectField(manifestJson, "ui", uiValue, manifestPath, error))
return false;
if (!uiValue)
return true;
ShaderUiDefinition ui;
if (!RequireNonEmptyStringField(*uiValue, "type", ui.type, manifestPath, error) ||
!RequireNonEmptyStringField(*uiValue, "entry", ui.entryPath, manifestPath, error) ||
!RequireNonEmptyStringField(*uiValue, "tag", ui.customElementTag, manifestPath, error))
{
error = "Shader UI definition is missing required 'type', 'entry', or 'tag' in: " + ManifestPathMessage(manifestPath);
return false;
}
if (ui.type != "webComponent")
{
error = "Shader UI type must be 'webComponent' in: " + ManifestPathMessage(manifestPath);
return false;
}
std::filesystem::path normalizedEntryPath;
if (!ShaderUiPath::NormalizeAssetPath(ui.entryPath, normalizedEntryPath) ||
!ShaderUiPath::IsModulePath(normalizedEntryPath))
{
error = "Shader UI entry must be a safe relative .js or .mjs path under ui/ in: " + ManifestPathMessage(manifestPath);
return false;
}
if (!IsValidCustomElementTag(ui.customElementTag))
{
error = "Shader UI tag must be a valid custom element name with a hyphen in: " + ManifestPathMessage(manifestPath);
return false;
}
std::filesystem::path entryPath;
if (!ShaderUiPath::ResolveAssetPath(shaderPackage.directoryPath, ui.entryPath, entryPath))
{
error = "Shader UI entry not found for package " + shaderPackage.id + ": " +
(shaderPackage.directoryPath / normalizedEntryPath).string();
return false;
}
ui.entryPath = normalizedEntryPath.generic_string();
ui.enabled = true;
shaderPackage.ui = ui;
return true;
}
}