More http post end points filled
All checks were successful
CI / React UI Build (push) Successful in 10s
CI / Native Windows Build And Tests (push) Successful in 3m1s
CI / Windows Release Package (push) Has been skipped

This commit is contained in:
Aiden
2026-05-12 14:23:53 +10:00
parent 38d729b346
commit 1ddcf5d621
15 changed files with 854 additions and 97 deletions

View File

@@ -333,6 +333,8 @@ set(RENDER_CADENCE_APP_SOURCES
"${RENDER_CADENCE_APP_DIR}/render/RuntimeShaderParams.h" "${RENDER_CADENCE_APP_DIR}/render/RuntimeShaderParams.h"
"${RENDER_CADENCE_APP_DIR}/render/SimpleMotionRenderer.cpp" "${RENDER_CADENCE_APP_DIR}/render/SimpleMotionRenderer.cpp"
"${RENDER_CADENCE_APP_DIR}/render/SimpleMotionRenderer.h" "${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.cpp"
"${RENDER_CADENCE_APP_DIR}/runtime/RuntimeShaderBridge.h" "${RENDER_CADENCE_APP_DIR}/runtime/RuntimeShaderBridge.h"
"${RENDER_CADENCE_APP_DIR}/runtime/RuntimeSlangShaderCompiler.cpp" "${RENDER_CADENCE_APP_DIR}/runtime/RuntimeSlangShaderCompiler.cpp"
@@ -821,6 +823,27 @@ endif()
add_test(NAME RenderCadenceCompositorRuntimeShaderParamsTests COMMAND RenderCadenceCompositorRuntimeShaderParamsTests) 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 add_executable(RenderCadenceCompositorSupportedShaderCatalogTests
"${APP_DIR}/shader/ShaderPackageRegistry.cpp" "${APP_DIR}/shader/ShaderPackageRegistry.cpp"
"${APP_DIR}/runtime/support/RuntimeJson.cpp" "${APP_DIR}/runtime/support/RuntimeJson.cpp"
@@ -873,9 +896,12 @@ add_test(NAME RenderCadenceCompositorJsonWriterTests COMMAND RenderCadenceCompos
add_executable(RenderCadenceCompositorRuntimeStateJsonTests add_executable(RenderCadenceCompositorRuntimeStateJsonTests
"${APP_DIR}/runtime/support/RuntimeJson.cpp" "${APP_DIR}/runtime/support/RuntimeJson.cpp"
"${APP_DIR}/shader/ShaderPackageRegistry.cpp"
"${RENDER_CADENCE_APP_DIR}/app/AppConfig.cpp" "${RENDER_CADENCE_APP_DIR}/app/AppConfig.cpp"
"${RENDER_CADENCE_APP_DIR}/app/AppConfigProvider.cpp" "${RENDER_CADENCE_APP_DIR}/app/AppConfigProvider.cpp"
"${RENDER_CADENCE_APP_DIR}/json/JsonWriter.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" "${CMAKE_CURRENT_SOURCE_DIR}/tests/RenderCadenceCompositorRuntimeStateJsonTests.cpp"
) )

View File

@@ -41,6 +41,7 @@ Included now:
- latest-N system-memory frame exchange - latest-N system-memory frame exchange
- rendered-frame warmup - rendered-frame warmup
- background Slang compile of `shaders/happy-accident` - 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 - app-owned submission of a completed shader artifact
- render-thread-only GL commit once the artifact is ready - render-thread-only GL commit once the artifact is ready
- manifest-driven stateless single-pass shader packages - manifest-driven stateless single-pass shader packages
@@ -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 /api/state`: returns OpenAPI-shaped display data with cadence telemetry, supported shaders, output status, and a read-only current runtime layer
- `GET /docs/openapi.yaml` and `GET /openapi.yaml`: serves the OpenAPI document - `GET /docs/openapi.yaml` and `GET /openapi.yaml`: serves the OpenAPI document
- `GET /docs`: serves Swagger UI - `GET /docs`: serves Swagger UI
- OpenAPI POST routes are present but return `{ "ok": false, "error": "Endpoint is not implemented in RenderCadenceCompositor yet." }` - `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. 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 `/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: 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 - `frames/`: system-memory handoff
- `platform/`: COM/Win32/hidden GL context support - `platform/`: COM/Win32/hidden GL context support
- `render/`: cadence, simple rendering, PBO readback - `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 - `control/`: local HTTP API edge and runtime-state JSON presentation
- `json/`: compact JSON serialization helpers - `json/`: compact JSON serialization helpers
- `video/`: DeckLink output wrapper and scheduling thread - `video/`: DeckLink output wrapper and scheduling thread

View File

@@ -3,15 +3,19 @@
#include "AppConfig.h" #include "AppConfig.h"
#include "AppConfigProvider.h" #include "AppConfigProvider.h"
#include "../logging/Logger.h" #include "../logging/Logger.h"
#include "../runtime/RuntimeLayerModel.h"
#include "../runtime/RuntimeShaderBridge.h" #include "../runtime/RuntimeShaderBridge.h"
#include "../runtime/SupportedShaderCatalog.h" #include "../runtime/SupportedShaderCatalog.h"
#include "../control/RuntimeStateJson.h" #include "../control/RuntimeStateJson.h"
#include "../telemetry/TelemetryHealthMonitor.h" #include "../telemetry/TelemetryHealthMonitor.h"
#include "../video/DeckLinkOutput.h" #include "../video/DeckLinkOutput.h"
#include "../video/DeckLinkOutputThread.h" #include "../video/DeckLinkOutputThread.h"
#include "RuntimeJson.h"
#include <chrono> #include <chrono>
#include <filesystem> #include <filesystem>
#include <map>
#include <memory>
#include <mutex> #include <mutex>
#include <string> #include <string>
#include <thread> #include <thread>
@@ -71,6 +75,7 @@ public:
bool Start(std::string& error) bool Start(std::string& error)
{ {
LoadSupportedShaderCatalog(); LoadSupportedShaderCatalog();
InitializeRuntimeLayerModel();
Log("app", "Starting render thread."); Log("app", "Starting render thread.");
if (!detail::StartRenderThread(mRenderThread, error, 0)) if (!detail::StartRenderThread(mRenderThread, error, 0))
@@ -174,6 +179,12 @@ private:
callbacks.getStateJson = [this]() { callbacks.getStateJson = [this]() {
return BuildStateJson(); 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; std::string error;
if (!mHttpServer.Start( if (!mHttpServer.Start(
@@ -191,17 +202,15 @@ private:
std::string BuildStateJson() std::string BuildStateJson()
{ {
CadenceTelemetrySnapshot telemetry = mHttpTelemetry.Sample(mFrameExchange, mOutput, mOutputThread, mRenderThread); CadenceTelemetrySnapshot telemetry = mHttpTelemetry.Sample(mFrameExchange, mOutput, mOutputThread, mRenderThread);
RuntimeDisplayState runtimeState = CopyRuntimeDisplayState(telemetry); RuntimeLayerModelSnapshot layerSnapshot = CopyRuntimeLayerSnapshot(telemetry);
return RuntimeStateToJson(RuntimeStateJsonInput{ return RuntimeStateToJson(RuntimeStateJsonInput{
mConfig, mConfig,
telemetry, telemetry,
mHttpServer.Port(), mHttpServer.Port(),
mVideoOutputEnabled, mVideoOutputEnabled,
mVideoOutputStatus, mVideoOutputStatus,
mShaderCatalog.Shaders(), mShaderCatalog,
runtimeState.compileSucceeded, layerSnapshot
runtimeState.compileMessage,
runtimeState.activeShaderPackage
}); });
} }
@@ -226,17 +235,9 @@ private:
} }
Log("runtime-shader", "Starting background Slang build for shader '" + mConfig.runtimeShaderId + "'."); Log("runtime-shader", "Starting background Slang build for shader '" + mConfig.runtimeShaderId + "'.");
SetRuntimeDisplayState(true, "Runtime Slang build started for shader '" + mConfig.runtimeShaderId + "'.", mConfig.runtimeShaderId); const std::string layerId = FirstRuntimeLayerId();
mShaderBridge.Start( if (!layerId.empty())
mConfig.runtimeShaderId, StartLayerShaderBuild(layerId, mConfig.runtimeShaderId, true);
[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);
});
} }
void LoadSupportedShaderCatalog() void LoadSupportedShaderCatalog()
@@ -252,41 +253,185 @@ private:
Log("runtime-shader", "Supported shader catalog loaded with " + std::to_string(mShaderCatalog.Shaders().size()) + " shader(s)."); Log("runtime-shader", "Supported shader catalog loaded with " + std::to_string(mShaderCatalog.Shaders().size()) + " shader(s).");
} }
void InitializeRuntimeLayerModel()
{
std::lock_guard<std::mutex> 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() void StopRuntimeShaderBuild()
{ {
mShaderBridge.Stop(); StopAllRuntimeShaderBuilds();
} }
struct RuntimeDisplayState void MarkRuntimeBuildStarted(const std::string& message)
{ {
bool compileSucceeded = true; std::lock_guard<std::mutex> lock(mRuntimeLayerMutex);
std::string compileMessage; std::string error;
const ShaderPackage* activeShaderPackage = nullptr; const std::string layerId = mRuntimeLayerModel.FirstLayerId();
}; if (!layerId.empty())
mRuntimeLayerModel.MarkBuildStarted(layerId, message, error);
void SetRuntimeDisplayState(bool compileSucceeded, const std::string& compileMessage, const std::string& activeShaderId = std::string())
{
std::lock_guard<std::mutex> lock(mRuntimeDisplayMutex);
mRuntimeCompileSucceeded = compileSucceeded;
mRuntimeCompileMessage = compileMessage;
if (!activeShaderId.empty())
mActiveShaderId = activeShaderId;
} }
RuntimeDisplayState CopyRuntimeDisplayState(const CadenceTelemetrySnapshot& telemetry) const void MarkRuntimeBuildReady(const RuntimeShaderArtifact& artifact)
{ {
std::lock_guard<std::mutex> lock(mRuntimeDisplayMutex); std::lock_guard<std::mutex> lock(mRuntimeLayerMutex);
RuntimeDisplayState state; std::string error;
state.compileSucceeded = mRuntimeCompileSucceeded && telemetry.shaderBuildFailures == 0; if (!mRuntimeLayerModel.MarkBuildReady(artifact, error))
state.compileMessage = mRuntimeCompileMessage; LogWarning("runtime-shader", error);
}
void MarkRuntimeBuildFailed(const std::string& message)
{
std::lock_guard<std::mutex> 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<std::mutex> lock(mRuntimeLayerMutex);
std::string error;
if (!mRuntimeLayerModel.MarkBuildFailed(layerId, message, error))
LogWarning("runtime-shader", error);
}
std::string FirstRuntimeLayerId() const
{
std::lock_guard<std::mutex> lock(mRuntimeLayerMutex);
return mRuntimeLayerModel.FirstLayerId();
}
void StartLayerShaderBuild(const std::string& layerId, const std::string& shaderId, bool submitToRender)
{
{
std::lock_guard<std::mutex> lock(mRuntimeLayerMutex);
std::string error;
mRuntimeLayerModel.MarkBuildStarted(layerId, "Runtime Slang build started for shader '" + shaderId + "'.", error);
}
auto bridge = std::make_unique<RuntimeShaderBridge>();
RuntimeShaderBridge* bridgePtr = bridge.get();
{
std::lock_guard<std::mutex> 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<RuntimeShaderBridge> bridge;
{
std::lock_guard<std::mutex> 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<std::string, std::unique_ptr<RuntimeShaderBridge>> builds;
{
std::lock_guard<std::mutex> 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<std::mutex> 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<std::mutex> 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<std::mutex> lock(mRuntimeLayerMutex);
RuntimeLayerModelSnapshot snapshot = mRuntimeLayerModel.Snapshot();
if (telemetry.shaderBuildFailures > 0) if (telemetry.shaderBuildFailures > 0)
state.compileMessage = "Runtime shader GL commit failed; see logs for details."; {
if (state.compileMessage.empty()) snapshot.compileSucceeded = false;
state.compileMessage = mConfig.runtimeShaderId.empty() snapshot.compileMessage = "Runtime shader GL commit failed; see logs for details.";
? "Runtime shader build disabled." }
: "Runtime shader build has not completed yet."; return snapshot;
state.activeShaderPackage = mShaderCatalog.FindPackage(mActiveShaderId);
return state;
} }
RenderThread& mRenderThread; RenderThread& mRenderThread;
@@ -297,12 +442,11 @@ private:
TelemetryHealthMonitor mTelemetryHealth; TelemetryHealthMonitor mTelemetryHealth;
CadenceTelemetry mHttpTelemetry; CadenceTelemetry mHttpTelemetry;
HttpControlServer mHttpServer; HttpControlServer mHttpServer;
RuntimeShaderBridge mShaderBridge;
SupportedShaderCatalog mShaderCatalog; SupportedShaderCatalog mShaderCatalog;
mutable std::mutex mRuntimeDisplayMutex; mutable std::mutex mRuntimeLayerMutex;
bool mRuntimeCompileSucceeded = true; RuntimeLayerModel mRuntimeLayerModel;
std::string mRuntimeCompileMessage; std::mutex mShaderBuildMutex;
std::string mActiveShaderId; std::map<std::string, std::unique_ptr<RuntimeShaderBridge>> mShaderBuilds;
bool mStarted = false; bool mStarted = false;
bool mVideoOutputEnabled = false; bool mVideoOutputEnabled = false;
std::string mVideoOutputStatus = "DeckLink output not started."; std::string mVideoOutputStatus = "DeckLink output not started.";

View File

@@ -266,6 +266,18 @@ HttpControlServer::HttpResponse HttpControlServer::ServePost(const HttpRequest&
if (!IsKnownPostEndpoint(request.path)) if (!IsKnownPostEndpoint(request.path))
return TextResponse("404 Not Found", "Not Found"); 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 { return {
"400 Bad Request", "400 Bad Request",
"application/json", "application/json",

View File

@@ -19,9 +19,17 @@ struct HttpControlServerConfig
std::chrono::milliseconds idleSleep = std::chrono::milliseconds(10); std::chrono::milliseconds idleSleep = std::chrono::milliseconds(10);
}; };
struct ControlActionResult
{
bool ok = false;
std::string error;
};
struct HttpControlServerCallbacks struct HttpControlServerCallbacks
{ {
std::function<std::string()> getStateJson; std::function<std::string()> getStateJson;
std::function<ControlActionResult(const std::string&)> addLayer;
std::function<ControlActionResult(const std::string&)> removeLayer;
}; };
class UniqueSocket class UniqueSocket

View File

@@ -3,6 +3,7 @@
#include "../app/AppConfig.h" #include "../app/AppConfig.h"
#include "../app/AppConfigProvider.h" #include "../app/AppConfigProvider.h"
#include "../json/JsonWriter.h" #include "../json/JsonWriter.h"
#include "../runtime/RuntimeLayerModel.h"
#include "../runtime/SupportedShaderCatalog.h" #include "../runtime/SupportedShaderCatalog.h"
#include "../telemetry/CadenceTelemetryJson.h" #include "../telemetry/CadenceTelemetryJson.h"
@@ -19,10 +20,8 @@ struct RuntimeStateJsonInput
unsigned short serverPort = 0; unsigned short serverPort = 0;
bool videoOutputEnabled = false; bool videoOutputEnabled = false;
std::string videoOutputStatus; std::string videoOutputStatus;
const std::vector<SupportedShaderSummary>& shaders; const SupportedShaderCatalog& shaderCatalog;
bool runtimeCompileSucceeded = true; const RuntimeLayerModelSnapshot& runtimeLayers;
std::string runtimeCompileMessage;
const ShaderPackage* activeShaderPackage = nullptr;
}; };
inline void WriteVideoIoStatusJson(JsonWriter& writer, const RuntimeStateJsonInput& input) inline void WriteVideoIoStatusJson(JsonWriter& writer, const RuntimeStateJsonInput& input)
@@ -112,6 +111,17 @@ inline void WriteFeedbackJson(JsonWriter& writer, const FeedbackSettings& feedba
writer.EndObject(); 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) inline void WriteParameterDefinitionJson(JsonWriter& writer, const ShaderParameterDefinition& parameter)
{ {
writer.BeginObject(); writer.BeginObject();
@@ -164,22 +174,34 @@ inline void WriteParameterDefinitionJson(JsonWriter& writer, const ShaderParamet
inline void WriteLayersJson(JsonWriter& writer, const RuntimeStateJsonInput& input) inline void WriteLayersJson(JsonWriter& writer, const RuntimeStateJsonInput& input)
{ {
writer.BeginArray(); 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.BeginObject();
writer.KeyString("id", "runtime-layer-1"); writer.KeyString("id", layer.id);
writer.KeyString("shaderId", shaderPackage.id); writer.KeyString("shaderId", layer.shaderId);
writer.KeyString("shaderName", shaderPackage.displayName.empty() ? shaderPackage.id : shaderPackage.displayName); writer.KeyString("shaderName", layer.shaderName);
writer.KeyBool("bypass", false); writer.KeyBool("bypass", layer.bypass);
writer.KeyString("buildState", RuntimeLayerBuildStateName(layer.buildState));
writer.KeyBool("renderReady", layer.renderReady);
writer.KeyString("message", layer.message);
writer.Key("temporal"); writer.Key("temporal");
WriteTemporalJson(writer, shaderPackage.temporal); if (shaderPackage)
WriteTemporalJson(writer, shaderPackage->temporal);
else
WriteTemporalJson(writer, TemporalSettings());
writer.Key("feedback"); writer.Key("feedback");
WriteFeedbackJson(writer, shaderPackage.feedback); if (shaderPackage)
WriteFeedbackJson(writer, shaderPackage->feedback);
else
WriteFeedbackJson(writer, FeedbackSettings());
writer.Key("parameters"); writer.Key("parameters");
writer.BeginArray(); writer.BeginArray();
for (const ShaderParameterDefinition& parameter : shaderPackage.parameters) if (shaderPackage)
WriteParameterDefinitionJson(writer, parameter); {
for (const ShaderParameterDefinition& parameter : shaderPackage->parameters)
WriteParameterDefinitionJson(writer, parameter);
}
writer.EndArray(); writer.EndArray();
writer.EndObject(); writer.EndObject();
} }
@@ -209,9 +231,9 @@ inline std::string RuntimeStateToJson(const RuntimeStateJsonInput& input)
writer.Key("runtime"); writer.Key("runtime");
writer.BeginObject(); writer.BeginObject();
writer.KeyUInt("layerCount", input.activeShaderPackage ? 1 : 0); writer.KeyUInt("layerCount", static_cast<uint64_t>(input.runtimeLayers.displayLayers.size()));
writer.KeyBool("compileSucceeded", input.runtimeCompileSucceeded); writer.KeyBool("compileSucceeded", input.runtimeLayers.compileSucceeded);
writer.KeyString("compileMessage", input.runtimeCompileMessage); writer.KeyString("compileMessage", input.runtimeLayers.compileMessage);
writer.EndObject(); writer.EndObject();
writer.Key("video"); writer.Key("video");
@@ -250,7 +272,7 @@ inline std::string RuntimeStateToJson(const RuntimeStateJsonInput& input)
writer.KeyNull("runtimeEvents"); writer.KeyNull("runtimeEvents");
writer.Key("shaders"); writer.Key("shaders");
writer.BeginArray(); writer.BeginArray();
for (const SupportedShaderSummary& shader : input.shaders) for (const SupportedShaderSummary& shader : input.shaderCatalog.Shaders())
{ {
writer.BeginObject(); writer.BeginObject();
writer.KeyString("id", shader.id); writer.KeyString("id", shader.id);

View File

@@ -0,0 +1,224 @@
#include "RuntimeLayerModel.h"
#include <utility>
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;
}
}

View File

@@ -0,0 +1,84 @@
#pragma once
#include "RuntimeShaderArtifact.h"
#include "SupportedShaderCatalog.h"
#include <cstdint>
#include <string>
#include <vector>
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<RuntimeLayerReadModel> displayLayers;
std::vector<RuntimeRenderLayerModel> 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<Layer> mLayers;
uint64_t mNextLayerNumber = 1;
};
}

View File

@@ -7,6 +7,7 @@
struct RuntimeShaderArtifact struct RuntimeShaderArtifact
{ {
std::string layerId;
std::string shaderId; std::string shaderId;
std::string displayName; std::string displayName;
std::string fragmentShaderSource; std::string fragmentShaderSource;

View File

@@ -8,11 +8,17 @@ RuntimeShaderBridge::~RuntimeShaderBridge()
} }
void RuntimeShaderBridge::Start(const std::string& shaderId, ArtifactCallback onArtifactReady, ErrorCallback onError) 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(); Stop();
if (shaderId.empty()) if (shaderId.empty())
return; return;
mLayerId = layerId;
mOnArtifactReady = std::move(onArtifactReady); mOnArtifactReady = std::move(onArtifactReady);
mOnError = std::move(onError); mOnError = std::move(onError);
mStopping.store(false, std::memory_order_release); mStopping.store(false, std::memory_order_release);
@@ -26,6 +32,7 @@ void RuntimeShaderBridge::Stop()
if (mThread.joinable()) if (mThread.joinable())
mThread.join(); mThread.join();
mCompiler.Stop(); mCompiler.Stop();
mLayerId.clear();
mOnArtifactReady = ArtifactCallback(); mOnArtifactReady = ArtifactCallback();
mOnError = ErrorCallback(); mOnError = ErrorCallback();
} }
@@ -39,6 +46,7 @@ void RuntimeShaderBridge::ThreadMain()
{ {
if (build.succeeded) if (build.succeeded)
{ {
build.artifact.layerId = mLayerId;
if (mOnArtifactReady) if (mOnArtifactReady)
mOnArtifactReady(build.artifact); mOnArtifactReady(build.artifact);
} }

View File

@@ -20,6 +20,7 @@ public:
~RuntimeShaderBridge(); ~RuntimeShaderBridge();
void Start(const std::string& shaderId, ArtifactCallback onArtifactReady, ErrorCallback onError); 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(); void Stop();
private: private:
@@ -28,6 +29,7 @@ private:
RuntimeSlangShaderCompiler mCompiler; RuntimeSlangShaderCompiler mCompiler;
std::thread mThread; std::thread mThread;
std::atomic<bool> mStopping{ false }; std::atomic<bool> mStopping{ false };
std::string mLayerId;
ArtifactCallback mOnArtifactReady; ArtifactCallback mOnArtifactReady;
ErrorCallback mOnError; ErrorCallback mOnError;
}; };

View File

@@ -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 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 ## 5. No Hidden Blocking In The Cadence Path
The render loop must not do work with unbounded or OS-dependent latency. The render loop must not do work with unbounded or OS-dependent latency.
@@ -124,6 +126,7 @@ Preferred boundaries:
- render cadence/thread ownership - render cadence/thread ownership
- GL rendering - GL rendering
- runtime artifact build/bridge - runtime artifact build/bridge
- app-owned display/render layer model
- parameter packing - parameter packing
- system-memory frame exchange - system-memory frame exchange
- DeckLink output scheduling - DeckLink output scheduling

View File

@@ -106,6 +106,39 @@ void TestKnownPostEndpointReturnsActionError()
Expect(response.body.find("not implemented") != std::string::npos, "unimplemented post reports diagnostic"); 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() void TestUnknownEndpointReturns404()
{ {
using namespace RenderCadenceCompositor; using namespace RenderCadenceCompositor;
@@ -126,6 +159,7 @@ int main()
TestStateEndpointUsesCallback(); TestStateEndpointUsesCallback();
TestRootServesUiIndex(); TestRootServesUiIndex();
TestKnownPostEndpointReturnsActionError(); TestKnownPostEndpointReturnsActionError();
TestLayerPostEndpointsUseCallbacks();
TestUnknownEndpointReturns404(); TestUnknownEndpointReturns404();
if (gFailures != 0) if (gFailures != 0)

View File

@@ -0,0 +1,163 @@
#include "RuntimeLayerModel.h"
#include <chrono>
#include <filesystem>
#include <fstream>
#include <iostream>
#include <string>
namespace
{
int gFailures = 0;
void Expect(bool condition, const std::string& message)
{
if (condition)
return;
++gFailures;
std::cerr << "FAIL: " << message << "\n";
}
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;
}

View File

@@ -1,8 +1,10 @@
#include "RuntimeStateJson.h" #include "RuntimeStateJson.h"
#include <chrono>
#include <filesystem>
#include <fstream>
#include <iostream> #include <iostream>
#include <string> #include <string>
#include <vector>
namespace namespace
{ {
@@ -17,29 +19,19 @@ void ExpectContains(const std::string& text, const std::string& fragment, const
std::cerr << "FAIL: " << message << "\n"; std::cerr << "FAIL: " << message << "\n";
} }
ShaderPackage MakePackage() std::filesystem::path MakeTestRoot()
{ {
ShaderPackage package; const auto stamp = std::chrono::steady_clock::now().time_since_epoch().count();
package.id = "solid-color"; const std::filesystem::path root = std::filesystem::temp_directory_path() / ("render-cadence-state-json-tests-" + std::to_string(stamp));
package.displayName = "Solid Color"; std::filesystem::create_directories(root);
package.description = "A single color shader."; return root;
package.category = "Generator"; }
ShaderPassDefinition pass; void WriteFile(const std::filesystem::path& path, const std::string& contents)
pass.id = "main"; {
package.passes.push_back(pass); std::filesystem::create_directories(path.parent_path());
std::ofstream output(path, std::ios::binary);
ShaderParameterDefinition color; output << contents;
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;
} }
} }
@@ -53,10 +45,41 @@ int main()
telemetry.renderFps = 59.94; telemetry.renderFps = 59.94;
telemetry.shaderBuildsCommitted = 1; telemetry.shaderBuildsCommitted = 1;
std::vector<RenderCadenceCompositor::SupportedShaderSummary> shaders = { const std::filesystem::path root = MakeTestRoot();
{ "solid-color", "Solid Color", "A single color shader.", "Generator" } 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"({
const ShaderPackage package = MakePackage(); "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{ const std::string json = RenderCadenceCompositor::RuntimeStateToJson(RenderCadenceCompositor::RuntimeStateJsonInput{
config, config,
@@ -64,10 +87,8 @@ int main()
8080, 8080,
true, true,
"DeckLink scheduled output running.", "DeckLink scheduled output running.",
shaders, shaderCatalog,
true, layerSnapshot
"Runtime shader committed.",
&package
}); });
ExpectContains(json, "\"shaders\":[{\"id\":\"solid-color\"", "state JSON should include supported shaders"); 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, "\"width\":1920", "state JSON should expose output width");
ExpectContains(json, "\"height\":1080", "state JSON should expose output height"); ExpectContains(json, "\"height\":1080", "state JSON should expose output height");
std::filesystem::remove_all(root);
if (gFailures != 0) if (gFailures != 0)
{ {
std::cerr << gFailures << " RenderCadenceCompositorRuntimeStateJson test failure(s).\n"; std::cerr << gFailures << " RenderCadenceCompositorRuntimeStateJson test failure(s).\n";