#include "stdafx.h" #include "ShaderManifestParser.h" #include "ShaderUiPath.h" #include #include #include namespace ShaderManifestParsing { std::string Trim(const std::string& text) { std::size_t start = 0; while (start < text.size() && std::isspace(static_cast(text[start]))) ++start; std::size_t end = text.size(); while (end > start && std::isspace(static_cast(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(tag.front()); if (first < 'a' || first > 'z') return false; for (char ch : tag) { const unsigned char value = static_cast(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& 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(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(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(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; } }