Clean up pass
All checks were successful
CI / React UI Build (push) Successful in 11s
CI / Native Windows Build And Tests (push) Successful in 2m55s
CI / Windows Release Package (push) Successful in 3m14s

This commit is contained in:
Aiden
2026-05-12 13:14:52 +10:00
parent 9938a6cc26
commit bc690e2a87
13 changed files with 417 additions and 220 deletions

View File

@@ -309,6 +309,7 @@ set(RENDER_CADENCE_APP_SOURCES
"${RENDER_CADENCE_APP_DIR}/app/RenderCadenceApp.h" "${RENDER_CADENCE_APP_DIR}/app/RenderCadenceApp.h"
"${RENDER_CADENCE_APP_DIR}/control/HttpControlServer.cpp" "${RENDER_CADENCE_APP_DIR}/control/HttpControlServer.cpp"
"${RENDER_CADENCE_APP_DIR}/control/HttpControlServer.h" "${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.cpp"
"${RENDER_CADENCE_APP_DIR}/frames/SystemFrameExchange.h" "${RENDER_CADENCE_APP_DIR}/frames/SystemFrameExchange.h"
"${RENDER_CADENCE_APP_DIR}/frames/SystemFrameTypes.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}/runtime/RuntimeSlangShaderCompiler.h"
"${RENDER_CADENCE_APP_DIR}/telemetry/CadenceTelemetryJson.h" "${RENDER_CADENCE_APP_DIR}/telemetry/CadenceTelemetryJson.h"
"${RENDER_CADENCE_APP_DIR}/telemetry/CadenceTelemetry.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.cpp"
"${RENDER_CADENCE_APP_DIR}/video/DeckLinkOutput.h" "${RENDER_CADENCE_APP_DIR}/video/DeckLinkOutput.h"
"${RENDER_CADENCE_APP_DIR}/video/DeckLinkOutputThread.h" "${RENDER_CADENCE_APP_DIR}/video/DeckLinkOutputThread.h"

View File

@@ -33,6 +33,7 @@ Startup warms up real rendered frames before DeckLink scheduled playback starts.
Included now: Included now:
- output-only DeckLink - output-only DeckLink
- non-blocking startup when DeckLink output is unavailable
- hidden render-thread-owned OpenGL context - hidden render-thread-owned OpenGL context
- simple smooth-motion renderer - simple smooth-motion renderer
- BGRA8-only output - BGRA8-only output
@@ -49,7 +50,7 @@ Included now:
- background logging with `log`, `warning`, and `error` levels - background logging with `log`, `warning`, and `error` levels
- local HTTP control server matching the OpenAPI route surface - local HTTP control server matching the OpenAPI route surface
- startup config provider for `config/runtime-host.json` - startup config provider for `config/runtime-host.json`
- compact telemetry - quiet telemetry health monitor
- non-GL frame-exchange tests - non-GL frame-exchange tests
Intentionally not included yet: 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 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 DeckLink output is an optional edge service in this app.
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
``` 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: 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 - `frames/`: system-memory handoff
- `platform/`: COM/Win32/hidden GL context support - `platform/`: COM/Win32/hidden GL context support
- `render/`: cadence, simple rendering, PBO readback - `render/`: cadence, simple rendering, PBO readback
- `control/`: local HTTP API edge - `control/`: local HTTP API edge and runtime-state JSON presentation
- `json/`: compact JSON serialization helpers - `json/`: compact JSON serialization helpers
- `video/`: DeckLink output wrapper and scheduling thread - `video/`: DeckLink output wrapper and scheduling thread
- `telemetry/`: cadence telemetry - `telemetry/`: cadence telemetry
- `telemetry/TelemetryHealthMonitor`: quiet health event logging from telemetry samples
- `app/`: startup/shutdown orchestration - `app/`: startup/shutdown orchestration
- `app/AppConfigProvider`: startup config loading and CLI overrides - `app/AppConfigProvider`: startup config loading and CLI overrides

View File

@@ -43,7 +43,7 @@ int main(int argc, char** argv)
{ {
RenderCadenceCompositor::AppConfigProvider configProvider; RenderCadenceCompositor::AppConfigProvider configProvider;
std::string configError; std::string configError;
if (!configProvider.Load("config/runtime-host.json", configError)) if (!configProvider.LoadDefault(configError))
{ {
RenderCadenceCompositor::Logger::Instance().Start(RenderCadenceCompositor::DefaultAppConfig().logging); RenderCadenceCompositor::Logger::Instance().Start(RenderCadenceCompositor::DefaultAppConfig().logging);
RenderCadenceCompositor::LogError("app", "Config load failed: " + configError); RenderCadenceCompositor::LogError("app", "Config load failed: " + configError);

View File

@@ -2,7 +2,7 @@
#include "../control/HttpControlServer.h" #include "../control/HttpControlServer.h"
#include "../logging/Logger.h" #include "../logging/Logger.h"
#include "../telemetry/TelemetryPrinter.h" #include "../telemetry/TelemetryHealthMonitor.h"
#include "../video/DeckLinkOutput.h" #include "../video/DeckLinkOutput.h"
#include "../video/DeckLinkOutputThread.h" #include "../video/DeckLinkOutputThread.h"
@@ -16,7 +16,7 @@ struct AppConfig
{ {
DeckLinkOutputConfig deckLink; DeckLinkOutputConfig deckLink;
DeckLinkOutputThreadConfig outputThread; DeckLinkOutputThreadConfig outputThread;
TelemetryPrinterConfig telemetry; TelemetryHealthMonitorConfig telemetry;
LoggerConfig logging; LoggerConfig logging;
HttpControlServerConfig http; HttpControlServerConfig http;
std::string shaderLibrary = "shaders"; std::string shaderLibrary = "shaders";

View File

@@ -7,11 +7,22 @@
#include <cstdlib> #include <cstdlib>
#include <fstream> #include <fstream>
#include <sstream> #include <sstream>
#include <vector>
#include <windows.h>
namespace RenderCadenceCompositor namespace RenderCadenceCompositor
{ {
namespace namespace
{ {
std::filesystem::path ExecutableDirectory()
{
char path[MAX_PATH] = {};
const DWORD length = GetModuleFileNameA(nullptr, path, static_cast<DWORD>(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::string ReadTextFile(const std::filesystem::path& path, std::string& error)
{ {
std::ifstream input(path, std::ios::binary); 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) bool AppConfigProvider::Load(const std::filesystem::path& path, std::string& error)
{ {
mConfig = DefaultAppConfig(); mConfig = DefaultAppConfig();
@@ -183,4 +205,28 @@ void VideoFormatDimensions(const std::string& formatName, unsigned& width, unsig
width = 1920; width = 1920;
height = 1080; height = 1080;
} }
std::filesystem::path FindConfigFile(const std::filesystem::path& relativePath)
{
std::vector<std::filesystem::path> 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();
}
} }

View File

@@ -12,7 +12,8 @@ class AppConfigProvider
public: public:
AppConfigProvider(); 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); void ApplyCommandLine(int argc, char** argv);
const AppConfig& Config() const { return mConfig; } const AppConfig& Config() const { return mConfig; }
@@ -27,4 +28,5 @@ private:
double FrameDurationMillisecondsFromRateString(const std::string& rateText, double fallbackRate = 59.94); double FrameDurationMillisecondsFromRateString(const std::string& rateText, double fallbackRate = 59.94);
void VideoFormatDimensions(const std::string& formatName, unsigned& width, unsigned& height); void VideoFormatDimensions(const std::string& formatName, unsigned& width, unsigned& height);
std::filesystem::path FindConfigFile(const std::filesystem::path& relativePath = "config/runtime-host.json");
} }

View File

@@ -1,12 +1,10 @@
#pragma once #pragma once
#include "AppConfig.h" #include "AppConfig.h"
#include "AppConfigProvider.h"
#include "../json/JsonWriter.h"
#include "../logging/Logger.h" #include "../logging/Logger.h"
#include "../runtime/RuntimeShaderBridge.h" #include "../runtime/RuntimeShaderBridge.h"
#include "../telemetry/CadenceTelemetryJson.h" #include "../control/RuntimeStateJson.h"
#include "../telemetry/TelemetryPrinter.h" #include "../telemetry/TelemetryHealthMonitor.h"
#include "../video/DeckLinkOutput.h" #include "../video/DeckLinkOutput.h"
#include "../video/DeckLinkOutputThread.h" #include "../video/DeckLinkOutputThread.h"
@@ -55,7 +53,7 @@ public:
mFrameExchange(frameExchange), mFrameExchange(frameExchange),
mConfig(config), mConfig(config),
mOutputThread(mOutput, mFrameExchange, mConfig.outputThread), mOutputThread(mOutput, mFrameExchange, mConfig.outputThread),
mTelemetry(mConfig.telemetry) mTelemetryHealth(mConfig.telemetry)
{ {
} }
@@ -69,18 +67,6 @@ public:
bool Start(std::string& error) 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."); Log("app", "Starting render thread.");
if (!detail::StartRenderThread(mRenderThread, error, 0)) if (!detail::StartRenderThread(mRenderThread, error, 0))
{ {
@@ -99,33 +85,8 @@ public:
return false; return false;
} }
Log("app", "Starting DeckLink output thread."); StartOptionalVideoOutput();
if (!mOutputThread.Start()) mTelemetryHealth.Start(mFrameExchange, mOutput, mOutputThread, mRenderThread);
{
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);
StartHttpServer(); StartHttpServer();
Log("app", "RenderCadenceCompositor started."); Log("app", "RenderCadenceCompositor started.");
mStarted = true; mStarted = true;
@@ -135,7 +96,7 @@ public:
void Stop() void Stop()
{ {
mHttpServer.Stop(); mHttpServer.Stop();
mTelemetry.Stop(); mTelemetryHealth.Stop();
mOutputThread.Stop(); mOutputThread.Stop();
mOutput.Stop(); mOutput.Stop();
StopRuntimeShaderBuild(); StopRuntimeShaderBuild();
@@ -150,6 +111,58 @@ public:
const DeckLinkOutput& Output() const { return mOutput; } const DeckLinkOutput& Output() const { return mOutput; }
private: 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() void StartHttpServer()
{ {
HttpControlServerCallbacks callbacks; HttpControlServerCallbacks callbacks;
@@ -168,65 +181,13 @@ private:
std::string BuildStateJson() std::string BuildStateJson()
{ {
CadenceTelemetrySnapshot telemetry = mHttpTelemetry.Sample(mFrameExchange, mOutput, mOutputThread, mRenderThread); CadenceTelemetrySnapshot telemetry = mHttpTelemetry.Sample(mFrameExchange, mOutput, mOutputThread, mRenderThread);
return RuntimeStateToJson(RuntimeStateJsonInput{
JsonWriter writer; mConfig,
writer.BeginObject(); telemetry,
writer.Key("app"); mHttpServer.Port(),
writer.BeginObject(); mVideoOutputEnabled,
writer.KeyUInt("serverPort", mHttpServer.Port()); mVideoOutputStatus
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<uint64_t>(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();
} }
bool WaitForPreroll() const bool WaitForPreroll() const
@@ -270,10 +231,12 @@ private:
AppConfig mConfig; AppConfig mConfig;
DeckLinkOutput mOutput; DeckLinkOutput mOutput;
DeckLinkOutputThread<SystemFrameExchange> mOutputThread; DeckLinkOutputThread<SystemFrameExchange> mOutputThread;
TelemetryPrinter mTelemetry; TelemetryHealthMonitor mTelemetryHealth;
CadenceTelemetry mHttpTelemetry; CadenceTelemetry mHttpTelemetry;
HttpControlServer mHttpServer; HttpControlServer mHttpServer;
RuntimeShaderBridge mShaderBridge; RuntimeShaderBridge mShaderBridge;
bool mStarted = false; bool mStarted = false;
bool mVideoOutputEnabled = false;
std::string mVideoOutputStatus = "DeckLink output not started.";
}; };
} }

View File

@@ -0,0 +1,108 @@
#pragma once
#include "../app/AppConfig.h"
#include "../app/AppConfigProvider.h"
#include "../json/JsonWriter.h"
#include "../telemetry/CadenceTelemetryJson.h"
#include <cstdint>
#include <string>
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<uint64_t>(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();
}
}

View File

@@ -0,0 +1,116 @@
#pragma once
#include "CadenceTelemetry.h"
#include "../logging/Logger.h"
#include <atomic>
#include <chrono>
#include <sstream>
#include <thread>
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 <typename SystemFrameExchange, typename Output, typename OutputThread, typename RenderThread>
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<bool> mStopping{ false };
std::atomic<bool> mRunning{ false };
};
}

View File

@@ -1,88 +0,0 @@
#pragma once
#include "CadenceTelemetry.h"
#include <atomic>
#include <chrono>
#include <iomanip>
#include <iostream>
#include <thread>
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 <typename SystemFrameExchange, typename Output, typename OutputThread, typename RenderThread>
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<bool> mStopping{ false };
std::atomic<bool> mRunning{ false };
};
}

View File

@@ -104,6 +104,13 @@ apps/RenderCadenceCompositor/
RenderCadenceApp.h RenderCadenceApp.h
AppConfig.cpp AppConfig.cpp
AppConfig.h AppConfig.h
AppConfigProvider.cpp
AppConfigProvider.h
control/
HttpControlServer.cpp
HttpControlServer.h
RuntimeStateJson.h
platform/ platform/
ComInit.cpp ComInit.cpp
@@ -139,8 +146,16 @@ apps/RenderCadenceCompositor/
telemetry/ telemetry/
CadenceTelemetry.cpp CadenceTelemetry.cpp
CadenceTelemetry.h CadenceTelemetry.h
TelemetryPrinter.cpp CadenceTelemetryJson.h
TelemetryPrinter.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: 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 - free/rendering/completed/scheduled slot counts
- actual DeckLink buffered frames - 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 Health events:
renderFps=59.9 scheduleFps=59.9 free=7 completed=1 scheduled=4 drops=0 pboMiss=0 completions=119 late=0 dropped=0 decklinkBuffered=4
``` - warning when DeckLink late/dropped-frame counters increase
- warning when schedule failures increase
- error when app/DeckLink output buffering is starved
## Startup Sequence ## Startup Sequence
@@ -371,24 +388,28 @@ Target first-version startup:
```text ```text
main main
-> parse AppConfig -> load AppConfig through AppConfigProvider
-> initialize COM -> initialize COM
-> DeckLinkOutput discover/select/configure output
-> DeckLinkOutput prepare output schedule
-> create SystemFrameExchange -> create SystemFrameExchange
-> start RenderThread -> start RenderThread
-> wait for completed frame warmup -> wait for completed frame warmup
-> optionally discover/select/configure DeckLink output
-> if DeckLink is available:
-> start DeckLinkOutputThread -> start DeckLinkOutputThread
-> wait for scheduled depth warmup -> wait for scheduled depth warmup
-> DeckLinkOutput start scheduled playback -> DeckLinkOutput start scheduled playback
-> start TelemetryPrinter -> if DeckLink is unavailable:
-> continue without video output
-> start TelemetryHealthMonitor
-> start HttpControlServer
-> wait for Enter -> wait for Enter
``` ```
Shutdown: Shutdown:
```text ```text
stop TelemetryPrinter stop HttpControlServer
stop TelemetryHealthMonitor
stop DeckLinkOutputThread stop DeckLinkOutputThread
DeckLinkOutput stop playback DeckLinkOutput stop playback
stop RenderThread stop RenderThread

View File

@@ -26,6 +26,7 @@ Not allowed on the render thread:
- network/API/OSC handling - network/API/OSC handling
- DeckLink scheduling - DeckLink scheduling
- blocking console logging - 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. 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 - system-memory frame exchange
- DeckLink output scheduling - DeckLink output scheduling
- telemetry - 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. If a file starts coordinating multiple subsystems and doing detailed work for each of them, split it before it becomes the new old app.

View File

@@ -104,6 +104,10 @@ void TestHelpers()
const double duration = FrameDurationMillisecondsFromRateString("50"); const double duration = FrameDurationMillisecondsFromRateString("50");
Expect(duration > 19.9 && duration < 20.1, "frame duration parses numeric rate"); 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");
} }
} }