optional preview frame
All checks were successful
CI / React UI Build (push) Successful in 11s
CI / Native Windows Build And Tests (push) Successful in 2m23s
CI / Windows Release Package (push) Has been skipped

This commit is contained in:
2026-05-20 14:37:24 +10:00
parent 1d4eb7a34c
commit bfaa3f5e0e
25 changed files with 700 additions and 2740 deletions

View File

@@ -18,6 +18,74 @@ add_video_shader_test(RenderCadenceCompositorTelemetryTests
"${TEST_DIR}/RenderCadenceCompositorTelemetryTests.cpp"
)
add_video_shader_test(RenderCadenceCompositorFrameExchangeTests
"${SRC_DIR}/frames/SystemFrameExchange.cpp"
"${SRC_DIR}/video/VideoIOFormat.cpp"
"${TEST_DIR}/RenderCadenceCompositorFrameExchangeTests.cpp"
)
add_video_shader_test(RenderCadenceCompositorInputFrameMailboxTests
"${SRC_DIR}/frames/InputFrameMailbox.cpp"
"${SRC_DIR}/video/VideoIOFormat.cpp"
"${TEST_DIR}/RenderCadenceCompositorInputFrameMailboxTests.cpp"
)
add_video_shader_test(RenderCadenceCompositorPreviewConfigTests
"${TEST_DIR}/RenderCadenceCompositorPreviewConfigTests.cpp"
)
add_video_shader_test(RenderCadenceCompositorRuntimeShaderParamsTests
"${SRC_DIR}/render/runtime/RuntimeShaderParams.cpp"
"${TEST_DIR}/RenderCadenceCompositorRuntimeShaderParamsTests.cpp"
)
add_video_shader_test(RenderCadenceCompositorRuntimeLayerModelTests
"${SRC_DIR}/runtime/RuntimeLayerModel.cpp"
"${SRC_DIR}/runtime/RuntimeJson.cpp"
"${SRC_DIR}/runtime/RuntimeParameterUtils.cpp"
"${SRC_DIR}/runtime/SupportedShaderCatalog.cpp"
"${SRC_DIR}/shader/ShaderPackageRegistry.cpp"
"${TEST_DIR}/RenderCadenceCompositorRuntimeLayerModelTests.cpp"
)
add_video_shader_test(RenderCadenceCompositorSupportedShaderCatalogTests
"${SRC_DIR}/runtime/RuntimeJson.cpp"
"${SRC_DIR}/runtime/SupportedShaderCatalog.cpp"
"${SRC_DIR}/shader/ShaderPackageRegistry.cpp"
"${TEST_DIR}/RenderCadenceCompositorSupportedShaderCatalogTests.cpp"
)
add_video_shader_test(RenderCadenceCompositorRuntimeStateJsonTests
"${SRC_DIR}/app/AppConfig.cpp"
"${SRC_DIR}/app/AppConfigProvider.cpp"
"${SRC_DIR}/json/JsonWriter.cpp"
"${SRC_DIR}/runtime/RuntimeJson.cpp"
"${SRC_DIR}/runtime/RuntimeLayerModel.cpp"
"${SRC_DIR}/runtime/RuntimeParameterUtils.cpp"
"${SRC_DIR}/runtime/SupportedShaderCatalog.cpp"
"${SRC_DIR}/shader/ShaderPackageRegistry.cpp"
"${TEST_DIR}/RenderCadenceCompositorRuntimeStateJsonTests.cpp"
)
add_video_shader_test(RenderCadenceCompositorHttpControlServerTests
"${SRC_DIR}/control/RuntimeControlCommand.cpp"
"${SRC_DIR}/control/http/HttpControlServer.cpp"
"${SRC_DIR}/control/http/HttpControlServerRoutes.cpp"
"${SRC_DIR}/control/http/HttpControlServerWebSocket.cpp"
"${SRC_DIR}/json/JsonWriter.cpp"
"${SRC_DIR}/logging/Logger.cpp"
"${SRC_DIR}/runtime/RuntimeJson.cpp"
"${TEST_DIR}/RenderCadenceCompositorHttpControlServerTests.cpp"
)
target_link_libraries(RenderCadenceCompositorHttpControlServerTests PRIVATE Ws2_32)
add_video_shader_test(RenderCadenceCompositorAppConfigProviderTests
"${SRC_DIR}/app/AppConfig.cpp"
"${SRC_DIR}/app/AppConfigProvider.cpp"
"${SRC_DIR}/runtime/RuntimeJson.cpp"
"${TEST_DIR}/RenderCadenceCompositorAppConfigProviderTests.cpp"
)
add_video_shader_test(RuntimeJsonTests
"${SRC_DIR}/runtime/RuntimeJson.cpp"
"${TEST_DIR}/RuntimeJsonTests.cpp"

View File

@@ -1,275 +0,0 @@
#include "HealthTelemetry.h"
#include <iostream>
namespace
{
int gFailures = 0;
void Expect(bool condition, const char* message)
{
if (condition)
return;
std::cerr << "FAIL: " << message << "\n";
++gFailures;
}
void TestRuntimeEventQueueMetrics()
{
HealthTelemetry telemetry;
telemetry.RecordRuntimeEventQueueMetrics("runtime-events", 3, 64, 2, 12.5);
const HealthTelemetry::RuntimeEventMetricsSnapshot metrics = telemetry.GetRuntimeEventMetricsSnapshot();
Expect(metrics.queue.queueName == "runtime-events", "queue metrics store queue name");
Expect(metrics.queue.depth == 3, "queue metrics store depth");
Expect(metrics.queue.capacity == 64, "queue metrics store capacity");
Expect(metrics.queue.droppedCount == 2, "queue metrics store dropped count");
Expect(metrics.queue.oldestEventAgeMilliseconds == 12.5, "queue metrics store oldest event age");
}
void TestRuntimeEventDispatchStats()
{
HealthTelemetry telemetry;
telemetry.RecordRuntimeEventDispatchStats(2, 5, 1, 0.75);
telemetry.RecordRuntimeEventDispatchStats(3, 6, 0, 0.25);
const HealthTelemetry::Snapshot snapshot = telemetry.GetSnapshot();
Expect(snapshot.runtimeEvents.dispatch.dispatchCallCount == 2, "dispatch stats count dispatch calls");
Expect(snapshot.runtimeEvents.dispatch.dispatchedEventCount == 5, "dispatch stats accumulate dispatched events");
Expect(snapshot.runtimeEvents.dispatch.handlerInvocationCount == 11, "dispatch stats accumulate handler invocations");
Expect(snapshot.runtimeEvents.dispatch.handlerFailureCount == 1, "dispatch stats accumulate handler failures");
Expect(snapshot.runtimeEvents.dispatch.lastDispatchDurationMilliseconds == 0.25, "dispatch stats store latest duration");
Expect(snapshot.runtimeEvents.dispatch.maxDispatchDurationMilliseconds == 0.75, "dispatch stats store max duration");
}
void TestRuntimeEventTryRecord()
{
HealthTelemetry telemetry;
Expect(telemetry.TryRecordRuntimeEventQueueMetrics("runtime-events", 1, 4, 0, -5.0), "try queue metrics succeeds when uncontended");
Expect(telemetry.TryRecordRuntimeEventDispatchStats(1, 2, 0, -1.0), "try dispatch stats succeeds when uncontended");
const HealthTelemetry::RuntimeEventMetricsSnapshot metrics = telemetry.GetRuntimeEventMetricsSnapshot();
Expect(metrics.queue.oldestEventAgeMilliseconds == 0.0, "queue age is clamped to non-negative values");
Expect(metrics.dispatch.lastDispatchDurationMilliseconds == 0.0, "dispatch duration is clamped to non-negative values");
}
void TestPersistenceWriteHealth()
{
HealthTelemetry telemetry;
telemetry.RecordPersistenceWriteResult(false, "runtime-state", "runtime/runtime_state.json", "UpdateLayerParameter",
"disk full", true);
HealthTelemetry::PersistenceSnapshot persistence = telemetry.GetPersistenceSnapshot();
Expect(persistence.writeFailureCount == 1, "persistence health counts write failures");
Expect(!persistence.lastWriteSucceeded, "persistence health records failed write state");
Expect(persistence.unsavedChanges, "persistence health reports unsaved changes after failure");
Expect(persistence.newerRequestPending, "persistence health records pending newer request");
Expect(persistence.lastTargetKind == "runtime-state", "persistence health records target kind");
Expect(persistence.lastReason == "UpdateLayerParameter", "persistence health records reason");
Expect(persistence.lastErrorMessage == "disk full", "persistence health records error message");
Expect(telemetry.TryRecordPersistenceWriteResult(true, "runtime-state", "runtime/runtime_state.json", "flush", "", false),
"try persistence health succeeds when uncontended");
persistence = telemetry.GetPersistenceSnapshot();
Expect(persistence.writeSuccessCount == 1, "persistence health counts write successes");
Expect(persistence.lastWriteSucceeded, "persistence health records successful write state");
Expect(!persistence.unsavedChanges, "persistence health clears unsaved changes after latest successful write with no pending request");
}
void TestBackendPlayoutHealth()
{
HealthTelemetry telemetry;
telemetry.RecordBackendPlayoutHealth(
"Degraded",
"Dropped",
1,
4,
12,
0,
3,
4,
10,
2,
1,
8.5,
7.25,
12.0,
1.0,
6.5,
0.5,
8,
11,
3,
2,
2,
1,
2,
5,
3,
1,
true,
"Output underrun");
const HealthTelemetry::BackendPlayoutSnapshot playout = telemetry.GetBackendPlayoutSnapshot();
Expect(playout.lifecycleState == "Degraded", "backend playout health stores lifecycle state");
Expect(playout.completionResult == "Dropped", "backend playout health stores completion result");
Expect(playout.readyQueueDepth == 1, "backend playout health stores ready queue depth");
Expect(playout.readyQueueCapacity == 4, "backend playout health stores ready queue capacity");
Expect(playout.minReadyQueueDepth == 0, "backend playout health stores min ready queue depth");
Expect(playout.maxReadyQueueDepth == 3, "backend playout health stores max ready queue depth");
Expect(playout.readyQueueZeroDepthCount == 4, "backend playout health stores zero-depth queue samples");
Expect(playout.readyQueueDroppedCount == 2, "backend playout health stores queue dropped count");
Expect(playout.readyQueueUnderrunCount == 1, "backend playout health stores queue underrun count");
Expect(playout.outputRenderMilliseconds == 8.5, "backend playout health stores output render duration");
Expect(playout.smoothedOutputRenderMilliseconds == 7.25, "backend playout health stores smoothed output render duration");
Expect(playout.maxOutputRenderMilliseconds == 12.0, "backend playout health stores max output render duration");
Expect(playout.outputFrameAcquireMilliseconds == 1.0, "backend playout health stores output frame acquire duration");
Expect(playout.outputFrameRenderRequestMilliseconds == 6.5, "backend playout health stores output render request duration");
Expect(playout.outputFrameEndAccessMilliseconds == 0.5, "backend playout health stores output frame end access duration");
Expect(playout.completedFrameIndex == 8, "backend playout health stores completed index");
Expect(playout.scheduledFrameIndex == 11, "backend playout health stores scheduled index");
Expect(playout.scheduledLeadFrames == 3, "backend playout health stores synthetic scheduled lead");
Expect(playout.measuredLagFrames == 2, "backend playout health stores measured lag");
Expect(playout.catchUpFrames == 2, "backend playout health stores catch-up frames");
Expect(playout.lateStreak == 1, "backend playout health stores late streak");
Expect(playout.dropStreak == 2, "backend playout health stores drop streak");
Expect(playout.lateFrameCount == 5, "backend playout health stores late frame count");
Expect(playout.droppedFrameCount == 3, "backend playout health stores dropped frame count");
Expect(playout.flushedFrameCount == 1, "backend playout health stores flushed frame count");
Expect(playout.degraded, "backend playout health stores degraded state");
Expect(playout.statusMessage == "Output underrun", "backend playout health stores status message");
Expect(telemetry.TryRecordBackendPlayoutHealth(
"Running",
"Completed",
2,
4,
13,
1,
3,
4,
11,
2,
1,
-5.0,
-4.0,
-3.0,
-2.0,
-1.0,
-0.5,
9,
12,
3,
0,
0,
0,
0,
5,
3,
1,
false,
""),
"try backend playout health succeeds when uncontended");
const HealthTelemetry::Snapshot snapshot = telemetry.GetSnapshot();
Expect(snapshot.backendPlayout.lifecycleState == "Running", "full health snapshot includes backend playout state");
Expect(!snapshot.backendPlayout.degraded, "full health snapshot includes backend degraded state");
}
void TestOutputRenderPipelineTiming()
{
HealthTelemetry telemetry;
telemetry.RecordOutputRenderQueueWait(2.5);
telemetry.RecordOutputRenderPipelineTiming(1.0, 0.5, 0.25, 0.75, 0.125, 0.375, 0.1, 0.2, 0.3, 0.4, 3.5, true, true, false);
Expect(telemetry.TryRecordOutputRenderPipelineTiming(-1.0, -2.0, -3.0, -4.0, -5.0, -6.0, -7.0, -8.0, -9.0, -10.0, -11.0, false, false, true),
"try output render timing succeeds when uncontended");
const HealthTelemetry::BackendPlayoutSnapshot playout = telemetry.GetBackendPlayoutSnapshot();
Expect(playout.outputRenderQueueWaitMilliseconds == 2.5, "output render timing stores queue wait");
Expect(playout.outputRenderDrawMilliseconds == 0.0, "output render timing clamps draw duration");
Expect(playout.outputReadbackFenceWaitMilliseconds == 0.0, "output render timing clamps fence wait duration");
Expect(playout.outputReadbackMapMilliseconds == 0.0, "output render timing clamps map duration");
Expect(playout.outputReadbackCopyMilliseconds == 0.0, "output render timing clamps readback copy duration");
Expect(playout.outputCachedCopyMilliseconds == 0.0, "output render timing clamps cached copy duration");
Expect(playout.outputAsyncQueueMilliseconds == 0.0, "output render timing clamps async queue duration");
Expect(playout.outputAsyncQueueBufferMilliseconds == 0.0, "output render timing clamps async queue buffer duration");
Expect(playout.outputAsyncQueueSetupMilliseconds == 0.0, "output render timing clamps async queue setup duration");
Expect(playout.outputAsyncQueueReadPixelsMilliseconds == 0.0, "output render timing clamps async queue read pixels duration");
Expect(playout.outputAsyncQueueFenceMilliseconds == 0.0, "output render timing clamps async queue fence duration");
Expect(playout.outputSyncReadMilliseconds == 0.0, "output render timing clamps sync read duration");
Expect(playout.outputAsyncReadbackMissCount == 1, "output render timing counts async readback misses");
Expect(playout.outputCachedFallbackCount == 1, "output render timing counts cached fallbacks");
Expect(playout.outputSyncFallbackCount == 1, "output render timing counts sync fallbacks");
}
void TestSystemMemoryPlayoutStats()
{
HealthTelemetry telemetry;
telemetry.RecordSystemMemoryPlayoutStats(2, 3, 1, 4, 5, 6, 12.5, 24.0);
HealthTelemetry::BackendPlayoutSnapshot playout = telemetry.GetBackendPlayoutSnapshot();
Expect(playout.systemFramePoolFree == 2, "system-memory playout stores free frame count");
Expect(playout.systemFramePoolReady == 3, "system-memory playout stores ready frame count");
Expect(playout.systemFramePoolScheduled == 1, "system-memory playout stores scheduled frame count");
Expect(playout.systemFrameUnderrunCount == 4, "system-memory playout stores underrun count");
Expect(playout.systemFrameRepeatCount == 5, "system-memory playout stores repeat count");
Expect(playout.systemFrameDropCount == 6, "system-memory playout stores drop count");
Expect(playout.systemFrameAgeAtScheduleMilliseconds == 12.5, "system-memory playout stores schedule age");
Expect(playout.systemFrameAgeAtCompletionMilliseconds == 24.0, "system-memory playout stores completion age");
Expect(telemetry.TryRecordSystemMemoryPlayoutStats(1, 0, 2, 7, 8, 9, -1.0, -2.0),
"try system-memory playout stats succeeds when uncontended");
playout = telemetry.GetBackendPlayoutSnapshot();
Expect(playout.systemFramePoolFree == 1, "try system-memory playout stores free frame count");
Expect(playout.systemFramePoolReady == 0, "try system-memory playout stores ready frame count");
Expect(playout.systemFramePoolScheduled == 2, "try system-memory playout stores scheduled frame count");
Expect(playout.systemFrameUnderrunCount == 7, "try system-memory playout stores underrun count");
Expect(playout.systemFrameRepeatCount == 8, "try system-memory playout stores repeat count");
Expect(playout.systemFrameDropCount == 9, "try system-memory playout stores drop count");
Expect(playout.systemFrameAgeAtScheduleMilliseconds == 0.0, "system-memory playout clamps negative schedule age");
Expect(playout.systemFrameAgeAtCompletionMilliseconds == 0.0, "system-memory playout clamps negative completion age");
}
void TestDeckLinkBufferTelemetry()
{
HealthTelemetry telemetry;
telemetry.RecordDeckLinkBufferTelemetry(true, 4, 5, 0.25, 2);
HealthTelemetry::BackendPlayoutSnapshot playout = telemetry.GetBackendPlayoutSnapshot();
Expect(playout.actualDeckLinkBufferedFramesAvailable, "DeckLink buffer telemetry records availability");
Expect(playout.actualDeckLinkBufferedFrames == 4, "DeckLink buffer telemetry stores actual device depth");
Expect(playout.targetDeckLinkBufferedFrames == 5, "DeckLink buffer telemetry stores target device depth");
Expect(playout.deckLinkScheduleCallMilliseconds == 0.25, "DeckLink buffer telemetry stores schedule call duration");
Expect(playout.deckLinkScheduleFailureCount == 2, "DeckLink buffer telemetry stores schedule failures");
Expect(telemetry.TryRecordDeckLinkBufferTelemetry(false, 9, 3, -1.0, 7),
"try DeckLink buffer telemetry succeeds when uncontended");
playout = telemetry.GetBackendPlayoutSnapshot();
Expect(!playout.actualDeckLinkBufferedFramesAvailable, "DeckLink buffer telemetry records unavailable device depth");
Expect(playout.actualDeckLinkBufferedFrames == 0, "unavailable DeckLink device depth clears actual count");
Expect(playout.targetDeckLinkBufferedFrames == 3, "try DeckLink buffer telemetry stores target device depth");
Expect(playout.deckLinkScheduleCallMilliseconds == 0.0, "DeckLink buffer telemetry clamps negative schedule call duration");
Expect(playout.deckLinkScheduleFailureCount == 7, "try DeckLink buffer telemetry stores schedule failures");
}
}
int main()
{
TestRuntimeEventQueueMetrics();
TestRuntimeEventDispatchStats();
TestRuntimeEventTryRecord();
TestPersistenceWriteHealth();
TestBackendPlayoutHealth();
TestOutputRenderPipelineTiming();
TestSystemMemoryPlayoutStats();
TestDeckLinkBufferTelemetry();
if (gFailures != 0)
{
std::cerr << gFailures << " HealthTelemetry test failure(s).\n";
return 1;
}
std::cout << "HealthTelemetry tests passed.\n";
return 0;
}

View File

@@ -1,236 +0,0 @@
#include "OscServer.h"
#include <cstring>
#include <functional>
#include <iostream>
#include <string>
#include <vector>
namespace
{
int gFailures = 0;
void Expect(bool condition, const char* message)
{
if (condition)
return;
std::cerr << "FAIL: " << message << "\n";
++gFailures;
}
void AppendPaddedString(std::vector<char>& packet, const std::string& text)
{
packet.insert(packet.end(), text.begin(), text.end());
packet.push_back('\0');
while (packet.size() % 4 != 0)
packet.push_back('\0');
}
void AppendInt32(std::vector<char>& packet, int value)
{
const unsigned int bits = static_cast<unsigned int>(value);
packet.push_back(static_cast<char>((bits >> 24) & 0xff));
packet.push_back(static_cast<char>((bits >> 16) & 0xff));
packet.push_back(static_cast<char>((bits >> 8) & 0xff));
packet.push_back(static_cast<char>(bits & 0xff));
}
void AppendFloat32(std::vector<char>& packet, float value)
{
unsigned int bits = 0;
std::memcpy(&bits, &value, sizeof(bits));
AppendInt32(packet, static_cast<int>(bits));
}
void AppendFloat64(std::vector<char>& packet, double value)
{
uint64_t bits = 0;
std::memcpy(&bits, &value, sizeof(bits));
for (int shift = 56; shift >= 0; shift -= 8)
packet.push_back(static_cast<char>((bits >> shift) & 0xff));
}
std::vector<char> BuildOscPacket(const std::string& address, const std::string& typeTags)
{
std::vector<char> packet;
AppendPaddedString(packet, address);
AppendPaddedString(packet, typeTags);
return packet;
}
}
struct OscServerTestAccess
{
using Message = OscServer::OscMessage;
static bool Decode(OscServer& server, const std::vector<char>& packet, Message& message, std::string& error)
{
return server.DecodeMessage(packet.data(), static_cast<int>(packet.size()), message, error);
}
static bool Dispatch(OscServer& server, const Message& message, std::string& error)
{
return server.DispatchMessage(message, error);
}
static bool TryParseBindAddress(const std::string& bindAddress, in_addr& address, std::string& error)
{
return OscServer::TryParseBindAddress(bindAddress, address, error);
}
static void SetUpdateParameterCallback(
OscServer& server,
const std::function<bool(const std::string&, const std::string&, const std::string&, std::string&)>& callback)
{
server.mCallbacks.updateParameter = callback;
}
};
namespace
{
void TestDecodeFloatMessage()
{
OscServer server;
std::vector<char> packet = BuildOscPacket("/VideoShaderToys/VHS/intensity", ",f");
AppendFloat32(packet, 0.75f);
OscServerTestAccess::Message message;
std::string error;
Expect(OscServerTestAccess::Decode(server, packet, message, error), "float OSC message decodes");
Expect(message.address == "/VideoShaderToys/VHS/intensity", "float OSC address is preserved");
Expect(message.valueJson.find("0.75") == 0, "float OSC value becomes JSON number");
}
void TestDecodeDoubleMessage()
{
OscServer server;
std::vector<char> packet = BuildOscPacket("/VideoShaderToys/fisheye-reproject/panDegrees", ",d");
AppendFloat64(packet, 51.5);
OscServerTestAccess::Message message;
std::string error;
Expect(OscServerTestAccess::Decode(server, packet, message, error), "double OSC message decodes");
Expect(message.address == "/VideoShaderToys/fisheye-reproject/panDegrees", "double OSC address is preserved");
Expect(message.valueJson.find("51.5") == 0, "double OSC value becomes JSON number");
}
void TestDecodeVectorMessage()
{
OscServer server;
std::vector<char> packet = BuildOscPacket("/VideoShaderToys/video-transform/pan", ",ff");
AppendFloat32(packet, 0.25f);
AppendFloat32(packet, -0.5f);
OscServerTestAccess::Message message;
std::string error;
Expect(OscServerTestAccess::Decode(server, packet, message, error), "multi-float OSC message decodes");
Expect(message.address == "/VideoShaderToys/video-transform/pan", "multi-float OSC address is preserved");
Expect(message.valueJson.find("[0.25,-0.5") == 0, "multi-float OSC value becomes JSON array");
}
void TestDecodeIntStringAndBoolMessages()
{
OscServer server;
std::vector<char> intPacket = BuildOscPacket("/VideoShaderToys/layer-1/mode", ",i");
AppendInt32(intPacket, 3);
OscServerTestAccess::Message intMessage;
std::string error;
Expect(OscServerTestAccess::Decode(server, intPacket, intMessage, error), "int OSC message decodes");
Expect(intMessage.valueJson == "3", "int OSC value becomes JSON number");
std::vector<char> stringPacket = BuildOscPacket("/VideoShaderToys/layer-1/mode", ",s");
AppendPaddedString(stringPacket, "equisolid");
OscServerTestAccess::Message stringMessage;
error.clear();
Expect(OscServerTestAccess::Decode(server, stringPacket, stringMessage, error), "string OSC message decodes");
Expect(stringMessage.valueJson == "\"equisolid\"", "string OSC value becomes JSON string");
std::vector<char> boolPacket = BuildOscPacket("/VideoShaderToys/layer-1/enabled", ",T");
OscServerTestAccess::Message boolMessage;
error.clear();
Expect(OscServerTestAccess::Decode(server, boolPacket, boolMessage, error), "boolean OSC message decodes");
Expect(boolMessage.valueJson == "true", "true OSC typetag becomes JSON boolean");
}
void TestDispatchValidAddress()
{
OscServer server;
std::string layerKey;
std::string parameterKey;
std::string valueJson;
OscServerTestAccess::SetUpdateParameterCallback(server, [&](const std::string& layer, const std::string& parameter, const std::string& value, std::string&)
{
layerKey = layer;
parameterKey = parameter;
valueJson = value;
return true;
});
OscServerTestAccess::Message message;
message.address = "/VideoShaderToys/VHS/intensity";
message.valueJson = "0.5";
std::string error;
Expect(OscServerTestAccess::Dispatch(server, message, error), "valid OSC control address dispatches");
Expect(layerKey == "VHS", "dispatch extracts layer key");
Expect(parameterKey == "intensity", "dispatch extracts parameter key");
Expect(valueJson == "0.5", "dispatch forwards JSON value");
}
void TestRejectsUnsupportedAddress()
{
OscServer server;
bool called = false;
OscServerTestAccess::SetUpdateParameterCallback(server, [&](const std::string&, const std::string&, const std::string&, std::string&)
{
called = true;
return true;
});
OscServerTestAccess::Message message;
message.address = "/OtherApp/VHS/intensity";
message.valueJson = "0.5";
std::string error;
Expect(!OscServerTestAccess::Dispatch(server, message, error), "unsupported OSC namespace is rejected");
Expect(!called, "unsupported OSC namespace does not invoke callback");
Expect(!error.empty(), "unsupported OSC address reports an error");
}
void TestParsesOscBindAddress()
{
in_addr loopback = {};
std::string error;
Expect(OscServerTestAccess::TryParseBindAddress("127.0.0.1", loopback, error), "loopback OSC bind address parses");
Expect(loopback.S_un.S_addr != 0, "loopback OSC bind address produces a socket address");
in_addr wildcard = {};
error.clear();
Expect(OscServerTestAccess::TryParseBindAddress("0.0.0.0", wildcard, error), "wildcard OSC bind address parses");
in_addr invalid = {};
error.clear();
Expect(!OscServerTestAccess::TryParseBindAddress("localhost", invalid, error), "hostname OSC bind address is rejected");
Expect(!error.empty(), "invalid OSC bind address reports an error");
}
}
int main()
{
TestDecodeFloatMessage();
TestDecodeDoubleMessage();
TestDecodeVectorMessage();
TestDecodeIntStringAndBoolMessages();
TestDispatchValidAddress();
TestRejectsUnsupportedAddress();
TestParsesOscBindAddress();
if (gFailures != 0)
{
std::cerr << gFailures << " OscServer test failure(s).\n";
return 1;
}
std::cout << "OscServer tests passed.\n";
return 0;
}

View File

@@ -1,210 +0,0 @@
#include "PersistenceWriter.h"
#include <condition_variable>
#include <chrono>
#include <filesystem>
#include <iostream>
#include <mutex>
#include <string>
#include <vector>
namespace
{
int gFailures = 0;
void Expect(bool condition, const char* message)
{
if (condition)
return;
std::cerr << "FAIL: " << message << "\n";
++gFailures;
}
PersistenceSnapshot MakeRuntimeSnapshot(const std::string& contents)
{
PersistenceSnapshot snapshot;
snapshot.targetKind = PersistenceTargetKind::RuntimeState;
snapshot.targetPath = std::filesystem::temp_directory_path() / "video-shader-persistence-writer-test.json";
snapshot.contents = contents;
snapshot.reason = "test";
snapshot.debounceKey = "runtime-state";
snapshot.debounceAllowed = true;
return snapshot;
}
void TestDebouncedRequestsCoalesceToNewestSnapshot()
{
std::mutex mutex;
std::vector<PersistenceSnapshot> writtenSnapshots;
PersistenceWriter writer(
std::chrono::milliseconds(1000),
[&](const PersistenceSnapshot& snapshot, std::string&) {
std::lock_guard<std::mutex> lock(mutex);
writtenSnapshots.push_back(snapshot);
return true;
});
std::string error;
Expect(writer.EnqueueSnapshot(MakeRuntimeSnapshot("first"), error), "first debounced snapshot enqueues");
Expect(writer.EnqueueSnapshot(MakeRuntimeSnapshot("second"), error), "second debounced snapshot enqueues");
PersistenceWriterMetrics metrics = writer.GetMetrics();
Expect(metrics.pendingCount == 1, "debounced snapshots share one pending slot");
Expect(metrics.enqueuedCount == 1, "first debounced snapshot counts as enqueue");
Expect(metrics.coalescedCount == 1, "second debounced snapshot counts as coalesced");
Expect(writer.StopAndFlush(std::chrono::seconds(1), error), "flush drains debounced snapshots");
{
std::lock_guard<std::mutex> lock(mutex);
Expect(writtenSnapshots.size() == 1, "flush writes one coalesced snapshot");
Expect(!writtenSnapshots.empty() && writtenSnapshots[0].contents == "second", "coalesced writer keeps newest snapshot");
}
metrics = writer.GetMetrics();
Expect(metrics.pendingCount == 0, "flush drains pending debounced snapshot");
Expect(metrics.writtenCount == 1, "flush records one successful write");
}
void TestImmediateRequestsAreNotCoalesced()
{
std::mutex mutex;
std::vector<PersistenceSnapshot> writtenSnapshots;
PersistenceWriter writer(
std::chrono::milliseconds(1000),
[&](const PersistenceSnapshot& snapshot, std::string&) {
std::lock_guard<std::mutex> lock(mutex);
writtenSnapshots.push_back(snapshot);
return true;
});
PersistenceSnapshot first = MakeRuntimeSnapshot("first");
first.debounceAllowed = false;
PersistenceSnapshot second = MakeRuntimeSnapshot("second");
second.debounceAllowed = false;
std::string error;
Expect(writer.EnqueueSnapshot(first, error), "first immediate snapshot enqueues");
Expect(writer.EnqueueSnapshot(second, error), "second immediate snapshot enqueues");
Expect(writer.StopAndFlush(std::chrono::seconds(1), error), "flush drains immediate snapshots");
{
std::lock_guard<std::mutex> lock(mutex);
Expect(writtenSnapshots.size() == 2, "immediate snapshots are written independently");
Expect(writtenSnapshots.size() == 2 && writtenSnapshots[0].contents == "first" && writtenSnapshots[1].contents == "second",
"immediate snapshots preserve order");
}
}
void TestWriteFailureReportsStructuredResult()
{
std::vector<PersistenceWriteResult> results;
PersistenceWriter writer(
std::chrono::milliseconds(1),
[](const PersistenceSnapshot&, std::string& error) {
error = "simulated failure";
return false;
});
writer.SetResultCallback([&results](const PersistenceWriteResult& result) {
results.push_back(result);
});
PersistenceSnapshot snapshot = MakeRuntimeSnapshot("payload");
snapshot.debounceAllowed = false;
snapshot.reason = "failure-test";
std::string error;
Expect(writer.EnqueueSnapshot(snapshot, error), "failing snapshot still enqueues");
Expect(writer.StopAndFlush(std::chrono::seconds(1), error), "flush reports failing snapshot result");
Expect(results.size() == 1, "writer reports one failure result");
Expect(!results.empty() && !results[0].succeeded, "writer result records failure");
Expect(!results.empty() && results[0].reason == "failure-test", "writer result preserves reason");
Expect(!results.empty() && results[0].errorMessage == "simulated failure", "writer result preserves error message");
Expect(!results.empty() && !results[0].newerRequestPending, "writer result reports no newer pending request");
Expect(writer.GetMetrics().failedCount == 1, "writer metrics count failed writes");
}
void TestShutdownFlushDrainsPendingSnapshotAndRejectsNewRequests()
{
std::mutex mutex;
std::vector<PersistenceSnapshot> writtenSnapshots;
PersistenceWriter writer(
std::chrono::milliseconds(1000),
[&](const PersistenceSnapshot& snapshot, std::string&) {
std::lock_guard<std::mutex> lock(mutex);
writtenSnapshots.push_back(snapshot);
return true;
});
std::string error;
Expect(writer.EnqueueSnapshot(MakeRuntimeSnapshot("pending"), error), "pending snapshot enqueues before shutdown");
Expect(writer.StopAndFlush(std::chrono::seconds(1), error), "bounded shutdown flush completes");
{
std::lock_guard<std::mutex> lock(mutex);
Expect(writtenSnapshots.size() == 1, "shutdown flush writes pending debounced snapshot");
Expect(!writtenSnapshots.empty() && writtenSnapshots[0].contents == "pending", "shutdown flush preserves pending snapshot contents");
}
Expect(!writer.EnqueueSnapshot(MakeRuntimeSnapshot("late"), error), "writer rejects requests after shutdown flush");
}
void TestShutdownFlushTimeoutCanBeRetried()
{
std::mutex mutex;
std::condition_variable condition;
bool sinkStarted = false;
bool releaseSink = false;
PersistenceWriter writer(
std::chrono::milliseconds(1),
[&](const PersistenceSnapshot&, std::string&) {
std::unique_lock<std::mutex> lock(mutex);
sinkStarted = true;
condition.notify_all();
condition.wait(lock, [&]() { return releaseSink; });
return true;
});
PersistenceSnapshot snapshot = MakeRuntimeSnapshot("slow");
snapshot.debounceAllowed = false;
std::string error;
Expect(writer.EnqueueSnapshot(snapshot, error), "slow snapshot enqueues");
{
std::unique_lock<std::mutex> lock(mutex);
Expect(condition.wait_for(lock, std::chrono::seconds(1), [&]() { return sinkStarted; }),
"slow sink starts before timeout test");
}
Expect(!writer.StopAndFlush(std::chrono::milliseconds(10), error), "bounded shutdown flush reports timeout");
Expect(error.find("Timed out") != std::string::npos, "shutdown timeout returns a useful error");
{
std::lock_guard<std::mutex> lock(mutex);
releaseSink = true;
}
condition.notify_all();
error.clear();
Expect(writer.StopAndFlush(std::chrono::seconds(1), error), "shutdown flush can complete after earlier timeout");
}
}
int main()
{
TestDebouncedRequestsCoalesceToNewestSnapshot();
TestImmediateRequestsAreNotCoalesced();
TestWriteFailureReportsStructuredResult();
TestShutdownFlushDrainsPendingSnapshotAndRejectsNewRequests();
TestShutdownFlushTimeoutCanBeRetried();
if (gFailures != 0)
{
std::cerr << gFailures << " persistence writer test(s) failed.\n";
return 1;
}
std::cout << "Persistence writer tests passed.\n";
return 0;
}

View File

@@ -36,6 +36,7 @@ std::filesystem::path WriteConfigFixture()
<< " \"outputFrameRate\": \"60\",\n"
<< " \"autoReload\": false,\n"
<< " \"maxTemporalHistoryFrames\": 8,\n"
<< " \"previewEnabled\": true,\n"
<< " \"previewFps\": 24,\n"
<< " \"enableExternalKeying\": true\n"
<< "}\n";
@@ -66,6 +67,7 @@ void TestLoadsRuntimeHostConfig()
Expect(config.outputFrameRate == "60", "output frame rate loads");
Expect(!config.autoReload, "auto reload loads");
Expect(config.maxTemporalHistoryFrames == 8, "history length loads");
Expect(config.previewEnabled, "preview enabled toggle loads");
Expect(config.previewFps == 24.0, "preview fps loads");
Expect(config.deckLink.externalKeyingEnabled, "external keying loads");
@@ -91,6 +93,15 @@ void TestCommandLineOverrides()
Expect(config.http.preferredPort == 8282, "port CLI override applies");
}
void TestPreviewDefaultsAreOptIn()
{
using namespace RenderCadenceCompositor;
const AppConfig config = DefaultAppConfig();
Expect(!config.previewEnabled, "preview is disabled by default");
Expect(config.previewFps == 30.0, "preview fps default is 30");
}
void TestHelpers()
{
using namespace RenderCadenceCompositor;
@@ -118,6 +129,7 @@ int main()
{
TestLoadsRuntimeHostConfig();
TestCommandLineOverrides();
TestPreviewDefaultsAreOptIn();
TestHelpers();
if (gFailures != 0)

View File

@@ -2,6 +2,7 @@
#include <chrono>
#include <cstdint>
#include <cstring>
#include <iostream>
namespace
@@ -184,6 +185,127 @@ void TestCompletedPollMissIsCounted()
Expect(metrics.completedPollMisses == 1, "completed poll miss is counted");
}
void TestLatestPublishedFrameCanBePreviewedWithoutConsuming()
{
SystemFrameExchange exchange(MakeConfig(2));
SystemFrame frame;
Expect(exchange.AcquireForRender(frame), "copy snapshot frame can be acquired");
frame.frameIndex = 77;
const unsigned char marker = 0x42;
std::memset(frame.bytes, marker, static_cast<std::size_t>(frame.rowBytes) * frame.height);
Expect(exchange.PublishCompleted(frame), "copy snapshot frame can be completed");
SystemFrame preview;
Expect(exchange.TryAcquireLatestForPreview(preview), "latest published frame can be acquired for preview");
Expect(preview.frameIndex == 77, "preview frame keeps frame index");
Expect(preview.width == 4 && preview.height == 3, "preview frame keeps frame dimensions");
Expect(preview.bytes != nullptr && static_cast<unsigned char*>(preview.bytes)[0] == marker, "preview frame points at frame bytes");
Expect(exchange.ReleasePreviewFrame(preview), "preview frame can be released");
SystemFrame scheduled;
Expect(exchange.ConsumeCompletedForSchedule(scheduled), "previewing frame does not consume completed frame");
}
void TestLatestPublishedFrameCanPreviewScheduledFrame()
{
SystemFrameExchange exchange(MakeConfig(1));
SystemFrame frame;
Expect(exchange.AcquireForRender(frame), "scheduled snapshot frame can be acquired");
frame.frameIndex = 88;
Expect(exchange.PublishCompleted(frame), "scheduled snapshot frame can be completed");
SystemFrame scheduled;
Expect(exchange.ConsumeCompletedForSchedule(scheduled), "snapshot test frame can be scheduled");
SystemFrame preview;
Expect(exchange.TryAcquireLatestForPreview(preview), "latest scheduled frame can be acquired for preview");
Expect(preview.frameIndex == 88, "scheduled preview keeps frame index");
Expect(exchange.ReleasePreviewFrame(preview), "scheduled preview frame can be released");
}
void TestPreviewFramePinsReleasedSlotUntilPreviewRelease()
{
SystemFrameExchange exchange(MakeConfig(1));
SystemFrame frame;
Expect(exchange.AcquireForRender(frame), "preview pin frame can be acquired");
frame.frameIndex = 99;
Expect(exchange.PublishCompleted(frame), "preview pin frame can be completed");
SystemFrame preview;
Expect(exchange.TryAcquireLatestForPreview(preview), "preview can acquire frame before schedule");
SystemFrame scheduled;
Expect(exchange.ConsumeCompletedForSchedule(scheduled), "previewed frame can still be scheduled");
Expect(exchange.ReleaseScheduledByBytes(scheduled.bytes), "scheduled frame can be released while preview holds it");
SystemFrame blocked;
Expect(!exchange.AcquireForRender(blocked), "preview reader prevents immediate slot reuse");
Expect(exchange.ReleasePreviewFrame(preview), "preview pin can be released");
Expect(exchange.AcquireForRender(blocked), "slot can be reused after preview release");
}
void TestMultiplePreviewReadersPinReleasedSlotUntilAllRelease()
{
SystemFrameExchange exchange(MakeConfig(1));
SystemFrame frame;
Expect(exchange.AcquireForRender(frame), "multi-preview frame can be acquired");
frame.frameIndex = 100;
Expect(exchange.PublishCompleted(frame), "multi-preview frame can be completed");
SystemFrame firstPreview;
SystemFrame secondPreview;
Expect(exchange.TryAcquireLatestForPreview(firstPreview), "first preview reader can acquire");
Expect(exchange.TryAcquireLatestForPreview(secondPreview), "second preview reader can acquire");
SystemFrame scheduled;
Expect(exchange.ConsumeCompletedForSchedule(scheduled), "multi-preview frame can be scheduled");
Expect(exchange.ReleaseScheduledByBytes(scheduled.bytes), "multi-preview scheduled frame can be released");
SystemFrame blocked;
Expect(!exchange.AcquireForRender(blocked), "slot remains pinned while two preview readers exist");
Expect(exchange.ReleasePreviewFrame(firstPreview), "first preview reader can release");
Expect(!exchange.AcquireForRender(blocked), "slot remains pinned until last preview reader releases");
Expect(exchange.ReleasePreviewFrame(secondPreview), "second preview reader can release");
Expect(exchange.AcquireForRender(blocked), "slot is reusable after all preview readers release");
}
void TestInvalidPreviewReleaseIsRejected()
{
SystemFrameExchange exchange(MakeConfig(1));
SystemFrame invalid;
Expect(!exchange.ReleasePreviewFrame(invalid), "empty preview frame release is rejected");
SystemFrame frame;
Expect(exchange.AcquireForRender(frame), "invalid release source frame can be acquired");
frame.frameIndex = 101;
Expect(exchange.PublishCompleted(frame), "invalid release source frame can be completed");
SystemFrame preview;
Expect(exchange.TryAcquireLatestForPreview(preview), "preview frame can be acquired for invalid release test");
Expect(exchange.ReleasePreviewFrame(preview), "valid preview release succeeds");
Expect(!exchange.ReleasePreviewFrame(preview), "double preview release is rejected");
}
void TestStalePreviewReleaseIsRejectedAfterReconfigure()
{
SystemFrameExchange exchange(MakeConfig(1));
SystemFrame frame;
Expect(exchange.AcquireForRender(frame), "stale preview source frame can be acquired");
frame.frameIndex = 102;
Expect(exchange.PublishCompleted(frame), "stale preview source frame can be completed");
SystemFrame preview;
Expect(exchange.TryAcquireLatestForPreview(preview), "preview frame can be acquired before reconfigure");
exchange.Configure(MakeConfig(1));
Expect(!exchange.ReleasePreviewFrame(preview), "preview release after reconfigure is rejected as stale");
}
void TestStableCompletedDepthCanBeObserved()
{
SystemFrameExchange exchange(MakeConfig(1));
@@ -214,6 +336,12 @@ int main()
TestGenerationValidationRejectsStaleFrames();
TestPixelFormatAwareSizing();
TestCompletedPollMissIsCounted();
TestLatestPublishedFrameCanBePreviewedWithoutConsuming();
TestLatestPublishedFrameCanPreviewScheduledFrame();
TestPreviewFramePinsReleasedSlotUntilPreviewRelease();
TestMultiplePreviewReadersPinReleasedSlotUntilAllRelease();
TestInvalidPreviewReleaseIsRejected();
TestStalePreviewReleaseIsRejectedAfterReconfigure();
TestStableCompletedDepthCanBeObserved();
TestStableCompletedDepthTimesOut();

View File

@@ -0,0 +1,50 @@
#include "PreviewConfig.h"
#include <iostream>
namespace
{
int gFailures = 0;
void Expect(bool condition, const char* message)
{
if (condition)
return;
std::cerr << "FAIL: " << message << "\n";
++gFailures;
}
void TestTimerIntervalUsesConfiguredFps()
{
Expect(RenderCadenceCompositor::PreviewTimerIntervalMilliseconds(25.0) == 40, "25 fps maps to 40 ms");
Expect(RenderCadenceCompositor::PreviewTimerIntervalMilliseconds(50.0) == 20, "50 fps maps to 20 ms");
}
void TestInvalidFpsUsesDefault()
{
Expect(
RenderCadenceCompositor::PreviewTimerIntervalMilliseconds(0.0) ==
RenderCadenceCompositor::PreviewTimerIntervalMilliseconds(RenderCadenceCompositor::kDefaultPreviewFps),
"zero preview fps uses default interval");
Expect(
RenderCadenceCompositor::PreviewTimerIntervalMilliseconds(-10.0) ==
RenderCadenceCompositor::PreviewTimerIntervalMilliseconds(RenderCadenceCompositor::kDefaultPreviewFps),
"negative preview fps uses default interval");
}
}
int main()
{
TestTimerIntervalUsesConfiguredFps();
TestInvalidFpsUsesDefault();
if (gFailures != 0)
{
std::cerr << gFailures << " preview config test failure(s).\n";
return 1;
}
std::cout << "RenderCadenceCompositor preview config tests passed.\n";
return 0;
}

View File

@@ -1,152 +0,0 @@
#include "RenderCommandQueue.h"
#include <iostream>
namespace
{
int gFailures = 0;
void Expect(bool condition, const char* message)
{
if (condition)
return;
std::cerr << "FAIL: " << message << "\n";
++gFailures;
}
void TestPreviewRequestUsesLatestValue()
{
RenderCommandQueue queue;
queue.RequestPreviewPresent({ 1920, 1080 });
queue.RequestPreviewPresent({ 1280, 720 });
const RenderCommandQueueMetrics metrics = queue.GetMetrics();
Expect(metrics.depth == 1, "preview requests coalesce to one pending command");
Expect(metrics.enqueuedCount == 1, "first preview request is counted as enqueued");
Expect(metrics.coalescedCount == 1, "second preview request is counted as coalesced");
RenderPreviewPresentRequest request;
Expect(queue.TryTakePreviewPresent(request), "preview request can be consumed");
Expect(request.outputFrameWidth == 1280 && request.outputFrameHeight == 720, "latest preview request wins");
Expect(!queue.TryTakePreviewPresent(request), "preview request is removed after consume");
Expect(queue.GetMetrics().depth == 0, "preview consume empties queue depth");
}
void TestScreenshotRequestUsesLatestValue()
{
RenderCommandQueue queue;
queue.RequestScreenshotCapture({ 640, 360 });
queue.RequestScreenshotCapture({ 3840, 2160 });
RenderScreenshotCaptureRequest request;
Expect(queue.TryTakeScreenshotCapture(request), "screenshot request can be consumed");
Expect(request.width == 3840 && request.height == 2160, "latest screenshot request wins");
Expect(!queue.TryTakeScreenshotCapture(request), "screenshot request is removed after consume");
}
void TestRenderResetScopesCoalesceToStrongestRequest()
{
RenderCommandQueue queue;
queue.RequestRenderReset(RenderCommandResetScope::TemporalHistoryOnly);
queue.RequestRenderReset(RenderCommandResetScope::ShaderFeedbackOnly);
RenderCommandResetScope scope = RenderCommandResetScope::None;
Expect(queue.TryTakeRenderReset(scope), "render reset request can be consumed");
Expect(scope == RenderCommandResetScope::TemporalHistoryAndFeedback, "temporal and feedback reset requests merge");
Expect(!queue.TryTakeRenderReset(scope), "render reset request is removed after consume");
queue.RequestRenderReset(RenderCommandResetScope::None);
Expect(queue.GetMetrics().depth == 0, "none reset request is ignored");
}
void TestInputUploadRequestUsesLatestValue()
{
int firstPixel = 1;
int secondPixel = 2;
RenderCommandQueue queue;
RenderInputUploadRequest firstRequest;
firstRequest.inputFrame.bytes = &firstPixel;
firstRequest.inputFrame.width = 1920;
firstRequest.videoState.captureTextureWidth = 1920;
firstRequest.ownedBytes = { 1, 2, 3, 4 };
queue.RequestInputUpload(firstRequest);
RenderInputUploadRequest secondRequest;
secondRequest.inputFrame.bytes = &secondPixel;
secondRequest.inputFrame.width = 1280;
secondRequest.videoState.captureTextureWidth = 1280;
secondRequest.ownedBytes = { 5, 6 };
queue.RequestInputUpload(secondRequest);
const RenderCommandQueueMetrics metrics = queue.GetMetrics();
Expect(metrics.depth == 1, "input upload requests coalesce to one pending command");
Expect(metrics.enqueuedCount == 1, "first input upload request is counted as enqueued");
Expect(metrics.coalescedCount == 1, "second input upload request is counted as coalesced");
RenderInputUploadRequest request;
Expect(queue.TryTakeInputUpload(request), "input upload request can be consumed");
Expect(request.inputFrame.bytes == &secondPixel, "latest input upload bytes pointer wins");
Expect(request.inputFrame.width == 1280, "latest input upload frame wins");
Expect(request.videoState.captureTextureWidth == 1280, "latest input upload state wins");
Expect(request.ownedBytes.size() == 2 && request.ownedBytes[0] == 5 && request.ownedBytes[1] == 6, "latest input upload owned bytes win");
Expect(!queue.TryTakeInputUpload(request), "input upload request is removed after consume");
}
void TestOutputFrameRequestsAreFifo()
{
RenderCommandQueue queue;
RenderOutputFrameRequest firstRequest;
firstRequest.videoState.outputFrameSize.width = 1920;
firstRequest.completion.result = VideoIOCompletionResult::Completed;
queue.RequestOutputFrame(firstRequest);
RenderOutputFrameRequest secondRequest;
secondRequest.videoState.outputFrameSize.width = 1280;
secondRequest.completion.result = VideoIOCompletionResult::Dropped;
queue.RequestOutputFrame(secondRequest);
Expect(queue.GetMetrics().depth == 2, "output frame requests are queued independently");
RenderOutputFrameRequest request;
Expect(queue.TryTakeOutputFrame(request), "first output request can be consumed");
Expect(request.videoState.outputFrameSize.width == 1920, "first output request is consumed first");
Expect(request.completion.result == VideoIOCompletionResult::Completed, "first output completion is preserved");
Expect(queue.TryTakeOutputFrame(request), "second output request can be consumed");
Expect(request.videoState.outputFrameSize.width == 1280, "second output request is consumed second");
Expect(request.completion.result == VideoIOCompletionResult::Dropped, "second output completion is preserved");
Expect(!queue.TryTakeOutputFrame(request), "output queue is empty after consuming all requests");
}
void TestIndependentCommandKindsShareDepth()
{
RenderCommandQueue queue;
queue.RequestPreviewPresent({ 1, 2 });
queue.RequestScreenshotCapture({ 3, 4 });
queue.RequestInputUpload({});
queue.RequestOutputFrame({});
queue.RequestRenderReset(RenderCommandResetScope::TemporalHistoryOnly);
Expect(queue.GetMetrics().depth == 5, "independent command kinds each contribute to depth");
}
}
int main()
{
TestPreviewRequestUsesLatestValue();
TestScreenshotRequestUsesLatestValue();
TestRenderResetScopesCoalesceToStrongestRequest();
TestInputUploadRequestUsesLatestValue();
TestOutputFrameRequestsAreFifo();
TestIndependentCommandKindsShareDepth();
if (gFailures != 0)
{
std::cerr << gFailures << " RenderCommandQueue test failure(s).\n";
return 1;
}
std::cout << "RenderCommandQueue tests passed.\n";
return 0;
}

View File

@@ -1,50 +0,0 @@
#include "RuntimeClock.h"
#include <cmath>
#include <iostream>
namespace
{
int gFailures = 0;
void Expect(bool condition, const char* message)
{
if (condition)
return;
std::cerr << "FAIL: " << message << "\n";
++gFailures;
}
void TestUtcSecondsOfDay()
{
const RuntimeClockSnapshot midnight = MakeRuntimeClockSnapshot(0);
Expect(midnight.utcTimeSeconds == 0.0, "Unix epoch starts at UTC midnight");
const RuntimeClockSnapshot midday = MakeRuntimeClockSnapshot(12 * 3600 + 34 * 60 + 56);
Expect(midday.utcTimeSeconds == 45296.0, "UTC time of day is seconds since midnight");
}
void TestOffsetLooksLikeTimezoneOffset()
{
const RuntimeClockSnapshot snapshot = MakeRuntimeClockSnapshot(12 * 3600);
Expect(std::fmod(snapshot.utcOffsetSeconds, 60.0) == 0.0, "UTC offset is minute-aligned");
Expect(snapshot.utcOffsetSeconds >= -14.0 * 3600.0 && snapshot.utcOffsetSeconds <= 14.0 * 3600.0,
"UTC offset is in the normal timezone range");
}
}
int main()
{
TestUtcSecondsOfDay();
TestOffsetLooksLikeTimezoneOffset();
if (gFailures != 0)
{
std::cerr << gFailures << " RuntimeClock test failure(s).\n";
return 1;
}
std::cout << "RuntimeClock tests passed.\n";
return 0;
}

View File

@@ -1,726 +0,0 @@
#include "RuntimeEvent.h"
#include "RuntimeEventCoalescingQueue.h"
#include "RuntimeEventDispatcher.h"
#include "RuntimeEventQueue.h"
#include "RuntimeEventType.h"
#include "RuntimeEventPayloads.h"
#include "RuntimeEventTestHarness.h"
#include <chrono>
#include <iostream>
#include <stdexcept>
#include <string_view>
#include <variant>
namespace
{
int gFailures = 0;
void Expect(bool condition, const char* message)
{
if (condition)
return;
std::cerr << "FAIL: " << message << "\n";
++gFailures;
}
void TestRuntimeEventTypeNames()
{
Expect(RuntimeEventTypeName(RuntimeEventType::Unknown) == "Unknown", "unknown event type has a stable name");
Expect(RuntimeEventTypeName(RuntimeEventType::OscCommitRequested) == "OscCommitRequested", "control event name is stable");
Expect(RuntimeEventTypeName(RuntimeEventType::RuntimeMutationAccepted) == "RuntimeMutationAccepted", "runtime event name is stable");
Expect(RuntimeEventTypeName(RuntimeEventType::ShaderBuildPrepared) == "ShaderBuildPrepared", "shader build event name is stable");
Expect(RuntimeEventTypeName(RuntimeEventType::RenderSnapshotPublished) == "RenderSnapshotPublished", "render event name is stable");
Expect(RuntimeEventTypeName(RuntimeEventType::BackendStateChanged) == "BackendStateChanged", "backend event name is stable");
Expect(RuntimeEventTypeName(RuntimeEventType::QueueDepthChanged) == "QueueDepthChanged", "telemetry event name is stable");
}
void TestRuntimeEventPayloadTypes()
{
OscCommitRequestedEvent oscCommit;
oscCommit.routeKey = "layer-1\namount";
oscCommit.layerKey = "layer-1";
oscCommit.parameterKey = "amount";
oscCommit.generation = 42;
Expect(RuntimeEventPayloadType(oscCommit) == RuntimeEventType::OscCommitRequested, "OSC commit payload maps to OSC commit event type");
Expect(oscCommit.generation == 42, "OSC commit payload keeps generation");
RuntimeMutationEvent acceptedMutation;
acceptedMutation.action = "SetLayerShader";
acceptedMutation.accepted = true;
acceptedMutation.shaderBuildRequested = true;
acceptedMutation.persistenceRequested = true;
Expect(RuntimeEventPayloadType(acceptedMutation) == RuntimeEventType::RuntimeMutationAccepted, "accepted mutation payload maps to accepted event type");
Expect(acceptedMutation.shaderBuildRequested, "mutation payload carries shader build follow-up");
Expect(acceptedMutation.persistenceRequested, "mutation payload carries persistence follow-up");
RuntimeMutationEvent rejectedMutation;
rejectedMutation.accepted = false;
rejectedMutation.errorMessage = "Unknown layer.";
Expect(RuntimeEventPayloadType(rejectedMutation) == RuntimeEventType::RuntimeMutationRejected, "rejected mutation payload maps to rejected event type");
Expect(rejectedMutation.errorMessage == "Unknown layer.", "mutation payload carries rejection error");
RuntimePersistenceRequestedEvent persistence;
persistence.request = PersistenceRequest::RuntimeStateRequest("UpdateLayerParameter");
Expect(RuntimeEventPayloadType(persistence) == RuntimeEventType::RuntimePersistenceRequested, "runtime persistence payload maps to persistence event type");
Expect(persistence.request.targetKind == PersistenceTargetKind::RuntimeState, "runtime persistence payload carries target kind");
Expect(persistence.request.reason == "UpdateLayerParameter", "runtime persistence payload carries request reason");
Expect(persistence.request.debounceAllowed, "runtime persistence payload carries debounce policy");
Expect(persistence.request.debounceKey == "runtime-state", "runtime persistence payload carries debounce key");
PersistenceSnapshot persistenceSnapshot;
persistenceSnapshot.targetKind = PersistenceTargetKind::RuntimeState;
persistenceSnapshot.reason = persistence.request.reason;
persistenceSnapshot.contents = "{}";
Expect(persistenceSnapshot.reason == "UpdateLayerParameter", "persistence snapshot carries capture reason");
Expect(persistenceSnapshot.contents == "{}", "persistence snapshot carries serialized content");
FileChangeDetectedEvent fileChange;
fileChange.path = "PollRuntimeStoreChanges";
fileChange.shaderPackageCandidate = true;
Expect(RuntimeEventPayloadType(fileChange) == RuntimeEventType::FileChangeDetected, "file change payload maps to file change event type");
Expect(fileChange.shaderPackageCandidate, "file change payload carries shader package candidate flag");
ManualReloadRequestedEvent manualReload;
manualReload.preserveFeedbackState = true;
manualReload.reason = "RequestShaderReload";
Expect(RuntimeEventPayloadType(manualReload) == RuntimeEventType::ManualReloadRequested, "manual reload payload maps to manual reload event type");
Expect(manualReload.preserveFeedbackState, "manual reload payload carries feedback preservation policy");
ShaderBuildEvent preparedBuild;
preparedBuild.phase = RuntimeEventShaderBuildPhase::Prepared;
preparedBuild.inputWidth = 1920;
preparedBuild.inputHeight = 1080;
Expect(RuntimeEventPayloadType(preparedBuild) == RuntimeEventType::ShaderBuildPrepared, "shader build payload maps by phase");
Expect(preparedBuild.inputWidth == 1920 && preparedBuild.inputHeight == 1080, "shader build payload carries input dimensions");
RenderResetEvent appliedReset;
appliedReset.scope = RuntimeEventRenderResetScope::TemporalHistoryAndFeedback;
appliedReset.applied = true;
Expect(RuntimeEventPayloadType(appliedReset) == RuntimeEventType::RenderResetApplied, "render reset payload maps applied state");
Expect(appliedReset.scope == RuntimeEventRenderResetScope::TemporalHistoryAndFeedback, "render reset payload carries reset scope");
RenderSnapshotPublishRequestedEvent snapshotRequest;
snapshotRequest.outputWidth = 1920;
snapshotRequest.outputHeight = 1080;
snapshotRequest.reason = "test";
Expect(RuntimeEventPayloadType(snapshotRequest) == RuntimeEventType::RenderSnapshotPublishRequested, "render snapshot request payload maps to request event");
RenderSnapshotPublishedEvent snapshotPublished;
snapshotPublished.snapshotVersion = 3;
snapshotPublished.parameterVersion = 4;
snapshotPublished.layerCount = 2;
Expect(RuntimeEventPayloadType(snapshotPublished) == RuntimeEventType::RenderSnapshotPublished, "render snapshot published payload maps to published event");
Expect(snapshotPublished.layerCount == 2, "render snapshot published payload carries layer count");
OutputFrameCompletedEvent completedFrame;
completedFrame.result = "Completed";
Expect(RuntimeEventPayloadType(completedFrame) == RuntimeEventType::OutputFrameCompleted, "completed output frame payload maps to completed event");
completedFrame.result = "DisplayedLate";
Expect(RuntimeEventPayloadType(completedFrame) == RuntimeEventType::OutputLateFrameDetected, "late output frame payload maps to late event");
completedFrame.result = "Dropped";
Expect(RuntimeEventPayloadType(completedFrame) == RuntimeEventType::OutputDroppedFrameDetected, "dropped output frame payload maps to dropped event");
TimingSampleRecordedEvent timingSample;
timingSample.subsystem = "RuntimeEventDispatcher";
timingSample.metric = "dispatchDuration";
timingSample.value = 0.5;
timingSample.unit = "ms";
Expect(RuntimeEventPayloadType(timingSample) == RuntimeEventType::TimingSampleRecorded, "timing sample payload maps to timing event");
QueueDepthChangedEvent queueDepth;
queueDepth.queueName = "runtime-events";
queueDepth.depth = 1;
queueDepth.capacity = 16;
Expect(RuntimeEventPayloadType(queueDepth) == RuntimeEventType::QueueDepthChanged, "queue depth payload maps to queue depth event");
SubsystemWarningEvent warning;
warning.subsystem = "VideoBackend";
warning.warningKey = "late-frame";
Expect(RuntimeEventPayloadType(warning) == RuntimeEventType::SubsystemWarningRaised, "warning payload maps to raised event by default");
warning.cleared = true;
Expect(RuntimeEventPayloadType(warning) == RuntimeEventType::SubsystemWarningCleared, "warning payload maps to cleared event when marked cleared");
}
void TestRuntimeEventEnvelope()
{
const auto createdAt = std::chrono::steady_clock::now();
OscCommitRequestedEvent oscCommit;
oscCommit.routeKey = "layer-1\namount";
oscCommit.layerKey = "layer-1";
oscCommit.parameterKey = "amount";
oscCommit.generation = 7;
RuntimeEvent event = MakeRuntimeEvent(oscCommit, "ControlServices", 12, createdAt);
Expect(event.type == RuntimeEventType::OscCommitRequested, "runtime event infers type from payload");
Expect(event.sequence == 12, "runtime event stores sequence");
Expect(event.source == "ControlServices", "runtime event stores source");
Expect(event.createdAt == createdAt, "runtime event stores creation time");
Expect(event.HasPayload(), "runtime event reports payload presence");
Expect(event.PayloadMatchesType(), "runtime event payload matches inferred type");
const auto* payload = std::get_if<OscCommitRequestedEvent>(&event.payload);
Expect(payload && payload->generation == 7, "runtime event stores typed payload in variant");
event.type = RuntimeEventType::RuntimeMutationAccepted;
Expect(!event.PayloadMatchesType(), "runtime event detects mismatched payload and type");
RuntimeEvent empty;
Expect(!empty.HasPayload(), "default runtime event has no payload");
Expect(empty.PayloadMatchesType(), "default runtime event unknown type matches empty payload");
RuntimeMutationEvent acceptedMutation;
acceptedMutation.accepted = true;
acceptedMutation.action = "SetLayerBypass";
RuntimeEvent acceptedEvent = MakeRuntimeEvent(acceptedMutation, "RuntimeCoordinator", 13, createdAt);
Expect(acceptedEvent.type == RuntimeEventType::RuntimeMutationAccepted, "runtime event handles payloads with dynamic accepted mapping");
RuntimeMutationEvent rejectedMutation;
rejectedMutation.accepted = false;
rejectedMutation.action = "SetLayerBypass";
RuntimeEvent rejectedEvent = MakeRuntimeEvent(rejectedMutation, "RuntimeCoordinator", 14, createdAt);
Expect(rejectedEvent.type == RuntimeEventType::RuntimeMutationRejected, "runtime event handles payloads with dynamic rejected mapping");
}
void TestRuntimeEventQueue()
{
RuntimeEventQueue queue(2);
const auto createdAt = std::chrono::steady_clock::now() - std::chrono::milliseconds(5);
RuntimeStateBroadcastRequestedEvent firstPayload;
firstPayload.reason = "first";
RuntimeStateBroadcastRequestedEvent secondPayload;
secondPayload.reason = "second";
RuntimeStateBroadcastRequestedEvent thirdPayload;
thirdPayload.reason = "third";
Expect(queue.Push(MakeRuntimeEvent(firstPayload, "test", 1, createdAt)), "queue accepts first event");
Expect(queue.Push(MakeRuntimeEvent(secondPayload, "test", 2, createdAt)), "queue accepts second event");
Expect(!queue.Push(MakeRuntimeEvent(thirdPayload, "test", 3, createdAt)), "queue rejects event when capacity is full");
RuntimeEventQueueMetrics fullMetrics = queue.GetMetrics(std::chrono::steady_clock::now());
Expect(fullMetrics.depth == 2, "queue metrics report depth");
Expect(fullMetrics.capacity == 2, "queue metrics report capacity");
Expect(fullMetrics.droppedCount == 1, "queue metrics report dropped count");
Expect(fullMetrics.oldestEventAgeMilliseconds >= 0.0, "queue metrics report oldest event age");
RuntimeEvent event;
Expect(queue.TryPop(event), "queue pops first event");
Expect(event.sequence == 1, "queue preserves FIFO order");
std::vector<RuntimeEvent> drained = queue.Drain();
Expect(drained.size() == 1, "queue drains remaining event");
Expect(drained[0].sequence == 2, "drained event preserves FIFO order");
Expect(queue.Depth() == 0, "queue is empty after drain");
}
void TestRuntimeEventDispatcher()
{
RuntimeEventDispatcher dispatcher(4);
int allHandlerCount = 0;
int broadcastHandlerCount = 0;
int failureHandlerCount = 0;
dispatcher.SubscribeAll([&](const RuntimeEvent& event) {
Expect(event.sequence != 0, "dispatcher assigns sequence before all-handler dispatch");
++allHandlerCount;
});
dispatcher.Subscribe(RuntimeEventType::RuntimeStateBroadcastRequested, [&](const RuntimeEvent& event) {
Expect(event.type == RuntimeEventType::RuntimeStateBroadcastRequested, "dispatcher invokes type-specific handler for matching event");
++broadcastHandlerCount;
});
dispatcher.Subscribe(RuntimeEventType::RuntimeStateBroadcastRequested, [&](const RuntimeEvent&) {
++failureHandlerCount;
throw std::runtime_error("test handler failure");
});
RuntimeStateBroadcastRequestedEvent broadcast;
broadcast.reason = "test";
Expect(dispatcher.PublishPayload(broadcast, "test"), "dispatcher publishes payload");
Expect(dispatcher.QueueDepth() == 1, "dispatcher reports queued depth");
RuntimeEventDispatchResult result = dispatcher.DispatchPending();
Expect(result.dispatchedEvents == 1, "dispatcher reports dispatched event count");
Expect(result.handlerInvocations == 3, "dispatcher reports handler invocation count");
Expect(result.handlerFailures == 1, "dispatcher catches and reports handler failures");
Expect(allHandlerCount == 1, "dispatcher invoked all-handler");
Expect(broadcastHandlerCount == 1, "dispatcher invoked type-specific handler");
Expect(failureHandlerCount == 1, "dispatcher invoked failing handler");
Expect(dispatcher.QueueDepth() == 0, "dispatcher queue is empty after dispatch");
RuntimeEvent mismatched = MakeRuntimeEvent(broadcast, "test");
mismatched.type = RuntimeEventType::ShaderBuildRequested;
Expect(!dispatcher.Publish(mismatched), "dispatcher rejects mismatched event type and payload");
RuntimeEventDispatcher tinyDispatcher(1);
RuntimeMutationEvent acceptedMutation;
acceptedMutation.accepted = true;
Expect(tinyDispatcher.PublishPayload(acceptedMutation, "test"), "tiny dispatcher accepts first FIFO event");
Expect(!tinyDispatcher.PublishPayload(acceptedMutation, "test"), "tiny dispatcher rejects FIFO event when queue is full");
RuntimeEventQueueMetrics metrics = tinyDispatcher.GetQueueMetrics();
Expect(metrics.droppedCount == 1, "dispatcher exposes queue drop metrics");
}
void TestRuntimeEventDispatcherCoalescing()
{
RuntimeEventDispatcher dispatcher(4);
std::string seenReason;
std::string seenShaderMessage;
double seenTimingValue = 0.0;
int broadcastHandlerCount = 0;
int shaderHandlerCount = 0;
int timingHandlerCount = 0;
dispatcher.Subscribe(RuntimeEventType::RuntimeStateBroadcastRequested, [&](const RuntimeEvent& event) {
const auto* payload = std::get_if<RuntimeStateBroadcastRequestedEvent>(&event.payload);
if (payload)
seenReason = payload->reason;
++broadcastHandlerCount;
});
dispatcher.Subscribe(RuntimeEventType::ShaderBuildRequested, [&](const RuntimeEvent& event) {
const auto* payload = std::get_if<ShaderBuildEvent>(&event.payload);
if (payload)
seenShaderMessage = payload->message;
++shaderHandlerCount;
});
dispatcher.Subscribe(RuntimeEventType::TimingSampleRecorded, [&](const RuntimeEvent& event) {
const auto* payload = std::get_if<TimingSampleRecordedEvent>(&event.payload);
if (payload)
seenTimingValue = payload->value;
++timingHandlerCount;
});
RuntimeStateBroadcastRequestedEvent first;
first.reason = "parameter";
RuntimeStateBroadcastRequestedEvent second;
second.reason = "reload";
Expect(dispatcher.PublishPayload(first, "RuntimeCoordinator"), "dispatcher accepts first coalescable event");
Expect(dispatcher.PublishPayload(second, "RuntimeCoordinator"), "dispatcher coalesces second matching event");
RuntimeEventQueueMetrics queuedMetrics = dispatcher.GetQueueMetrics();
Expect(queuedMetrics.depth == 1, "dispatcher reports coalesced event depth");
Expect(queuedMetrics.coalescedCount == 1, "dispatcher reports coalesced event count");
RuntimeEventDispatchResult result = dispatcher.DispatchPending();
Expect(result.dispatchedEvents == 1, "dispatcher dispatches one coalesced event");
Expect(broadcastHandlerCount == 1, "dispatcher invokes handler once for coalesced event");
Expect(seenReason == "reload", "dispatcher dispatches latest coalesced payload");
ShaderBuildEvent shaderFirst;
shaderFirst.phase = RuntimeEventShaderBuildPhase::Requested;
shaderFirst.inputWidth = 1920;
shaderFirst.inputHeight = 1080;
shaderFirst.preserveFeedbackState = true;
shaderFirst.message = "first";
ShaderBuildEvent shaderSecond = shaderFirst;
shaderSecond.message = "second";
Expect(dispatcher.PublishPayload(shaderFirst, "RuntimeCoordinator"), "dispatcher accepts first shader build request");
Expect(dispatcher.PublishPayload(shaderSecond, "RuntimeCoordinator"), "dispatcher coalesces matching shader build request");
result = dispatcher.DispatchPending();
Expect(result.dispatchedEvents == 1, "dispatcher dispatches one coalesced shader build request");
Expect(shaderHandlerCount == 1, "dispatcher invokes shader handler once for matching coalesced request");
Expect(seenShaderMessage == "second", "dispatcher dispatches latest shader build request payload");
TimingSampleRecordedEvent timingFirst;
timingFirst.subsystem = "RuntimeEventDispatcher";
timingFirst.metric = "dispatchDuration";
timingFirst.value = 1.0;
timingFirst.unit = "ms";
TimingSampleRecordedEvent timingSecond = timingFirst;
timingSecond.value = 2.0;
Expect(dispatcher.PublishPayload(timingFirst, "HealthTelemetry"), "dispatcher accepts first timing sample");
Expect(dispatcher.PublishPayload(timingSecond, "HealthTelemetry"), "dispatcher coalesces matching timing sample");
result = dispatcher.DispatchPending();
Expect(result.dispatchedEvents == 1, "dispatcher dispatches one coalesced timing sample");
Expect(timingHandlerCount == 1, "dispatcher invokes timing handler once for matching coalesced sample");
Expect(seenTimingValue == 2.0, "dispatcher dispatches latest timing sample payload");
}
void TestRuntimeEventCoalescingQueue()
{
RuntimeEventCoalescingQueue queue(2);
const auto createdAt = std::chrono::steady_clock::now() - std::chrono::milliseconds(5);
OscValueReceivedEvent first;
first.routeKey = "layer-1\namount";
first.layerKey = "layer-1";
first.parameterKey = "amount";
first.valueJson = "0.1";
first.generation = 1;
OscValueReceivedEvent replacement = first;
replacement.valueJson = "0.9";
replacement.generation = 2;
OscValueReceivedEvent otherRoute;
otherRoute.routeKey = "layer-2\namount";
otherRoute.layerKey = "layer-2";
otherRoute.parameterKey = "amount";
otherRoute.valueJson = "0.3";
otherRoute.generation = 3;
OscValueReceivedEvent overflow;
overflow.routeKey = "layer-3\namount";
overflow.layerKey = "layer-3";
overflow.parameterKey = "amount";
overflow.valueJson = "0.4";
overflow.generation = 4;
Expect(queue.Push(MakeRuntimeEvent(first, "ControlServices", 1, createdAt)), "coalescing queue accepts first keyed event");
Expect(queue.Push(MakeRuntimeEvent(replacement, "ControlServices", 2, std::chrono::steady_clock::now())), "coalescing queue replaces matching keyed event");
Expect(queue.Push(MakeRuntimeEvent(otherRoute, "ControlServices", 3, createdAt)), "coalescing queue accepts second keyed event");
Expect(!queue.Push(MakeRuntimeEvent(overflow, "ControlServices", 4, createdAt)), "coalescing queue rejects new key when full");
RuntimeEventCoalescingQueueMetrics metrics = queue.GetMetrics(std::chrono::steady_clock::now());
Expect(metrics.depth == 2, "coalescing queue metrics report unique-key depth");
Expect(metrics.capacity == 2, "coalescing queue metrics report capacity");
Expect(metrics.coalescedCount == 1, "coalescing queue metrics report coalesced count");
Expect(metrics.droppedCount == 1, "coalescing queue metrics report dropped count");
Expect(metrics.oldestEventAgeMilliseconds >= 0.0, "coalescing queue metrics report oldest event age");
std::vector<RuntimeEvent> drained = queue.Drain();
Expect(drained.size() == 2, "coalescing queue drains unique events");
Expect(drained[0].sequence == 2, "coalescing queue keeps latest replacement event");
Expect(drained[1].sequence == 3, "coalescing queue preserves first-seen key order");
const auto* latestPayload = std::get_if<OscValueReceivedEvent>(&drained[0].payload);
Expect(latestPayload && latestPayload->valueJson == "0.9", "coalescing queue keeps latest payload value");
Expect(queue.Depth() == 0, "coalescing queue is empty after drain");
}
void TestRuntimeEventCoalescingCustomKey()
{
RuntimeEventCoalescingQueue queue(4, [](const RuntimeEvent& event) {
return std::string(RuntimeEventTypeName(event.type));
});
RuntimeStateBroadcastRequestedEvent first;
first.reason = "parameter";
RuntimeStateBroadcastRequestedEvent second;
second.reason = "reload";
Expect(queue.Push(MakeRuntimeEvent(first, "RuntimeCoordinator", 10)), "custom-key coalescing queue accepts first event");
Expect(queue.Push(MakeRuntimeEvent(second, "RuntimeCoordinator", 11)), "custom-key coalescing queue coalesces second event by type");
std::vector<RuntimeEvent> drained = queue.Drain();
Expect(drained.size() == 1, "custom-key coalescing queue drains one coalesced event");
Expect(drained[0].sequence == 11, "custom-key coalescing queue keeps latest event");
const auto* payload = std::get_if<RuntimeStateBroadcastRequestedEvent>(&drained[0].payload);
Expect(payload && payload->reason == "reload", "custom-key coalescing queue keeps latest typed payload");
}
void TestRuntimeEventTestHarness()
{
RuntimeEventTestHarness harness;
RuntimeStateBroadcastRequestedEvent broadcast;
broadcast.reason = "parameter";
RuntimeEventDispatchResult firstDispatch = harness.PublishAndDispatch(broadcast, "RuntimeCoordinator");
Expect(firstDispatch.dispatchedEvents == 1, "test harness publishes and dispatches payloads");
Expect(harness.SeenCount() == 1, "test harness records dispatched events");
Expect(harness.SeenCount(RuntimeEventType::RuntimeStateBroadcastRequested) == 1, "test harness counts seen events by type");
const RuntimeEvent* seenBroadcast = harness.LastSeen(RuntimeEventType::RuntimeStateBroadcastRequested);
Expect(seenBroadcast && seenBroadcast->source == "RuntimeCoordinator", "test harness returns last seen event by type");
harness.ClearSeen();
Expect(harness.SeenCount() == 0, "test harness clears seen events");
OscValueReceivedEvent first;
first.routeKey = "layer-1\namount";
first.layerKey = "layer-1";
first.parameterKey = "amount";
first.valueJson = "0.1";
first.generation = 1;
OscValueReceivedEvent replacement = first;
replacement.valueJson = "0.8";
replacement.generation = 2;
Expect(harness.PublishCoalesced(first, "ControlServices", 20), "test harness accepts first coalesced payload");
Expect(harness.PublishCoalesced(replacement, "ControlServices", 21), "test harness accepts replacement coalesced payload");
RuntimeEventDispatchResult coalescedDispatch = harness.FlushCoalescedAndDispatch();
Expect(coalescedDispatch.dispatchedEvents == 1, "test harness dispatches one coalesced event");
Expect(harness.SeenCount(RuntimeEventType::OscValueReceived) == 1, "test harness records coalesced event");
const RuntimeEvent* seenOsc = harness.LastSeen(RuntimeEventType::OscValueReceived);
const auto* seenPayload = seenOsc ? std::get_if<OscValueReceivedEvent>(&seenOsc->payload) : nullptr;
Expect(seenPayload && seenPayload->valueJson == "0.8", "test harness keeps latest coalesced payload");
}
void TestAcceptedMutationFollowUps()
{
RuntimeEventTestHarness harness;
RuntimeMutationEvent mutation;
mutation.action = "SetLayerShader";
mutation.accepted = true;
mutation.runtimeStateChanged = true;
mutation.runtimeStateBroadcastRequired = true;
mutation.shaderBuildRequested = true;
mutation.persistenceRequested = true;
RuntimeStateChangedEvent stateChanged;
stateChanged.reason = mutation.action;
stateChanged.persistenceRequested = true;
RuntimePersistenceRequestedEvent persistence;
persistence.request = PersistenceRequest::RuntimeStateRequest(mutation.action);
RuntimeReloadRequestedEvent reload;
reload.reason = mutation.action;
reload.preserveFeedbackState = false;
ShaderBuildEvent build;
build.phase = RuntimeEventShaderBuildPhase::Requested;
build.succeeded = true;
build.message = "Shader rebuild queued.";
Expect(harness.Publish(mutation, "RuntimeCoordinator"), "accepted mutation event publishes");
Expect(harness.Publish(stateChanged, "RuntimeCoordinator"), "state changed follow-up publishes");
Expect(harness.Publish(persistence, "RuntimeCoordinator"), "persistence follow-up publishes");
Expect(harness.Publish(reload, "RuntimeCoordinator"), "reload follow-up publishes");
Expect(harness.Publish(build, "RuntimeCoordinator"), "shader build follow-up publishes");
RuntimeEventDispatchResult result = harness.DispatchPending();
Expect(result.dispatchedEvents == 5, "accepted mutation dispatches every expected follow-up");
Expect(harness.SeenCount(RuntimeEventType::RuntimeMutationAccepted) == 1, "accepted mutation fact is observed");
Expect(harness.SeenCount(RuntimeEventType::RuntimeStateChanged) == 1, "accepted mutation publishes state changed follow-up");
Expect(harness.SeenCount(RuntimeEventType::RuntimePersistenceRequested) == 1, "accepted mutation publishes persistence follow-up");
Expect(harness.SeenCount(RuntimeEventType::RuntimeReloadRequested) == 1, "accepted mutation publishes reload follow-up");
Expect(harness.SeenCount(RuntimeEventType::ShaderBuildRequested) == 1, "accepted mutation publishes shader build follow-up");
const RuntimeEvent* persistenceEvent = harness.LastSeen(RuntimeEventType::RuntimePersistenceRequested);
const auto* persistencePayload = persistenceEvent ? std::get_if<RuntimePersistenceRequestedEvent>(&persistenceEvent->payload) : nullptr;
Expect(persistencePayload && persistencePayload->request.reason == "SetLayerShader", "persistence follow-up preserves mutation action reason");
Expect(persistencePayload && persistencePayload->request.debounceKey == "runtime-state", "persistence follow-up preserves debounce key");
}
void TestAppLevelBroadcastAndBuildCoalescing()
{
RuntimeEventTestHarness harness;
RuntimeMutationEvent firstMutation;
firstMutation.action = "SetLayerShader";
firstMutation.accepted = true;
firstMutation.runtimeStateChanged = true;
firstMutation.runtimeStateBroadcastRequired = true;
firstMutation.shaderBuildRequested = true;
RuntimeMutationEvent secondMutation = firstMutation;
secondMutation.action = "LoadStackPreset";
RuntimeStateBroadcastRequestedEvent firstBroadcast;
firstBroadcast.reason = "SetLayerShader";
RuntimeStateBroadcastRequestedEvent secondBroadcast;
secondBroadcast.reason = "LoadStackPreset";
ShaderBuildEvent firstBuild;
firstBuild.phase = RuntimeEventShaderBuildPhase::Requested;
firstBuild.inputWidth = 1920;
firstBuild.inputHeight = 1080;
firstBuild.preserveFeedbackState = false;
firstBuild.message = "first build request";
ShaderBuildEvent secondBuild = firstBuild;
secondBuild.message = "second build request";
Expect(harness.Publish(firstMutation, "RuntimeCoordinator"), "first accepted mutation fact publishes");
Expect(harness.Publish(firstBroadcast, "RuntimeUpdateController"), "first broadcast request publishes through app dispatcher");
Expect(harness.Publish(firstBuild, "RuntimeCoordinator"), "first shader build request publishes through app dispatcher");
Expect(harness.Publish(secondMutation, "RuntimeCoordinator"), "second accepted mutation fact publishes");
Expect(harness.Publish(secondBroadcast, "RuntimeUpdateController"), "second broadcast request coalesces through app dispatcher");
Expect(harness.Publish(secondBuild, "RuntimeCoordinator"), "second shader build request coalesces through app dispatcher");
RuntimeEventQueueMetrics metrics = harness.Dispatcher().GetQueueMetrics();
Expect(metrics.depth == 4, "app dispatcher keeps FIFO facts plus coalesced broadcast/build requests");
Expect(metrics.coalescedCount == 2, "app dispatcher reports broadcast and build coalescing");
RuntimeEventDispatchResult result = harness.DispatchPending();
Expect(result.dispatchedEvents == 4, "app dispatcher dispatches FIFO facts plus one broadcast and one build request");
Expect(harness.SeenCount(RuntimeEventType::RuntimeMutationAccepted) == 2, "app dispatcher preserves every accepted mutation fact");
Expect(harness.SeenCount(RuntimeEventType::RuntimeStateBroadcastRequested) == 1, "app dispatcher coalesces broadcast requests");
Expect(harness.SeenCount(RuntimeEventType::ShaderBuildRequested) == 1, "app dispatcher coalesces matching shader build requests");
const RuntimeEvent* broadcastEvent = harness.LastSeen(RuntimeEventType::RuntimeStateBroadcastRequested);
const auto* broadcastPayload = broadcastEvent ? std::get_if<RuntimeStateBroadcastRequestedEvent>(&broadcastEvent->payload) : nullptr;
Expect(broadcastPayload && broadcastPayload->reason == "LoadStackPreset", "app dispatcher dispatches latest broadcast request");
const RuntimeEvent* buildEvent = harness.LastSeen(RuntimeEventType::ShaderBuildRequested);
const auto* buildPayload = buildEvent ? std::get_if<ShaderBuildEvent>(&buildEvent->payload) : nullptr;
Expect(buildPayload && buildPayload->message == "second build request", "app dispatcher dispatches latest shader build request");
}
void TestManualReloadBridgeEvents()
{
RuntimeEventTestHarness harness;
ManualReloadRequestedEvent manualReload;
manualReload.preserveFeedbackState = true;
manualReload.reason = "RequestShaderReload";
RuntimeReloadRequestedEvent runtimeReload;
runtimeReload.preserveFeedbackState = true;
runtimeReload.reason = "RequestShaderReload";
ShaderBuildEvent shaderBuild;
shaderBuild.phase = RuntimeEventShaderBuildPhase::Requested;
shaderBuild.preserveFeedbackState = true;
shaderBuild.message = "Shader rebuild queued.";
Expect(harness.Publish(manualReload, "RuntimeCoordinator"), "manual reload ingress event publishes");
Expect(harness.Publish(runtimeReload, "RuntimeCoordinator"), "manual reload bridge publishes runtime reload request");
Expect(harness.Publish(shaderBuild, "RuntimeCoordinator"), "manual reload bridge publishes shader build request");
RuntimeEventDispatchResult result = harness.DispatchPending();
Expect(result.dispatchedEvents == 3, "manual reload bridge dispatches ingress and follow-up events");
Expect(harness.SeenCount(RuntimeEventType::ManualReloadRequested) == 1, "manual reload ingress event is observed");
Expect(harness.SeenCount(RuntimeEventType::RuntimeReloadRequested) == 1, "manual reload runtime reload follow-up is observed");
Expect(harness.SeenCount(RuntimeEventType::ShaderBuildRequested) == 1, "manual reload shader build follow-up is observed");
const RuntimeEvent* reloadEvent = harness.LastSeen(RuntimeEventType::RuntimeReloadRequested);
const auto* reloadPayload = reloadEvent ? std::get_if<RuntimeReloadRequestedEvent>(&reloadEvent->payload) : nullptr;
Expect(reloadPayload && reloadPayload->preserveFeedbackState, "manual reload bridge preserves feedback policy in runtime reload event");
}
void TestFileReloadBridgeEvents()
{
RuntimeEventTestHarness harness;
FileChangeDetectedEvent fileChange;
fileChange.path = "PollRuntimeStoreChanges";
fileChange.shaderPackageCandidate = true;
RuntimeReloadRequestedEvent runtimeReload;
runtimeReload.preserveFeedbackState = false;
runtimeReload.reason = "PollRuntimeStoreChanges";
ShaderBuildEvent shaderBuild;
shaderBuild.phase = RuntimeEventShaderBuildPhase::Requested;
shaderBuild.preserveFeedbackState = false;
shaderBuild.message = "Shader rebuild queued.";
Expect(harness.Publish(fileChange, "RuntimeCoordinator"), "file change ingress event publishes");
Expect(harness.Publish(runtimeReload, "RuntimeCoordinator"), "file change bridge publishes runtime reload request");
Expect(harness.Publish(shaderBuild, "RuntimeCoordinator"), "file change bridge publishes shader build request");
RuntimeEventDispatchResult result = harness.DispatchPending();
Expect(result.dispatchedEvents == 3, "file reload bridge dispatches ingress and follow-up events");
Expect(harness.SeenCount(RuntimeEventType::FileChangeDetected) == 1, "file change ingress event is observed");
Expect(harness.SeenCount(RuntimeEventType::RuntimeReloadRequested) == 1, "file reload runtime reload follow-up is observed");
Expect(harness.SeenCount(RuntimeEventType::ShaderBuildRequested) == 1, "file reload shader build follow-up is observed");
const RuntimeEvent* fileEvent = harness.LastSeen(RuntimeEventType::FileChangeDetected);
const auto* filePayload = fileEvent ? std::get_if<FileChangeDetectedEvent>(&fileEvent->payload) : nullptr;
Expect(filePayload && filePayload->shaderPackageCandidate, "file reload bridge marks shader package candidate changes");
}
void TestRejectedMutationHasNoDownstreamFollowUps()
{
RuntimeEventTestHarness harness;
RuntimeMutationEvent mutation;
mutation.action = "SetLayerShader";
mutation.accepted = false;
mutation.errorMessage = "Unknown shader id: missing";
Expect(harness.Publish(mutation, "RuntimeCoordinator"), "rejected mutation event publishes");
RuntimeEventDispatchResult result = harness.DispatchPending();
Expect(result.dispatchedEvents == 1, "rejected mutation dispatches only the rejection fact");
Expect(harness.SeenCount(RuntimeEventType::RuntimeMutationRejected) == 1, "rejected mutation fact is observed");
Expect(harness.SeenCount(RuntimeEventType::RuntimeStateChanged) == 0, "rejected mutation has no state follow-up");
Expect(harness.SeenCount(RuntimeEventType::RuntimePersistenceRequested) == 0, "rejected mutation has no persistence follow-up");
Expect(harness.SeenCount(RuntimeEventType::RuntimeReloadRequested) == 0, "rejected mutation has no reload follow-up");
Expect(harness.SeenCount(RuntimeEventType::ShaderBuildRequested) == 0, "rejected mutation has no shader build follow-up");
const RuntimeEvent* rejectedEvent = harness.LastSeen(RuntimeEventType::RuntimeMutationRejected);
const auto* rejectedPayload = rejectedEvent ? std::get_if<RuntimeMutationEvent>(&rejectedEvent->payload) : nullptr;
Expect(rejectedPayload && rejectedPayload->errorMessage == "Unknown shader id: missing", "rejected mutation preserves error message");
}
void TestShaderBuildGenerationEventMatching()
{
RuntimeEventTestHarness harness;
std::size_t handledBuilds = 0;
uint64_t handledGeneration = 0;
harness.Dispatcher().Subscribe(RuntimeEventType::ShaderBuildPrepared, [&](const RuntimeEvent& event) {
const auto* payload = std::get_if<ShaderBuildEvent>(&event.payload);
if (!payload || payload->generation != 7)
return;
++handledBuilds;
handledGeneration = payload->generation;
});
ShaderBuildEvent stale;
stale.phase = RuntimeEventShaderBuildPhase::Prepared;
stale.generation = 6;
stale.succeeded = true;
ShaderBuildEvent current = stale;
current.generation = 7;
Expect(harness.Publish(stale, "ShaderBuildQueue"), "stale shader build event publishes");
Expect(harness.Publish(current, "ShaderBuildQueue"), "current shader build event publishes");
RuntimeEventDispatchResult result = harness.DispatchPending();
Expect(result.dispatchedEvents == 2, "shader build readiness events dispatch in order");
Expect(harness.SeenCount(RuntimeEventType::ShaderBuildPrepared) == 2, "both shader build readiness facts are observable");
Expect(handledBuilds == 1, "generation-aware handler applies only the expected build once");
Expect(handledGeneration == 7, "generation-aware handler records the expected generation");
}
void TestHandlerFailureCanBecomeTelemetryEvent()
{
RuntimeEventTestHarness harness;
harness.Dispatcher().Subscribe(RuntimeEventType::RuntimeStateBroadcastRequested, [](const RuntimeEvent&) {
throw std::runtime_error("handler failed");
});
RuntimeStateBroadcastRequestedEvent broadcast;
broadcast.reason = "test";
Expect(harness.Publish(broadcast, "test"), "broadcast event publishes before failing handler");
RuntimeEventDispatchResult result = harness.DispatchPending();
Expect(result.handlerFailures == 1, "dispatcher reports handler failure for telemetry");
TimingSampleRecordedEvent timing;
timing.subsystem = "RuntimeEventDispatcher";
timing.metric = "handlerFailures";
timing.value = static_cast<double>(result.handlerFailures);
timing.unit = "count";
Expect(harness.Publish(timing, "HealthTelemetry"), "handler failure timing sample publishes");
harness.DispatchPending();
Expect(harness.SeenCount(RuntimeEventType::TimingSampleRecorded) == 1, "handler failure can be observed as telemetry event");
}
}
int main()
{
TestRuntimeEventTypeNames();
TestRuntimeEventPayloadTypes();
TestRuntimeEventEnvelope();
TestRuntimeEventQueue();
TestRuntimeEventDispatcher();
TestRuntimeEventDispatcherCoalescing();
TestRuntimeEventCoalescingQueue();
TestRuntimeEventCoalescingCustomKey();
TestRuntimeEventTestHarness();
TestAcceptedMutationFollowUps();
TestAppLevelBroadcastAndBuildCoalescing();
TestManualReloadBridgeEvents();
TestFileReloadBridgeEvents();
TestRejectedMutationHasNoDownstreamFollowUps();
TestShaderBuildGenerationEventMatching();
TestHandlerFailureCanBecomeTelemetryEvent();
if (gFailures != 0)
{
std::cerr << gFailures << " RuntimeEventType test failure(s).\n";
return 1;
}
std::cout << "RuntimeEventType tests passed.\n";
return 0;
}

View File

@@ -1,611 +0,0 @@
#include "RenderStateComposer.h"
#include "RuntimeLiveState.h"
#include <chrono>
#include <cmath>
#include <iostream>
namespace
{
int gFailures = 0;
void Expect(bool condition, const char* message)
{
if (condition)
return;
std::cerr << "FAIL: " << message << "\n";
++gFailures;
}
ShaderParameterDefinition FloatDefinition(const std::string& id, const std::string& label)
{
ShaderParameterDefinition definition;
definition.id = id;
definition.label = label;
definition.type = ShaderParameterType::Float;
definition.defaultNumbers = { 0.0 };
definition.minNumbers = { 0.0 };
definition.maxNumbers = { 1.0 };
return definition;
}
ShaderParameterDefinition Vec2Definition(const std::string& id, const std::string& label)
{
ShaderParameterDefinition definition;
definition.id = id;
definition.label = label;
definition.type = ShaderParameterType::Vec2;
definition.defaultNumbers = { 0.0, 0.0 };
definition.minNumbers = { 0.0, 0.0 };
definition.maxNumbers = { 1.0, 1.0 };
return definition;
}
ShaderParameterDefinition TriggerDefinition(const std::string& id, const std::string& label)
{
ShaderParameterDefinition definition;
definition.id = id;
definition.label = label;
definition.type = ShaderParameterType::Trigger;
return definition;
}
JsonValue NumberArray(std::initializer_list<double> numbers)
{
JsonValue value = JsonValue::MakeArray();
for (double number : numbers)
value.pushBack(JsonValue(number));
return value;
}
RuntimeRenderState MakeLayerState()
{
RuntimeRenderState state;
state.layerId = "layer-one";
state.shaderId = "test-shader";
state.shaderName = "Test Shader";
state.parameterDefinitions.push_back(FloatDefinition("amount", "Amount"));
ShaderParameterValue amount;
amount.numberValues = { 0.25 };
state.parameterValues["amount"] = amount;
return state;
}
RuntimeRenderState MakeLayerStateWithDefinitions(const std::vector<ShaderParameterDefinition>& definitions)
{
RuntimeRenderState state;
state.layerId = "layer-one";
state.shaderId = "test-shader";
state.shaderName = "Test Shader";
state.parameterDefinitions = definitions;
return state;
}
void TestRuntimeLiveStateAppliesLatestOscOverlay()
{
RuntimeLiveState liveState;
RuntimeLiveOscUpdate first;
first.routeKey = "layer-one\namount";
first.layerKey = "layer-one";
first.parameterKey = "amount";
first.targetValue = JsonValue(0.5);
RuntimeLiveOscUpdate second = first;
second.targetValue = JsonValue(0.75);
liveState.ApplyOscUpdates({ first, second });
Expect(liveState.OverlayCount() == 1, "live state keeps one overlay per route");
std::vector<RuntimeRenderState> states = { MakeLayerState() };
RuntimeLiveStateApplyOptions options;
options.allowCommit = false;
options.smoothing = 0.0;
liveState.ApplyToLayerStates(states, options, nullptr);
const auto valueIt = states[0].parameterValues.find("amount");
Expect(valueIt != states[0].parameterValues.end(), "overlay writes the target parameter");
Expect(!valueIt->second.numberValues.empty() && std::fabs(valueIt->second.numberValues[0] - 0.75) < 0.0001,
"overlay applies the latest target value");
}
void TestRuntimeLiveStateIgnoresStaleCommitCompletions()
{
RuntimeLiveState liveState;
RuntimeLiveOscUpdate update;
update.routeKey = "layer-one\namount";
update.layerKey = "layer-one";
update.parameterKey = "amount";
update.targetValue = JsonValue(0.9);
liveState.ApplyOscUpdates({ update });
std::vector<RuntimeRenderState> states = { MakeLayerState() };
std::vector<RuntimeLiveOscCommitRequest> commitRequests;
RuntimeLiveStateApplyOptions options;
options.allowCommit = true;
options.smoothing = 0.0;
options.commitDelay = std::chrono::milliseconds(0);
options.now = std::chrono::steady_clock::now() + std::chrono::milliseconds(1);
liveState.ApplyToLayerStates(states, options, &commitRequests);
Expect(commitRequests.size() == 1, "initial commit request is queued");
liveState.ApplyOscCommitCompletions({ { "other-route", commitRequests[0].generation } });
Expect(liveState.OverlayCount() == 1, "completion for another route does not remove overlay");
liveState.ApplyOscCommitCompletions({ { commitRequests[0].routeKey, commitRequests[0].generation + 1 } });
Expect(liveState.OverlayCount() == 1, "completion for another generation does not remove overlay");
RuntimeLiveOscUpdate newerUpdate = update;
newerUpdate.targetValue = JsonValue(0.2);
liveState.ApplyOscUpdates({ newerUpdate });
liveState.ApplyOscCommitCompletions({ { commitRequests[0].routeKey, commitRequests[0].generation } });
Expect(liveState.OverlayCount() == 1, "stale completion for previous generation is ignored after newer update");
}
void TestRuntimeLiveStateQueuesAndCompletesCommit()
{
RuntimeLiveState liveState;
RuntimeLiveOscUpdate update;
update.routeKey = "layer-one\namount";
update.layerKey = "layer-one";
update.parameterKey = "amount";
update.targetValue = JsonValue(0.9);
liveState.ApplyOscUpdates({ update });
std::vector<RuntimeRenderState> states = { MakeLayerState() };
std::vector<RuntimeLiveOscCommitRequest> commitRequests;
RuntimeLiveStateApplyOptions options;
options.allowCommit = true;
options.smoothing = 0.0;
options.commitDelay = std::chrono::milliseconds(0);
options.now = std::chrono::steady_clock::now() + std::chrono::milliseconds(1);
liveState.ApplyToLayerStates(states, options, &commitRequests);
Expect(commitRequests.size() == 1, "live state queues a commit request once the overlay can settle");
Expect(commitRequests[0].routeKey == "layer-one\namount", "commit request preserves route");
Expect(commitRequests[0].generation == 1, "commit request carries overlay generation");
liveState.ApplyOscCommitCompletions({ { commitRequests[0].routeKey, commitRequests[0].generation } });
Expect(liveState.OverlayCount() == 0, "matching commit completion removes settled overlay");
}
void TestRuntimeLiveStateQueuesOneCommitPerGeneration()
{
RuntimeLiveState liveState;
RuntimeLiveOscUpdate update;
update.routeKey = "layer-one\namount";
update.layerKey = "layer-one";
update.parameterKey = "amount";
update.targetValue = JsonValue(0.8);
liveState.ApplyOscUpdates({ update });
RuntimeLiveStateApplyOptions options;
options.allowCommit = true;
options.smoothing = 0.0;
options.commitDelay = std::chrono::milliseconds(0);
options.now = std::chrono::steady_clock::now() + std::chrono::milliseconds(1);
std::vector<RuntimeRenderState> states = { MakeLayerState() };
std::vector<RuntimeLiveOscCommitRequest> commitRequests;
liveState.ApplyToLayerStates(states, options, &commitRequests);
Expect(commitRequests.size() == 1, "first apply queues one commit for generation");
Expect(commitRequests[0].generation == 1, "first commit uses generation one");
commitRequests.clear();
options.now += std::chrono::milliseconds(1);
liveState.ApplyToLayerStates(states, options, &commitRequests);
Expect(commitRequests.empty(), "second apply does not duplicate commit for same generation");
RuntimeLiveOscUpdate newerUpdate = update;
newerUpdate.targetValue = JsonValue(0.4);
liveState.ApplyOscUpdates({ newerUpdate });
commitRequests.clear();
options.now += std::chrono::milliseconds(1);
liveState.ApplyToLayerStates(states, options, &commitRequests);
Expect(commitRequests.size() == 1, "newer update allows a new commit request");
Expect(commitRequests[0].generation == 2, "new commit uses newer generation");
}
void TestRuntimeLiveStateSmoothingZeroAppliesTargetImmediately()
{
RuntimeLiveState liveState;
RuntimeLiveOscUpdate update;
update.routeKey = "layer-one\namount";
update.layerKey = "layer-one";
update.parameterKey = "amount";
update.targetValue = JsonValue(1.0);
liveState.ApplyOscUpdates({ update });
std::vector<RuntimeRenderState> states = { MakeLayerState() };
RuntimeLiveStateApplyOptions options;
options.smoothing = 0.0;
liveState.ApplyToLayerStates(states, options, nullptr);
const auto valueIt = states[0].parameterValues.find("amount");
Expect(valueIt != states[0].parameterValues.end(), "smoothing zero writes amount");
Expect(!valueIt->second.numberValues.empty() && std::fabs(valueIt->second.numberValues[0] - 1.0) < 0.0001,
"smoothing zero applies target immediately");
}
void TestRuntimeLiveStateSmoothingOneConvergesImmediately()
{
RuntimeLiveState liveState;
RuntimeLiveOscUpdate update;
update.routeKey = "layer-one\namount";
update.layerKey = "layer-one";
update.parameterKey = "amount";
update.targetValue = JsonValue(1.0);
liveState.ApplyOscUpdates({ update });
std::vector<RuntimeRenderState> states = { MakeLayerState() };
RuntimeLiveStateApplyOptions options;
options.smoothing = 1.0;
options.now = std::chrono::steady_clock::now() + std::chrono::milliseconds(16);
liveState.ApplyToLayerStates(states, options, nullptr);
const auto valueIt = states[0].parameterValues.find("amount");
Expect(valueIt != states[0].parameterValues.end(), "smoothing one writes amount");
Expect(!valueIt->second.numberValues.empty() && std::fabs(valueIt->second.numberValues[0] - 1.0) < 0.0001,
"smoothing one converges immediately");
}
void TestRuntimeLiveStateSmoothingPartiallyConverges()
{
RuntimeLiveState liveState;
RuntimeLiveOscUpdate update;
update.routeKey = "layer-one\namount";
update.layerKey = "layer-one";
update.parameterKey = "amount";
update.targetValue = JsonValue(1.0);
liveState.ApplyOscUpdates({ update });
std::vector<RuntimeRenderState> states = { MakeLayerState() };
ShaderParameterValue amount;
amount.numberValues = { 0.0 };
states[0].parameterValues["amount"] = amount;
RuntimeLiveStateApplyOptions options;
options.smoothing = 0.5;
options.now = std::chrono::steady_clock::now() + std::chrono::milliseconds(16);
liveState.ApplyToLayerStates(states, options, nullptr);
const auto valueIt = states[0].parameterValues.find("amount");
Expect(valueIt != states[0].parameterValues.end(), "partial smoothing writes amount");
Expect(!valueIt->second.numberValues.empty() &&
valueIt->second.numberValues[0] > 0.0 &&
valueIt->second.numberValues[0] < 1.0,
"partial smoothing advances toward target without snapping");
}
void TestRuntimeLiveStateSmoothingVectorSizeMismatchUsesTargetShape()
{
RuntimeLiveState liveState;
RuntimeLiveOscUpdate update;
update.routeKey = "layer-one\noffset";
update.layerKey = "layer-one";
update.parameterKey = "offset";
update.targetValue = NumberArray({ 0.25, 0.75 });
liveState.ApplyOscUpdates({ update });
std::vector<RuntimeRenderState> states = { MakeLayerStateWithDefinitions({ Vec2Definition("offset", "Offset") }) };
ShaderParameterValue malformedOffset;
malformedOffset.numberValues = { 0.1 };
states[0].parameterValues["offset"] = malformedOffset;
RuntimeLiveStateApplyOptions options;
options.smoothing = 0.5;
options.now = std::chrono::steady_clock::now() + std::chrono::milliseconds(16);
liveState.ApplyToLayerStates(states, options, nullptr);
const auto valueIt = states[0].parameterValues.find("offset");
Expect(valueIt != states[0].parameterValues.end(), "vector mismatch writes offset");
Expect(valueIt->second.numberValues.size() == 2, "vector mismatch restores target vector size");
Expect(valueIt->second.numberValues.size() == 2 &&
std::fabs(valueIt->second.numberValues[0] - 0.25) < 0.0001 &&
std::fabs(valueIt->second.numberValues[1] - 0.75) < 0.0001,
"vector mismatch snaps to validated target shape");
}
void TestRuntimeLiveStateTriggerOverlayIncrementsAndClears()
{
RuntimeLiveState liveState;
RuntimeLiveOscUpdate update;
update.routeKey = "layer-one\npulse";
update.layerKey = "layer-one";
update.parameterKey = "pulse";
update.targetValue = JsonValue(true);
liveState.ApplyOscUpdates({ update });
std::vector<RuntimeRenderState> states = { MakeLayerStateWithDefinitions({ TriggerDefinition("pulse", "Pulse") }) };
states[0].timeSeconds = 42.0;
ShaderParameterValue pulse;
pulse.numberValues = { 2.0, 10.0 };
states[0].parameterValues["pulse"] = pulse;
std::vector<RuntimeLiveOscCommitRequest> commitRequests;
RuntimeLiveStateApplyOptions options;
options.allowCommit = true;
options.smoothing = 0.0;
options.commitDelay = std::chrono::milliseconds(0);
liveState.ApplyToLayerStates(states, options, &commitRequests);
const auto valueIt = states[0].parameterValues.find("pulse");
Expect(valueIt != states[0].parameterValues.end(), "trigger overlay writes pulse");
Expect(valueIt->second.numberValues.size() == 2 &&
std::fabs(valueIt->second.numberValues[0] - 3.0) < 0.0001 &&
std::fabs(valueIt->second.numberValues[1] - 42.0) < 0.0001,
"trigger overlay increments count and stamps layer time");
Expect(commitRequests.empty(), "trigger overlay does not queue commit");
Expect(liveState.OverlayCount() == 0, "trigger overlay clears after apply");
}
void TestRuntimeLiveStateClearsOverlaysForLayerKey()
{
RuntimeLiveState liveState;
RuntimeLiveOscUpdate first;
first.routeKey = "layer-one\namount";
first.layerKey = "layer-one";
first.parameterKey = "amount";
first.targetValue = JsonValue(0.5);
RuntimeLiveOscUpdate second = first;
second.routeKey = "layer-two\namount";
second.layerKey = "layer-two";
second.targetValue = JsonValue(0.75);
liveState.ApplyOscUpdates({ first, second });
liveState.ClearForLayerKey("layer-one");
Expect(liveState.OverlayCount() == 1, "layer-scoped invalidation only removes matching overlays");
std::vector<RuntimeRenderState> states = { MakeLayerState() };
states[0].layerId = "layer-two";
RuntimeLiveStateApplyOptions options;
options.smoothing = 0.0;
liveState.ApplyToLayerStates(states, options, nullptr);
const auto valueIt = states[0].parameterValues.find("amount");
Expect(valueIt != states[0].parameterValues.end() &&
!valueIt->second.numberValues.empty() &&
std::fabs(valueIt->second.numberValues[0] - 0.75) < 0.0001,
"unmatched layer invalidation preserves unrelated overlay");
}
void TestRuntimeLiveStatePrunesRemovedLayerOverlay()
{
RuntimeLiveState liveState;
RuntimeLiveOscUpdate update;
update.routeKey = "removed-layer\namount";
update.layerKey = "removed-layer";
update.parameterKey = "amount";
update.targetValue = JsonValue(0.5);
liveState.ApplyOscUpdates({ update });
std::vector<RuntimeRenderState> states = { MakeLayerState() };
liveState.PruneIncompatibleOverlays(states);
Expect(liveState.OverlayCount() == 0, "invalidation policy removes overlays for missing layers");
}
void TestRuntimeLiveStatePrunesIncompatibleParameterOverlay()
{
RuntimeLiveState liveState;
RuntimeLiveOscUpdate update;
update.routeKey = "layer-one\namount";
update.layerKey = "layer-one";
update.parameterKey = "amount";
update.targetValue = JsonValue(0.5);
liveState.ApplyOscUpdates({ update });
std::vector<RuntimeRenderState> states = { MakeLayerStateWithDefinitions({ FloatDefinition("other", "Other") }) };
liveState.PruneIncompatibleOverlays(states);
Expect(liveState.OverlayCount() == 0, "invalidation policy removes overlays for missing parameters");
}
void TestRuntimeLiveStatePreservesCompatibleOverlayAfterShaderReload()
{
RuntimeLiveState liveState;
RuntimeLiveOscUpdate update;
update.routeKey = "layer-one\namount";
update.layerKey = "layer-one";
update.parameterKey = "Amount";
update.targetValue = JsonValue(0.5);
liveState.ApplyOscUpdates({ update });
std::vector<RuntimeRenderState> states = { MakeLayerStateWithDefinitions({ FloatDefinition("amount-renamed", "Amount") }) };
liveState.PruneIncompatibleOverlays(states);
Expect(liveState.OverlayCount() == 1, "invalidation policy preserves overlays that still map by control key");
RuntimeLiveStateApplyOptions options;
options.smoothing = 0.0;
liveState.ApplyToLayerStates(states, options, nullptr);
const auto valueIt = states[0].parameterValues.find("amount-renamed");
Expect(valueIt != states[0].parameterValues.end() &&
!valueIt->second.numberValues.empty() &&
std::fabs(valueIt->second.numberValues[0] - 0.5) < 0.0001,
"compatible overlay applies to the reloaded parameter id");
}
void TestRenderStateComposerBuildsFrameState()
{
RuntimeLiveState liveState;
RuntimeLiveOscUpdate update;
update.routeKey = "layer-one\namount";
update.layerKey = "Test Shader";
update.parameterKey = "Amount";
update.targetValue = JsonValue(0.6);
liveState.ApplyOscUpdates({ update });
LayeredRenderStateInput input;
std::vector<RuntimeRenderState> baseLayerStates = { MakeLayerState() };
input.committedLiveLayerStates = &baseLayerStates;
input.transientAutomationOverlay = &liveState;
input.allowTransientAutomationCommits = false;
input.transientAutomationSmoothing = 0.0;
RenderStateComposer composer;
RenderStateCompositionResult result = composer.BuildFrameState(input);
Expect(result.hasLayerStates, "composer reports that it composed base layer states");
Expect(result.layerStates.size() == 1, "composer returns composed layer state");
const auto valueIt = result.layerStates[0].parameterValues.find("amount");
Expect(valueIt != result.layerStates[0].parameterValues.end(), "composer applies live overlay through live state");
Expect(!valueIt->second.numberValues.empty() && std::fabs(valueIt->second.numberValues[0] - 0.6) < 0.0001,
"composer uses OSC key matching against shader names and labels");
const auto baseValueIt = baseLayerStates[0].parameterValues.find("amount");
Expect(baseValueIt != baseLayerStates[0].parameterValues.end() &&
!baseValueIt->second.numberValues.empty() &&
std::fabs(baseValueIt->second.numberValues[0] - 0.25) < 0.0001,
"composer leaves base layer states unchanged");
}
void TestRenderStateComposerUsesCommittedLayerOverBaseLayer()
{
std::vector<RuntimeRenderState> basePersistedLayerStates = { MakeLayerState() };
std::vector<RuntimeRenderState> committedLiveLayerStates = { MakeLayerState() };
committedLiveLayerStates[0].parameterValues["amount"].numberValues = { 0.4 };
LayeredRenderStateInput input;
input.basePersistedLayerStates = &basePersistedLayerStates;
input.committedLiveLayerStates = &committedLiveLayerStates;
RenderStateComposer composer;
RenderStateCompositionResult result = composer.BuildFrameState(input);
const auto valueIt = result.layerStates[0].parameterValues.find("amount");
Expect(valueIt != result.layerStates[0].parameterValues.end() &&
!valueIt->second.numberValues.empty() &&
std::fabs(valueIt->second.numberValues[0] - 0.4) < 0.0001,
"committed live layer overrides base persisted layer");
const auto baseValueIt = basePersistedLayerStates[0].parameterValues.find("amount");
Expect(baseValueIt != basePersistedLayerStates[0].parameterValues.end() &&
!baseValueIt->second.numberValues.empty() &&
std::fabs(baseValueIt->second.numberValues[0] - 0.25) < 0.0001,
"committed override leaves base persisted layer unchanged");
}
void TestRenderStateComposerUsesBaseLayerWhenCommittedLayerMissing()
{
std::vector<RuntimeRenderState> basePersistedLayerStates = { MakeLayerState() };
LayeredRenderStateInput input;
input.basePersistedLayerStates = &basePersistedLayerStates;
RenderStateComposer composer;
RenderStateCompositionResult result = composer.BuildFrameState(input);
Expect(result.hasLayerStates, "composer can use base persisted layer states without committed layer states");
const auto valueIt = result.layerStates[0].parameterValues.find("amount");
Expect(valueIt != result.layerStates[0].parameterValues.end() &&
!valueIt->second.numberValues.empty() &&
std::fabs(valueIt->second.numberValues[0] - 0.25) < 0.0001,
"base persisted value is used when no committed live value exists");
}
void TestRenderStateComposerQueuesCommitRequestsWhenEnabled()
{
RuntimeLiveState liveState;
RuntimeLiveOscUpdate update;
update.routeKey = "layer-one\namount";
update.layerKey = "layer-one";
update.parameterKey = "amount";
update.targetValue = JsonValue(0.8);
liveState.ApplyOscUpdates({ update });
std::vector<RuntimeRenderState> baseLayerStates = { MakeLayerState() };
LayeredRenderStateInput input;
input.committedLiveLayerStates = &baseLayerStates;
input.transientAutomationOverlay = &liveState;
input.allowTransientAutomationCommits = true;
input.collectTransientAutomationCommitRequests = true;
input.transientAutomationSmoothing = 0.0;
input.transientAutomationCommitDelay = std::chrono::milliseconds(0);
input.now = std::chrono::steady_clock::now() + std::chrono::milliseconds(1);
RenderStateComposer composer;
RenderStateCompositionResult result = composer.BuildFrameState(input);
Expect(result.commitRequests.size() == 1, "composer returns live commit requests when collection is enabled");
Expect(result.commitRequests[0].routeKey == "layer-one\namount", "composer commit request preserves route");
Expect(result.commitRequests[0].generation == 1, "composer commit request preserves generation");
}
void TestRenderStateComposerSuppressesCommitCollection()
{
RuntimeLiveState liveState;
RuntimeLiveOscUpdate update;
update.routeKey = "layer-one\namount";
update.layerKey = "layer-one";
update.parameterKey = "amount";
update.targetValue = JsonValue(0.7);
liveState.ApplyOscUpdates({ update });
std::vector<RuntimeRenderState> baseLayerStates = { MakeLayerState() };
LayeredRenderStateInput input;
input.committedLiveLayerStates = &baseLayerStates;
input.transientAutomationOverlay = &liveState;
input.allowTransientAutomationCommits = true;
input.collectTransientAutomationCommitRequests = false;
input.transientAutomationSmoothing = 0.0;
input.transientAutomationCommitDelay = std::chrono::milliseconds(0);
input.now = std::chrono::steady_clock::now() + std::chrono::milliseconds(1);
RenderStateComposer composer;
RenderStateCompositionResult result = composer.BuildFrameState(input);
Expect(result.commitRequests.empty(), "composer can apply overlays without collecting commit requests");
const auto valueIt = result.layerStates[0].parameterValues.find("amount");
Expect(valueIt != result.layerStates[0].parameterValues.end() &&
!valueIt->second.numberValues.empty() &&
std::fabs(valueIt->second.numberValues[0] - 0.7) < 0.0001,
"composer still applies overlays when commit collection is disabled");
}
}
int main()
{
TestRuntimeLiveStateAppliesLatestOscOverlay();
TestRuntimeLiveStateIgnoresStaleCommitCompletions();
TestRuntimeLiveStateQueuesAndCompletesCommit();
TestRuntimeLiveStateQueuesOneCommitPerGeneration();
TestRuntimeLiveStateSmoothingZeroAppliesTargetImmediately();
TestRuntimeLiveStateSmoothingOneConvergesImmediately();
TestRuntimeLiveStateSmoothingPartiallyConverges();
TestRuntimeLiveStateSmoothingVectorSizeMismatchUsesTargetShape();
TestRuntimeLiveStateTriggerOverlayIncrementsAndClears();
TestRuntimeLiveStateClearsOverlaysForLayerKey();
TestRuntimeLiveStatePrunesRemovedLayerOverlay();
TestRuntimeLiveStatePrunesIncompatibleParameterOverlay();
TestRuntimeLiveStatePreservesCompatibleOverlayAfterShaderReload();
TestRenderStateComposerBuildsFrameState();
TestRenderStateComposerUsesCommittedLayerOverBaseLayer();
TestRenderStateComposerUsesBaseLayerWhenCommittedLayerMissing();
TestRenderStateComposerQueuesCommitRequestsWhenEnabled();
TestRenderStateComposerSuppressesCommitCollection();
if (gFailures != 0)
{
std::cerr << gFailures << " RuntimeLiveState test failure(s).\n";
return 1;
}
std::cout << "RuntimeLiveState tests passed.\n";
return 0;
}

View File

@@ -1,88 +0,0 @@
#include "RuntimeStateLayerModel.h"
#include <iostream>
namespace
{
int gFailures = 0;
void Expect(bool condition, const char* message)
{
if (condition)
return;
std::cerr << "FAIL: " << message << "\n";
++gFailures;
}
void TestLayerPrecedence()
{
Expect(RuntimeStateLayerCompositionPrecedence(RuntimeStateLayerKind::BasePersisted) == 0,
"base persisted state is the first composition layer");
Expect(RuntimeStateLayerCompositionPrecedence(RuntimeStateLayerKind::CommittedLive) == 1,
"committed live state overlays persisted state");
Expect(RuntimeStateLayerCompositionPrecedence(RuntimeStateLayerKind::TransientAutomation) == 2,
"transient automation overlays committed state");
Expect(!RuntimeStateLayerParticipatesInParameterComposition(RuntimeStateLayerKind::RenderLocal),
"render-local state does not participate in parameter composition");
Expect(!RuntimeStateLayerParticipatesInParameterComposition(RuntimeStateLayerKind::HealthConfig),
"health/config state does not participate in parameter composition");
}
void TestFieldClassification()
{
Expect(ClassifyRuntimeStateField(RuntimeStateField::PersistedLayerStack) == RuntimeStateLayerKind::BasePersisted,
"layer stack is base persisted state");
Expect(ClassifyRuntimeStateField(RuntimeStateField::PersistedParameterValues) == RuntimeStateLayerKind::BasePersisted,
"saved parameters are base persisted state");
Expect(ClassifyRuntimeStateField(RuntimeStateField::CommittedSessionParameterValues) == RuntimeStateLayerKind::CommittedLive,
"session parameter values are committed live state");
Expect(ClassifyRuntimeStateField(RuntimeStateField::RuntimeCompileReloadFlags) == RuntimeStateLayerKind::CommittedLive,
"compile/reload flags are committed session coordination state");
Expect(ClassifyRuntimeStateField(RuntimeStateField::TransientOscOverlay) == RuntimeStateLayerKind::TransientAutomation,
"OSC overlays are transient automation state");
Expect(ClassifyRuntimeStateField(RuntimeStateField::TransientAutomationCommitState) == RuntimeStateLayerKind::TransientAutomation,
"overlay commit generations are transient automation state");
Expect(ClassifyRuntimeStateField(RuntimeStateField::RenderLocalTemporalHistory) == RuntimeStateLayerKind::RenderLocal,
"temporal history is render-local state");
Expect(ClassifyRuntimeStateField(RuntimeStateField::RenderLocalFeedbackState) == RuntimeStateLayerKind::RenderLocal,
"feedback state is render-local state");
Expect(ClassifyRuntimeStateField(RuntimeStateField::RuntimeConfiguration) == RuntimeStateLayerKind::HealthConfig,
"runtime configuration is outside parameter composition");
Expect(ClassifyRuntimeStateField(RuntimeStateField::HealthTelemetry) == RuntimeStateLayerKind::HealthConfig,
"health telemetry is outside parameter composition");
}
void TestInventoriesStayInSyncWithEnums()
{
const std::vector<RuntimeStateLayerDescriptor> layers = GetRuntimeStateLayerInventory();
Expect(layers.size() == 5, "layer inventory names all Phase 5 state categories");
const std::vector<RuntimeStateFieldDescriptor> fields = GetRuntimeStateFieldInventory();
Expect(fields.size() == 14, "field inventory names current state categories");
for (const RuntimeStateFieldDescriptor& field : fields)
{
Expect(field.layerKind == ClassifyRuntimeStateField(field.field),
"field inventory layer kind matches classifier");
Expect(field.name != nullptr && field.name[0] != '\0', "field inventory has a display name");
Expect(field.currentOwner != nullptr && field.currentOwner[0] != '\0', "field inventory has an owner");
}
}
}
int main()
{
TestLayerPrecedence();
TestFieldClassification();
TestInventoriesStayInSyncWithEnums();
if (gFailures != 0)
{
std::cerr << gFailures << " RuntimeStateLayerModel test failure(s).\n";
return 1;
}
std::cout << "RuntimeStateLayerModel tests passed.\n";
return 0;
}

View File

@@ -1,377 +0,0 @@
#include "LayerStackStore.h"
#include "RuntimeCoordinator.h"
#include "RuntimeEventDispatcher.h"
#include "RuntimeStateJson.h"
#include "RuntimeStore.h"
#include "ShaderPackageCatalog.h"
#include <chrono>
#include <filesystem>
#include <fstream>
#include <iostream>
#include <map>
#include <string>
#include <variant>
#include <windows.h>
namespace
{
int gFailures = 0;
void Expect(bool condition, const char* message)
{
if (condition)
return;
std::cerr << "FAIL: " << message << "\n";
++gFailures;
}
std::filesystem::path MakeTestRoot()
{
const auto stamp = std::chrono::steady_clock::now().time_since_epoch().count();
std::filesystem::path root = std::filesystem::temp_directory_path() / ("video-shader-runtime-subsystem-tests-" + std::to_string(stamp));
std::filesystem::create_directories(root);
return root;
}
void WriteFile(const std::filesystem::path& path, const std::string& contents)
{
std::filesystem::create_directories(path.parent_path());
std::ofstream output(path, std::ios::binary);
output << contents;
}
void WriteShaderPackage(const std::filesystem::path& root, const std::string& directoryName, const std::string& manifest)
{
const std::filesystem::path packageRoot = root / directoryName;
WriteFile(packageRoot / "shader.json", manifest);
WriteFile(packageRoot / "shader.slang", "float4 shadeVideo(float2 uv) { return float4(uv, 0.0, 1.0); }\n");
}
std::filesystem::path GetCurrentDirectoryPath()
{
char buffer[MAX_PATH] = {};
GetCurrentDirectoryA(MAX_PATH, buffer);
return std::filesystem::path(buffer);
}
class ScopedCurrentDirectory
{
public:
explicit ScopedCurrentDirectory(const std::filesystem::path& path) :
mPrevious(GetCurrentDirectoryPath())
{
SetCurrentDirectoryA(path.string().c_str());
}
~ScopedCurrentDirectory()
{
SetCurrentDirectoryA(mPrevious.string().c_str());
}
private:
std::filesystem::path mPrevious;
};
ShaderPackageCatalog BuildCatalog(const std::filesystem::path& root)
{
ShaderPackageCatalog catalog;
std::string error;
Expect(catalog.Scan(root, 4, error), "shader package catalog scans test packages");
Expect(error.empty(), "catalog scan does not report an error");
return catalog;
}
void TestLayerDefaultsAndCrud()
{
const std::filesystem::path root = MakeTestRoot();
WriteShaderPackage(root, "alpha", R"({
"id": "alpha",
"name": "Alpha",
"parameters": [
{ "id": "gain", "label": "Gain", "type": "float", "default": 0.25, "min": 0, "max": 1 },
{ "id": "enabled", "label": "Enabled", "type": "bool", "default": true }
]
})");
WriteShaderPackage(root, "beta", R"({
"id": "beta",
"name": "Beta",
"parameters": [
{ "id": "mode", "label": "Mode", "type": "enum", "default": "soft", "options": [
{ "value": "soft", "label": "Soft" },
{ "value": "hard", "label": "Hard" }
] }
]
})");
ShaderPackageCatalog catalog = BuildCatalog(root);
LayerStackStore layers;
std::string error;
Expect(layers.CreateLayer(catalog, "alpha", error), "layer store creates a layer for a known shader");
Expect(layers.LayerCount() == 1, "created layer is stored");
Expect(!layers.CreateLayer(catalog, "missing", error), "layer store rejects unknown shaders");
LayerStackStore::StoredParameterSnapshot snapshot;
Expect(layers.TryGetParameterById(catalog, layers.Layers()[0].id, "gain", snapshot, error), "parameter lookup by id succeeds");
Expect(snapshot.currentValue.numberValues.size() == 1 && snapshot.currentValue.numberValues[0] == 0.25, "default float value is persisted");
ShaderParameterValue value;
value.numberValues = { 0.75 };
Expect(layers.SetParameterValue(layers.Layers()[0].id, "gain", value, error), "parameter value can be updated");
Expect(layers.TryGetParameterById(catalog, layers.Layers()[0].id, "gain", snapshot, error), "updated parameter can be read");
Expect(snapshot.currentValue.numberValues.size() == 1 && snapshot.currentValue.numberValues[0] == 0.75, "updated float value is retained");
Expect(layers.SetLayerShaderSelection(catalog, layers.Layers()[0].id, "beta", error), "layer shader selection can change");
Expect(layers.Layers()[0].shaderId == "beta", "new shader id is stored");
Expect(layers.TryGetParameterById(catalog, layers.Layers()[0].id, "mode", snapshot, error), "new shader defaults are applied");
Expect(snapshot.currentValue.enumValue == "soft", "enum default is applied after shader change");
std::filesystem::remove_all(root);
}
void TestMoveClassificationAndPresetLoad()
{
const std::filesystem::path root = MakeTestRoot();
WriteShaderPackage(root, "alpha", R"({
"id": "alpha",
"name": "Alpha",
"parameters": [{ "id": "gain", "label": "Gain", "type": "float", "default": 0.5, "min": 0, "max": 1 }]
})");
WriteShaderPackage(root, "beta", R"({
"id": "beta",
"name": "Beta",
"parameters": []
})");
ShaderPackageCatalog catalog = BuildCatalog(root);
LayerStackStore layers;
std::string error;
Expect(layers.CreateLayer(catalog, "alpha", error), "first test layer is created");
Expect(layers.CreateLayer(catalog, "beta", error), "second test layer is created");
const std::string firstLayerId = layers.Layers()[0].id;
const std::string secondLayerId = layers.Layers()[1].id;
bool shouldMove = true;
Expect(layers.ResolveLayerMove(firstLayerId, -1, shouldMove, error), "top layer move up is classified");
Expect(!shouldMove, "top layer move up is a no-op");
Expect(layers.ResolveLayerMove(firstLayerId, 1, shouldMove, error), "top layer move down is classified");
Expect(shouldMove, "top layer move down should move");
Expect(layers.MoveLayer(firstLayerId, 1, error), "top layer moves down");
Expect(layers.Layers()[0].id == secondLayerId && layers.Layers()[1].id == firstLayerId, "layer order changed after move");
JsonValue preset = layers.BuildStackPresetValue(catalog, "Look One");
LayerStackStore loaded;
Expect(loaded.LoadStackPresetValue(catalog, preset, error), "stack preset value loads into a fresh layer store");
Expect(loaded.LayerCount() == 2, "loaded preset preserves layer count");
Expect(loaded.Layers()[0].shaderId == "beta" && loaded.Layers()[1].shaderId == "alpha", "loaded preset preserves shader order");
Expect(loaded.Layers()[0].id != secondLayerId, "loaded preset generates fresh layer ids");
std::filesystem::remove_all(root);
}
void TestRuntimeStateJsonReadModelSerialization()
{
ShaderPackage package;
package.id = "alpha";
package.displayName = "Alpha";
package.feedback.enabled = true;
package.feedback.writePassId = "main";
package.temporal.enabled = true;
package.temporal.historySource = TemporalHistorySource::Source;
package.temporal.requestedHistoryLength = 3;
package.temporal.effectiveHistoryLength = 3;
ShaderParameterDefinition gain;
gain.id = "gain";
gain.label = "Gain";
gain.type = ShaderParameterType::Float;
gain.defaultNumbers = { 0.5 };
gain.minNumbers = { 0.0 };
gain.maxNumbers = { 1.0 };
package.parameters.push_back(gain);
LayerStackStore::LayerPersistentState layer;
layer.id = "layer-1";
layer.shaderId = "alpha";
ShaderParameterValue gainValue;
gainValue.numberValues = { 0.8 };
layer.parameterValues["gain"] = gainValue;
JsonValue layersJson = RuntimeStateJson::SerializeLayerStack({ layer }, { { "alpha", package } });
Expect(layersJson.isArray() && layersJson.asArray().size() == 1, "runtime state layer serialization emits one layer");
const JsonValue& layerJson = layersJson.asArray()[0];
Expect(layerJson.find("shaderName") && layerJson.find("shaderName")->asString() == "Alpha", "serialized layer includes shader display name");
Expect(layerJson.find("temporal") && layerJson.find("temporal")->isObject(), "serialized layer includes temporal metadata");
Expect(layerJson.find("feedback") && layerJson.find("feedback")->isObject(), "serialized layer includes feedback metadata");
const JsonValue* parameters = layerJson.find("parameters");
Expect(parameters && parameters->isArray() && parameters->asArray().size() == 1, "serialized layer includes parameter metadata");
const JsonValue* value = parameters->asArray()[0].find("value");
Expect(value && value->asNumber() == 0.8, "serialized parameter includes current value");
}
void TestRuntimeCoordinatorPersistenceEvents()
{
const std::filesystem::path root = MakeTestRoot();
WriteFile(root / "CMakeLists.txt", "cmake_minimum_required(VERSION 3.24)\n");
std::filesystem::create_directories(root / "apps" / "LoopThroughWithOpenGLCompositing");
std::filesystem::create_directories(root / "runtime" / "templates");
WriteShaderPackage(root / "shaders", "alpha", R"({
"id": "alpha",
"name": "Alpha",
"parameters": [{ "id": "gain", "label": "Gain", "type": "float", "default": 0.5, "min": 0, "max": 1 }]
})");
WriteShaderPackage(root / "shaders", "beta", R"({
"id": "beta",
"name": "Beta",
"parameters": [{ "id": "amount", "label": "Amount", "type": "float", "default": 0.25, "min": 0, "max": 1 }]
})");
{
ScopedCurrentDirectory scopedDirectory(root);
RuntimeStore store;
std::string error;
Expect(store.InitializeStore(error), "runtime store initializes in isolated fixture");
Expect(error.empty(), "runtime store initialization has no error");
const PersistenceRequest snapshotRequest = PersistenceRequest::RuntimeStateRequest("unit-test");
const PersistenceSnapshot snapshot = store.BuildRuntimeStatePersistenceSnapshot(snapshotRequest);
Expect(snapshot.targetKind == PersistenceTargetKind::RuntimeState, "runtime store builds a runtime-state persistence snapshot");
Expect(snapshot.reason == "unit-test", "runtime-state persistence snapshot preserves request reason");
Expect(snapshot.targetPath.filename().string() == "runtime_state.json", "runtime-state persistence snapshot targets the runtime state file");
Expect(snapshot.contents.find("\"layers\"") != std::string::npos, "runtime-state persistence snapshot contains serialized layer state");
Expect(store.RequestPersistence(PersistenceRequest::RuntimeStateRequest("unit-test-request"), error),
"runtime store accepts runtime-state persistence requests");
PersistenceRequest unsupportedRequest;
unsupportedRequest.targetKind = PersistenceTargetKind::StackPreset;
unsupportedRequest.reason = "unsupported-unit-test";
Expect(!store.RequestPersistence(unsupportedRequest, error), "runtime store rejects unsupported persistence request targets");
RuntimeEventDispatcher dispatcher(64);
std::vector<RuntimeEvent> seenEvents;
dispatcher.SubscribeAll([&seenEvents](const RuntimeEvent& event) {
seenEvents.push_back(event);
});
RuntimeCoordinator coordinator(store, dispatcher);
auto dispatchAndClear = [&]() {
dispatcher.DispatchPending();
const std::vector<RuntimeEvent> events = seenEvents;
seenEvents.clear();
return events;
};
auto countEvents = [](const std::vector<RuntimeEvent>& events, RuntimeEventType type) {
return static_cast<std::size_t>(std::count_if(events.begin(), events.end(),
[type](const RuntimeEvent& event) { return event.type == type; }));
};
auto persistenceReason = [](const std::vector<RuntimeEvent>& events) {
for (const RuntimeEvent& event : events)
{
if (event.type != RuntimeEventType::RuntimePersistenceRequested)
continue;
const auto* payload = std::get_if<RuntimePersistenceRequestedEvent>(&event.payload);
return payload ? payload->request.reason : std::string();
}
return std::string();
};
auto expectAcceptedPersistence = [&](const RuntimeCoordinatorResult& result, const std::string& reason, const char* message) {
const std::vector<RuntimeEvent> events = dispatchAndClear();
Expect(result.accepted, message);
Expect(result.persistenceRequested, "accepted persistent mutation marks coordinator result");
Expect(countEvents(events, RuntimeEventType::RuntimeMutationAccepted) == 1, "persistent mutation publishes accepted fact");
Expect(countEvents(events, RuntimeEventType::RuntimePersistenceRequested) == 1, "persistent mutation publishes persistence request");
Expect(persistenceReason(events) == reason, "persistence request preserves coordinator action reason");
};
std::vector<RuntimeStore::LayerPersistentState> layers = store.CopyLayerStates();
Expect(layers.size() == 1, "isolated fixture starts with a default layer");
const std::string alphaLayerId = layers.empty() ? std::string() : layers[0].id;
expectAcceptedPersistence(coordinator.UpdateLayerParameter(alphaLayerId, "gain", JsonValue(0.75)), "UpdateLayerParameter",
"parameter changes are accepted");
RuntimeCoordinatorResult resetResult = coordinator.ResetLayerParameters(alphaLayerId);
std::vector<RuntimeEvent> resetEvents = dispatchAndClear();
Expect(resetResult.accepted, "parameter reset is accepted");
Expect(resetResult.transientOscInvalidation == RuntimeCoordinatorTransientOscInvalidation::Layer,
"parameter reset requests layer-scoped transient OSC invalidation");
Expect(resetResult.transientOscLayerKey == alphaLayerId, "parameter reset invalidates the target layer overlays");
Expect(countEvents(resetEvents, RuntimeEventType::RuntimeMutationAccepted) == 1, "parameter reset publishes accepted fact");
expectAcceptedPersistence(coordinator.AddLayer("beta"), "AddLayer", "stack edits are accepted");
layers = store.CopyLayerStates();
Expect(layers.size() == 2, "stack edit creates a second layer");
const std::string betaLayerId = layers.size() > 1 ? layers[1].id : std::string();
expectAcceptedPersistence(coordinator.MoveLayer(betaLayerId, -1), "MoveLayer", "layer order edits are accepted");
expectAcceptedPersistence(coordinator.SaveStackPreset("Look One"), "SaveStackPreset", "preset save is accepted");
expectAcceptedPersistence(coordinator.LoadStackPreset("Look One"), "LoadStackPreset", "preset load is accepted");
RuntimeCoordinatorResult rejected = coordinator.UpdateLayerParameter(alphaLayerId, "missing", JsonValue(0.5));
std::vector<RuntimeEvent> rejectedEvents = dispatchAndClear();
Expect(!rejected.accepted, "invalid parameter mutation is rejected");
Expect(!rejected.persistenceRequested, "rejected mutation does not mark persistence");
Expect(countEvents(rejectedEvents, RuntimeEventType::RuntimeMutationRejected) == 1, "rejected mutation publishes rejection fact");
Expect(countEvents(rejectedEvents, RuntimeEventType::RuntimePersistenceRequested) == 0, "rejected mutation publishes no persistence request");
OscOverlayEvent overlay;
overlay.routeKey = "alpha\ngain";
overlay.layerKey = "alpha";
overlay.parameterKey = "gain";
Expect(dispatcher.PublishPayload(overlay, "RuntimeLiveState"), "OSC overlay event publishes");
std::vector<RuntimeEvent> overlayEvents = dispatchAndClear();
Expect(countEvents(overlayEvents, RuntimeEventType::OscOverlayApplied) == 1, "transient OSC overlay is observable");
Expect(countEvents(overlayEvents, RuntimeEventType::RuntimePersistenceRequested) == 0, "transient OSC overlay does not request persistence");
RuntimeCoordinatorResult oscCommitResult = coordinator.CommitOscParameterByControlKey("alpha", "gain", JsonValue(0.2));
std::vector<RuntimeEvent> oscCommitEvents = dispatchAndClear();
Expect(oscCommitResult.accepted, "accepted OSC commit updates committed session state");
Expect(!oscCommitResult.persistenceRequested, "settled OSC commit does not request persistence by default");
Expect(countEvents(oscCommitEvents, RuntimeEventType::RuntimeMutationAccepted) == 1, "settled OSC commit publishes accepted fact");
Expect(countEvents(oscCommitEvents, RuntimeEventType::RuntimeStateChanged) == 1, "settled OSC commit publishes state change");
Expect(countEvents(oscCommitEvents, RuntimeEventType::RuntimePersistenceRequested) == 0, "settled OSC commit publishes no persistence request");
RuntimeStore::StoredParameterSnapshot oscCommitSnapshot;
Expect(store.TryGetStoredParameterByControlKey("alpha", "gain", oscCommitSnapshot, error), "settled OSC commit can be read back");
Expect(!oscCommitSnapshot.currentValue.numberValues.empty() &&
oscCommitSnapshot.currentValue.numberValues[0] == 0.2,
"settled OSC commit updates the committed session value");
CommittedLiveStateReadModel committedLiveState = store.BuildCommittedLiveStateReadModel();
Expect(!committedLiveState.layers.empty(), "committed live read model exposes current session layers");
const auto committedLayerIt = std::find_if(committedLiveState.layers.begin(), committedLiveState.layers.end(),
[&oscCommitSnapshot](const RuntimeStore::LayerPersistentState& layer) { return layer.id == oscCommitSnapshot.layerId; });
Expect(committedLayerIt != committedLiveState.layers.end(), "committed live read model preserves layer identity");
if (committedLayerIt != committedLiveState.layers.end())
{
const auto committedValueIt = committedLayerIt->parameterValues.find("gain");
Expect(committedValueIt != committedLayerIt->parameterValues.end() &&
!committedValueIt->second.numberValues.empty() &&
committedValueIt->second.numberValues[0] == 0.2,
"committed live read model includes session-only OSC commit value");
}
Expect(committedLiveState.packagesById.find("alpha") != committedLiveState.packagesById.end(),
"committed live read model carries package definitions for snapshot publication");
}
std::filesystem::remove_all(root);
}
}
int main()
{
TestLayerDefaultsAndCrud();
TestMoveClassificationAndPresetLoad();
TestRuntimeStateJsonReadModelSerialization();
TestRuntimeCoordinatorPersistenceEvents();
if (gFailures != 0)
{
std::cerr << gFailures << " RuntimeSubsystem test failure(s).\n";
return 1;
}
std::cout << "RuntimeSubsystem tests passed.\n";
return 0;
}