From 334693f28c4e94ac7fc8c70ef45e1d289cfe12ce Mon Sep 17 00:00:00 2001 From: Aiden <68633820+awils27@users.noreply.github.com> Date: Tue, 12 May 2026 15:26:02 +1000 Subject: [PATCH] Render udpates --- CMakeLists.txt | 3 + apps/RenderCadenceCompositor/README.md | 8 +- .../platform/HiddenGlWindow.cpp | 16 +- .../platform/HiddenGlWindow.h | 1 + .../render/RenderThread.cpp | 19 ++- .../render/RuntimeRenderScene.cpp | 75 ++++++++- .../render/RuntimeRenderScene.h | 9 +- .../render/RuntimeShaderPrepareWorker.cpp | 158 ++++++++++++++++++ .../render/RuntimeShaderPrepareWorker.h | 56 +++++++ .../render/RuntimeShaderProgram.h | 32 ++++ .../render/RuntimeShaderRenderer.cpp | 60 ++++++- .../render/RuntimeShaderRenderer.h | 14 +- docs/RENDER_CADENCE_GOLDEN_RULES.md | 9 +- 13 files changed, 437 insertions(+), 23 deletions(-) create mode 100644 apps/RenderCadenceCompositor/render/RuntimeShaderPrepareWorker.cpp create mode 100644 apps/RenderCadenceCompositor/render/RuntimeShaderPrepareWorker.h create mode 100644 apps/RenderCadenceCompositor/render/RuntimeShaderProgram.h diff --git a/CMakeLists.txt b/CMakeLists.txt index b3f2ef1..5f8bcd2 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -336,6 +336,9 @@ set(RENDER_CADENCE_APP_SOURCES "${RENDER_CADENCE_APP_DIR}/render/RuntimeShaderParams.h" "${RENDER_CADENCE_APP_DIR}/render/RuntimeRenderScene.cpp" "${RENDER_CADENCE_APP_DIR}/render/RuntimeRenderScene.h" + "${RENDER_CADENCE_APP_DIR}/render/RuntimeShaderPrepareWorker.cpp" + "${RENDER_CADENCE_APP_DIR}/render/RuntimeShaderPrepareWorker.h" + "${RENDER_CADENCE_APP_DIR}/render/RuntimeShaderProgram.h" "${RENDER_CADENCE_APP_DIR}/render/SimpleMotionRenderer.cpp" "${RENDER_CADENCE_APP_DIR}/render/SimpleMotionRenderer.h" "${RENDER_CADENCE_APP_DIR}/runtime/RuntimeLayerModel.cpp" diff --git a/apps/RenderCadenceCompositor/README.md b/apps/RenderCadenceCompositor/README.md index 3ba2718..e87370a 100644 --- a/apps/RenderCadenceCompositor/README.md +++ b/apps/RenderCadenceCompositor/README.md @@ -44,7 +44,8 @@ Included now: - app-owned display/render layer model for shader build readiness - app-owned submission of a completed shader artifact - render-thread-owned runtime render scene for ready shader layers -- render-thread-only GL commit once the artifact is ready +- 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 - default float, vec2, color, boolean, enum, and trigger parameters @@ -197,7 +198,7 @@ Healthy first-run signs: On startup the app begins compiling the selected shader package on a background thread owned by the app orchestration layer. The default is `shaders/happy-accident`. -The render thread keeps drawing the simple motion renderer while Slang compiles. It does not choose packages, launch Slang, or track build lifecycle. It only receives a completed shader artifact and attempts the OpenGL shader compile/link at a frame boundary. If either the Slang build or GL commit fails, the app keeps rendering the 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: @@ -213,7 +214,7 @@ The `/api/state` shader list uses the same support rules as runtime shader compi 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, commits/removes GL programs, and renders the 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. 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. Successful handoff signs: @@ -264,6 +265,7 @@ This app keeps the same core behavior but splits it into modules that can grow: - `platform/`: COM/Win32/hidden GL context support - `render/`: cadence, simple rendering, PBO readback - `render/RuntimeRenderScene`: render-thread-owned GL scene for ready runtime shader layers +- `render/RuntimeShaderPrepareWorker`: shared-context runtime shader program compile/link worker - `runtime/`: app-owned shader layer readiness model, runtime Slang build bridge, and completed artifact handoff - `control/`: local HTTP API edge and runtime-state JSON presentation - `json/`: compact JSON serialization helpers diff --git a/apps/RenderCadenceCompositor/platform/HiddenGlWindow.cpp b/apps/RenderCadenceCompositor/platform/HiddenGlWindow.cpp index 15e0599..07d7e81 100644 --- a/apps/RenderCadenceCompositor/platform/HiddenGlWindow.cpp +++ b/apps/RenderCadenceCompositor/platform/HiddenGlWindow.cpp @@ -11,6 +11,11 @@ HiddenGlWindow::~HiddenGlWindow() } bool HiddenGlWindow::Create(unsigned width, unsigned height, std::string& error) +{ + return CreateShared(width, height, nullptr, nullptr, error); +} + +bool HiddenGlWindow::CreateShared(unsigned width, unsigned height, HDC sharedDeviceContext, HGLRC sharedContext, std::string& error) { Destroy(); @@ -63,7 +68,11 @@ bool HiddenGlWindow::Create(unsigned width, unsigned height, std::string& error) pfd.cDepthBits = 0; pfd.iLayerType = PFD_MAIN_PLANE; - const int pixelFormat = ChoosePixelFormat(mDc, &pfd); + int pixelFormat = 0; + if (sharedDeviceContext != nullptr) + pixelFormat = GetPixelFormat(sharedDeviceContext); + if (pixelFormat == 0) + pixelFormat = ChoosePixelFormat(mDc, &pfd); if (pixelFormat == 0 || !SetPixelFormat(mDc, pixelFormat, &pfd)) { error = "Could not choose/set pixel format for hidden OpenGL window."; @@ -76,6 +85,11 @@ bool HiddenGlWindow::Create(unsigned width, unsigned height, std::string& error) error = "wglCreateContext failed for hidden OpenGL window."; return false; } + if (sharedContext != nullptr && wglShareLists(sharedContext, mGlrc) != TRUE) + { + error = "wglShareLists failed for hidden OpenGL shared context."; + return false; + } return true; } diff --git a/apps/RenderCadenceCompositor/platform/HiddenGlWindow.h b/apps/RenderCadenceCompositor/platform/HiddenGlWindow.h index 4f25f48..8465bb0 100644 --- a/apps/RenderCadenceCompositor/platform/HiddenGlWindow.h +++ b/apps/RenderCadenceCompositor/platform/HiddenGlWindow.h @@ -13,6 +13,7 @@ public: ~HiddenGlWindow(); bool Create(unsigned width, unsigned height, std::string& error); + bool CreateShared(unsigned width, unsigned height, HDC sharedDeviceContext, HGLRC sharedContext, std::string& error); bool MakeCurrent() const; void ClearCurrent() const; void Destroy(); diff --git a/apps/RenderCadenceCompositor/render/RenderThread.cpp b/apps/RenderCadenceCompositor/render/RenderThread.cpp index 2a5a195..fd9bcfe 100644 --- a/apps/RenderCadenceCompositor/render/RenderThread.cpp +++ b/apps/RenderCadenceCompositor/render/RenderThread.cpp @@ -11,6 +11,7 @@ #include "SimpleMotionRenderer.h" #include +#include #include RenderThread::RenderThread(SystemFrameExchange& frameExchange, Config config) : @@ -83,11 +84,22 @@ void RenderThread::ThreadMain() RenderCadenceCompositor::TryLog(RenderCadenceCompositor::LogLevel::Log, "render-thread", "Render thread starting."); HiddenGlWindow window; std::string error; - if (!window.Create(mConfig.width, mConfig.height, error) || !window.MakeCurrent()) + if (!window.Create(mConfig.width, mConfig.height, error)) { SignalStartupFailure(error.empty() ? "OpenGL context creation failed." : error); return; } + std::unique_ptr prepareWindow = std::make_unique(); + if (!prepareWindow->CreateShared(mConfig.width, mConfig.height, window.DeviceContext(), window.Context(), error)) + { + SignalStartupFailure(error.empty() ? "Runtime shader prepare shared context creation failed." : error); + return; + } + if (!window.MakeCurrent()) + { + SignalStartupFailure("OpenGL context creation failed."); + return; + } if (!ResolveGLExtensions()) { SignalStartupFailure("OpenGL extension resolution failed."); @@ -97,6 +109,11 @@ void RenderThread::ThreadMain() SimpleMotionRenderer renderer; RuntimeRenderScene runtimeRenderScene; Bgra8ReadbackPipeline readback; + if (!runtimeRenderScene.StartPrepareWorker(std::move(prepareWindow), error)) + { + SignalStartupFailure(error.empty() ? "Runtime shader prepare worker initialization failed." : error); + return; + } if (!renderer.InitializeGl(mConfig.width, mConfig.height) || !readback.Initialize(mConfig.width, mConfig.height, mConfig.pboDepth)) { SignalStartupFailure("Render pipeline initialization failed."); diff --git a/apps/RenderCadenceCompositor/render/RuntimeRenderScene.cpp b/apps/RenderCadenceCompositor/render/RuntimeRenderScene.cpp index 5471fba..0edc813 100644 --- a/apps/RenderCadenceCompositor/render/RuntimeRenderScene.cpp +++ b/apps/RenderCadenceCompositor/render/RuntimeRenderScene.cpp @@ -1,5 +1,7 @@ #include "RuntimeRenderScene.h" +#include "../platform/HiddenGlWindow.h" + #include #include #include @@ -9,9 +11,17 @@ RuntimeRenderScene::~RuntimeRenderScene() ShutdownGl(); } +bool RuntimeRenderScene::StartPrepareWorker(std::unique_ptr sharedWindow, std::string& error) +{ + return mPrepareWorker.Start(std::move(sharedWindow), error); +} + bool RuntimeRenderScene::CommitRenderLayers(const std::vector& layers, std::string& error) { + ConsumePreparedPrograms(); + std::vector nextOrder; + std::vector layersToPrepare; nextOrder.reserve(layers.size()); for (const RenderCadenceCompositor::RuntimeRenderLayerModel& layer : layers) nextOrder.push_back(layer.id); @@ -48,25 +58,38 @@ bool RuntimeRenderScene::CommitRenderLayers(const std::vectorshaderId == layer.shaderId && program->sourceFingerprint == fingerprint && program->renderer && program->renderer->HasProgram()) continue; + if (program->pendingFingerprint == fingerprint) + continue; - std::unique_ptr nextRenderer = std::make_unique(); - if (!nextRenderer->CommitShaderArtifact(layer.artifact, error)) - return false; - - if (program->renderer) - program->renderer->ShutdownGl(); program->shaderId = layer.shaderId; - program->sourceFingerprint = fingerprint; - program->renderer = std::move(nextRenderer); + program->pendingFingerprint = fingerprint; + layersToPrepare.push_back(layer); } mLayerOrder = std::move(nextOrder); + if (!layersToPrepare.empty()) + mPrepareWorker.Submit(layersToPrepare); error.clear(); return true; } +bool RuntimeRenderScene::HasLayers() +{ + ConsumePreparedPrograms(); + + for (const std::string& layerId : mLayerOrder) + { + const LayerProgram* layer = FindLayer(layerId); + if (layer && layer->renderer && layer->renderer->HasProgram()) + return true; + } + return false; +} + void RuntimeRenderScene::RenderFrame(uint64_t frameIndex, unsigned width, unsigned height) { + ConsumePreparedPrograms(); + for (const std::string& layerId : mLayerOrder) { LayerProgram* layer = FindLayer(layerId); @@ -78,6 +101,7 @@ void RuntimeRenderScene::RenderFrame(uint64_t frameIndex, unsigned width, unsign void RuntimeRenderScene::ShutdownGl() { + mPrepareWorker.Stop(); for (LayerProgram& layer : mLayers) { if (layer.renderer) @@ -87,6 +111,41 @@ void RuntimeRenderScene::ShutdownGl() mLayerOrder.clear(); } +void RuntimeRenderScene::ConsumePreparedPrograms() +{ + RuntimePreparedShaderProgram preparedProgram; + while (mPrepareWorker.TryConsume(preparedProgram)) + { + if (!preparedProgram.succeeded) + { + preparedProgram.ReleaseGl(); + continue; + } + + LayerProgram* layer = FindLayer(preparedProgram.layerId); + if (!layer || layer->pendingFingerprint != preparedProgram.sourceFingerprint) + { + preparedProgram.ReleaseGl(); + continue; + } + + std::unique_ptr nextRenderer = std::make_unique(); + std::string error; + if (!nextRenderer->CommitPreparedProgram(preparedProgram, error)) + { + preparedProgram.ReleaseGl(); + continue; + } + + if (layer->renderer) + layer->renderer->ShutdownGl(); + layer->renderer = std::move(nextRenderer); + layer->shaderId = preparedProgram.shaderId; + layer->sourceFingerprint = preparedProgram.sourceFingerprint; + layer->pendingFingerprint.clear(); + } +} + RuntimeRenderScene::LayerProgram* RuntimeRenderScene::FindLayer(const std::string& layerId) { for (LayerProgram& layer : mLayers) diff --git a/apps/RenderCadenceCompositor/render/RuntimeRenderScene.h b/apps/RenderCadenceCompositor/render/RuntimeRenderScene.h index 1895b04..d56faca 100644 --- a/apps/RenderCadenceCompositor/render/RuntimeRenderScene.h +++ b/apps/RenderCadenceCompositor/render/RuntimeRenderScene.h @@ -1,8 +1,11 @@ #pragma once #include "../runtime/RuntimeLayerModel.h" +#include "RuntimeShaderPrepareWorker.h" #include "RuntimeShaderRenderer.h" +#include + #include #include #include @@ -16,8 +19,9 @@ public: RuntimeRenderScene& operator=(const RuntimeRenderScene&) = delete; ~RuntimeRenderScene(); + bool StartPrepareWorker(std::unique_ptr sharedWindow, std::string& error); bool CommitRenderLayers(const std::vector& layers, std::string& error); - bool HasLayers() const { return !mLayerOrder.empty(); } + bool HasLayers(); void RenderFrame(uint64_t frameIndex, unsigned width, unsigned height); void ShutdownGl(); @@ -27,13 +31,16 @@ private: std::string layerId; std::string shaderId; std::string sourceFingerprint; + std::string pendingFingerprint; std::unique_ptr renderer; }; + void ConsumePreparedPrograms(); LayerProgram* FindLayer(const std::string& layerId); const LayerProgram* FindLayer(const std::string& layerId) const; static std::string Fingerprint(const RuntimeShaderArtifact& artifact); + RuntimeShaderPrepareWorker mPrepareWorker; std::vector mLayers; std::vector mLayerOrder; }; diff --git a/apps/RenderCadenceCompositor/render/RuntimeShaderPrepareWorker.cpp b/apps/RenderCadenceCompositor/render/RuntimeShaderPrepareWorker.cpp new file mode 100644 index 0000000..471fa4b --- /dev/null +++ b/apps/RenderCadenceCompositor/render/RuntimeShaderPrepareWorker.cpp @@ -0,0 +1,158 @@ +#include "RuntimeShaderPrepareWorker.h" + +#include "../platform/HiddenGlWindow.h" +#include "RuntimeShaderRenderer.h" + +#include +#include +#include +#include + +RuntimeShaderPrepareWorker::~RuntimeShaderPrepareWorker() +{ + Stop(); +} + +bool RuntimeShaderPrepareWorker::Start(std::unique_ptr sharedWindow, std::string& error) +{ + if (mThread.joinable()) + return true; + if (!sharedWindow || sharedWindow->DeviceContext() == nullptr || sharedWindow->Context() == nullptr) + { + error = "Runtime shader prepare worker needs an existing shared GL context."; + return false; + } + + mWindow = std::move(sharedWindow); + mStopping.store(false, std::memory_order_release); + mStarted.store(false, std::memory_order_release); + { + std::lock_guard lock(mMutex); + mStartupReady = false; + mStartupError.clear(); + } + mThread = std::thread([this]() { ThreadMain(); }); + + std::unique_lock lock(mMutex); + if (!mStartupCondition.wait_for(lock, std::chrono::seconds(3), [this]() { + return mStartupReady || !mStartupError.empty(); + })) + { + error = "Timed out starting runtime shader prepare worker."; + lock.unlock(); + Stop(); + return false; + } + if (!mStartupError.empty()) + { + error = mStartupError; + lock.unlock(); + Stop(); + return false; + } + return true; +} + +void RuntimeShaderPrepareWorker::Stop() +{ + mStopping.store(true, std::memory_order_release); + mCondition.notify_all(); + if (mThread.joinable()) + mThread.join(); + + std::deque completed; + { + std::lock_guard lock(mMutex); + mRequests.clear(); + completed.swap(mCompleted); + } + for (RuntimePreparedShaderProgram& program : completed) + program.ReleaseGl(); + mWindow.reset(); + mStarted.store(false, std::memory_order_release); +} + +void RuntimeShaderPrepareWorker::Submit(const std::vector& layers) +{ + std::lock_guard lock(mMutex); + for (const RenderCadenceCompositor::RuntimeRenderLayerModel& layer : layers) + { + if (layer.artifact.fragmentShaderSource.empty()) + continue; + + PrepareRequest request; + request.layerId = layer.id; + request.shaderId = layer.shaderId; + 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()); + mRequests.push_back(std::move(request)); + } + mCondition.notify_one(); +} + +bool RuntimeShaderPrepareWorker::TryConsume(RuntimePreparedShaderProgram& preparedProgram) +{ + std::lock_guard lock(mMutex); + if (mCompleted.empty()) + return false; + + preparedProgram = std::move(mCompleted.front()); + mCompleted.pop_front(); + return true; +} + +void RuntimeShaderPrepareWorker::ThreadMain() +{ + if (!mWindow || !mWindow->MakeCurrent()) + { + std::lock_guard lock(mMutex); + mStartupError = "Runtime shader prepare worker could not make shared GL context current."; + mStartupCondition.notify_all(); + return; + } + { + std::lock_guard lock(mMutex); + mStartupReady = true; + } + mStarted.store(true, std::memory_order_release); + mStartupCondition.notify_all(); + + while (!mStopping.load(std::memory_order_acquire)) + { + PrepareRequest request; + { + std::unique_lock lock(mMutex); + mCondition.wait(lock, [this]() { + return mStopping.load(std::memory_order_acquire) || !mRequests.empty(); + }); + if (mStopping.load(std::memory_order_acquire)) + break; + request = std::move(mRequests.front()); + mRequests.pop_front(); + } + + RuntimePreparedShaderProgram preparedProgram; + RuntimeShaderRenderer::BuildPreparedProgram( + request.layerId, + request.sourceFingerprint, + request.artifact, + preparedProgram); + glFlush(); + + std::lock_guard lock(mMutex); + mCompleted.push_back(std::move(preparedProgram)); + } + + mWindow->ClearCurrent(); +} + +std::string RuntimeShaderPrepareWorker::Fingerprint(const RuntimeShaderArtifact& artifact) +{ + const std::hash hasher; + return artifact.shaderId + ":" + std::to_string(artifact.fragmentShaderSource.size()) + ":" + std::to_string(hasher(artifact.fragmentShaderSource)); +} diff --git a/apps/RenderCadenceCompositor/render/RuntimeShaderPrepareWorker.h b/apps/RenderCadenceCompositor/render/RuntimeShaderPrepareWorker.h new file mode 100644 index 0000000..5299f43 --- /dev/null +++ b/apps/RenderCadenceCompositor/render/RuntimeShaderPrepareWorker.h @@ -0,0 +1,56 @@ +#pragma once + +#include "RuntimeShaderProgram.h" +#include "../runtime/RuntimeLayerModel.h" + +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +class HiddenGlWindow; + +class RuntimeShaderPrepareWorker +{ +public: + RuntimeShaderPrepareWorker() = default; + RuntimeShaderPrepareWorker(const RuntimeShaderPrepareWorker&) = delete; + RuntimeShaderPrepareWorker& operator=(const RuntimeShaderPrepareWorker&) = delete; + ~RuntimeShaderPrepareWorker(); + + bool Start(std::unique_ptr sharedWindow, std::string& error); + void Stop(); + + void Submit(const std::vector& layers); + bool TryConsume(RuntimePreparedShaderProgram& preparedProgram); + +private: + struct PrepareRequest + { + std::string layerId; + std::string shaderId; + std::string sourceFingerprint; + RuntimeShaderArtifact artifact; + }; + + void ThreadMain(); + static std::string Fingerprint(const RuntimeShaderArtifact& artifact); + + std::unique_ptr mWindow; + std::mutex mMutex; + std::condition_variable mCondition; + std::deque mRequests; + std::deque mCompleted; + std::condition_variable mStartupCondition; + std::thread mThread; + std::atomic mStopping{ false }; + std::atomic mStarted{ false }; + bool mStartupReady = false; + std::string mStartupError; +}; diff --git a/apps/RenderCadenceCompositor/render/RuntimeShaderProgram.h b/apps/RenderCadenceCompositor/render/RuntimeShaderProgram.h new file mode 100644 index 0000000..8f18825 --- /dev/null +++ b/apps/RenderCadenceCompositor/render/RuntimeShaderProgram.h @@ -0,0 +1,32 @@ +#pragma once + +#include "GLExtensions.h" +#include "../runtime/RuntimeShaderArtifact.h" + +#include + +struct RuntimePreparedShaderProgram +{ + std::string layerId; + std::string shaderId; + std::string sourceFingerprint; + RuntimeShaderArtifact artifact; + GLuint program = 0; + GLuint vertexShader = 0; + GLuint fragmentShader = 0; + bool succeeded = false; + std::string error; + + void ReleaseGl() + { + if (program != 0) + glDeleteProgram(program); + if (vertexShader != 0) + glDeleteShader(vertexShader); + if (fragmentShader != 0) + glDeleteShader(fragmentShader); + program = 0; + vertexShader = 0; + fragmentShader = 0; + } +}; diff --git a/apps/RenderCadenceCompositor/render/RuntimeShaderRenderer.cpp b/apps/RenderCadenceCompositor/render/RuntimeShaderRenderer.cpp index c58df2a..2566daa 100644 --- a/apps/RenderCadenceCompositor/render/RuntimeShaderRenderer.cpp +++ b/apps/RenderCadenceCompositor/render/RuntimeShaderRenderer.cpp @@ -71,6 +71,62 @@ bool RuntimeShaderRenderer::CommitShaderArtifact(const RuntimeShaderArtifact& ar return true; } +bool RuntimeShaderRenderer::CommitPreparedProgram(RuntimePreparedShaderProgram& preparedProgram, std::string& error) +{ + if (!preparedProgram.succeeded || preparedProgram.program == 0) + { + error = preparedProgram.error.empty() ? "Prepared runtime shader program is not valid." : preparedProgram.error; + return false; + } + + if (!EnsureStaticGlResources(error)) + return false; + + DestroyProgram(); + mProgram = preparedProgram.program; + mVertexShader = preparedProgram.vertexShader; + mFragmentShader = preparedProgram.fragmentShader; + mArtifact = preparedProgram.artifact; + preparedProgram.program = 0; + preparedProgram.vertexShader = 0; + preparedProgram.fragmentShader = 0; + return true; +} + +bool RuntimeShaderRenderer::BuildPreparedProgram( + const std::string& layerId, + const std::string& sourceFingerprint, + const RuntimeShaderArtifact& artifact, + RuntimePreparedShaderProgram& preparedProgram) +{ + preparedProgram = RuntimePreparedShaderProgram(); + preparedProgram.layerId = layerId; + preparedProgram.shaderId = artifact.shaderId; + preparedProgram.sourceFingerprint = sourceFingerprint; + preparedProgram.artifact = artifact; + + if (artifact.fragmentShaderSource.empty()) + { + preparedProgram.error = "Cannot prepare an empty fragment shader."; + return false; + } + + if (!BuildProgram( + artifact.fragmentShaderSource, + preparedProgram.program, + preparedProgram.vertexShader, + preparedProgram.fragmentShader, + preparedProgram.error)) + { + preparedProgram.ReleaseGl(); + return false; + } + + preparedProgram.succeeded = true; + AssignSamplerUniforms(preparedProgram.program); + return true; +} + void RuntimeShaderRenderer::RenderFrame(uint64_t frameIndex, unsigned width, unsigned height) { if (mProgram == 0) @@ -175,7 +231,7 @@ bool RuntimeShaderRenderer::BuildProgram(const std::string& fragmentShaderSource return true; } -void RuntimeShaderRenderer::AssignSamplerUniforms(GLuint program) const +void RuntimeShaderRenderer::AssignSamplerUniforms(GLuint program) { glUseProgram(program); const GLint videoInputLocation = glGetUniformLocation(program, "gVideoInput"); @@ -219,7 +275,7 @@ void RuntimeShaderRenderer::BindRuntimeTextures() glActiveTexture(GL_TEXTURE0); } -bool RuntimeShaderRenderer::CompileShader(GLenum shaderType, const char* source, GLuint& shader, std::string& error) const +bool RuntimeShaderRenderer::CompileShader(GLenum shaderType, const char* source, GLuint& shader, std::string& error) { shader = glCreateShader(shaderType); glShaderSource(shader, 1, &source, nullptr); diff --git a/apps/RenderCadenceCompositor/render/RuntimeShaderRenderer.h b/apps/RenderCadenceCompositor/render/RuntimeShaderRenderer.h index 28a7497..7763c1a 100644 --- a/apps/RenderCadenceCompositor/render/RuntimeShaderRenderer.h +++ b/apps/RenderCadenceCompositor/render/RuntimeShaderRenderer.h @@ -1,6 +1,7 @@ #pragma once #include "GLExtensions.h" +#include "RuntimeShaderProgram.h" #include "../runtime/RuntimeShaderArtifact.h" #include @@ -17,15 +18,22 @@ public: bool CommitFragmentShader(const std::string& fragmentShaderSource, std::string& error); 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 ShutdownGl(); + static bool BuildPreparedProgram( + const std::string& layerId, + const std::string& sourceFingerprint, + const RuntimeShaderArtifact& artifact, + RuntimePreparedShaderProgram& preparedProgram); + private: bool EnsureStaticGlResources(std::string& error); - bool CompileShader(GLenum shaderType, const char* source, GLuint& shader, std::string& error) const; - bool BuildProgram(const std::string& fragmentShaderSource, GLuint& program, GLuint& vertexShader, GLuint& fragmentShader, std::string& error); - void AssignSamplerUniforms(GLuint program) const; + static bool CompileShader(GLenum shaderType, const char* source, GLuint& shader, std::string& error); + 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 DestroyProgram(); diff --git a/docs/RENDER_CADENCE_GOLDEN_RULES.md b/docs/RENDER_CADENCE_GOLDEN_RULES.md index 303a89c..b5190f9 100644 --- a/docs/RENDER_CADENCE_GOLDEN_RULES.md +++ b/docs/RENDER_CADENCE_GOLDEN_RULES.md @@ -11,7 +11,7 @@ Only the render thread may bind and use its primary OpenGL context. Allowed on the render thread: - GL resource creation and destruction for resources it owns -- GL shader/program commit from an already-prepared artifact +- GL shader/program swap from an already-prepared GL program - drawing the next frame - async readback queueing and completion polling - publishing completed system-memory frames @@ -28,7 +28,7 @@ Not allowed on the render thread: - blocking console logging - config file discovery or parsing -If future GL preparation needs to happen off-thread, use an explicit shared-context GL prepare thread. Do not smuggle non-render work back into the cadence loop. +If GL preparation happens off-thread, use an explicit shared-context GL prepare thread. Do not smuggle non-render work back into the cadence loop. ## 2. Render Cadence Does Not Chase Buffers @@ -63,11 +63,12 @@ If no completed frame is available, record the miss and keep the ownership bound Runtime shader work is split into two phases: 1. CPU/build phase outside the render thread -2. GL commit phase on the render thread +2. shared-context GL preparation outside the render thread where practical +3. GL program swap on the render thread The CPU/build phase may parse manifests, invoke Slang, validate package shape, and prepare CPU-side data. -The render thread receives a completed artifact and either commits it at a frame boundary or rejects it. A failed artifact must not disturb the current renderer. +The render thread receives completed render-layer artifacts, asks the shared-context prepare worker to compile/link changed GL programs, and only swaps in prepared programs at a frame boundary. A failed artifact or failed GL preparation must not disturb the current renderer. The display/render layer model is app-owned. It may track requested shaders, build state, display metadata, and render-ready artifacts, but it must not perform GL work or drive render cadence directly.