diff --git a/apps/LoopThroughWithOpenGLCompositing/control/ControlServices.cpp b/apps/LoopThroughWithOpenGLCompositing/control/ControlServices.cpp index 4a4be87..22134b5 100644 --- a/apps/LoopThroughWithOpenGLCompositing/control/ControlServices.cpp +++ b/apps/LoopThroughWithOpenGLCompositing/control/ControlServices.cpp @@ -7,6 +7,11 @@ #include "RuntimeStore.h" #include +namespace +{ +constexpr auto kCompatibilityPollFallbackInterval = std::chrono::milliseconds(250); +} + ControlServices::ControlServices(RuntimeEventDispatcher& runtimeEventDispatcher) : mControlServer(std::make_unique()), mOscServer(std::make_unique()), @@ -130,6 +135,7 @@ bool ControlServices::QueueOscCommit(const std::string& routeKey, const std::str std::lock_guard lock(mPendingOscCommitMutex); mPendingOscCommits[routeKey] = std::move(commit); } + WakePolling(); return true; } @@ -184,6 +190,7 @@ void ControlServices::StopPolling() if (!mPollRunning.exchange(false)) return; + WakePolling(); if (mPollThread.joinable()) mPollThread.join(); } @@ -224,11 +231,23 @@ void ControlServices::PollLoop(RuntimeCoordinator& runtimeCoordinator) if (pollResult.runtimeStateBroadcastRequired || pollResult.shaderBuildRequested || pollResult.compileStatusChanged) QueueRuntimeCoordinatorResult(pollResult, pollResult.compileStatusChanged && !pollResult.compileStatusSucceeded && !pollResult.compileStatusMessage.empty()); - for (int i = 0; i < 25 && mPollRunning; ++i) - Sleep(10); + std::unique_lock wakeLock(mPollWakeMutex); + mPollWakeCondition.wait_for(wakeLock, kCompatibilityPollFallbackInterval, [this]() { + return !mPollRunning.load() || mPollWakeRequested; + }); + mPollWakeRequested = false; } } +void ControlServices::WakePolling() +{ + { + std::lock_guard lock(mPollWakeMutex); + mPollWakeRequested = true; + } + mPollWakeCondition.notify_one(); +} + void ControlServices::QueueRuntimeCoordinatorResult(const RuntimeCoordinatorResult& result, bool failed) { RuntimeCoordinatorServiceResult serviceResult; diff --git a/apps/LoopThroughWithOpenGLCompositing/control/ControlServices.h b/apps/LoopThroughWithOpenGLCompositing/control/ControlServices.h index 5f4a4fd..1657653 100644 --- a/apps/LoopThroughWithOpenGLCompositing/control/ControlServices.h +++ b/apps/LoopThroughWithOpenGLCompositing/control/ControlServices.h @@ -5,6 +5,8 @@ #include "ShaderTypes.h" #include +#include +#include #include #include #include @@ -76,6 +78,7 @@ private: void StartPolling(RuntimeCoordinator& runtimeCoordinator); void StopPolling(); void PollLoop(RuntimeCoordinator& runtimeCoordinator); + void WakePolling(); void QueueRuntimeCoordinatorResult(const RuntimeCoordinatorResult& result, bool failed = false); void PublishRuntimeStateBroadcastRequested(const std::string& reason); void PublishOscValueReceived(const PendingOscUpdate& update, const std::string& routeKey); @@ -86,6 +89,9 @@ private: RuntimeEventDispatcher& mRuntimeEventDispatcher; std::thread mPollThread; std::atomic mPollRunning; + std::mutex mPollWakeMutex; + std::condition_variable mPollWakeCondition; + bool mPollWakeRequested = false; std::mutex mRuntimeCoordinatorResultMutex; std::vector mRuntimeCoordinatorResults; std::mutex mPendingOscMutex; diff --git a/apps/LoopThroughWithOpenGLCompositing/gl/OpenGLComposite.cpp b/apps/LoopThroughWithOpenGLCompositing/gl/OpenGLComposite.cpp index f15677f..3e9f67c 100644 --- a/apps/LoopThroughWithOpenGLCompositing/gl/OpenGLComposite.cpp +++ b/apps/LoopThroughWithOpenGLCompositing/gl/OpenGLComposite.cpp @@ -38,7 +38,7 @@ OpenGLComposite::OpenGLComposite(HWND hWnd, HDC hDC, HGLRC hRC) : InitializeCriticalSection(&pMutex); mRuntimeStore = std::make_unique(); mRuntimeEventDispatcher = std::make_unique(); - mRuntimeSnapshotProvider = std::make_unique(mRuntimeStore->GetRenderSnapshotBuilder()); + mRuntimeSnapshotProvider = std::make_unique(mRuntimeStore->GetRenderSnapshotBuilder(), *mRuntimeEventDispatcher); mRuntimeCoordinator = std::make_unique(*mRuntimeStore, *mRuntimeEventDispatcher); mRenderEngine = std::make_unique( *mRuntimeSnapshotProvider, @@ -49,7 +49,7 @@ OpenGLComposite::OpenGLComposite(HWND hWnd, HDC hDC, HGLRC hRC) : [this]() { renderEffect(); }, [this]() { ProcessScreenshotRequest(); }, [this]() { paintGL(false); }); - mVideoBackend = std::make_unique(*mRenderEngine, mRuntimeStore->GetHealthTelemetry()); + mVideoBackend = std::make_unique(*mRenderEngine, mRuntimeStore->GetHealthTelemetry(), *mRuntimeEventDispatcher); mShaderBuildQueue = std::make_unique(*mRuntimeSnapshotProvider, *mRuntimeEventDispatcher); mRuntimeServices = std::make_unique(*mRuntimeEventDispatcher); mRuntimeUpdateController = std::make_unique( @@ -300,7 +300,7 @@ bool OpenGLComposite::RequestScreenshot(std::string& error) void OpenGLComposite::renderEffect() { - if (mRuntimeUpdateController) + if (mRuntimeUpdateController && ProcessRuntimeServiceResults()) mRuntimeUpdateController->ProcessRuntimeWork(); std::vector appliedOscUpdates; std::vector completedOscCommits; @@ -369,6 +369,25 @@ void OpenGLComposite::renderEffect() historyCap); } +bool OpenGLComposite::ProcessRuntimeServiceResults() +{ + if (!mRuntimeServices || !mRuntimeUpdateController) + return true; + + bool shaderBuildRequested = false; + std::vector serviceResults; + mRuntimeServices->ConsumeRuntimeCoordinatorResults(serviceResults); + for (const RuntimeCoordinatorServiceResult& serviceResult : serviceResults) + { + shaderBuildRequested = shaderBuildRequested || serviceResult.result.shaderBuildRequested; + mRuntimeUpdateController->ApplyRuntimeCoordinatorResult(serviceResult.result); + if (serviceResult.failed) + return false; + } + + return !shaderBuildRequested; +} + void OpenGLComposite::ProcessScreenshotRequest() { if (!mScreenshotRequested.exchange(false)) diff --git a/apps/LoopThroughWithOpenGLCompositing/gl/OpenGLComposite.h b/apps/LoopThroughWithOpenGLCompositing/gl/OpenGLComposite.h index 7bd86b8..ea4400e 100644 --- a/apps/LoopThroughWithOpenGLCompositing/gl/OpenGLComposite.h +++ b/apps/LoopThroughWithOpenGLCompositing/gl/OpenGLComposite.h @@ -87,6 +87,7 @@ private: bool InitOpenGLState(); void renderEffect(); + bool ProcessRuntimeServiceResults(); void ProcessScreenshotRequest(); std::filesystem::path BuildScreenshotPath() const; }; diff --git a/apps/LoopThroughWithOpenGLCompositing/gl/RuntimeUpdateController.cpp b/apps/LoopThroughWithOpenGLCompositing/gl/RuntimeUpdateController.cpp index 434ed93..aa37bf7 100644 --- a/apps/LoopThroughWithOpenGLCompositing/gl/RuntimeUpdateController.cpp +++ b/apps/LoopThroughWithOpenGLCompositing/gl/RuntimeUpdateController.cpp @@ -8,7 +8,6 @@ #include "VideoBackend.h" #include -#include namespace { @@ -107,20 +106,6 @@ bool RuntimeUpdateController::ApplyRuntimeCoordinatorResult(const RuntimeCoordin bool RuntimeUpdateController::ProcessRuntimeWork() { - bool shaderBuildRequested = false; - std::vector serviceResults; - mRuntimeServices.ConsumeRuntimeCoordinatorResults(serviceResults); - for (const RuntimeCoordinatorServiceResult& serviceResult : serviceResults) - { - shaderBuildRequested = shaderBuildRequested || serviceResult.result.shaderBuildRequested; - ApplyRuntimeCoordinatorResult(serviceResult.result); - if (serviceResult.failed) - return false; - } - - if (shaderBuildRequested) - return true; - DispatchRuntimeEvents(); return ConsumeReadyShaderBuild(0, true, true); @@ -324,5 +309,33 @@ RuntimeEventDispatchResult RuntimeUpdateController::DispatchRuntimeEvents(std::s queueMetrics.capacity, static_cast(queueMetrics.droppedCount), queueMetrics.oldestEventAgeMilliseconds); + PublishRuntimeEventHealthObservations(result); return result; } + +void RuntimeUpdateController::PublishRuntimeEventHealthObservations(const RuntimeEventDispatchResult& result) +{ + const RuntimeEventQueueMetrics queueMetrics = mRuntimeEventDispatcher.GetQueueMetrics(); + if (queueMetrics.depth != mLastReportedRuntimeEventQueueDepth || + queueMetrics.droppedCount != mLastReportedRuntimeEventDroppedCount) + { + QueueDepthChangedEvent queueDepth; + queueDepth.queueName = "runtime-events"; + queueDepth.depth = queueMetrics.depth; + queueDepth.capacity = queueMetrics.capacity; + queueDepth.droppedCount = queueMetrics.droppedCount; + mRuntimeEventDispatcher.PublishPayload(queueDepth, "HealthTelemetry"); + mLastReportedRuntimeEventQueueDepth = queueMetrics.depth; + mLastReportedRuntimeEventDroppedCount = queueMetrics.droppedCount; + } + + if (result.handlerInvocations == 0 && result.handlerFailures == 0) + return; + + TimingSampleRecordedEvent timing; + timing.subsystem = "RuntimeEventDispatcher"; + timing.metric = "dispatchDuration"; + timing.value = result.dispatchDurationMilliseconds; + timing.unit = "ms"; + mRuntimeEventDispatcher.PublishPayload(timing, "HealthTelemetry"); +} diff --git a/apps/LoopThroughWithOpenGLCompositing/gl/RuntimeUpdateController.h b/apps/LoopThroughWithOpenGLCompositing/gl/RuntimeUpdateController.h index 20ca169..93cfc6b 100644 --- a/apps/LoopThroughWithOpenGLCompositing/gl/RuntimeUpdateController.h +++ b/apps/LoopThroughWithOpenGLCompositing/gl/RuntimeUpdateController.h @@ -50,6 +50,7 @@ private: const std::string& message); bool ShouldSuppressCoordinatorFollowUp(const RuntimeEvent& event, std::size_t& pendingSuppressions); RuntimeEventDispatchResult DispatchRuntimeEvents(std::size_t maxEvents = 0); + void PublishRuntimeEventHealthObservations(const RuntimeEventDispatchResult& result); RuntimeStore& mRuntimeStore; RuntimeCoordinator& mRuntimeCoordinator; @@ -61,4 +62,6 @@ private: std::size_t mPendingCoordinatorShaderBuildEvents = 0; std::size_t mPendingCoordinatorCompileStatusEvents = 0; std::size_t mPendingCoordinatorRenderResetEvents = 0; + std::size_t mLastReportedRuntimeEventQueueDepth = static_cast(-1); + std::size_t mLastReportedRuntimeEventDroppedCount = static_cast(-1); }; diff --git a/apps/LoopThroughWithOpenGLCompositing/runtime/coordination/RuntimeCoordinator.cpp b/apps/LoopThroughWithOpenGLCompositing/runtime/coordination/RuntimeCoordinator.cpp index 151866f..47f91cc 100644 --- a/apps/LoopThroughWithOpenGLCompositing/runtime/coordination/RuntimeCoordinator.cpp +++ b/apps/LoopThroughWithOpenGLCompositing/runtime/coordination/RuntimeCoordinator.cpp @@ -34,12 +34,12 @@ RuntimeCoordinatorResult RuntimeCoordinator::AddLayer(const std::string& shaderI std::string error; if (!ValidateShaderExists(shaderId, error)) { - RuntimeCoordinatorResult result = ApplyStoreMutation(false, error, false, false); + RuntimeCoordinatorResult result = ApplyStoreMutation(false, error, false, false, false); PublishCoordinatorResult("AddLayer", result); return result; } - RuntimeCoordinatorResult result = ApplyStoreMutation(mRuntimeStore.CreateStoredLayer(shaderId, error), error, true, true); + RuntimeCoordinatorResult result = ApplyStoreMutation(mRuntimeStore.CreateStoredLayer(shaderId, error), error, true, true, true); PublishCoordinatorResult("AddLayer", result); return result; } @@ -50,12 +50,12 @@ RuntimeCoordinatorResult RuntimeCoordinator::RemoveLayer(const std::string& laye std::string error; if (!ValidateLayerExists(layerId, error)) { - RuntimeCoordinatorResult result = ApplyStoreMutation(false, error, false, false); + RuntimeCoordinatorResult result = ApplyStoreMutation(false, error, false, false, false); PublishCoordinatorResult("RemoveLayer", result); return result; } - RuntimeCoordinatorResult result = ApplyStoreMutation(mRuntimeStore.DeleteStoredLayer(layerId, error), error, true, true); + RuntimeCoordinatorResult result = ApplyStoreMutation(mRuntimeStore.DeleteStoredLayer(layerId, error), error, true, true, true); PublishCoordinatorResult("RemoveLayer", result); return result; } @@ -67,7 +67,7 @@ RuntimeCoordinatorResult RuntimeCoordinator::MoveLayer(const std::string& layerI bool shouldMove = false; if (!ResolveLayerMove(layerId, direction, shouldMove, error)) { - RuntimeCoordinatorResult result = ApplyStoreMutation(false, error, false, false); + RuntimeCoordinatorResult result = ApplyStoreMutation(false, error, false, false, false); PublishCoordinatorResult("MoveLayer", result); return result; } @@ -77,7 +77,7 @@ RuntimeCoordinatorResult RuntimeCoordinator::MoveLayer(const std::string& layerI return result; } - RuntimeCoordinatorResult result = ApplyStoreMutation(mRuntimeStore.MoveStoredLayer(layerId, direction, error), error, true, true); + RuntimeCoordinatorResult result = ApplyStoreMutation(mRuntimeStore.MoveStoredLayer(layerId, direction, error), error, true, true, true); PublishCoordinatorResult("MoveLayer", result); return result; } @@ -89,7 +89,7 @@ RuntimeCoordinatorResult RuntimeCoordinator::MoveLayerToIndex(const std::string& bool shouldMove = false; if (!ResolveLayerMoveToIndex(layerId, targetIndex, shouldMove, error)) { - RuntimeCoordinatorResult result = ApplyStoreMutation(false, error, false, false); + RuntimeCoordinatorResult result = ApplyStoreMutation(false, error, false, false, false); PublishCoordinatorResult("MoveLayerToIndex", result); return result; } @@ -99,7 +99,7 @@ RuntimeCoordinatorResult RuntimeCoordinator::MoveLayerToIndex(const std::string& return result; } - RuntimeCoordinatorResult result = ApplyStoreMutation(mRuntimeStore.MoveStoredLayerToIndex(layerId, targetIndex, error), error, true, true); + RuntimeCoordinatorResult result = ApplyStoreMutation(mRuntimeStore.MoveStoredLayerToIndex(layerId, targetIndex, error), error, true, true, true); PublishCoordinatorResult("MoveLayerToIndex", result); return result; } @@ -110,12 +110,12 @@ RuntimeCoordinatorResult RuntimeCoordinator::SetLayerBypass(const std::string& l std::string error; if (!ValidateLayerExists(layerId, error)) { - RuntimeCoordinatorResult result = ApplyStoreMutation(false, error, false, false); + RuntimeCoordinatorResult result = ApplyStoreMutation(false, error, false, false, false); PublishCoordinatorResult("SetLayerBypass", result); return result; } - RuntimeCoordinatorResult result = ApplyStoreMutation(mRuntimeStore.SetStoredLayerBypassState(layerId, bypassed, error), error, true, false); + RuntimeCoordinatorResult result = ApplyStoreMutation(mRuntimeStore.SetStoredLayerBypassState(layerId, bypassed, error), error, true, false, true); PublishCoordinatorResult("SetLayerBypass", result); return result; } @@ -126,12 +126,12 @@ RuntimeCoordinatorResult RuntimeCoordinator::SetLayerShader(const std::string& l std::string error; if (!ValidateLayerExists(layerId, error) || !ValidateShaderExists(shaderId, error)) { - RuntimeCoordinatorResult result = ApplyStoreMutation(false, error, false, false); + RuntimeCoordinatorResult result = ApplyStoreMutation(false, error, false, false, false); PublishCoordinatorResult("SetLayerShader", result); return result; } - RuntimeCoordinatorResult result = ApplyStoreMutation(mRuntimeStore.SetStoredLayerShaderSelection(layerId, shaderId, error), error, true, false); + RuntimeCoordinatorResult result = ApplyStoreMutation(mRuntimeStore.SetStoredLayerShaderSelection(layerId, shaderId, error), error, true, false, true); PublishCoordinatorResult("SetLayerShader", result); return result; } @@ -143,12 +143,12 @@ RuntimeCoordinatorResult RuntimeCoordinator::UpdateLayerParameter(const std::str ResolvedParameterMutation mutation; if (!BuildParameterMutationById(layerId, parameterId, newValue, true, mutation, error)) { - RuntimeCoordinatorResult result = ApplyStoreMutation(false, error, false, false); + RuntimeCoordinatorResult result = ApplyStoreMutation(false, error, false, false, false); PublishCoordinatorResult("UpdateLayerParameter", result); return result; } - RuntimeCoordinatorResult result = ApplyStoreMutation(mRuntimeStore.SetStoredParameterValue(mutation.layerId, mutation.parameterId, mutation.value, mutation.persistState, error), error, false, false); + RuntimeCoordinatorResult result = ApplyStoreMutation(mRuntimeStore.SetStoredParameterValue(mutation.layerId, mutation.parameterId, mutation.value, mutation.persistState, error), error, false, false, mutation.persistState); PublishCoordinatorResult("UpdateLayerParameter", result); return result; } @@ -160,12 +160,12 @@ RuntimeCoordinatorResult RuntimeCoordinator::UpdateLayerParameterByControlKey(co ResolvedParameterMutation mutation; if (!BuildParameterMutationByControlKey(layerKey, parameterKey, newValue, true, mutation, error)) { - RuntimeCoordinatorResult result = ApplyStoreMutation(false, error, false, false); + RuntimeCoordinatorResult result = ApplyStoreMutation(false, error, false, false, false); PublishCoordinatorResult("UpdateLayerParameterByControlKey", result); return result; } - RuntimeCoordinatorResult result = ApplyStoreMutation(mRuntimeStore.SetStoredParameterValue(mutation.layerId, mutation.parameterId, mutation.value, mutation.persistState, error), error, false, false); + RuntimeCoordinatorResult result = ApplyStoreMutation(mRuntimeStore.SetStoredParameterValue(mutation.layerId, mutation.parameterId, mutation.value, mutation.persistState, error), error, false, false, mutation.persistState); PublishCoordinatorResult("UpdateLayerParameterByControlKey", result); return result; } @@ -177,12 +177,12 @@ RuntimeCoordinatorResult RuntimeCoordinator::CommitOscParameterByControlKey(cons ResolvedParameterMutation mutation; if (!BuildParameterMutationByControlKey(layerKey, parameterKey, newValue, false, mutation, error)) { - RuntimeCoordinatorResult result = ApplyStoreMutation(false, error, false, false); + RuntimeCoordinatorResult result = ApplyStoreMutation(false, error, false, false, false); PublishCoordinatorResult("CommitOscParameterByControlKey", result); return result; } - RuntimeCoordinatorResult result = ApplyStoreMutation(mRuntimeStore.SetStoredParameterValue(mutation.layerId, mutation.parameterId, mutation.value, mutation.persistState, error), error, false, false); + RuntimeCoordinatorResult result = ApplyStoreMutation(mRuntimeStore.SetStoredParameterValue(mutation.layerId, mutation.parameterId, mutation.value, mutation.persistState, error), error, false, false, mutation.persistState); PublishCoordinatorResult("CommitOscParameterByControlKey", result); return result; } @@ -193,12 +193,12 @@ RuntimeCoordinatorResult RuntimeCoordinator::ResetLayerParameters(const std::str std::string error; if (!ValidateLayerExists(layerId, error)) { - RuntimeCoordinatorResult result = ApplyStoreMutation(false, error, false, false); + RuntimeCoordinatorResult result = ApplyStoreMutation(false, error, false, false, false); PublishCoordinatorResult("ResetLayerParameters", result); return result; } - RuntimeCoordinatorResult result = ApplyStoreMutation(mRuntimeStore.ResetStoredLayerParameterValues(layerId, error), error, false, false); + RuntimeCoordinatorResult result = ApplyStoreMutation(mRuntimeStore.ResetStoredLayerParameterValues(layerId, error), error, false, false, true); if (!result.accepted) { PublishCoordinatorResult("ResetLayerParameters", result); @@ -217,12 +217,12 @@ RuntimeCoordinatorResult RuntimeCoordinator::SaveStackPreset(const std::string& std::string error; if (!ValidatePresetName(presetName, error)) { - RuntimeCoordinatorResult result = ApplyStoreMutation(false, error, false, false); + RuntimeCoordinatorResult result = ApplyStoreMutation(false, error, false, false, false); PublishCoordinatorResult("SaveStackPreset", result); return result; } - RuntimeCoordinatorResult result = ApplyStoreMutation(mRuntimeStore.SaveStackPresetSnapshot(presetName, error), error, false, false); + RuntimeCoordinatorResult result = ApplyStoreMutation(mRuntimeStore.SaveStackPresetSnapshot(presetName, error), error, false, false, true); PublishCoordinatorResult("SaveStackPreset", result); return result; } @@ -233,12 +233,12 @@ RuntimeCoordinatorResult RuntimeCoordinator::LoadStackPreset(const std::string& std::string error; if (!ValidatePresetName(presetName, error)) { - RuntimeCoordinatorResult result = ApplyStoreMutation(false, error, false, false); + RuntimeCoordinatorResult result = ApplyStoreMutation(false, error, false, false, false); PublishCoordinatorResult("LoadStackPreset", result); return result; } - RuntimeCoordinatorResult result = ApplyStoreMutation(mRuntimeStore.LoadStackPresetSnapshot(presetName, error), error, true, false); + RuntimeCoordinatorResult result = ApplyStoreMutation(mRuntimeStore.LoadStackPresetSnapshot(presetName, error), error, true, false, true); PublishCoordinatorResult("LoadStackPreset", result); return result; } @@ -446,7 +446,7 @@ bool RuntimeCoordinator::ValidatePresetName(const std::string& presetName, std:: return false; } -RuntimeCoordinatorResult RuntimeCoordinator::ApplyStoreMutation(bool succeeded, const std::string& errorMessage, bool reloadRequired, bool preserveFeedbackState) +RuntimeCoordinatorResult RuntimeCoordinator::ApplyStoreMutation(bool succeeded, const std::string& errorMessage, bool reloadRequired, bool preserveFeedbackState, bool persistenceRequested) { if (!succeeded) { @@ -457,9 +457,15 @@ RuntimeCoordinatorResult RuntimeCoordinator::ApplyStoreMutation(bool succeeded, } if (reloadRequired) - return BuildQueuedReloadResult(preserveFeedbackState); + { + RuntimeCoordinatorResult result = BuildQueuedReloadResult(preserveFeedbackState); + result.persistenceRequested = persistenceRequested; + return result; + } - return BuildAcceptedNoReloadResult(); + RuntimeCoordinatorResult result = BuildAcceptedNoReloadResult(); + result.persistenceRequested = persistenceRequested; + return result; } RuntimeCoordinatorResult RuntimeCoordinator::BuildQueuedReloadResult(bool preserveFeedbackState) @@ -497,6 +503,7 @@ void RuntimeCoordinator::PublishCoordinatorResult(const std::string& action, con mutation.runtimeStateChanged = result.accepted && result.runtimeStateBroadcastRequired; mutation.runtimeStateBroadcastRequired = result.runtimeStateBroadcastRequired; mutation.shaderBuildRequested = result.shaderBuildRequested; + mutation.persistenceRequested = result.persistenceRequested; mutation.clearTransientOscState = result.clearTransientOscState; mutation.renderResetScope = ToRuntimeEventRenderResetScope(result.renderResetScope); mutation.errorMessage = result.errorMessage; @@ -521,9 +528,18 @@ void RuntimeCoordinator::PublishCoordinatorFollowUpEvents(const std::string& act RuntimeStateChangedEvent stateChanged; stateChanged.reason = action; stateChanged.renderVisible = result.renderResetScope != RuntimeCoordinatorRenderResetScope::None; + stateChanged.persistenceRequested = result.persistenceRequested; mRuntimeEventDispatcher.PublishPayload(stateChanged, "RuntimeCoordinator"); } + if (result.persistenceRequested) + { + RuntimePersistenceRequestedEvent persistenceRequested; + persistenceRequested.reason = action; + persistenceRequested.debounceAllowed = true; + mRuntimeEventDispatcher.PublishPayload(persistenceRequested, "RuntimeCoordinator"); + } + if (result.shaderBuildRequested) { RuntimeReloadRequestedEvent reloadRequested; diff --git a/apps/LoopThroughWithOpenGLCompositing/runtime/coordination/RuntimeCoordinator.h b/apps/LoopThroughWithOpenGLCompositing/runtime/coordination/RuntimeCoordinator.h index 7d4c3ee..292013e 100644 --- a/apps/LoopThroughWithOpenGLCompositing/runtime/coordination/RuntimeCoordinator.h +++ b/apps/LoopThroughWithOpenGLCompositing/runtime/coordination/RuntimeCoordinator.h @@ -30,6 +30,7 @@ struct RuntimeCoordinatorResult bool accepted = false; bool runtimeStateBroadcastRequired = false; bool shaderBuildRequested = false; + bool persistenceRequested = false; bool clearTransientOscState = false; bool compileStatusChanged = false; bool compileStatusSucceeded = false; @@ -89,7 +90,7 @@ private: bool ResolveLayerMove(const std::string& layerId, int direction, bool& shouldMove, std::string& error) const; bool ResolveLayerMoveToIndex(const std::string& layerId, std::size_t targetIndex, bool& shouldMove, std::string& error) const; bool ValidatePresetName(const std::string& presetName, std::string& error) const; - RuntimeCoordinatorResult ApplyStoreMutation(bool succeeded, const std::string& errorMessage, bool reloadRequired, bool preserveFeedbackState); + RuntimeCoordinatorResult ApplyStoreMutation(bool succeeded, const std::string& errorMessage, bool reloadRequired, bool preserveFeedbackState, bool persistenceRequested); RuntimeCoordinatorResult BuildQueuedReloadResult(bool preserveFeedbackState); RuntimeCoordinatorResult BuildAcceptedNoReloadResult() const; void PublishCoordinatorResult(const std::string& action, const RuntimeCoordinatorResult& result) const; diff --git a/apps/LoopThroughWithOpenGLCompositing/runtime/events/RuntimeEventPayloads.h b/apps/LoopThroughWithOpenGLCompositing/runtime/events/RuntimeEventPayloads.h index b09efd7..95489b9 100644 --- a/apps/LoopThroughWithOpenGLCompositing/runtime/events/RuntimeEventPayloads.h +++ b/apps/LoopThroughWithOpenGLCompositing/runtime/events/RuntimeEventPayloads.h @@ -406,8 +406,12 @@ constexpr RuntimeEventType RuntimeEventPayloadType(const OutputFrameScheduledEve return RuntimeEventType::OutputFrameScheduled; } -constexpr RuntimeEventType RuntimeEventPayloadType(const OutputFrameCompletedEvent&) +inline RuntimeEventType RuntimeEventPayloadType(const OutputFrameCompletedEvent& event) { + if (event.result == "DisplayedLate") + return RuntimeEventType::OutputLateFrameDetected; + if (event.result == "Dropped") + return RuntimeEventType::OutputDroppedFrameDetected; return RuntimeEventType::OutputFrameCompleted; } diff --git a/apps/LoopThroughWithOpenGLCompositing/runtime/snapshot/RuntimeSnapshotProvider.cpp b/apps/LoopThroughWithOpenGLCompositing/runtime/snapshot/RuntimeSnapshotProvider.cpp index 541cb61..00e3e89 100644 --- a/apps/LoopThroughWithOpenGLCompositing/runtime/snapshot/RuntimeSnapshotProvider.cpp +++ b/apps/LoopThroughWithOpenGLCompositing/runtime/snapshot/RuntimeSnapshotProvider.cpp @@ -1,9 +1,14 @@ #include "RuntimeSnapshotProvider.h" +#include "RuntimeEventDispatcher.h" + +#include + #include -RuntimeSnapshotProvider::RuntimeSnapshotProvider(RenderSnapshotBuilder& renderSnapshotBuilder) : - mRenderSnapshotBuilder(renderSnapshotBuilder) +RuntimeSnapshotProvider::RuntimeSnapshotProvider(RenderSnapshotBuilder& renderSnapshotBuilder, RuntimeEventDispatcher& runtimeEventDispatcher) : + mRenderSnapshotBuilder(renderSnapshotBuilder), + mRuntimeEventDispatcher(runtimeEventDispatcher) { } @@ -42,12 +47,17 @@ void RuntimeSnapshotProvider::AdvanceFrame() RuntimeRenderStateSnapshot RuntimeSnapshotProvider::PublishRenderStateSnapshot(unsigned outputWidth, unsigned outputHeight) const { + PublishRenderSnapshotPublishRequested(outputWidth, outputHeight, "publish-render-state-snapshot"); + for (;;) { const RuntimeSnapshotVersions versionsBefore = GetVersions(); RuntimeRenderStateSnapshot publishedSnapshot; if (TryGetPublishedRenderStateSnapshot(outputWidth, outputHeight, versionsBefore, publishedSnapshot)) + { + PublishRenderSnapshotPublished(publishedSnapshot); return publishedSnapshot; + } RuntimeRenderStateSnapshot snapshot; snapshot.outputWidth = outputWidth; @@ -60,6 +70,7 @@ RuntimeRenderStateSnapshot RuntimeSnapshotProvider::PublishRenderStateSnapshot(u { snapshot.versions = versionsAfter; StorePublishedRenderStateSnapshot(snapshot); + PublishRenderSnapshotPublished(snapshot); return snapshot; } } @@ -67,9 +78,14 @@ RuntimeRenderStateSnapshot RuntimeSnapshotProvider::PublishRenderStateSnapshot(u bool RuntimeSnapshotProvider::TryPublishRenderStateSnapshot(unsigned outputWidth, unsigned outputHeight, RuntimeRenderStateSnapshot& snapshot) const { + PublishRenderSnapshotPublishRequested(outputWidth, outputHeight, "try-publish-render-state-snapshot"); + const RuntimeSnapshotVersions versionsBefore = GetVersions(); if (TryGetPublishedRenderStateSnapshot(outputWidth, outputHeight, versionsBefore, snapshot)) + { + PublishRenderSnapshotPublished(snapshot); return true; + } std::vector states; if (!mRenderSnapshotBuilder.TryBuildLayerRenderStates(outputWidth, outputHeight, states)) @@ -87,6 +103,7 @@ bool RuntimeSnapshotProvider::TryPublishRenderStateSnapshot(unsigned outputWidth snapshot.versions = versionsAfter; snapshot.states = std::move(states); StorePublishedRenderStateSnapshot(snapshot); + PublishRenderSnapshotPublished(snapshot); return true; } @@ -102,6 +119,7 @@ bool RuntimeSnapshotProvider::TryRefreshPublishedSnapshotParameters(RuntimeRende snapshot.versions = versions; StorePublishedRenderStateSnapshot(snapshot); + PublishRenderSnapshotPublished(snapshot); return true; } @@ -139,3 +157,40 @@ bool RuntimeSnapshotProvider::SnapshotMatches(const RuntimeRenderStateSnapshot& snapshot.versions.renderStateVersion == versions.renderStateVersion && snapshot.versions.parameterStateVersion == versions.parameterStateVersion; } + +void RuntimeSnapshotProvider::PublishRenderSnapshotPublishRequested(unsigned outputWidth, unsigned outputHeight, const std::string& reason) const +{ + try + { + RenderSnapshotPublishRequestedEvent event; + event.outputWidth = outputWidth; + event.outputHeight = outputHeight; + event.reason = reason; + if (!mRuntimeEventDispatcher.PublishPayload(event, "RuntimeSnapshotProvider")) + OutputDebugStringA("RenderSnapshotPublishRequested event publish failed.\n"); + } + catch (...) + { + OutputDebugStringA("RenderSnapshotPublishRequested event publish threw.\n"); + } +} + +void RuntimeSnapshotProvider::PublishRenderSnapshotPublished(const RuntimeRenderStateSnapshot& snapshot) const +{ + try + { + RenderSnapshotPublishedEvent event; + event.snapshotVersion = snapshot.versions.renderStateVersion; + event.structureVersion = snapshot.versions.renderStateVersion; + event.parameterVersion = snapshot.versions.parameterStateVersion; + event.outputWidth = snapshot.outputWidth; + event.outputHeight = snapshot.outputHeight; + event.layerCount = snapshot.states.size(); + if (!mRuntimeEventDispatcher.PublishPayload(event, "RuntimeSnapshotProvider")) + OutputDebugStringA("RenderSnapshotPublished event publish failed.\n"); + } + catch (...) + { + OutputDebugStringA("RenderSnapshotPublished event publish threw.\n"); + } +} diff --git a/apps/LoopThroughWithOpenGLCompositing/runtime/snapshot/RuntimeSnapshotProvider.h b/apps/LoopThroughWithOpenGLCompositing/runtime/snapshot/RuntimeSnapshotProvider.h index f014720..cac112b 100644 --- a/apps/LoopThroughWithOpenGLCompositing/runtime/snapshot/RuntimeSnapshotProvider.h +++ b/apps/LoopThroughWithOpenGLCompositing/runtime/snapshot/RuntimeSnapshotProvider.h @@ -6,6 +6,8 @@ #include #include +class RuntimeEventDispatcher; + struct RuntimeRenderStateSnapshot { RuntimeSnapshotVersions versions; @@ -17,7 +19,7 @@ struct RuntimeRenderStateSnapshot class RuntimeSnapshotProvider { public: - explicit RuntimeSnapshotProvider(RenderSnapshotBuilder& renderSnapshotBuilder); + RuntimeSnapshotProvider(RenderSnapshotBuilder& renderSnapshotBuilder, RuntimeEventDispatcher& runtimeEventDispatcher); bool BuildLayerPassFragmentShaderSources(const std::string& layerId, std::vector& passSources, std::string& error) const; unsigned GetMaxTemporalHistoryFrames() const; @@ -34,8 +36,11 @@ private: void StorePublishedRenderStateSnapshot(const RuntimeRenderStateSnapshot& snapshot) const; static bool SnapshotMatches(const RuntimeRenderStateSnapshot& snapshot, unsigned outputWidth, unsigned outputHeight, const RuntimeSnapshotVersions& versions); + void PublishRenderSnapshotPublishRequested(unsigned outputWidth, unsigned outputHeight, const std::string& reason) const; + void PublishRenderSnapshotPublished(const RuntimeRenderStateSnapshot& snapshot) const; RenderSnapshotBuilder& mRenderSnapshotBuilder; + RuntimeEventDispatcher& mRuntimeEventDispatcher; mutable std::mutex mPublishedSnapshotMutex; mutable bool mHasPublishedRenderStateSnapshot = false; mutable RuntimeRenderStateSnapshot mPublishedRenderStateSnapshot; diff --git a/apps/LoopThroughWithOpenGLCompositing/videoio/VideoBackend.cpp b/apps/LoopThroughWithOpenGLCompositing/videoio/VideoBackend.cpp index ae5722e..7a75ab5 100644 --- a/apps/LoopThroughWithOpenGLCompositing/videoio/VideoBackend.cpp +++ b/apps/LoopThroughWithOpenGLCompositing/videoio/VideoBackend.cpp @@ -4,11 +4,14 @@ #include "OpenGLVideoIOBridge.h" #include "HealthTelemetry.h" #include "RenderEngine.h" +#include "RuntimeEventDispatcher.h" #include +#include -VideoBackend::VideoBackend(RenderEngine& renderEngine, HealthTelemetry& healthTelemetry) : +VideoBackend::VideoBackend(RenderEngine& renderEngine, HealthTelemetry& healthTelemetry, RuntimeEventDispatcher& runtimeEventDispatcher) : mHealthTelemetry(healthTelemetry), + mRuntimeEventDispatcher(runtimeEventDispatcher), mVideoIODevice(std::make_unique()), mBridge(std::make_unique(renderEngine)) { @@ -54,12 +57,16 @@ bool VideoBackend::ConfigureOutput(const VideoFormat& outputVideoMode, bool exte bool VideoBackend::Start() { - return mVideoIODevice->Start(); + const bool started = mVideoIODevice->Start(); + PublishBackendStateChanged(started ? "started" : "start-failed", started ? "Video backend started." : StatusMessage()); + return started; } bool VideoBackend::Stop() { - return mVideoIODevice->Stop(); + const bool stopped = mVideoIODevice->Stop(); + PublishBackendStateChanged(stopped ? "stopped" : "stop-failed", stopped ? "Video backend stopped." : StatusMessage()); + return stopped; } const VideoIOState& VideoBackend::State() const @@ -191,6 +198,7 @@ void VideoBackend::PublishStatus(bool externalKeyingConfigured, const std::strin externalKeyingConfigured, ExternalKeyingActive(), StatusMessage()); + PublishBackendStateChanged("status", StatusMessage()); } void VideoBackend::ReportNoInputDeviceSignalStatus() @@ -200,12 +208,15 @@ void VideoBackend::ReportNoInputDeviceSignalStatus() InputFrameWidth(), InputFrameHeight(), InputDisplayModeName()); + PublishBackendStateChanged("no-input-device", "No input device is available."); } void VideoBackend::HandleInputFrame(const VideoIOFrame& frame) { const VideoIOState& state = mVideoIODevice->State(); mHealthTelemetry.TryReportSignalStatus(!frame.hasNoInputSource, state.inputFrameSize.width, state.inputFrameSize.height, state.inputDisplayModeName); + PublishInputSignalChanged(frame, state); + PublishInputFrameArrived(frame); if (mBridge) mBridge->UploadInputFrame(frame, state); @@ -214,6 +225,7 @@ void VideoBackend::HandleInputFrame(const VideoIOFrame& frame) void VideoBackend::HandleOutputFrameCompletion(const VideoIOCompletion& completion) { RecordFramePacing(completion.result); + PublishOutputFrameCompleted(completion); VideoIOOutputFrame outputFrame; if (!BeginOutputFrame(outputFrame)) @@ -228,7 +240,8 @@ void VideoBackend::HandleOutputFrameCompletion(const VideoIOCompletion& completi // Schedule the next frame after render work is complete so device-side // bookkeeping stays with the backend seam and the bridge stays render-only. - ScheduleOutputFrame(outputFrame); + if (ScheduleOutputFrame(outputFrame)) + PublishOutputFrameScheduled(outputFrame); } void VideoBackend::RecordFramePacing(VideoIOCompletionResult completionResult) @@ -260,4 +273,152 @@ void VideoBackend::RecordFramePacing(VideoIOCompletionResult completionResult) mLateFrameCount, mDroppedFrameCount, mFlushedFrameCount); + PublishTimingSample("VideoBackend", "completionInterval", mCompletionIntervalMilliseconds, "ms"); + PublishTimingSample("VideoBackend", "smoothedCompletionInterval", mSmoothedCompletionIntervalMilliseconds, "ms"); +} + +void VideoBackend::PublishBackendStateChanged(const std::string& state, const std::string& message) +{ + try + { + BackendStateChangedEvent event; + event.backendName = "decklink"; + event.state = state; + event.message = message; + if (!mRuntimeEventDispatcher.PublishPayload(event, "VideoBackend")) + OutputDebugStringA("BackendStateChanged event publish failed.\n"); + } + catch (...) + { + OutputDebugStringA("BackendStateChanged event publish threw.\n"); + } +} + +void VideoBackend::PublishInputSignalChanged(const VideoIOFrame& frame, const VideoIOState& state) +{ + const bool hasSignal = !frame.hasNoInputSource; + const unsigned width = state.inputFrameSize.width; + const unsigned height = state.inputFrameSize.height; + if (mHasLastInputSignal && + mLastInputSignal == hasSignal && + mLastInputSignalWidth == width && + mLastInputSignalHeight == height && + mLastInputSignalModeName == state.inputDisplayModeName) + { + return; + } + + mHasLastInputSignal = true; + mLastInputSignal = hasSignal; + mLastInputSignalWidth = width; + mLastInputSignalHeight = height; + mLastInputSignalModeName = state.inputDisplayModeName; + + try + { + InputSignalChangedEvent event; + event.hasSignal = hasSignal; + event.width = width; + event.height = height; + event.modeName = state.inputDisplayModeName; + if (!mRuntimeEventDispatcher.PublishPayload(event, "VideoBackend")) + OutputDebugStringA("InputSignalChanged event publish failed.\n"); + } + catch (...) + { + OutputDebugStringA("InputSignalChanged event publish threw.\n"); + } +} + +void VideoBackend::PublishInputFrameArrived(const VideoIOFrame& frame) +{ + try + { + InputFrameArrivedEvent event; + event.frameIndex = ++mInputFrameIndex; + event.width = frame.width; + event.height = frame.height; + event.rowBytes = frame.rowBytes; + event.pixelFormat = PixelFormatName(frame.pixelFormat); + event.hasNoInputSource = frame.hasNoInputSource; + if (!mRuntimeEventDispatcher.PublishPayload(event, "VideoBackend")) + OutputDebugStringA("InputFrameArrived event publish failed.\n"); + } + catch (...) + { + OutputDebugStringA("InputFrameArrived event publish threw.\n"); + } +} + +void VideoBackend::PublishOutputFrameScheduled(const VideoIOOutputFrame& frame) +{ + try + { + OutputFrameScheduledEvent event; + event.frameIndex = ++mOutputFrameScheduleIndex; + (void)frame; + if (!mRuntimeEventDispatcher.PublishPayload(event, "VideoBackend")) + OutputDebugStringA("OutputFrameScheduled event publish failed.\n"); + } + catch (...) + { + OutputDebugStringA("OutputFrameScheduled event publish threw.\n"); + } +} + +void VideoBackend::PublishOutputFrameCompleted(const VideoIOCompletion& completion) +{ + try + { + OutputFrameCompletedEvent event; + event.frameIndex = ++mOutputFrameCompletionIndex; + event.result = CompletionResultName(completion.result); + if (!mRuntimeEventDispatcher.PublishPayload(event, "VideoBackend")) + OutputDebugStringA("OutputFrameCompleted event publish failed.\n"); + } + catch (...) + { + OutputDebugStringA("OutputFrameCompleted event publish threw.\n"); + } +} + +void VideoBackend::PublishTimingSample(const std::string& subsystem, const std::string& metric, double value, const std::string& unit) +{ + try + { + TimingSampleRecordedEvent event; + event.subsystem = subsystem; + event.metric = metric; + event.value = value; + event.unit = unit; + if (!mRuntimeEventDispatcher.PublishPayload(event, "HealthTelemetry")) + OutputDebugStringA("TimingSampleRecorded event publish failed.\n"); + } + catch (...) + { + OutputDebugStringA("TimingSampleRecorded event publish threw.\n"); + } +} + +std::string VideoBackend::CompletionResultName(VideoIOCompletionResult result) +{ + switch (result) + { + case VideoIOCompletionResult::Completed: + return "Completed"; + case VideoIOCompletionResult::DisplayedLate: + return "DisplayedLate"; + case VideoIOCompletionResult::Dropped: + return "Dropped"; + case VideoIOCompletionResult::Flushed: + return "Flushed"; + case VideoIOCompletionResult::Unknown: + default: + return "Unknown"; + } +} + +std::string VideoBackend::PixelFormatName(VideoIOPixelFormat pixelFormat) +{ + return std::string(VideoIOPixelFormatName(pixelFormat)); } diff --git a/apps/LoopThroughWithOpenGLCompositing/videoio/VideoBackend.h b/apps/LoopThroughWithOpenGLCompositing/videoio/VideoBackend.h index 5dd0e59..bc8a818 100644 --- a/apps/LoopThroughWithOpenGLCompositing/videoio/VideoBackend.h +++ b/apps/LoopThroughWithOpenGLCompositing/videoio/VideoBackend.h @@ -10,12 +10,13 @@ class HealthTelemetry; class OpenGLVideoIOBridge; class RenderEngine; +class RuntimeEventDispatcher; class VideoIODevice; class VideoBackend { public: - VideoBackend(RenderEngine& renderEngine, HealthTelemetry& healthTelemetry); + VideoBackend(RenderEngine& renderEngine, HealthTelemetry& healthTelemetry, RuntimeEventDispatcher& runtimeEventDispatcher); ~VideoBackend(); void ReleaseResources(); @@ -57,10 +58,27 @@ private: void HandleInputFrame(const VideoIOFrame& frame); void HandleOutputFrameCompletion(const VideoIOCompletion& completion); void RecordFramePacing(VideoIOCompletionResult completionResult); + void PublishBackendStateChanged(const std::string& state, const std::string& message); + void PublishInputSignalChanged(const VideoIOFrame& frame, const VideoIOState& state); + void PublishInputFrameArrived(const VideoIOFrame& frame); + void PublishOutputFrameScheduled(const VideoIOOutputFrame& frame); + void PublishOutputFrameCompleted(const VideoIOCompletion& completion); + void PublishTimingSample(const std::string& subsystem, const std::string& metric, double value, const std::string& unit); + static std::string CompletionResultName(VideoIOCompletionResult result); + static std::string PixelFormatName(VideoIOPixelFormat pixelFormat); HealthTelemetry& mHealthTelemetry; + RuntimeEventDispatcher& mRuntimeEventDispatcher; std::unique_ptr mVideoIODevice; std::unique_ptr mBridge; + uint64_t mInputFrameIndex = 0; + uint64_t mOutputFrameScheduleIndex = 0; + uint64_t mOutputFrameCompletionIndex = 0; + bool mHasLastInputSignal = false; + bool mLastInputSignal = false; + unsigned mLastInputSignalWidth = 0; + unsigned mLastInputSignalHeight = 0; + std::string mLastInputSignalModeName; std::chrono::steady_clock::time_point mLastPlayoutCompletionTime; double mCompletionIntervalMilliseconds = 0.0; double mSmoothedCompletionIntervalMilliseconds = 0.0; diff --git a/tests/RuntimeEventTypeTests.cpp b/tests/RuntimeEventTypeTests.cpp index 16daf61..df4b76a 100644 --- a/tests/RuntimeEventTypeTests.cpp +++ b/tests/RuntimeEventTypeTests.cpp @@ -50,8 +50,10 @@ void TestRuntimeEventPayloadTypes() 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; @@ -59,6 +61,12 @@ void TestRuntimeEventPayloadTypes() 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.reason = "UpdateLayerParameter"; + persistence.debounceAllowed = true; + Expect(RuntimeEventPayloadType(persistence) == RuntimeEventType::RuntimePersistenceRequested, "runtime persistence payload maps to persistence event type"); + Expect(persistence.debounceAllowed, "runtime persistence payload carries debounce policy"); + ShaderBuildEvent preparedBuild; preparedBuild.phase = RuntimeEventShaderBuildPhase::Prepared; preparedBuild.inputWidth = 1920; @@ -72,6 +80,40 @@ void TestRuntimeEventPayloadTypes() 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"; @@ -310,6 +352,132 @@ void TestRuntimeEventTestHarness() const auto* seenPayload = seenOsc ? std::get_if(&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.reason = mutation.action; + persistence.debounceAllowed = true; + + 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(&persistenceEvent->payload) : nullptr; + Expect(persistencePayload && persistencePayload->reason == "SetLayerShader", "persistence follow-up preserves mutation action reason"); +} + +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(&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(&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(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() @@ -322,6 +490,10 @@ int main() TestRuntimeEventCoalescingQueue(); TestRuntimeEventCoalescingCustomKey(); TestRuntimeEventTestHarness(); + TestAcceptedMutationFollowUps(); + TestRejectedMutationHasNoDownstreamFollowUps(); + TestShaderBuildGenerationEventMatching(); + TestHandlerFailureCanBecomeTelemetryEvent(); if (gFailures != 0) {