New rules based order
This commit is contained in:
@@ -287,8 +287,13 @@ set(RENDER_CADENCE_APP_SOURCES
|
||||
"${APP_DIR}/videoio/decklink/DeckLinkVideoIOFormat.h"
|
||||
"${APP_DIR}/gl/renderer/GLExtensions.cpp"
|
||||
"${APP_DIR}/gl/renderer/GLExtensions.h"
|
||||
"${APP_DIR}/gl/shader/Std140Buffer.h"
|
||||
"${APP_DIR}/runtime/support/RuntimeJson.cpp"
|
||||
"${APP_DIR}/runtime/support/RuntimeJson.h"
|
||||
"${APP_DIR}/shader/ShaderCompiler.cpp"
|
||||
"${APP_DIR}/shader/ShaderCompiler.h"
|
||||
"${APP_DIR}/shader/ShaderPackageRegistry.cpp"
|
||||
"${APP_DIR}/shader/ShaderPackageRegistry.h"
|
||||
"${APP_DIR}/shader/ShaderTypes.h"
|
||||
"${APP_DIR}/videoio/VideoIOFormat.cpp"
|
||||
"${APP_DIR}/videoio/VideoIOFormat.h"
|
||||
@@ -315,8 +320,12 @@ set(RENDER_CADENCE_APP_SOURCES
|
||||
"${RENDER_CADENCE_APP_DIR}/render/RenderThread.h"
|
||||
"${RENDER_CADENCE_APP_DIR}/render/RuntimeShaderRenderer.cpp"
|
||||
"${RENDER_CADENCE_APP_DIR}/render/RuntimeShaderRenderer.h"
|
||||
"${RENDER_CADENCE_APP_DIR}/render/RuntimeShaderParams.cpp"
|
||||
"${RENDER_CADENCE_APP_DIR}/render/RuntimeShaderParams.h"
|
||||
"${RENDER_CADENCE_APP_DIR}/render/SimpleMotionRenderer.cpp"
|
||||
"${RENDER_CADENCE_APP_DIR}/render/SimpleMotionRenderer.h"
|
||||
"${RENDER_CADENCE_APP_DIR}/runtime/RuntimeShaderBridge.cpp"
|
||||
"${RENDER_CADENCE_APP_DIR}/runtime/RuntimeShaderBridge.h"
|
||||
"${RENDER_CADENCE_APP_DIR}/runtime/RuntimeSlangShaderCompiler.cpp"
|
||||
"${RENDER_CADENCE_APP_DIR}/runtime/RuntimeSlangShaderCompiler.h"
|
||||
"${RENDER_CADENCE_APP_DIR}/telemetry/CadenceTelemetry.h"
|
||||
@@ -331,7 +340,9 @@ add_executable(RenderCadenceCompositor ${RENDER_CADENCE_APP_SOURCES})
|
||||
target_include_directories(RenderCadenceCompositor PRIVATE
|
||||
"${APP_DIR}"
|
||||
"${APP_DIR}/gl/renderer"
|
||||
"${APP_DIR}/gl/shader"
|
||||
"${APP_DIR}/platform"
|
||||
"${APP_DIR}/runtime/support"
|
||||
"${APP_DIR}/shader"
|
||||
"${APP_DIR}/videoio"
|
||||
"${APP_DIR}/videoio/decklink"
|
||||
@@ -774,6 +785,24 @@ endif()
|
||||
|
||||
add_test(NAME RenderCadenceCompositorTelemetryTests COMMAND RenderCadenceCompositorTelemetryTests)
|
||||
|
||||
add_executable(RenderCadenceCompositorRuntimeShaderParamsTests
|
||||
"${RENDER_CADENCE_APP_DIR}/render/RuntimeShaderParams.cpp"
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/tests/RenderCadenceCompositorRuntimeShaderParamsTests.cpp"
|
||||
)
|
||||
|
||||
target_include_directories(RenderCadenceCompositorRuntimeShaderParamsTests PRIVATE
|
||||
"${APP_DIR}/gl/shader"
|
||||
"${APP_DIR}/shader"
|
||||
"${RENDER_CADENCE_APP_DIR}/render"
|
||||
"${RENDER_CADENCE_APP_DIR}/runtime"
|
||||
)
|
||||
|
||||
if(MSVC)
|
||||
target_compile_options(RenderCadenceCompositorRuntimeShaderParamsTests PRIVATE /W3)
|
||||
endif()
|
||||
|
||||
add_test(NAME RenderCadenceCompositorRuntimeShaderParamsTests COMMAND RenderCadenceCompositorRuntimeShaderParamsTests)
|
||||
|
||||
add_executable(SystemOutputFramePoolTests
|
||||
"${APP_DIR}/videoio/SystemOutputFramePool.cpp"
|
||||
"${APP_DIR}/videoio/VideoIOFormat.cpp"
|
||||
|
||||
@@ -4,6 +4,8 @@ This app is the modular version of the working DeckLink render-cadence probe.
|
||||
|
||||
Its job is to prove the production-facing foundation before the current compositor's shader/runtime/control features are ported over.
|
||||
|
||||
Before adding features here, read the guardrails in [Render Cadence Golden Rules](../../docs/RENDER_CADENCE_GOLDEN_RULES.md).
|
||||
|
||||
## Architecture
|
||||
|
||||
```text
|
||||
@@ -40,13 +42,18 @@ Included now:
|
||||
- background Slang compile of `shaders/happy-accident`
|
||||
- app-owned submission of a completed shader artifact
|
||||
- render-thread-only GL commit once the artifact is ready
|
||||
- manifest-driven stateless single-pass shader packages
|
||||
- default float, vec2, color, boolean, enum, and trigger parameters
|
||||
- compact telemetry
|
||||
- non-GL frame-exchange tests
|
||||
|
||||
Intentionally not included yet:
|
||||
|
||||
- DeckLink input
|
||||
- shader package rendering
|
||||
- multipass shader rendering
|
||||
- temporal/history/feedback shader storage
|
||||
- texture/LUT asset upload
|
||||
- text-parameter rasterization
|
||||
- runtime state
|
||||
- OSC/API control
|
||||
- preview
|
||||
@@ -83,6 +90,14 @@ build\vs2022-x64-debug\Debug\RenderCadenceCompositor.exe
|
||||
|
||||
Press Enter to stop.
|
||||
|
||||
To test a different compatible shader package:
|
||||
|
||||
```powershell
|
||||
build\vs2022-x64-debug\Debug\RenderCadenceCompositor.exe --shader solid-color
|
||||
```
|
||||
|
||||
Use `--no-shader` to keep the simple motion fallback only.
|
||||
|
||||
## Expected Telemetry
|
||||
|
||||
The app prints one line per second:
|
||||
@@ -107,13 +122,22 @@ Healthy first-run signs:
|
||||
|
||||
## Runtime Slang Shader Test
|
||||
|
||||
On startup the app begins compiling `shaders/happy-accident` on a background thread owned by the app orchestration layer.
|
||||
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.
|
||||
|
||||
Current runtime shader support is deliberately limited to stateless single-pass packages:
|
||||
|
||||
- one pass only
|
||||
- no temporal history
|
||||
- no feedback storage
|
||||
- no texture/LUT assets yet
|
||||
- no text parameters yet
|
||||
- manifest defaults are used for parameters
|
||||
- `gVideoInput` and `gLayerInput` are bound to a small fallback source texture until DeckLink input is added
|
||||
|
||||
Successful handoff signs:
|
||||
|
||||
- console prints `Runtime shader committed: happy-accident`
|
||||
- telemetry shows `shaderCommitted=1`
|
||||
- output changes from the simple motion pattern to the Happy Accident shader
|
||||
- render/schedule cadence remains near 60 fps during and after the handoff
|
||||
|
||||
@@ -36,7 +36,7 @@ private:
|
||||
};
|
||||
}
|
||||
|
||||
int main()
|
||||
int main(int argc, char** argv)
|
||||
{
|
||||
ComInitGuard com;
|
||||
if (!com.Initialize())
|
||||
@@ -67,6 +67,21 @@ int main()
|
||||
RenderThread renderThread(frameExchange, renderConfig);
|
||||
|
||||
RenderCadenceCompositor::AppConfig appConfig = RenderCadenceCompositor::DefaultAppConfig();
|
||||
for (int index = 1; index < argc; ++index)
|
||||
{
|
||||
const std::string argument = argv[index];
|
||||
if (argument == "--shader" && index + 1 < argc)
|
||||
{
|
||||
appConfig.runtimeShaderId = argv[++index];
|
||||
continue;
|
||||
}
|
||||
if (argument == "--no-shader")
|
||||
{
|
||||
appConfig.runtimeShaderId.clear();
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
RenderCadenceCompositor::RenderCadenceApp<RenderThread, SystemFrameExchange> app(renderThread, frameExchange, appConfig);
|
||||
|
||||
std::string error;
|
||||
|
||||
@@ -13,6 +13,7 @@ AppConfig DefaultAppConfig()
|
||||
config.warmupTimeout = std::chrono::seconds(3);
|
||||
config.prerollTimeout = std::chrono::seconds(3);
|
||||
config.prerollPoll = std::chrono::milliseconds(2);
|
||||
config.runtimeShaderId = "happy-accident";
|
||||
return config;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
|
||||
#include <chrono>
|
||||
#include <cstddef>
|
||||
#include <string>
|
||||
|
||||
namespace RenderCadenceCompositor
|
||||
{
|
||||
@@ -18,6 +19,7 @@ struct AppConfig
|
||||
std::chrono::milliseconds warmupTimeout = std::chrono::seconds(3);
|
||||
std::chrono::milliseconds prerollTimeout = std::chrono::seconds(3);
|
||||
std::chrono::milliseconds prerollPoll = std::chrono::milliseconds(2);
|
||||
std::string runtimeShaderId = "happy-accident";
|
||||
};
|
||||
|
||||
AppConfig DefaultAppConfig();
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#pragma once
|
||||
|
||||
#include "AppConfig.h"
|
||||
#include "../runtime/RuntimeSlangShaderCompiler.h"
|
||||
#include "../runtime/RuntimeShaderBridge.h"
|
||||
#include "../telemetry/TelemetryPrinter.h"
|
||||
#include "../video/DeckLinkOutput.h"
|
||||
#include "../video/DeckLinkOutputThread.h"
|
||||
@@ -143,34 +143,19 @@ private:
|
||||
|
||||
void StartRuntimeShaderBuild()
|
||||
{
|
||||
mShaderCompiler.StartHappyAccidentBuild();
|
||||
mShaderBridgeStopping = false;
|
||||
mShaderBridgeThread = std::thread([this]() { ShaderBridgeMain(); });
|
||||
mShaderBridge.Start(
|
||||
mConfig.runtimeShaderId,
|
||||
[this](const RuntimeShaderArtifact& artifact) {
|
||||
mRenderThread.SubmitRuntimeShaderArtifact(artifact);
|
||||
},
|
||||
[](const std::string& message) {
|
||||
std::cout << "Runtime Slang build failed: " << message << "\n";
|
||||
});
|
||||
}
|
||||
|
||||
void StopRuntimeShaderBuild()
|
||||
{
|
||||
mShaderBridgeStopping = true;
|
||||
if (mShaderBridgeThread.joinable())
|
||||
mShaderBridgeThread.join();
|
||||
mShaderCompiler.Stop();
|
||||
}
|
||||
|
||||
void ShaderBridgeMain()
|
||||
{
|
||||
while (!mShaderBridgeStopping)
|
||||
{
|
||||
RuntimeSlangShaderBuild build;
|
||||
if (mShaderCompiler.TryConsume(build))
|
||||
{
|
||||
if (build.succeeded)
|
||||
mRenderThread.SubmitRuntimeShaderArtifact(build.artifact);
|
||||
else
|
||||
std::cout << "Runtime Slang build failed: " << build.message << "\n";
|
||||
return;
|
||||
}
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(5));
|
||||
}
|
||||
mShaderBridge.Stop();
|
||||
}
|
||||
|
||||
RenderThread& mRenderThread;
|
||||
@@ -179,9 +164,7 @@ private:
|
||||
DeckLinkOutput mOutput;
|
||||
DeckLinkOutputThread<SystemFrameExchange> mOutputThread;
|
||||
TelemetryPrinter mTelemetry;
|
||||
RuntimeSlangShaderCompiler mShaderCompiler;
|
||||
std::thread mShaderBridgeThread;
|
||||
std::atomic<bool> mShaderBridgeStopping{ false };
|
||||
RuntimeShaderBridge mShaderBridge;
|
||||
bool mStarted = false;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -9,7 +9,6 @@
|
||||
#include "SimpleMotionRenderer.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <iostream>
|
||||
#include <thread>
|
||||
#include <windows.h>
|
||||
|
||||
@@ -220,16 +219,14 @@ void RenderThread::TryCommitReadyRuntimeShader(RuntimeShaderRenderer& runtimeSha
|
||||
return;
|
||||
|
||||
std::string commitError;
|
||||
if (!runtimeShaderRenderer.CommitFragmentShader(artifact.fragmentShaderSource, commitError))
|
||||
if (!runtimeShaderRenderer.CommitShaderArtifact(artifact, commitError))
|
||||
{
|
||||
std::cout << "Runtime shader GL commit failed: " << commitError << "\n";
|
||||
OutputDebugStringA(("Runtime shader GL commit failed: " + commitError + "\n").c_str());
|
||||
std::lock_guard<std::mutex> lock(mMetricsMutex);
|
||||
++mMetrics.shaderBuildFailures;
|
||||
return;
|
||||
}
|
||||
|
||||
std::cout << "Runtime shader committed: " << artifact.shaderId << ". " << artifact.message << "\n";
|
||||
OutputDebugStringA(("Runtime shader committed: " + artifact.shaderId + ". " + artifact.message + "\n").c_str());
|
||||
std::lock_guard<std::mutex> lock(mMetricsMutex);
|
||||
++mMetrics.shaderBuildsCommitted;
|
||||
|
||||
120
apps/RenderCadenceCompositor/render/RuntimeShaderParams.cpp
Normal file
120
apps/RenderCadenceCompositor/render/RuntimeShaderParams.cpp
Normal file
@@ -0,0 +1,120 @@
|
||||
#include "RuntimeShaderParams.h"
|
||||
|
||||
#include "Std140Buffer.h"
|
||||
|
||||
#include <chrono>
|
||||
|
||||
namespace
|
||||
{
|
||||
ShaderParameterValue DefaultValueForDefinition(const ShaderParameterDefinition& definition)
|
||||
{
|
||||
ShaderParameterValue value;
|
||||
switch (definition.type)
|
||||
{
|
||||
case ShaderParameterType::Float:
|
||||
value.numberValues = definition.defaultNumbers.empty() ? std::vector<double>{ 0.0 } : definition.defaultNumbers;
|
||||
break;
|
||||
case ShaderParameterType::Vec2:
|
||||
value.numberValues = definition.defaultNumbers.size() == 2 ? definition.defaultNumbers : std::vector<double>{ 0.0, 0.0 };
|
||||
break;
|
||||
case ShaderParameterType::Color:
|
||||
value.numberValues = definition.defaultNumbers.size() == 4 ? definition.defaultNumbers : std::vector<double>{ 1.0, 1.0, 1.0, 1.0 };
|
||||
break;
|
||||
case ShaderParameterType::Boolean:
|
||||
value.booleanValue = definition.defaultBoolean;
|
||||
break;
|
||||
case ShaderParameterType::Enum:
|
||||
value.enumValue = definition.defaultEnumValue;
|
||||
break;
|
||||
case ShaderParameterType::Text:
|
||||
value.textValue = definition.defaultTextValue;
|
||||
break;
|
||||
case ShaderParameterType::Trigger:
|
||||
value.numberValues = { 0.0, -1000000.0 };
|
||||
break;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
int EnumIndexForDefault(const ShaderParameterDefinition& definition, const ShaderParameterValue& value)
|
||||
{
|
||||
for (std::size_t optionIndex = 0; optionIndex < definition.enumOptions.size(); ++optionIndex)
|
||||
{
|
||||
if (definition.enumOptions[optionIndex].value == value.enumValue)
|
||||
return static_cast<int>(optionIndex);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
double UtcSecondsOfDay()
|
||||
{
|
||||
const auto now = std::chrono::system_clock::now();
|
||||
const auto seconds = std::chrono::duration_cast<std::chrono::seconds>(now.time_since_epoch()).count();
|
||||
const long long secondsPerDay = 24 * 60 * 60;
|
||||
long long daySeconds = seconds % secondsPerDay;
|
||||
if (daySeconds < 0)
|
||||
daySeconds += secondsPerDay;
|
||||
return static_cast<double>(daySeconds);
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<unsigned char> BuildRuntimeShaderGlobalParamsStd140(
|
||||
const RuntimeShaderArtifact& artifact,
|
||||
uint64_t frameIndex,
|
||||
unsigned width,
|
||||
unsigned height)
|
||||
{
|
||||
std::vector<unsigned char> buffer;
|
||||
buffer.reserve(512);
|
||||
|
||||
AppendStd140Float(buffer, static_cast<float>(frameIndex) / 60.0f);
|
||||
AppendStd140Vec2(buffer, static_cast<float>(width), static_cast<float>(height));
|
||||
AppendStd140Vec2(buffer, static_cast<float>(width), static_cast<float>(height));
|
||||
AppendStd140Float(buffer, static_cast<float>(UtcSecondsOfDay()));
|
||||
AppendStd140Float(buffer, 0.0f);
|
||||
AppendStd140Float(buffer, 0.37f);
|
||||
AppendStd140Float(buffer, static_cast<float>(frameIndex));
|
||||
AppendStd140Float(buffer, 1.0f);
|
||||
AppendStd140Float(buffer, 0.0f);
|
||||
AppendStd140Int(buffer, 0);
|
||||
AppendStd140Int(buffer, 0);
|
||||
AppendStd140Int(buffer, 0);
|
||||
|
||||
for (const ShaderParameterDefinition& definition : artifact.parameterDefinitions)
|
||||
{
|
||||
const ShaderParameterValue value = DefaultValueForDefinition(definition);
|
||||
switch (definition.type)
|
||||
{
|
||||
case ShaderParameterType::Float:
|
||||
AppendStd140Float(buffer, value.numberValues.empty() ? 0.0f : static_cast<float>(value.numberValues[0]));
|
||||
break;
|
||||
case ShaderParameterType::Vec2:
|
||||
AppendStd140Vec2(buffer,
|
||||
value.numberValues.size() > 0 ? static_cast<float>(value.numberValues[0]) : 0.0f,
|
||||
value.numberValues.size() > 1 ? static_cast<float>(value.numberValues[1]) : 0.0f);
|
||||
break;
|
||||
case ShaderParameterType::Color:
|
||||
AppendStd140Vec4(buffer,
|
||||
value.numberValues.size() > 0 ? static_cast<float>(value.numberValues[0]) : 1.0f,
|
||||
value.numberValues.size() > 1 ? static_cast<float>(value.numberValues[1]) : 1.0f,
|
||||
value.numberValues.size() > 2 ? static_cast<float>(value.numberValues[2]) : 1.0f,
|
||||
value.numberValues.size() > 3 ? static_cast<float>(value.numberValues[3]) : 1.0f);
|
||||
break;
|
||||
case ShaderParameterType::Boolean:
|
||||
AppendStd140Int(buffer, value.booleanValue ? 1 : 0);
|
||||
break;
|
||||
case ShaderParameterType::Enum:
|
||||
AppendStd140Int(buffer, EnumIndexForDefault(definition, value));
|
||||
break;
|
||||
case ShaderParameterType::Text:
|
||||
break;
|
||||
case ShaderParameterType::Trigger:
|
||||
AppendStd140Int(buffer, 0);
|
||||
AppendStd140Float(buffer, -1000000.0f);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
buffer.resize(AlignStd140(buffer.size(), 16), 0);
|
||||
return buffer;
|
||||
}
|
||||
12
apps/RenderCadenceCompositor/render/RuntimeShaderParams.h
Normal file
12
apps/RenderCadenceCompositor/render/RuntimeShaderParams.h
Normal file
@@ -0,0 +1,12 @@
|
||||
#pragma once
|
||||
|
||||
#include "../runtime/RuntimeShaderArtifact.h"
|
||||
|
||||
#include <cstdint>
|
||||
#include <vector>
|
||||
|
||||
std::vector<unsigned char> BuildRuntimeShaderGlobalParamsStd140(
|
||||
const RuntimeShaderArtifact& artifact,
|
||||
uint64_t frameIndex,
|
||||
unsigned width,
|
||||
unsigned height);
|
||||
@@ -1,5 +1,7 @@
|
||||
#include "RuntimeShaderRenderer.h"
|
||||
|
||||
#include "RuntimeShaderParams.h"
|
||||
|
||||
#include <array>
|
||||
#include <cstring>
|
||||
#include <string>
|
||||
@@ -7,6 +9,7 @@
|
||||
namespace
|
||||
{
|
||||
constexpr GLuint kGlobalParamsBindingPoint = 0;
|
||||
constexpr GLuint kSourceTextureUnit = 0;
|
||||
|
||||
const char* kVertexShaderSource = R"GLSL(
|
||||
#version 430 core
|
||||
@@ -26,28 +29,6 @@ void main()
|
||||
}
|
||||
)GLSL";
|
||||
|
||||
struct GlobalParamsStd140
|
||||
{
|
||||
float time = 0.0f;
|
||||
float pad0 = 0.0f;
|
||||
float inputResolution[2] = {};
|
||||
float outputResolution[2] = {};
|
||||
float utcTimeSeconds = 0.0f;
|
||||
float utcOffsetSeconds = 0.0f;
|
||||
float startupRandom = 0.37f;
|
||||
float frameCount = 0.0f;
|
||||
float mixAmount = 1.0f;
|
||||
float bypass = 0.0f;
|
||||
int sourceHistoryLength = 0;
|
||||
int temporalHistoryLength = 0;
|
||||
int feedbackAvailable = 0;
|
||||
float speed = 1.0f;
|
||||
float scale = 1.0f;
|
||||
float raySteps = 77.0f;
|
||||
float intensity = 1.0f;
|
||||
float sourceMix = 0.0f;
|
||||
float pad1[3] = {};
|
||||
};
|
||||
}
|
||||
|
||||
RuntimeShaderRenderer::~RuntimeShaderRenderer()
|
||||
@@ -57,7 +38,16 @@ RuntimeShaderRenderer::~RuntimeShaderRenderer()
|
||||
|
||||
bool RuntimeShaderRenderer::CommitFragmentShader(const std::string& fragmentShaderSource, std::string& error)
|
||||
{
|
||||
if (fragmentShaderSource.empty())
|
||||
RuntimeShaderArtifact artifact;
|
||||
artifact.shaderId = "runtime-fragment";
|
||||
artifact.displayName = "Runtime Fragment";
|
||||
artifact.fragmentShaderSource = fragmentShaderSource;
|
||||
return CommitShaderArtifact(artifact, error);
|
||||
}
|
||||
|
||||
bool RuntimeShaderRenderer::CommitShaderArtifact(const RuntimeShaderArtifact& artifact, std::string& error)
|
||||
{
|
||||
if (artifact.fragmentShaderSource.empty())
|
||||
{
|
||||
error = "Cannot commit an empty fragment shader.";
|
||||
return false;
|
||||
@@ -69,50 +59,15 @@ bool RuntimeShaderRenderer::CommitFragmentShader(const std::string& fragmentShad
|
||||
GLuint vertexShader = 0;
|
||||
GLuint fragmentShader = 0;
|
||||
GLuint program = 0;
|
||||
if (!CompileShader(GL_VERTEX_SHADER, kVertexShaderSource, vertexShader, error))
|
||||
if (!BuildProgram(artifact.fragmentShaderSource, program, vertexShader, fragmentShader, error))
|
||||
return false;
|
||||
if (!CompileShader(GL_FRAGMENT_SHADER, fragmentShaderSource.c_str(), fragmentShader, error))
|
||||
{
|
||||
glDeleteShader(vertexShader);
|
||||
return false;
|
||||
}
|
||||
|
||||
program = glCreateProgram();
|
||||
glAttachShader(program, vertexShader);
|
||||
glAttachShader(program, fragmentShader);
|
||||
glLinkProgram(program);
|
||||
|
||||
GLint linkResult = GL_FALSE;
|
||||
glGetProgramiv(program, GL_LINK_STATUS, &linkResult);
|
||||
if (linkResult == GL_FALSE)
|
||||
{
|
||||
std::array<char, 4096> log = {};
|
||||
GLsizei length = 0;
|
||||
glGetProgramInfoLog(program, static_cast<GLsizei>(log.size()), &length, log.data());
|
||||
error = std::string(log.data(), static_cast<std::size_t>(length));
|
||||
glDeleteProgram(program);
|
||||
glDeleteShader(vertexShader);
|
||||
glDeleteShader(fragmentShader);
|
||||
return false;
|
||||
}
|
||||
|
||||
const GLuint globalParamsIndex = glGetUniformBlockIndex(program, "GlobalParams");
|
||||
if (globalParamsIndex != GL_INVALID_INDEX)
|
||||
glUniformBlockBinding(program, globalParamsIndex, kGlobalParamsBindingPoint);
|
||||
|
||||
glUseProgram(program);
|
||||
const GLint videoInputLocation = glGetUniformLocation(program, "gVideoInput");
|
||||
if (videoInputLocation >= 0)
|
||||
glUniform1i(videoInputLocation, 0);
|
||||
const GLint layerInputLocation = glGetUniformLocation(program, "gLayerInput");
|
||||
if (layerInputLocation >= 0)
|
||||
glUniform1i(layerInputLocation, 0);
|
||||
glUseProgram(0);
|
||||
|
||||
DestroyProgram();
|
||||
mProgram = program;
|
||||
mVertexShader = vertexShader;
|
||||
mFragmentShader = fragmentShader;
|
||||
mArtifact = artifact;
|
||||
AssignSamplerUniforms(mProgram);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -121,22 +76,12 @@ void RuntimeShaderRenderer::RenderFrame(uint64_t frameIndex, unsigned width, uns
|
||||
if (mProgram == 0)
|
||||
return;
|
||||
|
||||
GlobalParamsStd140 params;
|
||||
params.time = static_cast<float>(frameIndex) / 60.0f;
|
||||
params.inputResolution[0] = static_cast<float>(width);
|
||||
params.inputResolution[1] = static_cast<float>(height);
|
||||
params.outputResolution[0] = static_cast<float>(width);
|
||||
params.outputResolution[1] = static_cast<float>(height);
|
||||
params.frameCount = static_cast<float>(frameIndex);
|
||||
|
||||
glViewport(0, 0, static_cast<GLsizei>(width), static_cast<GLsizei>(height));
|
||||
glDisable(GL_SCISSOR_TEST);
|
||||
glDisable(GL_DEPTH_TEST);
|
||||
glDisable(GL_BLEND);
|
||||
glBindBuffer(GL_UNIFORM_BUFFER, mGlobalParamsBuffer);
|
||||
glBufferSubData(GL_UNIFORM_BUFFER, 0, sizeof(params), ¶ms);
|
||||
glBindBufferBase(GL_UNIFORM_BUFFER, kGlobalParamsBindingPoint, mGlobalParamsBuffer);
|
||||
glBindBuffer(GL_UNIFORM_BUFFER, 0);
|
||||
UpdateGlobalParams(frameIndex, width, height);
|
||||
BindRuntimeTextures();
|
||||
glBindVertexArray(mVertexArray);
|
||||
glUseProgram(mProgram);
|
||||
glDrawArrays(GL_TRIANGLES, 0, 3);
|
||||
@@ -158,11 +103,28 @@ bool RuntimeShaderRenderer::EnsureStaticGlResources(std::string& error)
|
||||
{
|
||||
glGenBuffers(1, &mGlobalParamsBuffer);
|
||||
glBindBuffer(GL_UNIFORM_BUFFER, mGlobalParamsBuffer);
|
||||
glBufferData(GL_UNIFORM_BUFFER, static_cast<GLsizeiptr>(sizeof(GlobalParamsStd140)), nullptr, GL_DYNAMIC_DRAW);
|
||||
glBufferData(GL_UNIFORM_BUFFER, 1024, nullptr, GL_DYNAMIC_DRAW);
|
||||
glBindBuffer(GL_UNIFORM_BUFFER, 0);
|
||||
}
|
||||
if (mFallbackSourceTexture == 0)
|
||||
{
|
||||
const unsigned char pixels[] = {
|
||||
0, 0, 0, 255,
|
||||
96, 64, 32, 255,
|
||||
64, 96, 160, 255,
|
||||
255, 255, 255, 255
|
||||
};
|
||||
glGenTextures(1, &mFallbackSourceTexture);
|
||||
glBindTexture(GL_TEXTURE_2D, mFallbackSourceTexture);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
|
||||
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, 2, 2, 0, GL_BGRA, GL_UNSIGNED_BYTE, pixels);
|
||||
glBindTexture(GL_TEXTURE_2D, 0);
|
||||
}
|
||||
|
||||
if (mVertexArray == 0 || mGlobalParamsBuffer == 0)
|
||||
if (mVertexArray == 0 || mGlobalParamsBuffer == 0 || mFallbackSourceTexture == 0)
|
||||
{
|
||||
error = "Failed to create runtime shader GL resources.";
|
||||
return false;
|
||||
@@ -170,6 +132,93 @@ bool RuntimeShaderRenderer::EnsureStaticGlResources(std::string& error)
|
||||
return true;
|
||||
}
|
||||
|
||||
bool RuntimeShaderRenderer::BuildProgram(const std::string& fragmentShaderSource, GLuint& program, GLuint& vertexShader, GLuint& fragmentShader, std::string& error)
|
||||
{
|
||||
program = 0;
|
||||
vertexShader = 0;
|
||||
fragmentShader = 0;
|
||||
|
||||
if (!CompileShader(GL_VERTEX_SHADER, kVertexShaderSource, vertexShader, error))
|
||||
return false;
|
||||
if (!CompileShader(GL_FRAGMENT_SHADER, fragmentShaderSource.c_str(), fragmentShader, error))
|
||||
{
|
||||
glDeleteShader(vertexShader);
|
||||
vertexShader = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
program = glCreateProgram();
|
||||
glAttachShader(program, vertexShader);
|
||||
glAttachShader(program, fragmentShader);
|
||||
glLinkProgram(program);
|
||||
|
||||
GLint linkResult = GL_FALSE;
|
||||
glGetProgramiv(program, GL_LINK_STATUS, &linkResult);
|
||||
if (linkResult == GL_FALSE)
|
||||
{
|
||||
std::array<char, 4096> log = {};
|
||||
GLsizei length = 0;
|
||||
glGetProgramInfoLog(program, static_cast<GLsizei>(log.size()), &length, log.data());
|
||||
error = std::string(log.data(), static_cast<std::size_t>(length));
|
||||
glDeleteProgram(program);
|
||||
glDeleteShader(vertexShader);
|
||||
glDeleteShader(fragmentShader);
|
||||
program = 0;
|
||||
vertexShader = 0;
|
||||
fragmentShader = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
const GLuint globalParamsIndex = glGetUniformBlockIndex(program, "GlobalParams");
|
||||
if (globalParamsIndex != GL_INVALID_INDEX)
|
||||
glUniformBlockBinding(program, globalParamsIndex, kGlobalParamsBindingPoint);
|
||||
return true;
|
||||
}
|
||||
|
||||
void RuntimeShaderRenderer::AssignSamplerUniforms(GLuint program) const
|
||||
{
|
||||
glUseProgram(program);
|
||||
const GLint videoInputLocation = glGetUniformLocation(program, "gVideoInput");
|
||||
if (videoInputLocation >= 0)
|
||||
glUniform1i(videoInputLocation, static_cast<GLint>(kSourceTextureUnit));
|
||||
const GLint videoInputArrayLocation = glGetUniformLocation(program, "gVideoInput_0");
|
||||
if (videoInputArrayLocation >= 0)
|
||||
glUniform1i(videoInputArrayLocation, static_cast<GLint>(kSourceTextureUnit));
|
||||
const GLint layerInputLocation = glGetUniformLocation(program, "gLayerInput");
|
||||
if (layerInputLocation >= 0)
|
||||
glUniform1i(layerInputLocation, static_cast<GLint>(kSourceTextureUnit));
|
||||
const GLint layerInputArrayLocation = glGetUniformLocation(program, "gLayerInput_0");
|
||||
if (layerInputArrayLocation >= 0)
|
||||
glUniform1i(layerInputArrayLocation, static_cast<GLint>(kSourceTextureUnit));
|
||||
glUseProgram(0);
|
||||
}
|
||||
|
||||
void RuntimeShaderRenderer::UpdateGlobalParams(uint64_t frameIndex, unsigned width, unsigned height)
|
||||
{
|
||||
std::vector<unsigned char>& buffer = mGlobalParamsScratch;
|
||||
buffer = BuildRuntimeShaderGlobalParamsStd140(mArtifact, frameIndex, width, height);
|
||||
glBindBuffer(GL_UNIFORM_BUFFER, mGlobalParamsBuffer);
|
||||
const GLsizeiptr bufferSize = static_cast<GLsizeiptr>(buffer.size());
|
||||
if (mGlobalParamsBufferSize != bufferSize)
|
||||
{
|
||||
glBufferData(GL_UNIFORM_BUFFER, bufferSize, buffer.data(), GL_DYNAMIC_DRAW);
|
||||
mGlobalParamsBufferSize = bufferSize;
|
||||
}
|
||||
else
|
||||
{
|
||||
glBufferSubData(GL_UNIFORM_BUFFER, 0, bufferSize, buffer.data());
|
||||
}
|
||||
glBindBufferBase(GL_UNIFORM_BUFFER, kGlobalParamsBindingPoint, mGlobalParamsBuffer);
|
||||
glBindBuffer(GL_UNIFORM_BUFFER, 0);
|
||||
}
|
||||
|
||||
void RuntimeShaderRenderer::BindRuntimeTextures()
|
||||
{
|
||||
glActiveTexture(GL_TEXTURE0 + kSourceTextureUnit);
|
||||
glBindTexture(GL_TEXTURE_2D, mFallbackSourceTexture);
|
||||
glActiveTexture(GL_TEXTURE0);
|
||||
}
|
||||
|
||||
bool RuntimeShaderRenderer::CompileShader(GLenum shaderType, const char* source, GLuint& shader, std::string& error) const
|
||||
{
|
||||
shader = glCreateShader(shaderType);
|
||||
@@ -209,6 +258,10 @@ void RuntimeShaderRenderer::DestroyStaticGlResources()
|
||||
glDeleteBuffers(1, &mGlobalParamsBuffer);
|
||||
if (mVertexArray != 0)
|
||||
glDeleteVertexArrays(1, &mVertexArray);
|
||||
if (mFallbackSourceTexture != 0)
|
||||
glDeleteTextures(1, &mFallbackSourceTexture);
|
||||
mGlobalParamsBuffer = 0;
|
||||
mGlobalParamsBufferSize = 0;
|
||||
mVertexArray = 0;
|
||||
mFallbackSourceTexture = 0;
|
||||
}
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
#pragma once
|
||||
|
||||
#include "GLExtensions.h"
|
||||
#include "../runtime/RuntimeShaderArtifact.h"
|
||||
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
class RuntimeShaderRenderer
|
||||
{
|
||||
@@ -14,6 +16,7 @@ public:
|
||||
~RuntimeShaderRenderer();
|
||||
|
||||
bool CommitFragmentShader(const std::string& fragmentShaderSource, std::string& error);
|
||||
bool CommitShaderArtifact(const RuntimeShaderArtifact& artifact, std::string& error);
|
||||
bool HasProgram() const { return mProgram != 0; }
|
||||
void RenderFrame(uint64_t frameIndex, unsigned width, unsigned height);
|
||||
void ShutdownGl();
|
||||
@@ -21,12 +24,20 @@ public:
|
||||
private:
|
||||
bool EnsureStaticGlResources(std::string& error);
|
||||
bool CompileShader(GLenum shaderType, const char* source, GLuint& shader, std::string& error) const;
|
||||
bool BuildProgram(const std::string& fragmentShaderSource, GLuint& program, GLuint& vertexShader, GLuint& fragmentShader, std::string& error);
|
||||
void AssignSamplerUniforms(GLuint program) const;
|
||||
void UpdateGlobalParams(uint64_t frameIndex, unsigned width, unsigned height);
|
||||
void BindRuntimeTextures();
|
||||
void DestroyProgram();
|
||||
void DestroyStaticGlResources();
|
||||
|
||||
RuntimeShaderArtifact mArtifact;
|
||||
GLuint mProgram = 0;
|
||||
GLuint mVertexShader = 0;
|
||||
GLuint mFragmentShader = 0;
|
||||
GLuint mVertexArray = 0;
|
||||
GLuint mGlobalParamsBuffer = 0;
|
||||
GLsizeiptr mGlobalParamsBufferSize = 0;
|
||||
GLuint mFallbackSourceTexture = 0;
|
||||
std::vector<unsigned char> mGlobalParamsScratch;
|
||||
};
|
||||
|
||||
@@ -1,10 +1,15 @@
|
||||
#pragma once
|
||||
|
||||
#include "ShaderTypes.h"
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
struct RuntimeShaderArtifact
|
||||
{
|
||||
std::string shaderId;
|
||||
std::string displayName;
|
||||
std::string fragmentShaderSource;
|
||||
std::string message;
|
||||
std::vector<ShaderParameterDefinition> parameterDefinitions;
|
||||
};
|
||||
|
||||
53
apps/RenderCadenceCompositor/runtime/RuntimeShaderBridge.cpp
Normal file
53
apps/RenderCadenceCompositor/runtime/RuntimeShaderBridge.cpp
Normal file
@@ -0,0 +1,53 @@
|
||||
#include "RuntimeShaderBridge.h"
|
||||
|
||||
#include <chrono>
|
||||
|
||||
RuntimeShaderBridge::~RuntimeShaderBridge()
|
||||
{
|
||||
Stop();
|
||||
}
|
||||
|
||||
void RuntimeShaderBridge::Start(const std::string& shaderId, ArtifactCallback onArtifactReady, ErrorCallback onError)
|
||||
{
|
||||
Stop();
|
||||
if (shaderId.empty())
|
||||
return;
|
||||
|
||||
mOnArtifactReady = std::move(onArtifactReady);
|
||||
mOnError = std::move(onError);
|
||||
mStopping.store(false, std::memory_order_release);
|
||||
mCompiler.StartShaderBuild(shaderId);
|
||||
mThread = std::thread([this]() { ThreadMain(); });
|
||||
}
|
||||
|
||||
void RuntimeShaderBridge::Stop()
|
||||
{
|
||||
mStopping.store(true, std::memory_order_release);
|
||||
if (mThread.joinable())
|
||||
mThread.join();
|
||||
mCompiler.Stop();
|
||||
mOnArtifactReady = ArtifactCallback();
|
||||
mOnError = ErrorCallback();
|
||||
}
|
||||
|
||||
void RuntimeShaderBridge::ThreadMain()
|
||||
{
|
||||
while (!mStopping.load(std::memory_order_acquire))
|
||||
{
|
||||
RuntimeSlangShaderBuild build;
|
||||
if (mCompiler.TryConsume(build))
|
||||
{
|
||||
if (build.succeeded)
|
||||
{
|
||||
if (mOnArtifactReady)
|
||||
mOnArtifactReady(build.artifact);
|
||||
}
|
||||
else if (mOnError)
|
||||
{
|
||||
mOnError(build.message);
|
||||
}
|
||||
return;
|
||||
}
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(5));
|
||||
}
|
||||
}
|
||||
33
apps/RenderCadenceCompositor/runtime/RuntimeShaderBridge.h
Normal file
33
apps/RenderCadenceCompositor/runtime/RuntimeShaderBridge.h
Normal file
@@ -0,0 +1,33 @@
|
||||
#pragma once
|
||||
|
||||
#include "RuntimeShaderArtifact.h"
|
||||
#include "RuntimeSlangShaderCompiler.h"
|
||||
|
||||
#include <atomic>
|
||||
#include <functional>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
|
||||
class RuntimeShaderBridge
|
||||
{
|
||||
public:
|
||||
using ArtifactCallback = std::function<void(const RuntimeShaderArtifact&)>;
|
||||
using ErrorCallback = std::function<void(const std::string&)>;
|
||||
|
||||
RuntimeShaderBridge() = default;
|
||||
RuntimeShaderBridge(const RuntimeShaderBridge&) = delete;
|
||||
RuntimeShaderBridge& operator=(const RuntimeShaderBridge&) = delete;
|
||||
~RuntimeShaderBridge();
|
||||
|
||||
void Start(const std::string& shaderId, ArtifactCallback onArtifactReady, ErrorCallback onError);
|
||||
void Stop();
|
||||
|
||||
private:
|
||||
void ThreadMain();
|
||||
|
||||
RuntimeSlangShaderCompiler mCompiler;
|
||||
std::thread mThread;
|
||||
std::atomic<bool> mStopping{ false };
|
||||
ArtifactCallback mOnArtifactReady;
|
||||
ErrorCallback mOnError;
|
||||
};
|
||||
@@ -1,6 +1,7 @@
|
||||
#include "RuntimeSlangShaderCompiler.h"
|
||||
|
||||
#include "ShaderCompiler.h"
|
||||
#include "ShaderPackageRegistry.h"
|
||||
#include "ShaderTypes.h"
|
||||
|
||||
#include <chrono>
|
||||
@@ -9,16 +10,6 @@
|
||||
|
||||
namespace
|
||||
{
|
||||
ShaderParameterDefinition FloatParam(const std::string& id, double defaultValue)
|
||||
{
|
||||
ShaderParameterDefinition parameter;
|
||||
parameter.id = id;
|
||||
parameter.label = id;
|
||||
parameter.type = ShaderParameterType::Float;
|
||||
parameter.defaultNumbers.push_back(defaultValue);
|
||||
return parameter;
|
||||
}
|
||||
|
||||
std::filesystem::path FindRepoRoot()
|
||||
{
|
||||
std::filesystem::path current = std::filesystem::current_path();
|
||||
@@ -36,6 +27,44 @@ std::filesystem::path FindRepoRoot()
|
||||
current = parent;
|
||||
}
|
||||
}
|
||||
|
||||
bool IsStatelessSinglePassPackage(const ShaderPackage& shaderPackage, std::string& error)
|
||||
{
|
||||
if (shaderPackage.passes.size() != 1)
|
||||
{
|
||||
error = "RenderCadenceCompositor currently supports only single-pass runtime shaders.";
|
||||
return false;
|
||||
}
|
||||
if (shaderPackage.temporal.enabled)
|
||||
{
|
||||
error = "RenderCadenceCompositor currently supports only stateless shaders; temporal history is not enabled in this app.";
|
||||
return false;
|
||||
}
|
||||
if (shaderPackage.feedback.enabled)
|
||||
{
|
||||
error = "RenderCadenceCompositor currently supports only stateless shaders; feedback storage is not enabled in this app.";
|
||||
return false;
|
||||
}
|
||||
if (!shaderPackage.textureAssets.empty())
|
||||
{
|
||||
error = "RenderCadenceCompositor does not load shader texture assets on the render thread; texture-backed shaders need a CPU-prepared asset handoff first.";
|
||||
return false;
|
||||
}
|
||||
if (!shaderPackage.fontAssets.empty())
|
||||
{
|
||||
error = "RenderCadenceCompositor does not load shader font assets on the render thread; text shaders need a CPU-prepared asset handoff first.";
|
||||
return false;
|
||||
}
|
||||
for (const ShaderParameterDefinition& parameter : shaderPackage.parameters)
|
||||
{
|
||||
if (parameter.type == ShaderParameterType::Text)
|
||||
{
|
||||
error = "RenderCadenceCompositor currently skips text parameters because they require per-shader text texture storage.";
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
RuntimeSlangShaderCompiler::~RuntimeSlangShaderCompiler()
|
||||
@@ -44,6 +73,11 @@ RuntimeSlangShaderCompiler::~RuntimeSlangShaderCompiler()
|
||||
}
|
||||
|
||||
void RuntimeSlangShaderCompiler::StartHappyAccidentBuild()
|
||||
{
|
||||
StartShaderBuild("happy-accident");
|
||||
}
|
||||
|
||||
void RuntimeSlangShaderCompiler::StartShaderBuild(const std::string& shaderId)
|
||||
{
|
||||
if (mRunning.load(std::memory_order_acquire))
|
||||
return;
|
||||
@@ -57,8 +91,8 @@ void RuntimeSlangShaderCompiler::StartHappyAccidentBuild()
|
||||
}
|
||||
|
||||
mRunning.store(true, std::memory_order_release);
|
||||
mThread = std::thread([this]() {
|
||||
RuntimeSlangShaderBuild build = BuildHappyAccident();
|
||||
mThread = std::thread([this, shaderId]() {
|
||||
RuntimeSlangShaderBuild build = BuildShader(shaderId);
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mMutex);
|
||||
mReadyBuild = std::move(build);
|
||||
@@ -86,57 +120,57 @@ bool RuntimeSlangShaderCompiler::TryConsume(RuntimeSlangShaderBuild& build)
|
||||
return true;
|
||||
}
|
||||
|
||||
RuntimeSlangShaderBuild RuntimeSlangShaderCompiler::BuildHappyAccident() const
|
||||
RuntimeSlangShaderBuild RuntimeSlangShaderCompiler::BuildShader(const std::string& shaderId) const
|
||||
{
|
||||
RuntimeSlangShaderBuild build;
|
||||
build.artifact.shaderId = "happy-accident";
|
||||
build.artifact.shaderId = shaderId;
|
||||
|
||||
try
|
||||
{
|
||||
const std::filesystem::path repoRoot = FindRepoRoot();
|
||||
const std::filesystem::path shaderDir = repoRoot / "shaders" / "happy-accident";
|
||||
const std::filesystem::path shaderDir = repoRoot / "shaders" / shaderId;
|
||||
const std::filesystem::path runtimeBuildDir = repoRoot / "runtime" / "generated" / "render-cadence-compositor";
|
||||
|
||||
ShaderPackageRegistry registry(0);
|
||||
ShaderPackage shaderPackage;
|
||||
shaderPackage.id = "happy-accident";
|
||||
shaderPackage.displayName = "Happy Accident";
|
||||
shaderPackage.entryPoint = "shadeVideo";
|
||||
shaderPackage.directoryPath = shaderDir;
|
||||
shaderPackage.shaderPath = shaderDir / "shader.slang";
|
||||
shaderPackage.manifestPath = shaderDir / "shader.json";
|
||||
shaderPackage.parameters.push_back(FloatParam("speed", 1.0));
|
||||
shaderPackage.parameters.push_back(FloatParam("scale", 1.0));
|
||||
shaderPackage.parameters.push_back(FloatParam("raySteps", 77.0));
|
||||
shaderPackage.parameters.push_back(FloatParam("intensity", 1.0));
|
||||
shaderPackage.parameters.push_back(FloatParam("sourceMix", 0.0));
|
||||
std::string error;
|
||||
if (!registry.ParseManifest(shaderDir / "shader.json", shaderPackage, error))
|
||||
{
|
||||
build.succeeded = false;
|
||||
build.message = error.empty() ? "Shader manifest parse failed." : error;
|
||||
return build;
|
||||
}
|
||||
if (!IsStatelessSinglePassPackage(shaderPackage, error))
|
||||
{
|
||||
build.succeeded = false;
|
||||
build.message = error;
|
||||
return build;
|
||||
}
|
||||
|
||||
ShaderPassDefinition pass;
|
||||
pass.id = "main";
|
||||
pass.entryPoint = shaderPackage.entryPoint;
|
||||
pass.sourcePath = shaderPackage.shaderPath;
|
||||
pass.outputName = "output";
|
||||
shaderPackage.passes.push_back(pass);
|
||||
const ShaderPassDefinition& pass = shaderPackage.passes.front();
|
||||
|
||||
ShaderCompiler compiler(
|
||||
repoRoot,
|
||||
runtimeBuildDir / "happy_accident_wrapper.slang",
|
||||
runtimeBuildDir / "happy_accident.generated.glsl",
|
||||
runtimeBuildDir / "happy_accident.patched.glsl",
|
||||
runtimeBuildDir / (shaderId + ".wrapper.slang"),
|
||||
runtimeBuildDir / (shaderId + ".generated.glsl"),
|
||||
runtimeBuildDir / (shaderId + ".patched.glsl"),
|
||||
0);
|
||||
|
||||
std::string error;
|
||||
const auto start = std::chrono::steady_clock::now();
|
||||
if (!compiler.BuildPassFragmentShaderSource(shaderPackage, pass, build.artifact.fragmentShaderSource, error))
|
||||
{
|
||||
build.succeeded = false;
|
||||
build.message = error.empty() ? "Happy Accident Slang compile failed." : error;
|
||||
build.message = error.empty() ? "Slang compile failed." : error;
|
||||
return build;
|
||||
}
|
||||
|
||||
const auto end = std::chrono::steady_clock::now();
|
||||
const double milliseconds = std::chrono::duration_cast<std::chrono::duration<double, std::milli>>(end - start).count();
|
||||
build.succeeded = true;
|
||||
build.artifact.message = "Happy Accident Slang compile completed in " + std::to_string(milliseconds) + " ms.";
|
||||
build.artifact.shaderId = shaderPackage.id;
|
||||
build.artifact.displayName = shaderPackage.displayName;
|
||||
build.artifact.parameterDefinitions = shaderPackage.parameters;
|
||||
build.artifact.message = shaderPackage.displayName + " Slang compile completed in " + std::to_string(milliseconds) + " ms.";
|
||||
build.message = build.artifact.message;
|
||||
return build;
|
||||
}
|
||||
|
||||
@@ -24,12 +24,13 @@ public:
|
||||
~RuntimeSlangShaderCompiler();
|
||||
|
||||
void StartHappyAccidentBuild();
|
||||
void StartShaderBuild(const std::string& shaderId);
|
||||
void Stop();
|
||||
bool TryConsume(RuntimeSlangShaderBuild& build);
|
||||
bool Running() const { return mRunning.load(std::memory_order_acquire); }
|
||||
|
||||
private:
|
||||
RuntimeSlangShaderBuild BuildHappyAccident() const;
|
||||
RuntimeSlangShaderBuild BuildShader(const std::string& shaderId) const;
|
||||
|
||||
std::thread mThread;
|
||||
std::atomic<bool> mRunning{ false };
|
||||
|
||||
139
docs/RENDER_CADENCE_GOLDEN_RULES.md
Normal file
139
docs/RENDER_CADENCE_GOLDEN_RULES.md
Normal file
@@ -0,0 +1,139 @@
|
||||
# Render Cadence Golden Rules
|
||||
|
||||
These are the non-negotiable rules for the new render-cadence architecture.
|
||||
|
||||
They exist because the old app drifted into a place where DeckLink timing, render work, shader build work, state coordination, readback, and recovery behavior all influenced each other. The new app should stay boring, explicit, and easy to reason about.
|
||||
|
||||
## 1. The Render Thread Owns Its GL Context
|
||||
|
||||
Only the render thread may bind and use its primary OpenGL context.
|
||||
|
||||
Allowed on the render thread:
|
||||
|
||||
- GL resource creation and destruction for resources it owns
|
||||
- GL shader/program commit from an already-prepared artifact
|
||||
- drawing the next frame
|
||||
- async readback queueing and completion polling
|
||||
- publishing completed system-memory frames
|
||||
|
||||
Not allowed on the render thread:
|
||||
|
||||
- Slang compiler invocation
|
||||
- manifest scanning/parsing
|
||||
- filesystem discovery
|
||||
- image/font/LUT decoding
|
||||
- persistence
|
||||
- network/API/OSC handling
|
||||
- DeckLink scheduling
|
||||
- blocking console logging
|
||||
|
||||
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.
|
||||
|
||||
## 2. Render Cadence Does Not Chase Buffers
|
||||
|
||||
The render thread runs at the selected render cadence.
|
||||
|
||||
It must not speed up to fill a DeckLink/system-memory buffer, and it must not slow down because a consumer is late. If the GPU is genuinely overloaded, record that as render overrun telemetry.
|
||||
|
||||
Buffers absorb timing differences. They do not control render cadence.
|
||||
|
||||
## 3. Video I/O Never Renders
|
||||
|
||||
DeckLink output consumes already-rendered system-memory frames.
|
||||
|
||||
The output/scheduling side may:
|
||||
|
||||
- schedule completed frames
|
||||
- release frames after DeckLink completion
|
||||
- report late/dropped/schedule telemetry
|
||||
- record app-side poll misses
|
||||
|
||||
It must not:
|
||||
|
||||
- render fallback frames
|
||||
- invoke GL
|
||||
- compile shaders
|
||||
- block the render cadence waiting for DeckLink
|
||||
|
||||
If no completed frame is available, record the miss and keep the ownership boundary intact.
|
||||
|
||||
## 4. Runtime Build Work Produces Artifacts
|
||||
|
||||
Runtime shader work is split into two phases:
|
||||
|
||||
1. CPU/build phase outside the render thread
|
||||
2. GL commit phase on the render thread
|
||||
|
||||
The CPU/build phase may parse manifests, invoke Slang, validate package shape, and prepare CPU-side data.
|
||||
|
||||
The render thread receives a completed artifact and either commits it at a frame boundary or rejects it. A failed artifact must not disturb the current renderer.
|
||||
|
||||
## 5. No Hidden Blocking In The Cadence Path
|
||||
|
||||
The render loop must not do work with unbounded or OS-dependent latency.
|
||||
|
||||
Examples to avoid:
|
||||
|
||||
- file reads
|
||||
- directory scans
|
||||
- image decoding
|
||||
- process launches
|
||||
- waits on worker threads
|
||||
- blocking locks around slow code
|
||||
- synchronous GPU readback waits
|
||||
- console I/O
|
||||
|
||||
Short mutex use for exchanging small already-prepared objects is acceptable. Holding a lock while doing heavy work is not.
|
||||
|
||||
## 6. System Memory Frames Are A Handoff, Not A Render Driver
|
||||
|
||||
The system-memory frame exchange stores the latest rendered frames and protects frames scheduled to DeckLink.
|
||||
|
||||
It may drop old completed, unscheduled frames when the render thread needs a free slot. It must never force the render thread to wait for the output side to consume a frame.
|
||||
|
||||
## 7. Startup Uses Warmup, Not Burst Rendering
|
||||
|
||||
DeckLink playback starts only after the render thread has produced enough real frames for preroll.
|
||||
|
||||
Warmup should happen at normal render cadence. Do not temporarily accelerate the renderer to fill buffers.
|
||||
|
||||
## 8. Telemetry Must Name Ownership Clearly
|
||||
|
||||
Counters should say which subsystem had the event.
|
||||
|
||||
Good examples:
|
||||
|
||||
- `renderFps`
|
||||
- `scheduleFps`
|
||||
- `completedPollMisses`
|
||||
- `scheduleFailures`
|
||||
- `decklinkBuffered`
|
||||
- `shaderCommitted`
|
||||
- `shaderFailures`
|
||||
|
||||
Avoid ambiguous names like `underrun` unless it is clear whether it means app-ready underrun, DeckLink buffered-frame underrun, render overrun, or schedule failure.
|
||||
|
||||
## 9. Keep Files Small And Role-Based
|
||||
|
||||
A file should have one clear reason to change.
|
||||
|
||||
Preferred boundaries:
|
||||
|
||||
- app orchestration
|
||||
- render cadence/thread ownership
|
||||
- GL rendering
|
||||
- runtime artifact build/bridge
|
||||
- parameter packing
|
||||
- system-memory frame exchange
|
||||
- DeckLink output scheduling
|
||||
- telemetry
|
||||
|
||||
If a file starts coordinating multiple subsystems and doing detailed work for each of them, split it before it becomes the new old app.
|
||||
|
||||
## 10. Prefer Explicit Unsupported States
|
||||
|
||||
If a feature needs storage, timing behavior, or ownership we have not designed yet, reject it clearly.
|
||||
|
||||
For example, in the current new app it is better to reject texture/LUT/text/temporal/feedback shaders than to quietly load files or allocate history state on the render thread.
|
||||
|
||||
Unsupported is healthy when it protects the architecture.
|
||||
110
tests/RenderCadenceCompositorRuntimeShaderParamsTests.cpp
Normal file
110
tests/RenderCadenceCompositorRuntimeShaderParamsTests.cpp
Normal file
@@ -0,0 +1,110 @@
|
||||
#include "RuntimeShaderParams.h"
|
||||
|
||||
#include <cstring>
|
||||
#include <iostream>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace
|
||||
{
|
||||
void Expect(bool condition, const char* message)
|
||||
{
|
||||
if (!condition)
|
||||
{
|
||||
std::cerr << "FAILED: " << message << "\n";
|
||||
std::exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
float ReadFloat(const std::vector<unsigned char>& buffer, std::size_t offset)
|
||||
{
|
||||
float value = 0.0f;
|
||||
std::memcpy(&value, buffer.data() + offset, sizeof(value));
|
||||
return value;
|
||||
}
|
||||
|
||||
int ReadInt(const std::vector<unsigned char>& buffer, std::size_t offset)
|
||||
{
|
||||
int value = 0;
|
||||
std::memcpy(&value, buffer.data() + offset, sizeof(value));
|
||||
return value;
|
||||
}
|
||||
|
||||
ShaderParameterDefinition FloatParam(const std::string& id, double defaultValue)
|
||||
{
|
||||
ShaderParameterDefinition definition;
|
||||
definition.id = id;
|
||||
definition.label = id;
|
||||
definition.type = ShaderParameterType::Float;
|
||||
definition.defaultNumbers = { defaultValue };
|
||||
return definition;
|
||||
}
|
||||
|
||||
ShaderParameterDefinition Vec2Param()
|
||||
{
|
||||
ShaderParameterDefinition definition;
|
||||
definition.id = "offset";
|
||||
definition.label = "Offset";
|
||||
definition.type = ShaderParameterType::Vec2;
|
||||
definition.defaultNumbers = { 2.0, 3.0 };
|
||||
return definition;
|
||||
}
|
||||
|
||||
ShaderParameterDefinition ColorParam()
|
||||
{
|
||||
ShaderParameterDefinition definition;
|
||||
definition.id = "tint";
|
||||
definition.label = "Tint";
|
||||
definition.type = ShaderParameterType::Color;
|
||||
definition.defaultNumbers = { 0.25, 0.5, 0.75, 1.0 };
|
||||
return definition;
|
||||
}
|
||||
|
||||
ShaderParameterDefinition BoolParam()
|
||||
{
|
||||
ShaderParameterDefinition definition;
|
||||
definition.id = "enabled";
|
||||
definition.label = "Enabled";
|
||||
definition.type = ShaderParameterType::Boolean;
|
||||
definition.defaultBoolean = true;
|
||||
return definition;
|
||||
}
|
||||
|
||||
ShaderParameterDefinition EnumParam()
|
||||
{
|
||||
ShaderParameterDefinition definition;
|
||||
definition.id = "mode";
|
||||
definition.label = "Mode";
|
||||
definition.type = ShaderParameterType::Enum;
|
||||
definition.defaultEnumValue = "hard";
|
||||
definition.enumOptions = { { "soft", "Soft" }, { "hard", "Hard" } };
|
||||
return definition;
|
||||
}
|
||||
}
|
||||
|
||||
int main()
|
||||
{
|
||||
RuntimeShaderArtifact artifact;
|
||||
artifact.parameterDefinitions.push_back(FloatParam("gain", 0.5));
|
||||
artifact.parameterDefinitions.push_back(Vec2Param());
|
||||
artifact.parameterDefinitions.push_back(ColorParam());
|
||||
artifact.parameterDefinitions.push_back(BoolParam());
|
||||
artifact.parameterDefinitions.push_back(EnumParam());
|
||||
|
||||
const std::vector<unsigned char> buffer = BuildRuntimeShaderGlobalParamsStd140(artifact, 120, 1920, 1080);
|
||||
|
||||
Expect(buffer.size() == 112, "runtime shader params block keeps expected std140 size");
|
||||
Expect(ReadFloat(buffer, 0) == 2.0f, "time is derived from frame index at 60 fps");
|
||||
Expect(ReadFloat(buffer, 8) == 1920.0f, "input width is packed after std140 vec2 alignment");
|
||||
Expect(ReadFloat(buffer, 12) == 1080.0f, "input height is packed after std140 vec2 alignment");
|
||||
Expect(ReadFloat(buffer, 60) == 0.5f, "float default parameter is packed");
|
||||
Expect(ReadFloat(buffer, 64) == 2.0f, "vec2 default x is packed");
|
||||
Expect(ReadFloat(buffer, 68) == 3.0f, "vec2 default y is packed");
|
||||
Expect(ReadFloat(buffer, 80) == 0.25f, "color default red is packed on vec4 alignment");
|
||||
Expect(ReadFloat(buffer, 92) == 1.0f, "color default alpha is packed");
|
||||
Expect(ReadInt(buffer, 96) == 1, "bool default is packed as int");
|
||||
Expect(ReadInt(buffer, 100) == 1, "enum default is packed as selected option index");
|
||||
|
||||
std::cout << "RenderCadenceCompositorRuntimeShaderParams tests passed.\n";
|
||||
return 0;
|
||||
}
|
||||
Reference in New Issue
Block a user