end point adjsutments
Some checks failed
CI / React UI Build (push) Successful in 11s
CI / Windows Release Package (push) Has been cancelled
CI / Native Windows Build And Tests (push) Has been cancelled

This commit is contained in:
Aiden
2026-05-12 13:50:32 +10:00
parent b44504500a
commit 430cf0733d
11 changed files with 773 additions and 57 deletions

View File

@@ -337,6 +337,8 @@ set(RENDER_CADENCE_APP_SOURCES
"${RENDER_CADENCE_APP_DIR}/runtime/RuntimeShaderBridge.h" "${RENDER_CADENCE_APP_DIR}/runtime/RuntimeShaderBridge.h"
"${RENDER_CADENCE_APP_DIR}/runtime/RuntimeSlangShaderCompiler.cpp" "${RENDER_CADENCE_APP_DIR}/runtime/RuntimeSlangShaderCompiler.cpp"
"${RENDER_CADENCE_APP_DIR}/runtime/RuntimeSlangShaderCompiler.h" "${RENDER_CADENCE_APP_DIR}/runtime/RuntimeSlangShaderCompiler.h"
"${RENDER_CADENCE_APP_DIR}/runtime/SupportedShaderCatalog.cpp"
"${RENDER_CADENCE_APP_DIR}/runtime/SupportedShaderCatalog.h"
"${RENDER_CADENCE_APP_DIR}/telemetry/CadenceTelemetryJson.h" "${RENDER_CADENCE_APP_DIR}/telemetry/CadenceTelemetryJson.h"
"${RENDER_CADENCE_APP_DIR}/telemetry/CadenceTelemetry.h" "${RENDER_CADENCE_APP_DIR}/telemetry/CadenceTelemetry.h"
"${RENDER_CADENCE_APP_DIR}/telemetry/TelemetryHealthMonitor.h" "${RENDER_CADENCE_APP_DIR}/telemetry/TelemetryHealthMonitor.h"
@@ -819,6 +821,26 @@ endif()
add_test(NAME RenderCadenceCompositorRuntimeShaderParamsTests COMMAND RenderCadenceCompositorRuntimeShaderParamsTests) add_test(NAME RenderCadenceCompositorRuntimeShaderParamsTests COMMAND RenderCadenceCompositorRuntimeShaderParamsTests)
add_executable(RenderCadenceCompositorSupportedShaderCatalogTests
"${APP_DIR}/shader/ShaderPackageRegistry.cpp"
"${APP_DIR}/runtime/support/RuntimeJson.cpp"
"${RENDER_CADENCE_APP_DIR}/runtime/SupportedShaderCatalog.cpp"
"${CMAKE_CURRENT_SOURCE_DIR}/tests/RenderCadenceCompositorSupportedShaderCatalogTests.cpp"
)
target_include_directories(RenderCadenceCompositorSupportedShaderCatalogTests PRIVATE
"${APP_DIR}"
"${APP_DIR}/runtime/support"
"${APP_DIR}/shader"
"${RENDER_CADENCE_APP_DIR}/runtime"
)
if(MSVC)
target_compile_options(RenderCadenceCompositorSupportedShaderCatalogTests PRIVATE /W3)
endif()
add_test(NAME RenderCadenceCompositorSupportedShaderCatalogTests COMMAND RenderCadenceCompositorSupportedShaderCatalogTests)
add_executable(RenderCadenceCompositorLoggerTests add_executable(RenderCadenceCompositorLoggerTests
"${RENDER_CADENCE_APP_DIR}/logging/Logger.cpp" "${RENDER_CADENCE_APP_DIR}/logging/Logger.cpp"
"${CMAKE_CURRENT_SOURCE_DIR}/tests/RenderCadenceCompositorLoggerTests.cpp" "${CMAKE_CURRENT_SOURCE_DIR}/tests/RenderCadenceCompositorLoggerTests.cpp"
@@ -849,6 +871,35 @@ endif()
add_test(NAME RenderCadenceCompositorJsonWriterTests COMMAND RenderCadenceCompositorJsonWriterTests) add_test(NAME RenderCadenceCompositorJsonWriterTests COMMAND RenderCadenceCompositorJsonWriterTests)
add_executable(RenderCadenceCompositorRuntimeStateJsonTests
"${APP_DIR}/runtime/support/RuntimeJson.cpp"
"${RENDER_CADENCE_APP_DIR}/app/AppConfig.cpp"
"${RENDER_CADENCE_APP_DIR}/app/AppConfigProvider.cpp"
"${RENDER_CADENCE_APP_DIR}/json/JsonWriter.cpp"
"${CMAKE_CURRENT_SOURCE_DIR}/tests/RenderCadenceCompositorRuntimeStateJsonTests.cpp"
)
target_include_directories(RenderCadenceCompositorRuntimeStateJsonTests PRIVATE
"${APP_DIR}"
"${APP_DIR}/runtime/support"
"${APP_DIR}/shader"
"${APP_DIR}/videoio"
"${APP_DIR}/videoio/decklink"
"${RENDER_CADENCE_APP_DIR}/app"
"${RENDER_CADENCE_APP_DIR}/control"
"${RENDER_CADENCE_APP_DIR}/json"
"${RENDER_CADENCE_APP_DIR}/logging"
"${RENDER_CADENCE_APP_DIR}/runtime"
"${RENDER_CADENCE_APP_DIR}/telemetry"
"${RENDER_CADENCE_APP_DIR}/video"
)
if(MSVC)
target_compile_options(RenderCadenceCompositorRuntimeStateJsonTests PRIVATE /W3)
endif()
add_test(NAME RenderCadenceCompositorRuntimeStateJsonTests COMMAND RenderCadenceCompositorRuntimeStateJsonTests)
add_executable(RenderCadenceCompositorHttpControlServerTests add_executable(RenderCadenceCompositorHttpControlServerTests
"${RENDER_CADENCE_APP_DIR}/control/HttpControlServer.cpp" "${RENDER_CADENCE_APP_DIR}/control/HttpControlServer.cpp"
"${RENDER_CADENCE_APP_DIR}/json/JsonWriter.cpp" "${RENDER_CADENCE_APP_DIR}/json/JsonWriter.cpp"

View File

@@ -44,6 +44,7 @@ Included now:
- app-owned submission of a completed shader artifact - app-owned submission of a completed shader artifact
- render-thread-only GL commit once the artifact is ready - render-thread-only GL commit once the artifact is ready
- manifest-driven stateless single-pass shader packages - manifest-driven stateless single-pass shader packages
- HTTP shader list populated from supported stateless single-pass shader packages
- default float, vec2, color, boolean, enum, and trigger parameters - default float, vec2, color, boolean, enum, and trigger parameters
- small JSON writer for future HTTP/WebSocket payloads - small JSON writer for future HTTP/WebSocket payloads
- JSON serialization for cadence telemetry snapshots - JSON serialization for cadence telemetry snapshots
@@ -145,7 +146,7 @@ The app starts a local HTTP control server on `http://127.0.0.1:8080` by default
Current endpoints: Current endpoints:
- `GET /` and UI asset paths: serve the bundled control UI from `ui/dist` - `GET /` and UI asset paths: serve the bundled control UI from `ui/dist`
- `GET /api/state`: returns an OpenAPI-shaped state scaffold with cadence telemetry under `performance.cadence` - `GET /api/state`: returns OpenAPI-shaped display data with cadence telemetry, supported shaders, output status, and a read-only current runtime layer
- `GET /docs/openapi.yaml` and `GET /openapi.yaml`: serves the OpenAPI document - `GET /docs/openapi.yaml` and `GET /openapi.yaml`: serves the OpenAPI document
- `GET /docs`: serves Swagger UI - `GET /docs`: serves Swagger UI
- OpenAPI POST routes are present but return `{ "ok": false, "error": "Endpoint is not implemented in RenderCadenceCompositor yet." }` - OpenAPI POST routes are present but return `{ "ok": false, "error": "Endpoint is not implemented in RenderCadenceCompositor yet." }`
@@ -205,6 +206,10 @@ Current runtime shader support is deliberately limited to stateless single-pass
- manifest defaults are used for parameters - manifest defaults are used for parameters
- `gVideoInput` and `gLayerInput` are bound to a small fallback source texture until DeckLink input is added - `gVideoInput` and `gLayerInput` are bound to a small fallback source texture until DeckLink input is added
The `/api/state` shader list uses the same support rules as runtime shader compilation and reports only packages this app can run today. Unsupported manifest feature sets such as multipass, temporal, feedback, texture-backed, font-backed, or text-parameter shaders are hidden from the control UI for now.
The current runtime shader is also exposed as a read-only display layer with manifest parameter defaults. POST control endpoints still intentionally return "not implemented" responses until the control/state ownership model is ported.
Successful handoff signs: Successful handoff signs:
- telemetry shows `shaderCommitted=1` - telemetry shows `shaderCommitted=1`

View File

@@ -4,6 +4,7 @@
#include "AppConfigProvider.h" #include "AppConfigProvider.h"
#include "../logging/Logger.h" #include "../logging/Logger.h"
#include "../runtime/RuntimeShaderBridge.h" #include "../runtime/RuntimeShaderBridge.h"
#include "../runtime/SupportedShaderCatalog.h"
#include "../control/RuntimeStateJson.h" #include "../control/RuntimeStateJson.h"
#include "../telemetry/TelemetryHealthMonitor.h" #include "../telemetry/TelemetryHealthMonitor.h"
#include "../video/DeckLinkOutput.h" #include "../video/DeckLinkOutput.h"
@@ -11,6 +12,7 @@
#include <chrono> #include <chrono>
#include <filesystem> #include <filesystem>
#include <mutex>
#include <string> #include <string>
#include <thread> #include <thread>
#include <type_traits> #include <type_traits>
@@ -68,6 +70,8 @@ public:
bool Start(std::string& error) bool Start(std::string& error)
{ {
LoadSupportedShaderCatalog();
Log("app", "Starting render thread."); Log("app", "Starting render thread.");
if (!detail::StartRenderThread(mRenderThread, error, 0)) if (!detail::StartRenderThread(mRenderThread, error, 0))
{ {
@@ -187,12 +191,17 @@ private:
std::string BuildStateJson() std::string BuildStateJson()
{ {
CadenceTelemetrySnapshot telemetry = mHttpTelemetry.Sample(mFrameExchange, mOutput, mOutputThread, mRenderThread); CadenceTelemetrySnapshot telemetry = mHttpTelemetry.Sample(mFrameExchange, mOutput, mOutputThread, mRenderThread);
RuntimeDisplayState runtimeState = CopyRuntimeDisplayState(telemetry);
return RuntimeStateToJson(RuntimeStateJsonInput{ return RuntimeStateToJson(RuntimeStateJsonInput{
mConfig, mConfig,
telemetry, telemetry,
mHttpServer.Port(), mHttpServer.Port(),
mVideoOutputEnabled, mVideoOutputEnabled,
mVideoOutputStatus mVideoOutputStatus,
mShaderCatalog.Shaders(),
runtimeState.compileSucceeded,
runtimeState.compileMessage,
runtimeState.activeShaderPackage
}); });
} }
@@ -217,21 +226,69 @@ private:
} }
Log("runtime-shader", "Starting background Slang build for shader '" + mConfig.runtimeShaderId + "'."); Log("runtime-shader", "Starting background Slang build for shader '" + mConfig.runtimeShaderId + "'.");
SetRuntimeDisplayState(true, "Runtime Slang build started for shader '" + mConfig.runtimeShaderId + "'.", mConfig.runtimeShaderId);
mShaderBridge.Start( mShaderBridge.Start(
mConfig.runtimeShaderId, mConfig.runtimeShaderId,
[this](const RuntimeShaderArtifact& artifact) { [this](const RuntimeShaderArtifact& artifact) {
SetRuntimeDisplayState(true, artifact.message.empty() ? "Runtime shader artifact is ready." : artifact.message, artifact.shaderId);
mRenderThread.SubmitRuntimeShaderArtifact(artifact); mRenderThread.SubmitRuntimeShaderArtifact(artifact);
}, },
[](const std::string& message) { [this](const std::string& message) {
SetRuntimeDisplayState(false, message);
LogError("runtime-shader", "Runtime Slang build failed: " + message); LogError("runtime-shader", "Runtime Slang build failed: " + message);
}); });
} }
void LoadSupportedShaderCatalog()
{
const std::filesystem::path shaderRoot = FindRepoPath(mConfig.shaderLibrary);
std::string error;
if (!mShaderCatalog.Load(shaderRoot, static_cast<unsigned>(mConfig.maxTemporalHistoryFrames), error))
{
LogWarning("runtime-shader", "Supported shader catalog is empty: " + error);
return;
}
Log("runtime-shader", "Supported shader catalog loaded with " + std::to_string(mShaderCatalog.Shaders().size()) + " shader(s).");
}
void StopRuntimeShaderBuild() void StopRuntimeShaderBuild()
{ {
mShaderBridge.Stop(); mShaderBridge.Stop();
} }
struct RuntimeDisplayState
{
bool compileSucceeded = true;
std::string compileMessage;
const ShaderPackage* activeShaderPackage = nullptr;
};
void SetRuntimeDisplayState(bool compileSucceeded, const std::string& compileMessage, const std::string& activeShaderId = std::string())
{
std::lock_guard<std::mutex> lock(mRuntimeDisplayMutex);
mRuntimeCompileSucceeded = compileSucceeded;
mRuntimeCompileMessage = compileMessage;
if (!activeShaderId.empty())
mActiveShaderId = activeShaderId;
}
RuntimeDisplayState CopyRuntimeDisplayState(const CadenceTelemetrySnapshot& telemetry) const
{
std::lock_guard<std::mutex> lock(mRuntimeDisplayMutex);
RuntimeDisplayState state;
state.compileSucceeded = mRuntimeCompileSucceeded && telemetry.shaderBuildFailures == 0;
state.compileMessage = mRuntimeCompileMessage;
if (telemetry.shaderBuildFailures > 0)
state.compileMessage = "Runtime shader GL commit failed; see logs for details.";
if (state.compileMessage.empty())
state.compileMessage = mConfig.runtimeShaderId.empty()
? "Runtime shader build disabled."
: "Runtime shader build has not completed yet.";
state.activeShaderPackage = mShaderCatalog.FindPackage(mActiveShaderId);
return state;
}
RenderThread& mRenderThread; RenderThread& mRenderThread;
SystemFrameExchange& mFrameExchange; SystemFrameExchange& mFrameExchange;
AppConfig mConfig; AppConfig mConfig;
@@ -241,6 +298,11 @@ private:
CadenceTelemetry mHttpTelemetry; CadenceTelemetry mHttpTelemetry;
HttpControlServer mHttpServer; HttpControlServer mHttpServer;
RuntimeShaderBridge mShaderBridge; RuntimeShaderBridge mShaderBridge;
SupportedShaderCatalog mShaderCatalog;
mutable std::mutex mRuntimeDisplayMutex;
bool mRuntimeCompileSucceeded = true;
std::string mRuntimeCompileMessage;
std::string mActiveShaderId;
bool mStarted = false; bool mStarted = false;
bool mVideoOutputEnabled = false; bool mVideoOutputEnabled = false;
std::string mVideoOutputStatus = "DeckLink output not started."; std::string mVideoOutputStatus = "DeckLink output not started.";

View File

@@ -3,10 +3,12 @@
#include "../app/AppConfig.h" #include "../app/AppConfig.h"
#include "../app/AppConfigProvider.h" #include "../app/AppConfigProvider.h"
#include "../json/JsonWriter.h" #include "../json/JsonWriter.h"
#include "../runtime/SupportedShaderCatalog.h"
#include "../telemetry/CadenceTelemetryJson.h" #include "../telemetry/CadenceTelemetryJson.h"
#include <cstdint> #include <cstdint>
#include <string> #include <string>
#include <vector>
namespace RenderCadenceCompositor namespace RenderCadenceCompositor
{ {
@@ -17,6 +19,10 @@ struct RuntimeStateJsonInput
unsigned short serverPort = 0; unsigned short serverPort = 0;
bool videoOutputEnabled = false; bool videoOutputEnabled = false;
std::string videoOutputStatus; std::string videoOutputStatus;
const std::vector<SupportedShaderSummary>& shaders;
bool runtimeCompileSucceeded = true;
std::string runtimeCompileMessage;
const ShaderPackage* activeShaderPackage = nullptr;
}; };
inline void WriteVideoIoStatusJson(JsonWriter& writer, const RuntimeStateJsonInput& input) inline void WriteVideoIoStatusJson(JsonWriter& writer, const RuntimeStateJsonInput& input)
@@ -33,6 +39,153 @@ inline void WriteVideoIoStatusJson(JsonWriter& writer, const RuntimeStateJsonInp
writer.EndObject(); writer.EndObject();
} }
inline void OutputDimensions(const RuntimeStateJsonInput& input, unsigned& width, unsigned& height)
{
VideoFormatDimensions(input.config.outputVideoFormat, width, height);
}
inline const char* ShaderParameterTypeName(ShaderParameterType type)
{
switch (type)
{
case ShaderParameterType::Float: return "float";
case ShaderParameterType::Vec2: return "vec2";
case ShaderParameterType::Color: return "color";
case ShaderParameterType::Boolean: return "bool";
case ShaderParameterType::Enum: return "enum";
case ShaderParameterType::Text: return "text";
case ShaderParameterType::Trigger: return "trigger";
}
return "unknown";
}
inline void WriteNumberArray(JsonWriter& writer, const std::vector<double>& values)
{
writer.BeginArray();
for (double value : values)
writer.Double(value);
writer.EndArray();
}
inline void WriteDefaultParameterValue(JsonWriter& writer, const ShaderParameterDefinition& parameter)
{
switch (parameter.type)
{
case ShaderParameterType::Boolean:
writer.Bool(parameter.defaultBoolean);
return;
case ShaderParameterType::Enum:
writer.String(parameter.defaultEnumValue);
return;
case ShaderParameterType::Text:
writer.String(parameter.defaultTextValue);
return;
case ShaderParameterType::Trigger:
writer.Double(0.0);
return;
case ShaderParameterType::Float:
writer.Double(parameter.defaultNumbers.empty() ? 0.0 : parameter.defaultNumbers.front());
return;
case ShaderParameterType::Vec2:
case ShaderParameterType::Color:
WriteNumberArray(writer, parameter.defaultNumbers);
return;
}
writer.Null();
}
inline void WriteTemporalJson(JsonWriter& writer, const TemporalSettings& temporal)
{
writer.BeginObject();
writer.KeyBool("enabled", temporal.enabled);
writer.KeyString("historySource", "none");
writer.KeyUInt("requestedHistoryLength", temporal.requestedHistoryLength);
writer.KeyUInt("effectiveHistoryLength", temporal.effectiveHistoryLength);
writer.EndObject();
}
inline void WriteFeedbackJson(JsonWriter& writer, const FeedbackSettings& feedback)
{
writer.BeginObject();
writer.KeyBool("enabled", feedback.enabled);
writer.KeyString("writePass", feedback.writePassId);
writer.EndObject();
}
inline void WriteParameterDefinitionJson(JsonWriter& writer, const ShaderParameterDefinition& parameter)
{
writer.BeginObject();
writer.KeyString("id", parameter.id);
writer.KeyString("label", parameter.label.empty() ? parameter.id : parameter.label);
writer.KeyString("description", parameter.description);
writer.KeyString("type", ShaderParameterTypeName(parameter.type));
writer.Key("defaultValue");
WriteDefaultParameterValue(writer, parameter);
writer.Key("value");
WriteDefaultParameterValue(writer, parameter);
if (!parameter.minNumbers.empty())
{
writer.Key("min");
WriteNumberArray(writer, parameter.minNumbers);
}
if (!parameter.maxNumbers.empty())
{
writer.Key("max");
WriteNumberArray(writer, parameter.maxNumbers);
}
if (!parameter.stepNumbers.empty())
{
writer.Key("step");
WriteNumberArray(writer, parameter.stepNumbers);
}
if (parameter.type == ShaderParameterType::Enum)
{
writer.Key("options");
writer.BeginArray();
for (const ShaderParameterOption& option : parameter.enumOptions)
{
writer.BeginObject();
writer.KeyString("value", option.value);
writer.KeyString("label", option.label.empty() ? option.value : option.label);
writer.EndObject();
}
writer.EndArray();
}
if (parameter.type == ShaderParameterType::Text)
{
writer.KeyUInt("maxLength", parameter.maxLength);
if (!parameter.fontId.empty())
writer.KeyString("font", parameter.fontId);
}
writer.EndObject();
}
inline void WriteLayersJson(JsonWriter& writer, const RuntimeStateJsonInput& input)
{
writer.BeginArray();
if (input.activeShaderPackage)
{
const ShaderPackage& shaderPackage = *input.activeShaderPackage;
writer.BeginObject();
writer.KeyString("id", "runtime-layer-1");
writer.KeyString("shaderId", shaderPackage.id);
writer.KeyString("shaderName", shaderPackage.displayName.empty() ? shaderPackage.id : shaderPackage.displayName);
writer.KeyBool("bypass", false);
writer.Key("temporal");
WriteTemporalJson(writer, shaderPackage.temporal);
writer.Key("feedback");
WriteFeedbackJson(writer, shaderPackage.feedback);
writer.Key("parameters");
writer.BeginArray();
for (const ShaderParameterDefinition& parameter : shaderPackage.parameters)
WriteParameterDefinitionJson(writer, parameter);
writer.EndArray();
writer.EndObject();
}
writer.EndArray();
}
inline std::string RuntimeStateToJson(const RuntimeStateJsonInput& input) inline std::string RuntimeStateToJson(const RuntimeStateJsonInput& input)
{ {
JsonWriter writer; JsonWriter writer;
@@ -56,17 +209,20 @@ inline std::string RuntimeStateToJson(const RuntimeStateJsonInput& input)
writer.Key("runtime"); writer.Key("runtime");
writer.BeginObject(); writer.BeginObject();
writer.KeyUInt("layerCount", 0); writer.KeyUInt("layerCount", input.activeShaderPackage ? 1 : 0);
writer.KeyBool("compileSucceeded", true); writer.KeyBool("compileSucceeded", input.runtimeCompileSucceeded);
writer.KeyString("compileMessage", "Runtime state is not ported into RenderCadenceCompositor yet."); writer.KeyString("compileMessage", input.runtimeCompileMessage);
writer.EndObject(); writer.EndObject();
writer.Key("video"); writer.Key("video");
writer.BeginObject(); writer.BeginObject();
writer.KeyBool("hasSignal", false); unsigned outputWidth = 0;
writer.KeyNull("width"); unsigned outputHeight = 0;
writer.KeyNull("height"); OutputDimensions(input, outputWidth, outputHeight);
writer.KeyString("modeName", "output-only"); writer.KeyBool("hasSignal", input.videoOutputEnabled);
writer.KeyUInt("width", outputWidth);
writer.KeyUInt("height", outputHeight);
writer.KeyString("modeName", input.config.outputVideoFormat + " output-only");
writer.EndObject(); writer.EndObject();
writer.Key("decklink"); writer.Key("decklink");
@@ -94,13 +250,23 @@ inline std::string RuntimeStateToJson(const RuntimeStateJsonInput& input)
writer.KeyNull("runtimeEvents"); writer.KeyNull("runtimeEvents");
writer.Key("shaders"); writer.Key("shaders");
writer.BeginArray(); writer.BeginArray();
for (const SupportedShaderSummary& shader : input.shaders)
{
writer.BeginObject();
writer.KeyString("id", shader.id);
writer.KeyString("name", shader.name);
writer.KeyString("description", shader.description);
writer.KeyString("category", shader.category);
writer.KeyBool("available", true);
writer.KeyNull("error");
writer.EndObject();
}
writer.EndArray(); writer.EndArray();
writer.Key("stackPresets"); writer.Key("stackPresets");
writer.BeginArray(); writer.BeginArray();
writer.EndArray(); writer.EndArray();
writer.Key("layers"); writer.Key("layers");
writer.BeginArray(); WriteLayersJson(writer, input);
writer.EndArray();
writer.EndObject(); writer.EndObject();
return writer.StringValue(); return writer.StringValue();

View File

@@ -3,10 +3,10 @@
#include "ShaderCompiler.h" #include "ShaderCompiler.h"
#include "ShaderPackageRegistry.h" #include "ShaderPackageRegistry.h"
#include "ShaderTypes.h" #include "ShaderTypes.h"
#include "SupportedShaderCatalog.h"
#include <chrono> #include <chrono>
#include <filesystem> #include <filesystem>
#include <iostream>
namespace namespace
{ {
@@ -28,43 +28,6 @@ std::filesystem::path FindRepoRoot()
} }
} }
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() RuntimeSlangShaderCompiler::~RuntimeSlangShaderCompiler()
@@ -140,10 +103,12 @@ RuntimeSlangShaderBuild RuntimeSlangShaderCompiler::BuildShader(const std::strin
build.message = error.empty() ? "Shader manifest parse failed." : error; build.message = error.empty() ? "Shader manifest parse failed." : error;
return build; return build;
} }
if (!IsStatelessSinglePassPackage(shaderPackage, error)) const RenderCadenceCompositor::ShaderSupportResult support =
RenderCadenceCompositor::CheckStatelessSinglePassShaderSupport(shaderPackage);
if (!support.supported)
{ {
build.succeeded = false; build.succeeded = false;
build.message = error; build.message = support.reason;
return build; return build;
} }

View File

@@ -0,0 +1,83 @@
#include "SupportedShaderCatalog.h"
#include "ShaderPackageRegistry.h"
#include <map>
#include <utility>
namespace RenderCadenceCompositor
{
ShaderSupportResult CheckStatelessSinglePassShaderSupport(const ShaderPackage& shaderPackage)
{
if (shaderPackage.passes.size() != 1)
return { false, "RenderCadenceCompositor currently supports only single-pass runtime shaders." };
if (shaderPackage.temporal.enabled)
return { false, "RenderCadenceCompositor currently supports only stateless shaders; temporal history is not enabled in this app." };
if (shaderPackage.feedback.enabled)
return { false, "RenderCadenceCompositor currently supports only stateless shaders; feedback storage is not enabled in this app." };
if (!shaderPackage.textureAssets.empty())
return { false, "RenderCadenceCompositor does not load shader texture assets yet; texture-backed shaders need a CPU-prepared asset handoff first." };
if (!shaderPackage.fontAssets.empty())
return { false, "RenderCadenceCompositor does not load shader font assets yet; text shaders need a CPU-prepared asset handoff first." };
for (const ShaderParameterDefinition& parameter : shaderPackage.parameters)
{
if (parameter.type == ShaderParameterType::Text)
return { false, "RenderCadenceCompositor currently skips text parameters because they require per-shader text texture storage." };
}
return { true, std::string() };
}
bool SupportedShaderCatalog::Load(const std::filesystem::path& shaderRoot, unsigned maxTemporalHistoryFrames, std::string& error)
{
mShaders.clear();
mPackagesById.clear();
if (shaderRoot.empty())
{
error = "Shader library path is empty.";
return false;
}
ShaderPackageRegistry registry(maxTemporalHistoryFrames);
std::map<std::string, ShaderPackage> packagesById;
std::vector<std::string> packageOrder;
std::vector<ShaderPackageStatus> packageStatuses;
if (!registry.Scan(shaderRoot, packagesById, packageOrder, packageStatuses, error))
return false;
for (const std::string& packageId : packageOrder)
{
const auto packageIt = packagesById.find(packageId);
if (packageIt == packagesById.end())
continue;
const ShaderPackage& shaderPackage = packageIt->second;
const ShaderSupportResult support = CheckStatelessSinglePassShaderSupport(shaderPackage);
if (!support.supported)
continue;
SupportedShaderSummary summary;
summary.id = shaderPackage.id;
summary.name = shaderPackage.displayName.empty() ? shaderPackage.id : shaderPackage.displayName;
summary.description = shaderPackage.description;
summary.category = shaderPackage.category;
mShaders.push_back(std::move(summary));
mPackagesById[shaderPackage.id] = shaderPackage;
}
error.clear();
return true;
}
const ShaderPackage* SupportedShaderCatalog::FindPackage(const std::string& shaderId) const
{
const auto packageIt = mPackagesById.find(shaderId);
return packageIt == mPackagesById.end() ? nullptr : &packageIt->second;
}
}

View File

@@ -0,0 +1,39 @@
#pragma once
#include "ShaderTypes.h"
#include <filesystem>
#include <map>
#include <string>
#include <vector>
namespace RenderCadenceCompositor
{
struct SupportedShaderSummary
{
std::string id;
std::string name;
std::string description;
std::string category;
};
struct ShaderSupportResult
{
bool supported = false;
std::string reason;
};
ShaderSupportResult CheckStatelessSinglePassShaderSupport(const ShaderPackage& shaderPackage);
class SupportedShaderCatalog
{
public:
bool Load(const std::filesystem::path& shaderRoot, unsigned maxTemporalHistoryFrames, std::string& error);
const std::vector<SupportedShaderSummary>& Shaders() const { return mShaders; }
const ShaderPackage* FindPackage(const std::string& shaderId) const;
private:
std::vector<SupportedShaderSummary> mShaders;
std::map<std::string, ShaderPackage> mPackagesById;
};
}

View File

@@ -6,17 +6,20 @@ info:
REST API exposed by the local Video Shader Toys control server. REST API exposed by the local Video Shader Toys control server.
The API is intended for local control tools and the bundled React UI. All mutating The API is intended for local control tools and the bundled React UI. All mutating
endpoints return a small action result object. Successful mutating requests also endpoints return a small action result object.
broadcast the latest runtime state over the `/ws` WebSocket.
WebSocket state streaming is not described by OpenAPI; connect to `ws://127.0.0.1:{port}/ws` WebSocket state streaming is planned for the control UI but is not currently served
to receive full runtime state JSON messages whenever state changes. by RenderCadenceCompositor. Clients should poll `/api/state` until `/ws` is implemented.
servers: servers:
- url: http://127.0.0.1:8080 - url: http://127.0.0.1:8080
description: Default local control server description: Default local control server
tags: tags:
- name: State - name: State
description: Runtime state and status. description: Runtime state and status.
- name: Static
description: Bundled control UI and static assets served by the local host.
- name: Docs
description: OpenAPI and Swagger UI documentation served by the local host.
- name: Layers - name: Layers
description: Layer stack control. description: Layer stack control.
- name: Stack Presets - name: Stack Presets
@@ -24,6 +27,146 @@ tags:
- name: Runtime - name: Runtime
description: Runtime actions. description: Runtime actions.
paths: paths:
/:
get:
tags: [Static]
summary: Serve the bundled control UI
description: Returns the built React control UI `index.html` from `ui/dist`.
operationId: getControlUiRoot
responses:
"200":
description: Control UI HTML.
content:
text/html:
schema:
type: string
"404":
description: UI bundle was not found.
content:
text/plain:
schema:
type: string
/index.html:
get:
tags: [Static]
summary: Serve the bundled control UI index file
description: Returns the built React control UI `index.html` from `ui/dist`.
operationId: getControlUiIndex
responses:
"200":
description: Control UI HTML.
content:
text/html:
schema:
type: string
"404":
description: UI bundle was not found.
content:
text/plain:
schema:
type: string
/assets/{assetPath}:
get:
tags: [Static]
summary: Serve a bundled control UI asset
description: Serves files from `ui/dist/assets`. The server rejects unsafe relative paths and guesses the content type from the file extension.
operationId: getControlUiAsset
parameters:
- name: assetPath
in: path
required: true
description: Relative asset path below `ui/dist/assets`.
schema:
type: string
responses:
"200":
description: Static asset.
content:
text/javascript:
schema:
type: string
text/css:
schema:
type: string
image/svg+xml:
schema:
type: string
image/png:
schema:
type: string
format: binary
text/plain:
schema:
type: string
"404":
description: Asset was not found or the path was unsafe.
content:
text/plain:
schema:
type: string
/docs:
get:
tags: [Docs]
summary: Serve Swagger UI
description: Returns a small Swagger UI page pointed at `/docs/openapi.yaml`.
operationId: getSwaggerUi
responses:
"200":
description: Swagger UI HTML.
content:
text/html:
schema:
type: string
/docs/:
get:
tags: [Docs]
summary: Serve Swagger UI
description: Alias for `/docs`.
operationId: getSwaggerUiWithTrailingSlash
responses:
"200":
description: Swagger UI HTML.
content:
text/html:
schema:
type: string
/docs/openapi.yaml:
get:
tags: [Docs]
summary: Serve the OpenAPI document
operationId: getOpenApiDocumentFromDocs
responses:
"200":
description: OpenAPI YAML document.
content:
application/yaml:
schema:
type: string
"404":
description: OpenAPI document was not found.
content:
text/plain:
schema:
type: string
/openapi.yaml:
get:
tags: [Docs]
summary: Serve the OpenAPI document
description: Alias for `/docs/openapi.yaml`.
operationId: getOpenApiDocument
responses:
"200":
description: OpenAPI YAML document.
content:
application/yaml:
schema:
type: string
"404":
description: OpenAPI document was not found.
content:
text/plain:
schema:
type: string
/api/state: /api/state:
get: get:
tags: [State] tags: [State]

View File

@@ -0,0 +1,89 @@
#include "RuntimeStateJson.h"
#include <iostream>
#include <string>
#include <vector>
namespace
{
int gFailures = 0;
void ExpectContains(const std::string& text, const std::string& fragment, const std::string& message)
{
if (text.find(fragment) != std::string::npos)
return;
++gFailures;
std::cerr << "FAIL: " << message << "\n";
}
ShaderPackage MakePackage()
{
ShaderPackage package;
package.id = "solid-color";
package.displayName = "Solid Color";
package.description = "A single color shader.";
package.category = "Generator";
ShaderPassDefinition pass;
pass.id = "main";
package.passes.push_back(pass);
ShaderParameterDefinition color;
color.id = "color";
color.label = "Color";
color.description = "Output color.";
color.type = ShaderParameterType::Color;
color.defaultNumbers = { 1.0, 0.25, 0.5, 1.0 };
color.minNumbers = { 0.0, 0.0, 0.0, 0.0 };
color.maxNumbers = { 1.0, 1.0, 1.0, 1.0 };
color.stepNumbers = { 0.01, 0.01, 0.01, 0.01 };
package.parameters.push_back(color);
return package;
}
}
int main()
{
RenderCadenceCompositor::AppConfig config = RenderCadenceCompositor::DefaultAppConfig();
config.outputVideoFormat = "1080p";
config.outputFrameRate = "59.94";
RenderCadenceCompositor::CadenceTelemetrySnapshot telemetry;
telemetry.renderFps = 59.94;
telemetry.shaderBuildsCommitted = 1;
std::vector<RenderCadenceCompositor::SupportedShaderSummary> shaders = {
{ "solid-color", "Solid Color", "A single color shader.", "Generator" }
};
const ShaderPackage package = MakePackage();
const std::string json = RenderCadenceCompositor::RuntimeStateToJson(RenderCadenceCompositor::RuntimeStateJsonInput{
config,
telemetry,
8080,
true,
"DeckLink scheduled output running.",
shaders,
true,
"Runtime shader committed.",
&package
});
ExpectContains(json, "\"shaders\":[{\"id\":\"solid-color\"", "state JSON should include supported shaders");
ExpectContains(json, "\"layerCount\":1", "state JSON should expose the display layer count");
ExpectContains(json, "\"layers\":[{\"id\":\"runtime-layer-1\"", "state JSON should expose the active display layer");
ExpectContains(json, "\"parameters\":[{\"id\":\"color\"", "state JSON should expose active shader parameters");
ExpectContains(json, "\"type\":\"color\"", "state JSON should serialize parameter types for the UI");
ExpectContains(json, "\"width\":1920", "state JSON should expose output width");
ExpectContains(json, "\"height\":1080", "state JSON should expose output height");
if (gFailures != 0)
{
std::cerr << gFailures << " RenderCadenceCompositorRuntimeStateJson test failure(s).\n";
return 1;
}
std::cout << "RenderCadenceCompositorRuntimeStateJson tests passed.\n";
return 0;
}

View File

@@ -0,0 +1,113 @@
#include "SupportedShaderCatalog.h"
#include <iostream>
#include <string>
namespace
{
int gFailures = 0;
void Expect(bool condition, const std::string& message)
{
if (condition)
return;
++gFailures;
std::cerr << "FAIL: " << message << "\n";
}
ShaderPackage MakeSinglePassPackage()
{
ShaderPackage shaderPackage;
shaderPackage.id = "supported";
ShaderPassDefinition pass;
pass.id = "main";
pass.entryPoint = "mainImage";
shaderPackage.passes.push_back(pass);
return shaderPackage;
}
void SupportsSinglePassStatelessPackage()
{
const ShaderPackage shaderPackage = MakeSinglePassPackage();
const RenderCadenceCompositor::ShaderSupportResult result =
RenderCadenceCompositor::CheckStatelessSinglePassShaderSupport(shaderPackage);
Expect(result.supported, "single-pass stateless packages should be supported");
Expect(result.reason.empty(), "supported packages should not report a rejection reason");
}
void RejectsMultipassPackage()
{
ShaderPackage shaderPackage = MakeSinglePassPackage();
ShaderPassDefinition secondPass;
secondPass.id = "second";
secondPass.entryPoint = "mainImage";
shaderPackage.passes.push_back(secondPass);
const RenderCadenceCompositor::ShaderSupportResult result =
RenderCadenceCompositor::CheckStatelessSinglePassShaderSupport(shaderPackage);
Expect(!result.supported, "multipass packages should be rejected");
Expect(result.reason.find("single-pass") != std::string::npos, "multipass rejection should explain the single-pass limit");
}
void RejectsTemporalPackage()
{
ShaderPackage shaderPackage = MakeSinglePassPackage();
shaderPackage.temporal.enabled = true;
const RenderCadenceCompositor::ShaderSupportResult result =
RenderCadenceCompositor::CheckStatelessSinglePassShaderSupport(shaderPackage);
Expect(!result.supported, "temporal packages should be rejected");
Expect(result.reason.find("temporal") != std::string::npos, "temporal rejection should mention temporal storage");
}
void RejectsTextureAssets()
{
ShaderPackage shaderPackage = MakeSinglePassPackage();
ShaderTextureAsset asset;
asset.id = "lut";
shaderPackage.textureAssets.push_back(asset);
const RenderCadenceCompositor::ShaderSupportResult result =
RenderCadenceCompositor::CheckStatelessSinglePassShaderSupport(shaderPackage);
Expect(!result.supported, "texture-backed packages should be rejected for now");
Expect(result.reason.find("texture") != std::string::npos, "texture rejection should mention texture assets");
}
void RejectsTextParameters()
{
ShaderPackage shaderPackage = MakeSinglePassPackage();
ShaderParameterDefinition parameter;
parameter.id = "caption";
parameter.type = ShaderParameterType::Text;
shaderPackage.parameters.push_back(parameter);
const RenderCadenceCompositor::ShaderSupportResult result =
RenderCadenceCompositor::CheckStatelessSinglePassShaderSupport(shaderPackage);
Expect(!result.supported, "text-parameter packages should be rejected for now");
Expect(result.reason.find("text") != std::string::npos, "text rejection should mention text parameters");
}
}
int main()
{
SupportsSinglePassStatelessPackage();
RejectsMultipassPackage();
RejectsTemporalPackage();
RejectsTextureAssets();
RejectsTextParameters();
if (gFailures != 0)
{
std::cerr << gFailures << " RenderCadenceCompositorSupportedShaderCatalog test failure(s).\n";
return 1;
}
std::cout << "RenderCadenceCompositorSupportedShaderCatalog tests passed.\n";
return 0;
}

View File

@@ -30,7 +30,7 @@ function App() {
const [dropTargetLayerId, setDropTargetLayerId] = useState(null); const [dropTargetLayerId, setDropTargetLayerId] = useState(null);
const layers = appState?.layers ?? []; const layers = appState?.layers ?? [];
const shaders = appState?.shaders ?? []; const shaders = (appState?.shaders ?? []).filter((shader) => shader.available !== false);
const performance = appState?.performance ?? {}; const performance = appState?.performance ?? {};
const runtime = appState?.runtime ?? {}; const runtime = appState?.runtime ?? {};
const video = appState?.video ?? {}; const video = appState?.video ?? {};