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}/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"

View File

@@ -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

View File

@@ -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);

View File

@@ -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";

View File

@@ -7,11 +7,22 @@
#include <cstdlib>
#include <fstream>
#include <sstream>
#include <vector>
#include <windows.h>
namespace RenderCadenceCompositor
{
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::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<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:
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");
}

View File

@@ -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<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();
return RuntimeStateToJson(RuntimeStateJsonInput{
mConfig,
telemetry,
mHttpServer.Port(),
mVideoOutputEnabled,
mVideoOutputStatus
});
}
bool WaitForPreroll() const
@@ -270,10 +231,12 @@ private:
AppConfig mConfig;
DeckLinkOutput mOutput;
DeckLinkOutputThread<SystemFrameExchange> mOutputThread;
TelemetryPrinter mTelemetry;
TelemetryHealthMonitor mTelemetryHealth;
CadenceTelemetry mHttpTelemetry;
HttpControlServer mHttpServer;
RuntimeShaderBridge mShaderBridge;
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
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

View File

@@ -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.

View File

@@ -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");
}
}