diff --git a/apps/RenderCadenceCompositor/README.md b/apps/RenderCadenceCompositor/README.md index ac5a73a..2db876c 100644 --- a/apps/RenderCadenceCompositor/README.md +++ b/apps/RenderCadenceCompositor/README.md @@ -54,6 +54,7 @@ Included now: - async PBO readback - latest-N system-memory frame exchange - rendered-frame warmup +- eight completed output warmup frames before DeckLink preroll, with DeckLink scheduled depth still targeted at four - background Slang compile of `shaders/happy-accident` - app-owned display/render layer model for shader build readiness - app-owned submission of a completed shader artifact @@ -198,6 +199,7 @@ Currently consumed fields: - `autoReload` - `maxTemporalHistoryFrames` - `previewFps` +- `startupSettleMs` - `enableExternalKeying` The loaded config is treated as a read-only startup snapshot. Subsystems that need config should receive this snapshot or a narrowed config struct from app orchestration; they should not reload files independently. diff --git a/apps/RenderCadenceCompositor/app/AppConfig.cpp b/apps/RenderCadenceCompositor/app/AppConfig.cpp index b9adea7..7aeabb9 100644 --- a/apps/RenderCadenceCompositor/app/AppConfig.cpp +++ b/apps/RenderCadenceCompositor/app/AppConfig.cpp @@ -29,8 +29,9 @@ AppConfig DefaultAppConfig() config.autoReload = true; config.maxTemporalHistoryFrames = 12; config.previewFps = 30.0; - config.warmupCompletedFrames = 4; + config.warmupCompletedFrames = 8; config.warmupTimeout = std::chrono::seconds(3); + config.startupSettle = std::chrono::seconds(5); config.prerollTimeout = std::chrono::seconds(3); config.prerollPoll = std::chrono::milliseconds(2); config.runtimeShaderId = "happy-accident"; diff --git a/apps/RenderCadenceCompositor/app/AppConfig.h b/apps/RenderCadenceCompositor/app/AppConfig.h index 7509760..5acf2d2 100644 --- a/apps/RenderCadenceCompositor/app/AppConfig.h +++ b/apps/RenderCadenceCompositor/app/AppConfig.h @@ -30,8 +30,9 @@ struct AppConfig bool autoReload = true; std::size_t maxTemporalHistoryFrames = 12; double previewFps = 30.0; - std::size_t warmupCompletedFrames = 4; + std::size_t warmupCompletedFrames = 8; std::chrono::milliseconds warmupTimeout = std::chrono::seconds(3); + std::chrono::milliseconds startupSettle = std::chrono::seconds(5); std::chrono::milliseconds prerollTimeout = std::chrono::seconds(3); std::chrono::milliseconds prerollPoll = std::chrono::milliseconds(2); std::string runtimeShaderId = "happy-accident"; diff --git a/apps/RenderCadenceCompositor/app/AppConfigProvider.cpp b/apps/RenderCadenceCompositor/app/AppConfigProvider.cpp index e859c70..bef9c57 100644 --- a/apps/RenderCadenceCompositor/app/AppConfigProvider.cpp +++ b/apps/RenderCadenceCompositor/app/AppConfigProvider.cpp @@ -133,6 +133,9 @@ bool AppConfigProvider::Load(const std::filesystem::path& path, std::string& err ApplySize(root, "maxTemporalHistoryFrames", mConfig.maxTemporalHistoryFrames); ApplyDouble(root, "previewFps", mConfig.previewFps); ApplyBool(root, "enableExternalKeying", mConfig.deckLink.externalKeyingEnabled); + std::size_t startupSettleMilliseconds = static_cast(mConfig.startupSettle.count()); + ApplySize(root, "startupSettleMs", startupSettleMilliseconds); + mConfig.startupSettle = std::chrono::milliseconds(startupSettleMilliseconds); mLoadedFromFile = true; error.clear(); diff --git a/apps/RenderCadenceCompositor/app/RenderCadenceApp.h b/apps/RenderCadenceCompositor/app/RenderCadenceApp.h index 19231af..fa1291c 100644 --- a/apps/RenderCadenceCompositor/app/RenderCadenceApp.h +++ b/apps/RenderCadenceCompositor/app/RenderCadenceApp.h @@ -96,6 +96,20 @@ public: return false; } + if (mConfig.startupSettle > std::chrono::milliseconds::zero()) + { + Log("app", "Settling render cadence before DeckLink output for " + std::to_string(mConfig.startupSettle.count()) + " ms."); + std::this_thread::sleep_for(mConfig.startupSettle); + Log("app", "Waiting for rendered reserve after startup settle."); + if (!mFrameExchange.WaitForCompletedDepth(mConfig.warmupCompletedFrames, mConfig.warmupTimeout)) + { + error = "Timed out waiting for rendered reserve after startup settle."; + LogError("app", error); + Stop(); + return false; + } + } + StartOptionalVideoOutput(); mTelemetryHealth.Start(mFrameExchange, mOutput, mOutputThread, mRenderThread); StartHttpServer(); diff --git a/apps/RenderCadenceCompositor/control/RuntimeStateJson.h b/apps/RenderCadenceCompositor/control/RuntimeStateJson.h index 41dc687..d7b2bb5 100644 --- a/apps/RenderCadenceCompositor/control/RuntimeStateJson.h +++ b/apps/RenderCadenceCompositor/control/RuntimeStateJson.h @@ -251,6 +251,7 @@ inline std::string RuntimeStateToJson(const RuntimeStateJsonInput& input) writer.KeyUInt("maxTemporalHistoryFrames", static_cast(input.config.maxTemporalHistoryFrames)); writer.KeyDouble("previewFps", input.config.previewFps); writer.KeyBool("enableExternalKeying", input.config.deckLink.externalKeyingEnabled); + writer.KeyUInt("startupSettleMs", static_cast(input.config.startupSettle.count())); writer.KeyString("inputVideoFormat", input.config.inputVideoFormat); writer.KeyString("inputFrameRate", input.config.inputFrameRate); writer.KeyString("outputVideoFormat", input.config.outputVideoFormat); diff --git a/apps/RenderCadenceCompositor/telemetry/CadenceTelemetry.h b/apps/RenderCadenceCompositor/telemetry/CadenceTelemetry.h index 2b44070..cd1750c 100644 --- a/apps/RenderCadenceCompositor/telemetry/CadenceTelemetry.h +++ b/apps/RenderCadenceCompositor/telemetry/CadenceTelemetry.h @@ -22,6 +22,8 @@ struct CadenceTelemetrySnapshot uint64_t completions = 0; uint64_t displayedLate = 0; uint64_t dropped = 0; + uint64_t clockOverruns = 0; + uint64_t clockSkippedFrames = 0; uint64_t shaderBuildsCommitted = 0; uint64_t shaderBuildFailures = 0; uint64_t inputFramesReceived = 0; @@ -104,6 +106,8 @@ public: { CadenceTelemetrySnapshot snapshot = Sample(exchange, output, outputThread); const auto renderMetrics = renderThread.GetMetrics(); + snapshot.clockOverruns = renderMetrics.clockOverruns; + snapshot.clockSkippedFrames = renderMetrics.skippedFrames; snapshot.shaderBuildsCommitted = renderMetrics.shaderBuildsCommitted; snapshot.shaderBuildFailures = renderMetrics.shaderBuildFailures; snapshot.inputFramesReceived = renderMetrics.inputFramesReceived; diff --git a/apps/RenderCadenceCompositor/telemetry/CadenceTelemetryJson.h b/apps/RenderCadenceCompositor/telemetry/CadenceTelemetryJson.h index 5f4f153..4b0882c 100644 --- a/apps/RenderCadenceCompositor/telemetry/CadenceTelemetryJson.h +++ b/apps/RenderCadenceCompositor/telemetry/CadenceTelemetryJson.h @@ -24,6 +24,10 @@ inline void WriteCadenceTelemetryJson(JsonWriter& writer, const CadenceTelemetry writer.KeyUInt("completions", snapshot.completions); writer.KeyUInt("late", snapshot.displayedLate); writer.KeyUInt("dropped", snapshot.dropped); + writer.KeyUInt("clockOverruns", snapshot.clockOverruns); + writer.KeyUInt("clockSkippedFrames", snapshot.clockSkippedFrames); + writer.KeyUInt("clockOveruns", snapshot.clockOverruns); + writer.KeyUInt("clockSkipped", snapshot.clockSkippedFrames); writer.KeyUInt("shaderCommitted", snapshot.shaderBuildsCommitted); writer.KeyUInt("shaderFailures", snapshot.shaderBuildFailures); writer.KeyUInt("inputFramesReceived", snapshot.inputFramesReceived); diff --git a/config/runtime-host.json b/config/runtime-host.json index b6944e7..8fad7c5 100644 --- a/config/runtime-host.json +++ b/config/runtime-host.json @@ -11,5 +11,6 @@ "autoReload": true, "maxTemporalHistoryFrames": 12, "previewFps": 30, + "startupSettleMs": 5000, "enableExternalKeying": true } diff --git a/docs/openapi.yaml b/docs/openapi.yaml index 0ac10c1..155b07a 100644 --- a/docs/openapi.yaml +++ b/docs/openapi.yaml @@ -557,6 +557,8 @@ components: type: number previewFps: type: number + startupSettleMs: + type: number enableExternalKeying: type: boolean inputVideoFormat: @@ -654,6 +656,20 @@ components: CadenceTelemetry: type: object properties: + clockOverruns: + type: number + description: Render cadence overruns where the render thread was late enough to skip one or more frame intervals. + clockSkippedFrames: + type: number + description: Total render cadence frame intervals skipped instead of catch-up rendering. + clockOveruns: + type: number + deprecated: true + description: Deprecated misspelled alias for clockOverruns. + clockSkipped: + type: number + deprecated: true + description: Deprecated alias for clockSkippedFrames. inputFramesReceived: type: number inputFramesDropped: diff --git a/tests/RenderCadenceCompositorAppConfigProviderTests.cpp b/tests/RenderCadenceCompositorAppConfigProviderTests.cpp index f1f8b70..7986912 100644 --- a/tests/RenderCadenceCompositorAppConfigProviderTests.cpp +++ b/tests/RenderCadenceCompositorAppConfigProviderTests.cpp @@ -1,5 +1,6 @@ #include "AppConfigProvider.h" +#include #include #include #include @@ -36,6 +37,7 @@ std::filesystem::path WriteConfigFixture() << " \"autoReload\": false,\n" << " \"maxTemporalHistoryFrames\": 8,\n" << " \"previewFps\": 24,\n" + << " \"startupSettleMs\": 2500,\n" << " \"enableExternalKeying\": true\n" << "}\n"; return path; @@ -66,6 +68,7 @@ void TestLoadsRuntimeHostConfig() Expect(!config.autoReload, "auto reload loads"); Expect(config.maxTemporalHistoryFrames == 8, "history length loads"); Expect(config.previewFps == 24.0, "preview fps loads"); + Expect(config.startupSettle == std::chrono::milliseconds(2500), "startup settle loads"); Expect(config.deckLink.externalKeyingEnabled, "external keying loads"); std::filesystem::remove(path); diff --git a/tests/RenderCadenceCompositorTelemetryTests.cpp b/tests/RenderCadenceCompositorTelemetryTests.cpp index d7b8a0e..31b6579 100644 --- a/tests/RenderCadenceCompositorTelemetryTests.cpp +++ b/tests/RenderCadenceCompositorTelemetryTests.cpp @@ -65,6 +65,8 @@ struct FakeOutput struct FakeRenderThreadMetrics { + uint64_t clockOverruns = 0; + uint64_t skippedFrames = 0; uint64_t shaderBuildsCommitted = 0; uint64_t shaderBuildFailures = 0; uint64_t inputFramesReceived = 0; @@ -104,6 +106,8 @@ void TestTelemetrySamplesCompletedPollMissesAndShaderCounts() outputThread.metrics.scheduleFailures = 0; FakeRenderThread renderThread; + renderThread.metrics.clockOverruns = 5; + renderThread.metrics.skippedFrames = 8; renderThread.metrics.shaderBuildsCommitted = 1; renderThread.metrics.shaderBuildFailures = 0; renderThread.metrics.inputFramesReceived = 9; @@ -122,6 +126,8 @@ void TestTelemetrySamplesCompletedPollMissesAndShaderCounts() Expect(snapshot.completedFrames == 1, "completed frame count is sampled"); Expect(snapshot.scheduledFrames == 4, "scheduled frame count is sampled"); Expect(snapshot.completedPollMisses == 12, "completed poll misses are sampled"); + Expect(snapshot.clockOverruns == 5, "clock overrun count is sampled"); + Expect(snapshot.clockSkippedFrames == 8, "clock skipped frame count is sampled"); Expect(snapshot.shaderBuildsCommitted == 1, "shader committed count is sampled"); Expect(snapshot.shaderBuildFailures == 0, "shader failure count is sampled"); Expect(snapshot.inputFramesReceived == 9, "input received count is sampled"); @@ -176,6 +182,8 @@ void TestTelemetrySerializesToJson() snapshot.completions = 117; snapshot.displayedLate = 1; snapshot.dropped = 2; + snapshot.clockOverruns = 3; + snapshot.clockSkippedFrames = 5; snapshot.shaderBuildsCommitted = 1; snapshot.shaderBuildFailures = 0; snapshot.inputFramesReceived = 10; @@ -206,6 +214,8 @@ void TestTelemetrySerializesToJson() "\"renderedTotal\":120,\"scheduledTotal\":118," "\"completedPollMisses\":3,\"scheduleFailures\":0," "\"completions\":117,\"late\":1,\"dropped\":2," + "\"clockOverruns\":3,\"clockSkippedFrames\":5," + "\"clockOveruns\":3,\"clockSkipped\":5," "\"shaderCommitted\":1,\"shaderFailures\":0," "\"inputFramesReceived\":10,\"inputFramesDropped\":1," "\"inputConsumeMisses\":2,\"inputUploadMisses\":3,"