From bc690e2a87391cde3efb2f0b04674f0d345337a7 Mon Sep 17 00:00:00 2001 From: Aiden <68633820+awils27@users.noreply.github.com> Date: Tue, 12 May 2026 13:14:52 +1000 Subject: [PATCH] Clean up pass --- CMakeLists.txt | 3 +- apps/RenderCadenceCompositor/README.md | 31 +++- .../RenderCadenceCompositor.cpp | 2 +- apps/RenderCadenceCompositor/app/AppConfig.h | 4 +- .../app/AppConfigProvider.cpp | 46 +++++ .../app/AppConfigProvider.h | 4 +- .../app/RenderCadenceApp.h | 173 +++++++----------- .../control/RuntimeStateJson.h | 108 +++++++++++ .../telemetry/TelemetryHealthMonitor.h | 116 ++++++++++++ .../telemetry/TelemetryPrinter.h | 88 --------- docs/NEW_RENDER_CADENCE_APP_PLAN.md | 53 ++++-- docs/RENDER_CADENCE_GOLDEN_RULES.md | 5 + ...adenceCompositorAppConfigProviderTests.cpp | 4 + 13 files changed, 417 insertions(+), 220 deletions(-) create mode 100644 apps/RenderCadenceCompositor/control/RuntimeStateJson.h create mode 100644 apps/RenderCadenceCompositor/telemetry/TelemetryHealthMonitor.h delete mode 100644 apps/RenderCadenceCompositor/telemetry/TelemetryPrinter.h diff --git a/CMakeLists.txt b/CMakeLists.txt index cda4e08..dc6410f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -309,6 +309,7 @@ set(RENDER_CADENCE_APP_SOURCES "${RENDER_CADENCE_APP_DIR}/app/RenderCadenceApp.h" "${RENDER_CADENCE_APP_DIR}/control/HttpControlServer.cpp" "${RENDER_CADENCE_APP_DIR}/control/HttpControlServer.h" + "${RENDER_CADENCE_APP_DIR}/control/RuntimeStateJson.h" "${RENDER_CADENCE_APP_DIR}/frames/SystemFrameExchange.cpp" "${RENDER_CADENCE_APP_DIR}/frames/SystemFrameExchange.h" "${RENDER_CADENCE_APP_DIR}/frames/SystemFrameTypes.h" @@ -338,7 +339,7 @@ set(RENDER_CADENCE_APP_SOURCES "${RENDER_CADENCE_APP_DIR}/runtime/RuntimeSlangShaderCompiler.h" "${RENDER_CADENCE_APP_DIR}/telemetry/CadenceTelemetryJson.h" "${RENDER_CADENCE_APP_DIR}/telemetry/CadenceTelemetry.h" - "${RENDER_CADENCE_APP_DIR}/telemetry/TelemetryPrinter.h" + "${RENDER_CADENCE_APP_DIR}/telemetry/TelemetryHealthMonitor.h" "${RENDER_CADENCE_APP_DIR}/video/DeckLinkOutput.cpp" "${RENDER_CADENCE_APP_DIR}/video/DeckLinkOutput.h" "${RENDER_CADENCE_APP_DIR}/video/DeckLinkOutputThread.h" diff --git a/apps/RenderCadenceCompositor/README.md b/apps/RenderCadenceCompositor/README.md index 54c195b..17f552c 100644 --- a/apps/RenderCadenceCompositor/README.md +++ b/apps/RenderCadenceCompositor/README.md @@ -33,6 +33,7 @@ Startup warms up real rendered frames before DeckLink scheduled playback starts. Included now: - output-only DeckLink +- non-blocking startup when DeckLink output is unavailable - hidden render-thread-owned OpenGL context - simple smooth-motion renderer - BGRA8-only output @@ -49,7 +50,7 @@ Included now: - background logging with `log`, `warning`, and `error` levels - local HTTP control server matching the OpenAPI route surface - startup config provider for `config/runtime-host.json` -- compact telemetry +- quiet telemetry health monitor - non-GL frame-exchange tests Intentionally not included yet: @@ -150,11 +151,28 @@ Current endpoints: The HTTP server runs on its own thread. It samples/copies telemetry through callbacks and does not call render work or DeckLink scheduling. -The app prints one line per second: +## Optional DeckLink Output -```text -renderFps=59.9 scheduleFps=59.9 free=7 completed=1 scheduled=4 completedPollMisses=0 scheduleFailures=0 completions=119 late=0 dropped=0 shaderCommitted=1 shaderFailures=0 decklinkBuffered=4 scheduleCallMs=0.0 -``` +DeckLink output is an optional edge service in this app. + +Startup order is: + +1. start render thread +2. warm up rendered system-memory frames +3. try to attach DeckLink output +4. start telemetry and HTTP either way + +If DeckLink discovery or output setup fails, the app logs a warning and continues running without starting the output scheduler or scheduled playback. This keeps render cadence, runtime shader testing, HTTP state, and logging available on machines without DeckLink hardware or drivers. + +`/api/state` reports the output status in `videoIO.statusMessage`. + +The app samples telemetry once per second. + +Normal cadence samples are available through `GET /api/state` and are not printed to the console. The telemetry monitor only logs health events: + +- warning when DeckLink late/dropped-frame counters increase +- warning when schedule failures increase +- error when the app/DeckLink output buffer is starved Healthy first-run signs: @@ -234,10 +252,11 @@ This app keeps the same core behavior but splits it into modules that can grow: - `frames/`: system-memory handoff - `platform/`: COM/Win32/hidden GL context support - `render/`: cadence, simple rendering, PBO readback -- `control/`: local HTTP API edge +- `control/`: local HTTP API edge and runtime-state JSON presentation - `json/`: compact JSON serialization helpers - `video/`: DeckLink output wrapper and scheduling thread - `telemetry/`: cadence telemetry +- `telemetry/TelemetryHealthMonitor`: quiet health event logging from telemetry samples - `app/`: startup/shutdown orchestration - `app/AppConfigProvider`: startup config loading and CLI overrides diff --git a/apps/RenderCadenceCompositor/RenderCadenceCompositor.cpp b/apps/RenderCadenceCompositor/RenderCadenceCompositor.cpp index c7e1c02..09cbe7b 100644 --- a/apps/RenderCadenceCompositor/RenderCadenceCompositor.cpp +++ b/apps/RenderCadenceCompositor/RenderCadenceCompositor.cpp @@ -43,7 +43,7 @@ int main(int argc, char** argv) { RenderCadenceCompositor::AppConfigProvider configProvider; std::string configError; - if (!configProvider.Load("config/runtime-host.json", configError)) + if (!configProvider.LoadDefault(configError)) { RenderCadenceCompositor::Logger::Instance().Start(RenderCadenceCompositor::DefaultAppConfig().logging); RenderCadenceCompositor::LogError("app", "Config load failed: " + configError); diff --git a/apps/RenderCadenceCompositor/app/AppConfig.h b/apps/RenderCadenceCompositor/app/AppConfig.h index 824b6e3..aeeca03 100644 --- a/apps/RenderCadenceCompositor/app/AppConfig.h +++ b/apps/RenderCadenceCompositor/app/AppConfig.h @@ -2,7 +2,7 @@ #include "../control/HttpControlServer.h" #include "../logging/Logger.h" -#include "../telemetry/TelemetryPrinter.h" +#include "../telemetry/TelemetryHealthMonitor.h" #include "../video/DeckLinkOutput.h" #include "../video/DeckLinkOutputThread.h" @@ -16,7 +16,7 @@ struct AppConfig { DeckLinkOutputConfig deckLink; DeckLinkOutputThreadConfig outputThread; - TelemetryPrinterConfig telemetry; + TelemetryHealthMonitorConfig telemetry; LoggerConfig logging; HttpControlServerConfig http; std::string shaderLibrary = "shaders"; diff --git a/apps/RenderCadenceCompositor/app/AppConfigProvider.cpp b/apps/RenderCadenceCompositor/app/AppConfigProvider.cpp index 415f7d0..3293841 100644 --- a/apps/RenderCadenceCompositor/app/AppConfigProvider.cpp +++ b/apps/RenderCadenceCompositor/app/AppConfigProvider.cpp @@ -7,11 +7,22 @@ #include #include #include +#include +#include namespace RenderCadenceCompositor { namespace { +std::filesystem::path ExecutableDirectory() +{ + char path[MAX_PATH] = {}; + const DWORD length = GetModuleFileNameA(nullptr, path, static_cast(sizeof(path))); + if (length == 0 || length >= sizeof(path)) + return std::filesystem::current_path(); + return std::filesystem::path(path).parent_path(); +} + std::string ReadTextFile(const std::filesystem::path& path, std::string& error) { std::ifstream input(path, std::ios::binary); @@ -76,6 +87,17 @@ AppConfigProvider::AppConfigProvider() : { } +bool AppConfigProvider::LoadDefault(std::string& error) +{ + const std::filesystem::path path = FindConfigFile(); + if (path.empty()) + { + error = "Could not locate config/runtime-host.json from current directory or executable directory."; + return false; + } + return Load(path, error); +} + bool AppConfigProvider::Load(const std::filesystem::path& path, std::string& error) { mConfig = DefaultAppConfig(); @@ -183,4 +205,28 @@ void VideoFormatDimensions(const std::string& formatName, unsigned& width, unsig width = 1920; height = 1080; } + +std::filesystem::path FindConfigFile(const std::filesystem::path& relativePath) +{ + std::vector starts; + starts.push_back(std::filesystem::current_path()); + starts.push_back(ExecutableDirectory()); + + for (std::filesystem::path start : starts) + { + for (;;) + { + const std::filesystem::path candidate = start / relativePath; + if (std::filesystem::exists(candidate)) + return candidate; + + const std::filesystem::path parent = start.parent_path(); + if (parent.empty() || parent == start) + break; + start = parent; + } + } + + return std::filesystem::path(); +} } diff --git a/apps/RenderCadenceCompositor/app/AppConfigProvider.h b/apps/RenderCadenceCompositor/app/AppConfigProvider.h index 35c06ff..9fc2a0f 100644 --- a/apps/RenderCadenceCompositor/app/AppConfigProvider.h +++ b/apps/RenderCadenceCompositor/app/AppConfigProvider.h @@ -12,7 +12,8 @@ class AppConfigProvider public: AppConfigProvider(); - bool Load(const std::filesystem::path& path, std::string& error); +bool Load(const std::filesystem::path& path, std::string& error); +bool LoadDefault(std::string& error); void ApplyCommandLine(int argc, char** argv); const AppConfig& Config() const { return mConfig; } @@ -27,4 +28,5 @@ private: double FrameDurationMillisecondsFromRateString(const std::string& rateText, double fallbackRate = 59.94); void VideoFormatDimensions(const std::string& formatName, unsigned& width, unsigned& height); +std::filesystem::path FindConfigFile(const std::filesystem::path& relativePath = "config/runtime-host.json"); } diff --git a/apps/RenderCadenceCompositor/app/RenderCadenceApp.h b/apps/RenderCadenceCompositor/app/RenderCadenceApp.h index f2b4b09..251fcb3 100644 --- a/apps/RenderCadenceCompositor/app/RenderCadenceApp.h +++ b/apps/RenderCadenceCompositor/app/RenderCadenceApp.h @@ -1,12 +1,10 @@ #pragma once #include "AppConfig.h" -#include "AppConfigProvider.h" -#include "../json/JsonWriter.h" #include "../logging/Logger.h" #include "../runtime/RuntimeShaderBridge.h" -#include "../telemetry/CadenceTelemetryJson.h" -#include "../telemetry/TelemetryPrinter.h" +#include "../control/RuntimeStateJson.h" +#include "../telemetry/TelemetryHealthMonitor.h" #include "../video/DeckLinkOutput.h" #include "../video/DeckLinkOutputThread.h" @@ -55,7 +53,7 @@ public: mFrameExchange(frameExchange), mConfig(config), mOutputThread(mOutput, mFrameExchange, mConfig.outputThread), - mTelemetry(mConfig.telemetry) + mTelemetryHealth(mConfig.telemetry) { } @@ -69,18 +67,6 @@ public: bool Start(std::string& error) { - Log("app", "Initializing DeckLink output."); - if (!mOutput.Initialize( - mConfig.deckLink, - [this](const VideoIOCompletion& completion) { - mFrameExchange.ReleaseScheduledByBytes(completion.outputFrameBuffer); - }, - error)) - { - LogError("app", "DeckLink output initialization failed: " + error); - return false; - } - Log("app", "Starting render thread."); if (!detail::StartRenderThread(mRenderThread, error, 0)) { @@ -99,33 +85,8 @@ public: return false; } - Log("app", "Starting DeckLink output thread."); - if (!mOutputThread.Start()) - { - error = "DeckLink output thread failed to start."; - LogError("app", error); - Stop(); - return false; - } - - Log("app", "Waiting for DeckLink preroll frames."); - if (!WaitForPreroll()) - { - error = "Timed out waiting for DeckLink preroll frames."; - LogError("app", error); - Stop(); - return false; - } - - Log("app", "Starting DeckLink scheduled playback."); - if (!mOutput.StartScheduledPlayback(error)) - { - LogError("app", "DeckLink scheduled playback failed: " + error); - Stop(); - return false; - } - - mTelemetry.Start(mFrameExchange, mOutput, mOutputThread, mRenderThread); + StartOptionalVideoOutput(); + mTelemetryHealth.Start(mFrameExchange, mOutput, mOutputThread, mRenderThread); StartHttpServer(); Log("app", "RenderCadenceCompositor started."); mStarted = true; @@ -135,7 +96,7 @@ public: void Stop() { mHttpServer.Stop(); - mTelemetry.Stop(); + mTelemetryHealth.Stop(); mOutputThread.Stop(); mOutput.Stop(); StopRuntimeShaderBuild(); @@ -150,6 +111,58 @@ public: const DeckLinkOutput& Output() const { return mOutput; } private: + void StartOptionalVideoOutput() + { + std::string outputError; + Log("app", "Initializing optional DeckLink output."); + if (!mOutput.Initialize( + mConfig.deckLink, + [this](const VideoIOCompletion& completion) { + mFrameExchange.ReleaseScheduledByBytes(completion.outputFrameBuffer); + }, + outputError)) + { + DisableVideoOutput("DeckLink output unavailable: " + outputError); + return; + } + + Log("app", "Starting DeckLink output thread."); + if (!mOutputThread.Start()) + { + DisableVideoOutput("DeckLink output thread failed to start."); + return; + } + + Log("app", "Waiting for DeckLink preroll frames."); + if (!WaitForPreroll()) + { + DisableVideoOutput("Timed out waiting for DeckLink preroll frames."); + return; + } + + Log("app", "Starting DeckLink scheduled playback."); + if (!mOutput.StartScheduledPlayback(outputError)) + { + DisableVideoOutput("DeckLink scheduled playback failed: " + outputError); + return; + } + + mVideoOutputEnabled = true; + mVideoOutputStatus = "DeckLink scheduled output running."; + Log("app", mVideoOutputStatus); + } + + void DisableVideoOutput(const std::string& reason) + { + mOutputThread.Stop(); + mOutput.Stop(); + mOutput.ReleaseResources(); + mFrameExchange.Clear(); + mVideoOutputEnabled = false; + mVideoOutputStatus = reason; + LogWarning("app", reason + " Continuing without video output."); + } + void StartHttpServer() { HttpControlServerCallbacks callbacks; @@ -168,65 +181,13 @@ private: std::string BuildStateJson() { CadenceTelemetrySnapshot telemetry = mHttpTelemetry.Sample(mFrameExchange, mOutput, mOutputThread, mRenderThread); - - JsonWriter writer; - writer.BeginObject(); - writer.Key("app"); - writer.BeginObject(); - writer.KeyUInt("serverPort", mHttpServer.Port()); - writer.KeyUInt("oscPort", mConfig.oscPort); - writer.KeyString("oscBindAddress", mConfig.oscBindAddress); - writer.KeyDouble("oscSmoothing", mConfig.oscSmoothing); - writer.KeyBool("autoReload", mConfig.autoReload); - writer.KeyUInt("maxTemporalHistoryFrames", static_cast(mConfig.maxTemporalHistoryFrames)); - writer.KeyDouble("previewFps", mConfig.previewFps); - writer.KeyBool("enableExternalKeying", mConfig.deckLink.externalKeyingEnabled); - writer.KeyString("inputVideoFormat", mConfig.inputVideoFormat); - writer.KeyString("inputFrameRate", mConfig.inputFrameRate); - writer.KeyString("outputVideoFormat", mConfig.outputVideoFormat); - writer.KeyString("outputFrameRate", mConfig.outputFrameRate); - writer.EndObject(); - - writer.Key("runtime"); - writer.BeginObject(); - writer.KeyUInt("layerCount", 0); - writer.KeyBool("compileSucceeded", true); - writer.KeyString("compileMessage", "Runtime state is not ported into RenderCadenceCompositor yet."); - writer.EndObject(); - - writer.KeyNull("video"); - writer.KeyNull("decklink"); - writer.KeyNull("videoIO"); - - writer.Key("performance"); - writer.BeginObject(); - writer.KeyDouble("frameBudgetMs", FrameDurationMillisecondsFromRateString(mConfig.outputFrameRate)); - writer.KeyNull("renderMs"); - writer.KeyNull("smoothedRenderMs"); - writer.KeyNull("budgetUsedPercent"); - writer.KeyNull("completionIntervalMs"); - writer.KeyNull("smoothedCompletionIntervalMs"); - writer.KeyNull("maxCompletionIntervalMs"); - writer.KeyUInt("lateFrameCount", telemetry.displayedLate); - writer.KeyUInt("droppedFrameCount", telemetry.dropped); - writer.KeyNull("flushedFrameCount"); - writer.Key("cadence"); - WriteCadenceTelemetryJson(writer, telemetry); - writer.EndObject(); - - writer.KeyNull("backendPlayout"); - writer.KeyNull("runtimeEvents"); - writer.Key("shaders"); - writer.BeginArray(); - writer.EndArray(); - writer.Key("stackPresets"); - writer.BeginArray(); - writer.EndArray(); - writer.Key("layers"); - writer.BeginArray(); - writer.EndArray(); - writer.EndObject(); - return writer.StringValue(); + return RuntimeStateToJson(RuntimeStateJsonInput{ + mConfig, + telemetry, + mHttpServer.Port(), + mVideoOutputEnabled, + mVideoOutputStatus + }); } bool WaitForPreroll() const @@ -270,10 +231,12 @@ private: AppConfig mConfig; DeckLinkOutput mOutput; DeckLinkOutputThread mOutputThread; - TelemetryPrinter mTelemetry; + TelemetryHealthMonitor mTelemetryHealth; CadenceTelemetry mHttpTelemetry; HttpControlServer mHttpServer; RuntimeShaderBridge mShaderBridge; bool mStarted = false; + bool mVideoOutputEnabled = false; + std::string mVideoOutputStatus = "DeckLink output not started."; }; } diff --git a/apps/RenderCadenceCompositor/control/RuntimeStateJson.h b/apps/RenderCadenceCompositor/control/RuntimeStateJson.h new file mode 100644 index 0000000..1690517 --- /dev/null +++ b/apps/RenderCadenceCompositor/control/RuntimeStateJson.h @@ -0,0 +1,108 @@ +#pragma once + +#include "../app/AppConfig.h" +#include "../app/AppConfigProvider.h" +#include "../json/JsonWriter.h" +#include "../telemetry/CadenceTelemetryJson.h" + +#include +#include + +namespace RenderCadenceCompositor +{ +struct RuntimeStateJsonInput +{ + const AppConfig& config; + const CadenceTelemetrySnapshot& telemetry; + unsigned short serverPort = 0; + bool videoOutputEnabled = false; + std::string videoOutputStatus; +}; + +inline void WriteVideoIoStatusJson(JsonWriter& writer, const RuntimeStateJsonInput& input) +{ + writer.BeginObject(); + writer.KeyString("backend", "decklink"); + writer.KeyNull("modelName"); + writer.KeyBool("supportsInternalKeying", false); + writer.KeyBool("supportsExternalKeying", false); + writer.KeyBool("keyerInterfaceAvailable", false); + writer.KeyBool("externalKeyingRequested", input.config.deckLink.externalKeyingEnabled); + writer.KeyBool("externalKeyingActive", input.videoOutputEnabled && input.config.deckLink.externalKeyingEnabled); + writer.KeyString("statusMessage", input.videoOutputStatus); + writer.EndObject(); +} + +inline std::string RuntimeStateToJson(const RuntimeStateJsonInput& input) +{ + JsonWriter writer; + writer.BeginObject(); + + writer.Key("app"); + writer.BeginObject(); + writer.KeyUInt("serverPort", input.serverPort); + writer.KeyUInt("oscPort", input.config.oscPort); + writer.KeyString("oscBindAddress", input.config.oscBindAddress); + writer.KeyDouble("oscSmoothing", input.config.oscSmoothing); + writer.KeyBool("autoReload", input.config.autoReload); + writer.KeyUInt("maxTemporalHistoryFrames", static_cast(input.config.maxTemporalHistoryFrames)); + writer.KeyDouble("previewFps", input.config.previewFps); + writer.KeyBool("enableExternalKeying", input.config.deckLink.externalKeyingEnabled); + writer.KeyString("inputVideoFormat", input.config.inputVideoFormat); + writer.KeyString("inputFrameRate", input.config.inputFrameRate); + writer.KeyString("outputVideoFormat", input.config.outputVideoFormat); + writer.KeyString("outputFrameRate", input.config.outputFrameRate); + writer.EndObject(); + + writer.Key("runtime"); + writer.BeginObject(); + writer.KeyUInt("layerCount", 0); + writer.KeyBool("compileSucceeded", true); + writer.KeyString("compileMessage", "Runtime state is not ported into RenderCadenceCompositor yet."); + writer.EndObject(); + + writer.Key("video"); + writer.BeginObject(); + writer.KeyBool("hasSignal", false); + writer.KeyNull("width"); + writer.KeyNull("height"); + writer.KeyString("modeName", "output-only"); + writer.EndObject(); + + writer.Key("decklink"); + WriteVideoIoStatusJson(writer, input); + writer.Key("videoIO"); + WriteVideoIoStatusJson(writer, input); + + writer.Key("performance"); + writer.BeginObject(); + writer.KeyDouble("frameBudgetMs", FrameDurationMillisecondsFromRateString(input.config.outputFrameRate)); + writer.KeyNull("renderMs"); + writer.KeyNull("smoothedRenderMs"); + writer.KeyNull("budgetUsedPercent"); + writer.KeyNull("completionIntervalMs"); + writer.KeyNull("smoothedCompletionIntervalMs"); + writer.KeyNull("maxCompletionIntervalMs"); + writer.KeyUInt("lateFrameCount", input.telemetry.displayedLate); + writer.KeyUInt("droppedFrameCount", input.telemetry.dropped); + writer.KeyNull("flushedFrameCount"); + writer.Key("cadence"); + WriteCadenceTelemetryJson(writer, input.telemetry); + writer.EndObject(); + + writer.KeyNull("backendPlayout"); + writer.KeyNull("runtimeEvents"); + writer.Key("shaders"); + writer.BeginArray(); + writer.EndArray(); + writer.Key("stackPresets"); + writer.BeginArray(); + writer.EndArray(); + writer.Key("layers"); + writer.BeginArray(); + writer.EndArray(); + + writer.EndObject(); + return writer.StringValue(); +} +} diff --git a/apps/RenderCadenceCompositor/telemetry/TelemetryHealthMonitor.h b/apps/RenderCadenceCompositor/telemetry/TelemetryHealthMonitor.h new file mode 100644 index 0000000..2f7237b --- /dev/null +++ b/apps/RenderCadenceCompositor/telemetry/TelemetryHealthMonitor.h @@ -0,0 +1,116 @@ +#pragma once + +#include "CadenceTelemetry.h" +#include "../logging/Logger.h" + +#include +#include +#include +#include + +namespace RenderCadenceCompositor +{ +struct TelemetryHealthMonitorConfig +{ + std::chrono::milliseconds interval = std::chrono::seconds(1); + std::size_t scheduledStarvationThreshold = 0; +}; + +class TelemetryHealthMonitor +{ +public: + explicit TelemetryHealthMonitor(TelemetryHealthMonitorConfig config = TelemetryHealthMonitorConfig()) : + mConfig(config) + { + } + + TelemetryHealthMonitor(const TelemetryHealthMonitor&) = delete; + TelemetryHealthMonitor& operator=(const TelemetryHealthMonitor&) = delete; + + ~TelemetryHealthMonitor() + { + Stop(); + } + + template + void Start(const SystemFrameExchange& exchange, const Output& output, const OutputThread& outputThread, const RenderThread& renderThread) + { + if (mRunning) + return; + mStopping = false; + mThread = std::thread([this, &exchange, &output, &outputThread, &renderThread]() { + CadenceTelemetry telemetry; + CadenceTelemetrySnapshot previous; + bool hasPrevious = false; + while (!mStopping) + { + std::this_thread::sleep_for(mConfig.interval); + const CadenceTelemetrySnapshot snapshot = telemetry.Sample(exchange, output, outputThread, renderThread); + ReportHealth(snapshot, hasPrevious ? &previous : nullptr); + previous = snapshot; + hasPrevious = true; + } + }); + mRunning = true; + } + + void Stop() + { + mStopping = true; + if (mThread.joinable()) + mThread.join(); + mRunning = false; + } + +private: + void ReportHealth(const CadenceTelemetrySnapshot& snapshot, const CadenceTelemetrySnapshot* previous) const + { + if (!previous) + return; + + const uint64_t lateDelta = snapshot.displayedLate - previous->displayedLate; + const uint64_t droppedDelta = snapshot.dropped - previous->dropped; + const uint64_t scheduleFailureDelta = snapshot.scheduleFailures - previous->scheduleFailures; + + if (droppedDelta > 0 || lateDelta > 0) + { + std::ostringstream message; + message << "DeckLink reported frame timing issue: lateDelta=" << lateDelta + << " droppedDelta=" << droppedDelta + << " totalLate=" << snapshot.displayedLate + << " totalDropped=" << snapshot.dropped; + LogWarning("telemetry", message.str()); + } + + if (scheduleFailureDelta > 0) + { + std::ostringstream message; + message << "DeckLink schedule failures increased: delta=" << scheduleFailureDelta + << " total=" << snapshot.scheduleFailures; + LogWarning("telemetry", message.str()); + } + + const bool appScheduledStarved = snapshot.scheduledFrames <= mConfig.scheduledStarvationThreshold + && snapshot.scheduledTotal > 0; + const bool deckLinkStarved = snapshot.deckLinkBufferedAvailable && snapshot.deckLinkBuffered == 0; + if (appScheduledStarved || deckLinkStarved) + { + std::ostringstream message; + message << "Output buffer starvation detected: scheduled=" << snapshot.scheduledFrames + << " decklinkBuffered="; + if (snapshot.deckLinkBufferedAvailable) + message << snapshot.deckLinkBuffered; + else + message << "n/a"; + message << " renderFps=" << snapshot.renderFps + << " scheduleFps=" << snapshot.scheduleFps; + LogError("telemetry", message.str()); + } + } + + TelemetryHealthMonitorConfig mConfig; + std::thread mThread; + std::atomic mStopping{ false }; + std::atomic mRunning{ false }; +}; +} diff --git a/apps/RenderCadenceCompositor/telemetry/TelemetryPrinter.h b/apps/RenderCadenceCompositor/telemetry/TelemetryPrinter.h deleted file mode 100644 index f73bbe1..0000000 --- a/apps/RenderCadenceCompositor/telemetry/TelemetryPrinter.h +++ /dev/null @@ -1,88 +0,0 @@ -#pragma once - -#include "CadenceTelemetry.h" - -#include -#include -#include -#include -#include - -namespace RenderCadenceCompositor -{ -struct TelemetryPrinterConfig -{ - std::chrono::milliseconds interval = std::chrono::seconds(1); -}; - -class TelemetryPrinter -{ -public: - explicit TelemetryPrinter(TelemetryPrinterConfig config = TelemetryPrinterConfig()) : - mConfig(config) - { - } - - TelemetryPrinter(const TelemetryPrinter&) = delete; - TelemetryPrinter& operator=(const TelemetryPrinter&) = delete; - - ~TelemetryPrinter() - { - Stop(); - } - - template - void Start(const SystemFrameExchange& exchange, const Output& output, const OutputThread& outputThread, const RenderThread& renderThread) - { - if (mRunning) - return; - mStopping = false; - mThread = std::thread([this, &exchange, &output, &outputThread, &renderThread]() { - CadenceTelemetry telemetry; - while (!mStopping) - { - std::this_thread::sleep_for(mConfig.interval); - Print(telemetry.Sample(exchange, output, outputThread, renderThread)); - } - }); - mRunning = true; - } - - void Stop() - { - mStopping = true; - if (mThread.joinable()) - mThread.join(); - mRunning = false; - } - -private: - static void Print(const CadenceTelemetrySnapshot& snapshot) - { - std::cout << std::fixed << std::setprecision(1) - << "renderFps=" << snapshot.renderFps - << " scheduleFps=" << snapshot.scheduleFps - << " free=" << snapshot.freeFrames - << " completed=" << snapshot.completedFrames - << " scheduled=" << snapshot.scheduledFrames - << " completedPollMisses=" << snapshot.completedPollMisses - << " scheduleFailures=" << snapshot.scheduleFailures - << " completions=" << snapshot.completions - << " late=" << snapshot.displayedLate - << " dropped=" << snapshot.dropped - << " shaderCommitted=" << snapshot.shaderBuildsCommitted - << " shaderFailures=" << snapshot.shaderBuildFailures - << " decklinkBuffered="; - if (snapshot.deckLinkBufferedAvailable) - std::cout << snapshot.deckLinkBuffered; - else - std::cout << "n/a"; - std::cout << " scheduleCallMs=" << snapshot.deckLinkScheduleCallMilliseconds << "\n"; - } - - TelemetryPrinterConfig mConfig; - std::thread mThread; - std::atomic mStopping{ false }; - std::atomic mRunning{ false }; -}; -} diff --git a/docs/NEW_RENDER_CADENCE_APP_PLAN.md b/docs/NEW_RENDER_CADENCE_APP_PLAN.md index ee2adf9..2efbd9d 100644 --- a/docs/NEW_RENDER_CADENCE_APP_PLAN.md +++ b/docs/NEW_RENDER_CADENCE_APP_PLAN.md @@ -104,6 +104,13 @@ apps/RenderCadenceCompositor/ RenderCadenceApp.h AppConfig.cpp AppConfig.h + AppConfigProvider.cpp + AppConfigProvider.h + + control/ + HttpControlServer.cpp + HttpControlServer.h + RuntimeStateJson.h platform/ ComInit.cpp @@ -139,8 +146,16 @@ apps/RenderCadenceCompositor/ telemetry/ CadenceTelemetry.cpp CadenceTelemetry.h - TelemetryPrinter.cpp - TelemetryPrinter.h + CadenceTelemetryJson.h + TelemetryHealthMonitor.h + + logging/ + Logger.cpp + Logger.h + + json/ + JsonWriter.cpp + JsonWriter.h ``` The new app can reuse selected existing source files from the current app at first: @@ -355,15 +370,17 @@ Initial counters: - free/rendering/completed/scheduled slot counts - actual DeckLink buffered frames -### `TelemetryPrinter` +### `TelemetryHealthMonitor` -Prints one stable line per interval, matching the probe where possible. +Samples cadence telemetry once per interval and logs only health events. -Example: +Normal telemetry is available through the HTTP state endpoint. The console should not receive a healthy once-per-second cadence line. -```text -renderFps=59.9 scheduleFps=59.9 free=7 completed=1 scheduled=4 drops=0 pboMiss=0 completions=119 late=0 dropped=0 decklinkBuffered=4 -``` +Health events: + +- warning when DeckLink late/dropped-frame counters increase +- warning when schedule failures increase +- error when app/DeckLink output buffering is starved ## Startup Sequence @@ -371,24 +388,28 @@ Target first-version startup: ```text main - -> parse AppConfig + -> load AppConfig through AppConfigProvider -> initialize COM - -> DeckLinkOutput discover/select/configure output - -> DeckLinkOutput prepare output schedule -> create SystemFrameExchange -> start RenderThread -> wait for completed frame warmup - -> start DeckLinkOutputThread - -> wait for scheduled depth warmup - -> DeckLinkOutput start scheduled playback - -> start TelemetryPrinter + -> optionally discover/select/configure DeckLink output + -> if DeckLink is available: + -> start DeckLinkOutputThread + -> wait for scheduled depth warmup + -> DeckLinkOutput start scheduled playback + -> if DeckLink is unavailable: + -> continue without video output + -> start TelemetryHealthMonitor + -> start HttpControlServer -> wait for Enter ``` Shutdown: ```text -stop TelemetryPrinter +stop HttpControlServer +stop TelemetryHealthMonitor stop DeckLinkOutputThread DeckLinkOutput stop playback stop RenderThread diff --git a/docs/RENDER_CADENCE_GOLDEN_RULES.md b/docs/RENDER_CADENCE_GOLDEN_RULES.md index a7acfb2..30cc1cd 100644 --- a/docs/RENDER_CADENCE_GOLDEN_RULES.md +++ b/docs/RENDER_CADENCE_GOLDEN_RULES.md @@ -26,6 +26,7 @@ Not allowed on the render thread: - network/API/OSC handling - DeckLink scheduling - blocking console logging +- config file discovery or parsing If future GL preparation needs to happen off-thread, use an explicit shared-context GL prepare thread. Do not smuggle non-render work back into the cadence loop. @@ -127,6 +128,10 @@ Preferred boundaries: - system-memory frame exchange - DeckLink output scheduling - telemetry +- local control/API edge +- config loading +- JSON presentation/serialization +- logging If a file starts coordinating multiple subsystems and doing detailed work for each of them, split it before it becomes the new old app. diff --git a/tests/RenderCadenceCompositorAppConfigProviderTests.cpp b/tests/RenderCadenceCompositorAppConfigProviderTests.cpp index 21fffca..f1f8b70 100644 --- a/tests/RenderCadenceCompositorAppConfigProviderTests.cpp +++ b/tests/RenderCadenceCompositorAppConfigProviderTests.cpp @@ -104,6 +104,10 @@ void TestHelpers() const double duration = FrameDurationMillisecondsFromRateString("50"); Expect(duration > 19.9 && duration < 20.1, "frame duration parses numeric rate"); + + const std::filesystem::path configPath = FindConfigFile(); + Expect(!configPath.empty(), "default config is discoverable from test working directory"); + Expect(configPath.filename() == "runtime-host.json", "default config discovery returns runtime-host.json"); } }