diff --git a/README.md b/README.md index a0644e4..c74f6f7 100644 --- a/README.md +++ b/README.md @@ -206,10 +206,11 @@ Each shader package lives under: shaders// 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 diff --git a/SHADER_CONTRACT.md b/SHADER_CONTRACT.md index 8c95156..64320ab 100644 --- a/SHADER_CONTRACT.md +++ b/SHADER_CONTRACT.md @@ -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`. diff --git a/apps/LoopThroughWithOpenGLCompositing/gl/shader/OpenGLShaderPrograms.cpp b/apps/LoopThroughWithOpenGLCompositing/gl/shader/OpenGLShaderPrograms.cpp index b9dd3b7..3cf09eb 100644 --- a/apps/LoopThroughWithOpenGLCompositing/gl/shader/OpenGLShaderPrograms.cpp +++ b/apps/LoopThroughWithOpenGLCompositing/gl/shader/OpenGLShaderPrograms.cpp @@ -2,6 +2,7 @@ #include #include +#include #include 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> 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); diff --git a/apps/LoopThroughWithOpenGLCompositing/gl/shader/ShaderBuildQueue.cpp b/apps/LoopThroughWithOpenGLCompositing/gl/shader/ShaderBuildQueue.cpp index f69b72b..e5a4e8a 100644 --- a/apps/LoopThroughWithOpenGLCompositing/gl/shader/ShaderBuildQueue.cpp +++ b/apps/LoopThroughWithOpenGLCompositing/gl/shader/ShaderBuildQueue.cpp @@ -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> 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)); } diff --git a/apps/LoopThroughWithOpenGLCompositing/gl/shader/ShaderBuildQueue.h b/apps/LoopThroughWithOpenGLCompositing/gl/shader/ShaderBuildQueue.h index 6975962..250a12d 100644 --- a/apps/LoopThroughWithOpenGLCompositing/gl/shader/ShaderBuildQueue.h +++ b/apps/LoopThroughWithOpenGLCompositing/gl/shader/ShaderBuildQueue.h @@ -13,8 +13,14 @@ class RuntimeHost; struct PreparedLayerShader { + struct Pass + { + std::string passId; + std::string fragmentShaderSource; + }; + RuntimeRenderState state; - std::string fragmentShaderSource; + std::vector passes; }; struct PreparedShaderBuild diff --git a/apps/LoopThroughWithOpenGLCompositing/gl/shader/ShaderProgramCompiler.cpp b/apps/LoopThroughWithOpenGLCompositing/gl/shader/ShaderProgramCompiler.cpp index 42002f3..7910415 100644 --- a/apps/LoopThroughWithOpenGLCompositing/gl/shader/ShaderProgramCompiler.cpp +++ b/apps/LoopThroughWithOpenGLCompositing/gl/shader/ShaderProgramCompiler.cpp @@ -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> 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> 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>& passSources, LayerProgram& layerProgram, int errorMessageSize, char* errorMessage) { GLsizei errorBufferSize = 0; - GLint compileResult = GL_FALSE; - GLint linkResult = GL_FALSE; std::string loadError; - std::vector 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 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 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 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; } diff --git a/apps/LoopThroughWithOpenGLCompositing/gl/shader/ShaderProgramCompiler.h b/apps/LoopThroughWithOpenGLCompositing/gl/shader/ShaderProgramCompiler.h index 4e4b622..68fdf6c 100644 --- a/apps/LoopThroughWithOpenGLCompositing/gl/shader/ShaderProgramCompiler.h +++ b/apps/LoopThroughWithOpenGLCompositing/gl/shader/ShaderProgramCompiler.h @@ -5,6 +5,8 @@ #include "ShaderTextureBindings.h" #include +#include +#include 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>& passSources, LayerProgram& layerProgram, int errorMessageSize, char* errorMessage); bool CompileDecodeShader(int errorMessageSize, char* errorMessage); bool CompileOutputPackShader(int errorMessageSize, char* errorMessage); diff --git a/apps/LoopThroughWithOpenGLCompositing/runtime/RuntimeHost.cpp b/apps/LoopThroughWithOpenGLCompositing/runtime/RuntimeHost.cpp index 434dd31..6621db4 100644 --- a/apps/LoopThroughWithOpenGLCompositing/runtime/RuntimeHost.cpp +++ b/apps/LoopThroughWithOpenGLCompositing/runtime/RuntimeHost.cpp @@ -1301,6 +1301,20 @@ bool RuntimeHost::TryAdvanceFrame() } bool RuntimeHost::BuildLayerFragmentShaderSource(const std::string& layerId, std::string& fragmentShaderSource, std::string& error) +{ + std::vector> 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>& 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 diff --git a/apps/LoopThroughWithOpenGLCompositing/runtime/RuntimeHost.h b/apps/LoopThroughWithOpenGLCompositing/runtime/RuntimeHost.h index b517cac..0f39157 100644 --- a/apps/LoopThroughWithOpenGLCompositing/runtime/RuntimeHost.h +++ b/apps/LoopThroughWithOpenGLCompositing/runtime/RuntimeHost.h @@ -9,6 +9,7 @@ #include #include #include +#include #include 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>& passSources, std::string& error); std::vector GetLayerRenderStates(unsigned outputWidth, unsigned outputHeight) const; bool TryGetLayerRenderStates(unsigned outputWidth, unsigned outputHeight, std::vector& states) const; void RefreshDynamicRenderStateFields(std::vector& states) const; diff --git a/apps/LoopThroughWithOpenGLCompositing/shader/ShaderCompiler.cpp b/apps/LoopThroughWithOpenGLCompositing/shader/ShaderCompiler.cpp index f5b6425..a7efd61 100644 --- a/apps/LoopThroughWithOpenGLCompositing/shader/ShaderCompiler.cpp +++ b/apps/LoopThroughWithOpenGLCompositing/shader/ShaderCompiler.cpp @@ -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; } diff --git a/apps/LoopThroughWithOpenGLCompositing/shader/ShaderCompiler.h b/apps/LoopThroughWithOpenGLCompositing/shader/ShaderCompiler.h index 2e4d8eb..8bf913d 100644 --- a/apps/LoopThroughWithOpenGLCompositing/shader/ShaderCompiler.h +++ b/apps/LoopThroughWithOpenGLCompositing/shader/ShaderCompiler.h @@ -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; diff --git a/apps/LoopThroughWithOpenGLCompositing/shader/ShaderPackageRegistry.cpp b/apps/LoopThroughWithOpenGLCompositing/shader/ShaderPackageRegistry.cpp index 4bc9d02..64c3e44 100644 --- a/apps/LoopThroughWithOpenGLCompositing/shader/ShaderPackageRegistry.cpp +++ b/apps/LoopThroughWithOpenGLCompositing/shader/ShaderPackageRegistry.cpp @@ -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) && diff --git a/apps/LoopThroughWithOpenGLCompositing/shader/ShaderTypes.h b/apps/LoopThroughWithOpenGLCompositing/shader/ShaderTypes.h index 8c84297..62ffa90 100644 --- a/apps/LoopThroughWithOpenGLCompositing/shader/ShaderTypes.h +++ b/apps/LoopThroughWithOpenGLCompositing/shader/ShaderTypes.h @@ -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 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 passes; std::vector parameters; std::vector textureAssets; std::vector fontAssets; diff --git a/tests/ShaderPackageRegistryTests.cpp b/tests/ShaderPackageRegistryTests.cpp index 3579edf..c44f9c0 100644 --- a/tests/ShaderPackageRegistryTests.cpp +++ b/tests/ShaderPackageRegistryTests.cpp @@ -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(); diff --git a/tests/ShaderSlangValidationTests.cpp b/tests/ShaderSlangValidationTests.cpp index 4acb5b8..063ec4a 100644 --- a/tests/ShaderSlangValidationTests.cpp +++ b/tests/ShaderSlangValidationTests.cpp @@ -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;