Clean up pass
This commit is contained in:
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
@@ -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.";
|
||||
};
|
||||
}
|
||||
|
||||
108
apps/RenderCadenceCompositor/control/RuntimeStateJson.h
Normal file
108
apps/RenderCadenceCompositor/control/RuntimeStateJson.h
Normal 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();
|
||||
}
|
||||
}
|
||||
116
apps/RenderCadenceCompositor/telemetry/TelemetryHealthMonitor.h
Normal file
116
apps/RenderCadenceCompositor/telemetry/TelemetryHealthMonitor.h
Normal 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 };
|
||||
};
|
||||
}
|
||||
@@ -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 };
|
||||
};
|
||||
}
|
||||
@@ -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
|
||||
-> optionally discover/select/configure DeckLink output
|
||||
-> if DeckLink is available:
|
||||
-> start DeckLinkOutputThread
|
||||
-> wait for scheduled depth warmup
|
||||
-> DeckLinkOutput start scheduled playback
|
||||
-> start TelemetryPrinter
|
||||
-> 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
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user