Generic telemetry
Some checks failed
CI / React UI Build (push) Successful in 10s
CI / Native Windows Build And Tests (push) Failing after 2m30s
CI / Windows Release Package (push) Has been skipped

This commit is contained in:
2026-05-22 15:32:55 +10:00
parent 64a6125c3f
commit e6faaee1ca
6 changed files with 119 additions and 2 deletions

View File

@@ -178,6 +178,8 @@ The control UI is available at:
http://127.0.0.1:<serverPort>
```
`/api/state` exposes backend-neutral output telemetry in `videoOutput`. Use `videoOutput.enabled`, `videoOutput.backend`, and `videoOutput.scheduleFailures` for portable status. Backend-specific counters live in `videoOutput.backendMetrics`.
## Runtime State
The current layer stack is autosaved to `runtime/runtime_state.json` whenever durable UI/API layer changes are accepted: add/remove, shader assignment, bypass state, ordering, parameter updates, parameter reset, and reload compatibility refreshes. Saves are debounced and written on a background worker, with a final flush during shutdown.

View File

@@ -139,6 +139,8 @@ The input edge writes CPU frames into `InputFrameMailbox`. The current DeckLink
The output edge consumes completed system-memory frames from `SystemFrameExchange`. The current DeckLink backend schedules those frames to DeckLink. If video output is unavailable, the app continues running render cadence, control, preview, telemetry, and logging.
Runtime state exposes backend-neutral output telemetry through `videoOutput`. Portable fields such as `enabled`, `backend`, and `scheduleFailures` stay at that level; backend-specific counters live under `videoOutput.backendMetrics`.
`PreviewWindowThread` is optional and uses a non-consuming system-memory tap. It paints with Win32/GDI on its own thread and skips preview ticks instead of blocking the frame exchange.
Screenshot routes are present in the UI/OpenAPI surface but are not implemented in the current native command path yet.

View File

@@ -520,6 +520,8 @@ components:
$ref: "#/components/schemas/RuntimeStatus"
video:
$ref: "#/components/schemas/VideoStatus"
videoOutput:
$ref: "#/components/schemas/VideoOutputStatus"
decklink:
$ref: "#/components/schemas/DeckLinkStatus"
videoIO:
@@ -561,6 +563,12 @@ components:
type: number
enableExternalKeying:
type: boolean
videoInputBackend:
type: string
enum: [decklink, none]
videoOutputBackend:
type: string
enum: [decklink, none]
inputVideoFormat:
type: string
inputFrameRate:
@@ -589,6 +597,52 @@ components:
type: number
modeName:
type: string
VideoOutputStatus:
type: object
description: Backend-neutral output telemetry. Backend-specific counters live under `backendMetrics`.
properties:
enabled:
type: boolean
backend:
type: string
example: decklink
statusMessage:
type: string
scheduleFailures:
type: number
completions:
type: number
late:
type: number
dropped:
type: number
backendMetrics:
$ref: "#/components/schemas/VideoOutputBackendMetrics"
VideoOutputBackendMetrics:
type: object
description: Backend-specific output metrics. For `decklink`, this contains DeckLink schedule and buffer telemetry.
additionalProperties: true
properties:
bufferedAvailable:
type: boolean
buffered:
type: number
nullable: true
scheduleCallMs:
type: number
scheduleLeadAvailable:
type: boolean
scheduleLeadFrames:
type: number
nullable: true
playbackFrameIndex:
type: number
nextScheduleFrameIndex:
type: number
playbackStreamTime:
type: number
scheduleRealignments:
type: number
DeckLinkStatus:
type: object
deprecated: true

View File

@@ -262,7 +262,7 @@ Startup order is:
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`.
`/api/state` reports backend-neutral output status in `videoOutput`. Portable fields live at `videoOutput.enabled`, `videoOutput.backend`, and `videoOutput.scheduleFailures`; backend-specific counters such as DeckLink buffered depth and schedule lead live under `videoOutput.backendMetrics`. The older `videoIO` and `decklink` status objects are retained for compatibility while clients migrate.
## Optional DeckLink Input

View File

@@ -27,7 +27,7 @@ struct RuntimeStateJsonInput
inline void WriteVideoIoStatusJson(JsonWriter& writer, const RuntimeStateJsonInput& input)
{
writer.BeginObject();
writer.KeyString("backend", "decklink");
writer.KeyString("backend", input.config.videoOutputBackend);
writer.KeyNull("modelName");
writer.KeyBool("supportsInternalKeying", false);
writer.KeyBool("supportsExternalKeying", false);
@@ -38,6 +38,47 @@ inline void WriteVideoIoStatusJson(JsonWriter& writer, const RuntimeStateJsonInp
writer.EndObject();
}
inline void WriteVideoOutputBackendMetricsJson(JsonWriter& writer, const RuntimeStateJsonInput& input)
{
writer.BeginObject();
if (input.config.videoOutputBackend == "decklink")
{
writer.KeyBool("bufferedAvailable", input.telemetry.deckLinkBufferedAvailable);
writer.Key("buffered");
if (input.telemetry.deckLinkBufferedAvailable)
writer.UInt(input.telemetry.deckLinkBuffered);
else
writer.Null();
writer.KeyDouble("scheduleCallMs", input.telemetry.deckLinkScheduleCallMilliseconds);
writer.KeyBool("scheduleLeadAvailable", input.telemetry.deckLinkScheduleLeadAvailable);
writer.Key("scheduleLeadFrames");
if (input.telemetry.deckLinkScheduleLeadAvailable)
writer.Int(input.telemetry.deckLinkScheduleLeadFrames);
else
writer.Null();
writer.KeyUInt("playbackFrameIndex", input.telemetry.deckLinkPlaybackFrameIndex);
writer.KeyUInt("nextScheduleFrameIndex", input.telemetry.deckLinkNextScheduleFrameIndex);
writer.KeyInt("playbackStreamTime", input.telemetry.deckLinkPlaybackStreamTime);
writer.KeyUInt("scheduleRealignments", input.telemetry.deckLinkScheduleRealignments);
}
writer.EndObject();
}
inline void WriteVideoOutputTelemetryJson(JsonWriter& writer, const RuntimeStateJsonInput& input)
{
writer.BeginObject();
writer.KeyBool("enabled", input.videoOutputEnabled);
writer.KeyString("backend", input.config.videoOutputBackend);
writer.KeyString("statusMessage", input.videoOutputStatus);
writer.KeyUInt("scheduleFailures", input.telemetry.scheduleFailures);
writer.KeyUInt("completions", input.telemetry.completions);
writer.KeyUInt("late", input.telemetry.displayedLate);
writer.KeyUInt("dropped", input.telemetry.dropped);
writer.Key("backendMetrics");
WriteVideoOutputBackendMetricsJson(writer, input);
writer.EndObject();
}
inline void OutputDimensions(const RuntimeStateJsonInput& input, unsigned& width, unsigned& height)
{
VideoFormatDimensions(input.config.outputVideoFormat, width, height);
@@ -277,6 +318,8 @@ inline std::string RuntimeStateToJson(const RuntimeStateJsonInput& input)
writer.KeyString("modeName", input.config.outputVideoFormat + " output-only");
writer.EndObject();
writer.Key("videoOutput");
WriteVideoOutputTelemetryJson(writer, input);
writer.Key("decklink");
WriteVideoIoStatusJson(writer, input);
writer.Key("videoIO");

View File

@@ -50,7 +50,19 @@ int main()
telemetry.completedReadbackCopyMilliseconds = 1.2;
telemetry.completedDrops = 3;
telemetry.acquireMisses = 4;
telemetry.scheduleFailures = 2;
telemetry.completions = 12;
telemetry.displayedLate = 1;
telemetry.shaderBuildsCommitted = 1;
telemetry.deckLinkBufferedAvailable = true;
telemetry.deckLinkBuffered = 4;
telemetry.deckLinkScheduleCallMilliseconds = 1.25;
telemetry.deckLinkScheduleLeadAvailable = true;
telemetry.deckLinkScheduleLeadFrames = 4;
telemetry.deckLinkPlaybackFrameIndex = 10;
telemetry.deckLinkNextScheduleFrameIndex = 14;
telemetry.deckLinkPlaybackStreamTime = 10010;
telemetry.deckLinkScheduleRealignments = 1;
const std::filesystem::path root = MakeTestRoot();
WriteFile(root / "solid-color" / "shader.slang", "float4 shadeVideo(float2 uv) { return float4(uv, 0.0, 1.0); }\n");
@@ -107,6 +119,10 @@ int main()
ExpectContains(json, "\"height\":1080", "state JSON should expose output height");
ExpectContains(json, "\"videoInputBackend\":\"decklink\"", "state JSON should expose input backend");
ExpectContains(json, "\"videoOutputBackend\":\"decklink\"", "state JSON should expose output backend");
ExpectContains(json, "\"videoOutput\":{\"enabled\":true,\"backend\":\"decklink\"", "state JSON should expose neutral video output status");
ExpectContains(json, "\"scheduleFailures\":2", "state JSON should expose neutral video output schedule failures");
ExpectContains(json, "\"backendMetrics\":{\"bufferedAvailable\":true,\"buffered\":4", "state JSON should expose backend-specific video output metrics");
ExpectContains(json, "\"scheduleLeadFrames\":4", "state JSON should expose backend-specific schedule lead");
ExpectContains(json, "\"renderMs\":2.5", "state JSON should expose top-level render timing");
ExpectContains(json, "\"budgetUsedPercent\":15", "state JSON should expose top-level render budget percentage");
ExpectContains(json, "\"renderFrameMs\":2.5", "state JSON should expose cadence render timing");