diff --git a/CMakeLists.txt b/CMakeLists.txt index 9a66db7..3ffd8b0 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -290,6 +290,8 @@ set(RENDER_CADENCE_APP_SOURCES "${APP_DIR}/gl/shader/Std140Buffer.h" "${APP_DIR}/runtime/support/RuntimeJson.cpp" "${APP_DIR}/runtime/support/RuntimeJson.h" + "${APP_DIR}/runtime/support/RuntimeParameterUtils.cpp" + "${APP_DIR}/runtime/support/RuntimeParameterUtils.h" "${APP_DIR}/shader/ShaderCompiler.cpp" "${APP_DIR}/shader/ShaderCompiler.h" "${APP_DIR}/shader/ShaderPackageRegistry.cpp" @@ -310,6 +312,8 @@ set(RENDER_CADENCE_APP_SOURCES "${RENDER_CADENCE_APP_DIR}/app/RuntimeLayerController.cpp" "${RENDER_CADENCE_APP_DIR}/app/RuntimeLayerController.h" "${RENDER_CADENCE_APP_DIR}/control/ControlActionResult.h" + "${RENDER_CADENCE_APP_DIR}/control/RuntimeControlCommand.cpp" + "${RENDER_CADENCE_APP_DIR}/control/RuntimeControlCommand.h" "${RENDER_CADENCE_APP_DIR}/control/http/HttpControlServer.cpp" "${RENDER_CADENCE_APP_DIR}/control/http/HttpControlServer.h" "${RENDER_CADENCE_APP_DIR}/control/http/HttpControlServerWebSocket.cpp" @@ -841,6 +845,7 @@ add_executable(RenderCadenceCompositorRuntimeLayerModelTests "${RENDER_CADENCE_APP_DIR}/runtime/SupportedShaderCatalog.cpp" "${APP_DIR}/shader/ShaderPackageRegistry.cpp" "${APP_DIR}/runtime/support/RuntimeJson.cpp" + "${APP_DIR}/runtime/support/RuntimeParameterUtils.cpp" "${CMAKE_CURRENT_SOURCE_DIR}/tests/RenderCadenceCompositorRuntimeLayerModelTests.cpp" ) @@ -909,6 +914,7 @@ add_test(NAME RenderCadenceCompositorJsonWriterTests COMMAND RenderCadenceCompos add_executable(RenderCadenceCompositorRuntimeStateJsonTests "${APP_DIR}/runtime/support/RuntimeJson.cpp" + "${APP_DIR}/runtime/support/RuntimeParameterUtils.cpp" "${APP_DIR}/shader/ShaderPackageRegistry.cpp" "${RENDER_CADENCE_APP_DIR}/app/AppConfig.cpp" "${RENDER_CADENCE_APP_DIR}/app/AppConfigProvider.cpp" @@ -940,6 +946,8 @@ endif() add_test(NAME RenderCadenceCompositorRuntimeStateJsonTests COMMAND RenderCadenceCompositorRuntimeStateJsonTests) add_executable(RenderCadenceCompositorHttpControlServerTests + "${APP_DIR}/runtime/support/RuntimeJson.cpp" + "${RENDER_CADENCE_APP_DIR}/control/RuntimeControlCommand.cpp" "${RENDER_CADENCE_APP_DIR}/control/http/HttpControlServer.cpp" "${RENDER_CADENCE_APP_DIR}/control/http/HttpControlServerWebSocket.cpp" "${RENDER_CADENCE_APP_DIR}/json/JsonWriter.cpp" @@ -948,6 +956,8 @@ add_executable(RenderCadenceCompositorHttpControlServerTests ) target_include_directories(RenderCadenceCompositorHttpControlServerTests PRIVATE + "${APP_DIR}" + "${APP_DIR}/runtime/support" "${RENDER_CADENCE_APP_DIR}/control" "${RENDER_CADENCE_APP_DIR}/control/http" "${RENDER_CADENCE_APP_DIR}/json" diff --git a/apps/RenderCadenceCompositor/README.md b/apps/RenderCadenceCompositor/README.md index 33bc7aa..fd0ba34 100644 --- a/apps/RenderCadenceCompositor/README.md +++ b/apps/RenderCadenceCompositor/README.md @@ -198,10 +198,10 @@ Current endpoints: - `GET /ws`: upgrades to a WebSocket and streams state snapshots when they change - `GET /docs/openapi.yaml` and `GET /openapi.yaml`: serves the OpenAPI document - `GET /docs`: serves Swagger UI -- `POST /api/layers/add` and `POST /api/layers/remove` mutate the app-owned display layer model only +- `POST /api/layers/add`, `/remove`, `/reorder`, `/set-bypass`, `/set-shader`, `/update-parameter`, and `/reset-parameters` use the shared runtime control-command path - 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. +The HTTP server runs on its own thread. It serves static UI/docs files, samples/copies telemetry through callbacks, and translates POST bodies into runtime control commands. Command execution is app-owned, so future OSC ingress can create the same commands without depending on HTTP route code. Control commands may update the display layer model, start background shader builds, or publish an already-built render-layer snapshot, but they do not call render work or DeckLink scheduling directly. ## Optional DeckLink Output diff --git a/apps/RenderCadenceCompositor/app/RenderCadenceApp.h b/apps/RenderCadenceCompositor/app/RenderCadenceApp.h index 2620fbb..32629b5 100644 --- a/apps/RenderCadenceCompositor/app/RenderCadenceApp.h +++ b/apps/RenderCadenceCompositor/app/RenderCadenceApp.h @@ -184,6 +184,13 @@ private: callbacks.removeLayer = [this](const std::string& body) { return mRuntimeLayers.HandleRemoveLayer(body); }; + callbacks.executePost = [this](const std::string& path, const std::string& body) { + RuntimeControlCommand command; + std::string error; + if (!ParseRuntimeControlCommand(path, body, command, error)) + return ControlActionResult{ false, error }; + return mRuntimeLayers.HandleControlCommand(command); + }; std::string error; if (!mHttpServer.Start( diff --git a/apps/RenderCadenceCompositor/app/RuntimeLayerController.cpp b/apps/RenderCadenceCompositor/app/RuntimeLayerController.cpp index d145d73..6a04c9b 100644 --- a/apps/RenderCadenceCompositor/app/RuntimeLayerController.cpp +++ b/apps/RenderCadenceCompositor/app/RuntimeLayerController.cpp @@ -90,6 +90,95 @@ ControlActionResult RuntimeLayerController::HandleRemoveLayer(const std::string& return { true, std::string() }; } +ControlActionResult RuntimeLayerController::HandleControlCommand(const RuntimeControlCommand& command) +{ + CleanupRetiredShaderBuilds(); + + std::string error; + switch (command.type) + { + case RuntimeControlCommandType::AddLayer: + { + std::string layerId; + { + std::lock_guard lock(mRuntimeLayerMutex); + if (!mRuntimeLayerModel.AddLayer(mShaderCatalog, command.shaderId, layerId, error)) + return { false, error }; + } + Log("runtime-shader", "Layer added: " + layerId + " shader=" + command.shaderId); + StartLayerShaderBuild(layerId, command.shaderId); + return { true, std::string() }; + } + case RuntimeControlCommandType::RemoveLayer: + { + { + std::lock_guard lock(mRuntimeLayerMutex); + if (!mRuntimeLayerModel.RemoveLayer(command.layerId, error)) + return { false, error }; + } + Log("runtime-shader", "Layer removed: " + command.layerId); + RetireLayerShaderBuild(command.layerId); + PublishRuntimeRenderLayers(); + return { true, std::string() }; + } + case RuntimeControlCommandType::ReorderLayer: + { + { + std::lock_guard lock(mRuntimeLayerMutex); + if (!mRuntimeLayerModel.ReorderLayer(command.layerId, command.targetIndex, error)) + return { false, error }; + } + PublishRuntimeRenderLayers(); + return { true, std::string() }; + } + case RuntimeControlCommandType::SetLayerBypass: + { + { + std::lock_guard lock(mRuntimeLayerMutex); + if (!mRuntimeLayerModel.SetLayerBypass(command.layerId, command.bypass, error)) + return { false, error }; + } + PublishRuntimeRenderLayers(); + return { true, std::string() }; + } + case RuntimeControlCommandType::SetLayerShader: + { + { + std::lock_guard lock(mRuntimeLayerMutex); + if (!mRuntimeLayerModel.SetLayerShader(mShaderCatalog, command.layerId, command.shaderId, error)) + return { false, error }; + } + Log("runtime-shader", "Layer shader changed: " + command.layerId + " shader=" + command.shaderId); + StartLayerShaderBuild(command.layerId, command.shaderId); + return { true, std::string() }; + } + case RuntimeControlCommandType::UpdateLayerParameter: + { + { + std::lock_guard lock(mRuntimeLayerMutex); + if (!mRuntimeLayerModel.UpdateParameter(command.layerId, command.parameterId, command.value, error)) + return { false, error }; + } + PublishRuntimeRenderLayers(); + return { true, std::string() }; + } + case RuntimeControlCommandType::ResetLayerParameters: + { + { + std::lock_guard lock(mRuntimeLayerMutex); + if (!mRuntimeLayerModel.ResetParameters(command.layerId, error)) + return { false, error }; + } + PublishRuntimeRenderLayers(); + return { true, std::string() }; + } + case RuntimeControlCommandType::Unsupported: + break; + } + + return { false, "Unsupported runtime control command." }; +} + RuntimeLayerModelSnapshot RuntimeLayerController::Snapshot(const CadenceTelemetrySnapshot& telemetry) const { std::lock_guard lock(mRuntimeLayerMutex); diff --git a/apps/RenderCadenceCompositor/app/RuntimeLayerController.h b/apps/RenderCadenceCompositor/app/RuntimeLayerController.h index cb52fca..e331685 100644 --- a/apps/RenderCadenceCompositor/app/RuntimeLayerController.h +++ b/apps/RenderCadenceCompositor/app/RuntimeLayerController.h @@ -1,6 +1,7 @@ #pragma once #include "../control/ControlActionResult.h" +#include "../control/RuntimeControlCommand.h" #include "../runtime/RuntimeLayerModel.h" #include "../runtime/RuntimeShaderBridge.h" #include "../runtime/SupportedShaderCatalog.h" @@ -32,6 +33,7 @@ public: ControlActionResult HandleAddLayer(const std::string& body); ControlActionResult HandleRemoveLayer(const std::string& body); + ControlActionResult HandleControlCommand(const RuntimeControlCommand& command); RuntimeLayerModelSnapshot Snapshot(const CadenceTelemetrySnapshot& telemetry) const; const SupportedShaderCatalog& ShaderCatalog() const { return mShaderCatalog; } diff --git a/apps/RenderCadenceCompositor/control/RuntimeControlCommand.cpp b/apps/RenderCadenceCompositor/control/RuntimeControlCommand.cpp new file mode 100644 index 0000000..96ad9e5 --- /dev/null +++ b/apps/RenderCadenceCompositor/control/RuntimeControlCommand.cpp @@ -0,0 +1,127 @@ +#include "RuntimeControlCommand.h" + +namespace RenderCadenceCompositor +{ +namespace +{ +const JsonValue* RequireObjectField(const JsonValue& root, const char* fieldName, std::string& error) +{ + const JsonValue* field = root.find(fieldName); + if (!field) + error = std::string("Request field '") + fieldName + "' is required."; + return field; +} + +bool RequireStringField(const JsonValue& root, const char* fieldName, std::string& value, std::string& error) +{ + const JsonValue* field = RequireObjectField(root, fieldName, error); + if (!field) + return false; + if (!field->isString() || field->asString().empty()) + { + error = std::string("Request field '") + fieldName + "' must be a non-empty string."; + return false; + } + value = field->asString(); + return true; +} + +bool RequireBoolField(const JsonValue& root, const char* fieldName, bool& value, std::string& error) +{ + const JsonValue* field = RequireObjectField(root, fieldName, error); + if (!field) + return false; + if (!field->isBoolean()) + { + error = std::string("Request field '") + fieldName + "' must be a boolean."; + return false; + } + value = field->asBoolean(); + return true; +} + +bool RequireIntegerField(const JsonValue& root, const char* fieldName, int& value, std::string& error) +{ + const JsonValue* field = RequireObjectField(root, fieldName, error); + if (!field) + return false; + if (!field->isNumber()) + { + error = std::string("Request field '") + fieldName + "' must be a number."; + return false; + } + value = static_cast(field->asNumber()); + return true; +} +} + +bool ParseRuntimeControlCommand( + const std::string& path, + const std::string& body, + RuntimeControlCommand& command, + std::string& error) +{ + command = RuntimeControlCommand(); + + 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; + } + + if (path == "/api/layers/add") + { + command.type = RuntimeControlCommandType::AddLayer; + return RequireStringField(root, "shaderId", command.shaderId, error); + } + if (path == "/api/layers/remove") + { + command.type = RuntimeControlCommandType::RemoveLayer; + return RequireStringField(root, "layerId", command.layerId, error); + } + if (path == "/api/layers/reorder") + { + command.type = RuntimeControlCommandType::ReorderLayer; + return RequireStringField(root, "layerId", command.layerId, error) + && RequireIntegerField(root, "targetIndex", command.targetIndex, error); + } + if (path == "/api/layers/set-bypass") + { + command.type = RuntimeControlCommandType::SetLayerBypass; + return RequireStringField(root, "layerId", command.layerId, error) + && RequireBoolField(root, "bypass", command.bypass, error); + } + if (path == "/api/layers/set-shader") + { + command.type = RuntimeControlCommandType::SetLayerShader; + return RequireStringField(root, "layerId", command.layerId, error) + && RequireStringField(root, "shaderId", command.shaderId, error); + } + if (path == "/api/layers/update-parameter") + { + command.type = RuntimeControlCommandType::UpdateLayerParameter; + const JsonValue* value = nullptr; + if (!RequireStringField(root, "layerId", command.layerId, error) + || !RequireStringField(root, "parameterId", command.parameterId, error)) + { + return false; + } + value = RequireObjectField(root, "value", error); + if (!value) + return false; + command.value = *value; + return true; + } + if (path == "/api/layers/reset-parameters") + { + command.type = RuntimeControlCommandType::ResetLayerParameters; + return RequireStringField(root, "layerId", command.layerId, error); + } + + command.type = RuntimeControlCommandType::Unsupported; + error = "Endpoint is not implemented in RenderCadenceCompositor yet."; + return false; +} +} diff --git a/apps/RenderCadenceCompositor/control/RuntimeControlCommand.h b/apps/RenderCadenceCompositor/control/RuntimeControlCommand.h new file mode 100644 index 0000000..0bb0095 --- /dev/null +++ b/apps/RenderCadenceCompositor/control/RuntimeControlCommand.h @@ -0,0 +1,37 @@ +#pragma once + +#include "RuntimeJson.h" + +#include + +namespace RenderCadenceCompositor +{ +enum class RuntimeControlCommandType +{ + AddLayer, + RemoveLayer, + ReorderLayer, + SetLayerBypass, + SetLayerShader, + UpdateLayerParameter, + ResetLayerParameters, + Unsupported +}; + +struct RuntimeControlCommand +{ + RuntimeControlCommandType type = RuntimeControlCommandType::Unsupported; + std::string layerId; + std::string shaderId; + std::string parameterId; + int targetIndex = 0; + bool bypass = false; + JsonValue value; +}; + +bool ParseRuntimeControlCommand( + const std::string& path, + const std::string& body, + RuntimeControlCommand& command, + std::string& error); +} diff --git a/apps/RenderCadenceCompositor/control/RuntimeStateJson.h b/apps/RenderCadenceCompositor/control/RuntimeStateJson.h index 4003eb3..41dc687 100644 --- a/apps/RenderCadenceCompositor/control/RuntimeStateJson.h +++ b/apps/RenderCadenceCompositor/control/RuntimeStateJson.h @@ -93,6 +93,31 @@ inline void WriteDefaultParameterValue(JsonWriter& writer, const ShaderParameter writer.Null(); } +inline void WriteParameterValue(JsonWriter& writer, const ShaderParameterDefinition& parameter, const ShaderParameterValue& value) +{ + switch (parameter.type) + { + case ShaderParameterType::Boolean: + writer.Bool(value.booleanValue); + return; + case ShaderParameterType::Enum: + writer.String(value.enumValue); + return; + case ShaderParameterType::Text: + writer.String(value.textValue); + return; + case ShaderParameterType::Trigger: + case ShaderParameterType::Float: + writer.Double(value.numberValues.empty() ? 0.0 : value.numberValues.front()); + return; + case ShaderParameterType::Vec2: + case ShaderParameterType::Color: + WriteNumberArray(writer, value.numberValues); + return; + } + writer.Null(); +} + inline void WriteTemporalJson(JsonWriter& writer, const TemporalSettings& temporal) { writer.BeginObject(); @@ -122,7 +147,7 @@ inline const char* RuntimeLayerBuildStateName(RuntimeLayerBuildState state) return "unknown"; } -inline void WriteParameterDefinitionJson(JsonWriter& writer, const ShaderParameterDefinition& parameter) +inline void WriteParameterDefinitionJson(JsonWriter& writer, const ShaderParameterDefinition& parameter, const ShaderParameterValue* value) { writer.BeginObject(); writer.KeyString("id", parameter.id); @@ -132,7 +157,10 @@ inline void WriteParameterDefinitionJson(JsonWriter& writer, const ShaderParamet writer.Key("defaultValue"); WriteDefaultParameterValue(writer, parameter); writer.Key("value"); - WriteDefaultParameterValue(writer, parameter); + if (value) + WriteParameterValue(writer, parameter, *value); + else + WriteDefaultParameterValue(writer, parameter); if (!parameter.minNumbers.empty()) { @@ -197,10 +225,10 @@ inline void WriteLayersJson(JsonWriter& writer, const RuntimeStateJsonInput& inp WriteFeedbackJson(writer, FeedbackSettings()); writer.Key("parameters"); writer.BeginArray(); - if (shaderPackage) + for (const ShaderParameterDefinition& parameter : layer.parameterDefinitions) { - for (const ShaderParameterDefinition& parameter : shaderPackage->parameters) - WriteParameterDefinitionJson(writer, parameter); + const auto valueIt = layer.parameterValues.find(parameter.id); + WriteParameterDefinitionJson(writer, parameter, valueIt == layer.parameterValues.end() ? nullptr : &valueIt->second); } writer.EndArray(); writer.EndObject(); diff --git a/apps/RenderCadenceCompositor/control/http/HttpControlServer.cpp b/apps/RenderCadenceCompositor/control/http/HttpControlServer.cpp index 4ffc962..d85dd58 100644 --- a/apps/RenderCadenceCompositor/control/http/HttpControlServer.cpp +++ b/apps/RenderCadenceCompositor/control/http/HttpControlServer.cpp @@ -286,6 +286,12 @@ HttpControlServer::HttpResponse HttpControlServer::ServePost(const HttpRequest& if (!IsKnownPostEndpoint(request.path)) return TextResponse("404 Not Found", "Not Found"); + if (mCallbacks.executePost) + { + const ControlActionResult result = mCallbacks.executePost(request.path, request.body); + return JsonResponse(result.ok ? "200 OK" : "400 Bad Request", ActionResponse(result.ok, result.error)); + } + if (request.path == "/api/layers/add" && mCallbacks.addLayer) { const ControlActionResult result = mCallbacks.addLayer(request.body); diff --git a/apps/RenderCadenceCompositor/control/http/HttpControlServer.h b/apps/RenderCadenceCompositor/control/http/HttpControlServer.h index 8b811eb..519f3c8 100644 --- a/apps/RenderCadenceCompositor/control/http/HttpControlServer.h +++ b/apps/RenderCadenceCompositor/control/http/HttpControlServer.h @@ -28,6 +28,7 @@ struct HttpControlServerCallbacks std::function getStateJson; std::function addLayer; std::function removeLayer; + std::function executePost; }; class UniqueSocket diff --git a/apps/RenderCadenceCompositor/render/runtime/RuntimeRenderScene.cpp b/apps/RenderCadenceCompositor/render/runtime/RuntimeRenderScene.cpp index 562c248..663848f 100644 --- a/apps/RenderCadenceCompositor/render/runtime/RuntimeRenderScene.cpp +++ b/apps/RenderCadenceCompositor/render/runtime/RuntimeRenderScene.cpp @@ -28,7 +28,10 @@ bool RuntimeRenderScene::CommitRenderLayers(const std::vector layersToPrepare; nextOrder.reserve(layers.size()); for (const RenderCadenceCompositor::RuntimeRenderLayerModel& layer : layers) - nextOrder.push_back(layer.id); + { + if (!layer.bypass) + nextOrder.push_back(layer.id); + } for (auto layerIt = mLayers.begin(); layerIt != mLayers.end();) { @@ -50,6 +53,8 @@ bool RuntimeRenderScene::CommitRenderLayers(const std::vectorshaderId == layer.shaderId && program->sourceFingerprint == fingerprint && hasReadyPass) + { + for (LayerProgram::PassProgram& pass : program->passes) + { + if (pass.renderer) + pass.renderer->UpdateArtifactState(layer.artifact); + } continue; + } if (program->pendingFingerprint == fingerprint) continue; diff --git a/apps/RenderCadenceCompositor/render/runtime/RuntimeShaderParams.cpp b/apps/RenderCadenceCompositor/render/runtime/RuntimeShaderParams.cpp index 184c58e..dccbf46 100644 --- a/apps/RenderCadenceCompositor/render/runtime/RuntimeShaderParams.cpp +++ b/apps/RenderCadenceCompositor/render/runtime/RuntimeShaderParams.cpp @@ -82,7 +82,10 @@ std::vector BuildRuntimeShaderGlobalParamsStd140( for (const ShaderParameterDefinition& definition : artifact.parameterDefinitions) { - const ShaderParameterValue value = DefaultValueForDefinition(definition); + const auto valueIt = artifact.parameterValues.find(definition.id); + const ShaderParameterValue value = valueIt == artifact.parameterValues.end() + ? DefaultValueForDefinition(definition) + : valueIt->second; switch (definition.type) { case ShaderParameterType::Float: diff --git a/apps/RenderCadenceCompositor/render/runtime/RuntimeShaderRenderer.cpp b/apps/RenderCadenceCompositor/render/runtime/RuntimeShaderRenderer.cpp index f586847..30fa9df 100644 --- a/apps/RenderCadenceCompositor/render/runtime/RuntimeShaderRenderer.cpp +++ b/apps/RenderCadenceCompositor/render/runtime/RuntimeShaderRenderer.cpp @@ -94,6 +94,13 @@ bool RuntimeShaderRenderer::CommitPreparedProgram(RuntimePreparedShaderProgram& return true; } +void RuntimeShaderRenderer::UpdateArtifactState(const RuntimeShaderArtifact& artifact) +{ + mArtifact.parameterDefinitions = artifact.parameterDefinitions; + mArtifact.parameterValues = artifact.parameterValues; + mArtifact.message = artifact.message; +} + bool RuntimeShaderRenderer::BuildPreparedProgram( const std::string& layerId, const std::string& sourceFingerprint, diff --git a/apps/RenderCadenceCompositor/render/runtime/RuntimeShaderRenderer.h b/apps/RenderCadenceCompositor/render/runtime/RuntimeShaderRenderer.h index e25547c..789672d 100644 --- a/apps/RenderCadenceCompositor/render/runtime/RuntimeShaderRenderer.h +++ b/apps/RenderCadenceCompositor/render/runtime/RuntimeShaderRenderer.h @@ -20,6 +20,7 @@ public: bool CommitShaderArtifact(const RuntimeShaderArtifact& artifact, std::string& error); bool CommitPreparedProgram(RuntimePreparedShaderProgram& preparedProgram, std::string& error); bool HasProgram() const { return mProgram != 0; } + void UpdateArtifactState(const RuntimeShaderArtifact& artifact); void RenderFrame(uint64_t frameIndex, unsigned width, unsigned height, GLuint sourceTexture = 0, GLuint layerInputTexture = 0); void ShutdownGl(); diff --git a/apps/RenderCadenceCompositor/runtime/RuntimeLayerModel.cpp b/apps/RenderCadenceCompositor/runtime/RuntimeLayerModel.cpp index 2d1edfb..4684323 100644 --- a/apps/RenderCadenceCompositor/runtime/RuntimeLayerModel.cpp +++ b/apps/RenderCadenceCompositor/runtime/RuntimeLayerModel.cpp @@ -1,5 +1,8 @@ #include "RuntimeLayerModel.h" +#include "RuntimeParameterUtils.h" + +#include #include namespace RenderCadenceCompositor @@ -26,6 +29,7 @@ bool RuntimeLayerModel::InitializeSingleLayer(const SupportedShaderCatalog& shad layer.shaderName = shaderPackage->displayName.empty() ? shaderPackage->id : shaderPackage->displayName; layer.buildState = RuntimeLayerBuildState::Pending; layer.message = "Runtime Slang build is waiting to start."; + InitializeDefaultParameterValues(layer, *shaderPackage); mLayers.push_back(std::move(layer)); error.clear(); return true; @@ -46,6 +50,7 @@ bool RuntimeLayerModel::AddLayer(const SupportedShaderCatalog& shaderCatalog, co layer.shaderName = shaderPackage->displayName.empty() ? shaderPackage->id : shaderPackage->displayName; layer.buildState = RuntimeLayerBuildState::Pending; layer.message = "Runtime Slang build is waiting to start."; + InitializeDefaultParameterValues(layer, *shaderPackage); layerId = layer.id; mLayers.push_back(std::move(layer)); error.clear(); @@ -68,6 +73,117 @@ bool RuntimeLayerModel::RemoveLayer(const std::string& layerId, std::string& err return false; } +bool RuntimeLayerModel::ReorderLayer(const std::string& layerId, int targetIndex, std::string& error) +{ + auto layerIt = std::find_if(mLayers.begin(), mLayers.end(), [&layerId](const Layer& layer) { + return layer.id == layerId; + }); + if (layerIt == mLayers.end()) + { + error = "Unknown runtime layer id: " + layerId; + return false; + } + + if (targetIndex < 0) + targetIndex = 0; + if (targetIndex >= static_cast(mLayers.size())) + targetIndex = static_cast(mLayers.size()) - 1; + + Layer layer = std::move(*layerIt); + mLayers.erase(layerIt); + std::size_t destinationIndex = static_cast(targetIndex); + if (destinationIndex > mLayers.size()) + destinationIndex = mLayers.size(); + mLayers.insert(mLayers.begin() + static_cast(destinationIndex), std::move(layer)); + error.clear(); + return true; +} + +bool RuntimeLayerModel::SetLayerBypass(const std::string& layerId, bool bypass, std::string& error) +{ + Layer* layer = FindLayer(layerId); + if (!layer) + { + error = "Unknown runtime layer id: " + layerId; + return false; + } + layer->bypass = bypass; + error.clear(); + return true; +} + +bool RuntimeLayerModel::SetLayerShader(const SupportedShaderCatalog& shaderCatalog, const std::string& layerId, const std::string& shaderId, std::string& error) +{ + Layer* layer = FindLayer(layerId); + if (!layer) + { + error = "Unknown runtime layer id: " + layerId; + return false; + } + + const ShaderPackage* shaderPackage = shaderCatalog.FindPackage(shaderId); + if (!shaderPackage) + { + error = "Shader '" + shaderId + "' is not in the supported shader catalog."; + return false; + } + + 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."; + layer->renderReady = false; + layer->artifact = RuntimeShaderArtifact(); + InitializeDefaultParameterValues(*layer, *shaderPackage); + error.clear(); + return true; +} + +bool RuntimeLayerModel::UpdateParameter(const std::string& layerId, const std::string& parameterId, const JsonValue& value, std::string& error) +{ + Layer* layer = FindLayer(layerId); + if (!layer) + { + error = "Unknown runtime layer id: " + layerId; + return false; + } + + const ShaderParameterDefinition* definition = FindParameterDefinition(*layer, parameterId); + if (!definition) + { + error = "Unknown parameter id '" + parameterId + "' for layer " + layerId + "."; + return false; + } + + ShaderParameterValue normalizedValue; + if (!NormalizeAndValidateParameterValue(*definition, value, normalizedValue, error)) + return false; + + layer->parameterValues[parameterId] = normalizedValue; + if (layer->renderReady) + layer->artifact.parameterValues = layer->parameterValues; + error.clear(); + return true; +} + +bool RuntimeLayerModel::ResetParameters(const std::string& layerId, std::string& error) +{ + Layer* layer = FindLayer(layerId); + if (!layer) + { + error = "Unknown runtime layer id: " + layerId; + return false; + } + + layer->parameterValues.clear(); + for (const ShaderParameterDefinition& definition : layer->parameterDefinitions) + layer->parameterValues[definition.id] = DefaultValueForDefinition(definition); + if (layer->renderReady) + layer->artifact.parameterValues = layer->parameterValues; + error.clear(); + return true; +} + void RuntimeLayerModel::Clear() { mLayers.clear(); @@ -106,6 +222,7 @@ bool RuntimeLayerModel::MarkBuildReady(const RuntimeShaderArtifact& artifact, st layer->message = artifact.message; layer->renderReady = true; layer->artifact = artifact; + layer->artifact.parameterValues = layer->parameterValues; error.clear(); return true; } @@ -159,7 +276,9 @@ RuntimeLayerModelSnapshot RuntimeLayerModel::Snapshot() const RuntimeRenderLayerModel renderLayer; renderLayer.id = layer.id; renderLayer.shaderId = layer.shaderId; + renderLayer.bypass = layer.bypass; renderLayer.artifact = layer.artifact; + renderLayer.artifact.parameterValues = layer.parameterValues; snapshot.renderLayers.push_back(std::move(renderLayer)); } } @@ -204,6 +323,24 @@ RuntimeLayerModel::Layer* RuntimeLayerModel::FindFirstLayerForShader(const std:: return nullptr; } +void RuntimeLayerModel::InitializeDefaultParameterValues(Layer& layer, const ShaderPackage& shaderPackage) +{ + layer.parameterDefinitions = shaderPackage.parameters; + layer.parameterValues.clear(); + for (const ShaderParameterDefinition& definition : layer.parameterDefinitions) + layer.parameterValues[definition.id] = DefaultValueForDefinition(definition); +} + +const ShaderParameterDefinition* RuntimeLayerModel::FindParameterDefinition(const Layer& layer, const std::string& parameterId) +{ + for (const ShaderParameterDefinition& definition : layer.parameterDefinitions) + { + if (definition.id == parameterId) + return &definition; + } + return nullptr; +} + std::string RuntimeLayerModel::AllocateLayerId() { return "runtime-layer-" + std::to_string(mNextLayerNumber++); @@ -219,6 +356,8 @@ RuntimeLayerReadModel RuntimeLayerModel::ToReadModel(const Layer& layer) readModel.buildState = layer.buildState; readModel.message = layer.message; readModel.renderReady = layer.renderReady; + readModel.parameterDefinitions = layer.parameterDefinitions; + readModel.parameterValues = layer.parameterValues; return readModel; } } diff --git a/apps/RenderCadenceCompositor/runtime/RuntimeLayerModel.h b/apps/RenderCadenceCompositor/runtime/RuntimeLayerModel.h index 1e5d478..35dcca3 100644 --- a/apps/RenderCadenceCompositor/runtime/RuntimeLayerModel.h +++ b/apps/RenderCadenceCompositor/runtime/RuntimeLayerModel.h @@ -1,9 +1,11 @@ #pragma once +#include "RuntimeJson.h" #include "RuntimeShaderArtifact.h" #include "SupportedShaderCatalog.h" #include +#include #include #include @@ -25,12 +27,15 @@ struct RuntimeLayerReadModel RuntimeLayerBuildState buildState = RuntimeLayerBuildState::Pending; std::string message; bool renderReady = false; + std::vector parameterDefinitions; + std::map parameterValues; }; struct RuntimeRenderLayerModel { std::string id; std::string shaderId; + bool bypass = false; RuntimeShaderArtifact artifact; }; @@ -50,6 +55,11 @@ public: 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 ReorderLayer(const std::string& layerId, int targetIndex, std::string& error); + bool SetLayerBypass(const std::string& layerId, bool bypass, std::string& error); + bool SetLayerShader(const SupportedShaderCatalog& shaderCatalog, const std::string& layerId, const std::string& shaderId, std::string& error); + bool UpdateParameter(const std::string& layerId, const std::string& parameterId, const JsonValue& value, std::string& error); + bool ResetParameters(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); @@ -69,12 +79,16 @@ private: RuntimeLayerBuildState buildState = RuntimeLayerBuildState::Pending; std::string message; bool renderReady = false; + std::vector parameterDefinitions; + std::map parameterValues; RuntimeShaderArtifact artifact; }; Layer* FindLayer(const std::string& layerId); const Layer* FindLayer(const std::string& layerId) const; Layer* FindFirstLayerForShader(const std::string& shaderId); + static void InitializeDefaultParameterValues(Layer& layer, const ShaderPackage& shaderPackage); + static const ShaderParameterDefinition* FindParameterDefinition(const Layer& layer, const std::string& parameterId); std::string AllocateLayerId(); static RuntimeLayerReadModel ToReadModel(const Layer& layer); diff --git a/apps/RenderCadenceCompositor/runtime/RuntimeShaderArtifact.h b/apps/RenderCadenceCompositor/runtime/RuntimeShaderArtifact.h index 160fb75..3c02a9d 100644 --- a/apps/RenderCadenceCompositor/runtime/RuntimeShaderArtifact.h +++ b/apps/RenderCadenceCompositor/runtime/RuntimeShaderArtifact.h @@ -2,6 +2,7 @@ #include "ShaderTypes.h" +#include #include #include @@ -22,4 +23,5 @@ struct RuntimeShaderArtifact std::vector passes; std::string message; std::vector parameterDefinitions; + std::map parameterValues; }; diff --git a/tests/RenderCadenceCompositorHttpControlServerTests.cpp b/tests/RenderCadenceCompositorHttpControlServerTests.cpp index 44e936d..7c6af72 100644 --- a/tests/RenderCadenceCompositorHttpControlServerTests.cpp +++ b/tests/RenderCadenceCompositorHttpControlServerTests.cpp @@ -147,6 +147,29 @@ void TestLayerPostEndpointsUseCallbacks() Expect(removeResponse.body.find("Unknown layer id.") != std::string::npos, "remove layer callback returns diagnostic"); } +void TestGenericPostCallbackHandlesControlRoutes() +{ + using namespace RenderCadenceCompositor; + + HttpControlServer server; + HttpControlServerCallbacks callbacks; + callbacks.executePost = [](const std::string& path, const std::string& body) { + ExpectEquals(path, "/api/layers/set-bypass", "generic callback receives route path"); + Expect(body.find("runtime-layer-1") != std::string::npos, "generic callback receives request body"); + return ControlActionResult{ true, std::string() }; + }; + server.SetCallbacksForTest(callbacks); + + HttpControlServer::HttpRequest request; + request.method = "POST"; + request.path = "/api/layers/set-bypass"; + request.body = "{\"layerId\":\"runtime-layer-1\",\"bypass\":true}"; + + const HttpControlServer::HttpResponse response = server.RouteRequestForTest(request); + ExpectEquals(response.status, "200 OK", "generic control callback success returns 200"); + Expect(response.body.find("\"ok\":true") != std::string::npos, "generic control callback returns action success"); +} + void TestUnknownEndpointReturns404() { using namespace RenderCadenceCompositor; @@ -169,6 +192,7 @@ int main() TestRootServesUiIndex(); TestKnownPostEndpointReturnsActionError(); TestLayerPostEndpointsUseCallbacks(); + TestGenericPostCallbackHandlesControlRoutes(); TestUnknownEndpointReturns404(); if (gFailures != 0) diff --git a/tests/RenderCadenceCompositorRuntimeLayerModelTests.cpp b/tests/RenderCadenceCompositorRuntimeLayerModelTests.cpp index 01a44bd..cdec60e 100644 --- a/tests/RenderCadenceCompositorRuntimeLayerModelTests.cpp +++ b/tests/RenderCadenceCompositorRuntimeLayerModelTests.cpp @@ -143,6 +143,49 @@ void TestAddAndRemoveLayers() std::filesystem::remove_all(root); } + +void TestLayerControlsUpdateDisplayAndRenderModels() +{ + 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 control layer can be added"); + Expect(model.AddLayer(catalog, "solid", secondLayerId, error), "second control layer can be added"); + + Expect(model.SetLayerBypass(firstLayerId, true, error), "bypass can be set"); + Expect(model.ReorderLayer(firstLayerId, 1, error), "layer can be reordered"); + RenderCadenceCompositor::RuntimeLayerModelSnapshot snapshot = model.Snapshot(); + Expect(snapshot.displayLayers[1].id == firstLayerId, "reordered layer moves to requested index"); + Expect(snapshot.displayLayers[1].bypass, "bypass state is visible in read model"); + + JsonValue gainValue(0.75); + Expect(model.UpdateParameter(firstLayerId, "gain", gainValue, error), "parameter value can be updated"); + snapshot = model.Snapshot(); + Expect(snapshot.displayLayers[1].parameterValues.at("gain").numberValues.front() == 0.75, "updated parameter value is visible"); + + RuntimeShaderArtifact artifact; + artifact.layerId = firstLayerId; + artifact.shaderId = "solid"; + artifact.displayName = "Solid"; + artifact.fragmentShaderSource = "void main(){}"; + artifact.parameterDefinitions = snapshot.displayLayers[1].parameterDefinitions; + artifact.message = "build ready"; + Expect(model.MarkBuildReady(artifact, error), "ready artifact keeps layer parameter state"); + snapshot = model.Snapshot(); + Expect(snapshot.renderLayers.size() == 1, "ready layer produces render model"); + Expect(snapshot.renderLayers[0].bypass, "render model carries bypass state"); + Expect(snapshot.renderLayers[0].artifact.parameterValues.at("gain").numberValues.front() == 0.75, "render artifact carries updated parameter value"); + + Expect(model.ResetParameters(firstLayerId, error), "parameters can reset to defaults"); + snapshot = model.Snapshot(); + Expect(snapshot.displayLayers[1].parameterValues.at("gain").numberValues.front() == 0.5, "reset restores default value"); + + std::filesystem::remove_all(root); +} } int main() @@ -151,6 +194,7 @@ int main() TestRejectsUnsupportedStartupShader(); TestBuildFailureStaysDisplaySide(); TestAddAndRemoveLayers(); + TestLayerControlsUpdateDisplayAndRenderModels(); if (gFailures != 0) {