event dispatcher
Some checks failed
CI / React UI Build (push) Successful in 11s
CI / Native Windows Build And Tests (push) Successful in 2m37s
CI / Windows Release Package (push) Has been cancelled

This commit is contained in:
Aiden
2026-05-11 15:15:42 +10:00
parent 5503ce85a9
commit b3705d96cc
20 changed files with 1617 additions and 10 deletions

View File

@@ -104,6 +104,12 @@ set(APP_SOURCES
"${APP_DIR}/resource.h" "${APP_DIR}/resource.h"
"${APP_DIR}/runtime/coordination/RuntimeCoordinator.cpp" "${APP_DIR}/runtime/coordination/RuntimeCoordinator.cpp"
"${APP_DIR}/runtime/coordination/RuntimeCoordinator.h" "${APP_DIR}/runtime/coordination/RuntimeCoordinator.h"
"${APP_DIR}/runtime/events/RuntimeEventCoalescingQueue.h"
"${APP_DIR}/runtime/events/RuntimeEventDispatcher.h"
"${APP_DIR}/runtime/events/RuntimeEvent.h"
"${APP_DIR}/runtime/events/RuntimeEventPayloads.h"
"${APP_DIR}/runtime/events/RuntimeEventQueue.h"
"${APP_DIR}/runtime/events/RuntimeEventType.h"
"${APP_DIR}/runtime/presentation/RuntimeStateJson.cpp" "${APP_DIR}/runtime/presentation/RuntimeStateJson.cpp"
"${APP_DIR}/runtime/presentation/RuntimeStateJson.h" "${APP_DIR}/runtime/presentation/RuntimeStateJson.h"
"${APP_DIR}/runtime/presentation/RuntimeStatePresenter.cpp" "${APP_DIR}/runtime/presentation/RuntimeStatePresenter.cpp"
@@ -158,6 +164,7 @@ target_include_directories(LoopThroughWithOpenGLCompositing PRIVATE
"${APP_DIR}/platform" "${APP_DIR}/platform"
"${APP_DIR}/runtime" "${APP_DIR}/runtime"
"${APP_DIR}/runtime/coordination" "${APP_DIR}/runtime/coordination"
"${APP_DIR}/runtime/events"
"${APP_DIR}/runtime/presentation" "${APP_DIR}/runtime/presentation"
"${APP_DIR}/runtime/snapshot" "${APP_DIR}/runtime/snapshot"
"${APP_DIR}/runtime/store" "${APP_DIR}/runtime/store"
@@ -242,6 +249,22 @@ endif()
add_test(NAME RuntimeParameterUtilsTests COMMAND RuntimeParameterUtilsTests) add_test(NAME RuntimeParameterUtilsTests COMMAND RuntimeParameterUtilsTests)
add_executable(RuntimeEventTypeTests
"${CMAKE_CURRENT_SOURCE_DIR}/tests/RuntimeEventTypeTests.cpp"
)
target_include_directories(RuntimeEventTypeTests PRIVATE
"${APP_DIR}"
"${APP_DIR}/runtime"
"${APP_DIR}/runtime/events"
)
if(MSVC)
target_compile_options(RuntimeEventTypeTests PRIVATE /W3)
endif()
add_test(NAME RuntimeEventTypeTests COMMAND RuntimeEventTypeTests)
add_executable(RuntimeSubsystemTests add_executable(RuntimeSubsystemTests
"${APP_DIR}/runtime/store/LayerStackStore.cpp" "${APP_DIR}/runtime/store/LayerStackStore.cpp"
"${APP_DIR}/runtime/store/ShaderPackageCatalog.cpp" "${APP_DIR}/runtime/store/ShaderPackageCatalog.cpp"

View File

@@ -6,9 +6,10 @@
#include "RuntimeStore.h" #include "RuntimeStore.h"
#include <windows.h> #include <windows.h>
ControlServices::ControlServices() : ControlServices::ControlServices(RuntimeEventDispatcher& runtimeEventDispatcher) :
mControlServer(std::make_unique<ControlServer>()), mControlServer(std::make_unique<ControlServer>()),
mOscServer(std::make_unique<OscServer>()), mOscServer(std::make_unique<OscServer>()),
mRuntimeEventDispatcher(runtimeEventDispatcher),
mPollRunning(false) mPollRunning(false)
{ {
} }

View File

@@ -15,6 +15,7 @@
class ControlServer; class ControlServer;
class OpenGLComposite; class OpenGLComposite;
class OscServer; class OscServer;
class RuntimeEventDispatcher;
class RuntimeStore; class RuntimeStore;
struct RuntimeCoordinatorServiceResult struct RuntimeCoordinatorServiceResult
@@ -40,7 +41,7 @@ public:
uint64_t generation = 0; uint64_t generation = 0;
}; };
ControlServices(); explicit ControlServices(RuntimeEventDispatcher& runtimeEventDispatcher);
~ControlServices(); ~ControlServices();
bool Start(OpenGLComposite& composite, RuntimeStore& runtimeStore, std::string& error); bool Start(OpenGLComposite& composite, RuntimeStore& runtimeStore, std::string& error);
@@ -79,6 +80,7 @@ private:
std::unique_ptr<ControlServer> mControlServer; std::unique_ptr<ControlServer> mControlServer;
std::unique_ptr<OscServer> mOscServer; std::unique_ptr<OscServer> mOscServer;
RuntimeEventDispatcher& mRuntimeEventDispatcher;
std::thread mPollThread; std::thread mPollThread;
std::atomic<bool> mPollRunning; std::atomic<bool> mPollRunning;
std::mutex mRuntimeCoordinatorResultMutex; std::mutex mRuntimeCoordinatorResultMutex;

View File

@@ -2,8 +2,8 @@
#include "RuntimeStore.h" #include "RuntimeStore.h"
RuntimeServices::RuntimeServices() : RuntimeServices::RuntimeServices(RuntimeEventDispatcher& runtimeEventDispatcher) :
mControlServices(std::make_unique<ControlServices>()) mControlServices(std::make_unique<ControlServices>(runtimeEventDispatcher))
{ {
} }

View File

@@ -6,6 +6,7 @@
#include <string> #include <string>
class OpenGLComposite; class OpenGLComposite;
class RuntimeCoordinator; class RuntimeCoordinator;
class RuntimeEventDispatcher;
class RuntimeStore; class RuntimeStore;
class RuntimeServices class RuntimeServices
@@ -14,7 +15,7 @@ public:
using AppliedOscUpdate = ControlServices::AppliedOscUpdate; using AppliedOscUpdate = ControlServices::AppliedOscUpdate;
using CompletedOscCommit = ControlServices::CompletedOscCommit; using CompletedOscCommit = ControlServices::CompletedOscCommit;
RuntimeServices(); explicit RuntimeServices(RuntimeEventDispatcher& runtimeEventDispatcher);
~RuntimeServices(); ~RuntimeServices();
bool Start(OpenGLComposite& composite, RuntimeStore& runtimeStore, std::string& error); bool Start(OpenGLComposite& composite, RuntimeStore& runtimeStore, std::string& error);

View File

@@ -5,6 +5,7 @@
#include "PngScreenshotWriter.h" #include "PngScreenshotWriter.h"
#include "RenderEngine.h" #include "RenderEngine.h"
#include "RuntimeCoordinator.h" #include "RuntimeCoordinator.h"
#include "RuntimeEventDispatcher.h"
#include "RuntimeParameterUtils.h" #include "RuntimeParameterUtils.h"
#include "RuntimeServices.h" #include "RuntimeServices.h"
#include "RuntimeSnapshotProvider.h" #include "RuntimeSnapshotProvider.h"
@@ -36,8 +37,9 @@ OpenGLComposite::OpenGLComposite(HWND hWnd, HDC hDC, HGLRC hRC) :
{ {
InitializeCriticalSection(&pMutex); InitializeCriticalSection(&pMutex);
mRuntimeStore = std::make_unique<RuntimeStore>(); mRuntimeStore = std::make_unique<RuntimeStore>();
mRuntimeEventDispatcher = std::make_unique<RuntimeEventDispatcher>();
mRuntimeSnapshotProvider = std::make_unique<RuntimeSnapshotProvider>(mRuntimeStore->GetRenderSnapshotBuilder()); mRuntimeSnapshotProvider = std::make_unique<RuntimeSnapshotProvider>(mRuntimeStore->GetRenderSnapshotBuilder());
mRuntimeCoordinator = std::make_unique<RuntimeCoordinator>(*mRuntimeStore); mRuntimeCoordinator = std::make_unique<RuntimeCoordinator>(*mRuntimeStore, *mRuntimeEventDispatcher);
mRenderEngine = std::make_unique<RenderEngine>( mRenderEngine = std::make_unique<RenderEngine>(
*mRuntimeSnapshotProvider, *mRuntimeSnapshotProvider,
mRuntimeStore->GetHealthTelemetry(), mRuntimeStore->GetHealthTelemetry(),
@@ -49,10 +51,11 @@ OpenGLComposite::OpenGLComposite(HWND hWnd, HDC hDC, HGLRC hRC) :
[this]() { paintGL(false); }); [this]() { paintGL(false); });
mVideoBackend = std::make_unique<VideoBackend>(*mRenderEngine, mRuntimeStore->GetHealthTelemetry()); mVideoBackend = std::make_unique<VideoBackend>(*mRenderEngine, mRuntimeStore->GetHealthTelemetry());
mShaderBuildQueue = std::make_unique<ShaderBuildQueue>(*mRuntimeSnapshotProvider); mShaderBuildQueue = std::make_unique<ShaderBuildQueue>(*mRuntimeSnapshotProvider);
mRuntimeServices = std::make_unique<RuntimeServices>(); mRuntimeServices = std::make_unique<RuntimeServices>(*mRuntimeEventDispatcher);
mRuntimeUpdateController = std::make_unique<RuntimeUpdateController>( mRuntimeUpdateController = std::make_unique<RuntimeUpdateController>(
*mRuntimeStore, *mRuntimeStore,
*mRuntimeCoordinator, *mRuntimeCoordinator,
*mRuntimeEventDispatcher,
*mRuntimeServices, *mRuntimeServices,
*mRenderEngine, *mRenderEngine,
*mShaderBuildQueue, *mShaderBuildQueue,

View File

@@ -22,6 +22,7 @@
class RenderEngine; class RenderEngine;
class RuntimeCoordinator; class RuntimeCoordinator;
class RuntimeEventDispatcher;
class RuntimeSnapshotProvider; class RuntimeSnapshotProvider;
class RuntimeServices; class RuntimeServices;
class RuntimeStore; class RuntimeStore;
@@ -76,6 +77,7 @@ private:
std::unique_ptr<RuntimeStore> mRuntimeStore; std::unique_ptr<RuntimeStore> mRuntimeStore;
std::unique_ptr<RuntimeCoordinator> mRuntimeCoordinator; std::unique_ptr<RuntimeCoordinator> mRuntimeCoordinator;
std::unique_ptr<RuntimeSnapshotProvider> mRuntimeSnapshotProvider; std::unique_ptr<RuntimeSnapshotProvider> mRuntimeSnapshotProvider;
std::unique_ptr<RuntimeEventDispatcher> mRuntimeEventDispatcher;
std::unique_ptr<RenderEngine> mRenderEngine; std::unique_ptr<RenderEngine> mRenderEngine;
std::unique_ptr<ShaderBuildQueue> mShaderBuildQueue; std::unique_ptr<ShaderBuildQueue> mShaderBuildQueue;
std::unique_ptr<RuntimeServices> mRuntimeServices; std::unique_ptr<RuntimeServices> mRuntimeServices;

View File

@@ -11,12 +11,14 @@
RuntimeUpdateController::RuntimeUpdateController( RuntimeUpdateController::RuntimeUpdateController(
RuntimeStore& runtimeStore, RuntimeStore& runtimeStore,
RuntimeCoordinator& runtimeCoordinator, RuntimeCoordinator& runtimeCoordinator,
RuntimeEventDispatcher& runtimeEventDispatcher,
RuntimeServices& runtimeServices, RuntimeServices& runtimeServices,
RenderEngine& renderEngine, RenderEngine& renderEngine,
ShaderBuildQueue& shaderBuildQueue, ShaderBuildQueue& shaderBuildQueue,
VideoBackend& videoBackend) : VideoBackend& videoBackend) :
mRuntimeStore(runtimeStore), mRuntimeStore(runtimeStore),
mRuntimeCoordinator(runtimeCoordinator), mRuntimeCoordinator(runtimeCoordinator),
mRuntimeEventDispatcher(runtimeEventDispatcher),
mRuntimeServices(runtimeServices), mRuntimeServices(runtimeServices),
mRenderEngine(renderEngine), mRenderEngine(renderEngine),
mShaderBuildQueue(shaderBuildQueue), mShaderBuildQueue(shaderBuildQueue),

View File

@@ -5,6 +5,7 @@
#include <string> #include <string>
class RenderEngine; class RenderEngine;
class RuntimeEventDispatcher;
class RuntimeServices; class RuntimeServices;
class RuntimeStore; class RuntimeStore;
class ShaderBuildQueue; class ShaderBuildQueue;
@@ -16,6 +17,7 @@ public:
RuntimeUpdateController( RuntimeUpdateController(
RuntimeStore& runtimeStore, RuntimeStore& runtimeStore,
RuntimeCoordinator& runtimeCoordinator, RuntimeCoordinator& runtimeCoordinator,
RuntimeEventDispatcher& runtimeEventDispatcher,
RuntimeServices& runtimeServices, RuntimeServices& runtimeServices,
RenderEngine& renderEngine, RenderEngine& renderEngine,
ShaderBuildQueue& shaderBuildQueue, ShaderBuildQueue& shaderBuildQueue,
@@ -29,6 +31,7 @@ public:
private: private:
RuntimeStore& mRuntimeStore; RuntimeStore& mRuntimeStore;
RuntimeCoordinator& mRuntimeCoordinator; RuntimeCoordinator& mRuntimeCoordinator;
RuntimeEventDispatcher& mRuntimeEventDispatcher;
RuntimeServices& mRuntimeServices; RuntimeServices& mRuntimeServices;
RenderEngine& mRenderEngine; RenderEngine& mRenderEngine;
ShaderBuildQueue& mShaderBuildQueue; ShaderBuildQueue& mShaderBuildQueue;

View File

@@ -3,8 +3,9 @@
#include "RuntimeParameterUtils.h" #include "RuntimeParameterUtils.h"
#include "RuntimeStore.h" #include "RuntimeStore.h"
RuntimeCoordinator::RuntimeCoordinator(RuntimeStore& runtimeStore) : RuntimeCoordinator::RuntimeCoordinator(RuntimeStore& runtimeStore, RuntimeEventDispatcher& runtimeEventDispatcher) :
mRuntimeStore(runtimeStore) mRuntimeStore(runtimeStore),
mRuntimeEventDispatcher(runtimeEventDispatcher)
{ {
} }

View File

@@ -9,6 +9,7 @@
#include <string> #include <string>
class RuntimeStore; class RuntimeStore;
class RuntimeEventDispatcher;
enum class RuntimeCoordinatorCommittedStateMode enum class RuntimeCoordinatorCommittedStateMode
{ {
@@ -42,7 +43,7 @@ struct RuntimeCoordinatorResult
class RuntimeCoordinator class RuntimeCoordinator
{ {
public: public:
explicit RuntimeCoordinator(RuntimeStore& runtimeStore); RuntimeCoordinator(RuntimeStore& runtimeStore, RuntimeEventDispatcher& runtimeEventDispatcher);
RuntimeCoordinatorResult AddLayer(const std::string& shaderId); RuntimeCoordinatorResult AddLayer(const std::string& shaderId);
RuntimeCoordinatorResult RemoveLayer(const std::string& layerId); RuntimeCoordinatorResult RemoveLayer(const std::string& layerId);
@@ -93,6 +94,7 @@ private:
RuntimeCoordinatorResult BuildAcceptedNoReloadResult() const; RuntimeCoordinatorResult BuildAcceptedNoReloadResult() const;
RuntimeStore& mRuntimeStore; RuntimeStore& mRuntimeStore;
RuntimeEventDispatcher& mRuntimeEventDispatcher;
mutable std::mutex mMutex; mutable std::mutex mMutex;
bool mPreserveFeedbackOnNextShaderBuild = false; bool mPreserveFeedbackOnNextShaderBuild = false;
std::atomic<bool> mUseCommittedLayerStates{ false }; std::atomic<bool> mUseCommittedLayerStates{ false };

View File

@@ -0,0 +1,87 @@
#pragma once
#include "RuntimeEventPayloads.h"
#include <chrono>
#include <cstdint>
#include <string>
#include <type_traits>
#include <utility>
#include <variant>
using RuntimeEventPayload = std::variant<
std::monostate,
OscValueReceivedEvent,
OscValueCoalescedEvent,
OscCommitRequestedEvent,
HttpControlMutationRequestedEvent,
WebSocketClientConnectedEvent,
RuntimeStateBroadcastRequestedEvent,
FileChangeDetectedEvent,
ManualReloadRequestedEvent,
RuntimeMutationEvent,
RuntimeStateChangedEvent,
RuntimePersistenceRequestedEvent,
RuntimeReloadRequestedEvent,
ShaderPackagesChangedEvent,
RenderSnapshotPublishRequestedEvent,
RuntimeStatePresentationChangedEvent,
ShaderBuildEvent,
CompileStatusChangedEvent,
RenderSnapshotPublishedEvent,
RenderResetEvent,
OscOverlayEvent,
FrameRenderedEvent,
PreviewFrameAvailableEvent,
InputSignalChangedEvent,
InputFrameArrivedEvent,
OutputFrameScheduledEvent,
OutputFrameCompletedEvent,
BackendStateChangedEvent,
SubsystemWarningEvent,
SubsystemRecoveredEvent,
TimingSampleRecordedEvent,
QueueDepthChangedEvent>;
inline RuntimeEventType RuntimeEventPayloadType(const RuntimeEventPayload& payload)
{
return std::visit([](const auto& value) -> RuntimeEventType {
using PayloadType = std::decay_t<decltype(value)>;
if constexpr (std::is_same_v<PayloadType, std::monostate>)
return RuntimeEventType::Unknown;
else
return RuntimeEventPayloadType(value);
}, payload);
}
struct RuntimeEvent
{
RuntimeEventType type = RuntimeEventType::Unknown;
uint64_t sequence = 0;
std::chrono::steady_clock::time_point createdAt = std::chrono::steady_clock::now();
std::string source;
RuntimeEventPayload payload;
bool HasPayload() const
{
return !std::holds_alternative<std::monostate>(payload);
}
bool PayloadMatchesType() const
{
return RuntimeEventPayloadType(payload) == type;
}
};
template <typename Payload>
RuntimeEvent MakeRuntimeEvent(Payload payload, std::string source = {}, uint64_t sequence = 0,
std::chrono::steady_clock::time_point createdAt = std::chrono::steady_clock::now())
{
RuntimeEvent event;
event.type = RuntimeEventPayloadType(payload);
event.sequence = sequence;
event.createdAt = createdAt;
event.source = std::move(source);
event.payload = std::move(payload);
return event;
}

View File

@@ -0,0 +1,147 @@
#pragma once
#include "RuntimeEvent.h"
#include <chrono>
#include <cstddef>
#include <deque>
#include <functional>
#include <map>
#include <mutex>
#include <string>
#include <utility>
#include <vector>
struct RuntimeEventCoalescingQueueMetrics
{
std::size_t depth = 0;
std::size_t capacity = 0;
std::size_t droppedCount = 0;
std::size_t coalescedCount = 0;
double oldestEventAgeMilliseconds = 0.0;
};
inline std::string RuntimeEventDefaultCoalescingKey(const RuntimeEvent& event)
{
if (const auto* payload = std::get_if<OscValueReceivedEvent>(&event.payload))
return std::string(RuntimeEventTypeName(event.type)) + ":" + payload->routeKey;
if (const auto* payload = std::get_if<OscCommitRequestedEvent>(&event.payload))
return std::string(RuntimeEventTypeName(event.type)) + ":" + payload->routeKey;
if (const auto* payload = std::get_if<FileChangeDetectedEvent>(&event.payload))
return std::string(RuntimeEventTypeName(event.type)) + ":" + payload->path;
if (const auto* payload = std::get_if<QueueDepthChangedEvent>(&event.payload))
return std::string(RuntimeEventTypeName(event.type)) + ":" + payload->queueName;
return std::string(RuntimeEventTypeName(event.type));
}
class RuntimeEventCoalescingQueue
{
public:
using KeySelector = std::function<std::string(const RuntimeEvent&)>;
explicit RuntimeEventCoalescingQueue(std::size_t capacity = 256, KeySelector keySelector = RuntimeEventDefaultCoalescingKey) :
mCapacity(capacity),
mKeySelector(std::move(keySelector))
{
}
bool Push(RuntimeEvent event)
{
const std::string key = mKeySelector(event);
if (key.empty())
return false;
std::lock_guard<std::mutex> lock(mMutex);
auto found = mEntries.find(key);
if (found != mEntries.end())
{
const auto firstCreatedAt = found->second.event.createdAt;
found->second.event = std::move(event);
found->second.event.createdAt = firstCreatedAt;
++found->second.coalescedCount;
++mCoalescedCount;
return true;
}
if (mEntries.size() >= mCapacity)
{
++mDroppedCount;
return false;
}
mOrder.push_back(key);
Entry entry;
entry.event = std::move(event);
mEntries.emplace(key, std::move(entry));
return true;
}
std::vector<RuntimeEvent> Drain(std::size_t maxEvents = 0)
{
std::vector<RuntimeEvent> events;
std::lock_guard<std::mutex> lock(mMutex);
const std::size_t count = maxEvents == 0 || maxEvents > mOrder.size() ? mOrder.size() : maxEvents;
events.reserve(count);
for (std::size_t index = 0; index < count; ++index)
{
const std::string key = std::move(mOrder.front());
mOrder.pop_front();
auto found = mEntries.find(key);
if (found == mEntries.end())
continue;
events.push_back(std::move(found->second.event));
mEntries.erase(found);
}
return events;
}
RuntimeEventCoalescingQueueMetrics GetMetrics(std::chrono::steady_clock::time_point now = std::chrono::steady_clock::now()) const
{
std::lock_guard<std::mutex> lock(mMutex);
RuntimeEventCoalescingQueueMetrics metrics;
metrics.depth = mEntries.size();
metrics.capacity = mCapacity;
metrics.droppedCount = mDroppedCount;
metrics.coalescedCount = mCoalescedCount;
if (!mOrder.empty())
{
const auto found = mEntries.find(mOrder.front());
if (found != mEntries.end())
{
const auto age = now - found->second.event.createdAt;
metrics.oldestEventAgeMilliseconds = std::chrono::duration<double, std::milli>(age).count();
}
}
return metrics;
}
std::size_t Depth() const
{
std::lock_guard<std::mutex> lock(mMutex);
return mEntries.size();
}
private:
struct Entry
{
RuntimeEvent event;
std::size_t coalescedCount = 0;
};
mutable std::mutex mMutex;
std::size_t mCapacity = 0;
KeySelector mKeySelector;
std::deque<std::string> mOrder;
std::map<std::string, Entry> mEntries;
std::size_t mDroppedCount = 0;
std::size_t mCoalescedCount = 0;
};

View File

@@ -0,0 +1,117 @@
#pragma once
#include "RuntimeEventQueue.h"
#include <atomic>
#include <functional>
#include <map>
#include <mutex>
#include <vector>
struct RuntimeEventDispatchResult
{
std::size_t dispatchedEvents = 0;
std::size_t handlerInvocations = 0;
std::size_t handlerFailures = 0;
};
class RuntimeEventDispatcher
{
public:
using Handler = std::function<void(const RuntimeEvent&)>;
explicit RuntimeEventDispatcher(std::size_t queueCapacity = 1024) :
mQueue(queueCapacity)
{
}
bool Publish(RuntimeEvent event)
{
if (!event.PayloadMatchesType())
return false;
if (event.sequence == 0)
event.sequence = mNextSequence.fetch_add(1);
return mQueue.Push(std::move(event));
}
template <typename Payload>
bool PublishPayload(Payload payload, std::string source = {})
{
return Publish(MakeRuntimeEvent(std::move(payload), std::move(source)));
}
void Subscribe(RuntimeEventType type, Handler handler)
{
std::lock_guard<std::mutex> lock(mHandlerMutex);
mHandlers[type].push_back(std::move(handler));
}
void SubscribeAll(Handler handler)
{
std::lock_guard<std::mutex> lock(mHandlerMutex);
mAllHandlers.push_back(std::move(handler));
}
RuntimeEventDispatchResult DispatchPending(std::size_t maxEvents = 0)
{
RuntimeEventDispatchResult result;
std::vector<RuntimeEvent> events = mQueue.Drain(maxEvents);
result.dispatchedEvents = events.size();
for (const RuntimeEvent& event : events)
{
std::vector<Handler> handlers = HandlersFor(event.type);
result.handlerInvocations += handlers.size();
for (const Handler& handler : handlers)
{
try
{
handler(event);
}
catch (...)
{
++result.handlerFailures;
}
}
}
return result;
}
bool TryPop(RuntimeEvent& event)
{
return mQueue.TryPop(event);
}
RuntimeEventQueueMetrics GetQueueMetrics(std::chrono::steady_clock::time_point now = std::chrono::steady_clock::now()) const
{
return mQueue.GetMetrics(now);
}
std::size_t QueueDepth() const
{
return mQueue.Depth();
}
private:
std::vector<Handler> HandlersFor(RuntimeEventType type) const
{
std::lock_guard<std::mutex> lock(mHandlerMutex);
std::vector<Handler> handlers = mAllHandlers;
const auto found = mHandlers.find(type);
if (found != mHandlers.end())
handlers.insert(handlers.end(), found->second.begin(), found->second.end());
return handlers;
}
RuntimeEventQueue mQueue;
std::atomic<uint64_t> mNextSequence{ 1 };
mutable std::mutex mHandlerMutex;
std::map<RuntimeEventType, std::vector<Handler>> mHandlers;
std::vector<Handler> mAllHandlers;
};

View File

@@ -0,0 +1,436 @@
#pragma once
#include "RuntimeEventType.h"
#include <cstddef>
#include <cstdint>
#include <string>
enum class RuntimeEventSeverity
{
Debug,
Info,
Warning,
Error
};
enum class RuntimeEventRenderResetScope
{
None,
TemporalHistoryOnly,
TemporalHistoryAndFeedback
};
enum class RuntimeEventShaderBuildPhase
{
Requested,
Prepared,
Applied,
Failed
};
struct OscValueReceivedEvent
{
std::string routeKey;
std::string layerKey;
std::string parameterKey;
std::string valueJson;
uint64_t generation = 0;
};
struct OscValueCoalescedEvent
{
std::string routeKey;
std::size_t coalescedCount = 0;
uint64_t latestGeneration = 0;
};
struct OscCommitRequestedEvent
{
std::string routeKey;
std::string layerKey;
std::string parameterKey;
std::string valueJson;
uint64_t generation = 0;
};
struct HttpControlMutationRequestedEvent
{
std::string method;
std::string path;
std::string bodyJson;
};
struct WebSocketClientConnectedEvent
{
std::string clientId;
std::size_t connectedClientCount = 0;
};
struct RuntimeStateBroadcastRequestedEvent
{
std::string reason;
bool coalescable = true;
};
struct FileChangeDetectedEvent
{
std::string path;
bool shaderPackageCandidate = false;
bool runtimeConfigCandidate = false;
bool presetCandidate = false;
};
struct ManualReloadRequestedEvent
{
bool preserveFeedbackState = false;
std::string reason;
};
struct RuntimeMutationEvent
{
std::string action;
bool accepted = false;
bool runtimeStateChanged = false;
bool runtimeStateBroadcastRequired = false;
bool shaderBuildRequested = false;
bool persistenceRequested = false;
bool clearTransientOscState = false;
RuntimeEventRenderResetScope renderResetScope = RuntimeEventRenderResetScope::None;
std::string errorMessage;
};
struct RuntimeStateChangedEvent
{
std::string reason;
bool renderVisible = false;
bool persistenceRequested = false;
};
struct RuntimePersistenceRequestedEvent
{
std::string reason;
bool debounceAllowed = true;
};
struct RuntimeReloadRequestedEvent
{
bool preserveFeedbackState = false;
std::string reason;
};
struct ShaderPackagesChangedEvent
{
bool registryChanged = false;
std::size_t packageCount = 0;
std::string reason;
};
struct RenderSnapshotPublishRequestedEvent
{
unsigned inputWidth = 0;
unsigned inputHeight = 0;
unsigned outputWidth = 0;
unsigned outputHeight = 0;
std::string reason;
};
struct RuntimeStatePresentationChangedEvent
{
std::string reason;
};
struct ShaderBuildEvent
{
RuntimeEventShaderBuildPhase phase = RuntimeEventShaderBuildPhase::Requested;
unsigned inputWidth = 0;
unsigned inputHeight = 0;
bool preserveFeedbackState = false;
bool succeeded = false;
std::string message;
};
struct CompileStatusChangedEvent
{
bool succeeded = false;
std::string message;
};
struct RenderSnapshotPublishedEvent
{
uint64_t snapshotVersion = 0;
uint64_t structureVersion = 0;
uint64_t parameterVersion = 0;
uint64_t packageVersion = 0;
unsigned outputWidth = 0;
unsigned outputHeight = 0;
std::size_t layerCount = 0;
};
struct RenderResetEvent
{
RuntimeEventRenderResetScope scope = RuntimeEventRenderResetScope::None;
bool applied = false;
std::string reason;
};
struct OscOverlayEvent
{
std::string routeKey;
std::string layerKey;
std::string parameterKey;
uint64_t generation = 0;
bool settled = false;
};
struct FrameRenderedEvent
{
uint64_t frameIndex = 0;
double renderMilliseconds = 0.0;
};
struct PreviewFrameAvailableEvent
{
uint64_t frameIndex = 0;
unsigned width = 0;
unsigned height = 0;
};
struct InputSignalChangedEvent
{
bool hasSignal = false;
unsigned width = 0;
unsigned height = 0;
std::string modeName;
};
struct InputFrameArrivedEvent
{
uint64_t frameIndex = 0;
unsigned width = 0;
unsigned height = 0;
long rowBytes = 0;
std::string pixelFormat;
bool hasNoInputSource = false;
};
struct OutputFrameScheduledEvent
{
uint64_t frameIndex = 0;
int64_t streamTime = 0;
int64_t duration = 0;
int64_t timeScale = 0;
};
struct OutputFrameCompletedEvent
{
uint64_t frameIndex = 0;
std::string result;
};
struct BackendStateChangedEvent
{
std::string backendName;
std::string state;
std::string message;
};
struct SubsystemWarningEvent
{
std::string subsystem;
std::string warningKey;
RuntimeEventSeverity severity = RuntimeEventSeverity::Warning;
std::string message;
bool cleared = false;
};
struct SubsystemRecoveredEvent
{
std::string subsystem;
std::string recoveryKey;
std::string message;
};
struct TimingSampleRecordedEvent
{
std::string subsystem;
std::string metric;
double value = 0.0;
std::string unit;
};
struct QueueDepthChangedEvent
{
std::string queueName;
std::size_t depth = 0;
std::size_t capacity = 0;
std::size_t droppedCount = 0;
std::size_t coalescedCount = 0;
};
constexpr RuntimeEventType RuntimeEventPayloadType(const OscValueReceivedEvent&)
{
return RuntimeEventType::OscValueReceived;
}
constexpr RuntimeEventType RuntimeEventPayloadType(const OscValueCoalescedEvent&)
{
return RuntimeEventType::OscValueCoalesced;
}
constexpr RuntimeEventType RuntimeEventPayloadType(const OscCommitRequestedEvent&)
{
return RuntimeEventType::OscCommitRequested;
}
constexpr RuntimeEventType RuntimeEventPayloadType(const HttpControlMutationRequestedEvent&)
{
return RuntimeEventType::HttpControlMutationRequested;
}
constexpr RuntimeEventType RuntimeEventPayloadType(const WebSocketClientConnectedEvent&)
{
return RuntimeEventType::WebSocketClientConnected;
}
constexpr RuntimeEventType RuntimeEventPayloadType(const RuntimeStateBroadcastRequestedEvent&)
{
return RuntimeEventType::RuntimeStateBroadcastRequested;
}
constexpr RuntimeEventType RuntimeEventPayloadType(const FileChangeDetectedEvent&)
{
return RuntimeEventType::FileChangeDetected;
}
constexpr RuntimeEventType RuntimeEventPayloadType(const ManualReloadRequestedEvent&)
{
return RuntimeEventType::ManualReloadRequested;
}
inline RuntimeEventType RuntimeEventPayloadType(const RuntimeMutationEvent& event)
{
return event.accepted ? RuntimeEventType::RuntimeMutationAccepted : RuntimeEventType::RuntimeMutationRejected;
}
constexpr RuntimeEventType RuntimeEventPayloadType(const RuntimeStateChangedEvent&)
{
return RuntimeEventType::RuntimeStateChanged;
}
constexpr RuntimeEventType RuntimeEventPayloadType(const RuntimePersistenceRequestedEvent&)
{
return RuntimeEventType::RuntimePersistenceRequested;
}
constexpr RuntimeEventType RuntimeEventPayloadType(const RuntimeReloadRequestedEvent&)
{
return RuntimeEventType::RuntimeReloadRequested;
}
constexpr RuntimeEventType RuntimeEventPayloadType(const ShaderPackagesChangedEvent&)
{
return RuntimeEventType::ShaderPackagesChanged;
}
constexpr RuntimeEventType RuntimeEventPayloadType(const RenderSnapshotPublishRequestedEvent&)
{
return RuntimeEventType::RenderSnapshotPublishRequested;
}
constexpr RuntimeEventType RuntimeEventPayloadType(const RuntimeStatePresentationChangedEvent&)
{
return RuntimeEventType::RuntimeStatePresentationChanged;
}
inline RuntimeEventType RuntimeEventPayloadType(const ShaderBuildEvent& event)
{
switch (event.phase)
{
case RuntimeEventShaderBuildPhase::Requested:
return RuntimeEventType::ShaderBuildRequested;
case RuntimeEventShaderBuildPhase::Prepared:
return RuntimeEventType::ShaderBuildPrepared;
case RuntimeEventShaderBuildPhase::Applied:
return RuntimeEventType::ShaderBuildApplied;
case RuntimeEventShaderBuildPhase::Failed:
return RuntimeEventType::ShaderBuildFailed;
}
return RuntimeEventType::ShaderBuildRequested;
}
constexpr RuntimeEventType RuntimeEventPayloadType(const CompileStatusChangedEvent&)
{
return RuntimeEventType::CompileStatusChanged;
}
constexpr RuntimeEventType RuntimeEventPayloadType(const RenderSnapshotPublishedEvent&)
{
return RuntimeEventType::RenderSnapshotPublished;
}
inline RuntimeEventType RuntimeEventPayloadType(const RenderResetEvent& event)
{
return event.applied ? RuntimeEventType::RenderResetApplied : RuntimeEventType::RenderResetRequested;
}
inline RuntimeEventType RuntimeEventPayloadType(const OscOverlayEvent& event)
{
return event.settled ? RuntimeEventType::OscOverlaySettled : RuntimeEventType::OscOverlayApplied;
}
constexpr RuntimeEventType RuntimeEventPayloadType(const FrameRenderedEvent&)
{
return RuntimeEventType::FrameRendered;
}
constexpr RuntimeEventType RuntimeEventPayloadType(const PreviewFrameAvailableEvent&)
{
return RuntimeEventType::PreviewFrameAvailable;
}
constexpr RuntimeEventType RuntimeEventPayloadType(const InputSignalChangedEvent&)
{
return RuntimeEventType::InputSignalChanged;
}
constexpr RuntimeEventType RuntimeEventPayloadType(const InputFrameArrivedEvent&)
{
return RuntimeEventType::InputFrameArrived;
}
constexpr RuntimeEventType RuntimeEventPayloadType(const OutputFrameScheduledEvent&)
{
return RuntimeEventType::OutputFrameScheduled;
}
constexpr RuntimeEventType RuntimeEventPayloadType(const OutputFrameCompletedEvent&)
{
return RuntimeEventType::OutputFrameCompleted;
}
constexpr RuntimeEventType RuntimeEventPayloadType(const BackendStateChangedEvent&)
{
return RuntimeEventType::BackendStateChanged;
}
inline RuntimeEventType RuntimeEventPayloadType(const SubsystemWarningEvent& event)
{
return event.cleared ? RuntimeEventType::SubsystemWarningCleared : RuntimeEventType::SubsystemWarningRaised;
}
constexpr RuntimeEventType RuntimeEventPayloadType(const SubsystemRecoveredEvent&)
{
return RuntimeEventType::SubsystemRecovered;
}
constexpr RuntimeEventType RuntimeEventPayloadType(const TimingSampleRecordedEvent&)
{
return RuntimeEventType::TimingSampleRecorded;
}
constexpr RuntimeEventType RuntimeEventPayloadType(const QueueDepthChangedEvent&)
{
return RuntimeEventType::QueueDepthChanged;
}

View File

@@ -0,0 +1,99 @@
#pragma once
#include "RuntimeEvent.h"
#include <chrono>
#include <cstddef>
#include <deque>
#include <mutex>
#include <vector>
struct RuntimeEventQueueMetrics
{
std::size_t depth = 0;
std::size_t capacity = 0;
std::size_t droppedCount = 0;
double oldestEventAgeMilliseconds = 0.0;
};
class RuntimeEventQueue
{
public:
explicit RuntimeEventQueue(std::size_t capacity = 1024) :
mCapacity(capacity)
{
}
bool Push(RuntimeEvent event)
{
std::lock_guard<std::mutex> lock(mMutex);
if (mEvents.size() >= mCapacity)
{
++mDroppedCount;
return false;
}
mEvents.push_back(std::move(event));
return true;
}
bool TryPop(RuntimeEvent& event)
{
std::lock_guard<std::mutex> lock(mMutex);
if (mEvents.empty())
return false;
event = std::move(mEvents.front());
mEvents.pop_front();
return true;
}
std::vector<RuntimeEvent> Drain(std::size_t maxEvents = 0)
{
std::vector<RuntimeEvent> events;
std::lock_guard<std::mutex> lock(mMutex);
const std::size_t count = maxEvents == 0 || maxEvents > mEvents.size() ? mEvents.size() : maxEvents;
events.reserve(count);
for (std::size_t index = 0; index < count; ++index)
{
events.push_back(std::move(mEvents.front()));
mEvents.pop_front();
}
return events;
}
RuntimeEventQueueMetrics GetMetrics(std::chrono::steady_clock::time_point now = std::chrono::steady_clock::now()) const
{
std::lock_guard<std::mutex> lock(mMutex);
RuntimeEventQueueMetrics metrics;
metrics.depth = mEvents.size();
metrics.capacity = mCapacity;
metrics.droppedCount = mDroppedCount;
if (!mEvents.empty())
{
const auto age = now - mEvents.front().createdAt;
metrics.oldestEventAgeMilliseconds = std::chrono::duration<double, std::milli>(age).count();
}
return metrics;
}
std::size_t Depth() const
{
std::lock_guard<std::mutex> lock(mMutex);
return mEvents.size();
}
std::size_t Capacity() const
{
return mCapacity;
}
private:
mutable std::mutex mMutex;
std::deque<RuntimeEvent> mEvents;
std::size_t mCapacity = 0;
std::size_t mDroppedCount = 0;
};

View File

@@ -0,0 +1,151 @@
#pragma once
#include <string_view>
enum class RuntimeEventType
{
Unknown = 0,
// Control ingress.
OscValueReceived,
OscValueCoalesced,
OscCommitRequested,
HttpControlMutationRequested,
WebSocketClientConnected,
RuntimeStateBroadcastRequested,
FileChangeDetected,
ManualReloadRequested,
// Runtime policy and state.
RuntimeMutationAccepted,
RuntimeMutationRejected,
RuntimeStateChanged,
RuntimePersistenceRequested,
RuntimeReloadRequested,
ShaderPackagesChanged,
RenderSnapshotPublishRequested,
RuntimeStatePresentationChanged,
// Shader build lifecycle.
ShaderBuildRequested,
ShaderBuildPrepared,
ShaderBuildApplied,
ShaderBuildFailed,
CompileStatusChanged,
// Render lifecycle.
RenderSnapshotPublished,
RenderResetRequested,
RenderResetApplied,
OscOverlayApplied,
OscOverlaySettled,
FrameRendered,
PreviewFrameAvailable,
// Video backend lifecycle.
InputSignalChanged,
InputFrameArrived,
OutputFrameScheduled,
OutputFrameCompleted,
OutputLateFrameDetected,
OutputDroppedFrameDetected,
BackendStateChanged,
// Health and telemetry.
SubsystemWarningRaised,
SubsystemWarningCleared,
SubsystemRecovered,
TimingSampleRecorded,
QueueDepthChanged
};
constexpr std::string_view RuntimeEventTypeName(RuntimeEventType type)
{
switch (type)
{
case RuntimeEventType::Unknown:
return "Unknown";
case RuntimeEventType::OscValueReceived:
return "OscValueReceived";
case RuntimeEventType::OscValueCoalesced:
return "OscValueCoalesced";
case RuntimeEventType::OscCommitRequested:
return "OscCommitRequested";
case RuntimeEventType::HttpControlMutationRequested:
return "HttpControlMutationRequested";
case RuntimeEventType::WebSocketClientConnected:
return "WebSocketClientConnected";
case RuntimeEventType::RuntimeStateBroadcastRequested:
return "RuntimeStateBroadcastRequested";
case RuntimeEventType::FileChangeDetected:
return "FileChangeDetected";
case RuntimeEventType::ManualReloadRequested:
return "ManualReloadRequested";
case RuntimeEventType::RuntimeMutationAccepted:
return "RuntimeMutationAccepted";
case RuntimeEventType::RuntimeMutationRejected:
return "RuntimeMutationRejected";
case RuntimeEventType::RuntimeStateChanged:
return "RuntimeStateChanged";
case RuntimeEventType::RuntimePersistenceRequested:
return "RuntimePersistenceRequested";
case RuntimeEventType::RuntimeReloadRequested:
return "RuntimeReloadRequested";
case RuntimeEventType::ShaderPackagesChanged:
return "ShaderPackagesChanged";
case RuntimeEventType::RenderSnapshotPublishRequested:
return "RenderSnapshotPublishRequested";
case RuntimeEventType::RuntimeStatePresentationChanged:
return "RuntimeStatePresentationChanged";
case RuntimeEventType::ShaderBuildRequested:
return "ShaderBuildRequested";
case RuntimeEventType::ShaderBuildPrepared:
return "ShaderBuildPrepared";
case RuntimeEventType::ShaderBuildApplied:
return "ShaderBuildApplied";
case RuntimeEventType::ShaderBuildFailed:
return "ShaderBuildFailed";
case RuntimeEventType::CompileStatusChanged:
return "CompileStatusChanged";
case RuntimeEventType::RenderSnapshotPublished:
return "RenderSnapshotPublished";
case RuntimeEventType::RenderResetRequested:
return "RenderResetRequested";
case RuntimeEventType::RenderResetApplied:
return "RenderResetApplied";
case RuntimeEventType::OscOverlayApplied:
return "OscOverlayApplied";
case RuntimeEventType::OscOverlaySettled:
return "OscOverlaySettled";
case RuntimeEventType::FrameRendered:
return "FrameRendered";
case RuntimeEventType::PreviewFrameAvailable:
return "PreviewFrameAvailable";
case RuntimeEventType::InputSignalChanged:
return "InputSignalChanged";
case RuntimeEventType::InputFrameArrived:
return "InputFrameArrived";
case RuntimeEventType::OutputFrameScheduled:
return "OutputFrameScheduled";
case RuntimeEventType::OutputFrameCompleted:
return "OutputFrameCompleted";
case RuntimeEventType::OutputLateFrameDetected:
return "OutputLateFrameDetected";
case RuntimeEventType::OutputDroppedFrameDetected:
return "OutputDroppedFrameDetected";
case RuntimeEventType::BackendStateChanged:
return "BackendStateChanged";
case RuntimeEventType::SubsystemWarningRaised:
return "SubsystemWarningRaised";
case RuntimeEventType::SubsystemWarningCleared:
return "SubsystemWarningCleared";
case RuntimeEventType::SubsystemRecovered:
return "SubsystemRecovered";
case RuntimeEventType::TimingSampleRecorded:
return "TimingSampleRecorded";
case RuntimeEventType::QueueDepthChanged:
return "QueueDepthChanged";
}
return "Unknown";
}

View File

@@ -307,6 +307,24 @@ Suggested components:
Initial implementation can be single-process and mostly single-dispatch-thread. The important part is that event publication and event handling become explicit. Initial implementation can be single-process and mostly single-dispatch-thread. The important part is that event publication and event handling become explicit.
### Dispatcher Ownership Decision
The first concrete implementation uses one app-owned `RuntimeEventDispatcher`.
Ownership:
- `OpenGLComposite` owns the dispatcher as part of the current composition root.
References:
- `RuntimeServices` receives the dispatcher and passes it to `ControlServices`.
- `RuntimeCoordinator` receives the dispatcher so coordinator outcomes can become explicit events.
- `RuntimeUpdateController` receives the dispatcher so it can become the first effect/apply handler.
This is intentionally a composition-root dependency, not a new subsystem dependency. Subsystems should not construct their own dispatchers, and future tests should use `RuntimeEventTestHarness` rather than creating ad hoc event plumbing.
The dispatcher should move out of `OpenGLComposite` only if a later application-shell/composition-root object replaces `OpenGLComposite` as the owner of subsystem wiring.
## Queue Policy ## Queue Policy
Not every event deserves the same queue semantics. Not every event deserves the same queue semantics.
@@ -342,6 +360,63 @@ Some calls may remain synchronous during Phase 2:
The rule is that synchronous calls should still publish events for accepted/rejected/completed work, so the rest of the app does not need to infer side effects from the call path. The rule is that synchronous calls should still publish events for accepted/rejected/completed work, so the rest of the app does not need to infer side effects from the call path.
## Event Bridge Policy
This section is the implementation rulebook for converting existing direct calls and result queues into events. Future Phase 2 lanes should use this table unless they deliberately update the policy here first.
### Bridge Categories
| Bridge category | Use when | Queue shape | Handler expectation |
| --- | --- | --- | --- |
| `fifo-fact` | every occurrence matters and must be observed in order | bounded FIFO | handler consumes each event exactly once |
| `coalesced-latest` | only the latest value per key matters | bounded coalescing queue | handler consumes the latest event and telemetry records collapsed count |
| `sync-command-with-event` | caller needs an immediate success/error result | direct owner call plus follow-up event publication | handler must not be required for the caller's response |
| `observation-only` | event is telemetry/diagnostic and must not drive core behavior | FIFO or coalesced depending on rate | handler failure must never block app behavior |
| `compatibility-poll` | source cannot yet publish an event directly | temporary poll adapter publishes typed events | poll interval should shrink or become wakeup-driven over Phase 2 |
### Current Bridge Decisions
| Current flow | First Phase 2 bridge | Event(s) | Queue policy |
| --- | --- | --- | --- |
| OSC latest-value updates | `ControlServices` ingress bridge | `OscValueReceived`, optional `OscValueCoalesced` | `coalesced-latest` by route key |
| OSC commit after settle | `ControlServices -> RuntimeCoordinator` bridge | `OscCommitRequested`, then `RuntimeMutationAccepted` or `RuntimeMutationRejected` | commit request `coalesced-latest` by route key; mutation result `fifo-fact` |
| HTTP/UI mutation needing response | direct call into `RuntimeCoordinator` | `RuntimeMutationAccepted` or `RuntimeMutationRejected` after the synchronous response path | `sync-command-with-event` |
| runtime-state broadcast request | presentation/broadcast bridge | `RuntimeStatePresentationChanged`, `RuntimeStateBroadcastRequested` | `coalesced-latest` by event type or reason family |
| manual reload button | control ingress bridge | `ManualReloadRequested`, then `RuntimeReloadRequested` | `fifo-fact` for manual request; reload execution may coalesce |
| file watcher changes | file-watch bridge | `FileChangeDetected`, then `RuntimeReloadRequested` | `coalesced-latest` by path, then coalesced reload request |
| runtime store poll fallback | compatibility poll adapter | `ShaderPackagesChanged`, `RuntimeReloadRequested`, or warning event | `compatibility-poll` until file events fully replace polling |
| shader build request | runtime/render bridge | `ShaderBuildRequested` | `coalesced-latest` by input dimensions and preserve-feedback flag |
| shader build ready/failure/apply | shader build lifecycle bridge | `ShaderBuildPrepared`, `ShaderBuildFailed`, `ShaderBuildApplied`, `CompileStatusChanged` | `fifo-fact` |
| render snapshot publication | snapshot bridge | `RenderSnapshotPublishRequested`, `RenderSnapshotPublished` | request may coalesce by output dimensions; published event is `fifo-fact` |
| render reset request/application | render bridge | `RenderResetRequested`, `RenderResetApplied` | `fifo-fact` |
| input signal changes | backend observation bridge | `InputSignalChanged` | `coalesced-latest` by signal lane |
| output late/dropped/completed frames | backend timing bridge | `OutputFrameCompleted`, `OutputLateFrameDetected`, `OutputDroppedFrameDetected` | late/dropped `fifo-fact`; high-rate completed frames may become `observation-only` coalesced metrics |
| warnings and recovery | telemetry bridge | `SubsystemWarningRaised`, `SubsystemWarningCleared`, `SubsystemRecovered` | `fifo-fact` for lifecycle transitions |
| queue depth/timing samples | telemetry metrics bridge | `QueueDepthChanged`, `TimingSampleRecorded` | `coalesced-latest` by metric key |
### Bridge Rules
- A bridge may translate an old direct call into an owner command, but it must publish the accepted/rejected/completed event that describes the outcome.
- A bridge must not mutate state owned by another subsystem just because it handles that subsystem's event.
- A coalesced event must have a stable key in code and a documented policy here.
- A FIFO event should be cheap enough that retaining every occurrence is useful. If not, turn it into a coalesced metric before putting it on a hot path.
- A synchronous bridge must treat event publication as a side effect of the owner decision, not as the mechanism that produces the direct caller's response.
- A compatibility poll adapter should be named as temporary in code so it does not become the new long-term coordination model.
- Handler failure should be reported through telemetry and dispatch metrics. It should not throw back across subsystem boundaries.
### First Integration Recommendation
The safest first behavior-changing bridge is `RuntimeStateBroadcastRequested`.
It is low risk because:
- it is already a side effect of many coordinator outcomes
- duplicate requests are naturally coalescable
- the handler can call the existing `ControlServices::BroadcastState()` path
- success can be verified through existing UI behavior and event tests
After that, the next bridge should be `ShaderBuildRequested`, because it already behaves like a queued side effect and has clear follow-up events.
## Target Flow Examples ## Target Flow Examples
### OSC Parameter Update ### OSC Parameter Update

View File

@@ -0,0 +1,121 @@
#pragma once
#include "RuntimeEventCoalescingQueue.h"
#include "RuntimeEventDispatcher.h"
#include <cstddef>
#include <utility>
#include <vector>
class RuntimeEventTestHarness
{
public:
explicit RuntimeEventTestHarness(std::size_t dispatchQueueCapacity = 64, std::size_t coalescingQueueCapacity = 64) :
mDispatcher(dispatchQueueCapacity),
mCoalescingQueue(coalescingQueueCapacity)
{
mDispatcher.SubscribeAll([this](const RuntimeEvent& event) {
mSeenEvents.push_back(event);
});
}
RuntimeEventDispatcher& Dispatcher()
{
return mDispatcher;
}
const RuntimeEventDispatcher& Dispatcher() const
{
return mDispatcher;
}
RuntimeEventCoalescingQueue& CoalescingQueue()
{
return mCoalescingQueue;
}
template <typename Payload>
bool Publish(Payload payload, std::string source = {})
{
return mDispatcher.PublishPayload(std::move(payload), std::move(source));
}
bool Publish(RuntimeEvent event)
{
return mDispatcher.Publish(std::move(event));
}
template <typename Payload>
RuntimeEventDispatchResult PublishAndDispatch(Payload payload, std::string source = {})
{
Publish(std::move(payload), std::move(source));
return DispatchPending();
}
RuntimeEventDispatchResult DispatchPending(std::size_t maxEvents = 0)
{
return mDispatcher.DispatchPending(maxEvents);
}
template <typename Payload>
bool PublishCoalesced(Payload payload, std::string source = {}, uint64_t sequence = 0)
{
return mCoalescingQueue.Push(MakeRuntimeEvent(std::move(payload), std::move(source), sequence));
}
std::size_t FlushCoalescedToDispatcher(std::size_t maxEvents = 0)
{
std::vector<RuntimeEvent> events = mCoalescingQueue.Drain(maxEvents);
const std::size_t eventCount = events.size();
for (RuntimeEvent& event : events)
mDispatcher.Publish(std::move(event));
return eventCount;
}
RuntimeEventDispatchResult FlushCoalescedAndDispatch(std::size_t maxEvents = 0)
{
FlushCoalescedToDispatcher(maxEvents);
return DispatchPending();
}
const std::vector<RuntimeEvent>& SeenEvents() const
{
return mSeenEvents;
}
std::size_t SeenCount() const
{
return mSeenEvents.size();
}
std::size_t SeenCount(RuntimeEventType type) const
{
std::size_t count = 0;
for (const RuntimeEvent& event : mSeenEvents)
{
if (event.type == type)
++count;
}
return count;
}
const RuntimeEvent* LastSeen(RuntimeEventType type) const
{
for (auto it = mSeenEvents.rbegin(); it != mSeenEvents.rend(); ++it)
{
if (it->type == type)
return &(*it);
}
return nullptr;
}
void ClearSeen()
{
mSeenEvents.clear();
}
private:
RuntimeEventDispatcher mDispatcher;
RuntimeEventCoalescingQueue mCoalescingQueue;
std::vector<RuntimeEvent> mSeenEvents;
};

View File

@@ -0,0 +1,334 @@
#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;
Expect(RuntimeEventPayloadType(acceptedMutation) == RuntimeEventType::RuntimeMutationAccepted, "accepted mutation payload maps to accepted event type");
Expect(acceptedMutation.shaderBuildRequested, "mutation payload carries shader build 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");
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");
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);
Expect(tinyDispatcher.PublishPayload(broadcast, "test"), "tiny dispatcher accepts first event");
Expect(!tinyDispatcher.PublishPayload(broadcast, "test"), "tiny dispatcher rejects event when queue is full");
RuntimeEventQueueMetrics metrics = tinyDispatcher.GetQueueMetrics();
Expect(metrics.droppedCount == 1, "dispatcher exposes queue drop metrics");
}
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");
}
}
int main()
{
TestRuntimeEventTypeNames();
TestRuntimeEventPayloadTypes();
TestRuntimeEventEnvelope();
TestRuntimeEventQueue();
TestRuntimeEventDispatcher();
TestRuntimeEventCoalescingQueue();
TestRuntimeEventCoalescingCustomKey();
TestRuntimeEventTestHarness();
if (gFailures != 0)
{
std::cerr << gFailures << " RuntimeEventType test failure(s).\n";
return 1;
}
std::cout << "RuntimeEventType tests passed.\n";
return 0;
}