Multipass shaders
This commit is contained in:
@@ -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:
|
||||
|
||||
|
||||
@@ -39,14 +39,18 @@ bool RuntimeRenderScene::CommitRenderLayers(const std::vector<RenderCadenceCompo
|
||||
continue;
|
||||
}
|
||||
|
||||
if (layerIt->renderer)
|
||||
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<RenderCadenceCompo
|
||||
{
|
||||
LayerProgram next;
|
||||
next.layerId = layer.id;
|
||||
next.renderer = std::make_unique<RuntimeShaderRenderer>();
|
||||
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,9 +97,14 @@ bool RuntimeRenderScene::HasLayers()
|
||||
for (const std::string& layerId : mLayerOrder)
|
||||
{
|
||||
const LayerProgram* layer = FindLayer(layerId);
|
||||
if (layer && layer->renderer && layer->renderer->HasProgram())
|
||||
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;
|
||||
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<GLuint>(outputFramebuffer), true);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!EnsureLayerTargets(width, height))
|
||||
{
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -130,12 +155,11 @@ void RuntimeRenderScene::RenderFrame(uint64_t frameIndex, unsigned width, unsign
|
||||
if (isFinalLayer)
|
||||
{
|
||||
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;
|
||||
}
|
||||
|
||||
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<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::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;
|
||||
}
|
||||
|
||||
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<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));
|
||||
}
|
||||
|
||||
@@ -32,21 +32,33 @@ private:
|
||||
std::string shaderId;
|
||||
std::string sourceFingerprint;
|
||||
std::string pendingFingerprint;
|
||||
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 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<LayerProgram> mLayers;
|
||||
std::vector<std::string> mLayerOrder;
|
||||
GLuint mLayerFramebuffers[2] = {};
|
||||
GLuint mLayerTextures[2] = {};
|
||||
GLuint mLayerFramebuffers[4] = {};
|
||||
GLuint mLayerTextures[4] = {};
|
||||
unsigned mLayerTargetWidth = 0;
|
||||
unsigned mLayerTargetHeight = 0;
|
||||
};
|
||||
|
||||
@@ -77,21 +77,36 @@ void RuntimeShaderPrepareWorker::Submit(const std::vector<RenderCadenceComposito
|
||||
std::lock_guard<std::mutex> lock(mMutex);
|
||||
for (const RenderCadenceCompositor::RuntimeRenderLayerModel& layer : layers)
|
||||
{
|
||||
if (layer.artifact.fragmentShaderSource.empty())
|
||||
if (layer.artifact.passes.empty() && layer.artifact.fragmentShaderSource.empty())
|
||||
continue;
|
||||
|
||||
std::vector<RuntimeShaderPassArtifact> 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 = [&layer](const PrepareRequest& existing) {
|
||||
return existing.layerId == layer.id;
|
||||
};
|
||||
mRequests.erase(std::remove_if(mRequests.begin(), mRequests.end(), sameLayer), mRequests.end());
|
||||
|
||||
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;
|
||||
|
||||
auto sameLayer = [&request](const PrepareRequest& existing) {
|
||||
return existing.layerId == request.layerId;
|
||||
};
|
||||
mRequests.erase(std::remove_if(mRequests.begin(), mRequests.end(), sameLayer), mRequests.end());
|
||||
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<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));
|
||||
}
|
||||
|
||||
@@ -35,8 +35,10 @@ private:
|
||||
{
|
||||
std::string layerId;
|
||||
std::string shaderId;
|
||||
std::string passId;
|
||||
std::string sourceFingerprint;
|
||||
RuntimeShaderArtifact artifact;
|
||||
RuntimeShaderPassArtifact passArtifact;
|
||||
};
|
||||
|
||||
void ThreadMain();
|
||||
|
||||
@@ -4,13 +4,18 @@
|
||||
#include "../../runtime/RuntimeShaderArtifact.h"
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
struct RuntimePreparedShaderProgram
|
||||
{
|
||||
std::string layerId;
|
||||
std::string shaderId;
|
||||
std::string passId;
|
||||
std::string sourceFingerprint;
|
||||
RuntimeShaderArtifact artifact;
|
||||
RuntimeShaderPassArtifact passArtifact;
|
||||
std::vector<std::string> inputNames;
|
||||
std::string outputName;
|
||||
GLuint program = 0;
|
||||
GLuint vertexShader = 0;
|
||||
GLuint fragmentShader = 0;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -5,12 +5,21 @@
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
struct RuntimeShaderPassArtifact
|
||||
{
|
||||
std::string passId;
|
||||
std::string fragmentShaderSource;
|
||||
std::vector<std::string> inputNames;
|
||||
std::string outputName;
|
||||
};
|
||||
|
||||
struct RuntimeShaderArtifact
|
||||
{
|
||||
std::string layerId;
|
||||
std::string shaderId;
|
||||
std::string displayName;
|
||||
std::string fragmentShaderSource;
|
||||
std::vector<RuntimeShaderPassArtifact> passes;
|
||||
std::string message;
|
||||
std::vector<ShaderParameterDefinition> parameterDefinitions;
|
||||
};
|
||||
|
||||
@@ -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,19 +120,32 @@ 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)
|
||||
{
|
||||
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();
|
||||
const double milliseconds = std::chrono::duration_cast<std::chrono::duration<double, std::milli>>(end - start).count();
|
||||
build.succeeded = true;
|
||||
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;
|
||||
|
||||
@@ -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() };
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user