optional preview frame
This commit is contained in:
@@ -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"
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
50
tests/RenderCadenceCompositorPreviewConfigTests.cpp
Normal file
50
tests/RenderCadenceCompositorPreviewConfigTests.cpp
Normal 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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user