Multipass shaders

This commit is contained in:
Aiden
2026-05-12 17:08:35 +10:00
parent 1429b2e660
commit dfd49fd0e3
12 changed files with 392 additions and 72 deletions

View File

@@ -47,7 +47,9 @@ Included now:
- shared-context GL prepare worker for runtime shader program compile/link - shared-context GL prepare worker for runtime shader program compile/link
- render-thread-only GL program swap once a prepared program is ready - render-thread-only GL program swap once a prepared program is ready
- manifest-driven stateless single-pass shader packages - 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 - default float, vec2, color, boolean, enum, and trigger parameters
- small JSON writer for future HTTP/WebSocket payloads - small JSON writer for future HTTP/WebSocket payloads
- JSON serialization for cadence telemetry snapshots - JSON serialization for cadence telemetry snapshots
@@ -60,7 +62,6 @@ Included now:
Intentionally not included yet: Intentionally not included yet:
- DeckLink input - DeckLink input
- multipass shader rendering
- temporal/history/feedback shader storage - temporal/history/feedback shader storage
- texture/LUT asset upload - texture/LUT asset upload
- text-parameter rasterization - text-parameter rasterization
@@ -86,6 +87,8 @@ This tracks parity with `apps/LoopThroughWithOpenGLCompositing`.
- [x] Shared-context GL shader/program preparation - [x] Shared-context GL shader/program preparation
- [x] Render-thread program swap at a frame boundary - [x] Render-thread program swap at a frame boundary
- [x] Stateless single-pass shader rendering - [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] Shader add/remove control path
- [x] Previous-layer texture handoff for stacked shaders - [x] Previous-layer texture handoff for stacked shaders
- [x] Supported shader list in HTTP/UI state - [x] Supported shader list in HTTP/UI state
@@ -99,7 +102,6 @@ This tracks parity with `apps/LoopThroughWithOpenGLCompositing`.
- [ ] DeckLink input capture - [ ] DeckLink input capture
- [ ] Input frame upload into the render scene - [ ] Input frame upload into the render scene
- [ ] Live video input bound to `gVideoInput` - [ ] Live video input bound to `gVideoInput`
- [ ] Multipass shader rendering
- [ ] Temporal history buffers - [ ] Temporal history buffers
- [ ] Feedback buffers - [ ] Feedback buffers
- [ ] Texture asset loading and upload - [ ] 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. 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 temporal history
- no feedback storage - no feedback storage
- no texture/LUT assets yet - 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 - 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` - 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. 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: Successful handoff signs:

View File

@@ -39,14 +39,18 @@ bool RuntimeRenderScene::CommitRenderLayers(const std::vector<RenderCadenceCompo
continue; continue;
} }
if (layerIt->renderer) for (LayerProgram::PassProgram& pass : layerIt->passes)
layerIt->renderer->ShutdownGl(); {
if (pass.renderer)
pass.renderer->ShutdownGl();
}
ReleasePendingPrograms(*layerIt);
layerIt = mLayers.erase(layerIt); layerIt = mLayers.erase(layerIt);
} }
for (const RenderCadenceCompositor::RuntimeRenderLayerModel& layer : layers) for (const RenderCadenceCompositor::RuntimeRenderLayerModel& layer : layers)
{ {
if (layer.artifact.fragmentShaderSource.empty()) if (layer.artifact.passes.empty() && layer.artifact.fragmentShaderSource.empty())
continue; continue;
const std::string fingerprint = Fingerprint(layer.artifact); const std::string fingerprint = Fingerprint(layer.artifact);
@@ -55,16 +59,25 @@ bool RuntimeRenderScene::CommitRenderLayers(const std::vector<RenderCadenceCompo
{ {
LayerProgram next; LayerProgram next;
next.layerId = layer.id; next.layerId = layer.id;
next.renderer = std::make_unique<RuntimeShaderRenderer>();
mLayers.push_back(std::move(next)); mLayers.push_back(std::move(next));
program = &mLayers.back(); 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; continue;
if (program->pendingFingerprint == fingerprint) if (program->pendingFingerprint == fingerprint)
continue; continue;
ReleasePendingPrograms(*program);
program->shaderId = layer.shaderId; program->shaderId = layer.shaderId;
program->pendingFingerprint = fingerprint; program->pendingFingerprint = fingerprint;
layersToPrepare.push_back(layer); layersToPrepare.push_back(layer);
@@ -84,8 +97,13 @@ bool RuntimeRenderScene::HasLayers()
for (const std::string& layerId : mLayerOrder) for (const std::string& layerId : mLayerOrder)
{ {
const LayerProgram* layer = FindLayer(layerId); const LayerProgram* layer = FindLayer(layerId);
if (layer && layer->renderer && layer->renderer->HasProgram()) if (!layer)
return true; continue;
for (const LayerProgram::PassProgram& pass : layer->passes)
{
if (pass.renderer && pass.renderer->HasProgram())
return true;
}
} }
return false; return false;
} }
@@ -98,9 +116,16 @@ void RuntimeRenderScene::RenderFrame(uint64_t frameIndex, unsigned width, unsign
for (const std::string& layerId : mLayerOrder) for (const std::string& layerId : mLayerOrder)
{ {
LayerProgram* layer = FindLayer(layerId); LayerProgram* layer = FindLayer(layerId);
if (!layer || !layer->renderer || !layer->renderer->HasProgram()) if (!layer)
continue; 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()) if (readyLayers.empty())
@@ -111,14 +136,14 @@ void RuntimeRenderScene::RenderFrame(uint64_t frameIndex, unsigned width, unsign
if (readyLayers.size() == 1) if (readyLayers.size() == 1)
{ {
readyLayers.front()->renderer->RenderFrame(frameIndex, width, height); RenderLayer(*readyLayers.front(), frameIndex, width, height, 0, static_cast<GLuint>(outputFramebuffer), true);
return; return;
} }
if (!EnsureLayerTargets(width, height)) if (!EnsureLayerTargets(width, height))
{ {
glBindFramebuffer(GL_FRAMEBUFFER, static_cast<GLuint>(outputFramebuffer)); glBindFramebuffer(GL_FRAMEBUFFER, static_cast<GLuint>(outputFramebuffer));
readyLayers.back()->renderer->RenderFrame(frameIndex, width, height); RenderLayer(*readyLayers.back(), frameIndex, width, height, 0, static_cast<GLuint>(outputFramebuffer), true);
return; return;
} }
@@ -130,12 +155,11 @@ void RuntimeRenderScene::RenderFrame(uint64_t frameIndex, unsigned width, unsign
if (isFinalLayer) if (isFinalLayer)
{ {
glBindFramebuffer(GL_FRAMEBUFFER, static_cast<GLuint>(outputFramebuffer)); glBindFramebuffer(GL_FRAMEBUFFER, static_cast<GLuint>(outputFramebuffer));
readyLayers[layerIndex]->renderer->RenderFrame(frameIndex, width, height, layerInputTexture, layerInputTexture); RenderLayer(*readyLayers[layerIndex], frameIndex, width, height, layerInputTexture, static_cast<GLuint>(outputFramebuffer), true);
continue; continue;
} }
glBindFramebuffer(GL_FRAMEBUFFER, mLayerFramebuffers[nextTargetIndex]); RenderLayer(*readyLayers[layerIndex], frameIndex, width, height, layerInputTexture, mLayerFramebuffers[nextTargetIndex], true);
readyLayers[layerIndex]->renderer->RenderFrame(frameIndex, width, height, layerInputTexture, layerInputTexture);
layerInputTexture = mLayerTextures[nextTargetIndex]; layerInputTexture = mLayerTextures[nextTargetIndex];
nextTargetIndex = 1 - nextTargetIndex; nextTargetIndex = 1 - nextTargetIndex;
} }
@@ -146,8 +170,12 @@ void RuntimeRenderScene::ShutdownGl()
mPrepareWorker.Stop(); mPrepareWorker.Stop();
for (LayerProgram& layer : mLayers) for (LayerProgram& layer : mLayers)
{ {
if (layer.renderer) for (LayerProgram::PassProgram& pass : layer.passes)
layer.renderer->ShutdownGl(); {
if (pass.renderer)
pass.renderer->ShutdownGl();
}
ReleasePendingPrograms(layer);
} }
mLayers.clear(); mLayers.clear();
mLayerOrder.clear(); mLayerOrder.clear();
@@ -172,28 +200,165 @@ void RuntimeRenderScene::ConsumePreparedPrograms()
continue; 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<LayerProgram::PassProgram> 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<RuntimeShaderRenderer> nextRenderer = std::make_unique<RuntimeShaderRenderer>(); std::unique_ptr<RuntimeShaderRenderer> nextRenderer = std::make_unique<RuntimeShaderRenderer>();
std::string error; 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<RuntimeShaderRenderer> nextRenderer = std::make_unique<RuntimeShaderRenderer>();
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; continue;
} }
if (layer->renderer) if (!EnsureLayerTargets(width, height))
layer->renderer->ShutdownGl(); continue;
layer->renderer = std::move(nextRenderer);
layer->shaderId = preparedProgram.shaderId; const std::size_t targetIndex = nextTargetIndex;
layer->sourceFingerprint = preparedProgram.sourceFingerprint; nextTargetIndex = nextTargetIndex == 2 ? 3 : 2;
layer->pendingFingerprint.clear(); 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) bool RuntimeRenderScene::EnsureLayerTargets(unsigned width, unsigned height)
{ {
if (width == 0 || height == 0) if (width == 0 || height == 0)
return false; 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) && mLayerTargetWidth == width && mLayerTargetHeight == height)
return true; return true;
@@ -201,9 +366,9 @@ bool RuntimeRenderScene::EnsureLayerTargets(unsigned width, unsigned height)
mLayerTargetWidth = width; mLayerTargetWidth = width;
mLayerTargetHeight = height; mLayerTargetHeight = height;
glGenFramebuffers(2, mLayerFramebuffers); glGenFramebuffers(4, mLayerFramebuffers);
glGenTextures(2, mLayerTextures); glGenTextures(4, mLayerTextures);
for (int index = 0; index < 2; ++index) for (int index = 0; index < 4; ++index)
{ {
glBindTexture(GL_TEXTURE_2D, mLayerTextures[index]); glBindTexture(GL_TEXTURE_2D, mLayerTextures[index]);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
@@ -239,14 +404,15 @@ bool RuntimeRenderScene::EnsureLayerTargets(unsigned width, unsigned height)
void RuntimeRenderScene::DestroyLayerTargets() void RuntimeRenderScene::DestroyLayerTargets()
{ {
if (mLayerFramebuffers[0] != 0 || mLayerFramebuffers[1] != 0) if (mLayerFramebuffers[0] != 0 || mLayerFramebuffers[1] != 0 || mLayerFramebuffers[2] != 0 || mLayerFramebuffers[3] != 0)
glDeleteFramebuffers(2, mLayerFramebuffers); glDeleteFramebuffers(4, mLayerFramebuffers);
if (mLayerTextures[0] != 0 || mLayerTextures[1] != 0) if (mLayerTextures[0] != 0 || mLayerTextures[1] != 0 || mLayerTextures[2] != 0 || mLayerTextures[3] != 0)
glDeleteTextures(2, mLayerTextures); glDeleteTextures(4, mLayerTextures);
mLayerFramebuffers[0] = 0; for (int index = 0; index < 4; ++index)
mLayerFramebuffers[1] = 0; {
mLayerTextures[0] = 0; mLayerFramebuffers[index] = 0;
mLayerTextures[1] = 0; mLayerTextures[index] = 0;
}
mLayerTargetWidth = 0; mLayerTargetWidth = 0;
mLayerTargetHeight = 0; mLayerTargetHeight = 0;
} }
@@ -271,8 +437,23 @@ const RuntimeRenderScene::LayerProgram* RuntimeRenderScene::FindLayer(const std:
return nullptr; 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) std::string RuntimeRenderScene::Fingerprint(const RuntimeShaderArtifact& artifact)
{ {
const std::hash<std::string> hasher; const std::hash<std::string> 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));
} }

View File

@@ -32,21 +32,33 @@ private:
std::string shaderId; std::string shaderId;
std::string sourceFingerprint; std::string sourceFingerprint;
std::string pendingFingerprint; std::string pendingFingerprint;
std::unique_ptr<RuntimeShaderRenderer> renderer; std::vector<RuntimePreparedShaderProgram> pendingPreparedPrograms;
struct PassProgram
{
std::string passId;
std::vector<std::string> inputNames;
std::string outputName;
std::unique_ptr<RuntimeShaderRenderer> renderer;
};
std::vector<PassProgram> passes;
}; };
void ConsumePreparedPrograms(); void ConsumePreparedPrograms();
void ReleasePendingPrograms(LayerProgram& layer);
void TryCommitPendingPrograms(LayerProgram& layer);
bool EnsureLayerTargets(unsigned width, unsigned height); bool EnsureLayerTargets(unsigned width, unsigned height);
void DestroyLayerTargets(); 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); LayerProgram* FindLayer(const std::string& layerId);
const LayerProgram* FindLayer(const std::string& layerId) const; const LayerProgram* FindLayer(const std::string& layerId) const;
LayerProgram::PassProgram* FindPass(LayerProgram& layer, const std::string& passId);
static std::string Fingerprint(const RuntimeShaderArtifact& artifact); static std::string Fingerprint(const RuntimeShaderArtifact& artifact);
RuntimeShaderPrepareWorker mPrepareWorker; RuntimeShaderPrepareWorker mPrepareWorker;
std::vector<LayerProgram> mLayers; std::vector<LayerProgram> mLayers;
std::vector<std::string> mLayerOrder; std::vector<std::string> mLayerOrder;
GLuint mLayerFramebuffers[2] = {}; GLuint mLayerFramebuffers[4] = {};
GLuint mLayerTextures[2] = {}; GLuint mLayerTextures[4] = {};
unsigned mLayerTargetWidth = 0; unsigned mLayerTargetWidth = 0;
unsigned mLayerTargetHeight = 0; unsigned mLayerTargetHeight = 0;
}; };

View File

@@ -77,20 +77,35 @@ void RuntimeShaderPrepareWorker::Submit(const std::vector<RenderCadenceComposito
std::lock_guard<std::mutex> lock(mMutex); std::lock_guard<std::mutex> lock(mMutex);
for (const RenderCadenceCompositor::RuntimeRenderLayerModel& layer : layers) for (const RenderCadenceCompositor::RuntimeRenderLayerModel& layer : layers)
{ {
if (layer.artifact.fragmentShaderSource.empty()) if (layer.artifact.passes.empty() && layer.artifact.fragmentShaderSource.empty())
continue; continue;
PrepareRequest request; std::vector<RuntimeShaderPassArtifact> passes = layer.artifact.passes;
request.layerId = layer.id; if (passes.empty())
request.shaderId = layer.shaderId; {
request.sourceFingerprint = Fingerprint(layer.artifact); RuntimeShaderPassArtifact pass;
request.artifact = layer.artifact; pass.passId = "main";
pass.fragmentShaderSource = layer.artifact.fragmentShaderSource;
pass.outputName = "layerOutput";
passes.push_back(std::move(pass));
}
auto sameLayer = [&request](const PrepareRequest& existing) { auto sameLayer = [&layer](const PrepareRequest& existing) {
return existing.layerId == request.layerId; return existing.layerId == layer.id;
}; };
mRequests.erase(std::remove_if(mRequests.begin(), mRequests.end(), sameLayer), mRequests.end()); 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(); mCondition.notify_one();
} }
@@ -137,10 +152,11 @@ void RuntimeShaderPrepareWorker::ThreadMain()
} }
RuntimePreparedShaderProgram preparedProgram; RuntimePreparedShaderProgram preparedProgram;
RuntimeShaderRenderer::BuildPreparedProgram( RuntimeShaderRenderer::BuildPreparedPassProgram(
request.layerId, request.layerId,
request.sourceFingerprint, request.sourceFingerprint,
request.artifact, request.artifact,
request.passArtifact,
preparedProgram); preparedProgram);
glFlush(); glFlush();
@@ -154,5 +170,10 @@ void RuntimeShaderPrepareWorker::ThreadMain()
std::string RuntimeShaderPrepareWorker::Fingerprint(const RuntimeShaderArtifact& artifact) std::string RuntimeShaderPrepareWorker::Fingerprint(const RuntimeShaderArtifact& artifact)
{ {
const std::hash<std::string> hasher; const std::hash<std::string> 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));
} }

View File

@@ -35,8 +35,10 @@ private:
{ {
std::string layerId; std::string layerId;
std::string shaderId; std::string shaderId;
std::string passId;
std::string sourceFingerprint; std::string sourceFingerprint;
RuntimeShaderArtifact artifact; RuntimeShaderArtifact artifact;
RuntimeShaderPassArtifact passArtifact;
}; };
void ThreadMain(); void ThreadMain();

View File

@@ -4,13 +4,18 @@
#include "../../runtime/RuntimeShaderArtifact.h" #include "../../runtime/RuntimeShaderArtifact.h"
#include <string> #include <string>
#include <vector>
struct RuntimePreparedShaderProgram struct RuntimePreparedShaderProgram
{ {
std::string layerId; std::string layerId;
std::string shaderId; std::string shaderId;
std::string passId;
std::string sourceFingerprint; std::string sourceFingerprint;
RuntimeShaderArtifact artifact; RuntimeShaderArtifact artifact;
RuntimeShaderPassArtifact passArtifact;
std::vector<std::string> inputNames;
std::string outputName;
GLuint program = 0; GLuint program = 0;
GLuint vertexShader = 0; GLuint vertexShader = 0;
GLuint fragmentShader = 0; GLuint fragmentShader = 0;

View File

@@ -99,21 +99,41 @@ bool RuntimeShaderRenderer::BuildPreparedProgram(
const std::string& sourceFingerprint, const std::string& sourceFingerprint,
const RuntimeShaderArtifact& artifact, const RuntimeShaderArtifact& artifact,
RuntimePreparedShaderProgram& preparedProgram) 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 = RuntimePreparedShaderProgram();
preparedProgram.layerId = layerId; preparedProgram.layerId = layerId;
preparedProgram.shaderId = artifact.shaderId; preparedProgram.shaderId = artifact.shaderId;
preparedProgram.passId = passArtifact.passId;
preparedProgram.sourceFingerprint = sourceFingerprint; preparedProgram.sourceFingerprint = sourceFingerprint;
preparedProgram.artifact = artifact; 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."; preparedProgram.error = "Cannot prepare an empty fragment shader.";
return false; return false;
} }
if (!BuildProgram( if (!BuildProgram(
artifact.fragmentShaderSource, passArtifact.fragmentShaderSource,
preparedProgram.program, preparedProgram.program,
preparedProgram.vertexShader, preparedProgram.vertexShader,
preparedProgram.fragmentShader, preparedProgram.fragmentShader,

View File

@@ -28,6 +28,12 @@ public:
const std::string& sourceFingerprint, const std::string& sourceFingerprint,
const RuntimeShaderArtifact& artifact, const RuntimeShaderArtifact& artifact,
RuntimePreparedShaderProgram& preparedProgram); RuntimePreparedShaderProgram& preparedProgram);
static bool BuildPreparedPassProgram(
const std::string& layerId,
const std::string& sourceFingerprint,
const RuntimeShaderArtifact& artifact,
const RuntimeShaderPassArtifact& passArtifact,
RuntimePreparedShaderProgram& preparedProgram);
private: private:
bool EnsureStaticGlResources(std::string& error); bool EnsureStaticGlResources(std::string& error);

View File

@@ -5,12 +5,21 @@
#include <string> #include <string>
#include <vector> #include <vector>
struct RuntimeShaderPassArtifact
{
std::string passId;
std::string fragmentShaderSource;
std::vector<std::string> inputNames;
std::string outputName;
};
struct RuntimeShaderArtifact struct RuntimeShaderArtifact
{ {
std::string layerId; std::string layerId;
std::string shaderId; std::string shaderId;
std::string displayName; std::string displayName;
std::string fragmentShaderSource; std::string fragmentShaderSource;
std::vector<RuntimeShaderPassArtifact> passes;
std::string message; std::string message;
std::vector<ShaderParameterDefinition> parameterDefinitions; std::vector<ShaderParameterDefinition> parameterDefinitions;
}; };

View File

@@ -112,8 +112,6 @@ RuntimeSlangShaderBuild RuntimeSlangShaderCompiler::BuildShader(const std::strin
return build; return build;
} }
const ShaderPassDefinition& pass = shaderPackage.passes.front();
ShaderCompiler compiler( ShaderCompiler compiler(
repoRoot, repoRoot,
runtimeBuildDir / (shaderId + ".wrapper.slang"), runtimeBuildDir / (shaderId + ".wrapper.slang"),
@@ -122,11 +120,22 @@ RuntimeSlangShaderBuild RuntimeSlangShaderCompiler::BuildShader(const std::strin
0); 0);
const auto start = std::chrono::steady_clock::now(); 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; std::string fragmentShaderSource;
build.message = error.empty() ? "Slang compile failed." : error; if (!compiler.BuildPassFragmentShaderSource(shaderPackage, pass, fragmentShaderSource, error))
return build; {
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(); 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.shaderId = shaderPackage.id;
build.artifact.displayName = shaderPackage.displayName; build.artifact.displayName = shaderPackage.displayName;
build.artifact.parameterDefinitions = shaderPackage.parameters; 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.artifact.message = shaderPackage.displayName + " Slang compile completed in " + std::to_string(milliseconds) + " ms.";
build.message = build.artifact.message; build.message = build.artifact.message;
return build; return build;

View File

@@ -9,8 +9,8 @@ namespace RenderCadenceCompositor
{ {
ShaderSupportResult CheckStatelessSinglePassShaderSupport(const ShaderPackage& shaderPackage) ShaderSupportResult CheckStatelessSinglePassShaderSupport(const ShaderPackage& shaderPackage)
{ {
if (shaderPackage.passes.size() != 1) if (shaderPackage.passes.empty())
return { false, "RenderCadenceCompositor currently supports only single-pass runtime shaders." }; return { false, "Shader package has no render passes." };
if (shaderPackage.temporal.enabled) if (shaderPackage.temporal.enabled)
return { false, "RenderCadenceCompositor currently supports only stateless shaders; temporal history is not enabled in this app." }; 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." }; 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() }; return { true, std::string() };
} }

View File

@@ -23,6 +23,8 @@ ShaderPackage MakeSinglePassPackage()
ShaderPassDefinition pass; ShaderPassDefinition pass;
pass.id = "main"; pass.id = "main";
pass.entryPoint = "mainImage"; pass.entryPoint = "mainImage";
pass.sourcePath = "shader.slang";
pass.outputName = "layerOutput";
shaderPackage.passes.push_back(pass); shaderPackage.passes.push_back(pass);
return shaderPackage; return shaderPackage;
} }
@@ -37,19 +39,35 @@ void SupportsSinglePassStatelessPackage()
Expect(result.reason.empty(), "supported packages should not report a rejection reason"); Expect(result.reason.empty(), "supported packages should not report a rejection reason");
} }
void RejectsMultipassPackage() void SupportsStatelessNamedPassPackage()
{ {
ShaderPackage shaderPackage = MakeSinglePassPackage(); ShaderPackage shaderPackage = MakeSinglePassPackage();
shaderPackage.passes.front().outputName = "generatedMask";
ShaderPassDefinition secondPass; ShaderPassDefinition secondPass;
secondPass.id = "second"; secondPass.id = "second";
secondPass.entryPoint = "mainImage"; secondPass.entryPoint = "mainImage";
secondPass.sourcePath = "shader.slang";
secondPass.inputNames.push_back("generatedMask");
secondPass.outputName = "layerOutput";
shaderPackage.passes.push_back(secondPass); shaderPackage.passes.push_back(secondPass);
const RenderCadenceCompositor::ShaderSupportResult result = const RenderCadenceCompositor::ShaderSupportResult result =
RenderCadenceCompositor::CheckStatelessSinglePassShaderSupport(shaderPackage); RenderCadenceCompositor::CheckStatelessSinglePassShaderSupport(shaderPackage);
Expect(!result.supported, "multipass packages should be rejected"); Expect(result.supported, "stateless named-pass packages should be supported");
Expect(result.reason.find("single-pass") != std::string::npos, "multipass rejection should explain the single-pass limit"); 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() void RejectsTemporalPackage()
@@ -97,7 +115,8 @@ void RejectsTextParameters()
int main() int main()
{ {
SupportsSinglePassStatelessPackage(); SupportsSinglePassStatelessPackage();
RejectsMultipassPackage(); SupportsStatelessNamedPassPackage();
RejectsUnknownPassInput();
RejectsTemporalPackage(); RejectsTemporalPackage();
RejectsTextureAssets(); RejectsTextureAssets();
RejectsTextParameters(); RejectsTextParameters();