Add manifest support for pass declarations
All checks were successful
CI / React UI Build (push) Successful in 11s
CI / Native Windows Build And Tests (push) Successful in 2m17s
CI / Windows Release Package (push) Successful in 2m28s

This commit is contained in:
2026-05-08 17:19:30 +10:00
parent 87cb55b80b
commit 596d370f43
15 changed files with 362 additions and 128 deletions

View File

@@ -206,10 +206,11 @@ Each shader package lives under:
shaders/<id>/
shader.json
shader.slang
optional-extra-pass.slang
optional-font-or-texture-assets
```
See `SHADER_CONTRACT.md` for the manifest schema, parameter types, texture assets, font/text assets, temporal history support, and the Slang entry point contract. `shaders/text-overlay/` is the reference live text package and bundles Roboto Regular with its OFL license.
See `SHADER_CONTRACT.md` for the manifest schema, parameter types, texture assets, font/text assets, temporal history support, optional render-pass declarations, and the Slang entry point contract. `shaders/text-overlay/` is the reference live text package and bundles Roboto Regular with its OFL license.
## Generated Files

View File

@@ -97,6 +97,7 @@ Optional fields:
- `description`: display/help text for the shader library.
- `category`: UI grouping label.
- `entryPoint`: Slang function to call. Defaults to `shadeVideo`.
- `passes`: advanced render-pass declarations. Omit this for normal single-pass shaders.
- `textures`: texture assets to load and expose as samplers.
- `fonts`: packaged font assets for live text parameters.
- `temporal`: history-buffer requirements.
@@ -110,6 +111,52 @@ Shader-visible identifiers must be valid Slang-style identifiers:
Use letters, numbers, and underscores only, and start with a letter or underscore. For example, `logoTexture` is valid; `logo-texture` is not valid as a shader-visible texture ID.
## Render Passes
Most shaders should omit `passes`. The runtime then creates one implicit pass:
```json
{
"id": "main",
"source": "shader.slang",
"entryPoint": "shadeVideo",
"output": "layerOutput"
}
```
Advanced shaders may declare explicit passes:
```json
{
"passes": [
{
"id": "blurX",
"source": "blur-x.slang",
"entryPoint": "blurHorizontal",
"inputs": ["layerInput"],
"output": "blurredX"
},
{
"id": "final",
"source": "final.slang",
"entryPoint": "finish",
"inputs": ["blurredX"],
"output": "layerOutput"
}
]
}
```
Pass fields:
- `id`: required pass identifier. It must be a valid shader identifier and unique inside the package.
- `source`: required Slang source path relative to the package directory.
- `entryPoint`: optional Slang function for this pass. Defaults to the package-level `entryPoint`.
- `inputs`: optional list of named inputs. Current reserved names include `layerInput` and future pass output names.
- `output`: optional output name. Use `layerOutput` for the final visible layer result.
Current runtime note: pass manifests are parsed and every declared pass is Slang-validated/compiled, but execution still uses the first pass until multipass render-target routing is enabled. Existing single-pass shaders are unaffected.
## Slang Entry Point
Your shader file must implement the manifest `entryPoint`.

View File

@@ -2,6 +2,7 @@
#include <cstring>
#include <string>
#include <utility>
#include <vector>
namespace
@@ -91,7 +92,11 @@ bool OpenGLShaderPrograms::CommitPreparedLayerPrograms(const PreparedShaderBuild
for (const PreparedLayerShader& preparedLayer : preparedBuild.layers)
{
LayerProgram layerProgram;
if (!mCompiler.CompilePreparedLayerProgram(preparedLayer.state, preparedLayer.fragmentShaderSource, layerProgram, errorMessageSize, errorMessage))
std::vector<std::pair<std::string, std::string>> passSources;
passSources.reserve(preparedLayer.passes.size());
for (const PreparedLayerShader::Pass& pass : preparedLayer.passes)
passSources.push_back(std::make_pair(pass.passId, pass.fragmentShaderSource));
if (!mCompiler.CompilePreparedLayerProgram(preparedLayer.state, passSources, layerProgram, errorMessageSize, errorMessage))
{
for (LayerProgram& program : newPrograms)
DestroySingleLayerProgram(program);

View File

@@ -120,11 +120,15 @@ PreparedShaderBuild ShaderBuildQueue::Build(uint64_t generation, unsigned output
{
PreparedLayerShader layer;
layer.state = state;
if (!mRuntimeHost.BuildLayerFragmentShaderSource(state.layerId, layer.fragmentShaderSource, build.message))
std::vector<std::pair<std::string, std::string>> passSources;
if (!mRuntimeHost.BuildLayerPassFragmentShaderSources(state.layerId, passSources, build.message))
{
build.succeeded = false;
return build;
}
layer.passes.reserve(passSources.size());
for (auto& passSource : passSources)
layer.passes.push_back({ std::move(passSource.first), std::move(passSource.second) });
build.layers.push_back(std::move(layer));
}

View File

@@ -13,8 +13,14 @@ class RuntimeHost;
struct PreparedLayerShader
{
struct Pass
{
std::string passId;
std::string fragmentShaderSource;
};
RuntimeRenderState state;
std::string fragmentShaderSource;
std::vector<Pass> passes;
};
struct PreparedShaderBuild

View File

@@ -28,102 +28,118 @@ ShaderProgramCompiler::ShaderProgramCompiler(OpenGLRenderer& renderer, RuntimeHo
bool ShaderProgramCompiler::CompileLayerProgram(const RuntimeRenderState& state, LayerProgram& layerProgram, int errorMessageSize, char* errorMessage)
{
std::string fragmentShaderSource;
std::vector<std::pair<std::string, std::string>> passSources;
std::string loadError;
if (!mRuntimeHost.BuildLayerFragmentShaderSource(state.layerId, fragmentShaderSource, loadError))
if (!mRuntimeHost.BuildLayerPassFragmentShaderSources(state.layerId, passSources, loadError))
{
CopyErrorMessage(loadError, errorMessageSize, errorMessage);
return false;
}
return CompilePreparedLayerProgram(state, fragmentShaderSource, layerProgram, errorMessageSize, errorMessage);
return CompilePreparedLayerProgram(state, passSources, layerProgram, errorMessageSize, errorMessage);
}
bool ShaderProgramCompiler::CompilePreparedLayerProgram(const RuntimeRenderState& state, const std::string& fragmentShaderSource, LayerProgram& layerProgram, int errorMessageSize, char* errorMessage)
{
std::vector<std::pair<std::string, std::string>> passSources;
passSources.push_back(std::make_pair(std::string("main"), fragmentShaderSource));
return CompilePreparedLayerProgram(state, passSources, layerProgram, errorMessageSize, errorMessage);
}
bool ShaderProgramCompiler::CompilePreparedLayerProgram(const RuntimeRenderState& state, const std::vector<std::pair<std::string, std::string>>& passSources, LayerProgram& layerProgram, int errorMessageSize, char* errorMessage)
{
GLsizei errorBufferSize = 0;
GLint compileResult = GL_FALSE;
GLint linkResult = GL_FALSE;
std::string loadError;
std::vector<LayerProgram::TextureBinding> textureBindings;
const char* vertexSource = kFullscreenTriangleVertexShaderSource;
const char* fragmentSource = fragmentShaderSource.c_str();
ScopedGlShader newVertexShader(glCreateShader(GL_VERTEX_SHADER));
glShaderSource(newVertexShader.get(), 1, (const GLchar**)&vertexSource, NULL);
glCompileShader(newVertexShader.get());
glGetShaderiv(newVertexShader.get(), GL_COMPILE_STATUS, &compileResult);
if (compileResult == GL_FALSE)
{
glGetShaderInfoLog(newVertexShader.get(), errorMessageSize, &errorBufferSize, errorMessage);
return false;
}
ScopedGlShader newFragmentShader(glCreateShader(GL_FRAGMENT_SHADER));
glShaderSource(newFragmentShader.get(), 1, (const GLchar**)&fragmentSource, NULL);
glCompileShader(newFragmentShader.get());
glGetShaderiv(newFragmentShader.get(), GL_COMPILE_STATUS, &compileResult);
if (compileResult == GL_FALSE)
{
glGetShaderInfoLog(newFragmentShader.get(), errorMessageSize, &errorBufferSize, errorMessage);
return false;
}
ScopedGlProgram newProgram(glCreateProgram());
glAttachShader(newProgram.get(), newVertexShader.get());
glAttachShader(newProgram.get(), newFragmentShader.get());
glLinkProgram(newProgram.get());
glGetProgramiv(newProgram.get(), GL_LINK_STATUS, &linkResult);
if (linkResult == GL_FALSE)
{
glGetProgramInfoLog(newProgram.get(), errorMessageSize, &errorBufferSize, errorMessage);
return false;
}
for (const ShaderTextureAsset& textureAsset : state.textureAssets)
{
LayerProgram::TextureBinding textureBinding;
textureBinding.samplerName = textureAsset.id;
textureBinding.sourcePath = textureAsset.path;
if (!mTextureBindings.LoadTextureAsset(textureAsset, textureBinding.texture, loadError))
{
for (LayerProgram::TextureBinding& loadedTexture : textureBindings)
{
if (loadedTexture.texture != 0)
glDeleteTextures(1, &loadedTexture.texture);
}
CopyErrorMessage(loadError, errorMessageSize, errorMessage);
return false;
}
textureBindings.push_back(textureBinding);
}
std::vector<LayerProgram::TextBinding> textBindings;
mTextureBindings.CreateTextBindings(state, textBindings);
layerProgram.layerId = state.layerId;
layerProgram.shaderId = state.shaderId;
layerProgram.passes.clear();
PassProgram passProgram;
passProgram.passId = "main";
passProgram.shaderTextureBase = mTextureBindings.ResolveShaderTextureBase(state, mRuntimeHost.GetMaxTemporalHistoryFrames());
passProgram.textureBindings.swap(textureBindings);
passProgram.textBindings.swap(textBindings);
for (const auto& passSource : passSources)
{
GLint compileResult = GL_FALSE;
GLint linkResult = GL_FALSE;
const char* fragmentSource = passSource.second.c_str();
const GLuint globalParamsIndex = glGetUniformBlockIndex(newProgram.get(), "GlobalParams");
if (globalParamsIndex != GL_INVALID_INDEX)
glUniformBlockBinding(newProgram.get(), globalParamsIndex, kGlobalParamsBindingPoint);
ScopedGlShader newVertexShader(glCreateShader(GL_VERTEX_SHADER));
glShaderSource(newVertexShader.get(), 1, (const GLchar**)&vertexSource, NULL);
glCompileShader(newVertexShader.get());
glGetShaderiv(newVertexShader.get(), GL_COMPILE_STATUS, &compileResult);
if (compileResult == GL_FALSE)
{
glGetShaderInfoLog(newVertexShader.get(), errorMessageSize, &errorBufferSize, errorMessage);
mRenderer.DestroySingleLayerProgram(layerProgram);
return false;
}
const unsigned historyCap = mRuntimeHost.GetMaxTemporalHistoryFrames();
glUseProgram(newProgram.get());
mTextureBindings.AssignLayerSamplerUniforms(newProgram.get(), state, passProgram, historyCap);
glUseProgram(0);
ScopedGlShader newFragmentShader(glCreateShader(GL_FRAGMENT_SHADER));
glShaderSource(newFragmentShader.get(), 1, (const GLchar**)&fragmentSource, NULL);
glCompileShader(newFragmentShader.get());
glGetShaderiv(newFragmentShader.get(), GL_COMPILE_STATUS, &compileResult);
if (compileResult == GL_FALSE)
{
glGetShaderInfoLog(newFragmentShader.get(), errorMessageSize, &errorBufferSize, errorMessage);
mRenderer.DestroySingleLayerProgram(layerProgram);
return false;
}
passProgram.program = newProgram.release();
passProgram.vertexShader = newVertexShader.release();
passProgram.fragmentShader = newFragmentShader.release();
layerProgram.passes.push_back(std::move(passProgram));
ScopedGlProgram newProgram(glCreateProgram());
glAttachShader(newProgram.get(), newVertexShader.get());
glAttachShader(newProgram.get(), newFragmentShader.get());
glLinkProgram(newProgram.get());
glGetProgramiv(newProgram.get(), GL_LINK_STATUS, &linkResult);
if (linkResult == GL_FALSE)
{
glGetProgramInfoLog(newProgram.get(), errorMessageSize, &errorBufferSize, errorMessage);
mRenderer.DestroySingleLayerProgram(layerProgram);
return false;
}
std::vector<LayerProgram::TextureBinding> textureBindings;
for (const ShaderTextureAsset& textureAsset : state.textureAssets)
{
LayerProgram::TextureBinding textureBinding;
textureBinding.samplerName = textureAsset.id;
textureBinding.sourcePath = textureAsset.path;
if (!mTextureBindings.LoadTextureAsset(textureAsset, textureBinding.texture, loadError))
{
for (LayerProgram::TextureBinding& loadedTexture : textureBindings)
{
if (loadedTexture.texture != 0)
glDeleteTextures(1, &loadedTexture.texture);
}
CopyErrorMessage(loadError, errorMessageSize, errorMessage);
mRenderer.DestroySingleLayerProgram(layerProgram);
return false;
}
textureBindings.push_back(textureBinding);
}
std::vector<LayerProgram::TextBinding> textBindings;
mTextureBindings.CreateTextBindings(state, textBindings);
PassProgram passProgram;
passProgram.passId = passSource.first;
passProgram.shaderTextureBase = mTextureBindings.ResolveShaderTextureBase(state, mRuntimeHost.GetMaxTemporalHistoryFrames());
passProgram.textureBindings.swap(textureBindings);
passProgram.textBindings.swap(textBindings);
const GLuint globalParamsIndex = glGetUniformBlockIndex(newProgram.get(), "GlobalParams");
if (globalParamsIndex != GL_INVALID_INDEX)
glUniformBlockBinding(newProgram.get(), globalParamsIndex, kGlobalParamsBindingPoint);
const unsigned historyCap = mRuntimeHost.GetMaxTemporalHistoryFrames();
glUseProgram(newProgram.get());
mTextureBindings.AssignLayerSamplerUniforms(newProgram.get(), state, passProgram, historyCap);
glUseProgram(0);
passProgram.program = newProgram.release();
passProgram.vertexShader = newVertexShader.release();
passProgram.fragmentShader = newFragmentShader.release();
layerProgram.passes.push_back(std::move(passProgram));
}
return true;
}

View File

@@ -5,6 +5,8 @@
#include "ShaderTextureBindings.h"
#include <string>
#include <utility>
#include <vector>
class ShaderProgramCompiler
{
@@ -16,6 +18,7 @@ public:
bool CompileLayerProgram(const RuntimeRenderState& state, LayerProgram& layerProgram, int errorMessageSize, char* errorMessage);
bool CompilePreparedLayerProgram(const RuntimeRenderState& state, const std::string& fragmentShaderSource, LayerProgram& layerProgram, int errorMessageSize, char* errorMessage);
bool CompilePreparedLayerProgram(const RuntimeRenderState& state, const std::vector<std::pair<std::string, std::string>>& passSources, LayerProgram& layerProgram, int errorMessageSize, char* errorMessage);
bool CompileDecodeShader(int errorMessageSize, char* errorMessage);
bool CompileOutputPackShader(int errorMessageSize, char* errorMessage);

View File

@@ -1301,6 +1301,20 @@ bool RuntimeHost::TryAdvanceFrame()
}
bool RuntimeHost::BuildLayerFragmentShaderSource(const std::string& layerId, std::string& fragmentShaderSource, std::string& error)
{
std::vector<std::pair<std::string, std::string>> passSources;
if (!BuildLayerPassFragmentShaderSources(layerId, passSources, error))
return false;
if (passSources.empty())
{
error = "Shader layer produced no compiled passes: " + layerId;
return false;
}
fragmentShaderSource = passSources.front().second;
return true;
}
bool RuntimeHost::BuildLayerPassFragmentShaderSources(const std::string& layerId, std::vector<std::pair<std::string, std::string>>& passSources, std::string& error)
{
try
{
@@ -1324,16 +1338,25 @@ bool RuntimeHost::BuildLayerFragmentShaderSource(const std::string& layerId, std
}
ShaderCompiler compiler(mRepoRoot, mWrapperPath, mGeneratedGlslPath, mPatchedGlslPath, mConfig.maxTemporalHistoryFrames);
return compiler.BuildLayerFragmentShaderSource(shaderPackage, fragmentShaderSource, error);
passSources.clear();
passSources.reserve(shaderPackage.passes.size());
for (const ShaderPassDefinition& pass : shaderPackage.passes)
{
std::string fragmentShaderSource;
if (!compiler.BuildPassFragmentShaderSource(shaderPackage, pass, fragmentShaderSource, error))
return false;
passSources.push_back(std::make_pair(pass.id, std::move(fragmentShaderSource)));
}
return true;
}
catch (const std::exception& exception)
{
error = std::string("RuntimeHost::BuildLayerFragmentShaderSource exception: ") + exception.what();
error = std::string("RuntimeHost::BuildLayerPassFragmentShaderSources exception: ") + exception.what();
return false;
}
catch (...)
{
error = "RuntimeHost::BuildLayerFragmentShaderSource threw a non-standard exception.";
error = "RuntimeHost::BuildLayerPassFragmentShaderSources threw a non-standard exception.";
return false;
}
}
@@ -1673,35 +1696,8 @@ bool RuntimeHost::ScanShaderPackages(std::string& error)
bool RuntimeHost::ParseShaderManifest(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, mConfig.maxTemporalHistoryFrames, manifestPath, error) &&
ParseParameterDefinitions(manifestJson, shaderPackage, manifestPath, error);
ShaderPackageRegistry registry(mConfig.maxTemporalHistoryFrames);
return registry.ParseManifest(manifestPath, shaderPackage, error);
}
bool RuntimeHost::NormalizeAndValidateValue(const ShaderParameterDefinition& definition, const JsonValue& value, ShaderParameterValue& normalizedValue, std::string& error) const

View File

@@ -9,6 +9,7 @@
#include <map>
#include <mutex>
#include <string>
#include <utility>
#include <vector>
class RuntimeHost
@@ -51,6 +52,7 @@ public:
bool TryAdvanceFrame();
bool BuildLayerFragmentShaderSource(const std::string& layerId, std::string& fragmentShaderSource, std::string& error);
bool BuildLayerPassFragmentShaderSources(const std::string& layerId, std::vector<std::pair<std::string, std::string>>& passSources, std::string& error);
std::vector<RuntimeRenderState> GetLayerRenderStates(unsigned outputWidth, unsigned outputHeight) const;
bool TryGetLayerRenderStates(unsigned outputWidth, unsigned outputHeight, std::vector<RuntimeRenderState>& states) const;
void RefreshDynamicRenderStateFields(std::vector<RuntimeRenderState>& states) const;

View File

@@ -144,9 +144,19 @@ ShaderCompiler::ShaderCompiler(
}
bool ShaderCompiler::BuildLayerFragmentShaderSource(const ShaderPackage& shaderPackage, std::string& fragmentShaderSource, std::string& error) const
{
if (shaderPackage.passes.empty())
{
error = "Shader package has no render passes: " + shaderPackage.id;
return false;
}
return BuildPassFragmentShaderSource(shaderPackage, shaderPackage.passes.front(), fragmentShaderSource, error);
}
bool ShaderCompiler::BuildPassFragmentShaderSource(const ShaderPackage& shaderPackage, const ShaderPassDefinition& pass, std::string& fragmentShaderSource, std::string& error) const
{
std::string wrapperSource;
if (!BuildWrapperSlangSource(shaderPackage, wrapperSource, error))
if (!BuildWrapperSlangSource(shaderPackage, pass, wrapperSource, error))
return false;
if (!WriteTextFile(mWrapperPath, wrapperSource, error))
return false;
@@ -167,7 +177,7 @@ bool ShaderCompiler::BuildLayerFragmentShaderSource(const ShaderPackage& shaderP
return true;
}
bool ShaderCompiler::BuildWrapperSlangSource(const ShaderPackage& shaderPackage, std::string& wrapperSource, std::string& error) const
bool ShaderCompiler::BuildWrapperSlangSource(const ShaderPackage& shaderPackage, const ShaderPassDefinition& pass, std::string& wrapperSource, std::string& error) const
{
const std::filesystem::path templatePath = mRepoRoot / "runtime" / "templates" / "shader_wrapper.slang.in";
wrapperSource = ReadTextFile(templatePath, error);
@@ -183,8 +193,8 @@ bool ShaderCompiler::BuildWrapperSlangSource(const ShaderPackage& shaderPackage,
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)");
wrapperSource = ReplaceAll(wrapperSource, "{{USER_SHADER_INCLUDE}}", pass.sourcePath.generic_string());
wrapperSource = ReplaceAll(wrapperSource, "{{ENTRY_POINT_CALL}}", pass.entryPoint + "(context)");
return true;
}

View File

@@ -16,9 +16,10 @@ public:
unsigned maxTemporalHistoryFrames);
bool BuildLayerFragmentShaderSource(const ShaderPackage& shaderPackage, std::string& fragmentShaderSource, std::string& error) const;
bool BuildPassFragmentShaderSource(const ShaderPackage& shaderPackage, const ShaderPassDefinition& pass, std::string& fragmentShaderSource, std::string& error) const;
private:
bool BuildWrapperSlangSource(const ShaderPackage& shaderPackage, std::string& wrapperSource, std::string& error) const;
bool BuildWrapperSlangSource(const ShaderPackage& shaderPackage, const ShaderPassDefinition& pass, 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;

View File

@@ -250,6 +250,103 @@ bool ParseShaderMetadata(const JsonValue& manifestJson, ShaderPackage& shaderPac
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 ParseTextureAssets(const JsonValue& manifestJson, ShaderPackage& shaderPackage, const std::filesystem::path& manifestPath, std::string& error)
{
const JsonValue* texturesValue = nullptr;
@@ -666,13 +763,15 @@ bool ShaderPackageRegistry::ParseManifest(const std::filesystem::path& manifestP
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();
if (!ParsePassDefinitions(manifestJson, shaderPackage, manifestPath, error))
return false;
}
shaderPackage.shaderWriteTime = std::filesystem::last_write_time(shaderPackage.shaderPath);
shaderPackage.shaderWriteTime = shaderPackage.passes.front().sourceWriteTime;
for (const ShaderPassDefinition& pass : shaderPackage.passes)
{
if (pass.sourceWriteTime > shaderPackage.shaderWriteTime)
shaderPackage.shaderWriteTime = pass.sourceWriteTime;
}
shaderPackage.manifestWriteTime = std::filesystem::last_write_time(shaderPackage.manifestPath);
return ParseTextureAssets(manifestJson, shaderPackage, manifestPath, error) &&

View File

@@ -76,6 +76,16 @@ struct ShaderFontAsset
std::filesystem::file_time_type writeTime;
};
struct ShaderPassDefinition
{
std::string id;
std::string entryPoint;
std::filesystem::path sourcePath;
std::filesystem::file_time_type sourceWriteTime;
std::vector<std::string> inputNames;
std::string outputName;
};
struct ShaderPackage
{
std::string id;
@@ -86,6 +96,7 @@ struct ShaderPackage
std::filesystem::path directoryPath;
std::filesystem::path shaderPath;
std::filesystem::path manifestPath;
std::vector<ShaderPassDefinition> passes;
std::vector<ShaderParameterDefinition> parameters;
std::vector<ShaderTextureAsset> textureAssets;
std::vector<ShaderFontAsset> fontAssets;

View File

@@ -80,6 +80,34 @@ void TestValidManifest()
Expect(package.parameters.size() == 4, "parameters parse");
Expect(package.parameters[1].type == ShaderParameterType::Text && package.parameters[1].defaultTextValue == "LIVE", "text parameter parses");
Expect(package.parameters[3].type == ShaderParameterType::Trigger, "trigger parameter parses");
Expect(package.passes.size() == 1 && package.passes[0].id == "main", "legacy manifests get an implicit main pass");
std::filesystem::remove_all(root);
}
void TestExplicitPassManifest()
{
const std::filesystem::path root = MakeTestRoot();
WriteShaderPackage(root, "multi", R"({
"id": "multi-pass",
"name": "Multi Pass",
"passes": [
{ "id": "blurX", "source": "blur-x.slang", "entryPoint": "blurHorizontal", "inputs": ["layerInput"], "output": "blurredX" },
{ "id": "final", "source": "final.slang", "entryPoint": "finish", "inputs": ["blurredX"], "output": "layerOutput" }
],
"parameters": []
})");
WriteFile(root / "multi" / "blur-x.slang", "float4 blurHorizontal(float2 uv) { return float4(uv, 0.0, 1.0); }\n");
WriteFile(root / "multi" / "final.slang", "float4 finish(float2 uv) { return float4(uv, 1.0, 1.0); }\n");
ShaderPackageRegistry registry(4);
ShaderPackage package;
std::string error;
Expect(registry.ParseManifest(root / "multi" / "shader.json", package, error), "explicit pass manifest parses");
Expect(package.passes.size() == 2, "explicit passes parse");
Expect(package.passes[0].id == "blurX" && package.passes[0].entryPoint == "blurHorizontal", "first pass metadata parses");
Expect(package.passes[0].inputNames.size() == 1 && package.passes[0].inputNames[0] == "layerInput", "pass inputs parse");
Expect(package.passes[1].outputName == "layerOutput", "pass output parses");
std::filesystem::remove_all(root);
}
@@ -231,6 +259,7 @@ void TestInvalidPackageDoesNotFailScan()
int main()
{
TestValidManifest();
TestExplicitPassManifest();
TestMissingFontAsset();
TestInvalidManifest();
TestInvalidTemporalSettings();

View File

@@ -81,15 +81,19 @@ int main()
if (packageIt == packagesById.end())
continue;
std::string fragmentShaderSource;
std::string compileError;
if (!compiler.BuildLayerFragmentShaderSource(packageIt->second, fragmentShaderSource, compileError))
const ShaderPackage& shaderPackage = packageIt->second;
for (const ShaderPassDefinition& pass : shaderPackage.passes)
{
Fail("Shader package '" + packageId + "' failed Slang validation: " + compileError);
continue;
std::string fragmentShaderSource;
std::string compileError;
if (!compiler.BuildPassFragmentShaderSource(shaderPackage, pass, fragmentShaderSource, compileError))
{
Fail("Shader package '" + packageId + "' pass '" + pass.id + "' failed Slang validation: " + compileError);
continue;
}
if (fragmentShaderSource.find("#version 430 core") == std::string::npos)
Fail("Shader package '" + packageId + "' pass '" + pass.id + "' generated GLSL without the expected patched GLSL version header.");
}
if (fragmentShaderSource.find("#version 430 core") == std::string::npos)
Fail("Shader package '" + packageId + "' generated GLSL without the expected patched GLSL version header.");
}
std::error_code removeError;