From dfd49fd0e310168ab70c87e9d1935c4f2cae7adb Mon Sep 17 00:00:00 2001 From: Aiden <68633820+awils27@users.noreply.github.com> Date: Tue, 12 May 2026 17:08:35 +1000 Subject: [PATCH] Multipass shaders --- apps/RenderCadenceCompositor/README.md | 19 +- .../render/runtime/RuntimeRenderScene.cpp | 255 +++++++++++++++--- .../render/runtime/RuntimeRenderScene.h | 18 +- .../runtime/RuntimeShaderPrepareWorker.cpp | 43 ++- .../runtime/RuntimeShaderPrepareWorker.h | 2 + .../render/runtime/RuntimeShaderProgram.h | 5 + .../render/runtime/RuntimeShaderRenderer.cpp | 24 +- .../render/runtime/RuntimeShaderRenderer.h | 6 + .../runtime/RuntimeShaderArtifact.h | 9 + .../runtime/RuntimeSlangShaderCompiler.cpp | 23 +- .../runtime/SupportedShaderCatalog.cpp | 33 ++- ...eCompositorSupportedShaderCatalogTests.cpp | 27 +- 12 files changed, 392 insertions(+), 72 deletions(-) diff --git a/apps/RenderCadenceCompositor/README.md b/apps/RenderCadenceCompositor/README.md index 1992c97..33bc7aa 100644 --- a/apps/RenderCadenceCompositor/README.md +++ b/apps/RenderCadenceCompositor/README.md @@ -47,7 +47,9 @@ Included now: - shared-context GL prepare worker for runtime shader program compile/link - render-thread-only GL program swap once a prepared program is ready - manifest-driven stateless single-pass shader packages -- HTTP shader list populated from supported stateless single-pass shader packages +- manifest-driven stateless named-pass shader packages +- atomic render-plan swap after every pass program is prepared +- HTTP shader list populated from supported stateless full-frame shader packages - default float, vec2, color, boolean, enum, and trigger parameters - small JSON writer for future HTTP/WebSocket payloads - JSON serialization for cadence telemetry snapshots @@ -60,7 +62,6 @@ Included now: Intentionally not included yet: - DeckLink input -- multipass shader rendering - temporal/history/feedback shader storage - texture/LUT asset upload - text-parameter rasterization @@ -86,6 +87,8 @@ This tracks parity with `apps/LoopThroughWithOpenGLCompositing`. - [x] Shared-context GL shader/program preparation - [x] Render-thread program swap at a frame boundary - [x] Stateless single-pass shader rendering +- [x] Stateless named-pass shader rendering +- [x] Atomic multipass render-plan commit - [x] Shader add/remove control path - [x] Previous-layer texture handoff for stacked shaders - [x] Supported shader list in HTTP/UI state @@ -99,7 +102,6 @@ This tracks parity with `apps/LoopThroughWithOpenGLCompositing`. - [ ] DeckLink input capture - [ ] Input frame upload into the render scene - [ ] Live video input bound to `gVideoInput` -- [ ] Multipass shader rendering - [ ] Temporal history buffers - [ ] Feedback buffers - [ ] Texture asset loading and upload @@ -244,9 +246,12 @@ On startup the app begins compiling the selected shader package on a background The render thread keeps drawing the simple motion renderer while Slang compiles. It does not choose packages, launch Slang, or track build lifecycle. Once a completed shader artifact is published, the render-thread-owned runtime scene queues changed layers to a shared-context GL prepare worker. That worker compiles/links runtime shader programs off the cadence thread. The render thread only swaps in an already-prepared GL program at a frame boundary. If either the Slang build or GL preparation fails, the app keeps rendering the current renderer or simple motion fallback. -Current runtime shader support is deliberately limited to stateless single-pass packages: +Current runtime shader support is deliberately limited to stateless full-frame packages: -- one pass only +- one or more named passes +- one sampled source input per pass +- named intermediate outputs routed by the pass manifest +- final visible output must be named `layerOutput` - no temporal history - no feedback storage - no texture/LUT assets yet @@ -255,11 +260,11 @@ Current runtime shader support is deliberately limited to stateless single-pass - the first layer receives a small fallback source texture until DeckLink input is added - stacked layers receive the previous ready layer output through both `gVideoInput` and `gLayerInput` -The `/api/state` shader list uses the same support rules as runtime shader compilation and reports only packages this app can run today. Unsupported manifest feature sets such as multipass, temporal, feedback, texture-backed, font-backed, or text-parameter shaders are hidden from the control UI for now. +The `/api/state` shader list uses the same support rules as runtime shader compilation and reports only packages this app can run today. Unsupported manifest feature sets such as temporal, feedback, texture-backed, font-backed, or text-parameter shaders are hidden from the control UI for now. Runtime shaders are exposed through `RuntimeLayerModel` as display layers with manifest parameter defaults. The model also records whether each layer has a render-ready artifact. Add/remove POST controls mutate this app-owned model and may start background shader builds. -When a layer becomes render-ready, the app publishes the ready render-layer snapshot to the render thread. The render thread owns the GL-side `RuntimeRenderScene`, diffs that snapshot at a frame boundary, queues new or changed programs to the shared-context prepare worker, swaps in prepared programs when available, removes obsolete GL programs, and renders ready layers in order. Stacked stateless full-frame shaders render through internal ping-pong targets so each layer can sample the previous layer through `gLayerInput`; the final ready layer renders to the output target. +When a layer becomes render-ready, the app publishes the ready render-layer snapshot to the render thread. The render thread owns the GL-side `RuntimeRenderScene`, diffs that snapshot at a frame boundary, queues new or changed pass programs to the shared-context prepare worker, swaps in a full prepared render plan only after every pass is ready, removes obsolete GL programs, and renders ready layers in order. Stacked stateless full-frame shaders render through internal ping-pong targets so each layer can sample the previous layer through `gLayerInput`; multipass shaders route named intermediate outputs through their manifest-declared pass inputs, and the final ready layer renders to the output target. Successful handoff signs: diff --git a/apps/RenderCadenceCompositor/render/runtime/RuntimeRenderScene.cpp b/apps/RenderCadenceCompositor/render/runtime/RuntimeRenderScene.cpp index be22c4a..562c248 100644 --- a/apps/RenderCadenceCompositor/render/runtime/RuntimeRenderScene.cpp +++ b/apps/RenderCadenceCompositor/render/runtime/RuntimeRenderScene.cpp @@ -39,14 +39,18 @@ bool RuntimeRenderScene::CommitRenderLayers(const std::vectorrenderer) - layerIt->renderer->ShutdownGl(); + for (LayerProgram::PassProgram& pass : layerIt->passes) + { + if (pass.renderer) + pass.renderer->ShutdownGl(); + } + ReleasePendingPrograms(*layerIt); layerIt = mLayers.erase(layerIt); } for (const RenderCadenceCompositor::RuntimeRenderLayerModel& layer : layers) { - if (layer.artifact.fragmentShaderSource.empty()) + if (layer.artifact.passes.empty() && layer.artifact.fragmentShaderSource.empty()) continue; const std::string fingerprint = Fingerprint(layer.artifact); @@ -55,16 +59,25 @@ bool RuntimeRenderScene::CommitRenderLayers(const std::vector(); mLayers.push_back(std::move(next)); program = &mLayers.back(); } - if (program->shaderId == layer.shaderId && program->sourceFingerprint == fingerprint && program->renderer && program->renderer->HasProgram()) + bool hasReadyPass = false; + for (const LayerProgram::PassProgram& pass : program->passes) + { + if (pass.renderer && pass.renderer->HasProgram()) + { + hasReadyPass = true; + break; + } + } + if (program->shaderId == layer.shaderId && program->sourceFingerprint == fingerprint && hasReadyPass) continue; if (program->pendingFingerprint == fingerprint) continue; + ReleasePendingPrograms(*program); program->shaderId = layer.shaderId; program->pendingFingerprint = fingerprint; layersToPrepare.push_back(layer); @@ -84,8 +97,13 @@ bool RuntimeRenderScene::HasLayers() for (const std::string& layerId : mLayerOrder) { const LayerProgram* layer = FindLayer(layerId); - if (layer && layer->renderer && layer->renderer->HasProgram()) - return true; + if (!layer) + continue; + for (const LayerProgram::PassProgram& pass : layer->passes) + { + if (pass.renderer && pass.renderer->HasProgram()) + return true; + } } return false; } @@ -98,9 +116,16 @@ void RuntimeRenderScene::RenderFrame(uint64_t frameIndex, unsigned width, unsign for (const std::string& layerId : mLayerOrder) { LayerProgram* layer = FindLayer(layerId); - if (!layer || !layer->renderer || !layer->renderer->HasProgram()) + if (!layer) continue; - readyLayers.push_back(layer); + for (const LayerProgram::PassProgram& pass : layer->passes) + { + if (pass.renderer && pass.renderer->HasProgram()) + { + readyLayers.push_back(layer); + break; + } + } } if (readyLayers.empty()) @@ -111,14 +136,14 @@ void RuntimeRenderScene::RenderFrame(uint64_t frameIndex, unsigned width, unsign if (readyLayers.size() == 1) { - readyLayers.front()->renderer->RenderFrame(frameIndex, width, height); + RenderLayer(*readyLayers.front(), frameIndex, width, height, 0, static_cast(outputFramebuffer), true); return; } if (!EnsureLayerTargets(width, height)) { glBindFramebuffer(GL_FRAMEBUFFER, static_cast(outputFramebuffer)); - readyLayers.back()->renderer->RenderFrame(frameIndex, width, height); + RenderLayer(*readyLayers.back(), frameIndex, width, height, 0, static_cast(outputFramebuffer), true); return; } @@ -130,12 +155,11 @@ void RuntimeRenderScene::RenderFrame(uint64_t frameIndex, unsigned width, unsign if (isFinalLayer) { glBindFramebuffer(GL_FRAMEBUFFER, static_cast(outputFramebuffer)); - readyLayers[layerIndex]->renderer->RenderFrame(frameIndex, width, height, layerInputTexture, layerInputTexture); + RenderLayer(*readyLayers[layerIndex], frameIndex, width, height, layerInputTexture, static_cast(outputFramebuffer), true); continue; } - glBindFramebuffer(GL_FRAMEBUFFER, mLayerFramebuffers[nextTargetIndex]); - readyLayers[layerIndex]->renderer->RenderFrame(frameIndex, width, height, layerInputTexture, layerInputTexture); + RenderLayer(*readyLayers[layerIndex], frameIndex, width, height, layerInputTexture, mLayerFramebuffers[nextTargetIndex], true); layerInputTexture = mLayerTextures[nextTargetIndex]; nextTargetIndex = 1 - nextTargetIndex; } @@ -146,8 +170,12 @@ void RuntimeRenderScene::ShutdownGl() mPrepareWorker.Stop(); for (LayerProgram& layer : mLayers) { - if (layer.renderer) - layer.renderer->ShutdownGl(); + for (LayerProgram::PassProgram& pass : layer.passes) + { + if (pass.renderer) + pass.renderer->ShutdownGl(); + } + ReleasePendingPrograms(layer); } mLayers.clear(); mLayerOrder.clear(); @@ -172,28 +200,165 @@ void RuntimeRenderScene::ConsumePreparedPrograms() continue; } + bool replacesExistingPendingPass = false; + for (RuntimePreparedShaderProgram& existing : layer->pendingPreparedPrograms) + { + if (existing.passId != preparedProgram.passId) + continue; + existing.ReleaseGl(); + existing = std::move(preparedProgram); + replacesExistingPendingPass = true; + break; + } + if (!replacesExistingPendingPass) + layer->pendingPreparedPrograms.push_back(std::move(preparedProgram)); + TryCommitPendingPrograms(*layer); + } +} + +void RuntimeRenderScene::ReleasePendingPrograms(LayerProgram& layer) +{ + for (RuntimePreparedShaderProgram& program : layer.pendingPreparedPrograms) + program.ReleaseGl(); + layer.pendingPreparedPrograms.clear(); +} + +void RuntimeRenderScene::TryCommitPendingPrograms(LayerProgram& layer) +{ + if (layer.pendingPreparedPrograms.empty()) + return; + + const RuntimeShaderArtifact& artifact = layer.pendingPreparedPrograms.front().artifact; + const std::size_t expectedPassCount = artifact.passes.empty() ? 1 : artifact.passes.size(); + if (layer.pendingPreparedPrograms.size() < expectedPassCount) + return; + + std::vector nextPasses; + nextPasses.reserve(expectedPassCount); + for (const RuntimeShaderPassArtifact& passArtifact : artifact.passes) + { + auto preparedIt = std::find_if( + layer.pendingPreparedPrograms.begin(), + layer.pendingPreparedPrograms.end(), + [&passArtifact](const RuntimePreparedShaderProgram& prepared) { + return prepared.passId == passArtifact.passId; + }); + if (preparedIt == layer.pendingPreparedPrograms.end()) + return; + std::unique_ptr nextRenderer = std::make_unique(); std::string error; - if (!nextRenderer->CommitPreparedProgram(preparedProgram, error)) + if (!nextRenderer->CommitPreparedProgram(*preparedIt, error)) { - preparedProgram.ReleaseGl(); + ReleasePendingPrograms(layer); + return; + } + + LayerProgram::PassProgram nextPass; + nextPass.passId = preparedIt->passId; + nextPass.inputNames = preparedIt->inputNames; + nextPass.outputName = preparedIt->outputName.empty() ? preparedIt->passId : preparedIt->outputName; + nextPass.renderer = std::move(nextRenderer); + nextPasses.push_back(std::move(nextPass)); + } + if (artifact.passes.empty()) + { + RuntimePreparedShaderProgram& prepared = layer.pendingPreparedPrograms.front(); + std::unique_ptr nextRenderer = std::make_unique(); + std::string error; + if (!nextRenderer->CommitPreparedProgram(prepared, error)) + { + ReleasePendingPrograms(layer); + return; + } + + LayerProgram::PassProgram nextPass; + nextPass.passId = prepared.passId; + nextPass.inputNames = prepared.inputNames; + nextPass.outputName = prepared.outputName.empty() ? prepared.passId : prepared.outputName; + nextPass.renderer = std::move(nextRenderer); + nextPasses.push_back(std::move(nextPass)); + } + + for (LayerProgram::PassProgram& pass : layer.passes) + { + if (pass.renderer) + pass.renderer->ShutdownGl(); + } + layer.passes = std::move(nextPasses); + layer.shaderId = artifact.shaderId; + layer.sourceFingerprint = layer.pendingPreparedPrograms.front().sourceFingerprint; + layer.pendingFingerprint.clear(); + layer.pendingPreparedPrograms.clear(); +} + +GLuint RuntimeRenderScene::RenderLayer( + LayerProgram& layer, + uint64_t frameIndex, + unsigned width, + unsigned height, + GLuint layerInputTexture, + GLuint outputFramebuffer, + bool renderToOutput) +{ + GLuint namedOutputs[2] = {}; + std::string namedOutputNames[2]; + std::size_t nextTargetIndex = 2; + GLuint lastOutputTexture = layerInputTexture; + + for (LayerProgram::PassProgram& pass : layer.passes) + { + if (!pass.renderer || !pass.renderer->HasProgram()) + continue; + + GLuint sourceTexture = layerInputTexture; + if (!pass.inputNames.empty()) + { + const std::string& inputName = pass.inputNames.front(); + if (inputName != "layerInput" && inputName != "videoInput") + { + for (std::size_t index = 0; index < 2; ++index) + { + if (namedOutputNames[index] == inputName) + { + sourceTexture = namedOutputs[index]; + break; + } + } + } + } + + const bool writesLayerOutput = pass.outputName == "layerOutput"; + if (writesLayerOutput && renderToOutput) + { + glBindFramebuffer(GL_FRAMEBUFFER, outputFramebuffer); + pass.renderer->RenderFrame(frameIndex, width, height, sourceTexture, sourceTexture); + lastOutputTexture = 0; continue; } - if (layer->renderer) - layer->renderer->ShutdownGl(); - layer->renderer = std::move(nextRenderer); - layer->shaderId = preparedProgram.shaderId; - layer->sourceFingerprint = preparedProgram.sourceFingerprint; - layer->pendingFingerprint.clear(); + if (!EnsureLayerTargets(width, height)) + continue; + + const std::size_t targetIndex = nextTargetIndex; + nextTargetIndex = nextTargetIndex == 2 ? 3 : 2; + glBindFramebuffer(GL_FRAMEBUFFER, mLayerFramebuffers[targetIndex]); + pass.renderer->RenderFrame(frameIndex, width, height, sourceTexture, sourceTexture); + const std::size_t namedIndex = targetIndex - 2; + namedOutputs[namedIndex] = mLayerTextures[targetIndex]; + namedOutputNames[namedIndex] = pass.outputName; + lastOutputTexture = mLayerTextures[targetIndex]; } + + return lastOutputTexture; } bool RuntimeRenderScene::EnsureLayerTargets(unsigned width, unsigned height) { if (width == 0 || height == 0) return false; - if (mLayerFramebuffers[0] != 0 && mLayerFramebuffers[1] != 0 && mLayerTextures[0] != 0 && mLayerTextures[1] != 0 + if (mLayerFramebuffers[0] != 0 && mLayerFramebuffers[1] != 0 && mLayerFramebuffers[2] != 0 && mLayerFramebuffers[3] != 0 + && mLayerTextures[0] != 0 && mLayerTextures[1] != 0 && mLayerTextures[2] != 0 && mLayerTextures[3] != 0 && mLayerTargetWidth == width && mLayerTargetHeight == height) return true; @@ -201,9 +366,9 @@ bool RuntimeRenderScene::EnsureLayerTargets(unsigned width, unsigned height) mLayerTargetWidth = width; mLayerTargetHeight = height; - glGenFramebuffers(2, mLayerFramebuffers); - glGenTextures(2, mLayerTextures); - for (int index = 0; index < 2; ++index) + glGenFramebuffers(4, mLayerFramebuffers); + glGenTextures(4, mLayerTextures); + for (int index = 0; index < 4; ++index) { glBindTexture(GL_TEXTURE_2D, mLayerTextures[index]); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); @@ -239,14 +404,15 @@ bool RuntimeRenderScene::EnsureLayerTargets(unsigned width, unsigned height) void RuntimeRenderScene::DestroyLayerTargets() { - if (mLayerFramebuffers[0] != 0 || mLayerFramebuffers[1] != 0) - glDeleteFramebuffers(2, mLayerFramebuffers); - if (mLayerTextures[0] != 0 || mLayerTextures[1] != 0) - glDeleteTextures(2, mLayerTextures); - mLayerFramebuffers[0] = 0; - mLayerFramebuffers[1] = 0; - mLayerTextures[0] = 0; - mLayerTextures[1] = 0; + if (mLayerFramebuffers[0] != 0 || mLayerFramebuffers[1] != 0 || mLayerFramebuffers[2] != 0 || mLayerFramebuffers[3] != 0) + glDeleteFramebuffers(4, mLayerFramebuffers); + if (mLayerTextures[0] != 0 || mLayerTextures[1] != 0 || mLayerTextures[2] != 0 || mLayerTextures[3] != 0) + glDeleteTextures(4, mLayerTextures); + for (int index = 0; index < 4; ++index) + { + mLayerFramebuffers[index] = 0; + mLayerTextures[index] = 0; + } mLayerTargetWidth = 0; mLayerTargetHeight = 0; } @@ -271,8 +437,23 @@ const RuntimeRenderScene::LayerProgram* RuntimeRenderScene::FindLayer(const std: return nullptr; } +RuntimeRenderScene::LayerProgram::PassProgram* RuntimeRenderScene::FindPass(LayerProgram& layer, const std::string& passId) +{ + for (LayerProgram::PassProgram& pass : layer.passes) + { + if (pass.passId == passId) + return &pass; + } + return nullptr; +} + std::string RuntimeRenderScene::Fingerprint(const RuntimeShaderArtifact& artifact) { const std::hash hasher; - return artifact.shaderId + ":" + std::to_string(artifact.fragmentShaderSource.size()) + ":" + std::to_string(hasher(artifact.fragmentShaderSource)); + std::string source; + for (const RuntimeShaderPassArtifact& pass : artifact.passes) + source += pass.passId + ":" + pass.outputName + ":" + pass.fragmentShaderSource + "\n"; + if (source.empty()) + source = artifact.fragmentShaderSource; + return artifact.shaderId + ":" + std::to_string(source.size()) + ":" + std::to_string(hasher(source)); } diff --git a/apps/RenderCadenceCompositor/render/runtime/RuntimeRenderScene.h b/apps/RenderCadenceCompositor/render/runtime/RuntimeRenderScene.h index 6dff98b..25824bb 100644 --- a/apps/RenderCadenceCompositor/render/runtime/RuntimeRenderScene.h +++ b/apps/RenderCadenceCompositor/render/runtime/RuntimeRenderScene.h @@ -32,21 +32,33 @@ private: std::string shaderId; std::string sourceFingerprint; std::string pendingFingerprint; - std::unique_ptr renderer; + std::vector pendingPreparedPrograms; + struct PassProgram + { + std::string passId; + std::vector inputNames; + std::string outputName; + std::unique_ptr renderer; + }; + std::vector passes; }; void ConsumePreparedPrograms(); + void ReleasePendingPrograms(LayerProgram& layer); + void TryCommitPendingPrograms(LayerProgram& layer); bool EnsureLayerTargets(unsigned width, unsigned height); void DestroyLayerTargets(); + GLuint RenderLayer(LayerProgram& layer, uint64_t frameIndex, unsigned width, unsigned height, GLuint layerInputTexture, GLuint outputFramebuffer, bool renderToOutput); LayerProgram* FindLayer(const std::string& layerId); const LayerProgram* FindLayer(const std::string& layerId) const; + LayerProgram::PassProgram* FindPass(LayerProgram& layer, const std::string& passId); static std::string Fingerprint(const RuntimeShaderArtifact& artifact); RuntimeShaderPrepareWorker mPrepareWorker; std::vector mLayers; std::vector mLayerOrder; - GLuint mLayerFramebuffers[2] = {}; - GLuint mLayerTextures[2] = {}; + GLuint mLayerFramebuffers[4] = {}; + GLuint mLayerTextures[4] = {}; unsigned mLayerTargetWidth = 0; unsigned mLayerTargetHeight = 0; }; diff --git a/apps/RenderCadenceCompositor/render/runtime/RuntimeShaderPrepareWorker.cpp b/apps/RenderCadenceCompositor/render/runtime/RuntimeShaderPrepareWorker.cpp index 27d3f38..7510d68 100644 --- a/apps/RenderCadenceCompositor/render/runtime/RuntimeShaderPrepareWorker.cpp +++ b/apps/RenderCadenceCompositor/render/runtime/RuntimeShaderPrepareWorker.cpp @@ -77,20 +77,35 @@ void RuntimeShaderPrepareWorker::Submit(const std::vector lock(mMutex); for (const RenderCadenceCompositor::RuntimeRenderLayerModel& layer : layers) { - if (layer.artifact.fragmentShaderSource.empty()) + if (layer.artifact.passes.empty() && layer.artifact.fragmentShaderSource.empty()) continue; - PrepareRequest request; - request.layerId = layer.id; - request.shaderId = layer.shaderId; - request.sourceFingerprint = Fingerprint(layer.artifact); - request.artifact = layer.artifact; + std::vector passes = layer.artifact.passes; + if (passes.empty()) + { + RuntimeShaderPassArtifact pass; + pass.passId = "main"; + pass.fragmentShaderSource = layer.artifact.fragmentShaderSource; + pass.outputName = "layerOutput"; + passes.push_back(std::move(pass)); + } - auto sameLayer = [&request](const PrepareRequest& existing) { - return existing.layerId == request.layerId; + auto sameLayer = [&layer](const PrepareRequest& existing) { + return existing.layerId == layer.id; }; mRequests.erase(std::remove_if(mRequests.begin(), mRequests.end(), sameLayer), mRequests.end()); - mRequests.push_back(std::move(request)); + + for (const RuntimeShaderPassArtifact& pass : passes) + { + PrepareRequest request; + request.layerId = layer.id; + request.shaderId = layer.shaderId; + request.passId = pass.passId; + request.sourceFingerprint = Fingerprint(layer.artifact); + request.artifact = layer.artifact; + request.passArtifact = pass; + mRequests.push_back(std::move(request)); + } } mCondition.notify_one(); } @@ -137,10 +152,11 @@ void RuntimeShaderPrepareWorker::ThreadMain() } RuntimePreparedShaderProgram preparedProgram; - RuntimeShaderRenderer::BuildPreparedProgram( + RuntimeShaderRenderer::BuildPreparedPassProgram( request.layerId, request.sourceFingerprint, request.artifact, + request.passArtifact, preparedProgram); glFlush(); @@ -154,5 +170,10 @@ void RuntimeShaderPrepareWorker::ThreadMain() std::string RuntimeShaderPrepareWorker::Fingerprint(const RuntimeShaderArtifact& artifact) { const std::hash hasher; - return artifact.shaderId + ":" + std::to_string(artifact.fragmentShaderSource.size()) + ":" + std::to_string(hasher(artifact.fragmentShaderSource)); + std::string source; + for (const RuntimeShaderPassArtifact& pass : artifact.passes) + source += pass.passId + ":" + pass.outputName + ":" + pass.fragmentShaderSource + "\n"; + if (source.empty()) + source = artifact.fragmentShaderSource; + return artifact.shaderId + ":" + std::to_string(source.size()) + ":" + std::to_string(hasher(source)); } diff --git a/apps/RenderCadenceCompositor/render/runtime/RuntimeShaderPrepareWorker.h b/apps/RenderCadenceCompositor/render/runtime/RuntimeShaderPrepareWorker.h index 5e83001..9454b3c 100644 --- a/apps/RenderCadenceCompositor/render/runtime/RuntimeShaderPrepareWorker.h +++ b/apps/RenderCadenceCompositor/render/runtime/RuntimeShaderPrepareWorker.h @@ -35,8 +35,10 @@ private: { std::string layerId; std::string shaderId; + std::string passId; std::string sourceFingerprint; RuntimeShaderArtifact artifact; + RuntimeShaderPassArtifact passArtifact; }; void ThreadMain(); diff --git a/apps/RenderCadenceCompositor/render/runtime/RuntimeShaderProgram.h b/apps/RenderCadenceCompositor/render/runtime/RuntimeShaderProgram.h index efb4cf0..2719e70 100644 --- a/apps/RenderCadenceCompositor/render/runtime/RuntimeShaderProgram.h +++ b/apps/RenderCadenceCompositor/render/runtime/RuntimeShaderProgram.h @@ -4,13 +4,18 @@ #include "../../runtime/RuntimeShaderArtifact.h" #include +#include struct RuntimePreparedShaderProgram { std::string layerId; std::string shaderId; + std::string passId; std::string sourceFingerprint; RuntimeShaderArtifact artifact; + RuntimeShaderPassArtifact passArtifact; + std::vector inputNames; + std::string outputName; GLuint program = 0; GLuint vertexShader = 0; GLuint fragmentShader = 0; diff --git a/apps/RenderCadenceCompositor/render/runtime/RuntimeShaderRenderer.cpp b/apps/RenderCadenceCompositor/render/runtime/RuntimeShaderRenderer.cpp index 3cc6889..f586847 100644 --- a/apps/RenderCadenceCompositor/render/runtime/RuntimeShaderRenderer.cpp +++ b/apps/RenderCadenceCompositor/render/runtime/RuntimeShaderRenderer.cpp @@ -99,21 +99,41 @@ bool RuntimeShaderRenderer::BuildPreparedProgram( const std::string& sourceFingerprint, const RuntimeShaderArtifact& artifact, RuntimePreparedShaderProgram& preparedProgram) +{ + RuntimeShaderPassArtifact passArtifact; + passArtifact.passId = "main"; + passArtifact.fragmentShaderSource = artifact.fragmentShaderSource; + passArtifact.outputName = "layerOutput"; + if (!artifact.passes.empty()) + passArtifact = artifact.passes.front(); + return BuildPreparedPassProgram(layerId, sourceFingerprint, artifact, passArtifact, preparedProgram); +} + +bool RuntimeShaderRenderer::BuildPreparedPassProgram( + const std::string& layerId, + const std::string& sourceFingerprint, + const RuntimeShaderArtifact& artifact, + const RuntimeShaderPassArtifact& passArtifact, + RuntimePreparedShaderProgram& preparedProgram) { preparedProgram = RuntimePreparedShaderProgram(); preparedProgram.layerId = layerId; preparedProgram.shaderId = artifact.shaderId; + preparedProgram.passId = passArtifact.passId; preparedProgram.sourceFingerprint = sourceFingerprint; preparedProgram.artifact = artifact; + preparedProgram.passArtifact = passArtifact; + preparedProgram.inputNames = passArtifact.inputNames; + preparedProgram.outputName = passArtifact.outputName.empty() ? passArtifact.passId : passArtifact.outputName; - if (artifact.fragmentShaderSource.empty()) + if (passArtifact.fragmentShaderSource.empty()) { preparedProgram.error = "Cannot prepare an empty fragment shader."; return false; } if (!BuildProgram( - artifact.fragmentShaderSource, + passArtifact.fragmentShaderSource, preparedProgram.program, preparedProgram.vertexShader, preparedProgram.fragmentShader, diff --git a/apps/RenderCadenceCompositor/render/runtime/RuntimeShaderRenderer.h b/apps/RenderCadenceCompositor/render/runtime/RuntimeShaderRenderer.h index 730a998..e25547c 100644 --- a/apps/RenderCadenceCompositor/render/runtime/RuntimeShaderRenderer.h +++ b/apps/RenderCadenceCompositor/render/runtime/RuntimeShaderRenderer.h @@ -28,6 +28,12 @@ public: const std::string& sourceFingerprint, const RuntimeShaderArtifact& artifact, RuntimePreparedShaderProgram& preparedProgram); + static bool BuildPreparedPassProgram( + const std::string& layerId, + const std::string& sourceFingerprint, + const RuntimeShaderArtifact& artifact, + const RuntimeShaderPassArtifact& passArtifact, + RuntimePreparedShaderProgram& preparedProgram); private: bool EnsureStaticGlResources(std::string& error); diff --git a/apps/RenderCadenceCompositor/runtime/RuntimeShaderArtifact.h b/apps/RenderCadenceCompositor/runtime/RuntimeShaderArtifact.h index 3025603..160fb75 100644 --- a/apps/RenderCadenceCompositor/runtime/RuntimeShaderArtifact.h +++ b/apps/RenderCadenceCompositor/runtime/RuntimeShaderArtifact.h @@ -5,12 +5,21 @@ #include #include +struct RuntimeShaderPassArtifact +{ + std::string passId; + std::string fragmentShaderSource; + std::vector inputNames; + std::string outputName; +}; + struct RuntimeShaderArtifact { std::string layerId; std::string shaderId; std::string displayName; std::string fragmentShaderSource; + std::vector passes; std::string message; std::vector parameterDefinitions; }; diff --git a/apps/RenderCadenceCompositor/runtime/RuntimeSlangShaderCompiler.cpp b/apps/RenderCadenceCompositor/runtime/RuntimeSlangShaderCompiler.cpp index f05beb2..9e61fc4 100644 --- a/apps/RenderCadenceCompositor/runtime/RuntimeSlangShaderCompiler.cpp +++ b/apps/RenderCadenceCompositor/runtime/RuntimeSlangShaderCompiler.cpp @@ -112,8 +112,6 @@ RuntimeSlangShaderBuild RuntimeSlangShaderCompiler::BuildShader(const std::strin return build; } - const ShaderPassDefinition& pass = shaderPackage.passes.front(); - ShaderCompiler compiler( repoRoot, runtimeBuildDir / (shaderId + ".wrapper.slang"), @@ -122,11 +120,22 @@ RuntimeSlangShaderBuild RuntimeSlangShaderCompiler::BuildShader(const std::strin 0); const auto start = std::chrono::steady_clock::now(); - if (!compiler.BuildPassFragmentShaderSource(shaderPackage, pass, build.artifact.fragmentShaderSource, error)) + for (const ShaderPassDefinition& pass : shaderPackage.passes) { - build.succeeded = false; - build.message = error.empty() ? "Slang compile failed." : error; - return build; + std::string fragmentShaderSource; + if (!compiler.BuildPassFragmentShaderSource(shaderPackage, pass, fragmentShaderSource, error)) + { + build.succeeded = false; + build.message = error.empty() ? "Slang compile failed." : error; + return build; + } + + RuntimeShaderPassArtifact passArtifact; + passArtifact.passId = pass.id; + passArtifact.fragmentShaderSource = std::move(fragmentShaderSource); + passArtifact.inputNames = pass.inputNames; + passArtifact.outputName = pass.outputName; + build.artifact.passes.push_back(std::move(passArtifact)); } const auto end = std::chrono::steady_clock::now(); @@ -135,6 +144,8 @@ RuntimeSlangShaderBuild RuntimeSlangShaderCompiler::BuildShader(const std::strin build.artifact.shaderId = shaderPackage.id; build.artifact.displayName = shaderPackage.displayName; build.artifact.parameterDefinitions = shaderPackage.parameters; + if (!build.artifact.passes.empty()) + build.artifact.fragmentShaderSource = build.artifact.passes.front().fragmentShaderSource; build.artifact.message = shaderPackage.displayName + " Slang compile completed in " + std::to_string(milliseconds) + " ms."; build.message = build.artifact.message; return build; diff --git a/apps/RenderCadenceCompositor/runtime/SupportedShaderCatalog.cpp b/apps/RenderCadenceCompositor/runtime/SupportedShaderCatalog.cpp index 634801c..6d5e5ed 100644 --- a/apps/RenderCadenceCompositor/runtime/SupportedShaderCatalog.cpp +++ b/apps/RenderCadenceCompositor/runtime/SupportedShaderCatalog.cpp @@ -9,8 +9,8 @@ namespace RenderCadenceCompositor { ShaderSupportResult CheckStatelessSinglePassShaderSupport(const ShaderPackage& shaderPackage) { - if (shaderPackage.passes.size() != 1) - return { false, "RenderCadenceCompositor currently supports only single-pass runtime shaders." }; + if (shaderPackage.passes.empty()) + return { false, "Shader package has no render passes." }; if (shaderPackage.temporal.enabled) return { false, "RenderCadenceCompositor currently supports only stateless shaders; temporal history is not enabled in this app." }; @@ -30,6 +30,35 @@ ShaderSupportResult CheckStatelessSinglePassShaderSupport(const ShaderPackage& s return { false, "RenderCadenceCompositor currently skips text parameters because they require per-shader text texture storage." }; } + bool writesLayerOutput = false; + for (const ShaderPassDefinition& pass : shaderPackage.passes) + { + if (pass.sourcePath.empty()) + { + return { false, "Shader pass '" + pass.id + "' has no source." }; + } + if (pass.outputName == "layerOutput") + writesLayerOutput = true; + for (const std::string& inputName : pass.inputNames) + { + if (inputName == "videoInput" || inputName == "layerInput") + continue; + bool matchesNamedOutput = false; + for (const ShaderPassDefinition& outputPass : shaderPackage.passes) + { + if (outputPass.outputName == inputName) + { + matchesNamedOutput = true; + break; + } + } + if (!matchesNamedOutput) + return { false, "Shader pass '" + pass.id + "' references unknown input '" + inputName + "'." }; + } + } + if (!writesLayerOutput) + return { false, "Shader package must write a pass output named 'layerOutput'." }; + return { true, std::string() }; } diff --git a/tests/RenderCadenceCompositorSupportedShaderCatalogTests.cpp b/tests/RenderCadenceCompositorSupportedShaderCatalogTests.cpp index dae0e4a..4c0f025 100644 --- a/tests/RenderCadenceCompositorSupportedShaderCatalogTests.cpp +++ b/tests/RenderCadenceCompositorSupportedShaderCatalogTests.cpp @@ -23,6 +23,8 @@ ShaderPackage MakeSinglePassPackage() ShaderPassDefinition pass; pass.id = "main"; pass.entryPoint = "mainImage"; + pass.sourcePath = "shader.slang"; + pass.outputName = "layerOutput"; shaderPackage.passes.push_back(pass); return shaderPackage; } @@ -37,19 +39,35 @@ void SupportsSinglePassStatelessPackage() Expect(result.reason.empty(), "supported packages should not report a rejection reason"); } -void RejectsMultipassPackage() +void SupportsStatelessNamedPassPackage() { ShaderPackage shaderPackage = MakeSinglePassPackage(); + shaderPackage.passes.front().outputName = "generatedMask"; ShaderPassDefinition secondPass; secondPass.id = "second"; secondPass.entryPoint = "mainImage"; + secondPass.sourcePath = "shader.slang"; + secondPass.inputNames.push_back("generatedMask"); + secondPass.outputName = "layerOutput"; shaderPackage.passes.push_back(secondPass); const RenderCadenceCompositor::ShaderSupportResult result = RenderCadenceCompositor::CheckStatelessSinglePassShaderSupport(shaderPackage); - Expect(!result.supported, "multipass packages should be rejected"); - Expect(result.reason.find("single-pass") != std::string::npos, "multipass rejection should explain the single-pass limit"); + Expect(result.supported, "stateless named-pass packages should be supported"); + Expect(result.reason.empty(), "supported named-pass packages should not report a rejection reason"); +} + +void RejectsUnknownPassInput() +{ + ShaderPackage shaderPackage = MakeSinglePassPackage(); + shaderPackage.passes.front().inputNames.push_back("missingIntermediate"); + + const RenderCadenceCompositor::ShaderSupportResult result = + RenderCadenceCompositor::CheckStatelessSinglePassShaderSupport(shaderPackage); + + Expect(!result.supported, "packages with unknown pass inputs should be rejected"); + Expect(result.reason.find("unknown input") != std::string::npos, "unknown input rejection should explain the missing named output"); } void RejectsTemporalPackage() @@ -97,7 +115,8 @@ void RejectsTextParameters() int main() { SupportsSinglePassStatelessPackage(); - RejectsMultipassPackage(); + SupportsStatelessNamedPassPackage(); + RejectsUnknownPassInput(); RejectsTemporalPackage(); RejectsTextureAssets(); RejectsTextParameters();