From 1429b2e6608962b6f5ce9ee3279b122b6cf0554e Mon Sep 17 00:00:00 2001 From: Aiden <68633820+awils27@users.noreply.github.com> Date: Tue, 12 May 2026 16:52:15 +1000 Subject: [PATCH] Update shader --- apps/RenderCadenceCompositor/README.md | 48 +++++++- .../render/runtime/RuntimeRenderScene.cpp | 107 +++++++++++++++++- .../render/runtime/RuntimeRenderScene.h | 6 + .../render/runtime/RuntimeShaderRenderer.cpp | 25 ++-- .../render/runtime/RuntimeShaderRenderer.h | 4 +- shaders/crt-bulge/shader.slang | 6 +- 6 files changed, 178 insertions(+), 18 deletions(-) diff --git a/apps/RenderCadenceCompositor/README.md b/apps/RenderCadenceCompositor/README.md index b78d9ef..1992c97 100644 --- a/apps/RenderCadenceCompositor/README.md +++ b/apps/RenderCadenceCompositor/README.md @@ -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: diff --git a/apps/RenderCadenceCompositor/render/runtime/RuntimeRenderScene.cpp b/apps/RenderCadenceCompositor/render/runtime/RuntimeRenderScene.cpp index a56876b..be22c4a 100644 --- a/apps/RenderCadenceCompositor/render/runtime/RuntimeRenderScene.cpp +++ b/apps/RenderCadenceCompositor/render/runtime/RuntimeRenderScene.cpp @@ -6,6 +6,10 @@ #include #include +#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 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(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(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(width), + static_cast(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) diff --git a/apps/RenderCadenceCompositor/render/runtime/RuntimeRenderScene.h b/apps/RenderCadenceCompositor/render/runtime/RuntimeRenderScene.h index d56faca..6dff98b 100644 --- a/apps/RenderCadenceCompositor/render/runtime/RuntimeRenderScene.h +++ b/apps/RenderCadenceCompositor/render/runtime/RuntimeRenderScene.h @@ -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 mLayers; std::vector mLayerOrder; + GLuint mLayerFramebuffers[2] = {}; + GLuint mLayerTextures[2] = {}; + unsigned mLayerTargetWidth = 0; + unsigned mLayerTargetHeight = 0; }; diff --git a/apps/RenderCadenceCompositor/render/runtime/RuntimeShaderRenderer.cpp b/apps/RenderCadenceCompositor/render/runtime/RuntimeShaderRenderer.cpp index 2566daa..3cc6889 100644 --- a/apps/RenderCadenceCompositor/render/runtime/RuntimeShaderRenderer.cpp +++ b/apps/RenderCadenceCompositor/render/runtime/RuntimeShaderRenderer.cpp @@ -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(kSourceTextureUnit)); + glUniform1i(videoInputLocation, static_cast(kVideoInputTextureUnit)); const GLint videoInputArrayLocation = glGetUniformLocation(program, "gVideoInput_0"); if (videoInputArrayLocation >= 0) - glUniform1i(videoInputArrayLocation, static_cast(kSourceTextureUnit)); + glUniform1i(videoInputArrayLocation, static_cast(kVideoInputTextureUnit)); const GLint layerInputLocation = glGetUniformLocation(program, "gLayerInput"); if (layerInputLocation >= 0) - glUniform1i(layerInputLocation, static_cast(kSourceTextureUnit)); + glUniform1i(layerInputLocation, static_cast(kLayerInputTextureUnit)); const GLint layerInputArrayLocation = glGetUniformLocation(program, "gLayerInput_0"); if (layerInputArrayLocation >= 0) - glUniform1i(layerInputArrayLocation, static_cast(kSourceTextureUnit)); + glUniform1i(layerInputArrayLocation, static_cast(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); } diff --git a/apps/RenderCadenceCompositor/render/runtime/RuntimeShaderRenderer.h b/apps/RenderCadenceCompositor/render/runtime/RuntimeShaderRenderer.h index 2effb48..730a998 100644 --- a/apps/RenderCadenceCompositor/render/runtime/RuntimeShaderRenderer.h +++ b/apps/RenderCadenceCompositor/render/runtime/RuntimeShaderRenderer.h @@ -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(); diff --git a/shaders/crt-bulge/shader.slang b/shaders/crt-bulge/shader.slang index 6bed29a..3dde76b 100644 --- a/shaders/crt-bulge/shader.slang +++ b/shaders/crt-bulge/shader.slang @@ -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); }