Shader ownership change
All checks were successful
CI / React UI Build (push) Successful in 11s
CI / Native Windows Build And Tests (push) Successful in 2m52s
CI / Windows Release Package (push) Successful in 2m59s

This commit is contained in:
Aiden
2026-05-12 02:15:03 +10:00
parent 4ea829af85
commit c0d7e84495
12 changed files with 370 additions and 36 deletions

View File

@@ -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"

View File

@@ -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:

View File

@@ -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 <chrono>
#include <iostream>
#include <string>
#include <thread>
#include <type_traits>
@@ -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<SystemFrameExchange> mOutputThread;
TelemetryPrinter mTelemetry;
RuntimeSlangShaderCompiler mShaderCompiler;
std::thread mShaderBridgeThread;
std::atomic<bool> mShaderBridgeStopping{ false };
bool mStarted = false;
};
}

View File

@@ -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<std::mutex> lock(mShaderArtifactMutex);
mPendingShaderArtifact = artifact;
mHasPendingShaderArtifact = true;
}
bool RenderThread::TryTakePendingRuntimeShaderArtifact(RuntimeShaderArtifact& artifact)
{
std::lock_guard<std::mutex> 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<std::mutex> 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<std::mutex> lock(mMetricsMutex);
++mMetrics.shaderBuildsCommitted;
}

View File

@@ -1,7 +1,7 @@
#pragma once
#include "RenderCadenceClock.h"
#include "../runtime/RuntimeSlangShaderCompiler.h"
#include "../runtime/RuntimeShaderArtifact.h"
#include "RuntimeShaderRenderer.h"
#include <atomic>
@@ -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<bool> mStopping{ false };
std::atomic<bool> mRunning{ false };
@@ -71,4 +72,8 @@ private:
mutable std::mutex mMetricsMutex;
Metrics mMetrics;
std::mutex mShaderArtifactMutex;
bool mHasPendingShaderArtifact = false;
RuntimeShaderArtifact mPendingShaderArtifact;
};

View File

@@ -0,0 +1,10 @@
#pragma once
#include <string>
struct RuntimeShaderArtifact
{
std::string shaderId;
std::string fragmentShaderSource;
std::string message;
};

View File

@@ -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<std::chrono::duration<double, std::milli>>(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)

View File

@@ -1,5 +1,7 @@
#pragma once
#include "RuntimeShaderArtifact.h"
#include <atomic>
#include <mutex>
#include <string>
@@ -9,8 +11,7 @@ struct RuntimeSlangShaderBuild
{
bool available = false;
bool succeeded = false;
std::string shaderId;
std::string fragmentShaderSource;
RuntimeShaderArtifact artifact;
std::string message;
};

View File

@@ -1,8 +1,5 @@
#pragma once
#include "../video/DeckLinkOutput.h"
#include "../video/DeckLinkOutputThread.h"
#include <chrono>
#include <cstddef>
#include <cstdint>
@@ -34,10 +31,10 @@ struct CadenceTelemetrySnapshot
class CadenceTelemetry
{
public:
template <typename SystemFrameExchange, typename OutputThread>
template <typename SystemFrameExchange, typename Output, typename OutputThread>
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 <typename SystemFrameExchange, typename OutputThread, typename RenderThread>
template <typename SystemFrameExchange, typename Output, typename OutputThread, typename RenderThread>
CadenceTelemetrySnapshot Sample(
const SystemFrameExchange& exchange,
const DeckLinkOutput& output,
const Output& output,
const OutputThread& outputThread,
const RenderThread& renderThread)
{

View File

@@ -31,8 +31,8 @@ public:
Stop();
}
template <typename SystemFrameExchange, typename OutputThread, typename RenderThread>
void Start(const SystemFrameExchange& exchange, const DeckLinkOutput& output, const OutputThread& outputThread, const RenderThread& renderThread)
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;

View File

@@ -0,0 +1,94 @@
#include "RenderCadenceClock.h"
#include <chrono>
#include <iostream>
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;
}

View File

@@ -0,0 +1,146 @@
#include "CadenceTelemetry.h"
#include <chrono>
#include <cstdint>
#include <iostream>
#include <thread>
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;
}