diff --git a/CMakeLists.txt b/CMakeLists.txt index dc6410f..e55c2ee 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -337,6 +337,8 @@ set(RENDER_CADENCE_APP_SOURCES "${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}/runtime/SupportedShaderCatalog.cpp" + "${RENDER_CADENCE_APP_DIR}/runtime/SupportedShaderCatalog.h" "${RENDER_CADENCE_APP_DIR}/telemetry/CadenceTelemetryJson.h" "${RENDER_CADENCE_APP_DIR}/telemetry/CadenceTelemetry.h" "${RENDER_CADENCE_APP_DIR}/telemetry/TelemetryHealthMonitor.h" @@ -819,6 +821,26 @@ endif() 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 "${RENDER_CADENCE_APP_DIR}/logging/Logger.cpp" "${CMAKE_CURRENT_SOURCE_DIR}/tests/RenderCadenceCompositorLoggerTests.cpp" @@ -849,6 +871,35 @@ endif() 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 "${RENDER_CADENCE_APP_DIR}/control/HttpControlServer.cpp" "${RENDER_CADENCE_APP_DIR}/json/JsonWriter.cpp" diff --git a/apps/RenderCadenceCompositor/README.md b/apps/RenderCadenceCompositor/README.md index 146ef67..6aa460b 100644 --- a/apps/RenderCadenceCompositor/README.md +++ b/apps/RenderCadenceCompositor/README.md @@ -44,6 +44,7 @@ Included now: - 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 +- HTTP shader list populated from supported stateless single-pass shader packages - default float, vec2, color, boolean, enum, and trigger parameters - small JSON writer for future HTTP/WebSocket payloads - 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: - `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`: serves Swagger UI - 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 - `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: - telemetry shows `shaderCommitted=1` diff --git a/apps/RenderCadenceCompositor/app/RenderCadenceApp.h b/apps/RenderCadenceCompositor/app/RenderCadenceApp.h index 6b7a159..ebaf9ef 100644 --- a/apps/RenderCadenceCompositor/app/RenderCadenceApp.h +++ b/apps/RenderCadenceCompositor/app/RenderCadenceApp.h @@ -4,6 +4,7 @@ #include "AppConfigProvider.h" #include "../logging/Logger.h" #include "../runtime/RuntimeShaderBridge.h" +#include "../runtime/SupportedShaderCatalog.h" #include "../control/RuntimeStateJson.h" #include "../telemetry/TelemetryHealthMonitor.h" #include "../video/DeckLinkOutput.h" @@ -11,6 +12,7 @@ #include #include +#include #include #include #include @@ -68,6 +70,8 @@ public: bool Start(std::string& error) { + LoadSupportedShaderCatalog(); + Log("app", "Starting render thread."); if (!detail::StartRenderThread(mRenderThread, error, 0)) { @@ -187,12 +191,17 @@ private: std::string BuildStateJson() { CadenceTelemetrySnapshot telemetry = mHttpTelemetry.Sample(mFrameExchange, mOutput, mOutputThread, mRenderThread); + RuntimeDisplayState runtimeState = CopyRuntimeDisplayState(telemetry); return RuntimeStateToJson(RuntimeStateJsonInput{ mConfig, telemetry, mHttpServer.Port(), 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 + "'."); + SetRuntimeDisplayState(true, "Runtime Slang build started for shader '" + mConfig.runtimeShaderId + "'.", mConfig.runtimeShaderId); mShaderBridge.Start( mConfig.runtimeShaderId, [this](const RuntimeShaderArtifact& artifact) { + SetRuntimeDisplayState(true, artifact.message.empty() ? "Runtime shader artifact is ready." : artifact.message, artifact.shaderId); mRenderThread.SubmitRuntimeShaderArtifact(artifact); }, - [](const std::string& message) { + [this](const std::string& message) { + SetRuntimeDisplayState(false, 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(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() { 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 lock(mRuntimeDisplayMutex); + mRuntimeCompileSucceeded = compileSucceeded; + mRuntimeCompileMessage = compileMessage; + if (!activeShaderId.empty()) + mActiveShaderId = activeShaderId; + } + + RuntimeDisplayState CopyRuntimeDisplayState(const CadenceTelemetrySnapshot& telemetry) const + { + std::lock_guard 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; SystemFrameExchange& mFrameExchange; AppConfig mConfig; @@ -241,6 +298,11 @@ private: CadenceTelemetry mHttpTelemetry; HttpControlServer mHttpServer; RuntimeShaderBridge mShaderBridge; + SupportedShaderCatalog mShaderCatalog; + mutable std::mutex mRuntimeDisplayMutex; + bool mRuntimeCompileSucceeded = true; + std::string mRuntimeCompileMessage; + std::string mActiveShaderId; bool mStarted = false; bool mVideoOutputEnabled = false; std::string mVideoOutputStatus = "DeckLink output not started."; diff --git a/apps/RenderCadenceCompositor/control/RuntimeStateJson.h b/apps/RenderCadenceCompositor/control/RuntimeStateJson.h index 1690517..7b7da23 100644 --- a/apps/RenderCadenceCompositor/control/RuntimeStateJson.h +++ b/apps/RenderCadenceCompositor/control/RuntimeStateJson.h @@ -3,10 +3,12 @@ #include "../app/AppConfig.h" #include "../app/AppConfigProvider.h" #include "../json/JsonWriter.h" +#include "../runtime/SupportedShaderCatalog.h" #include "../telemetry/CadenceTelemetryJson.h" #include #include +#include namespace RenderCadenceCompositor { @@ -17,6 +19,10 @@ struct RuntimeStateJsonInput unsigned short serverPort = 0; bool videoOutputEnabled = false; std::string videoOutputStatus; + const std::vector& shaders; + bool runtimeCompileSucceeded = true; + std::string runtimeCompileMessage; + const ShaderPackage* activeShaderPackage = nullptr; }; inline void WriteVideoIoStatusJson(JsonWriter& writer, const RuntimeStateJsonInput& input) @@ -33,6 +39,153 @@ inline void WriteVideoIoStatusJson(JsonWriter& writer, const RuntimeStateJsonInp 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& 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) { JsonWriter writer; @@ -56,17 +209,20 @@ inline std::string RuntimeStateToJson(const RuntimeStateJsonInput& input) writer.Key("runtime"); writer.BeginObject(); - writer.KeyUInt("layerCount", 0); - writer.KeyBool("compileSucceeded", true); - writer.KeyString("compileMessage", "Runtime state is not ported into RenderCadenceCompositor yet."); + writer.KeyUInt("layerCount", input.activeShaderPackage ? 1 : 0); + writer.KeyBool("compileSucceeded", input.runtimeCompileSucceeded); + writer.KeyString("compileMessage", input.runtimeCompileMessage); writer.EndObject(); writer.Key("video"); writer.BeginObject(); - writer.KeyBool("hasSignal", false); - writer.KeyNull("width"); - writer.KeyNull("height"); - writer.KeyString("modeName", "output-only"); + unsigned outputWidth = 0; + unsigned outputHeight = 0; + OutputDimensions(input, outputWidth, outputHeight); + writer.KeyBool("hasSignal", input.videoOutputEnabled); + writer.KeyUInt("width", outputWidth); + writer.KeyUInt("height", outputHeight); + writer.KeyString("modeName", input.config.outputVideoFormat + " output-only"); writer.EndObject(); writer.Key("decklink"); @@ -94,13 +250,23 @@ inline std::string RuntimeStateToJson(const RuntimeStateJsonInput& input) writer.KeyNull("runtimeEvents"); writer.Key("shaders"); 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.Key("stackPresets"); writer.BeginArray(); writer.EndArray(); writer.Key("layers"); - writer.BeginArray(); - writer.EndArray(); + WriteLayersJson(writer, input); writer.EndObject(); return writer.StringValue(); diff --git a/apps/RenderCadenceCompositor/runtime/RuntimeSlangShaderCompiler.cpp b/apps/RenderCadenceCompositor/runtime/RuntimeSlangShaderCompiler.cpp index 809c86a..f05beb2 100644 --- a/apps/RenderCadenceCompositor/runtime/RuntimeSlangShaderCompiler.cpp +++ b/apps/RenderCadenceCompositor/runtime/RuntimeSlangShaderCompiler.cpp @@ -3,10 +3,10 @@ #include "ShaderCompiler.h" #include "ShaderPackageRegistry.h" #include "ShaderTypes.h" +#include "SupportedShaderCatalog.h" #include #include -#include 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() @@ -140,10 +103,12 @@ RuntimeSlangShaderBuild RuntimeSlangShaderCompiler::BuildShader(const std::strin build.message = error.empty() ? "Shader manifest parse failed." : error; return build; } - if (!IsStatelessSinglePassPackage(shaderPackage, error)) + const RenderCadenceCompositor::ShaderSupportResult support = + RenderCadenceCompositor::CheckStatelessSinglePassShaderSupport(shaderPackage); + if (!support.supported) { build.succeeded = false; - build.message = error; + build.message = support.reason; return build; } diff --git a/apps/RenderCadenceCompositor/runtime/SupportedShaderCatalog.cpp b/apps/RenderCadenceCompositor/runtime/SupportedShaderCatalog.cpp new file mode 100644 index 0000000..634801c --- /dev/null +++ b/apps/RenderCadenceCompositor/runtime/SupportedShaderCatalog.cpp @@ -0,0 +1,83 @@ +#include "SupportedShaderCatalog.h" + +#include "ShaderPackageRegistry.h" + +#include +#include + +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 packagesById; + std::vector packageOrder; + std::vector 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; +} +} diff --git a/apps/RenderCadenceCompositor/runtime/SupportedShaderCatalog.h b/apps/RenderCadenceCompositor/runtime/SupportedShaderCatalog.h new file mode 100644 index 0000000..92eb94d --- /dev/null +++ b/apps/RenderCadenceCompositor/runtime/SupportedShaderCatalog.h @@ -0,0 +1,39 @@ +#pragma once + +#include "ShaderTypes.h" + +#include +#include +#include +#include + +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& Shaders() const { return mShaders; } + const ShaderPackage* FindPackage(const std::string& shaderId) const; + +private: + std::vector mShaders; + std::map mPackagesById; +}; +} diff --git a/docs/openapi.yaml b/docs/openapi.yaml index 7e7858a..7045158 100644 --- a/docs/openapi.yaml +++ b/docs/openapi.yaml @@ -6,17 +6,20 @@ info: 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 - endpoints return a small action result object. Successful mutating requests also - broadcast the latest runtime state over the `/ws` WebSocket. + endpoints return a small action result object. - WebSocket state streaming is not described by OpenAPI; connect to `ws://127.0.0.1:{port}/ws` - to receive full runtime state JSON messages whenever state changes. + WebSocket state streaming is planned for the control UI but is not currently served + by RenderCadenceCompositor. Clients should poll `/api/state` until `/ws` is implemented. servers: - url: http://127.0.0.1:8080 description: Default local control server tags: - name: State 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 description: Layer stack control. - name: Stack Presets @@ -24,6 +27,146 @@ tags: - name: Runtime description: Runtime actions. 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: get: tags: [State] diff --git a/tests/RenderCadenceCompositorRuntimeStateJsonTests.cpp b/tests/RenderCadenceCompositorRuntimeStateJsonTests.cpp new file mode 100644 index 0000000..327d771 --- /dev/null +++ b/tests/RenderCadenceCompositorRuntimeStateJsonTests.cpp @@ -0,0 +1,89 @@ +#include "RuntimeStateJson.h" + +#include +#include +#include + +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 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; +} diff --git a/tests/RenderCadenceCompositorSupportedShaderCatalogTests.cpp b/tests/RenderCadenceCompositorSupportedShaderCatalogTests.cpp new file mode 100644 index 0000000..dae0e4a --- /dev/null +++ b/tests/RenderCadenceCompositorSupportedShaderCatalogTests.cpp @@ -0,0 +1,113 @@ +#include "SupportedShaderCatalog.h" + +#include +#include + +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; +} diff --git a/ui/src/App.jsx b/ui/src/App.jsx index 0f42422..3a1741b 100644 --- a/ui/src/App.jsx +++ b/ui/src/App.jsx @@ -30,7 +30,7 @@ function App() { const [dropTargetLayerId, setDropTargetLayerId] = useState(null); const layers = appState?.layers ?? []; - const shaders = appState?.shaders ?? []; + const shaders = (appState?.shaders ?? []).filter((shader) => shader.available !== false); const performance = appState?.performance ?? {}; const runtime = appState?.runtime ?? {}; const video = appState?.video ?? {};