data storage
This commit is contained in:
@@ -65,6 +65,8 @@ set(APP_SOURCES
|
||||
"${APP_DIR}/gl/pipeline/OpenGLRenderPipeline.cpp"
|
||||
"${APP_DIR}/gl/pipeline/OpenGLRenderPipeline.h"
|
||||
"${APP_DIR}/gl/pipeline/RenderPassDescriptor.h"
|
||||
"${APP_DIR}/gl/pipeline/ShaderFeedbackBuffers.cpp"
|
||||
"${APP_DIR}/gl/pipeline/ShaderFeedbackBuffers.h"
|
||||
"${APP_DIR}/gl/renderer/OpenGLRenderer.cpp"
|
||||
"${APP_DIR}/gl/renderer/OpenGLRenderer.h"
|
||||
"${APP_DIR}/gl/renderer/RenderTargetPool.cpp"
|
||||
@@ -347,7 +349,7 @@ install(FILES "${SLANG_LICENSE_FILE}"
|
||||
RENAME "SLANG_LICENSE.txt"
|
||||
)
|
||||
|
||||
install(FILES "${CMAKE_CURRENT_SOURCE_DIR}/SHADER_CONTRACT.md"
|
||||
install(FILES "${CMAKE_CURRENT_SOURCE_DIR}/shaders/SHADER_CONTRACT.md"
|
||||
DESTINATION "."
|
||||
)
|
||||
|
||||
|
||||
@@ -328,15 +328,6 @@ bool OpenGLComposite::InitOpenGLState()
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!mShaderPrograms->CompileLayerPrograms(mVideoIO->InputFrameWidth(), mVideoIO->InputFrameHeight(), sizeof(compilerErrorMessage), compilerErrorMessage))
|
||||
{
|
||||
MessageBoxA(NULL, compilerErrorMessage, "OpenGL shader failed to load or compile", MB_OK);
|
||||
return false;
|
||||
}
|
||||
mCachedLayerRenderStates = mShaderPrograms->CommittedLayerStates();
|
||||
mUseCommittedLayerStates = false;
|
||||
mShaderPrograms->ResetTemporalHistoryState();
|
||||
|
||||
std::string rendererError;
|
||||
if (!mRenderer->InitializeResources(
|
||||
mVideoIO->InputFrameWidth(),
|
||||
@@ -351,6 +342,17 @@ bool OpenGLComposite::InitOpenGLState()
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!mShaderPrograms->CompileLayerPrograms(mVideoIO->InputFrameWidth(), mVideoIO->InputFrameHeight(), sizeof(compilerErrorMessage), compilerErrorMessage))
|
||||
{
|
||||
MessageBoxA(NULL, compilerErrorMessage, "OpenGL shader failed to load or compile", MB_OK);
|
||||
return false;
|
||||
}
|
||||
mCachedLayerRenderStates = mShaderPrograms->CommittedLayerStates();
|
||||
mUseCommittedLayerStates = false;
|
||||
|
||||
mShaderPrograms->ResetTemporalHistoryState();
|
||||
mShaderPrograms->ResetShaderFeedbackState();
|
||||
|
||||
broadcastRuntimeState();
|
||||
mRuntimeServices->BeginPolling(*mRuntimeHost);
|
||||
return true;
|
||||
@@ -647,8 +649,8 @@ void OpenGLComposite::renderEffect()
|
||||
[this](const RuntimeRenderState& state, LayerProgram::TextBinding& textBinding, std::string& error) {
|
||||
return mShaderPrograms->UpdateTextBindingTexture(state, textBinding, error);
|
||||
},
|
||||
[this](const RuntimeRenderState& state, unsigned availableSourceHistoryLength, unsigned availableTemporalHistoryLength) {
|
||||
return mShaderPrograms->UpdateGlobalParamsBuffer(state, availableSourceHistoryLength, availableTemporalHistoryLength);
|
||||
[this](const RuntimeRenderState& state, unsigned availableSourceHistoryLength, unsigned availableTemporalHistoryLength, bool feedbackAvailable) {
|
||||
return mShaderPrograms->UpdateGlobalParamsBuffer(state, availableSourceHistoryLength, availableTemporalHistoryLength, feedbackAvailable);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -749,6 +751,7 @@ bool OpenGLComposite::ProcessRuntimePollResults()
|
||||
mUseCommittedLayerStates = false;
|
||||
mCachedLayerRenderStates = mShaderPrograms->CommittedLayerStates();
|
||||
mShaderPrograms->ResetTemporalHistoryState();
|
||||
mShaderPrograms->ResetShaderFeedbackState();
|
||||
broadcastRuntimeState();
|
||||
return true;
|
||||
}
|
||||
@@ -779,6 +782,7 @@ void OpenGLComposite::broadcastRuntimeState()
|
||||
void OpenGLComposite::resetTemporalHistoryState()
|
||||
{
|
||||
mShaderPrograms->ResetTemporalHistoryState();
|
||||
mShaderPrograms->ResetShaderFeedbackState();
|
||||
}
|
||||
|
||||
bool OpenGLComposite::CheckOpenGLExtensions()
|
||||
|
||||
@@ -59,6 +59,7 @@ void OpenGLRenderPass::Render(
|
||||
}
|
||||
|
||||
mRenderer.TemporalHistory().PushSourceFramebuffer(mRenderer.DecodeFramebuffer(), inputFrameWidth, inputFrameHeight);
|
||||
mRenderer.FeedbackBuffers().FinalizeFrame();
|
||||
}
|
||||
|
||||
void OpenGLRenderPass::RenderDecodePass(unsigned inputFrameWidth, unsigned inputFrameHeight, unsigned captureTextureWidth, VideoIOPixelFormat inputPixelFormat)
|
||||
@@ -195,6 +196,7 @@ std::vector<RenderPassDescriptor> OpenGLRenderPass::BuildLayerPassDescriptors(
|
||||
pass.passProgram = &passProgram;
|
||||
pass.layerState = &state;
|
||||
pass.capturePreLayerHistory = passIndex == 0 && state.temporalHistorySource == TemporalHistorySource::PreLayerInput;
|
||||
pass.captureFeedbackWrite = state.feedback.enabled && passProgram.passId == state.feedback.writePassId;
|
||||
passes.push_back(pass);
|
||||
|
||||
// A later pass can reference either the explicit output name or the
|
||||
@@ -236,6 +238,8 @@ void OpenGLRenderPass::RenderLayerPass(
|
||||
|
||||
if (pass.capturePreLayerHistory)
|
||||
mRenderer.TemporalHistory().PushPreLayerFramebuffer(pass.layerId, pass.sourceFramebuffer, inputFrameWidth, inputFrameHeight);
|
||||
if (pass.captureFeedbackWrite)
|
||||
mRenderer.FeedbackBuffers().CaptureFeedbackFramebuffer(pass.layerId, pass.destinationFramebuffer, inputFrameWidth, inputFrameHeight);
|
||||
}
|
||||
|
||||
void OpenGLRenderPass::RenderShaderProgram(
|
||||
@@ -261,14 +265,19 @@ void OpenGLRenderPass::RenderShaderProgram(
|
||||
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
|
||||
const std::vector<GLuint> sourceHistoryTextures = mRenderer.TemporalHistory().ResolveSourceHistoryTextures(sourceTexture, state.isTemporal ? historyCap : 0);
|
||||
const std::vector<GLuint> temporalHistoryTextures = mRenderer.TemporalHistory().ResolveTemporalHistoryTextures(state, sourceTexture, state.isTemporal ? historyCap : 0);
|
||||
const GLuint feedbackTexture = mRenderer.FeedbackBuffers().ResolveReadTexture(state);
|
||||
const ShaderTextureBindings::RuntimeTextureBindingPlan texturePlan =
|
||||
mTextureBindings.BuildLayerRuntimeBindingPlan(passProgram, sourceTexture, sourceHistoryTextures, temporalHistoryTextures);
|
||||
mTextureBindings.BuildLayerRuntimeBindingPlan(passProgram, sourceTexture, state, feedbackTexture, sourceHistoryTextures, temporalHistoryTextures);
|
||||
mTextureBindings.BindRuntimeTexturePlan(texturePlan);
|
||||
glBindVertexArray(mRenderer.FullscreenVertexArray());
|
||||
glUseProgram(passProgram.program);
|
||||
// The UBO is shared by every pass in a layer; texture routing is what
|
||||
// changes from pass to pass.
|
||||
updateGlobalParams(state, mRenderer.TemporalHistory().SourceAvailableCount(), mRenderer.TemporalHistory().AvailableCountForLayer(state.layerId));
|
||||
updateGlobalParams(
|
||||
state,
|
||||
mRenderer.TemporalHistory().SourceAvailableCount(),
|
||||
mRenderer.TemporalHistory().AvailableCountForLayer(state.layerId),
|
||||
mRenderer.FeedbackBuffers().FeedbackAvailable(state));
|
||||
glDrawArrays(GL_TRIANGLES, 0, 3);
|
||||
glUseProgram(0);
|
||||
glBindVertexArray(0);
|
||||
|
||||
@@ -16,7 +16,7 @@ public:
|
||||
using LayerProgram = OpenGLRenderer::LayerProgram;
|
||||
using PassProgram = OpenGLRenderer::LayerProgram::PassProgram;
|
||||
using TextBindingUpdater = std::function<bool(const RuntimeRenderState&, LayerProgram::TextBinding&, std::string&)>;
|
||||
using GlobalParamsUpdater = std::function<bool(const RuntimeRenderState&, unsigned, unsigned)>;
|
||||
using GlobalParamsUpdater = std::function<bool(const RuntimeRenderState&, unsigned, unsigned, bool)>;
|
||||
|
||||
explicit OpenGLRenderPass(OpenGLRenderer& renderer);
|
||||
|
||||
|
||||
@@ -36,4 +36,5 @@ struct RenderPassDescriptor
|
||||
OpenGLRenderer::LayerProgram::PassProgram* passProgram = nullptr;
|
||||
const RuntimeRenderState* layerState = nullptr;
|
||||
bool capturePreLayerHistory = false;
|
||||
bool captureFeedbackWrite = false;
|
||||
};
|
||||
|
||||
@@ -0,0 +1,202 @@
|
||||
#include "ShaderFeedbackBuffers.h"
|
||||
|
||||
#include <set>
|
||||
|
||||
namespace
|
||||
{
|
||||
void ConfigureFeedbackTexture(unsigned frameWidth, unsigned frameHeight)
|
||||
{
|
||||
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_RGBA16F, frameWidth, frameHeight, 0, GL_RGBA, GL_FLOAT, NULL);
|
||||
}
|
||||
}
|
||||
|
||||
bool ShaderFeedbackBuffers::EnsureResources(const std::vector<RuntimeRenderState>& layerStates, unsigned frameWidth, unsigned frameHeight, std::string& error)
|
||||
{
|
||||
if (!EnsureZeroTexture())
|
||||
{
|
||||
error = "Failed to initialize shader feedback fallback texture.";
|
||||
return false;
|
||||
}
|
||||
|
||||
std::set<std::string> requiredLayerIds;
|
||||
for (const RuntimeRenderState& state : layerStates)
|
||||
{
|
||||
if (!state.feedback.enabled)
|
||||
continue;
|
||||
|
||||
requiredLayerIds.insert(state.layerId);
|
||||
auto surfaceIt = mSurfacesByLayerId.find(state.layerId);
|
||||
if (surfaceIt == mSurfacesByLayerId.end() ||
|
||||
surfaceIt->second.width != frameWidth ||
|
||||
surfaceIt->second.height != frameHeight)
|
||||
{
|
||||
Surface replacement;
|
||||
if (!CreateSurface(replacement, frameWidth, frameHeight, error))
|
||||
return false;
|
||||
mSurfacesByLayerId[state.layerId] = std::move(replacement);
|
||||
}
|
||||
}
|
||||
|
||||
for (auto it = mSurfacesByLayerId.begin(); it != mSurfacesByLayerId.end();)
|
||||
{
|
||||
if (requiredLayerIds.find(it->first) == requiredLayerIds.end())
|
||||
{
|
||||
DestroySurface(it->second);
|
||||
it = mSurfacesByLayerId.erase(it);
|
||||
}
|
||||
else
|
||||
{
|
||||
++it;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void ShaderFeedbackBuffers::DestroyResources()
|
||||
{
|
||||
for (auto& entry : mSurfacesByLayerId)
|
||||
DestroySurface(entry.second);
|
||||
mSurfacesByLayerId.clear();
|
||||
|
||||
if (mZeroTexture != 0)
|
||||
{
|
||||
glDeleteTextures(1, &mZeroTexture);
|
||||
mZeroTexture = 0;
|
||||
}
|
||||
}
|
||||
|
||||
void ShaderFeedbackBuffers::ResetState()
|
||||
{
|
||||
for (auto& entry : mSurfacesByLayerId)
|
||||
ClearSurfaceState(entry.second);
|
||||
}
|
||||
|
||||
GLuint ShaderFeedbackBuffers::ResolveReadTexture(const RuntimeRenderState& state) const
|
||||
{
|
||||
if (!state.feedback.enabled)
|
||||
return mZeroTexture;
|
||||
|
||||
auto surfaceIt = mSurfacesByLayerId.find(state.layerId);
|
||||
if (surfaceIt == mSurfacesByLayerId.end() || !surfaceIt->second.hasData)
|
||||
return mZeroTexture;
|
||||
|
||||
return surfaceIt->second.slots[surfaceIt->second.readIndex].texture != 0
|
||||
? surfaceIt->second.slots[surfaceIt->second.readIndex].texture
|
||||
: mZeroTexture;
|
||||
}
|
||||
|
||||
bool ShaderFeedbackBuffers::FeedbackAvailable(const RuntimeRenderState& state) const
|
||||
{
|
||||
if (!state.feedback.enabled)
|
||||
return false;
|
||||
|
||||
auto surfaceIt = mSurfacesByLayerId.find(state.layerId);
|
||||
return surfaceIt != mSurfacesByLayerId.end() && surfaceIt->second.hasData;
|
||||
}
|
||||
|
||||
void ShaderFeedbackBuffers::CaptureFeedbackFramebuffer(const std::string& layerId, GLuint sourceFramebuffer, unsigned frameWidth, unsigned frameHeight)
|
||||
{
|
||||
auto surfaceIt = mSurfacesByLayerId.find(layerId);
|
||||
if (surfaceIt == mSurfacesByLayerId.end())
|
||||
return;
|
||||
|
||||
Surface& surface = surfaceIt->second;
|
||||
const unsigned writeIndex = 1u - surface.readIndex;
|
||||
glBindFramebuffer(GL_READ_FRAMEBUFFER, sourceFramebuffer);
|
||||
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, surface.slots[writeIndex].framebuffer);
|
||||
glBlitFramebuffer(0, 0, frameWidth, frameHeight, 0, 0, frameWidth, frameHeight, GL_COLOR_BUFFER_BIT, GL_LINEAR);
|
||||
surface.pendingWrite = true;
|
||||
}
|
||||
|
||||
void ShaderFeedbackBuffers::FinalizeFrame()
|
||||
{
|
||||
for (auto& entry : mSurfacesByLayerId)
|
||||
{
|
||||
Surface& surface = entry.second;
|
||||
if (!surface.pendingWrite)
|
||||
continue;
|
||||
|
||||
surface.readIndex = 1u - surface.readIndex;
|
||||
surface.hasData = true;
|
||||
surface.pendingWrite = false;
|
||||
}
|
||||
}
|
||||
|
||||
bool ShaderFeedbackBuffers::EnsureZeroTexture()
|
||||
{
|
||||
if (mZeroTexture != 0)
|
||||
return true;
|
||||
|
||||
glGenTextures(1, &mZeroTexture);
|
||||
glBindTexture(GL_TEXTURE_2D, mZeroTexture);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
|
||||
const float zeroPixel[4] = { 0.0f, 0.0f, 0.0f, 0.0f };
|
||||
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA16F, 1, 1, 0, GL_RGBA, GL_FLOAT, zeroPixel);
|
||||
glBindTexture(GL_TEXTURE_2D, 0);
|
||||
return mZeroTexture != 0;
|
||||
}
|
||||
|
||||
bool ShaderFeedbackBuffers::CreateSurface(Surface& surface, unsigned frameWidth, unsigned frameHeight, std::string& error)
|
||||
{
|
||||
DestroySurface(surface);
|
||||
|
||||
surface.width = frameWidth;
|
||||
surface.height = frameHeight;
|
||||
for (Slot& slot : surface.slots)
|
||||
{
|
||||
glGenTextures(1, &slot.texture);
|
||||
glBindTexture(GL_TEXTURE_2D, slot.texture);
|
||||
ConfigureFeedbackTexture(frameWidth, frameHeight);
|
||||
|
||||
glGenFramebuffers(1, &slot.framebuffer);
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, slot.framebuffer);
|
||||
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, slot.texture, 0);
|
||||
if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE)
|
||||
{
|
||||
error = "Failed to initialize a shader feedback framebuffer.";
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||
glBindTexture(GL_TEXTURE_2D, 0);
|
||||
DestroySurface(surface);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||
glBindTexture(GL_TEXTURE_2D, 0);
|
||||
ClearSurfaceState(surface);
|
||||
return true;
|
||||
}
|
||||
|
||||
void ShaderFeedbackBuffers::DestroySurface(Surface& surface)
|
||||
{
|
||||
for (Slot& slot : surface.slots)
|
||||
{
|
||||
if (slot.framebuffer != 0)
|
||||
glDeleteFramebuffers(1, &slot.framebuffer);
|
||||
if (slot.texture != 0)
|
||||
glDeleteTextures(1, &slot.texture);
|
||||
slot.framebuffer = 0;
|
||||
slot.texture = 0;
|
||||
}
|
||||
|
||||
surface.width = 0;
|
||||
surface.height = 0;
|
||||
surface.readIndex = 0;
|
||||
surface.hasData = false;
|
||||
surface.pendingWrite = false;
|
||||
}
|
||||
|
||||
void ShaderFeedbackBuffers::ClearSurfaceState(Surface& surface)
|
||||
{
|
||||
surface.readIndex = 0;
|
||||
surface.hasData = false;
|
||||
surface.pendingWrite = false;
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
#pragma once
|
||||
|
||||
#include "GLExtensions.h"
|
||||
#include "ShaderTypes.h"
|
||||
|
||||
#include <map>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
class ShaderFeedbackBuffers
|
||||
{
|
||||
public:
|
||||
struct Slot
|
||||
{
|
||||
GLuint texture = 0;
|
||||
GLuint framebuffer = 0;
|
||||
};
|
||||
|
||||
struct Surface
|
||||
{
|
||||
Slot slots[2];
|
||||
unsigned width = 0;
|
||||
unsigned height = 0;
|
||||
unsigned readIndex = 0;
|
||||
bool hasData = false;
|
||||
bool pendingWrite = false;
|
||||
};
|
||||
|
||||
bool EnsureResources(const std::vector<RuntimeRenderState>& layerStates, unsigned frameWidth, unsigned frameHeight, std::string& error);
|
||||
void DestroyResources();
|
||||
void ResetState();
|
||||
GLuint ResolveReadTexture(const RuntimeRenderState& state) const;
|
||||
bool FeedbackAvailable(const RuntimeRenderState& state) const;
|
||||
void CaptureFeedbackFramebuffer(const std::string& layerId, GLuint sourceFramebuffer, unsigned frameWidth, unsigned frameHeight);
|
||||
void FinalizeFrame();
|
||||
|
||||
private:
|
||||
bool EnsureZeroTexture();
|
||||
bool CreateSurface(Surface& surface, unsigned frameWidth, unsigned frameHeight, std::string& error);
|
||||
void DestroySurface(Surface& surface);
|
||||
void ClearSurfaceState(Surface& surface);
|
||||
|
||||
private:
|
||||
std::map<std::string, Surface> mSurfacesByLayerId;
|
||||
GLuint mZeroTexture = 0;
|
||||
};
|
||||
@@ -19,7 +19,8 @@ bool TemporalHistoryBuffers::ValidateTextureUnitBudget(const std::vector<Runtime
|
||||
++textTextureCount;
|
||||
}
|
||||
const unsigned totalShaderTextures = static_cast<unsigned>(state.textureAssets.size()) + textTextureCount;
|
||||
const unsigned layerRequiredUnits = kSourceHistoryTextureUnitBase + (state.isTemporal ? historyCap + historyCap : 0u) + totalShaderTextures;
|
||||
const unsigned feedbackTextureCount = state.feedback.enabled ? 1u : 0u;
|
||||
const unsigned layerRequiredUnits = kSourceHistoryTextureUnitBase + (state.isTemporal ? historyCap + historyCap : 0u) + feedbackTextureCount + totalShaderTextures;
|
||||
if (layerRequiredUnits > requiredUnits)
|
||||
requiredUnits = layerRequiredUnits;
|
||||
}
|
||||
|
||||
@@ -63,6 +63,7 @@ bool OpenGLRenderer::InitializeResources(unsigned inputFrameWidth, unsigned inpu
|
||||
glBindBufferBase(GL_UNIFORM_BUFFER, kGlobalParamsBindingPoint, mGlobalParamsUBO);
|
||||
glBindBuffer(GL_UNIFORM_BUFFER, 0);
|
||||
|
||||
mResourcesInitialized = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -157,8 +158,10 @@ void OpenGLRenderer::DestroyResources()
|
||||
mCaptureTexture = 0;
|
||||
mTextureUploadBuffer = 0;
|
||||
mGlobalParamsUBOSize = 0;
|
||||
mResourcesInitialized = false;
|
||||
|
||||
mTemporalHistory.DestroyResources();
|
||||
mFeedbackBuffers.DestroyResources();
|
||||
DestroyLayerPrograms();
|
||||
DestroyDecodeShaderProgram();
|
||||
DestroyOutputPackShaderProgram();
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
#include "GLExtensions.h"
|
||||
#include "RenderTargetPool.h"
|
||||
#include "ShaderFeedbackBuffers.h"
|
||||
#include "ShaderTypes.h"
|
||||
#include "TemporalHistoryBuffers.h"
|
||||
|
||||
@@ -78,6 +79,7 @@ public:
|
||||
GLint OutputPackFormatLocation() const { return mOutputPackFormatLocation; }
|
||||
GLsizeiptr GlobalParamsUBOSize() const { return mGlobalParamsUBOSize; }
|
||||
void SetGlobalParamsUBOSize(GLsizeiptr size) { mGlobalParamsUBOSize = size; }
|
||||
bool ResourcesInitialized() const { return mResourcesInitialized; }
|
||||
void ReplaceLayerPrograms(std::vector<LayerProgram>& newPrograms) { mLayerPrograms.swap(newPrograms); }
|
||||
std::vector<LayerProgram>& LayerPrograms() { return mLayerPrograms; }
|
||||
const std::vector<LayerProgram>& LayerPrograms() const { return mLayerPrograms; }
|
||||
@@ -86,6 +88,8 @@ public:
|
||||
std::size_t TemporaryRenderTargetCount() const { return mRenderTargets.TemporaryTargetCount(); }
|
||||
TemporalHistoryBuffers& TemporalHistory() { return mTemporalHistory; }
|
||||
const TemporalHistoryBuffers& TemporalHistory() const { return mTemporalHistory; }
|
||||
ShaderFeedbackBuffers& FeedbackBuffers() { return mFeedbackBuffers; }
|
||||
const ShaderFeedbackBuffers& FeedbackBuffers() const { return mFeedbackBuffers; }
|
||||
void SetDecodeShaderProgram(GLuint program, GLuint vertexShader, GLuint fragmentShader);
|
||||
void SetOutputPackShaderProgram(GLuint program, GLuint vertexShader, GLuint fragmentShader);
|
||||
bool InitializeResources(unsigned inputFrameWidth, unsigned inputFrameHeight, unsigned captureTextureWidth, unsigned outputFrameWidth, unsigned outputFrameHeight, unsigned outputPackTextureWidth, std::string& error);
|
||||
@@ -117,9 +121,11 @@ private:
|
||||
GLint mOutputPackActiveWordsLocation = -1;
|
||||
GLint mOutputPackFormatLocation = -1;
|
||||
GLsizeiptr mGlobalParamsUBOSize = 0;
|
||||
bool mResourcesInitialized = false;
|
||||
int mViewWidth = 0;
|
||||
int mViewHeight = 0;
|
||||
std::vector<LayerProgram> mLayerPrograms;
|
||||
RenderTargetPool mRenderTargets;
|
||||
TemporalHistoryBuffers mTemporalHistory;
|
||||
ShaderFeedbackBuffers mFeedbackBuffers;
|
||||
};
|
||||
|
||||
@@ -10,7 +10,7 @@ GlobalParamsBuffer::GlobalParamsBuffer(OpenGLRenderer& renderer) :
|
||||
{
|
||||
}
|
||||
|
||||
bool GlobalParamsBuffer::Update(const RuntimeRenderState& state, unsigned availableSourceHistoryLength, unsigned availableTemporalHistoryLength)
|
||||
bool GlobalParamsBuffer::Update(const RuntimeRenderState& state, unsigned availableSourceHistoryLength, unsigned availableTemporalHistoryLength, bool feedbackAvailable)
|
||||
{
|
||||
std::vector<unsigned char>& buffer = mScratchBuffer;
|
||||
buffer.clear();
|
||||
@@ -33,6 +33,7 @@ bool GlobalParamsBuffer::Update(const RuntimeRenderState& state, unsigned availa
|
||||
: 0u;
|
||||
AppendStd140Int(buffer, static_cast<int>(effectiveSourceHistoryLength));
|
||||
AppendStd140Int(buffer, static_cast<int>(effectiveTemporalHistoryLength));
|
||||
AppendStd140Int(buffer, feedbackAvailable ? 1 : 0);
|
||||
|
||||
for (const ShaderParameterDefinition& definition : state.parameterDefinitions)
|
||||
{
|
||||
|
||||
@@ -10,7 +10,7 @@ class GlobalParamsBuffer
|
||||
public:
|
||||
explicit GlobalParamsBuffer(OpenGLRenderer& renderer);
|
||||
|
||||
bool Update(const RuntimeRenderState& state, unsigned availableSourceHistoryLength, unsigned availableTemporalHistoryLength);
|
||||
bool Update(const RuntimeRenderState& state, unsigned availableSourceHistoryLength, unsigned availableTemporalHistoryLength, bool feedbackAvailable);
|
||||
|
||||
private:
|
||||
OpenGLRenderer& mRenderer;
|
||||
|
||||
@@ -52,6 +52,12 @@ bool OpenGLShaderPrograms::CompileLayerPrograms(unsigned inputFrameWidth, unsign
|
||||
CopyErrorMessage(temporalError, errorMessageSize, errorMessage);
|
||||
return false;
|
||||
}
|
||||
if (mRenderer.ResourcesInitialized() &&
|
||||
!mRenderer.FeedbackBuffers().EnsureResources(layerStates, inputFrameWidth, inputFrameHeight, temporalError))
|
||||
{
|
||||
CopyErrorMessage(temporalError, errorMessageSize, errorMessage);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Initial startup still compiles synchronously; auto-reload uses the build
|
||||
// queue so Slang/file work stays off the playback path.
|
||||
@@ -109,6 +115,12 @@ bool OpenGLShaderPrograms::CommitPreparedLayerPrograms(const PreparedShaderBuild
|
||||
CopyErrorMessage(temporalError, errorMessageSize, errorMessage);
|
||||
return false;
|
||||
}
|
||||
if (mRenderer.ResourcesInitialized() &&
|
||||
!mRenderer.FeedbackBuffers().EnsureResources(preparedBuild.layerStates, inputFrameWidth, inputFrameHeight, temporalError))
|
||||
{
|
||||
CopyErrorMessage(temporalError, errorMessageSize, errorMessage);
|
||||
return false;
|
||||
}
|
||||
|
||||
// The prepared build already contains GLSL text for each pass. This commit
|
||||
// step performs the short GL work on the render thread.
|
||||
@@ -176,12 +188,17 @@ void OpenGLShaderPrograms::ResetTemporalHistoryState()
|
||||
mRenderer.TemporalHistory().ResetState();
|
||||
}
|
||||
|
||||
void OpenGLShaderPrograms::ResetShaderFeedbackState()
|
||||
{
|
||||
mRenderer.FeedbackBuffers().ResetState();
|
||||
}
|
||||
|
||||
bool OpenGLShaderPrograms::UpdateTextBindingTexture(const RuntimeRenderState& state, LayerProgram::TextBinding& textBinding, std::string& error)
|
||||
{
|
||||
return mTextureBindings.UpdateTextBindingTexture(state, textBinding, error);
|
||||
}
|
||||
|
||||
bool OpenGLShaderPrograms::UpdateGlobalParamsBuffer(const RuntimeRenderState& state, unsigned availableSourceHistoryLength, unsigned availableTemporalHistoryLength)
|
||||
bool OpenGLShaderPrograms::UpdateGlobalParamsBuffer(const RuntimeRenderState& state, unsigned availableSourceHistoryLength, unsigned availableTemporalHistoryLength, bool feedbackAvailable)
|
||||
{
|
||||
return mGlobalParamsBuffer.Update(state, availableSourceHistoryLength, availableTemporalHistoryLength);
|
||||
return mGlobalParamsBuffer.Update(state, availableSourceHistoryLength, availableTemporalHistoryLength, feedbackAvailable);
|
||||
}
|
||||
|
||||
@@ -25,9 +25,10 @@ public:
|
||||
void DestroySingleLayerProgram(LayerProgram& layerProgram);
|
||||
void DestroyDecodeShaderProgram();
|
||||
void ResetTemporalHistoryState();
|
||||
void ResetShaderFeedbackState();
|
||||
const std::vector<RuntimeRenderState>& CommittedLayerStates() const { return mCommittedLayerStates; }
|
||||
bool UpdateTextBindingTexture(const RuntimeRenderState& state, LayerProgram::TextBinding& textBinding, std::string& error);
|
||||
bool UpdateGlobalParamsBuffer(const RuntimeRenderState& state, unsigned availableSourceHistoryLength, unsigned availableTemporalHistoryLength);
|
||||
bool UpdateGlobalParamsBuffer(const RuntimeRenderState& state, unsigned availableSourceHistoryLength, unsigned availableTemporalHistoryLength, bool feedbackAvailable);
|
||||
|
||||
private:
|
||||
OpenGLRenderer& mRenderer;
|
||||
|
||||
@@ -103,11 +103,16 @@ GLint ShaderTextureBindings::FindSamplerUniformLocation(GLuint program, const st
|
||||
return glGetUniformLocation(program, (samplerName + "_0").c_str());
|
||||
}
|
||||
|
||||
GLuint ShaderTextureBindings::ResolveShaderTextureBase(const RuntimeRenderState& state, unsigned historyCap) const
|
||||
GLuint ShaderTextureBindings::ResolveFeedbackTextureUnit(const RuntimeRenderState& state, unsigned historyCap) const
|
||||
{
|
||||
return state.isTemporal ? kSourceHistoryTextureUnitBase + historyCap + historyCap : kSourceHistoryTextureUnitBase;
|
||||
}
|
||||
|
||||
GLuint ShaderTextureBindings::ResolveShaderTextureBase(const RuntimeRenderState& state, unsigned historyCap) const
|
||||
{
|
||||
return ResolveFeedbackTextureUnit(state, historyCap) + (state.feedback.enabled ? 1u : 0u);
|
||||
}
|
||||
|
||||
void ShaderTextureBindings::AssignLayerSamplerUniforms(GLuint program, const RuntimeRenderState& state, const PassProgram& passProgram, unsigned historyCap) const
|
||||
{
|
||||
const GLuint shaderTextureBase = ResolveShaderTextureBase(state, historyCap);
|
||||
@@ -129,6 +134,13 @@ void ShaderTextureBindings::AssignLayerSamplerUniforms(GLuint program, const Run
|
||||
glUniform1i(temporalSamplerLocation, static_cast<GLint>(kSourceHistoryTextureUnitBase + historyCap + index));
|
||||
}
|
||||
|
||||
if (state.feedback.enabled)
|
||||
{
|
||||
const GLint feedbackSamplerLocation = glGetUniformLocation(program, "gFeedbackState");
|
||||
if (feedbackSamplerLocation >= 0)
|
||||
glUniform1i(feedbackSamplerLocation, static_cast<GLint>(ResolveFeedbackTextureUnit(state, historyCap)));
|
||||
}
|
||||
|
||||
for (std::size_t index = 0; index < passProgram.textureBindings.size(); ++index)
|
||||
{
|
||||
const GLint textureSamplerLocation = FindSamplerUniformLocation(program, passProgram.textureBindings[index].samplerName);
|
||||
@@ -148,6 +160,8 @@ void ShaderTextureBindings::AssignLayerSamplerUniforms(GLuint program, const Run
|
||||
ShaderTextureBindings::RuntimeTextureBindingPlan ShaderTextureBindings::BuildLayerRuntimeBindingPlan(
|
||||
const PassProgram& passProgram,
|
||||
GLuint layerInputTexture,
|
||||
const RuntimeRenderState& state,
|
||||
GLuint feedbackTexture,
|
||||
const std::vector<GLuint>& sourceHistoryTextures,
|
||||
const std::vector<GLuint>& temporalHistoryTextures) const
|
||||
{
|
||||
@@ -175,7 +189,20 @@ ShaderTextureBindings::RuntimeTextureBindingPlan ShaderTextureBindings::BuildLay
|
||||
});
|
||||
}
|
||||
|
||||
const GLuint shaderTextureBase = passProgram.shaderTextureBase != 0 ? passProgram.shaderTextureBase : kSourceHistoryTextureUnitBase;
|
||||
const GLuint feedbackTextureUnit = ResolveFeedbackTextureUnit(state, static_cast<unsigned>(sourceHistoryTextures.size()));
|
||||
if (state.feedback.enabled)
|
||||
{
|
||||
plan.bindings.push_back({
|
||||
"feedbackState",
|
||||
"gFeedbackState",
|
||||
feedbackTexture,
|
||||
feedbackTextureUnit
|
||||
});
|
||||
}
|
||||
|
||||
const GLuint shaderTextureBase = passProgram.shaderTextureBase != 0
|
||||
? passProgram.shaderTextureBase
|
||||
: feedbackTextureUnit + (state.feedback.enabled ? 1u : 0u);
|
||||
for (std::size_t index = 0; index < passProgram.textureBindings.size(); ++index)
|
||||
{
|
||||
const LayerProgram::TextureBinding& textureBinding = passProgram.textureBindings[index];
|
||||
|
||||
@@ -29,11 +29,14 @@ public:
|
||||
void CreateTextBindings(const RuntimeRenderState& state, std::vector<LayerProgram::TextBinding>& textBindings);
|
||||
bool UpdateTextBindingTexture(const RuntimeRenderState& state, LayerProgram::TextBinding& textBinding, std::string& error);
|
||||
GLint FindSamplerUniformLocation(GLuint program, const std::string& samplerName) const;
|
||||
GLuint ResolveFeedbackTextureUnit(const RuntimeRenderState& state, unsigned historyCap) const;
|
||||
GLuint ResolveShaderTextureBase(const RuntimeRenderState& state, unsigned historyCap) const;
|
||||
void AssignLayerSamplerUniforms(GLuint program, const RuntimeRenderState& state, const PassProgram& passProgram, unsigned historyCap) const;
|
||||
RuntimeTextureBindingPlan BuildLayerRuntimeBindingPlan(
|
||||
const PassProgram& passProgram,
|
||||
GLuint layerInputTexture,
|
||||
const RuntimeRenderState& state,
|
||||
GLuint feedbackTexture,
|
||||
const std::vector<GLuint>& sourceHistoryTextures,
|
||||
const std::vector<GLuint>& temporalHistoryTextures) const;
|
||||
void BindRuntimeTexturePlan(const RuntimeTextureBindingPlan& plan) const;
|
||||
|
||||
@@ -1620,6 +1620,7 @@ void RuntimeHost::BuildLayerRenderStatesLocked(unsigned outputWidth, unsigned ou
|
||||
state.temporalHistorySource = shaderIt->second.temporal.historySource;
|
||||
state.requestedTemporalHistoryLength = shaderIt->second.temporal.requestedHistoryLength;
|
||||
state.effectiveTemporalHistoryLength = shaderIt->second.temporal.effectiveHistoryLength;
|
||||
state.feedback = shaderIt->second.feedback;
|
||||
|
||||
for (const ShaderParameterDefinition& definition : shaderIt->second.parameters)
|
||||
{
|
||||
@@ -2147,6 +2148,13 @@ JsonValue RuntimeHost::BuildStateValue() const
|
||||
temporal.set("effectiveHistoryLength", JsonValue(static_cast<double>(shaderIt->second.temporal.effectiveHistoryLength)));
|
||||
shader.set("temporal", temporal);
|
||||
}
|
||||
if (status.available && shaderIt != mPackagesById.end() && shaderIt->second.feedback.enabled)
|
||||
{
|
||||
JsonValue feedback = JsonValue::MakeObject();
|
||||
feedback.set("enabled", JsonValue(true));
|
||||
feedback.set("writePass", JsonValue(shaderIt->second.feedback.writePassId));
|
||||
shader.set("feedback", feedback);
|
||||
}
|
||||
shaderLibrary.pushBack(shader);
|
||||
}
|
||||
root.set("shaders", shaderLibrary);
|
||||
@@ -2184,6 +2192,13 @@ JsonValue RuntimeHost::SerializeLayerStackLocked() const
|
||||
temporal.set("effectiveHistoryLength", JsonValue(static_cast<double>(shaderIt->second.temporal.effectiveHistoryLength)));
|
||||
layerValue.set("temporal", temporal);
|
||||
}
|
||||
if (shaderIt->second.feedback.enabled)
|
||||
{
|
||||
JsonValue feedback = JsonValue::MakeObject();
|
||||
feedback.set("enabled", JsonValue(true));
|
||||
feedback.set("writePass", JsonValue(shaderIt->second.feedback.writePassId));
|
||||
layerValue.set("feedback", feedback);
|
||||
}
|
||||
|
||||
JsonValue parameters = JsonValue::MakeArray();
|
||||
for (const ShaderParameterDefinition& definition : shaderIt->second.parameters)
|
||||
|
||||
@@ -178,6 +178,11 @@ bool ShaderCompiler::BuildWrapperSlangSource(const ShaderPackage& shaderPackage,
|
||||
const unsigned historySamplerCount = shaderPackage.temporal.enabled ? mMaxTemporalHistoryFrames : 0;
|
||||
wrapperSource = ReplaceAll(wrapperSource, "{{SOURCE_HISTORY_SAMPLERS}}", BuildHistorySamplerDeclarations("gSourceHistory", historySamplerCount));
|
||||
wrapperSource = ReplaceAll(wrapperSource, "{{TEMPORAL_HISTORY_SAMPLERS}}", BuildHistorySamplerDeclarations("gTemporalHistory", historySamplerCount));
|
||||
wrapperSource = ReplaceAll(wrapperSource, "{{FEEDBACK_SAMPLER}}", shaderPackage.feedback.enabled ? "Sampler2D<float4> gFeedbackState;\n" : "");
|
||||
wrapperSource = ReplaceAll(wrapperSource, "{{FEEDBACK_HELPER}}",
|
||||
shaderPackage.feedback.enabled
|
||||
? "float4 sampleFeedback(float2 tc)\n{\n\tif (gFeedbackAvailable <= 0)\n\t\treturn float4(0.0, 0.0, 0.0, 0.0);\n\treturn gFeedbackState.Sample(tc);\n}\n"
|
||||
: "float4 sampleFeedback(float2 tc)\n{\n\treturn float4(0.0, 0.0, 0.0, 0.0);\n}\n");
|
||||
wrapperSource = ReplaceAll(wrapperSource, "{{TEXTURE_SAMPLERS}}", BuildTextureSamplerDeclarations(shaderPackage.textureAssets));
|
||||
wrapperSource = ReplaceAll(wrapperSource, "{{TEXT_SAMPLERS}}", BuildTextSamplerDeclarations(shaderPackage.parameters));
|
||||
wrapperSource = ReplaceAll(wrapperSource, "{{TEXT_HELPERS}}", BuildTextHelpers(shaderPackage.parameters));
|
||||
|
||||
@@ -473,6 +473,46 @@ bool ParseTemporalSettings(const JsonValue& manifestJson, ShaderPackage& shaderP
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ParseFeedbackSettings(const JsonValue& manifestJson, ShaderPackage& shaderPackage, const std::filesystem::path& manifestPath, std::string& error)
|
||||
{
|
||||
const JsonValue* feedbackValue = nullptr;
|
||||
if (!OptionalObjectField(manifestJson, "feedback", feedbackValue, manifestPath, error))
|
||||
return false;
|
||||
if (!feedbackValue)
|
||||
return true;
|
||||
|
||||
const JsonValue* enabledValue = feedbackValue->find("enabled");
|
||||
if (!enabledValue || !enabledValue->asBoolean(false))
|
||||
return true;
|
||||
|
||||
shaderPackage.feedback.enabled = true;
|
||||
if (!OptionalStringField(*feedbackValue, "writePass", shaderPackage.feedback.writePassId, "", manifestPath, error))
|
||||
return false;
|
||||
|
||||
if (shaderPackage.feedback.writePassId.empty())
|
||||
{
|
||||
if (shaderPackage.passes.empty())
|
||||
{
|
||||
error = "Feedback-enabled shader has no passes to target in: " + ManifestPathMessage(manifestPath);
|
||||
return false;
|
||||
}
|
||||
shaderPackage.feedback.writePassId = shaderPackage.passes.back().id;
|
||||
}
|
||||
|
||||
if (!ValidateShaderIdentifier(shaderPackage.feedback.writePassId, "feedback.writePass", manifestPath, error))
|
||||
return false;
|
||||
|
||||
const auto passIt = std::find_if(shaderPackage.passes.begin(), shaderPackage.passes.end(),
|
||||
[&shaderPackage](const ShaderPassDefinition& pass) { return pass.id == shaderPackage.feedback.writePassId; });
|
||||
if (passIt == shaderPackage.passes.end())
|
||||
{
|
||||
error = "Feedback writePass '" + shaderPackage.feedback.writePassId + "' does not match any declared pass in: " + ManifestPathMessage(manifestPath);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ParseParameterNumberField(const JsonValue& parameterJson, const char* fieldName, std::vector<double>& values, const std::filesystem::path& manifestPath, std::string& error)
|
||||
{
|
||||
if (const JsonValue* fieldValue = parameterJson.find(fieldName))
|
||||
@@ -773,5 +813,6 @@ bool ShaderPackageRegistry::ParseManifest(const std::filesystem::path& manifestP
|
||||
return ParseTextureAssets(manifestJson, shaderPackage, manifestPath, error) &&
|
||||
ParseFontAssets(manifestJson, shaderPackage, manifestPath, error) &&
|
||||
ParseTemporalSettings(manifestJson, shaderPackage, mMaxTemporalHistoryFrames, manifestPath, error) &&
|
||||
ParseFeedbackSettings(manifestJson, shaderPackage, manifestPath, error) &&
|
||||
ParseParameterDefinitions(manifestJson, shaderPackage, manifestPath, error);
|
||||
}
|
||||
|
||||
@@ -63,6 +63,12 @@ struct TemporalSettings
|
||||
unsigned effectiveHistoryLength = 0;
|
||||
};
|
||||
|
||||
struct FeedbackSettings
|
||||
{
|
||||
bool enabled = false;
|
||||
std::string writePassId;
|
||||
};
|
||||
|
||||
struct ShaderTextureAsset
|
||||
{
|
||||
std::string id;
|
||||
@@ -110,6 +116,7 @@ struct ShaderPackage
|
||||
std::vector<ShaderTextureAsset> textureAssets;
|
||||
std::vector<ShaderFontAsset> fontAssets;
|
||||
TemporalSettings temporal;
|
||||
FeedbackSettings feedback;
|
||||
std::filesystem::file_time_type shaderWriteTime;
|
||||
std::filesystem::file_time_type manifestWriteTime;
|
||||
};
|
||||
@@ -148,4 +155,5 @@ struct RuntimeRenderState
|
||||
TemporalHistorySource temporalHistorySource = TemporalHistorySource::None;
|
||||
unsigned requestedTemporalHistoryLength = 0;
|
||||
unsigned effectiveTemporalHistoryLength = 0;
|
||||
FeedbackSettings feedback;
|
||||
};
|
||||
|
||||
456
docs/ARCHITECTURE_RESILIENCE_REVIEW.md
Normal file
456
docs/ARCHITECTURE_RESILIENCE_REVIEW.md
Normal file
@@ -0,0 +1,456 @@
|
||||
# Architecture Resilience Review
|
||||
|
||||
This note summarizes the main architectural improvements that would make the app more resilient during live use, especially around timing isolation, failure isolation, and recoverability.
|
||||
|
||||
## Key Findings
|
||||
|
||||
### 1. `RuntimeHost` is carrying too many responsibilities
|
||||
|
||||
`RuntimeHost` currently acts as:
|
||||
|
||||
- config store
|
||||
- persistent state store
|
||||
- live parameter/state authority
|
||||
- shader package registry owner
|
||||
- status/telemetry sink
|
||||
- control mutation entrypoint
|
||||
|
||||
That makes it a single contention and failure domain. It is also why OSC and render timing issues repeatedly surfaced around shared state access.
|
||||
|
||||
Relevant code:
|
||||
|
||||
- [RuntimeHost.h](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/runtime/RuntimeHost.h:15)
|
||||
|
||||
Recommended direction:
|
||||
|
||||
- split persisted config/state from live render-facing state
|
||||
- separate status/telemetry updates from control mutation paths
|
||||
- make render consume snapshots rather than sharing a large mutable authority object
|
||||
|
||||
### 2. OpenGL ownership is still centralized behind one shared lock
|
||||
|
||||
Even after recent timing improvements, preview, input upload, and playout rendering still rely on one shared GL context protected by one `CRITICAL_SECTION`.
|
||||
|
||||
Relevant code:
|
||||
|
||||
- [OpenGLComposite.h](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/gl/OpenGLComposite.h:93)
|
||||
- [OpenGLComposite.cpp](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/gl/OpenGLComposite.cpp:253)
|
||||
- [OpenGLVideoIOBridge.cpp](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/gl/pipeline/OpenGLVideoIOBridge.cpp:70)
|
||||
|
||||
This is still a central choke point and limits timing isolation.
|
||||
|
||||
Recommended direction:
|
||||
|
||||
- use one dedicated render thread as the sole GL owner
|
||||
- have input/output/control threads queue work instead of performing GL work directly
|
||||
- remove ad hoc GL use from callback threads
|
||||
|
||||
### 3. Control flow is spread across polling and shared-memory patterns
|
||||
|
||||
`RuntimeServices` currently mixes:
|
||||
|
||||
- file polling
|
||||
- deferred OSC commit handling
|
||||
- control service orchestration
|
||||
|
||||
OSC ingest, overlay application, and host sync are distributed across several components.
|
||||
|
||||
Relevant code:
|
||||
|
||||
- [RuntimeServices.h](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/control/RuntimeServices.h:26)
|
||||
- [RuntimeServices.cpp](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/control/RuntimeServices.cpp:178)
|
||||
|
||||
Recommended direction:
|
||||
|
||||
- introduce a small internal event pipeline or message bus
|
||||
- use typed events for OSC, reloads, persistence requests, and status changes
|
||||
- make timing ownership explicit per subsystem
|
||||
|
||||
Example event types:
|
||||
|
||||
- `OscParameterTargeted`
|
||||
- `RenderOverlaySettled`
|
||||
- `PersistStateRequested`
|
||||
- `ShaderReloadRequested`
|
||||
- `DeckLinkStatusChanged`
|
||||
|
||||
### 4. Error handling is still heavily UI-coupled
|
||||
|
||||
Failures are often surfaced via `MessageBoxA`, while background services mainly log with `OutputDebugStringA`.
|
||||
|
||||
Relevant code:
|
||||
|
||||
- [OpenGLComposite.cpp](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/gl/OpenGLComposite.cpp:314)
|
||||
- [DeckLinkSession.cpp](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/videoio/decklink/DeckLinkSession.cpp:478)
|
||||
- [RuntimeServices.cpp](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/control/RuntimeServices.cpp:205)
|
||||
|
||||
This is not ideal for a live system where modal dialogs and silent debug logging are both poor operational behavior.
|
||||
|
||||
Recommended direction:
|
||||
|
||||
- introduce structured in-app error reporting
|
||||
- define severity levels and counters
|
||||
- prefer degraded runtime states over modal failure handling where possible
|
||||
- add a rolling log file for operational troubleshooting
|
||||
|
||||
### 5. Live OSC overlay and persisted state are still separate concepts without a formal model
|
||||
|
||||
The current design works better now, but it still relies on hand-managed reconciliation between:
|
||||
|
||||
- persisted parameter state in `RuntimeHost`
|
||||
- transient OSC overlay state in `OpenGLComposite`
|
||||
|
||||
Relevant code:
|
||||
|
||||
- [OpenGLComposite.h](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/gl/OpenGLComposite.h:66)
|
||||
|
||||
Recommended direction:
|
||||
|
||||
Formalize three layers of state:
|
||||
|
||||
- base persisted state
|
||||
- operator/UI committed state
|
||||
- transient live automation overlay
|
||||
|
||||
Then render can always resolve:
|
||||
|
||||
- `final = base + committed + transient`
|
||||
|
||||
That avoids special-case sync behavior becoming scattered across the code.
|
||||
|
||||
### 6. DeckLink lifecycle could be modeled more explicitly
|
||||
|
||||
`DeckLinkSession` has a number of imperative calls, but startup, preroll, running, degraded, and stopped are not represented as an explicit state machine.
|
||||
|
||||
Relevant code:
|
||||
|
||||
- [DeckLinkSession.h](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/videoio/decklink/DeckLinkSession.h:17)
|
||||
|
||||
Recommended direction:
|
||||
|
||||
- introduce explicit session states
|
||||
- define allowed transitions
|
||||
- centralize recovery behavior
|
||||
- make shutdown ordering and degraded-mode behavior more predictable
|
||||
|
||||
### 7. Persistence should be more asynchronous and debounced
|
||||
|
||||
`SavePersistentState()` is still called directly from many update paths.
|
||||
|
||||
Relevant code:
|
||||
|
||||
- [RuntimeHost.cpp](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/runtime/RuntimeHost.cpp:1841)
|
||||
|
||||
Recent OSC work already reduced this problem for live automation, but the broader architecture would still benefit from:
|
||||
|
||||
- a debounced persistence queue
|
||||
- atomic write-behind snapshots
|
||||
- clear separation between state mutation and disk flush
|
||||
|
||||
This improves both resilience and timing safety.
|
||||
|
||||
### 8. Telemetry is useful, but still too coarse
|
||||
|
||||
The app already records render timing and playout pacing, which is a good foundation.
|
||||
|
||||
Relevant code:
|
||||
|
||||
- [OpenGLRenderPipeline.cpp](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/gl/pipeline/OpenGLRenderPipeline.cpp:24)
|
||||
- [OpenGLVideoIOBridge.cpp](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/apps/LoopThroughWithOpenGLCompositing/gl/pipeline/OpenGLVideoIOBridge.cpp:24)
|
||||
|
||||
Recommended direction:
|
||||
|
||||
Add lightweight tracing for:
|
||||
|
||||
- input callback latency
|
||||
- GL lock wait time
|
||||
- render time
|
||||
- readback time
|
||||
- output scheduling lag
|
||||
- control queue depth
|
||||
- `RuntimeHost` lock contention
|
||||
|
||||
That would make future tuning and failure diagnosis much easier.
|
||||
|
||||
## Phased Roadmap
|
||||
|
||||
This roadmap is ordered by architectural dependency rather than by “quick wins.” The goal is to move the app toward clearer ownership boundaries and safer live behavior without doing later work on top of foundations that are likely to change again.
|
||||
|
||||
### Phase 1. Define subsystem boundaries and target architecture
|
||||
|
||||
Before changing major internals, formalize the target responsibilities for each major part of the app.
|
||||
|
||||
Target split:
|
||||
|
||||
- `RuntimeStore`
|
||||
- persisted config
|
||||
- persisted layer stack
|
||||
- preset persistence
|
||||
- `RuntimeSnapshot`
|
||||
- render-facing immutable or near-immutable snapshots
|
||||
- parameter values prepared for the render path
|
||||
- `ControlServices`
|
||||
- OSC ingress
|
||||
- web control ingress
|
||||
- reload/file-watch requests
|
||||
- commit/persist requests
|
||||
- `RenderEngine`
|
||||
- sole owner of live GL rendering
|
||||
- sole consumer of render snapshots plus transient overlays
|
||||
- `VideoBackend`
|
||||
- DeckLink input/output lifecycle
|
||||
- pacing and scheduling
|
||||
- `Health/Telemetry`
|
||||
- logging
|
||||
- counters
|
||||
- timing traces
|
||||
- degraded-state reporting
|
||||
|
||||
Why this phase comes first:
|
||||
|
||||
- it prevents later refactors from reintroducing responsibility overlap
|
||||
- it gives names to the seams the later phases will build around
|
||||
- it reduces the risk of replacing one monolith with several poorly-defined ones
|
||||
|
||||
Suggested deliverables:
|
||||
|
||||
- a short architecture diagram
|
||||
- a responsibility table for each subsystem
|
||||
- a list of allowed dependencies between subsystems
|
||||
|
||||
### Phase 2. Introduce an internal event model
|
||||
|
||||
Once subsystem boundaries are defined, introduce a typed event pipeline between them. This should happen before large state splits so the app has a stable coordination model.
|
||||
|
||||
Example event families:
|
||||
|
||||
- control events
|
||||
- `OscParameterTargeted`
|
||||
- `UiParameterCommitted`
|
||||
- `TriggerFired`
|
||||
- runtime events
|
||||
- `ShaderReloadRequested`
|
||||
- `PackagesRescanned`
|
||||
- `PersistStateRequested`
|
||||
- render events
|
||||
- `OverlayApplied`
|
||||
- `OverlaySettled`
|
||||
- `SnapshotPublished`
|
||||
- backend events
|
||||
- `InputSignalChanged`
|
||||
- `OutputLateFrameDetected`
|
||||
- `OutputDroppedFrameDetected`
|
||||
- health events
|
||||
- `SubsystemWarningRaised`
|
||||
- `SubsystemRecovered`
|
||||
|
||||
Why this phase comes second:
|
||||
|
||||
- it provides a migration path away from direct cross-calls
|
||||
- it makes ownership explicit before data structures are split apart
|
||||
- it lets you move one subsystem at a time without losing coordination
|
||||
|
||||
Suggested outcome:
|
||||
|
||||
- the app stops relying on “shared object plus mutex plus polling” as the default coordination pattern
|
||||
|
||||
### Phase 3. Split `RuntimeHost` into persistent state, render snapshot state, and service-facing coordination
|
||||
|
||||
After the event model exists, break apart `RuntimeHost`.
|
||||
|
||||
Recommended split:
|
||||
|
||||
- `RuntimeStore`
|
||||
- owns config and saved layer data
|
||||
- handles serialization/deserialization
|
||||
- does not sit on the live render path
|
||||
- `RuntimeCoordinator`
|
||||
- resolves control actions
|
||||
- validates mutations
|
||||
- publishes new snapshots
|
||||
- bridges events between services and render
|
||||
- `RuntimeSnapshotProvider`
|
||||
- publishes immutable render snapshots
|
||||
- avoids large shared mutable structures on the render path
|
||||
|
||||
Why this phase comes before render-thread isolation:
|
||||
|
||||
- render isolation is easier when the render thread consumes clean snapshots instead of a large mutable host object
|
||||
- otherwise the GL refactor still drags along too much shared state complexity
|
||||
|
||||
Primary design rule:
|
||||
|
||||
- render should read snapshots
|
||||
- persistence should write stored state
|
||||
- services should request mutations through the coordinator
|
||||
|
||||
### Phase 4. Make the render thread the sole GL owner
|
||||
|
||||
With state and coordination cleaner, move to a dedicated render-thread model.
|
||||
|
||||
Target behavior:
|
||||
|
||||
- one thread owns the GL context
|
||||
- input callbacks never perform GL work directly
|
||||
- output callbacks never perform GL work directly
|
||||
- preview presentation, texture upload, render passes, readback, and output pack work are all issued by the render thread
|
||||
|
||||
Other threads should only:
|
||||
|
||||
- enqueue new video frames
|
||||
- enqueue control updates
|
||||
- enqueue backend events
|
||||
- consume produced output buffers
|
||||
|
||||
Why this phase comes here:
|
||||
|
||||
- it is much safer once state access and control coordination are no longer centered on `RuntimeHost`
|
||||
- it avoids coupling the render-thread refactor to storage and service refactors at the same time
|
||||
|
||||
Expected benefits:
|
||||
|
||||
- less cross-thread GL contention
|
||||
- easier timing reasoning
|
||||
- much lower risk of callback-driven stalls
|
||||
- a clearer foundation for future GPU pipeline work
|
||||
|
||||
### Phase 5. Refactor live state layering into an explicit composition model
|
||||
|
||||
Once rendering and snapshots are isolated, formalize how final parameter values are derived.
|
||||
|
||||
Recommended layers:
|
||||
|
||||
- base persisted state
|
||||
- operator-committed live state
|
||||
- transient automation overlay
|
||||
|
||||
Render should derive final values from a clear composition rule such as:
|
||||
|
||||
- `final = base + committed + transient`
|
||||
|
||||
Why this phase follows render isolation:
|
||||
|
||||
- once render owns snapshot consumption, it becomes much easier to cleanly evaluate layered state without touching persistence or control services
|
||||
- it turns the current OSC overlay behavior into a first-class model instead of an implementation detail
|
||||
|
||||
Expected benefits:
|
||||
|
||||
- fewer one-off sync rules
|
||||
- clearer behavior for OSC, UI changes, and automation
|
||||
- easier future expansion to presets, cues, or timed transitions
|
||||
|
||||
### Phase 6. Move persistence onto a background snapshot writer
|
||||
|
||||
After the state model is explicit, persistence should become a background concern rather than a synchronous side effect of mutations.
|
||||
|
||||
Target behavior:
|
||||
|
||||
- mutations update authoritative in-memory stored state
|
||||
- persistence requests are queued
|
||||
- disk writes are debounced and coalesced
|
||||
- writes are atomic and versioned where practical
|
||||
|
||||
Why this phase comes after state splitting:
|
||||
|
||||
- otherwise persistence logic will need to be rewritten twice
|
||||
- it should operate on the new `RuntimeStore` model, not on the current mixed-responsibility object
|
||||
|
||||
Expected benefits:
|
||||
|
||||
- less timing interference
|
||||
- better corruption resistance
|
||||
- cleaner restart/recovery semantics
|
||||
|
||||
### Phase 7. Make DeckLink/backend lifecycle explicit with a state machine
|
||||
|
||||
Once the render and state layers are cleaner, refactor the video backend into an explicit lifecycle model.
|
||||
|
||||
Suggested states:
|
||||
|
||||
- uninitialized
|
||||
- devices-discovered
|
||||
- configured
|
||||
- prerolling
|
||||
- running
|
||||
- degraded
|
||||
- stopping
|
||||
- stopped
|
||||
- failed
|
||||
|
||||
Why this phase belongs here:
|
||||
|
||||
- the backend should integrate with the new event model
|
||||
- degraded/recovery behavior will be easier once rendering and state coordination are already more deterministic
|
||||
|
||||
Expected benefits:
|
||||
|
||||
- safer startup/shutdown ordering
|
||||
- clearer recovery behavior
|
||||
- easier handling of missing input, dropped frames, or reconfiguration
|
||||
|
||||
### Phase 8. Add structured health, telemetry, and operational reporting
|
||||
|
||||
This phase should happen after the main ownership changes so the telemetry can reflect the final architecture instead of a transient one.
|
||||
|
||||
Recommended coverage:
|
||||
|
||||
- render queue depth
|
||||
- GL lock wait time, if any shared lock remains
|
||||
- input callback latency
|
||||
- output scheduling lag
|
||||
- readback timing
|
||||
- snapshot publish frequency
|
||||
- persistence queue depth
|
||||
- event queue depth
|
||||
- backend state transitions
|
||||
- warning/error counters per subsystem
|
||||
|
||||
Also replace modal-only error handling with:
|
||||
|
||||
- structured in-app health state
|
||||
- severity-based logging
|
||||
- rolling log files
|
||||
- operator-visible degraded-state messages
|
||||
|
||||
Why this phase comes last:
|
||||
|
||||
- it should instrument the architecture you intend to keep
|
||||
- otherwise instrumentation work gets invalidated by the refactor
|
||||
|
||||
## Recommended Execution Order
|
||||
|
||||
If this is approached as a serious architecture program rather than opportunistic cleanup, the recommended order is:
|
||||
|
||||
1. Define subsystem boundaries and target architecture.
|
||||
2. Introduce the internal event model.
|
||||
3. Split `RuntimeHost`.
|
||||
4. Make the render thread the sole GL owner.
|
||||
5. Formalize live state layering and composition.
|
||||
6. Move persistence to a background snapshot writer.
|
||||
7. Refactor DeckLink/backend lifecycle into an explicit state machine.
|
||||
8. Add structured telemetry, health reporting, and operational diagnostics.
|
||||
|
||||
## Why This Order Makes Sense
|
||||
|
||||
This order tries to avoid doing foundational work twice.
|
||||
|
||||
- The event model comes before major subsystem extraction so coordination patterns stabilize early.
|
||||
- `RuntimeHost` is split before render isolation so the render thread does not inherit the current monolithic state model.
|
||||
- Live state layering is formalized only after render ownership is clearer.
|
||||
- Persistence is moved later so it can target the final state model rather than the current one.
|
||||
- Telemetry is intentionally late so it instruments the architecture that survives the refactor.
|
||||
|
||||
## Short Version
|
||||
|
||||
The app is in a much better place than it was before the OSC timing work, but the main remaining architectural risk is still shared ownership. Too many responsibilities converge on `RuntimeHost` and the shared GL path. The most sensible path forward is:
|
||||
|
||||
1. define boundaries
|
||||
2. establish an event model
|
||||
3. split state ownership
|
||||
4. isolate rendering
|
||||
5. formalize layered live state
|
||||
6. background persistence
|
||||
7. explicit backend lifecycle
|
||||
8. health and telemetry
|
||||
|
||||
That sequence gives each later phase a cleaner foundation than the current app has today.
|
||||
201
docs/SHADER_FEEDBACK_TARGET_IDEA.md
Normal file
201
docs/SHADER_FEEDBACK_TARGET_IDEA.md
Normal file
@@ -0,0 +1,201 @@
|
||||
# Shader Feedback Target Idea
|
||||
|
||||
This note summarizes a possible feature where a shader can request a persistent render target for storing and reusing its own internal information across frames.
|
||||
|
||||
## Goal
|
||||
|
||||
Allow a shader to keep shader-local state without writing arbitrary values back into host-owned parameters.
|
||||
|
||||
This is useful for cases like:
|
||||
|
||||
- storing sampled color information across frames
|
||||
- storing luminance or mask values per pixel
|
||||
- keeping running filtered values
|
||||
- tracking simple analysis data
|
||||
- reserving a small texel region as an array-like metadata store
|
||||
|
||||
## Core Idea
|
||||
|
||||
A shader may opt in, via `shader.json`, to receive one persistent `RGBA16F` render target at client/output resolution.
|
||||
|
||||
The intended model is:
|
||||
|
||||
- the shader writes into the feedback target this frame
|
||||
- the shader reads the previous frame’s feedback target on the next frame
|
||||
|
||||
This makes it a shader-local “previous frame state” surface.
|
||||
|
||||
## Why This Makes Sense
|
||||
|
||||
This is a better fit than letting shaders push arbitrary values back into the host because:
|
||||
|
||||
- state stays inside the render domain
|
||||
- shaders remain render-focused rather than becoming host-state mutators
|
||||
- host-owned parameters, UI state, and persistence remain predictable
|
||||
- timing is easier to reason about
|
||||
- it fits naturally with multipass and temporal rendering patterns
|
||||
|
||||
## Recommended Behavior
|
||||
|
||||
### 1. Make it opt-in
|
||||
|
||||
Shaders should explicitly request this capability in `shader.json`.
|
||||
|
||||
Reasons:
|
||||
|
||||
- most shaders will not need it
|
||||
- it avoids unnecessary VRAM/bandwidth cost
|
||||
- it keeps shader capabilities explicit
|
||||
- it avoids silently changing the contract for every shader
|
||||
|
||||
The runtime should only allocate/bind the feedback surface for shaders that request it.
|
||||
|
||||
### 2. Start with previous-frame feedback only
|
||||
|
||||
The first version should expose only one frame of history:
|
||||
|
||||
- current frame writes
|
||||
- next frame reads previous state
|
||||
|
||||
Reasons:
|
||||
|
||||
- simpler mental model
|
||||
- lower memory cost
|
||||
- easier to document
|
||||
- enough for many practical use cases
|
||||
|
||||
If a shader wants longer memory, it can accumulate or encode that over time into the same persistent surface.
|
||||
|
||||
### 3. Keep it shader-local
|
||||
|
||||
The feedback target should be treated as internal shader state, not as host-visible parameter state.
|
||||
|
||||
That means:
|
||||
|
||||
- it does not automatically update exposed parameters
|
||||
- it does not automatically show up in the UI
|
||||
- it does not automatically persist to runtime state
|
||||
|
||||
### 4. Keep it separate from normal multipass chaining
|
||||
|
||||
This feedback target should not replace or blur the meaning of the existing multipass system.
|
||||
|
||||
The clean model is:
|
||||
|
||||
- normal multipass outputs are for same-frame chaining
|
||||
- the feedback target is for previous-frame persistent state
|
||||
|
||||
In other words:
|
||||
|
||||
- pass A can write an output that pass B reads later in the same frame
|
||||
- the feedback target written during frame `N` is read back during frame `N + 1`
|
||||
|
||||
That means the feedback target should be thought of as a separate cross-frame resource, not as “another pass output.”
|
||||
|
||||
Recommended behavior for multipass shaders that request feedback:
|
||||
|
||||
- all passes in the shader may read the same previous-frame feedback surface
|
||||
- one designated pass should produce the next feedback surface for the following frame
|
||||
- feedback writes should not be interpreted as same-frame pass-to-pass communication
|
||||
|
||||
This avoids ambiguity such as:
|
||||
|
||||
- whether pass 2 sees pass 1’s feedback writes from the same frame
|
||||
- whether multiple passes are racing to write the persistent surface
|
||||
- whether feedback is supposed to mean same-frame scratch space or next-frame state
|
||||
|
||||
The intended separation is:
|
||||
|
||||
- use named pass outputs and `previousPass` for same-frame chaining
|
||||
- use the feedback target for persistent previous-frame state
|
||||
|
||||
## What It Could Store
|
||||
|
||||
Because the target would be a full-resolution `RGBA16F` texture, a shader could use it in a few ways.
|
||||
|
||||
### Full-frame per-pixel storage
|
||||
|
||||
Examples:
|
||||
|
||||
- luminance per pixel
|
||||
- confidence/mask values
|
||||
- filtered or decayed image information
|
||||
- rolling per-pixel state used by a temporal effect
|
||||
|
||||
### Small array-like metadata regions
|
||||
|
||||
A shader could reserve a few texels or a small block as a logical data region.
|
||||
|
||||
Example:
|
||||
|
||||
- pixel `(0, 0)` stores value 0
|
||||
- pixel `(1, 0)` stores value 1
|
||||
- pixel `(2, 0)` stores value 2
|
||||
|
||||
Because each texel is `RGBA16F`, one texel can hold up to four scalar values.
|
||||
|
||||
This makes it possible to emulate a small array-like structure inside the texture.
|
||||
|
||||
## Important Caveats
|
||||
|
||||
This is not a true random-access structured buffer. It is still a texture-backed GPU surface.
|
||||
|
||||
That means:
|
||||
|
||||
- it is best suited to texel- or pixel-oriented storage
|
||||
- per-pixel “write to your own location” patterns are natural
|
||||
- many-to-one reductions or arbitrary scatter writes are harder
|
||||
- precision is limited to half-float storage
|
||||
|
||||
So the main question is usually not “can the shader store this?” but “can the shader update it cleanly with fragment-style GPU access?”
|
||||
|
||||
## Example Use Case
|
||||
|
||||
For a greenscreen workflow, a shader could:
|
||||
|
||||
- sample a small box region of the input
|
||||
- compute an average or representative screen color
|
||||
- store that color in reserved texels of the feedback target
|
||||
- reuse that stored value next frame as its internal key color
|
||||
|
||||
This would let the shader maintain its own sampled screen color over time without mutating the exposed host-side `screenColor` parameter.
|
||||
|
||||
## Multipass Interaction Summary
|
||||
|
||||
For a multipass shader, the most sensible mental model is:
|
||||
|
||||
- same-frame intermediate images still flow through the existing pass system
|
||||
- previous-frame persistent state flows through the feedback target
|
||||
|
||||
So if a shader has multiple passes:
|
||||
|
||||
- pass outputs are still used for within-frame work
|
||||
- the feedback target is read as last frame’s stored state
|
||||
- the feedback target is written once for use on the next frame
|
||||
|
||||
This keeps the feature understandable and prevents the feedback surface from becoming a confusing second pass graph.
|
||||
|
||||
## Recommended First Version
|
||||
|
||||
The simplest strong first version would be:
|
||||
|
||||
- opt-in via `shader.json`
|
||||
- one persistent `RGBA16F` target
|
||||
- full client/output resolution
|
||||
- shader reads previous frame’s feedback
|
||||
- shader writes current frame’s feedback
|
||||
- no deeper history at first
|
||||
- no automatic host writeback
|
||||
|
||||
## Summary
|
||||
|
||||
This feature would give shaders a safe, GPU-native way to hold internal state across frames.
|
||||
|
||||
The recommended approach is:
|
||||
|
||||
- make it opt-in per shader
|
||||
- keep it shader-local
|
||||
- expose only previous-frame feedback initially
|
||||
- treat it as a persistent render-state surface, not host parameter state
|
||||
|
||||
That keeps the design powerful without crossing the architectural boundary into shader-driven host mutation.
|
||||
@@ -19,6 +19,7 @@ struct ShaderContext
|
||||
float bypass;
|
||||
int sourceHistoryLength;
|
||||
int temporalHistoryLength;
|
||||
int feedbackAvailable;
|
||||
};
|
||||
|
||||
cbuffer GlobalParams
|
||||
@@ -34,10 +35,11 @@ cbuffer GlobalParams
|
||||
float gBypass;
|
||||
int gSourceHistoryLength;
|
||||
int gTemporalHistoryLength;
|
||||
int gFeedbackAvailable;
|
||||
{{PARAMETER_UNIFORMS}}};
|
||||
|
||||
Sampler2D<float4> gVideoInput;
|
||||
{{SOURCE_HISTORY_SAMPLERS}}{{TEMPORAL_HISTORY_SAMPLERS}}{{TEXTURE_SAMPLERS}}
|
||||
{{SOURCE_HISTORY_SAMPLERS}}{{TEMPORAL_HISTORY_SAMPLERS}}{{FEEDBACK_SAMPLER}}{{TEXTURE_SAMPLERS}}
|
||||
{{TEXT_SAMPLERS}}
|
||||
float4 sampleVideo(float2 tc)
|
||||
{
|
||||
@@ -74,6 +76,8 @@ float4 sampleTemporalHistory(int framesAgo, float2 tc)
|
||||
}
|
||||
}
|
||||
|
||||
{{FEEDBACK_HELPER}}
|
||||
|
||||
{{TEXT_HELPERS}}
|
||||
#include "{{USER_SHADER_INCLUDE}}"
|
||||
|
||||
@@ -94,6 +98,7 @@ float4 fragmentMain(FragmentInput input) : SV_Target
|
||||
context.bypass = gBypass;
|
||||
context.sourceHistoryLength = gSourceHistoryLength;
|
||||
context.temporalHistoryLength = gTemporalHistoryLength;
|
||||
context.feedbackAvailable = gFeedbackAvailable;
|
||||
float4 effectedColor = {{ENTRY_POINT_CALL}};
|
||||
float mixValue = clamp(gBypass > 0.5 ? 0.0 : gMixAmount, 0.0, 1.0);
|
||||
return lerp(context.sourceColor, effectedColor, mixValue);
|
||||
|
||||
@@ -101,6 +101,7 @@ Optional fields:
|
||||
- `textures`: texture assets to load and expose as samplers.
|
||||
- `fonts`: packaged font assets for live text parameters.
|
||||
- `temporal`: history-buffer requirements.
|
||||
- `feedback`: optional previous-frame shader-local feedback surface.
|
||||
|
||||
Parameter objects may also include an optional `description` string. The control UI displays it as one-line helper text with the full text available on hover, so use it for short operational guidance rather than long documentation.
|
||||
|
||||
@@ -194,6 +195,88 @@ Pass output names:
|
||||
|
||||
If the final declared pass does not explicitly output `layerOutput`, the runtime still treats that final pass as the visible layer output. Existing single-pass shaders are unaffected.
|
||||
|
||||
## Feedback Surface
|
||||
|
||||
Shaders may opt in to a persistent previous-frame feedback surface:
|
||||
|
||||
```json
|
||||
{
|
||||
"feedback": {
|
||||
"enabled": true,
|
||||
"writePass": "final"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Fields:
|
||||
|
||||
- `enabled`: when `true`, the runtime allocates one persistent `RGBA16F` feedback surface for this shader at the current render resolution.
|
||||
- `writePass`: optional pass `id` whose output should become next frame's feedback surface. If omitted, the runtime uses the final declared pass, or the implicit `main` pass for single-pass shaders.
|
||||
|
||||
Behavior:
|
||||
|
||||
- all passes may sample the same previous-frame feedback surface
|
||||
- one designated pass writes the next feedback surface
|
||||
- feedback is previous-frame state, not same-frame pass chaining
|
||||
|
||||
Single-pass example:
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "feedback-glow",
|
||||
"name": "Feedback Glow",
|
||||
"feedback": {
|
||||
"enabled": true
|
||||
},
|
||||
"parameters": []
|
||||
}
|
||||
```
|
||||
|
||||
Multipass example:
|
||||
|
||||
```json
|
||||
{
|
||||
"passes": [
|
||||
{
|
||||
"id": "analysis",
|
||||
"source": "shader.slang",
|
||||
"entryPoint": "analyzeFrame",
|
||||
"output": "analysisBuffer"
|
||||
},
|
||||
{
|
||||
"id": "final",
|
||||
"source": "shader.slang",
|
||||
"entryPoint": "finishFrame",
|
||||
"inputs": ["analysisBuffer"],
|
||||
"output": "layerOutput"
|
||||
}
|
||||
],
|
||||
"feedback": {
|
||||
"enabled": true,
|
||||
"writePass": "final"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The wrapper exposes:
|
||||
|
||||
```slang
|
||||
float4 sampleFeedback(float2 uv);
|
||||
```
|
||||
|
||||
On the first frame, or after a reset, `sampleFeedback` returns transparent black.
|
||||
|
||||
Feedback resets when:
|
||||
|
||||
- layers are added, removed, or reordered
|
||||
- a layer bypass state changes
|
||||
- a layer changes shader
|
||||
- a shader is reloaded or recompiled
|
||||
- render dimensions change
|
||||
- the app restarts
|
||||
|
||||
So feedback should be treated as live runtime state, not durable saved state.
|
||||
|
||||
## Slang Entry Point
|
||||
|
||||
Your shader file must implement the manifest `entryPoint`.
|
||||
@@ -239,6 +322,7 @@ struct ShaderContext
|
||||
float bypass;
|
||||
int sourceHistoryLength;
|
||||
int temporalHistoryLength;
|
||||
int feedbackAvailable;
|
||||
};
|
||||
```
|
||||
|
||||
@@ -257,6 +341,7 @@ Fields:
|
||||
- `bypass`: `1.0` when the layer is bypassed, otherwise `0.0`.
|
||||
- `sourceHistoryLength`: number of usable source-history frames currently available.
|
||||
- `temporalHistoryLength`: number of usable temporal frames currently available for this layer.
|
||||
- `feedbackAvailable`: `1` when previous-frame feedback exists for this layer, otherwise `0`.
|
||||
|
||||
Color/precision notes:
|
||||
|
||||
@@ -273,6 +358,7 @@ The wrapper provides:
|
||||
float4 sampleVideo(float2 uv);
|
||||
float4 sampleSourceHistory(int framesAgo, float2 uv);
|
||||
float4 sampleTemporalHistory(int framesAgo, float2 uv);
|
||||
float4 sampleFeedback(float2 uv);
|
||||
```
|
||||
|
||||
`sampleVideo` samples the live decoded source video.
|
||||
@@ -281,6 +367,8 @@ float4 sampleTemporalHistory(int framesAgo, float2 uv);
|
||||
|
||||
`sampleTemporalHistory` samples previous pre-layer input frames for temporal shaders that request `preLayerInput` history. `framesAgo` is clamped into the available range. If no temporal history is available, it falls back to `sampleVideo`.
|
||||
|
||||
`sampleFeedback` samples the shader-local previous-frame feedback surface. If feedback has not been written yet, it returns transparent black.
|
||||
|
||||
Example:
|
||||
|
||||
```slang
|
||||
@@ -291,6 +379,40 @@ float4 shadeVideo(ShaderContext context)
|
||||
}
|
||||
```
|
||||
|
||||
Feedback example:
|
||||
|
||||
```slang
|
||||
float4 shadeVideo(ShaderContext context)
|
||||
{
|
||||
float4 previous = sampleFeedback(context.uv);
|
||||
float4 current = context.sourceColor;
|
||||
return lerp(current, previous, 0.2);
|
||||
}
|
||||
```
|
||||
|
||||
Multipass feedback example:
|
||||
|
||||
```slang
|
||||
float4 analyzeFrame(ShaderContext context)
|
||||
{
|
||||
float4 previous = sampleFeedback(context.uv);
|
||||
float luma = dot(context.sourceColor.rgb, float3(0.2126, 0.7152, 0.0722));
|
||||
return float4(lerp(previous.rgb, float3(luma), 0.1), 1.0);
|
||||
}
|
||||
|
||||
float4 finishFrame(ShaderContext context)
|
||||
{
|
||||
float4 analysis = context.sourceColor;
|
||||
return float4(analysis.rgb, 1.0);
|
||||
}
|
||||
```
|
||||
|
||||
In that multipass case:
|
||||
|
||||
- `analyzeFrame` reads last frame's feedback
|
||||
- `finishFrame` receives the same-frame pass output through normal multipass routing
|
||||
- the `writePass` decides which pass output becomes next frame's feedback
|
||||
|
||||
## Parameters
|
||||
|
||||
Manifest parameters are exposed to Slang as global values with the same `id`.
|
||||
72
shaders/feedback-highlight-accumulator/shader.json
Normal file
72
shaders/feedback-highlight-accumulator/shader.json
Normal file
@@ -0,0 +1,72 @@
|
||||
{
|
||||
"id": "feedback-highlight-accumulator",
|
||||
"name": "Feedback Highlight Accumulator",
|
||||
"description": "Demonstrates shader-local feedback by accumulating only the brightest-looking parts of the image over a timed window, then resetting and starting again. The highlight selection is an approximation based on a luminance threshold rather than an exact percentile reduction.",
|
||||
"category": "Feedback",
|
||||
"entryPoint": "shadeVideo",
|
||||
"feedback": {
|
||||
"enabled": true
|
||||
},
|
||||
"parameters": [
|
||||
{
|
||||
"id": "highlightThreshold",
|
||||
"label": "Highlight Threshold",
|
||||
"type": "float",
|
||||
"default": 0.99,
|
||||
"min": 0.5,
|
||||
"max": 1.5,
|
||||
"step": 0.001,
|
||||
"description": "Approximate cutoff for the brightest parts of the frame."
|
||||
},
|
||||
{
|
||||
"id": "softness",
|
||||
"label": "Threshold Softness",
|
||||
"type": "float",
|
||||
"default": 0.05,
|
||||
"min": 0.001,
|
||||
"max": 0.25,
|
||||
"step": 0.001,
|
||||
"description": "Softens the threshold so near-highlights can contribute gradually."
|
||||
},
|
||||
{
|
||||
"id": "accumulateAmount",
|
||||
"label": "Accumulate Amount",
|
||||
"type": "float",
|
||||
"default": 0.2,
|
||||
"min": 0.0,
|
||||
"max": 2.0,
|
||||
"step": 0.001,
|
||||
"description": "How much of each bright sample gets added into the running feedback image."
|
||||
},
|
||||
{
|
||||
"id": "displayGain",
|
||||
"label": "Display Gain",
|
||||
"type": "float",
|
||||
"default": 1.0,
|
||||
"min": 0.0,
|
||||
"max": 4.0,
|
||||
"step": 0.01,
|
||||
"description": "Brightness applied to the accumulated feedback when displayed."
|
||||
},
|
||||
{
|
||||
"id": "baseMix",
|
||||
"label": "Base Mix",
|
||||
"type": "float",
|
||||
"default": 0.25,
|
||||
"min": 0.0,
|
||||
"max": 1.0,
|
||||
"step": 0.01,
|
||||
"description": "Amount of the live source image kept under the accumulation."
|
||||
},
|
||||
{
|
||||
"id": "resetSeconds",
|
||||
"label": "Reset Seconds",
|
||||
"type": "float",
|
||||
"default": 10.0,
|
||||
"min": 1.0,
|
||||
"max": 60.0,
|
||||
"step": 0.1,
|
||||
"description": "Length of each accumulation window before the feedback resets."
|
||||
}
|
||||
]
|
||||
}
|
||||
31
shaders/feedback-highlight-accumulator/shader.slang
Normal file
31
shaders/feedback-highlight-accumulator/shader.slang
Normal file
@@ -0,0 +1,31 @@
|
||||
float luminance(float3 color)
|
||||
{
|
||||
return dot(color, float3(0.2126, 0.7152, 0.0722));
|
||||
}
|
||||
|
||||
float currentResetPhase(float timeSeconds, float resetInterval)
|
||||
{
|
||||
float safeInterval = max(resetInterval, 0.001);
|
||||
float cycleIndex = floor(timeSeconds / safeInterval);
|
||||
return frac(cycleIndex * 0.5) >= 0.5 ? 1.0 : 0.0;
|
||||
}
|
||||
|
||||
float4 shadeVideo(ShaderContext context)
|
||||
{
|
||||
float safeResetSeconds = max(resetSeconds, 0.001);
|
||||
float phase = currentResetPhase(context.time, safeResetSeconds);
|
||||
|
||||
float4 previousFeedback = sampleFeedback(context.uv);
|
||||
bool phaseMatches = context.feedbackAvailable > 0 && abs(previousFeedback.a - phase) < 0.25;
|
||||
float3 previousAccumulation = phaseMatches ? previousFeedback.rgb : float3(0.0, 0.0, 0.0);
|
||||
|
||||
float currentLuma = luminance(context.sourceColor.rgb);
|
||||
float thresholdWidth = max(softness, 0.0001);
|
||||
float brightMask = smoothstep(highlightThreshold - thresholdWidth, highlightThreshold + thresholdWidth, currentLuma);
|
||||
|
||||
float3 currentContribution = context.sourceColor.rgb * brightMask * max(accumulateAmount, 0.0);
|
||||
float3 nextAccumulation = saturate(previousAccumulation + currentContribution);
|
||||
|
||||
float3 displayColor = context.sourceColor.rgb * saturate(baseMix) + nextAccumulation * max(displayGain, 0.0);
|
||||
return float4(saturate(displayColor), phase);
|
||||
}
|
||||
@@ -59,6 +59,26 @@
|
||||
],
|
||||
"description": "Normalized fisheye radius; adjust X/Y when the image is cropped or squeezed."
|
||||
},
|
||||
{
|
||||
"id": "sourceEdgeCut",
|
||||
"label": "Source Edge Cut",
|
||||
"type": "float",
|
||||
"default": 0.01,
|
||||
"min": 0,
|
||||
"max": 0.2,
|
||||
"step": 0.001,
|
||||
"description": "Cuts slightly inward from all four source-frame edges before sampling to hide empty border regions."
|
||||
},
|
||||
{
|
||||
"id": "sourceEdgeFeather",
|
||||
"label": "Source Edge Feather",
|
||||
"type": "float",
|
||||
"default": 0.02,
|
||||
"min": 0,
|
||||
"max": 0.2,
|
||||
"step": 0.001,
|
||||
"description": "Softens the trimmed source edges into the outside color for easier background blending."
|
||||
},
|
||||
{
|
||||
"id": "virtualFovDegrees",
|
||||
"label": "Virtual FOV",
|
||||
|
||||
@@ -61,6 +61,20 @@ float normalizedFisheyeRadius(float theta, float halfFov)
|
||||
return theta / safeHalfFov;
|
||||
}
|
||||
|
||||
float sourceUvRectMask(float2 uv, float2 inputResolution)
|
||||
{
|
||||
float2 pixel = 1.0 / max(inputResolution, float2(1.0, 1.0));
|
||||
float cut = max(sourceEdgeCut, 0.0);
|
||||
float feather = max(sourceEdgeFeather, 0.0);
|
||||
float2 featherSize = max(float2(feather, feather), pixel * 0.5);
|
||||
|
||||
float left = smoothstep(cut, cut + featherSize.x, uv.x);
|
||||
float right = 1.0 - smoothstep(1.0 - cut - featherSize.x, 1.0 - cut, uv.x);
|
||||
float top = smoothstep(cut, cut + featherSize.y, uv.y);
|
||||
float bottom = 1.0 - smoothstep(1.0 - cut - featherSize.y, 1.0 - cut, uv.y);
|
||||
return saturate(left * right * top * bottom);
|
||||
}
|
||||
|
||||
float4 shadeVideo(ShaderContext context)
|
||||
{
|
||||
float2 screen = float2(context.uv.x * 2.0 - 1.0, 1.0 - context.uv.y * 2.0);
|
||||
@@ -99,5 +113,7 @@ float4 shadeVideo(ShaderContext context)
|
||||
if (sourceUv.x < 0.0 || sourceUv.x > 1.0 || sourceUv.y < 0.0 || sourceUv.y > 1.0)
|
||||
return outsideColor;
|
||||
|
||||
return sampleVideo(sourceUv);
|
||||
float sourceMask = sourceUvRectMask(sourceUv, context.inputResolution);
|
||||
float4 sourceColor = sampleVideo(sourceUv);
|
||||
return saturate(lerp(outsideColor, sourceColor, sourceMask));
|
||||
}
|
||||
|
||||
@@ -58,6 +58,7 @@ void TestValidManifest()
|
||||
"textures": [{ "id": "maskTex", "path": "mask.png" }],
|
||||
"fonts": [{ "id": "inter", "path": "Inter.ttf" }],
|
||||
"temporal": { "enabled": true, "historySource": "source", "historyLength": 8 },
|
||||
"feedback": { "enabled": true },
|
||||
"parameters": [
|
||||
{ "id": "gain", "label": "Gain", "description": "Scales the output intensity.", "type": "float", "default": 0.5, "min": 0, "max": 1 },
|
||||
{ "id": "titleText", "label": "Title", "type": "text", "default": "LIVE", "font": "inter", "maxLength": 32 },
|
||||
@@ -77,6 +78,7 @@ void TestValidManifest()
|
||||
Expect(package.textureAssets.size() == 1 && package.textureAssets[0].id == "maskTex", "texture assets parse");
|
||||
Expect(package.fontAssets.size() == 1 && package.fontAssets[0].id == "inter", "font assets parse");
|
||||
Expect(package.temporal.enabled && package.temporal.effectiveHistoryLength == 4, "temporal history is capped");
|
||||
Expect(package.feedback.enabled && package.feedback.writePassId == "main", "feedback defaults to the implicit main pass");
|
||||
Expect(package.parameters.size() == 4, "parameters parse");
|
||||
Expect(package.parameters[0].description == "Scales the output intensity.", "parameter descriptions parse");
|
||||
Expect(package.parameters[1].type == ShaderParameterType::Text && package.parameters[1].defaultTextValue == "LIVE", "text parameter parses");
|
||||
@@ -96,6 +98,7 @@ void TestExplicitPassManifest()
|
||||
{ "id": "blurX", "source": "blur-x.slang", "entryPoint": "blurHorizontal", "inputs": ["layerInput"], "output": "blurredX" },
|
||||
{ "id": "final", "source": "final.slang", "entryPoint": "finish", "inputs": ["blurredX"], "output": "layerOutput" }
|
||||
],
|
||||
"feedback": { "enabled": true, "writePass": "blurX" },
|
||||
"parameters": []
|
||||
})");
|
||||
WriteFile(root / "multi" / "blur-x.slang", "float4 blurHorizontal(float2 uv) { return float4(uv, 0.0, 1.0); }\n");
|
||||
@@ -109,6 +112,7 @@ void TestExplicitPassManifest()
|
||||
Expect(package.passes[0].id == "blurX" && package.passes[0].entryPoint == "blurHorizontal", "first pass metadata parses");
|
||||
Expect(package.passes[0].inputNames.size() == 1 && package.passes[0].inputNames[0] == "layerInput", "pass inputs parse");
|
||||
Expect(package.passes[1].outputName == "layerOutput", "pass output parses");
|
||||
Expect(package.feedback.enabled && package.feedback.writePassId == "blurX", "explicit feedback write pass parses");
|
||||
|
||||
std::filesystem::remove_all(root);
|
||||
}
|
||||
@@ -190,6 +194,25 @@ void TestInvalidTemporalSettings()
|
||||
std::filesystem::remove_all(root);
|
||||
}
|
||||
|
||||
void TestInvalidFeedbackSettings()
|
||||
{
|
||||
const std::filesystem::path root = MakeTestRoot();
|
||||
WriteShaderPackage(root, "bad-feedback", R"({
|
||||
"id": "bad-feedback",
|
||||
"name": "Bad Feedback",
|
||||
"feedback": { "enabled": true, "writePass": "missingPass" },
|
||||
"parameters": []
|
||||
})");
|
||||
|
||||
ShaderPackageRegistry registry(4);
|
||||
ShaderPackage package;
|
||||
std::string error;
|
||||
Expect(!registry.ParseManifest(root / "bad-feedback" / "shader.json", package, error), "invalid feedback manifest is rejected");
|
||||
Expect(error.find("writePass") != std::string::npos, "invalid feedback error names writePass");
|
||||
|
||||
std::filesystem::remove_all(root);
|
||||
}
|
||||
|
||||
void TestDisabledTemporalSettingsAreIgnored()
|
||||
{
|
||||
const std::filesystem::path root = MakeTestRoot();
|
||||
@@ -264,6 +287,7 @@ int main()
|
||||
TestMissingFontAsset();
|
||||
TestInvalidManifest();
|
||||
TestInvalidTemporalSettings();
|
||||
TestInvalidFeedbackSettings();
|
||||
TestDisabledTemporalSettingsAreIgnored();
|
||||
TestDuplicateScan();
|
||||
TestInvalidPackageDoesNotFailScan();
|
||||
|
||||
Reference in New Issue
Block a user