Update shader
All checks were successful
CI / React UI Build (push) Successful in 11s
CI / Native Windows Build And Tests (push) Successful in 3m0s
CI / Windows Release Package (push) Has been skipped

This commit is contained in:
Aiden
2026-05-12 16:52:15 +10:00
parent 02b221f481
commit 1429b2e660
6 changed files with 178 additions and 18 deletions

View File

@@ -72,6 +72,49 @@ Intentionally not included yet:
Those features should be ported only after the cadence spine is stable.
## V1 Feature Parity Checklist
This tracks parity with `apps/LoopThroughWithOpenGLCompositing`.
- [x] Stable DeckLink output cadence
- [x] BGRA8 system-memory output path
- [x] Render thread owns its primary GL context
- [x] Output startup warmup before scheduled playback
- [x] Non-blocking startup when DeckLink output is unavailable
- [x] Runtime shader package discovery
- [x] Background Slang shader compile
- [x] Shared-context GL shader/program preparation
- [x] Render-thread program swap at a frame boundary
- [x] Stateless single-pass shader rendering
- [x] Shader add/remove control path
- [x] Previous-layer texture handoff for stacked shaders
- [x] Supported shader list in HTTP/UI state
- [x] Local HTTP server
- [x] WebSocket state updates for the UI
- [x] OpenAPI document serving
- [x] Static control UI serving
- [x] Startup config loading from `config/runtime-host.json`
- [x] Cadence telemetry JSON
- [x] Health logging for schedule/drop/starvation events
- [ ] 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
- [ ] LUT asset loading and upload
- [ ] Text parameter rasterization
- [ ] Runtime parameter updates from controls
- [ ] Layer reorder/bypass/set-shader/update-parameter/reset-parameter controls
- [ ] Full runtime state store/read model
- [ ] Persistent layer stack/config writes
- [ ] OSC ingress
- [ ] Preview output
- [ ] Screenshot capture
- [ ] External keying support
- [ ] Full V1 health/runtime presentation model
## Build
```powershell
@@ -209,13 +252,14 @@ Current runtime shader support is deliberately limited to stateless single-pass
- no texture/LUT assets yet
- no text parameters yet
- manifest defaults are used for parameters
- `gVideoInput` and `gLayerInput` are bound to 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`
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.
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. Current layer rendering is still deliberately simple: each stateless full-frame shader draws to the output target using fallback source textures until proper layer-input texture handoff is designed.
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.
Successful handoff signs:

View File

@@ -6,6 +6,10 @@
#include <functional>
#include <utility>
#ifndef GL_FRAMEBUFFER_BINDING
#define GL_FRAMEBUFFER_BINDING 0x8CA6
#endif
RuntimeRenderScene::~RuntimeRenderScene()
{
ShutdownGl();
@@ -90,12 +94,50 @@ void RuntimeRenderScene::RenderFrame(uint64_t frameIndex, unsigned width, unsign
{
ConsumePreparedPrograms();
std::vector<LayerProgram*> readyLayers;
for (const std::string& layerId : mLayerOrder)
{
LayerProgram* layer = FindLayer(layerId);
if (!layer || !layer->renderer || !layer->renderer->HasProgram())
continue;
layer->renderer->RenderFrame(frameIndex, width, height);
readyLayers.push_back(layer);
}
if (readyLayers.empty())
return;
GLint outputFramebuffer = 0;
glGetIntegerv(GL_FRAMEBUFFER_BINDING, &outputFramebuffer);
if (readyLayers.size() == 1)
{
readyLayers.front()->renderer->RenderFrame(frameIndex, width, height);
return;
}
if (!EnsureLayerTargets(width, height))
{
glBindFramebuffer(GL_FRAMEBUFFER, static_cast<GLuint>(outputFramebuffer));
readyLayers.back()->renderer->RenderFrame(frameIndex, width, height);
return;
}
GLuint layerInputTexture = 0;
std::size_t nextTargetIndex = 0;
for (std::size_t layerIndex = 0; layerIndex < readyLayers.size(); ++layerIndex)
{
const bool isFinalLayer = layerIndex == readyLayers.size() - 1;
if (isFinalLayer)
{
glBindFramebuffer(GL_FRAMEBUFFER, static_cast<GLuint>(outputFramebuffer));
readyLayers[layerIndex]->renderer->RenderFrame(frameIndex, width, height, layerInputTexture, layerInputTexture);
continue;
}
glBindFramebuffer(GL_FRAMEBUFFER, mLayerFramebuffers[nextTargetIndex]);
readyLayers[layerIndex]->renderer->RenderFrame(frameIndex, width, height, layerInputTexture, layerInputTexture);
layerInputTexture = mLayerTextures[nextTargetIndex];
nextTargetIndex = 1 - nextTargetIndex;
}
}
@@ -109,6 +151,7 @@ void RuntimeRenderScene::ShutdownGl()
}
mLayers.clear();
mLayerOrder.clear();
DestroyLayerTargets();
}
void RuntimeRenderScene::ConsumePreparedPrograms()
@@ -146,6 +189,68 @@ void RuntimeRenderScene::ConsumePreparedPrograms()
}
}
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
&& mLayerTargetWidth == width && mLayerTargetHeight == height)
return true;
DestroyLayerTargets();
mLayerTargetWidth = width;
mLayerTargetHeight = height;
glGenFramebuffers(2, mLayerFramebuffers);
glGenTextures(2, mLayerTextures);
for (int index = 0; index < 2; ++index)
{
glBindTexture(GL_TEXTURE_2D, mLayerTextures[index]);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
glTexImage2D(
GL_TEXTURE_2D,
0,
GL_RGBA8,
static_cast<GLsizei>(width),
static_cast<GLsizei>(height),
0,
GL_BGRA,
GL_UNSIGNED_INT_8_8_8_8_REV,
nullptr);
glBindFramebuffer(GL_FRAMEBUFFER, mLayerFramebuffers[index]);
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, mLayerTextures[index], 0);
if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE)
{
glBindFramebuffer(GL_FRAMEBUFFER, 0);
glBindTexture(GL_TEXTURE_2D, 0);
DestroyLayerTargets();
return false;
}
}
glBindFramebuffer(GL_FRAMEBUFFER, 0);
glBindTexture(GL_TEXTURE_2D, 0);
return true;
}
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;
mLayerTargetWidth = 0;
mLayerTargetHeight = 0;
}
RuntimeRenderScene::LayerProgram* RuntimeRenderScene::FindLayer(const std::string& layerId)
{
for (LayerProgram& layer : mLayers)

View File

@@ -36,6 +36,8 @@ private:
};
void ConsumePreparedPrograms();
bool EnsureLayerTargets(unsigned width, unsigned height);
void DestroyLayerTargets();
LayerProgram* FindLayer(const std::string& layerId);
const LayerProgram* FindLayer(const std::string& layerId) const;
static std::string Fingerprint(const RuntimeShaderArtifact& artifact);
@@ -43,4 +45,8 @@ private:
RuntimeShaderPrepareWorker mPrepareWorker;
std::vector<LayerProgram> mLayers;
std::vector<std::string> mLayerOrder;
GLuint mLayerFramebuffers[2] = {};
GLuint mLayerTextures[2] = {};
unsigned mLayerTargetWidth = 0;
unsigned mLayerTargetHeight = 0;
};

View File

@@ -9,7 +9,8 @@
namespace
{
constexpr GLuint kGlobalParamsBindingPoint = 0;
constexpr GLuint kSourceTextureUnit = 0;
constexpr GLuint kVideoInputTextureUnit = 0;
constexpr GLuint kLayerInputTextureUnit = 1;
const char* kVertexShaderSource = R"GLSL(
#version 430 core
@@ -127,7 +128,7 @@ bool RuntimeShaderRenderer::BuildPreparedProgram(
return true;
}
void RuntimeShaderRenderer::RenderFrame(uint64_t frameIndex, unsigned width, unsigned height)
void RuntimeShaderRenderer::RenderFrame(uint64_t frameIndex, unsigned width, unsigned height, GLuint sourceTexture, GLuint layerInputTexture)
{
if (mProgram == 0)
return;
@@ -137,7 +138,7 @@ void RuntimeShaderRenderer::RenderFrame(uint64_t frameIndex, unsigned width, uns
glDisable(GL_DEPTH_TEST);
glDisable(GL_BLEND);
UpdateGlobalParams(frameIndex, width, height);
BindRuntimeTextures();
BindRuntimeTextures(sourceTexture, layerInputTexture);
glBindVertexArray(mVertexArray);
glUseProgram(mProgram);
glDrawArrays(GL_TRIANGLES, 0, 3);
@@ -236,16 +237,16 @@ void RuntimeShaderRenderer::AssignSamplerUniforms(GLuint program)
glUseProgram(program);
const GLint videoInputLocation = glGetUniformLocation(program, "gVideoInput");
if (videoInputLocation >= 0)
glUniform1i(videoInputLocation, static_cast<GLint>(kSourceTextureUnit));
glUniform1i(videoInputLocation, static_cast<GLint>(kVideoInputTextureUnit));
const GLint videoInputArrayLocation = glGetUniformLocation(program, "gVideoInput_0");
if (videoInputArrayLocation >= 0)
glUniform1i(videoInputArrayLocation, static_cast<GLint>(kSourceTextureUnit));
glUniform1i(videoInputArrayLocation, static_cast<GLint>(kVideoInputTextureUnit));
const GLint layerInputLocation = glGetUniformLocation(program, "gLayerInput");
if (layerInputLocation >= 0)
glUniform1i(layerInputLocation, static_cast<GLint>(kSourceTextureUnit));
glUniform1i(layerInputLocation, static_cast<GLint>(kLayerInputTextureUnit));
const GLint layerInputArrayLocation = glGetUniformLocation(program, "gLayerInput_0");
if (layerInputArrayLocation >= 0)
glUniform1i(layerInputArrayLocation, static_cast<GLint>(kSourceTextureUnit));
glUniform1i(layerInputArrayLocation, static_cast<GLint>(kLayerInputTextureUnit));
glUseProgram(0);
}
@@ -268,10 +269,14 @@ void RuntimeShaderRenderer::UpdateGlobalParams(uint64_t frameIndex, unsigned wid
glBindBuffer(GL_UNIFORM_BUFFER, 0);
}
void RuntimeShaderRenderer::BindRuntimeTextures()
void RuntimeShaderRenderer::BindRuntimeTextures(GLuint sourceTexture, GLuint layerInputTexture)
{
glActiveTexture(GL_TEXTURE0 + kSourceTextureUnit);
glBindTexture(GL_TEXTURE_2D, mFallbackSourceTexture);
const GLuint resolvedSourceTexture = sourceTexture != 0 ? sourceTexture : mFallbackSourceTexture;
const GLuint resolvedLayerInputTexture = layerInputTexture != 0 ? layerInputTexture : resolvedSourceTexture;
glActiveTexture(GL_TEXTURE0 + kVideoInputTextureUnit);
glBindTexture(GL_TEXTURE_2D, resolvedSourceTexture);
glActiveTexture(GL_TEXTURE0 + kLayerInputTextureUnit);
glBindTexture(GL_TEXTURE_2D, resolvedLayerInputTexture);
glActiveTexture(GL_TEXTURE0);
}

View File

@@ -20,7 +20,7 @@ public:
bool CommitShaderArtifact(const RuntimeShaderArtifact& artifact, std::string& error);
bool CommitPreparedProgram(RuntimePreparedShaderProgram& preparedProgram, std::string& error);
bool HasProgram() const { return mProgram != 0; }
void RenderFrame(uint64_t frameIndex, unsigned width, unsigned height);
void RenderFrame(uint64_t frameIndex, unsigned width, unsigned height, GLuint sourceTexture = 0, GLuint layerInputTexture = 0);
void ShutdownGl();
static bool BuildPreparedProgram(
@@ -35,7 +35,7 @@ private:
static bool BuildProgram(const std::string& fragmentShaderSource, GLuint& program, GLuint& vertexShader, GLuint& fragmentShader, std::string& error);
static void AssignSamplerUniforms(GLuint program);
void UpdateGlobalParams(uint64_t frameIndex, unsigned width, unsigned height);
void BindRuntimeTextures();
void BindRuntimeTextures(GLuint sourceTexture, GLuint layerInputTexture);
void DestroyProgram();
void DestroyStaticGlResources();

View File

@@ -40,12 +40,12 @@ float4 sampleWarped(float2 uv, float2 resolution, out bool insideSource)
insideSource = uv.x >= 0.0 && uv.x <= 1.0 && uv.y >= 0.0 && uv.y <= 1.0;
if (edgeMode == 1)
return sampleVideo(clamp(uv, 0.0, 1.0));
return sampleLayerInput(clamp(uv, 0.0, 1.0));
if (edgeMode == 2)
return sampleVideo(float2(mirroredCoordinate(uv.x), mirroredCoordinate(uv.y)));
return sampleLayerInput(float2(mirroredCoordinate(uv.x), mirroredCoordinate(uv.y)));
float edgeMask = sourceBoundsMask(uv, resolution);
float4 color = sampleVideo(clamp(uv, 0.0, 1.0));
float4 color = sampleLayerInput(clamp(uv, 0.0, 1.0));
return lerp(outsideColor, color, edgeMask);
}