diff --git a/CMakeLists.txt b/CMakeLists.txt index e55c2ee..950cdc2 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -333,6 +333,8 @@ set(RENDER_CADENCE_APP_SOURCES "${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/RuntimeLayerModel.cpp" + "${RENDER_CADENCE_APP_DIR}/runtime/RuntimeLayerModel.h" "${RENDER_CADENCE_APP_DIR}/runtime/RuntimeShaderBridge.cpp" "${RENDER_CADENCE_APP_DIR}/runtime/RuntimeShaderBridge.h" "${RENDER_CADENCE_APP_DIR}/runtime/RuntimeSlangShaderCompiler.cpp" @@ -821,6 +823,27 @@ endif() add_test(NAME RenderCadenceCompositorRuntimeShaderParamsTests COMMAND RenderCadenceCompositorRuntimeShaderParamsTests) +add_executable(RenderCadenceCompositorRuntimeLayerModelTests + "${RENDER_CADENCE_APP_DIR}/runtime/RuntimeLayerModel.cpp" + "${RENDER_CADENCE_APP_DIR}/runtime/SupportedShaderCatalog.cpp" + "${APP_DIR}/shader/ShaderPackageRegistry.cpp" + "${APP_DIR}/runtime/support/RuntimeJson.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/tests/RenderCadenceCompositorRuntimeLayerModelTests.cpp" +) + +target_include_directories(RenderCadenceCompositorRuntimeLayerModelTests PRIVATE + "${APP_DIR}" + "${APP_DIR}/runtime/support" + "${APP_DIR}/shader" + "${RENDER_CADENCE_APP_DIR}/runtime" +) + +if(MSVC) + target_compile_options(RenderCadenceCompositorRuntimeLayerModelTests PRIVATE /W3) +endif() + +add_test(NAME RenderCadenceCompositorRuntimeLayerModelTests COMMAND RenderCadenceCompositorRuntimeLayerModelTests) + add_executable(RenderCadenceCompositorSupportedShaderCatalogTests "${APP_DIR}/shader/ShaderPackageRegistry.cpp" "${APP_DIR}/runtime/support/RuntimeJson.cpp" @@ -873,9 +896,12 @@ add_test(NAME RenderCadenceCompositorJsonWriterTests COMMAND RenderCadenceCompos add_executable(RenderCadenceCompositorRuntimeStateJsonTests "${APP_DIR}/runtime/support/RuntimeJson.cpp" + "${APP_DIR}/shader/ShaderPackageRegistry.cpp" "${RENDER_CADENCE_APP_DIR}/app/AppConfig.cpp" "${RENDER_CADENCE_APP_DIR}/app/AppConfigProvider.cpp" "${RENDER_CADENCE_APP_DIR}/json/JsonWriter.cpp" + "${RENDER_CADENCE_APP_DIR}/runtime/RuntimeLayerModel.cpp" + "${RENDER_CADENCE_APP_DIR}/runtime/SupportedShaderCatalog.cpp" "${CMAKE_CURRENT_SOURCE_DIR}/tests/RenderCadenceCompositorRuntimeStateJsonTests.cpp" ) diff --git a/apps/RenderCadenceCompositor/README.md b/apps/RenderCadenceCompositor/README.md index 6aa460b..5410c07 100644 --- a/apps/RenderCadenceCompositor/README.md +++ b/apps/RenderCadenceCompositor/README.md @@ -41,6 +41,7 @@ Included now: - latest-N system-memory frame exchange - rendered-frame warmup - background Slang compile of `shaders/happy-accident` +- app-owned display/render layer model for shader build readiness - 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 @@ -149,7 +150,8 @@ Current endpoints: - `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." }` +- `POST /api/layers/add` and `POST /api/layers/remove` mutate the app-owned display layer model only +- other OpenAPI POST routes are present but return `{ "ok": false, "error": "Endpoint is not implemented in RenderCadenceCompositor yet." }` The HTTP server runs on its own thread. It serves static UI/docs files, samples/copies telemetry through callbacks, and does not call render work or DeckLink scheduling. @@ -208,7 +210,7 @@ Current runtime shader support is deliberately limited to stateless single-pass 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. +Runtime shaders are exposed through `RuntimeLayerModel` as display layers with manifest parameter defaults. The model also records whether each layer has a render-ready artifact. Stage 1 add/remove POST controls mutate this app-owned model and may start background shader builds, but multi-layer render-scene handoff is not ported yet. Successful handoff signs: @@ -258,6 +260,7 @@ This app keeps the same core behavior but splits it into modules that can grow: - `frames/`: system-memory handoff - `platform/`: COM/Win32/hidden GL context support - `render/`: cadence, simple rendering, PBO readback +- `runtime/`: app-owned shader layer readiness model, runtime Slang build bridge, and completed artifact handoff - `control/`: local HTTP API edge and runtime-state JSON presentation - `json/`: compact JSON serialization helpers - `video/`: DeckLink output wrapper and scheduling thread diff --git a/apps/RenderCadenceCompositor/app/RenderCadenceApp.h b/apps/RenderCadenceCompositor/app/RenderCadenceApp.h index ebaf9ef..527f798 100644 --- a/apps/RenderCadenceCompositor/app/RenderCadenceApp.h +++ b/apps/RenderCadenceCompositor/app/RenderCadenceApp.h @@ -3,15 +3,19 @@ #include "AppConfig.h" #include "AppConfigProvider.h" #include "../logging/Logger.h" +#include "../runtime/RuntimeLayerModel.h" #include "../runtime/RuntimeShaderBridge.h" #include "../runtime/SupportedShaderCatalog.h" #include "../control/RuntimeStateJson.h" #include "../telemetry/TelemetryHealthMonitor.h" #include "../video/DeckLinkOutput.h" #include "../video/DeckLinkOutputThread.h" +#include "RuntimeJson.h" #include #include +#include +#include #include #include #include @@ -71,6 +75,7 @@ public: bool Start(std::string& error) { LoadSupportedShaderCatalog(); + InitializeRuntimeLayerModel(); Log("app", "Starting render thread."); if (!detail::StartRenderThread(mRenderThread, error, 0)) @@ -174,6 +179,12 @@ private: callbacks.getStateJson = [this]() { return BuildStateJson(); }; + callbacks.addLayer = [this](const std::string& body) { + return HandleAddLayer(body); + }; + callbacks.removeLayer = [this](const std::string& body) { + return HandleRemoveLayer(body); + }; std::string error; if (!mHttpServer.Start( @@ -191,17 +202,15 @@ private: std::string BuildStateJson() { CadenceTelemetrySnapshot telemetry = mHttpTelemetry.Sample(mFrameExchange, mOutput, mOutputThread, mRenderThread); - RuntimeDisplayState runtimeState = CopyRuntimeDisplayState(telemetry); + RuntimeLayerModelSnapshot layerSnapshot = CopyRuntimeLayerSnapshot(telemetry); return RuntimeStateToJson(RuntimeStateJsonInput{ mConfig, telemetry, mHttpServer.Port(), mVideoOutputEnabled, mVideoOutputStatus, - mShaderCatalog.Shaders(), - runtimeState.compileSucceeded, - runtimeState.compileMessage, - runtimeState.activeShaderPackage + mShaderCatalog, + layerSnapshot }); } @@ -226,17 +235,9 @@ 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); - }, - [this](const std::string& message) { - SetRuntimeDisplayState(false, message); - LogError("runtime-shader", "Runtime Slang build failed: " + message); - }); + const std::string layerId = FirstRuntimeLayerId(); + if (!layerId.empty()) + StartLayerShaderBuild(layerId, mConfig.runtimeShaderId, true); } void LoadSupportedShaderCatalog() @@ -252,41 +253,185 @@ private: Log("runtime-shader", "Supported shader catalog loaded with " + std::to_string(mShaderCatalog.Shaders().size()) + " shader(s)."); } + void InitializeRuntimeLayerModel() + { + std::lock_guard lock(mRuntimeLayerMutex); + std::string error; + if (!mRuntimeLayerModel.InitializeSingleLayer(mShaderCatalog, mConfig.runtimeShaderId, error)) + { + LogWarning("runtime-shader", error + " Runtime shader build disabled."); + mConfig.runtimeShaderId.clear(); + mRuntimeLayerModel.Clear(); + } + } + void StopRuntimeShaderBuild() { - mShaderBridge.Stop(); + StopAllRuntimeShaderBuilds(); } - struct RuntimeDisplayState + void MarkRuntimeBuildStarted(const std::string& message) { - 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; + std::lock_guard lock(mRuntimeLayerMutex); + std::string error; + const std::string layerId = mRuntimeLayerModel.FirstLayerId(); + if (!layerId.empty()) + mRuntimeLayerModel.MarkBuildStarted(layerId, message, error); } - RuntimeDisplayState CopyRuntimeDisplayState(const CadenceTelemetrySnapshot& telemetry) const + void MarkRuntimeBuildReady(const RuntimeShaderArtifact& artifact) { - std::lock_guard lock(mRuntimeDisplayMutex); - RuntimeDisplayState state; - state.compileSucceeded = mRuntimeCompileSucceeded && telemetry.shaderBuildFailures == 0; - state.compileMessage = mRuntimeCompileMessage; + std::lock_guard lock(mRuntimeLayerMutex); + std::string error; + if (!mRuntimeLayerModel.MarkBuildReady(artifact, error)) + LogWarning("runtime-shader", error); + } + + void MarkRuntimeBuildFailed(const std::string& message) + { + std::lock_guard lock(mRuntimeLayerMutex); + if (!mRuntimeLayerModel.MarkBuildFailedForShader(mConfig.runtimeShaderId, message)) + LogWarning("runtime-shader", "Runtime shader failed without a matching display layer: " + message); + } + + void MarkRuntimeBuildFailedForLayer(const std::string& layerId, const std::string& message) + { + std::lock_guard lock(mRuntimeLayerMutex); + std::string error; + if (!mRuntimeLayerModel.MarkBuildFailed(layerId, message, error)) + LogWarning("runtime-shader", error); + } + + std::string FirstRuntimeLayerId() const + { + std::lock_guard lock(mRuntimeLayerMutex); + return mRuntimeLayerModel.FirstLayerId(); + } + + void StartLayerShaderBuild(const std::string& layerId, const std::string& shaderId, bool submitToRender) + { + { + std::lock_guard lock(mRuntimeLayerMutex); + std::string error; + mRuntimeLayerModel.MarkBuildStarted(layerId, "Runtime Slang build started for shader '" + shaderId + "'.", error); + } + + auto bridge = std::make_unique(); + RuntimeShaderBridge* bridgePtr = bridge.get(); + { + std::lock_guard lock(mShaderBuildMutex); + auto existingIt = mShaderBuilds.find(layerId); + if (existingIt != mShaderBuilds.end()) + existingIt->second->Stop(); + mShaderBuilds[layerId] = std::move(bridge); + } + + bridgePtr->Start( + layerId, + shaderId, + [this, submitToRender](const RuntimeShaderArtifact& artifact) { + MarkRuntimeBuildReady(artifact); + if (submitToRender) + mRenderThread.SubmitRuntimeShaderArtifact(artifact); + }, + [this, layerId](const std::string& message) { + MarkRuntimeBuildFailedForLayer(layerId, message); + LogError("runtime-shader", "Runtime Slang build failed: " + message); + }); + } + + void StopLayerShaderBuild(const std::string& layerId) + { + std::unique_ptr bridge; + { + std::lock_guard lock(mShaderBuildMutex); + auto bridgeIt = mShaderBuilds.find(layerId); + if (bridgeIt == mShaderBuilds.end()) + return; + bridge = std::move(bridgeIt->second); + mShaderBuilds.erase(bridgeIt); + } + bridge->Stop(); + } + + void StopAllRuntimeShaderBuilds() + { + std::map> builds; + { + std::lock_guard lock(mShaderBuildMutex); + builds.swap(mShaderBuilds); + } + for (auto& entry : builds) + entry.second->Stop(); + } + + ControlActionResult HandleAddLayer(const std::string& body) + { + std::string shaderId; + std::string error; + if (!ExtractStringField(body, "shaderId", shaderId, error)) + return { false, error }; + + std::string layerId; + { + std::lock_guard lock(mRuntimeLayerMutex); + if (!mRuntimeLayerModel.AddLayer(mShaderCatalog, shaderId, layerId, error)) + return { false, error }; + } + + StartLayerShaderBuild(layerId, shaderId, false); + return { true, std::string() }; + } + + ControlActionResult HandleRemoveLayer(const std::string& body) + { + std::string layerId; + std::string error; + if (!ExtractStringField(body, "layerId", layerId, error)) + return { false, error }; + + { + std::lock_guard lock(mRuntimeLayerMutex); + if (!mRuntimeLayerModel.RemoveLayer(layerId, error)) + return { false, error }; + } + + StopLayerShaderBuild(layerId); + return { true, std::string() }; + } + + static bool ExtractStringField(const std::string& body, const char* fieldName, std::string& value, std::string& error) + { + JsonValue root; + std::string parseError; + if (!ParseJson(body.empty() ? "{}" : body, root, parseError) || !root.isObject()) + { + error = parseError.empty() ? "Request body must be a JSON object." : parseError; + return false; + } + + const JsonValue* field = root.find(fieldName); + if (!field || !field->isString() || field->asString().empty()) + { + error = std::string("Request field '") + fieldName + "' must be a non-empty string."; + return false; + } + + value = field->asString(); + error.clear(); + return true; + } + + RuntimeLayerModelSnapshot CopyRuntimeLayerSnapshot(const CadenceTelemetrySnapshot& telemetry) const + { + std::lock_guard lock(mRuntimeLayerMutex); + RuntimeLayerModelSnapshot snapshot = mRuntimeLayerModel.Snapshot(); 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; + { + snapshot.compileSucceeded = false; + snapshot.compileMessage = "Runtime shader GL commit failed; see logs for details."; + } + return snapshot; } RenderThread& mRenderThread; @@ -297,12 +442,11 @@ private: TelemetryHealthMonitor mTelemetryHealth; CadenceTelemetry mHttpTelemetry; HttpControlServer mHttpServer; - RuntimeShaderBridge mShaderBridge; SupportedShaderCatalog mShaderCatalog; - mutable std::mutex mRuntimeDisplayMutex; - bool mRuntimeCompileSucceeded = true; - std::string mRuntimeCompileMessage; - std::string mActiveShaderId; + mutable std::mutex mRuntimeLayerMutex; + RuntimeLayerModel mRuntimeLayerModel; + std::mutex mShaderBuildMutex; + std::map> mShaderBuilds; bool mStarted = false; bool mVideoOutputEnabled = false; std::string mVideoOutputStatus = "DeckLink output not started."; diff --git a/apps/RenderCadenceCompositor/control/HttpControlServer.cpp b/apps/RenderCadenceCompositor/control/HttpControlServer.cpp index 5d5fa20..6f80f89 100644 --- a/apps/RenderCadenceCompositor/control/HttpControlServer.cpp +++ b/apps/RenderCadenceCompositor/control/HttpControlServer.cpp @@ -266,6 +266,18 @@ HttpControlServer::HttpResponse HttpControlServer::ServePost(const HttpRequest& if (!IsKnownPostEndpoint(request.path)) return TextResponse("404 Not Found", "Not Found"); + if (request.path == "/api/layers/add" && mCallbacks.addLayer) + { + const ControlActionResult result = mCallbacks.addLayer(request.body); + return JsonResponse(result.ok ? "200 OK" : "400 Bad Request", ActionResponse(result.ok, result.error)); + } + + if (request.path == "/api/layers/remove" && mCallbacks.removeLayer) + { + const ControlActionResult result = mCallbacks.removeLayer(request.body); + return JsonResponse(result.ok ? "200 OK" : "400 Bad Request", ActionResponse(result.ok, result.error)); + } + return { "400 Bad Request", "application/json", diff --git a/apps/RenderCadenceCompositor/control/HttpControlServer.h b/apps/RenderCadenceCompositor/control/HttpControlServer.h index d816751..93b2e7f 100644 --- a/apps/RenderCadenceCompositor/control/HttpControlServer.h +++ b/apps/RenderCadenceCompositor/control/HttpControlServer.h @@ -19,9 +19,17 @@ struct HttpControlServerConfig std::chrono::milliseconds idleSleep = std::chrono::milliseconds(10); }; +struct ControlActionResult +{ + bool ok = false; + std::string error; +}; + struct HttpControlServerCallbacks { std::function getStateJson; + std::function addLayer; + std::function removeLayer; }; class UniqueSocket diff --git a/apps/RenderCadenceCompositor/control/RuntimeStateJson.h b/apps/RenderCadenceCompositor/control/RuntimeStateJson.h index 7b7da23..4003eb3 100644 --- a/apps/RenderCadenceCompositor/control/RuntimeStateJson.h +++ b/apps/RenderCadenceCompositor/control/RuntimeStateJson.h @@ -3,6 +3,7 @@ #include "../app/AppConfig.h" #include "../app/AppConfigProvider.h" #include "../json/JsonWriter.h" +#include "../runtime/RuntimeLayerModel.h" #include "../runtime/SupportedShaderCatalog.h" #include "../telemetry/CadenceTelemetryJson.h" @@ -19,10 +20,8 @@ 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; + const SupportedShaderCatalog& shaderCatalog; + const RuntimeLayerModelSnapshot& runtimeLayers; }; inline void WriteVideoIoStatusJson(JsonWriter& writer, const RuntimeStateJsonInput& input) @@ -112,6 +111,17 @@ inline void WriteFeedbackJson(JsonWriter& writer, const FeedbackSettings& feedba writer.EndObject(); } +inline const char* RuntimeLayerBuildStateName(RuntimeLayerBuildState state) +{ + switch (state) + { + case RuntimeLayerBuildState::Pending: return "pending"; + case RuntimeLayerBuildState::Ready: return "ready"; + case RuntimeLayerBuildState::Failed: return "failed"; + } + return "unknown"; +} + inline void WriteParameterDefinitionJson(JsonWriter& writer, const ShaderParameterDefinition& parameter) { writer.BeginObject(); @@ -164,22 +174,34 @@ inline void WriteParameterDefinitionJson(JsonWriter& writer, const ShaderParamet inline void WriteLayersJson(JsonWriter& writer, const RuntimeStateJsonInput& input) { writer.BeginArray(); - if (input.activeShaderPackage) + for (const RuntimeLayerReadModel& layer : input.runtimeLayers.displayLayers) { - const ShaderPackage& shaderPackage = *input.activeShaderPackage; + const ShaderPackage* shaderPackage = input.shaderCatalog.FindPackage(layer.shaderId); 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.KeyString("id", layer.id); + writer.KeyString("shaderId", layer.shaderId); + writer.KeyString("shaderName", layer.shaderName); + writer.KeyBool("bypass", layer.bypass); + writer.KeyString("buildState", RuntimeLayerBuildStateName(layer.buildState)); + writer.KeyBool("renderReady", layer.renderReady); + writer.KeyString("message", layer.message); writer.Key("temporal"); - WriteTemporalJson(writer, shaderPackage.temporal); + if (shaderPackage) + WriteTemporalJson(writer, shaderPackage->temporal); + else + WriteTemporalJson(writer, TemporalSettings()); writer.Key("feedback"); - WriteFeedbackJson(writer, shaderPackage.feedback); + if (shaderPackage) + WriteFeedbackJson(writer, shaderPackage->feedback); + else + WriteFeedbackJson(writer, FeedbackSettings()); writer.Key("parameters"); writer.BeginArray(); - for (const ShaderParameterDefinition& parameter : shaderPackage.parameters) - WriteParameterDefinitionJson(writer, parameter); + if (shaderPackage) + { + for (const ShaderParameterDefinition& parameter : shaderPackage->parameters) + WriteParameterDefinitionJson(writer, parameter); + } writer.EndArray(); writer.EndObject(); } @@ -209,9 +231,9 @@ inline std::string RuntimeStateToJson(const RuntimeStateJsonInput& input) writer.Key("runtime"); writer.BeginObject(); - writer.KeyUInt("layerCount", input.activeShaderPackage ? 1 : 0); - writer.KeyBool("compileSucceeded", input.runtimeCompileSucceeded); - writer.KeyString("compileMessage", input.runtimeCompileMessage); + writer.KeyUInt("layerCount", static_cast(input.runtimeLayers.displayLayers.size())); + writer.KeyBool("compileSucceeded", input.runtimeLayers.compileSucceeded); + writer.KeyString("compileMessage", input.runtimeLayers.compileMessage); writer.EndObject(); writer.Key("video"); @@ -250,7 +272,7 @@ inline std::string RuntimeStateToJson(const RuntimeStateJsonInput& input) writer.KeyNull("runtimeEvents"); writer.Key("shaders"); writer.BeginArray(); - for (const SupportedShaderSummary& shader : input.shaders) + for (const SupportedShaderSummary& shader : input.shaderCatalog.Shaders()) { writer.BeginObject(); writer.KeyString("id", shader.id); diff --git a/apps/RenderCadenceCompositor/runtime/RuntimeLayerModel.cpp b/apps/RenderCadenceCompositor/runtime/RuntimeLayerModel.cpp new file mode 100644 index 0000000..2d1edfb --- /dev/null +++ b/apps/RenderCadenceCompositor/runtime/RuntimeLayerModel.cpp @@ -0,0 +1,224 @@ +#include "RuntimeLayerModel.h" + +#include + +namespace RenderCadenceCompositor +{ +bool RuntimeLayerModel::InitializeSingleLayer(const SupportedShaderCatalog& shaderCatalog, const std::string& shaderId, std::string& error) +{ + Clear(); + if (shaderId.empty()) + { + error.clear(); + return true; + } + + const ShaderPackage* shaderPackage = shaderCatalog.FindPackage(shaderId); + if (!shaderPackage) + { + error = "Shader '" + shaderId + "' is not in the supported shader catalog."; + return false; + } + + Layer layer; + layer.id = AllocateLayerId(); + layer.shaderId = shaderPackage->id; + layer.shaderName = shaderPackage->displayName.empty() ? shaderPackage->id : shaderPackage->displayName; + layer.buildState = RuntimeLayerBuildState::Pending; + layer.message = "Runtime Slang build is waiting to start."; + mLayers.push_back(std::move(layer)); + error.clear(); + return true; +} + +bool RuntimeLayerModel::AddLayer(const SupportedShaderCatalog& shaderCatalog, const std::string& shaderId, std::string& layerId, std::string& error) +{ + const ShaderPackage* shaderPackage = shaderCatalog.FindPackage(shaderId); + if (!shaderPackage) + { + error = "Shader '" + shaderId + "' is not in the supported shader catalog."; + return false; + } + + Layer layer; + layer.id = AllocateLayerId(); + layer.shaderId = shaderPackage->id; + layer.shaderName = shaderPackage->displayName.empty() ? shaderPackage->id : shaderPackage->displayName; + layer.buildState = RuntimeLayerBuildState::Pending; + layer.message = "Runtime Slang build is waiting to start."; + layerId = layer.id; + mLayers.push_back(std::move(layer)); + error.clear(); + return true; +} + +bool RuntimeLayerModel::RemoveLayer(const std::string& layerId, std::string& error) +{ + for (auto layerIt = mLayers.begin(); layerIt != mLayers.end(); ++layerIt) + { + if (layerIt->id != layerId) + continue; + + mLayers.erase(layerIt); + error.clear(); + return true; + } + + error = "Unknown runtime layer id: " + layerId; + return false; +} + +void RuntimeLayerModel::Clear() +{ + mLayers.clear(); +} + +bool RuntimeLayerModel::MarkBuildStarted(const std::string& layerId, const std::string& message, std::string& error) +{ + Layer* layer = FindLayer(layerId); + if (!layer) + { + error = "Unknown runtime layer id: " + layerId; + return false; + } + + layer->buildState = RuntimeLayerBuildState::Pending; + layer->message = message; + layer->renderReady = false; + layer->artifact = RuntimeShaderArtifact(); + error.clear(); + return true; +} + +bool RuntimeLayerModel::MarkBuildReady(const RuntimeShaderArtifact& artifact, std::string& error) +{ + Layer* layer = artifact.layerId.empty() ? FindFirstLayerForShader(artifact.shaderId) : FindLayer(artifact.layerId); + if (!layer) + { + error = artifact.layerId.empty() + ? "No runtime layer is waiting for shader artifact: " + artifact.shaderId + : "No runtime layer is waiting for shader artifact on layer: " + artifact.layerId; + return false; + } + + layer->shaderName = artifact.displayName.empty() ? artifact.shaderId : artifact.displayName; + layer->buildState = RuntimeLayerBuildState::Ready; + layer->message = artifact.message; + layer->renderReady = true; + layer->artifact = artifact; + error.clear(); + return true; +} + +bool RuntimeLayerModel::MarkBuildFailedForShader(const std::string& shaderId, const std::string& message) +{ + Layer* layer = FindFirstLayerForShader(shaderId); + if (!layer) + return false; + + std::string error; + return MarkBuildFailed(layer->id, message, error); +} + +bool RuntimeLayerModel::MarkBuildFailed(const std::string& layerId, const std::string& message, std::string& error) +{ + Layer* layer = FindLayer(layerId); + if (!layer) + { + error = "Unknown runtime layer id: " + layerId; + return false; + } + + layer->buildState = RuntimeLayerBuildState::Failed; + layer->message = message; + layer->renderReady = false; + layer->artifact = RuntimeShaderArtifact(); + error.clear(); + return true; +} + +bool RuntimeLayerModel::MarkRenderCommitFailed(const std::string& layerId, const std::string& message, std::string& error) +{ + return MarkBuildFailed(layerId, message, error); +} + +RuntimeLayerModelSnapshot RuntimeLayerModel::Snapshot() const +{ + RuntimeLayerModelSnapshot snapshot; + snapshot.compileSucceeded = true; + + for (const Layer& layer : mLayers) + { + snapshot.displayLayers.push_back(ToReadModel(layer)); + if (!layer.message.empty() && snapshot.compileMessage.empty()) + snapshot.compileMessage = layer.message; + if (layer.buildState == RuntimeLayerBuildState::Failed) + snapshot.compileSucceeded = false; + if (layer.renderReady) + { + RuntimeRenderLayerModel renderLayer; + renderLayer.id = layer.id; + renderLayer.shaderId = layer.shaderId; + renderLayer.artifact = layer.artifact; + snapshot.renderLayers.push_back(std::move(renderLayer)); + } + } + + if (snapshot.compileMessage.empty()) + snapshot.compileMessage = mLayers.empty() ? "Runtime shader build disabled." : "Runtime shader build has not completed yet."; + return snapshot; +} + +std::string RuntimeLayerModel::FirstLayerId() const +{ + return mLayers.empty() ? std::string() : mLayers.front().id; +} + +RuntimeLayerModel::Layer* RuntimeLayerModel::FindLayer(const std::string& layerId) +{ + for (Layer& layer : mLayers) + { + if (layer.id == layerId) + return &layer; + } + return nullptr; +} + +const RuntimeLayerModel::Layer* RuntimeLayerModel::FindLayer(const std::string& layerId) const +{ + for (const Layer& layer : mLayers) + { + if (layer.id == layerId) + return &layer; + } + return nullptr; +} + +RuntimeLayerModel::Layer* RuntimeLayerModel::FindFirstLayerForShader(const std::string& shaderId) +{ + for (Layer& layer : mLayers) + { + if (layer.shaderId == shaderId) + return &layer; + } + return nullptr; +} + +std::string RuntimeLayerModel::AllocateLayerId() +{ + return "runtime-layer-" + std::to_string(mNextLayerNumber++); +} + +RuntimeLayerReadModel RuntimeLayerModel::ToReadModel(const Layer& layer) +{ + RuntimeLayerReadModel readModel; + readModel.id = layer.id; + readModel.shaderId = layer.shaderId; + readModel.shaderName = layer.shaderName; + readModel.bypass = layer.bypass; + readModel.buildState = layer.buildState; + readModel.message = layer.message; + readModel.renderReady = layer.renderReady; + return readModel; +} +} diff --git a/apps/RenderCadenceCompositor/runtime/RuntimeLayerModel.h b/apps/RenderCadenceCompositor/runtime/RuntimeLayerModel.h new file mode 100644 index 0000000..1e5d478 --- /dev/null +++ b/apps/RenderCadenceCompositor/runtime/RuntimeLayerModel.h @@ -0,0 +1,84 @@ +#pragma once + +#include "RuntimeShaderArtifact.h" +#include "SupportedShaderCatalog.h" + +#include +#include +#include + +namespace RenderCadenceCompositor +{ +enum class RuntimeLayerBuildState +{ + Pending, + Ready, + Failed +}; + +struct RuntimeLayerReadModel +{ + std::string id; + std::string shaderId; + std::string shaderName; + bool bypass = false; + RuntimeLayerBuildState buildState = RuntimeLayerBuildState::Pending; + std::string message; + bool renderReady = false; +}; + +struct RuntimeRenderLayerModel +{ + std::string id; + std::string shaderId; + RuntimeShaderArtifact artifact; +}; + +struct RuntimeLayerModelSnapshot +{ + bool compileSucceeded = true; + std::string compileMessage; + std::vector displayLayers; + std::vector renderLayers; +}; + +class RuntimeLayerModel +{ +public: + bool InitializeSingleLayer(const SupportedShaderCatalog& shaderCatalog, const std::string& shaderId, std::string& error); + void Clear(); + + bool AddLayer(const SupportedShaderCatalog& shaderCatalog, const std::string& shaderId, std::string& layerId, std::string& error); + bool RemoveLayer(const std::string& layerId, std::string& error); + bool MarkBuildStarted(const std::string& layerId, const std::string& message, std::string& error); + bool MarkBuildReady(const RuntimeShaderArtifact& artifact, std::string& error); + bool MarkBuildFailedForShader(const std::string& shaderId, const std::string& message); + bool MarkBuildFailed(const std::string& layerId, const std::string& message, std::string& error); + bool MarkRenderCommitFailed(const std::string& layerId, const std::string& message, std::string& error); + + RuntimeLayerModelSnapshot Snapshot() const; + std::string FirstLayerId() const; + +private: + struct Layer + { + std::string id; + std::string shaderId; + std::string shaderName; + bool bypass = false; + RuntimeLayerBuildState buildState = RuntimeLayerBuildState::Pending; + std::string message; + bool renderReady = false; + RuntimeShaderArtifact artifact; + }; + + Layer* FindLayer(const std::string& layerId); + const Layer* FindLayer(const std::string& layerId) const; + Layer* FindFirstLayerForShader(const std::string& shaderId); + std::string AllocateLayerId(); + static RuntimeLayerReadModel ToReadModel(const Layer& layer); + + std::vector mLayers; + uint64_t mNextLayerNumber = 1; +}; +} diff --git a/apps/RenderCadenceCompositor/runtime/RuntimeShaderArtifact.h b/apps/RenderCadenceCompositor/runtime/RuntimeShaderArtifact.h index ce2bf4e..3025603 100644 --- a/apps/RenderCadenceCompositor/runtime/RuntimeShaderArtifact.h +++ b/apps/RenderCadenceCompositor/runtime/RuntimeShaderArtifact.h @@ -7,6 +7,7 @@ struct RuntimeShaderArtifact { + std::string layerId; std::string shaderId; std::string displayName; std::string fragmentShaderSource; diff --git a/apps/RenderCadenceCompositor/runtime/RuntimeShaderBridge.cpp b/apps/RenderCadenceCompositor/runtime/RuntimeShaderBridge.cpp index 0ab162e..dc64dbd 100644 --- a/apps/RenderCadenceCompositor/runtime/RuntimeShaderBridge.cpp +++ b/apps/RenderCadenceCompositor/runtime/RuntimeShaderBridge.cpp @@ -8,11 +8,17 @@ RuntimeShaderBridge::~RuntimeShaderBridge() } void RuntimeShaderBridge::Start(const std::string& shaderId, ArtifactCallback onArtifactReady, ErrorCallback onError) +{ + Start(std::string(), shaderId, std::move(onArtifactReady), std::move(onError)); +} + +void RuntimeShaderBridge::Start(const std::string& layerId, const std::string& shaderId, ArtifactCallback onArtifactReady, ErrorCallback onError) { Stop(); if (shaderId.empty()) return; + mLayerId = layerId; mOnArtifactReady = std::move(onArtifactReady); mOnError = std::move(onError); mStopping.store(false, std::memory_order_release); @@ -26,6 +32,7 @@ void RuntimeShaderBridge::Stop() if (mThread.joinable()) mThread.join(); mCompiler.Stop(); + mLayerId.clear(); mOnArtifactReady = ArtifactCallback(); mOnError = ErrorCallback(); } @@ -39,6 +46,7 @@ void RuntimeShaderBridge::ThreadMain() { if (build.succeeded) { + build.artifact.layerId = mLayerId; if (mOnArtifactReady) mOnArtifactReady(build.artifact); } diff --git a/apps/RenderCadenceCompositor/runtime/RuntimeShaderBridge.h b/apps/RenderCadenceCompositor/runtime/RuntimeShaderBridge.h index c60d22d..60256a5 100644 --- a/apps/RenderCadenceCompositor/runtime/RuntimeShaderBridge.h +++ b/apps/RenderCadenceCompositor/runtime/RuntimeShaderBridge.h @@ -20,6 +20,7 @@ public: ~RuntimeShaderBridge(); void Start(const std::string& shaderId, ArtifactCallback onArtifactReady, ErrorCallback onError); + void Start(const std::string& layerId, const std::string& shaderId, ArtifactCallback onArtifactReady, ErrorCallback onError); void Stop(); private: @@ -28,6 +29,7 @@ private: RuntimeSlangShaderCompiler mCompiler; std::thread mThread; std::atomic mStopping{ false }; + std::string mLayerId; ArtifactCallback mOnArtifactReady; ErrorCallback mOnError; }; diff --git a/docs/RENDER_CADENCE_GOLDEN_RULES.md b/docs/RENDER_CADENCE_GOLDEN_RULES.md index 30cc1cd..303a89c 100644 --- a/docs/RENDER_CADENCE_GOLDEN_RULES.md +++ b/docs/RENDER_CADENCE_GOLDEN_RULES.md @@ -69,6 +69,8 @@ The CPU/build phase may parse manifests, invoke Slang, validate package shape, a The render thread receives a completed artifact and either commits it at a frame boundary or rejects it. A failed artifact must not disturb the current renderer. +The display/render layer model is app-owned. It may track requested shaders, build state, display metadata, and render-ready artifacts, but it must not perform GL work or drive render cadence directly. + ## 5. No Hidden Blocking In The Cadence Path The render loop must not do work with unbounded or OS-dependent latency. @@ -124,6 +126,7 @@ Preferred boundaries: - render cadence/thread ownership - GL rendering - runtime artifact build/bridge +- app-owned display/render layer model - parameter packing - system-memory frame exchange - DeckLink output scheduling diff --git a/tests/RenderCadenceCompositorHttpControlServerTests.cpp b/tests/RenderCadenceCompositorHttpControlServerTests.cpp index 33bead0..6865e15 100644 --- a/tests/RenderCadenceCompositorHttpControlServerTests.cpp +++ b/tests/RenderCadenceCompositorHttpControlServerTests.cpp @@ -106,6 +106,39 @@ void TestKnownPostEndpointReturnsActionError() Expect(response.body.find("not implemented") != std::string::npos, "unimplemented post reports diagnostic"); } +void TestLayerPostEndpointsUseCallbacks() +{ + using namespace RenderCadenceCompositor; + + HttpControlServer server; + HttpControlServerCallbacks callbacks; + callbacks.addLayer = [](const std::string& body) { + Expect(body.find("solid") != std::string::npos, "add callback receives request body"); + return ControlActionResult{ true, std::string() }; + }; + callbacks.removeLayer = [](const std::string& body) { + Expect(body.find("runtime-layer-1") != std::string::npos, "remove callback receives request body"); + return ControlActionResult{ false, "Unknown layer id." }; + }; + server.SetCallbacksForTest(callbacks); + + HttpControlServer::HttpRequest addRequest; + addRequest.method = "POST"; + addRequest.path = "/api/layers/add"; + addRequest.body = "{\"shaderId\":\"solid\"}"; + const HttpControlServer::HttpResponse addResponse = server.RouteRequestForTest(addRequest); + ExpectEquals(addResponse.status, "200 OK", "add layer callback success returns 200"); + Expect(addResponse.body.find("\"ok\":true") != std::string::npos, "add layer callback returns action success"); + + HttpControlServer::HttpRequest removeRequest; + removeRequest.method = "POST"; + removeRequest.path = "/api/layers/remove"; + removeRequest.body = "{\"layerId\":\"runtime-layer-1\"}"; + const HttpControlServer::HttpResponse removeResponse = server.RouteRequestForTest(removeRequest); + ExpectEquals(removeResponse.status, "400 Bad Request", "remove layer callback failure returns 400"); + Expect(removeResponse.body.find("Unknown layer id.") != std::string::npos, "remove layer callback returns diagnostic"); +} + void TestUnknownEndpointReturns404() { using namespace RenderCadenceCompositor; @@ -126,6 +159,7 @@ int main() TestStateEndpointUsesCallback(); TestRootServesUiIndex(); TestKnownPostEndpointReturnsActionError(); + TestLayerPostEndpointsUseCallbacks(); TestUnknownEndpointReturns404(); if (gFailures != 0) diff --git a/tests/RenderCadenceCompositorRuntimeLayerModelTests.cpp b/tests/RenderCadenceCompositorRuntimeLayerModelTests.cpp new file mode 100644 index 0000000..01a44bd --- /dev/null +++ b/tests/RenderCadenceCompositorRuntimeLayerModelTests.cpp @@ -0,0 +1,163 @@ +#include "RuntimeLayerModel.h" + +#include +#include +#include +#include +#include + +namespace +{ +int gFailures = 0; + +void Expect(bool condition, const std::string& message) +{ + if (condition) + return; + + ++gFailures; + std::cerr << "FAIL: " << message << "\n"; +} + +std::filesystem::path MakeTestRoot() +{ + const auto stamp = std::chrono::steady_clock::now().time_since_epoch().count(); + const std::filesystem::path root = std::filesystem::temp_directory_path() / ("render-cadence-layer-model-tests-" + std::to_string(stamp)); + std::filesystem::create_directories(root); + return root; +} + +void WriteFile(const std::filesystem::path& path, const std::string& contents) +{ + std::filesystem::create_directories(path.parent_path()); + std::ofstream output(path, std::ios::binary); + output << contents; +} + +RenderCadenceCompositor::SupportedShaderCatalog MakeCatalog(std::filesystem::path& root) +{ + root = MakeTestRoot(); + WriteFile(root / "solid" / "shader.slang", "float4 shadeVideo(float2 uv) { return float4(uv, 0.0, 1.0); }\n"); + WriteFile(root / "solid" / "shader.json", R"({ + "id": "solid", + "name": "Solid", + "description": "Solid test shader", + "category": "Tests", + "entryPoint": "shadeVideo", + "parameters": [ + { "id": "gain", "label": "Gain", "type": "float", "default": 0.5 } + ] + })"); + + RenderCadenceCompositor::SupportedShaderCatalog catalog; + std::string error; + Expect(catalog.Load(root, 4, error), error.empty() ? "catalog loads test shader" : error); + return catalog; +} + +void TestSingleLayerLifecycle() +{ + std::filesystem::path root; + RenderCadenceCompositor::SupportedShaderCatalog catalog = MakeCatalog(root); + + RenderCadenceCompositor::RuntimeLayerModel model; + std::string error; + Expect(model.InitializeSingleLayer(catalog, "solid", error), "model initializes a supported startup shader"); + Expect(model.FirstLayerId() == "runtime-layer-1", "startup layer id is stable"); + + Expect(model.MarkBuildStarted(model.FirstLayerId(), "build started", error), "build start updates the layer"); + RenderCadenceCompositor::RuntimeLayerModelSnapshot snapshot = model.Snapshot(); + Expect(snapshot.displayLayers.size() == 1, "snapshot exposes the display layer"); + Expect(snapshot.displayLayers[0].buildState == RenderCadenceCompositor::RuntimeLayerBuildState::Pending, "started layer is pending"); + Expect(!snapshot.displayLayers[0].renderReady, "started layer is not render-ready yet"); + + RuntimeShaderArtifact artifact; + artifact.shaderId = "solid"; + artifact.displayName = "Solid"; + artifact.fragmentShaderSource = "void main(){}"; + artifact.message = "build ready"; + Expect(model.MarkBuildReady(artifact, error), "ready artifact updates the matching layer"); + + snapshot = model.Snapshot(); + Expect(snapshot.compileSucceeded, "ready layer reports compile success"); + Expect(snapshot.displayLayers[0].buildState == RenderCadenceCompositor::RuntimeLayerBuildState::Ready, "ready layer is marked ready"); + Expect(snapshot.displayLayers[0].renderReady, "ready layer exposes render readiness"); + Expect(snapshot.renderLayers.size() == 1, "ready layer produces one render layer artifact"); + Expect(snapshot.renderLayers[0].artifact.shaderId == "solid", "render layer carries the artifact"); + + std::filesystem::remove_all(root); +} + +void TestRejectsUnsupportedStartupShader() +{ + std::filesystem::path root; + RenderCadenceCompositor::SupportedShaderCatalog catalog = MakeCatalog(root); + + RenderCadenceCompositor::RuntimeLayerModel model; + std::string error; + Expect(!model.InitializeSingleLayer(catalog, "missing", error), "model rejects unsupported shader ids"); + Expect(!error.empty(), "unsupported shader rejection explains the problem"); + Expect(model.Snapshot().displayLayers.empty(), "rejected startup shader leaves no display layer"); + + std::filesystem::remove_all(root); +} + +void TestBuildFailureStaysDisplaySide() +{ + std::filesystem::path root; + RenderCadenceCompositor::SupportedShaderCatalog catalog = MakeCatalog(root); + + RenderCadenceCompositor::RuntimeLayerModel model; + std::string error; + Expect(model.InitializeSingleLayer(catalog, "solid", error), "model initializes for failure test"); + Expect(model.MarkBuildFailed(model.FirstLayerId(), "compile failed", error), "build failure updates the layer"); + + const RenderCadenceCompositor::RuntimeLayerModelSnapshot snapshot = model.Snapshot(); + Expect(!snapshot.compileSucceeded, "failed layer reports compile failure"); + Expect(snapshot.displayLayers[0].buildState == RenderCadenceCompositor::RuntimeLayerBuildState::Failed, "failed layer is marked failed"); + Expect(!snapshot.displayLayers[0].renderReady, "failed layer is not render-ready"); + Expect(snapshot.renderLayers.empty(), "failed layer does not produce a render artifact"); + + std::filesystem::remove_all(root); +} + +void TestAddAndRemoveLayers() +{ + std::filesystem::path root; + RenderCadenceCompositor::SupportedShaderCatalog catalog = MakeCatalog(root); + + RenderCadenceCompositor::RuntimeLayerModel model; + std::string error; + std::string firstLayerId; + std::string secondLayerId; + Expect(model.AddLayer(catalog, "solid", firstLayerId, error), "first layer can be added"); + Expect(model.AddLayer(catalog, "solid", secondLayerId, error), "second layer can be added"); + Expect(firstLayerId != secondLayerId, "added layers receive distinct ids"); + Expect(model.Snapshot().displayLayers.size() == 2, "added layers appear in display snapshot"); + + Expect(model.RemoveLayer(firstLayerId, error), "existing layer can be removed"); + RenderCadenceCompositor::RuntimeLayerModelSnapshot snapshot = model.Snapshot(); + Expect(snapshot.displayLayers.size() == 1, "removed layer leaves snapshot"); + Expect(snapshot.displayLayers[0].id == secondLayerId, "remaining layer identity is preserved"); + Expect(!model.RemoveLayer(firstLayerId, error), "removed layer cannot be removed twice"); + + std::filesystem::remove_all(root); +} +} + +int main() +{ + TestSingleLayerLifecycle(); + TestRejectsUnsupportedStartupShader(); + TestBuildFailureStaysDisplaySide(); + TestAddAndRemoveLayers(); + + if (gFailures != 0) + { + std::cerr << gFailures << " RenderCadenceCompositorRuntimeLayerModel test failure(s).\n"; + return 1; + } + + std::cout << "RenderCadenceCompositorRuntimeLayerModel tests passed.\n"; + return 0; +} diff --git a/tests/RenderCadenceCompositorRuntimeStateJsonTests.cpp b/tests/RenderCadenceCompositorRuntimeStateJsonTests.cpp index 327d771..7836387 100644 --- a/tests/RenderCadenceCompositorRuntimeStateJsonTests.cpp +++ b/tests/RenderCadenceCompositorRuntimeStateJsonTests.cpp @@ -1,8 +1,10 @@ #include "RuntimeStateJson.h" +#include +#include +#include #include #include -#include namespace { @@ -17,29 +19,19 @@ void ExpectContains(const std::string& text, const std::string& fragment, const std::cerr << "FAIL: " << message << "\n"; } -ShaderPackage MakePackage() +std::filesystem::path MakeTestRoot() { - ShaderPackage package; - package.id = "solid-color"; - package.displayName = "Solid Color"; - package.description = "A single color shader."; - package.category = "Generator"; + const auto stamp = std::chrono::steady_clock::now().time_since_epoch().count(); + const std::filesystem::path root = std::filesystem::temp_directory_path() / ("render-cadence-state-json-tests-" + std::to_string(stamp)); + std::filesystem::create_directories(root); + return root; +} - 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; +void WriteFile(const std::filesystem::path& path, const std::string& contents) +{ + std::filesystem::create_directories(path.parent_path()); + std::ofstream output(path, std::ios::binary); + output << contents; } } @@ -53,10 +45,41 @@ int main() 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::filesystem::path root = MakeTestRoot(); + WriteFile(root / "solid-color" / "shader.slang", "float4 shadeVideo(float2 uv) { return float4(uv, 0.0, 1.0); }\n"); + WriteFile(root / "solid-color" / "shader.json", R"({ + "id": "solid-color", + "name": "Solid Color", + "description": "A single color shader.", + "category": "Generator", + "entryPoint": "shadeVideo", + "parameters": [ + { + "id": "color", + "label": "Color", + "description": "Output color.", + "type": "color", + "default": [1.0, 0.25, 0.5, 1.0], + "min": [0.0, 0.0, 0.0, 0.0], + "max": [1.0, 1.0, 1.0, 1.0], + "step": [0.01, 0.01, 0.01, 0.01] + } + ] + })"); + + RenderCadenceCompositor::SupportedShaderCatalog shaderCatalog; + std::string error; + ExpectContains(shaderCatalog.Load(root, 4, error) ? "loaded" : error, "loaded", "test shader catalog should load"); + + RenderCadenceCompositor::RuntimeLayerModel layerModel; + layerModel.InitializeSingleLayer(shaderCatalog, "solid-color", error); + RuntimeShaderArtifact artifact; + artifact.shaderId = "solid-color"; + artifact.displayName = "Solid Color"; + artifact.fragmentShaderSource = "void main(){}"; + artifact.message = "Runtime shader committed."; + layerModel.MarkBuildReady(artifact, error); + const RenderCadenceCompositor::RuntimeLayerModelSnapshot layerSnapshot = layerModel.Snapshot(); const std::string json = RenderCadenceCompositor::RuntimeStateToJson(RenderCadenceCompositor::RuntimeStateJsonInput{ config, @@ -64,10 +87,8 @@ int main() 8080, true, "DeckLink scheduled output running.", - shaders, - true, - "Runtime shader committed.", - &package + shaderCatalog, + layerSnapshot }); ExpectContains(json, "\"shaders\":[{\"id\":\"solid-color\"", "state JSON should include supported shaders"); @@ -78,6 +99,8 @@ int main() ExpectContains(json, "\"width\":1920", "state JSON should expose output width"); ExpectContains(json, "\"height\":1080", "state JSON should expose output height"); + std::filesystem::remove_all(root); + if (gFailures != 0) { std::cerr << gFailures << " RenderCadenceCompositorRuntimeStateJson test failure(s).\n";