Refactor
Some checks failed
CI / Native Windows Build And Tests (push) Has been cancelled
CI / React UI Build (push) Has been cancelled
CI / Windows Release Package (push) Has been cancelled

This commit is contained in:
2026-05-06 09:28:46 +10:00
parent 437199f3f0
commit 0bfffa6552
41 changed files with 1592 additions and 1368 deletions

View File

@@ -0,0 +1,302 @@
#include "stdafx.h"
#include "ShaderCompiler.h"
#include "NativeHandles.h"
#include <fstream>
#include <cctype>
#include <regex>
#include <sstream>
#include <vector>
namespace
{
std::string ReplaceAll(std::string text, const std::string& from, const std::string& to)
{
std::size_t offset = 0;
while ((offset = text.find(from, offset)) != std::string::npos)
{
text.replace(offset, from.length(), to);
offset += to.length();
}
return text;
}
std::string SlangCBufferTypeForParameter(ShaderParameterType type)
{
switch (type)
{
case ShaderParameterType::Float: return "float";
case ShaderParameterType::Vec2: return "float2";
case ShaderParameterType::Color: return "float4";
case ShaderParameterType::Boolean: return "bool";
case ShaderParameterType::Enum: return "int";
case ShaderParameterType::Text: return "";
}
return "float";
}
std::string CapitalizeIdentifier(const std::string& identifier)
{
if (identifier.empty())
return identifier;
std::string text = identifier;
text[0] = static_cast<char>(std::toupper(static_cast<unsigned char>(text[0])));
return text;
}
std::string BuildParameterUniforms(const std::vector<ShaderParameterDefinition>& parameters)
{
std::ostringstream source;
for (const ShaderParameterDefinition& definition : parameters)
{
if (definition.type == ShaderParameterType::Text)
continue;
source << "\t" << SlangCBufferTypeForParameter(definition.type) << " " << definition.id << ";\n";
}
return source.str();
}
std::string BuildHistorySamplerDeclarations(const std::string& samplerPrefix, unsigned historyLength)
{
std::ostringstream source;
for (unsigned index = 0; index < historyLength; ++index)
source << "Sampler2D<float4> " << samplerPrefix << index << ";\n";
return source.str();
}
std::string BuildTextureSamplerDeclarations(const std::vector<ShaderTextureAsset>& textureAssets)
{
std::ostringstream source;
for (const ShaderTextureAsset& textureAsset : textureAssets)
source << "Sampler2D<float4> " << textureAsset.id << ";\n";
if (!textureAssets.empty())
source << "\n";
return source.str();
}
std::string BuildTextSamplerDeclarations(const std::vector<ShaderParameterDefinition>& parameters)
{
std::ostringstream source;
for (const ShaderParameterDefinition& definition : parameters)
{
if (definition.type != ShaderParameterType::Text)
continue;
source << "Sampler2D<float4> " << definition.id << "Texture;\n";
}
if (source.tellp() > 0)
source << "\n";
return source.str();
}
std::string BuildTextHelpers(const std::vector<ShaderParameterDefinition>& parameters)
{
std::ostringstream source;
for (const ShaderParameterDefinition& definition : parameters)
{
if (definition.type != ShaderParameterType::Text)
continue;
const std::string suffix = CapitalizeIdentifier(definition.id);
source
<< "float sample" << suffix << "(float2 uv)\n"
<< "{\n"
<< "\tif (uv.x < 0.0 || uv.x > 1.0 || uv.y < 0.0 || uv.y > 1.0)\n"
<< "\t\treturn 0.0;\n"
<< "\treturn " << definition.id << "Texture.Sample(uv).r;\n"
<< "}\n\n"
<< "float4 draw" << suffix << "(float2 uv, float4 fillColor)\n"
<< "{\n"
<< "\tfloat alpha = sample" << suffix << "(uv) * fillColor.a;\n"
<< "\treturn float4(fillColor.rgb * alpha, alpha);\n"
<< "}\n\n";
}
return source.str();
}
std::string BuildHistorySwitchCases(const std::string& samplerPrefix, unsigned historyLength)
{
std::ostringstream source;
for (unsigned index = 0; index < historyLength; ++index)
source << "\tcase " << index << ": return " << samplerPrefix << index << ".Sample(tc);\n";
return source.str();
}
}
ShaderCompiler::ShaderCompiler(
const std::filesystem::path& repoRoot,
const std::filesystem::path& wrapperPath,
const std::filesystem::path& generatedGlslPath,
const std::filesystem::path& patchedGlslPath,
unsigned maxTemporalHistoryFrames)
: mRepoRoot(repoRoot),
mWrapperPath(wrapperPath),
mGeneratedGlslPath(generatedGlslPath),
mPatchedGlslPath(patchedGlslPath),
mMaxTemporalHistoryFrames(maxTemporalHistoryFrames)
{
}
bool ShaderCompiler::BuildLayerFragmentShaderSource(const ShaderPackage& shaderPackage, std::string& fragmentShaderSource, std::string& error) const
{
std::string wrapperSource;
if (!BuildWrapperSlangSource(shaderPackage, wrapperSource, error))
return false;
if (!WriteTextFile(mWrapperPath, wrapperSource, error))
return false;
if (!RunSlangCompiler(mWrapperPath, mGeneratedGlslPath, error))
return false;
fragmentShaderSource = ReadTextFile(mGeneratedGlslPath, error);
if (fragmentShaderSource.empty())
return false;
if (!PatchGeneratedGlsl(fragmentShaderSource, error))
return false;
if (!WriteTextFile(mPatchedGlslPath, fragmentShaderSource, error))
return false;
return true;
}
bool ShaderCompiler::BuildWrapperSlangSource(const ShaderPackage& shaderPackage, std::string& wrapperSource, std::string& error) const
{
const std::filesystem::path templatePath = mRepoRoot / "runtime" / "templates" / "shader_wrapper.slang.in";
wrapperSource = ReadTextFile(templatePath, error);
if (wrapperSource.empty())
return false;
wrapperSource = ReplaceAll(wrapperSource, "{{PARAMETER_UNIFORMS}}", BuildParameterUniforms(shaderPackage.parameters));
const unsigned historySamplerCount = shaderPackage.temporal.enabled ? mMaxTemporalHistoryFrames : 0;
wrapperSource = ReplaceAll(wrapperSource, "{{SOURCE_HISTORY_SAMPLERS}}", BuildHistorySamplerDeclarations("gSourceHistory", historySamplerCount));
wrapperSource = ReplaceAll(wrapperSource, "{{TEMPORAL_HISTORY_SAMPLERS}}", BuildHistorySamplerDeclarations("gTemporalHistory", historySamplerCount));
wrapperSource = ReplaceAll(wrapperSource, "{{TEXTURE_SAMPLERS}}", BuildTextureSamplerDeclarations(shaderPackage.textureAssets));
wrapperSource = ReplaceAll(wrapperSource, "{{TEXT_SAMPLERS}}", BuildTextSamplerDeclarations(shaderPackage.parameters));
wrapperSource = ReplaceAll(wrapperSource, "{{TEXT_HELPERS}}", BuildTextHelpers(shaderPackage.parameters));
wrapperSource = ReplaceAll(wrapperSource, "{{SOURCE_HISTORY_SWITCH_CASES}}", BuildHistorySwitchCases("gSourceHistory", historySamplerCount));
wrapperSource = ReplaceAll(wrapperSource, "{{TEMPORAL_HISTORY_SWITCH_CASES}}", BuildHistorySwitchCases("gTemporalHistory", historySamplerCount));
wrapperSource = ReplaceAll(wrapperSource, "{{USER_SHADER_INCLUDE}}", shaderPackage.shaderPath.generic_string());
wrapperSource = ReplaceAll(wrapperSource, "{{ENTRY_POINT_CALL}}", shaderPackage.entryPoint + "(context)");
return true;
}
bool ShaderCompiler::FindSlangCompiler(std::filesystem::path& compilerPath, std::string& error) const
{
std::filesystem::path thirdPartyRoot = mRepoRoot / "3rdParty";
if (!std::filesystem::exists(thirdPartyRoot))
{
error = "3rdParty directory was not found under the repository root.";
return false;
}
for (const auto& entry : std::filesystem::directory_iterator(thirdPartyRoot))
{
if (!entry.is_directory())
continue;
std::filesystem::path candidate = entry.path() / "bin" / "slangc.exe";
if (std::filesystem::exists(candidate))
{
compilerPath = candidate;
return true;
}
}
error = "Could not find slangc.exe under 3rdParty.";
return false;
}
bool ShaderCompiler::RunSlangCompiler(const std::filesystem::path& wrapperPath, const std::filesystem::path& outputPath, std::string& error) const
{
std::filesystem::path compilerPath;
if (!FindSlangCompiler(compilerPath, error))
return false;
std::string commandLine = "\"" + compilerPath.string() + "\" \"" + wrapperPath.string()
+ "\" -target glsl -profile glsl_430 -entry fragmentMain -stage fragment -o \"" + outputPath.string() + "\"";
STARTUPINFOA startupInfo = {};
PROCESS_INFORMATION processInfo = {};
startupInfo.cb = sizeof(startupInfo);
std::vector<char> mutableCommandLine(commandLine.begin(), commandLine.end());
mutableCommandLine.push_back('\0');
if (!CreateProcessA(NULL, mutableCommandLine.data(), NULL, NULL, FALSE, CREATE_NO_WINDOW, NULL, mRepoRoot.string().c_str(), &startupInfo, &processInfo))
{
error = "Failed to launch slangc.exe.";
return false;
}
UniqueHandle processHandle(processInfo.hProcess);
UniqueHandle threadHandle(processInfo.hThread);
WaitForSingleObject(processHandle.get(), INFINITE);
DWORD exitCode = 0;
GetExitCodeProcess(processHandle.get(), &exitCode);
if (exitCode != 0)
{
error = "slangc.exe returned a non-zero exit code while compiling the active shader package.";
return false;
}
return true;
}
bool ShaderCompiler::PatchGeneratedGlsl(std::string& shaderText, std::string& error) const
{
if (shaderText.find("#version 450") == std::string::npos)
{
error = "Generated GLSL did not include the expected version header.";
return false;
}
shaderText = ReplaceAll(shaderText, "#version 450", "#version 430 core");
shaderText = std::regex_replace(shaderText, std::regex(R"(#extension GL_EXT_samplerless_texture_functions : require\r?\n)"), "");
shaderText = std::regex_replace(shaderText, std::regex(R"(layout\(row_major\) uniform;\r?\n)"), "");
shaderText = std::regex_replace(shaderText, std::regex(R"(layout\(row_major\) buffer;\r?\n)"), "");
shaderText = std::regex_replace(shaderText, std::regex(R"(layout\(location = 0\)\s*in vec2 ([A-Za-z0-9_]+);)"), "in vec2 vTexCoord;");
shaderText = ReplaceAll(shaderText, "input_texCoord_0", "vTexCoord");
std::smatch match;
std::regex outRegex(R"(layout\(location = 0\)\s*out vec4 ([A-Za-z0-9_]+);)");
if (std::regex_search(shaderText, match, outRegex))
{
const std::string outputName = match[1].str();
shaderText = std::regex_replace(shaderText, outRegex, "layout(location = 0) out vec4 fragColor;");
shaderText = ReplaceAll(shaderText, outputName + " =", "fragColor =");
}
return true;
}
std::string ShaderCompiler::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 ShaderCompiler::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);
std::ofstream output(path, std::ios::binary);
if (!output)
{
error = "Could not write file: " + path.string();
return false;
}
output << contents;
return output.good();
}

View File

@@ -0,0 +1,34 @@
#pragma once
#include "ShaderTypes.h"
#include <filesystem>
#include <string>
class ShaderCompiler
{
public:
ShaderCompiler(
const std::filesystem::path& repoRoot,
const std::filesystem::path& wrapperPath,
const std::filesystem::path& generatedGlslPath,
const std::filesystem::path& patchedGlslPath,
unsigned maxTemporalHistoryFrames);
bool BuildLayerFragmentShaderSource(const ShaderPackage& shaderPackage, std::string& fragmentShaderSource, std::string& error) const;
private:
bool BuildWrapperSlangSource(const ShaderPackage& shaderPackage, std::string& wrapperSource, std::string& error) const;
bool FindSlangCompiler(std::filesystem::path& compilerPath, std::string& error) const;
bool RunSlangCompiler(const std::filesystem::path& wrapperPath, const std::filesystem::path& outputPath, std::string& error) const;
bool PatchGeneratedGlsl(std::string& shaderText, std::string& error) const;
std::string ReadTextFile(const std::filesystem::path& path, std::string& error) const;
bool WriteTextFile(const std::filesystem::path& path, const std::string& contents, std::string& error) const;
private:
std::filesystem::path mRepoRoot;
std::filesystem::path mWrapperPath;
std::filesystem::path mGeneratedGlslPath;
std::filesystem::path mPatchedGlslPath;
unsigned mMaxTemporalHistoryFrames;
};

View File

@@ -0,0 +1,633 @@
#include "stdafx.h"
#include "ShaderPackageRegistry.h"
#include "RuntimeJson.h"
#include <algorithm>
#include <cmath>
#include <cctype>
#include <fstream>
#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;
}
std::vector<double> JsonArrayToNumbers(const JsonValue& value)
{
std::vector<double> numbers;
for (const JsonValue& item : value.asArray())
{
if (item.isNumber())
numbers.push_back(item.asNumber());
}
return numbers;
}
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;
}
return false;
}
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;
}
std::string ReadTextFile(const std::filesystem::path& path, std::string& error)
{
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();
}
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 (!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;
}
}
ShaderPackageRegistry::ShaderPackageRegistry(unsigned maxTemporalHistoryFrames)
: mMaxTemporalHistoryFrames(maxTemporalHistoryFrames)
{
}
bool ShaderPackageRegistry::Scan(const std::filesystem::path& shaderRoot, std::map<std::string, ShaderPackage>& packagesById, std::vector<std::string>& packageOrder, std::string& error) const
{
packagesById.clear();
packageOrder.clear();
if (!std::filesystem::exists(shaderRoot))
{
error = "Shader library directory does not exist: " + shaderRoot.string();
return false;
}
for (const auto& entry : std::filesystem::directory_iterator(shaderRoot))
{
if (!entry.is_directory())
continue;
std::filesystem::path manifestPath = entry.path() / "shader.json";
if (!std::filesystem::exists(manifestPath))
continue;
ShaderPackage shaderPackage;
if (!ParseManifest(manifestPath, shaderPackage, error))
return false;
if (packagesById.find(shaderPackage.id) != packagesById.end())
{
error = "Duplicate shader id found: " + shaderPackage.id;
return false;
}
packageOrder.push_back(shaderPackage.id);
packagesById[shaderPackage.id] = shaderPackage;
}
std::sort(packageOrder.begin(), packageOrder.end());
return true;
}
bool ShaderPackageRegistry::ParseManifest(const std::filesystem::path& manifestPath, ShaderPackage& shaderPackage, std::string& error) const
{
const std::string manifestText = ReadTextFile(manifestPath, error);
if (manifestText.empty())
return false;
JsonValue manifestJson;
if (!ParseJson(manifestText, manifestJson, error))
return false;
if (!manifestJson.isObject())
{
error = "Shader manifest root must be an object: " + manifestPath.string();
return false;
}
if (!ParseShaderMetadata(manifestJson, shaderPackage, manifestPath, error))
return false;
if (!std::filesystem::exists(shaderPackage.shaderPath))
{
error = "Shader source not found for package " + shaderPackage.id + ": " + shaderPackage.shaderPath.string();
return false;
}
shaderPackage.shaderWriteTime = std::filesystem::last_write_time(shaderPackage.shaderPath);
shaderPackage.manifestWriteTime = std::filesystem::last_write_time(shaderPackage.manifestPath);
return ParseTextureAssets(manifestJson, shaderPackage, manifestPath, error) &&
ParseFontAssets(manifestJson, shaderPackage, manifestPath, error) &&
ParseTemporalSettings(manifestJson, shaderPackage, mMaxTemporalHistoryFrames, manifestPath, error) &&
ParseParameterDefinitions(manifestJson, shaderPackage, manifestPath, error);
}

View File

@@ -0,0 +1,20 @@
#pragma once
#include "ShaderTypes.h"
#include <filesystem>
#include <map>
#include <string>
#include <vector>
class ShaderPackageRegistry
{
public:
explicit ShaderPackageRegistry(unsigned maxTemporalHistoryFrames);
bool Scan(const std::filesystem::path& shaderRoot, std::map<std::string, ShaderPackage>& packagesById, std::vector<std::string>& packageOrder, std::string& error) const;
bool ParseManifest(const std::filesystem::path& manifestPath, ShaderPackage& shaderPackage, std::string& error) const;
private:
unsigned mMaxTemporalHistoryFrames;
};

View File

@@ -0,0 +1,116 @@
#pragma once
#include <filesystem>
#include <map>
#include <string>
#include <vector>
enum class ShaderParameterType
{
Float,
Vec2,
Color,
Boolean,
Enum,
Text
};
struct ShaderParameterOption
{
std::string value;
std::string label;
};
struct ShaderParameterDefinition
{
std::string id;
std::string label;
ShaderParameterType type = ShaderParameterType::Float;
std::vector<double> defaultNumbers;
std::vector<double> minNumbers;
std::vector<double> maxNumbers;
std::vector<double> stepNumbers;
bool defaultBoolean = false;
std::string defaultEnumValue;
std::string defaultTextValue;
std::string fontId;
unsigned maxLength = 64;
std::vector<ShaderParameterOption> enumOptions;
};
struct ShaderParameterValue
{
std::vector<double> numberValues;
bool booleanValue = false;
std::string enumValue;
std::string textValue;
};
enum class TemporalHistorySource
{
None,
Source,
PreLayerInput
};
struct TemporalSettings
{
bool enabled = false;
TemporalHistorySource historySource = TemporalHistorySource::None;
unsigned requestedHistoryLength = 0;
unsigned effectiveHistoryLength = 0;
};
struct ShaderTextureAsset
{
std::string id;
std::filesystem::path path;
std::filesystem::file_time_type writeTime;
};
struct ShaderFontAsset
{
std::string id;
std::filesystem::path path;
std::filesystem::file_time_type writeTime;
};
struct ShaderPackage
{
std::string id;
std::string displayName;
std::string description;
std::string category;
std::string entryPoint;
std::filesystem::path directoryPath;
std::filesystem::path shaderPath;
std::filesystem::path manifestPath;
std::vector<ShaderParameterDefinition> parameters;
std::vector<ShaderTextureAsset> textureAssets;
std::vector<ShaderFontAsset> fontAssets;
TemporalSettings temporal;
std::filesystem::file_time_type shaderWriteTime;
std::filesystem::file_time_type manifestWriteTime;
};
struct RuntimeRenderState
{
std::string layerId;
std::string shaderId;
std::vector<ShaderParameterDefinition> parameterDefinitions;
std::map<std::string, ShaderParameterValue> parameterValues;
std::vector<ShaderTextureAsset> textureAssets;
std::vector<ShaderFontAsset> fontAssets;
double timeSeconds = 0.0;
double frameCount = 0.0;
double mixAmount = 1.0;
double bypass = 0.0;
unsigned inputWidth = 0;
unsigned inputHeight = 0;
unsigned outputWidth = 0;
unsigned outputHeight = 0;
bool isTemporal = false;
TemporalHistorySource temporalHistorySource = TemporalHistorySource::None;
unsigned requestedTemporalHistoryLength = 0;
unsigned effectiveTemporalHistoryLength = 0;
};