diff --git a/README.md b/README.md index ac57e99..f92b47e 100644 --- a/README.md +++ b/README.md @@ -178,6 +178,8 @@ The control UI is available at: http://127.0.0.1: ``` +`/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. diff --git a/docs/CURRENT_SYSTEM_ARCHITECTURE.md b/docs/CURRENT_SYSTEM_ARCHITECTURE.md index 5519e0b..8dd07f7 100644 --- a/docs/CURRENT_SYSTEM_ARCHITECTURE.md +++ b/docs/CURRENT_SYSTEM_ARCHITECTURE.md @@ -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. diff --git a/docs/openapi.yaml b/docs/openapi.yaml index 5027c34..b506e90 100644 --- a/docs/openapi.yaml +++ b/docs/openapi.yaml @@ -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 diff --git a/src/README.md b/src/README.md index 6cc361c..16b1191 100644 --- a/src/README.md +++ b/src/README.md @@ -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 diff --git a/src/control/RuntimeStateJson.h b/src/control/RuntimeStateJson.h index 17d5713..5d2fb73 100644 --- a/src/control/RuntimeStateJson.h +++ b/src/control/RuntimeStateJson.h @@ -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"); diff --git a/tests/RenderCadenceCompositorRuntimeStateJsonTests.cpp b/tests/RenderCadenceCompositorRuntimeStateJsonTests.cpp index 1e742da..407e2dd 100644 --- a/tests/RenderCadenceCompositorRuntimeStateJsonTests.cpp +++ b/tests/RenderCadenceCompositorRuntimeStateJsonTests.cpp @@ -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");