Render udpates

This commit is contained in:
Aiden
2026-05-12 15:26:02 +10:00
parent c5fd8e72b4
commit 334693f28c
13 changed files with 437 additions and 23 deletions

View File

@@ -336,6 +336,9 @@ set(RENDER_CADENCE_APP_SOURCES
"${RENDER_CADENCE_APP_DIR}/render/RuntimeShaderParams.h" "${RENDER_CADENCE_APP_DIR}/render/RuntimeShaderParams.h"
"${RENDER_CADENCE_APP_DIR}/render/RuntimeRenderScene.cpp" "${RENDER_CADENCE_APP_DIR}/render/RuntimeRenderScene.cpp"
"${RENDER_CADENCE_APP_DIR}/render/RuntimeRenderScene.h" "${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.cpp"
"${RENDER_CADENCE_APP_DIR}/render/SimpleMotionRenderer.h" "${RENDER_CADENCE_APP_DIR}/render/SimpleMotionRenderer.h"
"${RENDER_CADENCE_APP_DIR}/runtime/RuntimeLayerModel.cpp" "${RENDER_CADENCE_APP_DIR}/runtime/RuntimeLayerModel.cpp"

View File

@@ -44,7 +44,8 @@ Included now:
- app-owned display/render layer model for shader build readiness - app-owned display/render layer model for shader build readiness
- app-owned submission of a completed shader artifact - app-owned submission of a completed shader artifact
- render-thread-owned runtime render scene for ready shader layers - 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 - manifest-driven stateless single-pass shader packages
- HTTP shader list populated from supported 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 - 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`. 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: 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. 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: 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 - `platform/`: COM/Win32/hidden GL context support
- `render/`: cadence, simple rendering, PBO readback - `render/`: cadence, simple rendering, PBO readback
- `render/RuntimeRenderScene`: render-thread-owned GL scene for ready runtime shader layers - `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 - `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 - `control/`: local HTTP API edge and runtime-state JSON presentation
- `json/`: compact JSON serialization helpers - `json/`: compact JSON serialization helpers

View File

@@ -11,6 +11,11 @@ HiddenGlWindow::~HiddenGlWindow()
} }
bool HiddenGlWindow::Create(unsigned width, unsigned height, std::string& error) 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(); Destroy();
@@ -63,7 +68,11 @@ bool HiddenGlWindow::Create(unsigned width, unsigned height, std::string& error)
pfd.cDepthBits = 0; pfd.cDepthBits = 0;
pfd.iLayerType = PFD_MAIN_PLANE; 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)) if (pixelFormat == 0 || !SetPixelFormat(mDc, pixelFormat, &pfd))
{ {
error = "Could not choose/set pixel format for hidden OpenGL window."; 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."; error = "wglCreateContext failed for hidden OpenGL window.";
return false; return false;
} }
if (sharedContext != nullptr && wglShareLists(sharedContext, mGlrc) != TRUE)
{
error = "wglShareLists failed for hidden OpenGL shared context.";
return false;
}
return true; return true;
} }

View File

@@ -13,6 +13,7 @@ public:
~HiddenGlWindow(); ~HiddenGlWindow();
bool Create(unsigned width, unsigned height, std::string& error); 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; bool MakeCurrent() const;
void ClearCurrent() const; void ClearCurrent() const;
void Destroy(); void Destroy();

View File

@@ -11,6 +11,7 @@
#include "SimpleMotionRenderer.h" #include "SimpleMotionRenderer.h"
#include <algorithm> #include <algorithm>
#include <memory>
#include <thread> #include <thread>
RenderThread::RenderThread(SystemFrameExchange& frameExchange, Config config) : RenderThread::RenderThread(SystemFrameExchange& frameExchange, Config config) :
@@ -83,11 +84,22 @@ void RenderThread::ThreadMain()
RenderCadenceCompositor::TryLog(RenderCadenceCompositor::LogLevel::Log, "render-thread", "Render thread starting."); RenderCadenceCompositor::TryLog(RenderCadenceCompositor::LogLevel::Log, "render-thread", "Render thread starting.");
HiddenGlWindow window; HiddenGlWindow window;
std::string error; 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); SignalStartupFailure(error.empty() ? "OpenGL context creation failed." : error);
return; return;
} }
std::unique_ptr<HiddenGlWindow> prepareWindow = std::make_unique<HiddenGlWindow>();
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()) if (!ResolveGLExtensions())
{ {
SignalStartupFailure("OpenGL extension resolution failed."); SignalStartupFailure("OpenGL extension resolution failed.");
@@ -97,6 +109,11 @@ void RenderThread::ThreadMain()
SimpleMotionRenderer renderer; SimpleMotionRenderer renderer;
RuntimeRenderScene runtimeRenderScene; RuntimeRenderScene runtimeRenderScene;
Bgra8ReadbackPipeline readback; 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)) if (!renderer.InitializeGl(mConfig.width, mConfig.height) || !readback.Initialize(mConfig.width, mConfig.height, mConfig.pboDepth))
{ {
SignalStartupFailure("Render pipeline initialization failed."); SignalStartupFailure("Render pipeline initialization failed.");

View File

@@ -1,5 +1,7 @@
#include "RuntimeRenderScene.h" #include "RuntimeRenderScene.h"
#include "../platform/HiddenGlWindow.h"
#include <algorithm> #include <algorithm>
#include <functional> #include <functional>
#include <utility> #include <utility>
@@ -9,9 +11,17 @@ RuntimeRenderScene::~RuntimeRenderScene()
ShutdownGl(); ShutdownGl();
} }
bool RuntimeRenderScene::StartPrepareWorker(std::unique_ptr<HiddenGlWindow> sharedWindow, std::string& error)
{
return mPrepareWorker.Start(std::move(sharedWindow), error);
}
bool RuntimeRenderScene::CommitRenderLayers(const std::vector<RenderCadenceCompositor::RuntimeRenderLayerModel>& layers, std::string& error) bool RuntimeRenderScene::CommitRenderLayers(const std::vector<RenderCadenceCompositor::RuntimeRenderLayerModel>& layers, std::string& error)
{ {
ConsumePreparedPrograms();
std::vector<std::string> nextOrder; std::vector<std::string> nextOrder;
std::vector<RenderCadenceCompositor::RuntimeRenderLayerModel> layersToPrepare;
nextOrder.reserve(layers.size()); nextOrder.reserve(layers.size());
for (const RenderCadenceCompositor::RuntimeRenderLayerModel& layer : layers) for (const RenderCadenceCompositor::RuntimeRenderLayerModel& layer : layers)
nextOrder.push_back(layer.id); nextOrder.push_back(layer.id);
@@ -48,25 +58,38 @@ bool RuntimeRenderScene::CommitRenderLayers(const std::vector<RenderCadenceCompo
if (program->shaderId == layer.shaderId && program->sourceFingerprint == fingerprint && program->renderer && program->renderer->HasProgram()) if (program->shaderId == layer.shaderId && program->sourceFingerprint == fingerprint && program->renderer && program->renderer->HasProgram())
continue; continue;
if (program->pendingFingerprint == fingerprint)
continue;
std::unique_ptr<RuntimeShaderRenderer> nextRenderer = std::make_unique<RuntimeShaderRenderer>();
if (!nextRenderer->CommitShaderArtifact(layer.artifact, error))
return false;
if (program->renderer)
program->renderer->ShutdownGl();
program->shaderId = layer.shaderId; program->shaderId = layer.shaderId;
program->sourceFingerprint = fingerprint; program->pendingFingerprint = fingerprint;
program->renderer = std::move(nextRenderer); layersToPrepare.push_back(layer);
} }
mLayerOrder = std::move(nextOrder); mLayerOrder = std::move(nextOrder);
if (!layersToPrepare.empty())
mPrepareWorker.Submit(layersToPrepare);
error.clear(); error.clear();
return true; 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) void RuntimeRenderScene::RenderFrame(uint64_t frameIndex, unsigned width, unsigned height)
{ {
ConsumePreparedPrograms();
for (const std::string& layerId : mLayerOrder) for (const std::string& layerId : mLayerOrder)
{ {
LayerProgram* layer = FindLayer(layerId); LayerProgram* layer = FindLayer(layerId);
@@ -78,6 +101,7 @@ void RuntimeRenderScene::RenderFrame(uint64_t frameIndex, unsigned width, unsign
void RuntimeRenderScene::ShutdownGl() void RuntimeRenderScene::ShutdownGl()
{ {
mPrepareWorker.Stop();
for (LayerProgram& layer : mLayers) for (LayerProgram& layer : mLayers)
{ {
if (layer.renderer) if (layer.renderer)
@@ -87,6 +111,41 @@ void RuntimeRenderScene::ShutdownGl()
mLayerOrder.clear(); 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<RuntimeShaderRenderer> nextRenderer = std::make_unique<RuntimeShaderRenderer>();
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) RuntimeRenderScene::LayerProgram* RuntimeRenderScene::FindLayer(const std::string& layerId)
{ {
for (LayerProgram& layer : mLayers) for (LayerProgram& layer : mLayers)

View File

@@ -1,8 +1,11 @@
#pragma once #pragma once
#include "../runtime/RuntimeLayerModel.h" #include "../runtime/RuntimeLayerModel.h"
#include "RuntimeShaderPrepareWorker.h"
#include "RuntimeShaderRenderer.h" #include "RuntimeShaderRenderer.h"
#include <windows.h>
#include <cstdint> #include <cstdint>
#include <memory> #include <memory>
#include <string> #include <string>
@@ -16,8 +19,9 @@ public:
RuntimeRenderScene& operator=(const RuntimeRenderScene&) = delete; RuntimeRenderScene& operator=(const RuntimeRenderScene&) = delete;
~RuntimeRenderScene(); ~RuntimeRenderScene();
bool StartPrepareWorker(std::unique_ptr<HiddenGlWindow> sharedWindow, std::string& error);
bool CommitRenderLayers(const std::vector<RenderCadenceCompositor::RuntimeRenderLayerModel>& layers, std::string& error); bool CommitRenderLayers(const std::vector<RenderCadenceCompositor::RuntimeRenderLayerModel>& layers, std::string& error);
bool HasLayers() const { return !mLayerOrder.empty(); } bool HasLayers();
void RenderFrame(uint64_t frameIndex, unsigned width, unsigned height); void RenderFrame(uint64_t frameIndex, unsigned width, unsigned height);
void ShutdownGl(); void ShutdownGl();
@@ -27,13 +31,16 @@ private:
std::string layerId; std::string layerId;
std::string shaderId; std::string shaderId;
std::string sourceFingerprint; std::string sourceFingerprint;
std::string pendingFingerprint;
std::unique_ptr<RuntimeShaderRenderer> renderer; std::unique_ptr<RuntimeShaderRenderer> renderer;
}; };
void ConsumePreparedPrograms();
LayerProgram* FindLayer(const std::string& layerId); LayerProgram* FindLayer(const std::string& layerId);
const LayerProgram* FindLayer(const std::string& layerId) const; const LayerProgram* FindLayer(const std::string& layerId) const;
static std::string Fingerprint(const RuntimeShaderArtifact& artifact); static std::string Fingerprint(const RuntimeShaderArtifact& artifact);
RuntimeShaderPrepareWorker mPrepareWorker;
std::vector<LayerProgram> mLayers; std::vector<LayerProgram> mLayers;
std::vector<std::string> mLayerOrder; std::vector<std::string> mLayerOrder;
}; };

View File

@@ -0,0 +1,158 @@
#include "RuntimeShaderPrepareWorker.h"
#include "../platform/HiddenGlWindow.h"
#include "RuntimeShaderRenderer.h"
#include <algorithm>
#include <chrono>
#include <functional>
#include <utility>
RuntimeShaderPrepareWorker::~RuntimeShaderPrepareWorker()
{
Stop();
}
bool RuntimeShaderPrepareWorker::Start(std::unique_ptr<HiddenGlWindow> 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<std::mutex> lock(mMutex);
mStartupReady = false;
mStartupError.clear();
}
mThread = std::thread([this]() { ThreadMain(); });
std::unique_lock<std::mutex> 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<RuntimePreparedShaderProgram> completed;
{
std::lock_guard<std::mutex> 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<RenderCadenceCompositor::RuntimeRenderLayerModel>& layers)
{
std::lock_guard<std::mutex> 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<std::mutex> 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<std::mutex> lock(mMutex);
mStartupError = "Runtime shader prepare worker could not make shared GL context current.";
mStartupCondition.notify_all();
return;
}
{
std::lock_guard<std::mutex> 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<std::mutex> 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<std::mutex> lock(mMutex);
mCompleted.push_back(std::move(preparedProgram));
}
mWindow->ClearCurrent();
}
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));
}

View File

@@ -0,0 +1,56 @@
#pragma once
#include "RuntimeShaderProgram.h"
#include "../runtime/RuntimeLayerModel.h"
#include <windows.h>
#include <atomic>
#include <condition_variable>
#include <deque>
#include <memory>
#include <mutex>
#include <string>
#include <thread>
#include <vector>
class HiddenGlWindow;
class RuntimeShaderPrepareWorker
{
public:
RuntimeShaderPrepareWorker() = default;
RuntimeShaderPrepareWorker(const RuntimeShaderPrepareWorker&) = delete;
RuntimeShaderPrepareWorker& operator=(const RuntimeShaderPrepareWorker&) = delete;
~RuntimeShaderPrepareWorker();
bool Start(std::unique_ptr<HiddenGlWindow> sharedWindow, std::string& error);
void Stop();
void Submit(const std::vector<RenderCadenceCompositor::RuntimeRenderLayerModel>& 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<HiddenGlWindow> mWindow;
std::mutex mMutex;
std::condition_variable mCondition;
std::deque<PrepareRequest> mRequests;
std::deque<RuntimePreparedShaderProgram> mCompleted;
std::condition_variable mStartupCondition;
std::thread mThread;
std::atomic<bool> mStopping{ false };
std::atomic<bool> mStarted{ false };
bool mStartupReady = false;
std::string mStartupError;
};

View File

@@ -0,0 +1,32 @@
#pragma once
#include "GLExtensions.h"
#include "../runtime/RuntimeShaderArtifact.h"
#include <string>
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;
}
};

View File

@@ -71,6 +71,62 @@ bool RuntimeShaderRenderer::CommitShaderArtifact(const RuntimeShaderArtifact& ar
return true; 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) void RuntimeShaderRenderer::RenderFrame(uint64_t frameIndex, unsigned width, unsigned height)
{ {
if (mProgram == 0) if (mProgram == 0)
@@ -175,7 +231,7 @@ bool RuntimeShaderRenderer::BuildProgram(const std::string& fragmentShaderSource
return true; return true;
} }
void RuntimeShaderRenderer::AssignSamplerUniforms(GLuint program) const void RuntimeShaderRenderer::AssignSamplerUniforms(GLuint program)
{ {
glUseProgram(program); glUseProgram(program);
const GLint videoInputLocation = glGetUniformLocation(program, "gVideoInput"); const GLint videoInputLocation = glGetUniformLocation(program, "gVideoInput");
@@ -219,7 +275,7 @@ void RuntimeShaderRenderer::BindRuntimeTextures()
glActiveTexture(GL_TEXTURE0); 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); shader = glCreateShader(shaderType);
glShaderSource(shader, 1, &source, nullptr); glShaderSource(shader, 1, &source, nullptr);

View File

@@ -1,6 +1,7 @@
#pragma once #pragma once
#include "GLExtensions.h" #include "GLExtensions.h"
#include "RuntimeShaderProgram.h"
#include "../runtime/RuntimeShaderArtifact.h" #include "../runtime/RuntimeShaderArtifact.h"
#include <cstdint> #include <cstdint>
@@ -17,15 +18,22 @@ public:
bool CommitFragmentShader(const std::string& fragmentShaderSource, std::string& error); bool CommitFragmentShader(const std::string& fragmentShaderSource, std::string& error);
bool CommitShaderArtifact(const RuntimeShaderArtifact& artifact, std::string& error); bool CommitShaderArtifact(const RuntimeShaderArtifact& artifact, std::string& error);
bool CommitPreparedProgram(RuntimePreparedShaderProgram& preparedProgram, std::string& error);
bool HasProgram() const { return mProgram != 0; } bool HasProgram() const { return mProgram != 0; }
void RenderFrame(uint64_t frameIndex, unsigned width, unsigned height); void RenderFrame(uint64_t frameIndex, unsigned width, unsigned height);
void ShutdownGl(); void ShutdownGl();
static bool BuildPreparedProgram(
const std::string& layerId,
const std::string& sourceFingerprint,
const RuntimeShaderArtifact& artifact,
RuntimePreparedShaderProgram& preparedProgram);
private: private:
bool EnsureStaticGlResources(std::string& error); bool EnsureStaticGlResources(std::string& error);
bool CompileShader(GLenum shaderType, const char* source, GLuint& shader, std::string& error) const; static bool CompileShader(GLenum shaderType, const char* source, GLuint& shader, std::string& error);
bool BuildProgram(const std::string& fragmentShaderSource, GLuint& program, GLuint& vertexShader, GLuint& fragmentShader, std::string& error); static bool BuildProgram(const std::string& fragmentShaderSource, GLuint& program, GLuint& vertexShader, GLuint& fragmentShader, std::string& error);
void AssignSamplerUniforms(GLuint program) const; static void AssignSamplerUniforms(GLuint program);
void UpdateGlobalParams(uint64_t frameIndex, unsigned width, unsigned height); void UpdateGlobalParams(uint64_t frameIndex, unsigned width, unsigned height);
void BindRuntimeTextures(); void BindRuntimeTextures();
void DestroyProgram(); void DestroyProgram();

View File

@@ -11,7 +11,7 @@ Only the render thread may bind and use its primary OpenGL context.
Allowed on the render thread: Allowed on the render thread:
- GL resource creation and destruction for resources it owns - 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 - drawing the next frame
- async readback queueing and completion polling - async readback queueing and completion polling
- publishing completed system-memory frames - publishing completed system-memory frames
@@ -28,7 +28,7 @@ Not allowed on the render thread:
- blocking console logging - blocking console logging
- config file discovery or parsing - 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 ## 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: Runtime shader work is split into two phases:
1. CPU/build phase outside the render thread 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 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. 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.