From c0d7e844954563087807dcc2e5b59a8a489f86f4 Mon Sep 17 00:00:00 2001 From: Aiden <68633820+awils27@users.noreply.github.com> Date: Tue, 12 May 2026 02:15:03 +1000 Subject: [PATCH] Shader ownership change --- CMakeLists.txt | 29 ++++ apps/RenderCadenceCompositor/README.md | 7 +- .../app/RenderCadenceApp.h | 39 +++++ .../render/RenderThread.cpp | 43 ++++-- .../render/RenderThread.h | 9 +- .../runtime/RuntimeShaderArtifact.h | 10 ++ .../runtime/RuntimeSlangShaderCompiler.cpp | 7 +- .../runtime/RuntimeSlangShaderCompiler.h | 5 +- .../telemetry/CadenceTelemetry.h | 13 +- .../telemetry/TelemetryPrinter.h | 4 +- tests/RenderCadenceCompositorClockTests.cpp | 94 +++++++++++ .../RenderCadenceCompositorTelemetryTests.cpp | 146 ++++++++++++++++++ 12 files changed, 370 insertions(+), 36 deletions(-) create mode 100644 apps/RenderCadenceCompositor/runtime/RuntimeShaderArtifact.h create mode 100644 tests/RenderCadenceCompositorClockTests.cpp create mode 100644 tests/RenderCadenceCompositorTelemetryTests.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index d91e680..4ce6f46 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -745,6 +745,35 @@ endif() add_test(NAME RenderCadenceCompositorFrameExchangeTests COMMAND RenderCadenceCompositorFrameExchangeTests) +add_executable(RenderCadenceCompositorClockTests + "${RENDER_CADENCE_APP_DIR}/render/RenderCadenceClock.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/tests/RenderCadenceCompositorClockTests.cpp" +) + +target_include_directories(RenderCadenceCompositorClockTests PRIVATE + "${RENDER_CADENCE_APP_DIR}/render" +) + +if(MSVC) + target_compile_options(RenderCadenceCompositorClockTests PRIVATE /W3) +endif() + +add_test(NAME RenderCadenceCompositorClockTests COMMAND RenderCadenceCompositorClockTests) + +add_executable(RenderCadenceCompositorTelemetryTests + "${CMAKE_CURRENT_SOURCE_DIR}/tests/RenderCadenceCompositorTelemetryTests.cpp" +) + +target_include_directories(RenderCadenceCompositorTelemetryTests PRIVATE + "${RENDER_CADENCE_APP_DIR}/telemetry" +) + +if(MSVC) + target_compile_options(RenderCadenceCompositorTelemetryTests PRIVATE /W3) +endif() + +add_test(NAME RenderCadenceCompositorTelemetryTests COMMAND RenderCadenceCompositorTelemetryTests) + add_executable(SystemOutputFramePoolTests "${APP_DIR}/videoio/SystemOutputFramePool.cpp" "${APP_DIR}/videoio/VideoIOFormat.cpp" diff --git a/apps/RenderCadenceCompositor/README.md b/apps/RenderCadenceCompositor/README.md index 5a58821..3a2a8ed 100644 --- a/apps/RenderCadenceCompositor/README.md +++ b/apps/RenderCadenceCompositor/README.md @@ -38,7 +38,8 @@ Included now: - latest-N system-memory frame exchange - rendered-frame warmup - background Slang compile of `shaders/happy-accident` -- render-thread-only GL commit once compiled shader source is ready +- app-owned submission of a completed shader artifact +- render-thread-only GL commit once the artifact is ready - compact telemetry - non-GL frame-exchange tests @@ -106,9 +107,9 @@ Healthy first-run signs: ## Runtime Slang Shader Test -On startup the app begins compiling `shaders/happy-accident` on a background thread. +On startup the app begins compiling `shaders/happy-accident` on a background thread owned by the app orchestration layer. -The render thread keeps drawing the simple motion renderer while Slang compiles. It only attempts the OpenGL shader compile/link once a complete GLSL fragment shader is ready. If either the Slang build or GL commit fails, the app keeps rendering the simple motion fallback. +The render thread keeps drawing the simple motion renderer while Slang compiles. It does not choose packages, launch Slang, or track build lifecycle. It only receives a completed shader artifact and attempts the OpenGL shader compile/link at a frame boundary. If either the Slang build or GL commit fails, the app keeps rendering the simple motion fallback. Successful handoff signs: diff --git a/apps/RenderCadenceCompositor/app/RenderCadenceApp.h b/apps/RenderCadenceCompositor/app/RenderCadenceApp.h index 9a7c3d1..078f9f6 100644 --- a/apps/RenderCadenceCompositor/app/RenderCadenceApp.h +++ b/apps/RenderCadenceCompositor/app/RenderCadenceApp.h @@ -1,11 +1,13 @@ #pragma once #include "AppConfig.h" +#include "../runtime/RuntimeSlangShaderCompiler.h" #include "../telemetry/TelemetryPrinter.h" #include "../video/DeckLinkOutput.h" #include "../video/DeckLinkOutputThread.h" #include +#include #include #include #include @@ -78,6 +80,7 @@ public: Stop(); return false; } + StartRuntimeShaderBuild(); if (!mFrameExchange.WaitForCompletedDepth(mConfig.warmupCompletedFrames, mConfig.warmupTimeout)) { @@ -116,6 +119,7 @@ public: mTelemetry.Stop(); mOutputThread.Stop(); mOutput.Stop(); + StopRuntimeShaderBuild(); mRenderThread.Stop(); mOutput.ReleaseResources(); mStarted = false; @@ -137,12 +141,47 @@ private: return false; } + void StartRuntimeShaderBuild() + { + mShaderCompiler.StartHappyAccidentBuild(); + mShaderBridgeStopping = false; + mShaderBridgeThread = std::thread([this]() { ShaderBridgeMain(); }); + } + + void StopRuntimeShaderBuild() + { + mShaderBridgeStopping = true; + if (mShaderBridgeThread.joinable()) + mShaderBridgeThread.join(); + mShaderCompiler.Stop(); + } + + void ShaderBridgeMain() + { + while (!mShaderBridgeStopping) + { + RuntimeSlangShaderBuild build; + if (mShaderCompiler.TryConsume(build)) + { + if (build.succeeded) + mRenderThread.SubmitRuntimeShaderArtifact(build.artifact); + else + std::cout << "Runtime Slang build failed: " << build.message << "\n"; + return; + } + std::this_thread::sleep_for(std::chrono::milliseconds(5)); + } + } + RenderThread& mRenderThread; SystemFrameExchange& mFrameExchange; AppConfig mConfig; DeckLinkOutput mOutput; DeckLinkOutputThread mOutputThread; TelemetryPrinter mTelemetry; + RuntimeSlangShaderCompiler mShaderCompiler; + std::thread mShaderBridgeThread; + std::atomic mShaderBridgeStopping{ false }; bool mStarted = false; }; } diff --git a/apps/RenderCadenceCompositor/render/RenderThread.cpp b/apps/RenderCadenceCompositor/render/RenderThread.cpp index 6765104..08fe762 100644 --- a/apps/RenderCadenceCompositor/render/RenderThread.cpp +++ b/apps/RenderCadenceCompositor/render/RenderThread.cpp @@ -93,7 +93,6 @@ void RenderThread::ThreadMain() SignalStartupFailure("Render pipeline initialization failed."); return; } - mSlangCompiler.StartHappyAccidentBuild(); RenderCadenceClock clock(mConfig.frameDurationMilliseconds); uint64_t frameIndex = 0; @@ -156,7 +155,6 @@ void RenderThread::ThreadMain() readback.Shutdown(); runtimeShaderRenderer.ShutdownGl(); renderer.ShutdownGl(); - mSlangCompiler.Stop(); window.ClearCurrent(); mRunning.store(false, std::memory_order_release); } @@ -193,23 +191,36 @@ void RenderThread::CountAcquireMiss() ++mMetrics.acquireMisses; } +void RenderThread::SubmitRuntimeShaderArtifact(const RuntimeShaderArtifact& artifact) +{ + if (artifact.fragmentShaderSource.empty()) + return; + + std::lock_guard lock(mShaderArtifactMutex); + mPendingShaderArtifact = artifact; + mHasPendingShaderArtifact = true; +} + +bool RenderThread::TryTakePendingRuntimeShaderArtifact(RuntimeShaderArtifact& artifact) +{ + std::lock_guard lock(mShaderArtifactMutex); + if (!mHasPendingShaderArtifact) + return false; + + artifact = std::move(mPendingShaderArtifact); + mPendingShaderArtifact = RuntimeShaderArtifact(); + mHasPendingShaderArtifact = false; + return true; +} + void RenderThread::TryCommitReadyRuntimeShader(RuntimeShaderRenderer& runtimeShaderRenderer) { - RuntimeSlangShaderBuild build; - if (!mSlangCompiler.TryConsume(build)) + RuntimeShaderArtifact artifact; + if (!TryTakePendingRuntimeShaderArtifact(artifact)) return; - if (!build.succeeded) - { - std::cout << "Runtime Slang build failed: " << build.message << "\n"; - OutputDebugStringA(("Runtime Slang build failed: " + build.message + "\n").c_str()); - std::lock_guard lock(mMetricsMutex); - ++mMetrics.shaderBuildFailures; - return; - } - std::string commitError; - if (!runtimeShaderRenderer.CommitFragmentShader(build.fragmentShaderSource, commitError)) + if (!runtimeShaderRenderer.CommitFragmentShader(artifact.fragmentShaderSource, commitError)) { std::cout << "Runtime shader GL commit failed: " << commitError << "\n"; OutputDebugStringA(("Runtime shader GL commit failed: " + commitError + "\n").c_str()); @@ -218,8 +229,8 @@ void RenderThread::TryCommitReadyRuntimeShader(RuntimeShaderRenderer& runtimeSha return; } - std::cout << "Runtime shader committed: " << build.shaderId << ". " << build.message << "\n"; - OutputDebugStringA(("Runtime shader committed: " + build.shaderId + ". " + build.message + "\n").c_str()); + std::cout << "Runtime shader committed: " << artifact.shaderId << ". " << artifact.message << "\n"; + OutputDebugStringA(("Runtime shader committed: " + artifact.shaderId + ". " + artifact.message + "\n").c_str()); std::lock_guard lock(mMetricsMutex); ++mMetrics.shaderBuildsCommitted; } diff --git a/apps/RenderCadenceCompositor/render/RenderThread.h b/apps/RenderCadenceCompositor/render/RenderThread.h index 6e49d07..0d05c47 100644 --- a/apps/RenderCadenceCompositor/render/RenderThread.h +++ b/apps/RenderCadenceCompositor/render/RenderThread.h @@ -1,7 +1,7 @@ #pragma once #include "RenderCadenceClock.h" -#include "../runtime/RuntimeSlangShaderCompiler.h" +#include "../runtime/RuntimeShaderArtifact.h" #include "RuntimeShaderRenderer.h" #include @@ -44,6 +44,7 @@ public: bool Start(std::string& error); void Stop(); + void SubmitRuntimeShaderArtifact(const RuntimeShaderArtifact& artifact); Metrics GetMetrics() const; bool IsRunning() const { return mRunning.load(std::memory_order_acquire); } @@ -56,10 +57,10 @@ private: void CountCompleted(); void CountAcquireMiss(); void TryCommitReadyRuntimeShader(RuntimeShaderRenderer& runtimeShaderRenderer); + bool TryTakePendingRuntimeShaderArtifact(RuntimeShaderArtifact& artifact); SystemFrameExchange& mFrameExchange; Config mConfig; - RuntimeSlangShaderCompiler mSlangCompiler; std::thread mThread; std::atomic mStopping{ false }; std::atomic mRunning{ false }; @@ -71,4 +72,8 @@ private: mutable std::mutex mMetricsMutex; Metrics mMetrics; + + std::mutex mShaderArtifactMutex; + bool mHasPendingShaderArtifact = false; + RuntimeShaderArtifact mPendingShaderArtifact; }; diff --git a/apps/RenderCadenceCompositor/runtime/RuntimeShaderArtifact.h b/apps/RenderCadenceCompositor/runtime/RuntimeShaderArtifact.h new file mode 100644 index 0000000..69c8ba3 --- /dev/null +++ b/apps/RenderCadenceCompositor/runtime/RuntimeShaderArtifact.h @@ -0,0 +1,10 @@ +#pragma once + +#include + +struct RuntimeShaderArtifact +{ + std::string shaderId; + std::string fragmentShaderSource; + std::string message; +}; diff --git a/apps/RenderCadenceCompositor/runtime/RuntimeSlangShaderCompiler.cpp b/apps/RenderCadenceCompositor/runtime/RuntimeSlangShaderCompiler.cpp index cc1a992..23860fd 100644 --- a/apps/RenderCadenceCompositor/runtime/RuntimeSlangShaderCompiler.cpp +++ b/apps/RenderCadenceCompositor/runtime/RuntimeSlangShaderCompiler.cpp @@ -89,7 +89,7 @@ bool RuntimeSlangShaderCompiler::TryConsume(RuntimeSlangShaderBuild& build) RuntimeSlangShaderBuild RuntimeSlangShaderCompiler::BuildHappyAccident() const { RuntimeSlangShaderBuild build; - build.shaderId = "happy-accident"; + build.artifact.shaderId = "happy-accident"; try { @@ -126,7 +126,7 @@ RuntimeSlangShaderBuild RuntimeSlangShaderCompiler::BuildHappyAccident() const std::string error; const auto start = std::chrono::steady_clock::now(); - if (!compiler.BuildPassFragmentShaderSource(shaderPackage, pass, build.fragmentShaderSource, error)) + if (!compiler.BuildPassFragmentShaderSource(shaderPackage, pass, build.artifact.fragmentShaderSource, error)) { build.succeeded = false; build.message = error.empty() ? "Happy Accident Slang compile failed." : error; @@ -136,7 +136,8 @@ RuntimeSlangShaderBuild RuntimeSlangShaderCompiler::BuildHappyAccident() const const auto end = std::chrono::steady_clock::now(); const double milliseconds = std::chrono::duration_cast>(end - start).count(); build.succeeded = true; - build.message = "Happy Accident Slang compile completed in " + std::to_string(milliseconds) + " ms."; + build.artifact.message = "Happy Accident Slang compile completed in " + std::to_string(milliseconds) + " ms."; + build.message = build.artifact.message; return build; } catch (const std::exception& exception) diff --git a/apps/RenderCadenceCompositor/runtime/RuntimeSlangShaderCompiler.h b/apps/RenderCadenceCompositor/runtime/RuntimeSlangShaderCompiler.h index 202eb97..85dab96 100644 --- a/apps/RenderCadenceCompositor/runtime/RuntimeSlangShaderCompiler.h +++ b/apps/RenderCadenceCompositor/runtime/RuntimeSlangShaderCompiler.h @@ -1,5 +1,7 @@ #pragma once +#include "RuntimeShaderArtifact.h" + #include #include #include @@ -9,8 +11,7 @@ struct RuntimeSlangShaderBuild { bool available = false; bool succeeded = false; - std::string shaderId; - std::string fragmentShaderSource; + RuntimeShaderArtifact artifact; std::string message; }; diff --git a/apps/RenderCadenceCompositor/telemetry/CadenceTelemetry.h b/apps/RenderCadenceCompositor/telemetry/CadenceTelemetry.h index 0b2ee34..6ff08e4 100644 --- a/apps/RenderCadenceCompositor/telemetry/CadenceTelemetry.h +++ b/apps/RenderCadenceCompositor/telemetry/CadenceTelemetry.h @@ -1,8 +1,5 @@ #pragma once -#include "../video/DeckLinkOutput.h" -#include "../video/DeckLinkOutputThread.h" - #include #include #include @@ -34,10 +31,10 @@ struct CadenceTelemetrySnapshot class CadenceTelemetry { public: - template + template CadenceTelemetrySnapshot Sample( const SystemFrameExchange& exchange, - const DeckLinkOutput& output, + const Output& output, const OutputThread& outputThread) { const auto now = Clock::now(); @@ -46,7 +43,7 @@ public: : 0.0; const auto exchangeMetrics = exchange.Metrics(); - const DeckLinkOutputMetrics outputMetrics = output.Metrics(); + const auto outputMetrics = output.Metrics(); const auto threadMetrics = outputThread.Metrics(); CadenceTelemetrySnapshot snapshot; @@ -80,10 +77,10 @@ public: return snapshot; } - template + template CadenceTelemetrySnapshot Sample( const SystemFrameExchange& exchange, - const DeckLinkOutput& output, + const Output& output, const OutputThread& outputThread, const RenderThread& renderThread) { diff --git a/apps/RenderCadenceCompositor/telemetry/TelemetryPrinter.h b/apps/RenderCadenceCompositor/telemetry/TelemetryPrinter.h index 6582f24..f73bbe1 100644 --- a/apps/RenderCadenceCompositor/telemetry/TelemetryPrinter.h +++ b/apps/RenderCadenceCompositor/telemetry/TelemetryPrinter.h @@ -31,8 +31,8 @@ public: Stop(); } - template - void Start(const SystemFrameExchange& exchange, const DeckLinkOutput& output, const OutputThread& outputThread, const RenderThread& renderThread) + template + void Start(const SystemFrameExchange& exchange, const Output& output, const OutputThread& outputThread, const RenderThread& renderThread) { if (mRunning) return; diff --git a/tests/RenderCadenceCompositorClockTests.cpp b/tests/RenderCadenceCompositorClockTests.cpp new file mode 100644 index 0000000..e72d8ed --- /dev/null +++ b/tests/RenderCadenceCompositorClockTests.cpp @@ -0,0 +1,94 @@ +#include "RenderCadenceClock.h" + +#include +#include + +namespace +{ +int gFailures = 0; + +void Expect(bool condition, const char* message) +{ + if (condition) + return; + + std::cerr << "FAIL: " << message << "\n"; + ++gFailures; +} + +void TestEarlyPollWaitsWithoutAdvancing() +{ + using Clock = RenderCadenceClock::Clock; + RenderCadenceClock cadence(16.0); + const auto start = Clock::now(); + cadence.Reset(start); + + const auto tick = cadence.Poll(start - std::chrono::milliseconds(1)); + Expect(!tick.due, "early poll is not due"); + Expect(tick.sleepFor > RenderCadenceClock::Duration::zero(), "early poll returns a sleep duration"); + Expect(cadence.OverrunCount() == 0, "early poll does not count overrun"); + Expect(cadence.SkippedFrameCount() == 0, "early poll does not skip frames"); +} + +void TestDuePollRendersWithoutSkipping() +{ + using Clock = RenderCadenceClock::Clock; + RenderCadenceClock cadence(16.0); + const auto start = Clock::now(); + cadence.Reset(start); + + const auto tick = cadence.Poll(start); + Expect(tick.due, "exact cadence time is due"); + Expect(tick.skippedFrames == 0, "exact cadence time skips no frames"); + Expect(cadence.OverrunCount() == 0, "exact cadence time is not an overrun"); + + cadence.MarkRendered(start); + Expect(cadence.NextRenderTime() > start, "mark rendered advances next render time"); +} + +void TestLatePollRecordsSkippedFrames() +{ + using Clock = RenderCadenceClock::Clock; + RenderCadenceClock cadence(10.0); + const auto start = Clock::now(); + cadence.Reset(start); + + const auto tick = cadence.Poll(start + std::chrono::milliseconds(35)); + Expect(tick.due, "late poll is due"); + Expect(tick.skippedFrames == 3, "late poll records skipped frame intervals"); + Expect(cadence.OverrunCount() == 1, "late poll records overrun"); + Expect(cadence.SkippedFrameCount() == 3, "late poll accumulates skipped frames"); +} + +void TestMarkRenderedRebasesAfterLargeStall() +{ + using Clock = RenderCadenceClock::Clock; + RenderCadenceClock cadence(10.0); + const auto start = Clock::now(); + cadence.Reset(start); + + const auto stalled = start + std::chrono::milliseconds(100); + cadence.MarkRendered(stalled); + + const auto next = cadence.NextRenderTime(); + Expect(next > stalled, "large stall rebases next render time after now"); + Expect(next - stalled <= std::chrono::milliseconds(11), "large stall rebases to roughly one frame ahead"); +} +} + +int main() +{ + TestEarlyPollWaitsWithoutAdvancing(); + TestDuePollRendersWithoutSkipping(); + TestLatePollRecordsSkippedFrames(); + TestMarkRenderedRebasesAfterLargeStall(); + + if (gFailures != 0) + { + std::cerr << gFailures << " cadence clock test failure(s).\n"; + return 1; + } + + std::cout << "RenderCadenceCompositor cadence clock tests passed.\n"; + return 0; +} diff --git a/tests/RenderCadenceCompositorTelemetryTests.cpp b/tests/RenderCadenceCompositorTelemetryTests.cpp new file mode 100644 index 0000000..2fa0756 --- /dev/null +++ b/tests/RenderCadenceCompositorTelemetryTests.cpp @@ -0,0 +1,146 @@ +#include "CadenceTelemetry.h" + +#include +#include +#include +#include + +namespace +{ +int gFailures = 0; + +void Expect(bool condition, const char* message) +{ + if (condition) + return; + + std::cerr << "FAIL: " << message << "\n"; + ++gFailures; +} + +struct FakeExchangeMetrics +{ + std::size_t freeCount = 0; + std::size_t completedCount = 0; + std::size_t scheduledCount = 0; + uint64_t completedFrames = 0; + uint64_t scheduledFrames = 0; +}; + +struct FakeExchange +{ + FakeExchangeMetrics metrics; + FakeExchangeMetrics Metrics() const { return metrics; } +}; + +struct FakeOutputThreadMetrics +{ + uint64_t completedPollMisses = 0; + uint64_t scheduleFailures = 0; +}; + +struct FakeOutputThread +{ + FakeOutputThreadMetrics metrics; + FakeOutputThreadMetrics Metrics() const { return metrics; } +}; + +struct FakeOutputMetrics +{ + uint64_t completions = 0; + uint64_t displayedLate = 0; + uint64_t dropped = 0; + uint64_t scheduleFailures = 0; + bool actualBufferedFramesAvailable = false; + uint64_t actualBufferedFrames = 0; + double scheduleCallMilliseconds = 0.0; +}; + +struct FakeOutput +{ + FakeOutputMetrics metrics; + FakeOutputMetrics Metrics() const { return metrics; } +}; + +struct FakeRenderThreadMetrics +{ + uint64_t shaderBuildsCommitted = 0; + uint64_t shaderBuildFailures = 0; +}; + +struct FakeRenderThread +{ + FakeRenderThreadMetrics metrics; + FakeRenderThreadMetrics GetMetrics() const { return metrics; } +}; + +void TestTelemetrySamplesCompletedPollMissesAndShaderCounts() +{ + RenderCadenceCompositor::CadenceTelemetry telemetry; + FakeExchange exchange; + exchange.metrics.freeCount = 7; + exchange.metrics.completedCount = 1; + exchange.metrics.scheduledCount = 4; + exchange.metrics.completedFrames = 100; + exchange.metrics.scheduledFrames = 96; + + FakeOutput output; + output.metrics.actualBufferedFramesAvailable = true; + output.metrics.actualBufferedFrames = 4; + + FakeOutputThread outputThread; + outputThread.metrics.completedPollMisses = 12; + outputThread.metrics.scheduleFailures = 0; + + FakeRenderThread renderThread; + renderThread.metrics.shaderBuildsCommitted = 1; + renderThread.metrics.shaderBuildFailures = 0; + + const auto snapshot = telemetry.Sample(exchange, output, outputThread, renderThread); + Expect(snapshot.freeFrames == 7, "free frame count is sampled"); + 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.shaderBuildsCommitted == 1, "shader committed count is sampled"); + Expect(snapshot.shaderBuildFailures == 0, "shader failure count is sampled"); + Expect(snapshot.deckLinkBufferedAvailable, "buffer telemetry availability is sampled"); + Expect(snapshot.deckLinkBuffered == 4, "buffer depth is sampled"); +} + +void TestTelemetryComputesRatesFromDeltas() +{ + RenderCadenceCompositor::CadenceTelemetry telemetry; + FakeExchange exchange; + FakeOutput output; + FakeOutputThread outputThread; + FakeRenderThread renderThread; + + exchange.metrics.completedFrames = 10; + exchange.metrics.scheduledFrames = 10; + (void)telemetry.Sample(exchange, output, outputThread, renderThread); + + std::this_thread::sleep_for(std::chrono::milliseconds(5)); + + exchange.metrics.completedFrames = 20; + exchange.metrics.scheduledFrames = 19; + const auto snapshot = telemetry.Sample(exchange, output, outputThread, renderThread); + Expect(snapshot.sampleSeconds > 0.0, "second telemetry sample has elapsed time"); + Expect(snapshot.renderFps > 0.0, "render fps is computed from completed frame delta"); + Expect(snapshot.scheduleFps > 0.0, "schedule fps is computed from scheduled frame delta"); +} +} + +int main() +{ + TestTelemetrySamplesCompletedPollMissesAndShaderCounts(); + TestTelemetryComputesRatesFromDeltas(); + + if (gFailures != 0) + { + std::cerr << gFailures << " telemetry test failure(s).\n"; + return 1; + } + + std::cout << "RenderCadenceCompositor telemetry tests passed.\n"; + return 0; +}