clean split
All checks were successful
CI / React UI Build (push) Successful in 12s
CI / Native Windows Build And Tests (push) Successful in 2m14s
CI / Windows Release Package (push) Has been skipped

This commit is contained in:
Aiden
2026-05-30 21:22:03 +10:00
parent fbc2851ccb
commit 56883439a6
10 changed files with 269 additions and 63 deletions

View File

@@ -30,6 +30,8 @@ Native app internals are grouped by boundary:
- `video/playout/`: backend-adjacent playout policy, queues, frame pools, and scheduling helpers. - `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. - `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 ## Requirements
- Windows with Visual Studio 2022 C++ tooling. - Windows with Visual Studio 2022 C++ tooling.

View File

@@ -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 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. 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/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/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/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 ## 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/render/runtime`: current runtime shader scene, renderer, text texture cache, and shared-context shader preparation.
- `src/runtime/shader`: background Slang package build bridge. - `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/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. - `shaders/`: bundled shader package library.
- `runtime/templates/shader_wrapper.slang.in`: only needed for the current Slang package pipeline. - `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. - `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 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. 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 ## 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 `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. - 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. - 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. - 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 `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. - 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 ## 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`. - 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`. - 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. - `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. - `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 ## Recommended Fork Sequence
1. Make the hygiene fixes above in this repo or immediately after the fork. 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. 2. Keep `IRuntimeContentController` as the app-side boundary and replace `ShaderRuntimeContentController` with the fork's D3D engine control/state implementation.
3. Keep `RuntimeShaderRenderContent` as the baseline implementation until the fork's renderer exists. 3. Keep `IRenderContent` as the render-thread draw-call boundary only if the fork remains GL-backed or uses a GL/D3D bridge.
4. Add the new renderer beside it or replace the default adapter. 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. 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. 6. Only then remove shader package pieces that the new repo no longer needs.

View File

@@ -66,6 +66,7 @@ Included now:
- app-owned display/render layer model for shader build readiness - 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-owned render-content interface with runtime shader content as the default implementation - 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 - 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 - shared-context GL prepare worker for runtime shader program compile/link
- render-thread-only GL program swap once a prepared program is ready - 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. `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 ## Optional DeckLink Output
DeckLink output is an optional edge service in this app. DeckLink output is an optional edge service in this app.

View File

@@ -2,6 +2,7 @@
#include "app/AppConfigProvider.h" #include "app/AppConfigProvider.h"
#include "app/AppRestart.h" #include "app/AppRestart.h"
#include "app/RenderCadenceApp.h" #include "app/RenderCadenceApp.h"
#include "app/ShaderRuntimeContentController.h"
#include "app/VideoBackendFactory.h" #include "app/VideoBackendFactory.h"
#include "frames/InputFrameMailbox.h" #include "frames/InputFrameMailbox.h"
#include "frames/SystemFrameExchange.h" #include "frames/SystemFrameExchange.h"
@@ -16,8 +17,10 @@
#include <chrono> #include <chrono>
#include <cctype> #include <cctype>
#include <iostream> #include <iostream>
#include <memory>
#include <sstream> #include <sstream>
#include <string> #include <string>
#include <vector>
namespace namespace
{ {
@@ -196,11 +199,16 @@ int main(int argc, char** argv)
RenderThread renderThread(frameExchange, &inputMailbox, renderConfig); RenderThread renderThread(frameExchange, &inputMailbox, renderConfig);
auto outputBackend = RenderCadenceCompositor::CreateVideoOutputBackend(appConfig); auto outputBackend = RenderCadenceCompositor::CreateVideoOutputBackend(appConfig);
auto runtimeContent = std::make_unique<RenderCadenceCompositor::ShaderRuntimeContentController>(
[&renderThread](const std::vector<RenderCadenceCompositor::RuntimeRenderLayerModel>& layers) {
renderThread.SubmitRuntimeRenderLayers(layers);
});
RenderCadenceCompositor::RenderCadenceApp<RenderThread, SystemFrameExchange> app( RenderCadenceCompositor::RenderCadenceApp<RenderThread, SystemFrameExchange> app(
renderThread, renderThread,
frameExchange, frameExchange,
appConfig, appConfig,
std::move(outputBackend), std::move(outputBackend),
std::move(runtimeContent),
configProvider.SourcePath()); configProvider.SourcePath());
app.SetVideoInputMetricsProvider([inputBackend = inputBackend.get()]() { app.SetVideoInputMetricsProvider([inputBackend = inputBackend.get()]() {
return inputBackend ? inputBackend->Metrics() : RenderCadenceCompositor::VideoInputEdgeMetrics(); return inputBackend ? inputBackend->Metrics() : RenderCadenceCompositor::VideoInputEdgeMetrics();

View File

@@ -5,7 +5,7 @@
#include "AppConfigProvider.h" #include "AppConfigProvider.h"
#include "AppRestart.h" #include "AppRestart.h"
#include "RenderCadenceHttpRoutes.h" #include "RenderCadenceHttpRoutes.h"
#include "RuntimeLayerController.h" #include "RuntimeContentController.h"
#include "../logging/Logger.h" #include "../logging/Logger.h"
#include "../control/RuntimeStateJson.h" #include "../control/RuntimeStateJson.h"
#include "../control/osc/OscControlServer.h" #include "../control/osc/OscControlServer.h"
@@ -65,20 +65,19 @@ public:
SystemFrameExchange& frameExchange, SystemFrameExchange& frameExchange,
AppConfig config, AppConfig config,
std::unique_ptr<IVideoOutputEdge> output, std::unique_ptr<IVideoOutputEdge> output,
std::unique_ptr<IRuntimeContentController> runtimeContent,
std::filesystem::path configPath = std::filesystem::path()) : std::filesystem::path configPath = std::filesystem::path()) :
mRenderThread(renderThread), mRenderThread(renderThread),
mFrameExchange(frameExchange), mFrameExchange(frameExchange),
mConfig(config), mConfig(config),
mConfigPath(std::move(configPath)), mConfigPath(std::move(configPath)),
mOutput(std::move(output)), mOutput(std::move(output)),
mRuntimeContent(std::move(runtimeContent)),
mOutputThread(*mOutput, mFrameExchange, VideoOutputThreadConfig{ mOutputThread(*mOutput, mFrameExchange, VideoOutputThreadConfig{
mConfig.outputThread.targetBufferedFrames, mConfig.outputThread.targetBufferedFrames,
mConfig.outputThread.idleSleep mConfig.outputThread.idleSleep
}), }),
mTelemetryHealth(mConfig.telemetry), mTelemetryHealth(mConfig.telemetry)
mRuntimeLayers([this](const std::vector<RuntimeRenderLayerModel>& layers) {
mRenderThread.SubmitRuntimeRenderLayers(layers);
})
{ {
} }
@@ -92,10 +91,8 @@ public:
bool Start(std::string& error) bool Start(std::string& error)
{ {
mRuntimeLayers.Initialize( if (mRuntimeContent)
mConfig.shaderLibrary, mRuntimeContent->Initialize(mConfig);
static_cast<unsigned>(mConfig.maxTemporalHistoryFrames),
mConfig.runtimeShaderId);
Log("app", "Starting render thread."); Log("app", "Starting render thread.");
if (!detail::StartRenderThread(mRenderThread, error, 0)) if (!detail::StartRenderThread(mRenderThread, error, 0))
@@ -104,7 +101,8 @@ public:
Stop(); Stop();
return false; return false;
} }
mRuntimeLayers.StartStartupBuild(mConfig.runtimeShaderId); if (mRuntimeContent)
mRuntimeContent->Start();
if (!BuildSettledOutputReserve(error)) if (!BuildSettledOutputReserve(error))
{ {
@@ -131,7 +129,8 @@ public:
mPreviewWindow.Stop(); mPreviewWindow.Stop();
mOutputThread.Stop(); mOutputThread.Stop();
mOutput->Stop(); mOutput->Stop();
mRuntimeLayers.Stop(); if (mRuntimeContent)
mRuntimeContent->Stop();
mRenderThread.Stop(); mRenderThread.Stop();
mOutput->ReleaseResources(); mOutput->ReleaseResources();
if (mStarted) if (mStarted)
@@ -260,23 +259,15 @@ private:
routeCallbacks.getNdiSourcesJson = [this]() { routeCallbacks.getNdiSourcesJson = [this]() {
return BuildNdiSourcesJson(); 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) { routeCallbacks.executePost = [this](const std::string& path, const std::string& body) {
if (path == "/api/config/save") if (path == "/api/config/save")
return HandleConfigSave(body); return HandleConfigSave(body);
if (path == "/api/app/restart") if (path == "/api/app/restart")
return HandleAppRestart(); return HandleAppRestart();
RuntimeControlCommand command; if (mRuntimeContent)
std::string error; return mRuntimeContent->HandlePost(path, body);
if (!ParseRuntimeControlCommand(path, body, command, error)) return ControlActionResult{ false, "No runtime content controller is active." };
return ControlActionResult{ false, error };
return mRuntimeLayers.HandleControlCommand(command);
}; };
HttpControlServerCallbacks callbacks; HttpControlServerCallbacks callbacks;
@@ -320,17 +311,27 @@ private:
{ {
CadenceTelemetrySnapshot telemetry = mHttpTelemetry.Sample(mFrameExchange, *mOutput, mOutputThread, mRenderThread); CadenceTelemetrySnapshot telemetry = mHttpTelemetry.Sample(mFrameExchange, *mOutput, mOutputThread, mRenderThread);
ApplyVideoInputMetrics(telemetry); ApplyVideoInputMetrics(telemetry);
RuntimeLayerModelSnapshot layerSnapshot = mRuntimeLayers.Snapshot(telemetry); RuntimeStateJsonInput stateInput{
return RuntimeStateToJson(RuntimeStateJsonInput{
mConfig, mConfig,
telemetry, telemetry,
mHttpServer.Port(), mHttpServer.Port(),
mVideoOutputEnabled, mVideoOutputEnabled,
mVideoOutputStatus, mVideoOutputStatus,
mRuntimeLayers.ShaderCatalog(),
layerSnapshot,
&mOscServer.State() &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() void StartOscServer()
@@ -460,13 +461,13 @@ private:
AppConfig mConfig; AppConfig mConfig;
std::filesystem::path mConfigPath; std::filesystem::path mConfigPath;
std::unique_ptr<IVideoOutputEdge> mOutput; std::unique_ptr<IVideoOutputEdge> mOutput;
std::unique_ptr<IRuntimeContentController> mRuntimeContent;
VideoOutputThread<SystemFrameExchange> mOutputThread; VideoOutputThread<SystemFrameExchange> mOutputThread;
TelemetryHealthMonitor mTelemetryHealth; TelemetryHealthMonitor mTelemetryHealth;
CadenceTelemetry mHttpTelemetry; CadenceTelemetry mHttpTelemetry;
HttpControlServer mHttpServer; HttpControlServer mHttpServer;
OscControlServer mOscServer; OscControlServer mOscServer;
PreviewWindowThread mPreviewWindow; PreviewWindowThread mPreviewWindow;
RuntimeLayerController mRuntimeLayers;
std::function<VideoInputEdgeMetrics()> mVideoInputMetricsProvider; std::function<VideoInputEdgeMetrics()> mVideoInputMetricsProvider;
uint64_t mLastInputCapturedFrames = 0; uint64_t mLastInputCapturedFrames = 0;
std::atomic<bool> mRestartRequired{ false }; std::atomic<bool> mRestartRequired{ false };

View File

@@ -0,0 +1,26 @@
#pragma once
#include "AppConfig.h"
#include "../control/ControlActionResult.h"
#include "../json/JsonWriter.h"
#include "../telemetry/CadenceTelemetry.h"
#include <string>
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;
};
}

View File

@@ -0,0 +1,67 @@
#include "ShaderRuntimeContentController.h"
#include "../control/RuntimeControlCommand.h"
#include "../control/RuntimeStateJson.h"
#include <utility>
namespace RenderCadenceCompositor
{
ShaderRuntimeContentController::ShaderRuntimeContentController(RenderLayerPublisher publisher) :
mRuntimeLayers(std::move(publisher))
{
}
ShaderRuntimeContentController::~ShaderRuntimeContentController()
{
Stop();
}
void ShaderRuntimeContentController::Initialize(AppConfig& config)
{
mRuntimeLayers.Initialize(
config.shaderLibrary,
static_cast<unsigned>(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));
}
}

View File

@@ -0,0 +1,34 @@
#pragma once
#include "RuntimeContentController.h"
#include "RuntimeLayerController.h"
#include <string>
#include <vector>
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;
};
}

View File

@@ -9,6 +9,7 @@
#include "../telemetry/CadenceTelemetryJson.h" #include "../telemetry/CadenceTelemetryJson.h"
#include <cstdint> #include <cstdint>
#include <functional>
#include <string> #include <string>
#include <vector> #include <vector>
@@ -21,9 +22,10 @@ struct RuntimeStateJsonInput
unsigned short serverPort = 0; unsigned short serverPort = 0;
bool videoOutputEnabled = false; bool videoOutputEnabled = false;
std::string videoOutputStatus; std::string videoOutputStatus;
const SupportedShaderCatalog& shaderCatalog;
const RuntimeLayerModelSnapshot& runtimeLayers;
const OscControlServerState* osc = nullptr; const OscControlServerState* osc = nullptr;
std::function<void(JsonWriter&)> writeRuntimeJson;
std::function<void(JsonWriter&)> writeCatalogJson;
std::function<void(JsonWriter&)> writeLayersJson;
}; };
inline void WriteVideoIoStatusJson(JsonWriter& writer, const RuntimeStateJsonInput& input) inline void WriteVideoIoStatusJson(JsonWriter& writer, const RuntimeStateJsonInput& input)
@@ -244,12 +246,56 @@ inline void WriteParameterDefinitionJson(JsonWriter& writer, const ShaderParamet
writer.EndObject(); 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<uint64_t>(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(); 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.BeginObject();
writer.KeyString("id", layer.id); writer.KeyString("id", layer.id);
writer.KeyString("shaderId", layer.shaderId); writer.KeyString("shaderId", layer.shaderId);
@@ -281,6 +327,12 @@ inline void WriteLayersJson(JsonWriter& writer, const RuntimeStateJsonInput& inp
writer.EndArray(); writer.EndArray();
} }
inline void WriteEmptyLayersJson(JsonWriter& writer)
{
writer.BeginArray();
writer.EndArray();
}
inline void WriteOscJson(JsonWriter& writer, const RuntimeStateJsonInput& input) inline void WriteOscJson(JsonWriter& writer, const RuntimeStateJsonInput& input)
{ {
const bool configured = input.osc ? input.osc->configured : input.config.oscPort != 0; 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.EndObject();
writer.Key("runtime"); writer.Key("runtime");
writer.BeginObject(); if (input.writeRuntimeJson)
writer.KeyUInt("layerCount", static_cast<uint64_t>(input.runtimeLayers.displayLayers.size())); input.writeRuntimeJson(writer);
writer.KeyBool("compileSucceeded", input.runtimeLayers.compileSucceeded); else
writer.KeyString("compileMessage", input.runtimeLayers.compileMessage); WriteEmptyRuntimeJson(writer);
writer.EndObject();
writer.Key("video"); writer.Key("video");
writer.BeginObject(); writer.BeginObject();
@@ -386,24 +437,18 @@ inline std::string RuntimeStateToJson(const RuntimeStateJsonInput& input)
writer.KeyNull("backendPlayout"); writer.KeyNull("backendPlayout");
writer.KeyNull("runtimeEvents"); writer.KeyNull("runtimeEvents");
writer.Key("shaders"); writer.Key("shaders");
writer.BeginArray(); if (input.writeCatalogJson)
for (const SupportedShaderSummary& shader : input.shaderCatalog.Shaders()) input.writeCatalogJson(writer);
{ else
writer.BeginObject(); WriteEmptyCatalogJson(writer);
writer.KeyString("id", shader.id);
writer.KeyString("name", shader.name);
writer.KeyString("description", shader.description);
writer.KeyString("category", shader.category);
writer.KeyBool("available", true);
writer.KeyNull("error");
writer.EndObject();
}
writer.EndArray();
writer.Key("stackPresets"); writer.Key("stackPresets");
writer.BeginArray(); writer.BeginArray();
writer.EndArray(); writer.EndArray();
writer.Key("layers"); writer.Key("layers");
WriteLayersJson(writer, input); if (input.writeLayersJson)
input.writeLayersJson(writer);
else
WriteEmptyLayersJson(writer);
writer.EndObject(); writer.EndObject();
return writer.StringValue(); return writer.StringValue();

View File

@@ -100,15 +100,24 @@ int main()
layerModel.MarkBuildReady(artifact, error); layerModel.MarkBuildReady(artifact, error);
const RenderCadenceCompositor::RuntimeLayerModelSnapshot layerSnapshot = layerModel.Snapshot(); const RenderCadenceCompositor::RuntimeLayerModelSnapshot layerSnapshot = layerModel.Snapshot();
const std::string json = RenderCadenceCompositor::RuntimeStateToJson(RenderCadenceCompositor::RuntimeStateJsonInput{ RenderCadenceCompositor::RuntimeStateJsonInput stateInput{
config, config,
telemetry, telemetry,
8080, 8080,
true, true,
"DeckLink scheduled output running.", "DeckLink scheduled output running.",
shaderCatalog, nullptr
layerSnapshot };
}); 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, "\"shaders\":[{\"id\":\"solid-color\"", "state JSON should include supported shaders");
ExpectContains(json, "\"layerCount\":1", "state JSON should expose the display layer count"); ExpectContains(json, "\"layerCount\":1", "state JSON should expose the display layer count");