diff --git a/README.md b/README.md index e701037..f2182b8 100644 --- a/README.md +++ b/README.md @@ -30,6 +30,8 @@ Native app internals are grouped by boundary: - `video/playout/`: backend-adjacent playout policy, queues, frame pools, and scheduling helpers. - `video/legacy/`: older backend pipeline pieces kept separate while the new edge model settles. +The runtime shader stack is plugged into the host through `src/app/RuntimeContentController.h`. The checked-in app wires `ShaderRuntimeContentController` into that slot; a fork can provide another runtime-content controller for a larger render engine while keeping the cadence/video I/O shell. + ## Requirements - Windows with Visual Studio 2022 C++ tooling. diff --git a/docs/FORKING_RENDER_CADENCE_BASE.md b/docs/FORKING_RENDER_CADENCE_BASE.md index c48400a..08d019c 100644 --- a/docs/FORKING_RENDER_CADENCE_BASE.md +++ b/docs/FORKING_RENDER_CADENCE_BASE.md @@ -6,7 +6,9 @@ This note captures the fork-readiness review for using this repository as a base The repository is clean enough for an internal fork, but it needs a small hygiene pass before it becomes a comfortable long-lived base repo. -The important architecture is already in place: render cadence, video input/output, frame exchange, readback, preview, control, and shader build work are mostly separated by role. The main replacement point is the render-content adapter in `src/render/RuntimeShaderRenderContent.*`, where the current shader-package renderer decides what to draw into the framebuffer handed to it by `RenderThread`. +The important architecture is already in place: render cadence, video input/output, frame exchange, readback, preview, control, and shader build work are mostly separated by role. The app-side replacement point is now `IRuntimeContentController` in `src/app/RuntimeContentController.h`. The current shader package stack is plugged in by `ShaderRuntimeContentController`, while a fork can provide a D3D-engine controller without making the host app own Slang, shader catalogs, or shader layer state. + +The current render-thread implementation still uses OpenGL for its context and readback path. If the fork's renderer is truly D3D rather than GL content, keep the app/video/frame-exchange boundary and replace the render-thread/content implementation that publishes completed system-memory frames. For a new repo, keep the cadence and frame handoff machinery, then replace or narrow the runtime shader rendering layer. @@ -23,6 +25,7 @@ These parts are the useful base for the fork: - `src/app`: startup, config, video backend factory, runtime layer orchestration, preview, telemetry, and HTTP server hookup. - `src/control/http`, `src/control/osc`, `src/telemetry`, `src/logging`, and `ui`: useful if the new repo still wants a local control surface. `control/osc` is currently a status/lifecycle stub, not a UDP listener. - `src/app/RenderCadenceHttpRoutes.*`: useful only if the new repo keeps this app's current `/api/...` control surface. +- `src/app/RuntimeContentController.h`: the app-side runtime-content boundary. Keep the interface shape and provide a fork-owned implementation for D3D engine control/state. ## Replace Or Rework @@ -31,6 +34,7 @@ These are most likely to change when the fork renders something other than shade - `src/render/runtime`: current runtime shader scene, renderer, text texture cache, and shared-context shader preparation. - `src/runtime/shader`: background Slang package build bridge. - `src/shader`: shader package manifest parsing and Slang wrapper generation, unless the new renderer keeps the same shader package contract. +- `src/app/ShaderRuntimeContentController.*`: current Slang shader-stack implementation of the app runtime-content boundary. - `shaders/`: bundled shader package library. - `runtime/templates/shader_wrapper.slang.in`: only needed for the current Slang package pipeline. - `src/app/RenderCadenceHttpRoutes.*`: replace this with a fork-owned route module if the new renderer has different controls, while keeping `src/control/http/HttpControlServer.*` as the socket/static/WebSocket shell. @@ -38,6 +42,8 @@ These are most likely to change when the fork renders something other than shade The first fork step is now in place: `RenderThread` preserves the cadence/readback shell and calls a narrow render-content interface behind the draw call. A new repo can swap that implementation without touching video I/O scheduling. +The shader stack is also separated from `RenderCadenceApp` now. The app owns only an `IRuntimeContentController`; `RenderCadenceCompositor.cpp` wires the current `ShaderRuntimeContentController` into that slot. A D3D fork should wire a D3D runtime-content controller there and then replace the render-thread content/readback implementation as needed. + The HTTP control boundary is also split now. `HttpControlServer` owns transport, static-file helpers, OpenAPI helper serving, and WebSocket state transport; `RenderCadenceHttpRoutes` owns this app's REST endpoint map. A fork that wants the same browser/server plumbing can provide its own route callback and leave the Render Cadence-specific endpoints behind. ## Current Swap Point @@ -68,17 +74,18 @@ Before cutting a long-lived fork, fix or decide these items: - Keep `runtimeShaderId` empty in checked-in config unless this repo intentionally wants a default startup shader again. - Keep runtime third-party discovery aligned with CMake. Font atlas generation and Slang compilation now both check explicit tool roots, `THIRD_PARTY_ROOT`, the private `video-io-3rdParty` bundle, and legacy or packaged `3rdParty` layouts. - Make `config/runtime-host.json` portable. Current checked-in defaults include a local NDI source name and DeckLink output. -- Decide whether the fork keeps the Slang shader package contract. If not, retire or clearly isolate `shaders/SHADER_CONTRACT.md`, shader package UI, and shader manifest tests. +- Decide whether the fork keeps the Slang shader package contract. If not, replace `ShaderRuntimeContentController`, retire or clearly isolate `shaders/SHADER_CONTRACT.md`, shader package UI, and shader manifest tests. +- For a D3D engine fork, decide whether to bridge D3D output into the existing GL/readback path temporarily or replace `RenderThread`/readback with a D3D-native publisher that still writes completed `SystemFrameExchange` frames. - Mark older docs that reference `apps/LoopThroughWithOpenGLCompositing` as historical, or update them to point at the current `src/` implementation. - Keep `runtime/` generated output ignored, and keep only `runtime/templates/` plus `runtime/README.md` tracked. - Keep the private SDK bundle as a submodule only if the new repo is intended for the same org/build environment. External forks should use ignored `3rdParty/` or explicit CMake SDK paths. ## Verification Snapshot -Last checked locally on 2026-05-30 after the Slang lookup alignment: +Last checked locally on 2026-05-30 after the runtime-content controller split: - UI production build passed with `npm.cmd run build`. -- Native debug build passed for `RenderCadenceCompositor`, `ShaderSlangValidationTests`, and `ShaderCompilerLookupTests`. +- Native debug build passed for `RenderCadenceCompositor`. - Native tests passed 26 of 26 with `ctest --test-dir build\vs2022-x64-debug -C Debug --output-on-failure`. - `FontAtlasBuilderTests` now passes with `msdf-atlas-gen.exe` supplied by the private `video-io-3rdParty/msdf-atlas-gen` bundle. - `ShaderCompilerLookupTests` covers `SLANG_ROOT`, `THIRD_PARTY_ROOT`, repo `video-io-3rdParty`, legacy `3rdParty`, and packaged `3rdParty/slang` Slang layouts. @@ -88,9 +95,9 @@ The generated Visual Studio `RUN_TESTS` target did not build missing test execut ## Recommended Fork Sequence 1. Make the hygiene fixes above in this repo or immediately after the fork. -2. Keep `IRenderContent` as the boundary behind the `RenderThread` draw call. -3. Keep `RuntimeShaderRenderContent` as the baseline implementation until the fork's renderer exists. -4. Add the new renderer beside it or replace the default adapter. +2. Keep `IRuntimeContentController` as the app-side boundary and replace `ShaderRuntimeContentController` with the fork's D3D engine control/state implementation. +3. Keep `IRenderContent` as the render-thread draw-call boundary only if the fork remains GL-backed or uses a GL/D3D bridge. +4. If the fork is D3D-native, replace the render-thread/readback implementation while preserving completed-frame publication into `SystemFrameExchange`. 5. Verify that video output still consumes completed frames and never requests rendering directly. 6. Only then remove shader package pieces that the new repo no longer needs. diff --git a/src/README.md b/src/README.md index c46f0db..709cb89 100644 --- a/src/README.md +++ b/src/README.md @@ -66,6 +66,7 @@ Included now: - app-owned display/render layer model for shader build readiness - app-owned submission of a completed shader artifact - render-thread-owned render-content interface with runtime shader content as the default implementation +- app-owned runtime-content controller interface with the Slang shader stack as the default implementation - render-thread-owned runtime render scene for ready shader layers inside the default render-content adapter - shared-context GL prepare worker for runtime shader program compile/link - render-thread-only GL program swap once a prepared program is ready @@ -265,6 +266,12 @@ The HTTP server runs on its own thread. `control/http/HttpControlServer` owns so `control/osc/OscControlServer` is currently a lifecycle/status stub. It consumes the startup OSC config and reports whether OSC is configured or disabled, but it deliberately does not open a UDP socket or dispatch parameter changes yet. +## Runtime Content Boundary + +`app/RenderCadenceApp` owns startup, video output, preview, telemetry, OSC status, and HTTP server lifetime. It does not own the Slang shader stack directly. Runtime content is supplied through `IRuntimeContentController` in `app/RuntimeContentController.h`. + +The default implementation is `ShaderRuntimeContentController`. It wraps `RuntimeLayerController`, exposes shader catalog/layer state to `/api/state`, handles the shader layer POST commands, and publishes render-ready shader layers to the current render thread. A fork that uses a D3D engine should replace this controller with a D3D engine controller and decide separately whether to keep the current GL render/readback thread as a bridge or replace it with a D3D-native frame publisher. + ## Optional DeckLink Output DeckLink output is an optional edge service in this app. diff --git a/src/RenderCadenceCompositor.cpp b/src/RenderCadenceCompositor.cpp index 6f6665c..7b62809 100644 --- a/src/RenderCadenceCompositor.cpp +++ b/src/RenderCadenceCompositor.cpp @@ -2,6 +2,7 @@ #include "app/AppConfigProvider.h" #include "app/AppRestart.h" #include "app/RenderCadenceApp.h" +#include "app/ShaderRuntimeContentController.h" #include "app/VideoBackendFactory.h" #include "frames/InputFrameMailbox.h" #include "frames/SystemFrameExchange.h" @@ -16,8 +17,10 @@ #include #include #include +#include #include #include +#include namespace { @@ -196,11 +199,16 @@ int main(int argc, char** argv) RenderThread renderThread(frameExchange, &inputMailbox, renderConfig); auto outputBackend = RenderCadenceCompositor::CreateVideoOutputBackend(appConfig); + auto runtimeContent = std::make_unique( + [&renderThread](const std::vector& layers) { + renderThread.SubmitRuntimeRenderLayers(layers); + }); RenderCadenceCompositor::RenderCadenceApp app( renderThread, frameExchange, appConfig, std::move(outputBackend), + std::move(runtimeContent), configProvider.SourcePath()); app.SetVideoInputMetricsProvider([inputBackend = inputBackend.get()]() { return inputBackend ? inputBackend->Metrics() : RenderCadenceCompositor::VideoInputEdgeMetrics(); diff --git a/src/app/RenderCadenceApp.h b/src/app/RenderCadenceApp.h index 0f072dc..90ea8bf 100644 --- a/src/app/RenderCadenceApp.h +++ b/src/app/RenderCadenceApp.h @@ -5,7 +5,7 @@ #include "AppConfigProvider.h" #include "AppRestart.h" #include "RenderCadenceHttpRoutes.h" -#include "RuntimeLayerController.h" +#include "RuntimeContentController.h" #include "../logging/Logger.h" #include "../control/RuntimeStateJson.h" #include "../control/osc/OscControlServer.h" @@ -65,20 +65,19 @@ public: SystemFrameExchange& frameExchange, AppConfig config, std::unique_ptr output, + std::unique_ptr runtimeContent, std::filesystem::path configPath = std::filesystem::path()) : mRenderThread(renderThread), mFrameExchange(frameExchange), mConfig(config), mConfigPath(std::move(configPath)), mOutput(std::move(output)), + mRuntimeContent(std::move(runtimeContent)), mOutputThread(*mOutput, mFrameExchange, VideoOutputThreadConfig{ mConfig.outputThread.targetBufferedFrames, mConfig.outputThread.idleSleep }), - mTelemetryHealth(mConfig.telemetry), - mRuntimeLayers([this](const std::vector& layers) { - mRenderThread.SubmitRuntimeRenderLayers(layers); - }) + mTelemetryHealth(mConfig.telemetry) { } @@ -92,10 +91,8 @@ public: bool Start(std::string& error) { - mRuntimeLayers.Initialize( - mConfig.shaderLibrary, - static_cast(mConfig.maxTemporalHistoryFrames), - mConfig.runtimeShaderId); + if (mRuntimeContent) + mRuntimeContent->Initialize(mConfig); Log("app", "Starting render thread."); if (!detail::StartRenderThread(mRenderThread, error, 0)) @@ -104,7 +101,8 @@ public: Stop(); return false; } - mRuntimeLayers.StartStartupBuild(mConfig.runtimeShaderId); + if (mRuntimeContent) + mRuntimeContent->Start(); if (!BuildSettledOutputReserve(error)) { @@ -131,7 +129,8 @@ public: mPreviewWindow.Stop(); mOutputThread.Stop(); mOutput->Stop(); - mRuntimeLayers.Stop(); + if (mRuntimeContent) + mRuntimeContent->Stop(); mRenderThread.Stop(); mOutput->ReleaseResources(); if (mStarted) @@ -260,23 +259,15 @@ private: routeCallbacks.getNdiSourcesJson = [this]() { return BuildNdiSourcesJson(); }; - routeCallbacks.addLayer = [this](const std::string& body) { - return mRuntimeLayers.HandleAddLayer(body); - }; - routeCallbacks.removeLayer = [this](const std::string& body) { - return mRuntimeLayers.HandleRemoveLayer(body); - }; routeCallbacks.executePost = [this](const std::string& path, const std::string& body) { if (path == "/api/config/save") return HandleConfigSave(body); if (path == "/api/app/restart") return HandleAppRestart(); - RuntimeControlCommand command; - std::string error; - if (!ParseRuntimeControlCommand(path, body, command, error)) - return ControlActionResult{ false, error }; - return mRuntimeLayers.HandleControlCommand(command); + if (mRuntimeContent) + return mRuntimeContent->HandlePost(path, body); + return ControlActionResult{ false, "No runtime content controller is active." }; }; HttpControlServerCallbacks callbacks; @@ -320,17 +311,27 @@ private: { CadenceTelemetrySnapshot telemetry = mHttpTelemetry.Sample(mFrameExchange, *mOutput, mOutputThread, mRenderThread); ApplyVideoInputMetrics(telemetry); - RuntimeLayerModelSnapshot layerSnapshot = mRuntimeLayers.Snapshot(telemetry); - return RuntimeStateToJson(RuntimeStateJsonInput{ + RuntimeStateJsonInput stateInput{ mConfig, telemetry, mHttpServer.Port(), mVideoOutputEnabled, mVideoOutputStatus, - mRuntimeLayers.ShaderCatalog(), - layerSnapshot, &mOscServer.State() - }); + }; + if (mRuntimeContent) + { + stateInput.writeRuntimeJson = [this, telemetry](JsonWriter& writer) { + mRuntimeContent->WriteRuntimeJson(writer, telemetry); + }; + stateInput.writeCatalogJson = [this](JsonWriter& writer) { + mRuntimeContent->WriteCatalogJson(writer); + }; + stateInput.writeLayersJson = [this, telemetry](JsonWriter& writer) { + mRuntimeContent->WriteLayersJson(writer, telemetry); + }; + } + return RuntimeStateToJson(stateInput); } void StartOscServer() @@ -460,13 +461,13 @@ private: AppConfig mConfig; std::filesystem::path mConfigPath; std::unique_ptr mOutput; + std::unique_ptr mRuntimeContent; VideoOutputThread mOutputThread; TelemetryHealthMonitor mTelemetryHealth; CadenceTelemetry mHttpTelemetry; HttpControlServer mHttpServer; OscControlServer mOscServer; PreviewWindowThread mPreviewWindow; - RuntimeLayerController mRuntimeLayers; std::function mVideoInputMetricsProvider; uint64_t mLastInputCapturedFrames = 0; std::atomic mRestartRequired{ false }; diff --git a/src/app/RuntimeContentController.h b/src/app/RuntimeContentController.h new file mode 100644 index 0000000..7b7f6be --- /dev/null +++ b/src/app/RuntimeContentController.h @@ -0,0 +1,26 @@ +#pragma once + +#include "AppConfig.h" +#include "../control/ControlActionResult.h" +#include "../json/JsonWriter.h" +#include "../telemetry/CadenceTelemetry.h" + +#include + +namespace RenderCadenceCompositor +{ +class IRuntimeContentController +{ +public: + virtual ~IRuntimeContentController() = default; + + virtual void Initialize(AppConfig& config) = 0; + virtual void Start() = 0; + virtual void Stop() = 0; + + virtual ControlActionResult HandlePost(const std::string& path, const std::string& body) = 0; + virtual void WriteRuntimeJson(JsonWriter& writer, const CadenceTelemetrySnapshot& telemetry) const = 0; + virtual void WriteCatalogJson(JsonWriter& writer) const = 0; + virtual void WriteLayersJson(JsonWriter& writer, const CadenceTelemetrySnapshot& telemetry) const = 0; +}; +} diff --git a/src/app/ShaderRuntimeContentController.cpp b/src/app/ShaderRuntimeContentController.cpp new file mode 100644 index 0000000..ce96b86 --- /dev/null +++ b/src/app/ShaderRuntimeContentController.cpp @@ -0,0 +1,67 @@ +#include "ShaderRuntimeContentController.h" + +#include "../control/RuntimeControlCommand.h" +#include "../control/RuntimeStateJson.h" + +#include + +namespace RenderCadenceCompositor +{ +ShaderRuntimeContentController::ShaderRuntimeContentController(RenderLayerPublisher publisher) : + mRuntimeLayers(std::move(publisher)) +{ +} + +ShaderRuntimeContentController::~ShaderRuntimeContentController() +{ + Stop(); +} + +void ShaderRuntimeContentController::Initialize(AppConfig& config) +{ + mRuntimeLayers.Initialize( + config.shaderLibrary, + static_cast(config.maxTemporalHistoryFrames), + config.runtimeShaderId); + mStartupShaderId = config.runtimeShaderId; +} + +void ShaderRuntimeContentController::Start() +{ + mRuntimeLayers.StartStartupBuild(mStartupShaderId); +} + +void ShaderRuntimeContentController::Stop() +{ + mRuntimeLayers.Stop(); +} + +ControlActionResult ShaderRuntimeContentController::HandlePost(const std::string& path, const std::string& body) +{ + if (path == "/api/layers/add") + return mRuntimeLayers.HandleAddLayer(body); + if (path == "/api/layers/remove") + return mRuntimeLayers.HandleRemoveLayer(body); + + RuntimeControlCommand command; + std::string error; + if (!ParseRuntimeControlCommand(path, body, command, error)) + return ControlActionResult{ false, error }; + return mRuntimeLayers.HandleControlCommand(command); +} + +void ShaderRuntimeContentController::WriteRuntimeJson(JsonWriter& writer, const CadenceTelemetrySnapshot& telemetry) const +{ + WriteShaderRuntimeJson(writer, mRuntimeLayers.Snapshot(telemetry)); +} + +void ShaderRuntimeContentController::WriteCatalogJson(JsonWriter& writer) const +{ + WriteShaderCatalogJson(writer, mRuntimeLayers.ShaderCatalog()); +} + +void ShaderRuntimeContentController::WriteLayersJson(JsonWriter& writer, const CadenceTelemetrySnapshot& telemetry) const +{ + WriteRuntimeShaderLayersJson(writer, mRuntimeLayers.ShaderCatalog(), mRuntimeLayers.Snapshot(telemetry)); +} +} diff --git a/src/app/ShaderRuntimeContentController.h b/src/app/ShaderRuntimeContentController.h new file mode 100644 index 0000000..fb52e61 --- /dev/null +++ b/src/app/ShaderRuntimeContentController.h @@ -0,0 +1,34 @@ +#pragma once + +#include "RuntimeContentController.h" +#include "RuntimeLayerController.h" + +#include +#include + +namespace RenderCadenceCompositor +{ +class ShaderRuntimeContentController final : public IRuntimeContentController +{ +public: + using RenderLayerPublisher = RuntimeLayerController::RenderLayerPublisher; + + explicit ShaderRuntimeContentController(RenderLayerPublisher publisher = RenderLayerPublisher()); + ShaderRuntimeContentController(const ShaderRuntimeContentController&) = delete; + ShaderRuntimeContentController& operator=(const ShaderRuntimeContentController&) = delete; + ~ShaderRuntimeContentController() override; + + void Initialize(AppConfig& config) override; + void Start() override; + void Stop() override; + + ControlActionResult HandlePost(const std::string& path, const std::string& body) override; + void WriteRuntimeJson(JsonWriter& writer, const CadenceTelemetrySnapshot& telemetry) const override; + void WriteCatalogJson(JsonWriter& writer) const override; + void WriteLayersJson(JsonWriter& writer, const CadenceTelemetrySnapshot& telemetry) const override; + +private: + RuntimeLayerController mRuntimeLayers; + std::string mStartupShaderId; +}; +} diff --git a/src/control/RuntimeStateJson.h b/src/control/RuntimeStateJson.h index fb036e2..a0f6b06 100644 --- a/src/control/RuntimeStateJson.h +++ b/src/control/RuntimeStateJson.h @@ -9,6 +9,7 @@ #include "../telemetry/CadenceTelemetryJson.h" #include +#include #include #include @@ -21,9 +22,10 @@ struct RuntimeStateJsonInput unsigned short serverPort = 0; bool videoOutputEnabled = false; std::string videoOutputStatus; - const SupportedShaderCatalog& shaderCatalog; - const RuntimeLayerModelSnapshot& runtimeLayers; const OscControlServerState* osc = nullptr; + std::function writeRuntimeJson; + std::function writeCatalogJson; + std::function writeLayersJson; }; inline void WriteVideoIoStatusJson(JsonWriter& writer, const RuntimeStateJsonInput& input) @@ -244,12 +246,56 @@ inline void WriteParameterDefinitionJson(JsonWriter& writer, const ShaderParamet writer.EndObject(); } -inline void WriteLayersJson(JsonWriter& writer, const RuntimeStateJsonInput& input) +inline void WriteShaderRuntimeJson(JsonWriter& writer, const RuntimeLayerModelSnapshot& runtimeLayers) +{ + writer.BeginObject(); + writer.KeyUInt("layerCount", static_cast(runtimeLayers.displayLayers.size())); + writer.KeyBool("compileSucceeded", runtimeLayers.compileSucceeded); + writer.KeyString("compileMessage", runtimeLayers.compileMessage); + writer.EndObject(); +} + +inline void WriteEmptyRuntimeJson(JsonWriter& writer) +{ + writer.BeginObject(); + writer.KeyUInt("layerCount", 0); + writer.KeyBool("compileSucceeded", true); + writer.KeyString("compileMessage", "No runtime content controller is active."); + writer.EndObject(); +} + +inline void WriteShaderCatalogJson(JsonWriter& writer, const SupportedShaderCatalog& shaderCatalog) { writer.BeginArray(); - for (const RuntimeLayerReadModel& layer : input.runtimeLayers.displayLayers) + for (const SupportedShaderSummary& shader : shaderCatalog.Shaders()) { - const ShaderPackage* shaderPackage = input.shaderCatalog.FindPackage(layer.shaderId); + writer.BeginObject(); + writer.KeyString("id", shader.id); + writer.KeyString("name", shader.name); + writer.KeyString("description", shader.description); + writer.KeyString("category", shader.category); + writer.KeyBool("available", true); + writer.KeyNull("error"); + writer.EndObject(); + } + writer.EndArray(); +} + +inline void WriteEmptyCatalogJson(JsonWriter& writer) +{ + writer.BeginArray(); + writer.EndArray(); +} + +inline void WriteRuntimeShaderLayersJson( + JsonWriter& writer, + const SupportedShaderCatalog& shaderCatalog, + const RuntimeLayerModelSnapshot& runtimeLayers) +{ + writer.BeginArray(); + for (const RuntimeLayerReadModel& layer : runtimeLayers.displayLayers) + { + const ShaderPackage* shaderPackage = shaderCatalog.FindPackage(layer.shaderId); writer.BeginObject(); writer.KeyString("id", layer.id); writer.KeyString("shaderId", layer.shaderId); @@ -281,6 +327,12 @@ inline void WriteLayersJson(JsonWriter& writer, const RuntimeStateJsonInput& inp writer.EndArray(); } +inline void WriteEmptyLayersJson(JsonWriter& writer) +{ + writer.BeginArray(); + writer.EndArray(); +} + inline void WriteOscJson(JsonWriter& writer, const RuntimeStateJsonInput& input) { const bool configured = input.osc ? input.osc->configured : input.config.oscPort != 0; @@ -343,11 +395,10 @@ inline std::string RuntimeStateToJson(const RuntimeStateJsonInput& input) writer.EndObject(); writer.Key("runtime"); - writer.BeginObject(); - writer.KeyUInt("layerCount", static_cast(input.runtimeLayers.displayLayers.size())); - writer.KeyBool("compileSucceeded", input.runtimeLayers.compileSucceeded); - writer.KeyString("compileMessage", input.runtimeLayers.compileMessage); - writer.EndObject(); + if (input.writeRuntimeJson) + input.writeRuntimeJson(writer); + else + WriteEmptyRuntimeJson(writer); writer.Key("video"); writer.BeginObject(); @@ -386,24 +437,18 @@ inline std::string RuntimeStateToJson(const RuntimeStateJsonInput& input) writer.KeyNull("backendPlayout"); writer.KeyNull("runtimeEvents"); writer.Key("shaders"); - writer.BeginArray(); - for (const SupportedShaderSummary& shader : input.shaderCatalog.Shaders()) - { - writer.BeginObject(); - writer.KeyString("id", shader.id); - writer.KeyString("name", shader.name); - writer.KeyString("description", shader.description); - writer.KeyString("category", shader.category); - writer.KeyBool("available", true); - writer.KeyNull("error"); - writer.EndObject(); - } - writer.EndArray(); + if (input.writeCatalogJson) + input.writeCatalogJson(writer); + else + WriteEmptyCatalogJson(writer); writer.Key("stackPresets"); writer.BeginArray(); writer.EndArray(); writer.Key("layers"); - WriteLayersJson(writer, input); + if (input.writeLayersJson) + input.writeLayersJson(writer); + else + WriteEmptyLayersJson(writer); writer.EndObject(); return writer.StringValue(); diff --git a/tests/RenderCadenceCompositorRuntimeStateJsonTests.cpp b/tests/RenderCadenceCompositorRuntimeStateJsonTests.cpp index 66eb633..5cb08ae 100644 --- a/tests/RenderCadenceCompositorRuntimeStateJsonTests.cpp +++ b/tests/RenderCadenceCompositorRuntimeStateJsonTests.cpp @@ -100,15 +100,24 @@ int main() layerModel.MarkBuildReady(artifact, error); const RenderCadenceCompositor::RuntimeLayerModelSnapshot layerSnapshot = layerModel.Snapshot(); - const std::string json = RenderCadenceCompositor::RuntimeStateToJson(RenderCadenceCompositor::RuntimeStateJsonInput{ + RenderCadenceCompositor::RuntimeStateJsonInput stateInput{ config, telemetry, 8080, true, "DeckLink scheduled output running.", - shaderCatalog, - layerSnapshot - }); + nullptr + }; + stateInput.writeRuntimeJson = [&layerSnapshot](RenderCadenceCompositor::JsonWriter& writer) { + RenderCadenceCompositor::WriteShaderRuntimeJson(writer, layerSnapshot); + }; + stateInput.writeCatalogJson = [&shaderCatalog](RenderCadenceCompositor::JsonWriter& writer) { + RenderCadenceCompositor::WriteShaderCatalogJson(writer, shaderCatalog); + }; + stateInput.writeLayersJson = [&shaderCatalog, &layerSnapshot](RenderCadenceCompositor::JsonWriter& writer) { + RenderCadenceCompositor::WriteRuntimeShaderLayersJson(writer, shaderCatalog, layerSnapshot); + }; + const std::string json = RenderCadenceCompositor::RuntimeStateToJson(stateInput); ExpectContains(json, "\"shaders\":[{\"id\":\"solid-color\"", "state JSON should include supported shaders"); ExpectContains(json, "\"layerCount\":1", "state JSON should expose the display layer count");