diff --git a/apps/LoopThroughWithOpenGLCompositing/control/ControlServices.cpp b/apps/LoopThroughWithOpenGLCompositing/control/ControlServices.cpp index 1104e4f..c55d5ec 100644 --- a/apps/LoopThroughWithOpenGLCompositing/control/ControlServices.cpp +++ b/apps/LoopThroughWithOpenGLCompositing/control/ControlServices.cpp @@ -155,6 +155,40 @@ void ControlServices::ClearOscState() } } +void ControlServices::ClearOscStateForLayerKey(const std::string& layerKey) +{ + { + std::lock_guard lock(mPendingOscMutex); + for (auto it = mPendingOscUpdates.begin(); it != mPendingOscUpdates.end();) + { + if (it->second.layerKey == layerKey) + it = mPendingOscUpdates.erase(it); + else + ++it; + } + } + { + std::lock_guard lock(mPendingOscCommitMutex); + for (auto it = mPendingOscCommits.begin(); it != mPendingOscCommits.end();) + { + if (it->second.layerKey == layerKey) + it = mPendingOscCommits.erase(it); + else + ++it; + } + } + { + std::lock_guard lock(mCompletedOscCommitMutex); + for (auto it = mCompletedOscCommits.begin(); it != mCompletedOscCommits.end();) + { + if (it->routeKey.rfind(layerKey + "\n", 0) == 0) + it = mCompletedOscCommits.erase(it); + else + ++it; + } + } +} + void ControlServices::ConsumeCompletedOscCommits(std::vector& completedCommits) { completedCommits.clear(); diff --git a/apps/LoopThroughWithOpenGLCompositing/control/ControlServices.h b/apps/LoopThroughWithOpenGLCompositing/control/ControlServices.h index d1704d5..1620d15 100644 --- a/apps/LoopThroughWithOpenGLCompositing/control/ControlServices.h +++ b/apps/LoopThroughWithOpenGLCompositing/control/ControlServices.h @@ -49,6 +49,7 @@ public: bool ApplyPendingOscUpdates(std::vector& appliedUpdates, std::string& error); bool QueueOscCommit(const std::string& routeKey, const std::string& layerKey, const std::string& parameterKey, const JsonValue& value, uint64_t generation, std::string& error); void ClearOscState(); + void ClearOscStateForLayerKey(const std::string& layerKey); void ConsumeCompletedOscCommits(std::vector& completedCommits); private: diff --git a/apps/LoopThroughWithOpenGLCompositing/control/RuntimeServices.cpp b/apps/LoopThroughWithOpenGLCompositing/control/RuntimeServices.cpp index 535d074..1b2bf8c 100644 --- a/apps/LoopThroughWithOpenGLCompositing/control/RuntimeServices.cpp +++ b/apps/LoopThroughWithOpenGLCompositing/control/RuntimeServices.cpp @@ -68,6 +68,12 @@ void RuntimeServices::ClearOscState() mControlServices->ClearOscState(); } +void RuntimeServices::ClearOscStateForLayerKey(const std::string& layerKey) +{ + if (mControlServices) + mControlServices->ClearOscStateForLayerKey(layerKey); +} + void RuntimeServices::ConsumeCompletedOscCommits(std::vector& completedCommits) { if (!mControlServices) diff --git a/apps/LoopThroughWithOpenGLCompositing/control/RuntimeServices.h b/apps/LoopThroughWithOpenGLCompositing/control/RuntimeServices.h index 8480143..80d79f7 100644 --- a/apps/LoopThroughWithOpenGLCompositing/control/RuntimeServices.h +++ b/apps/LoopThroughWithOpenGLCompositing/control/RuntimeServices.h @@ -27,6 +27,7 @@ public: bool ApplyPendingOscUpdates(std::vector& appliedUpdates, std::string& error); bool QueueOscCommit(const std::string& routeKey, const std::string& layerKey, const std::string& parameterKey, const JsonValue& value, uint64_t generation, std::string& error); void ClearOscState(); + void ClearOscStateForLayerKey(const std::string& layerKey); void ConsumeCompletedOscCommits(std::vector& completedCommits); private: diff --git a/apps/LoopThroughWithOpenGLCompositing/gl/RenderEngine.cpp b/apps/LoopThroughWithOpenGLCompositing/gl/RenderEngine.cpp index 9e09e5b..4372e07 100644 --- a/apps/LoopThroughWithOpenGLCompositing/gl/RenderEngine.cpp +++ b/apps/LoopThroughWithOpenGLCompositing/gl/RenderEngine.cpp @@ -415,7 +415,16 @@ void RenderEngine::ProcessScreenshotCaptureCommandsOnRenderThread() void RenderEngine::ClearOscOverlayState() { - mRuntimeLiveState.Clear(); + InvokeOnRenderThread([this]() { + mRuntimeLiveState.Clear(); + }); +} + +void RenderEngine::ClearOscOverlayStateForLayerKey(const std::string& layerKey) +{ + InvokeOnRenderThread([this, layerKey]() { + mRuntimeLiveState.ClearForLayerKey(layerKey); + }); } void RenderEngine::UpdateOscOverlayState( diff --git a/apps/LoopThroughWithOpenGLCompositing/gl/RenderEngine.h b/apps/LoopThroughWithOpenGLCompositing/gl/RenderEngine.h index 033ae9e..4a32840 100644 --- a/apps/LoopThroughWithOpenGLCompositing/gl/RenderEngine.h +++ b/apps/LoopThroughWithOpenGLCompositing/gl/RenderEngine.h @@ -95,6 +95,7 @@ public: void ResetShaderFeedbackState(); void ApplyRuntimeCoordinatorRenderReset(RuntimeCoordinatorRenderResetScope resetScope); void ClearOscOverlayState(); + void ClearOscOverlayStateForLayerKey(const std::string& layerKey); void UpdateOscOverlayState( const std::vector& updates, const std::vector& completedCommits); diff --git a/apps/LoopThroughWithOpenGLCompositing/gl/RuntimeUpdateController.cpp b/apps/LoopThroughWithOpenGLCompositing/gl/RuntimeUpdateController.cpp index f19a933..6ab870e 100644 --- a/apps/LoopThroughWithOpenGLCompositing/gl/RuntimeUpdateController.cpp +++ b/apps/LoopThroughWithOpenGLCompositing/gl/RuntimeUpdateController.cpp @@ -85,10 +85,19 @@ bool RuntimeUpdateController::ApplyRuntimeCoordinatorResult(const RuntimeCoordin mRuntimeCoordinator.ApplyCommittedStateMode(result.committedStateMode); - if (result.clearTransientOscState) + switch (result.transientOscInvalidation) { + case RuntimeCoordinatorTransientOscInvalidation::All: mRenderEngine.ClearOscOverlayState(); mRuntimeServices.ClearOscState(); + break; + case RuntimeCoordinatorTransientOscInvalidation::Layer: + mRenderEngine.ClearOscOverlayStateForLayerKey(result.transientOscLayerKey); + mRuntimeServices.ClearOscStateForLayerKey(result.transientOscLayerKey); + break; + case RuntimeCoordinatorTransientOscInvalidation::None: + default: + break; } mRenderEngine.ApplyRuntimeCoordinatorRenderReset(result.renderResetScope); diff --git a/apps/LoopThroughWithOpenGLCompositing/runtime/coordination/RuntimeCoordinator.cpp b/apps/LoopThroughWithOpenGLCompositing/runtime/coordination/RuntimeCoordinator.cpp index 04cc2bc..a5c73c2 100644 --- a/apps/LoopThroughWithOpenGLCompositing/runtime/coordination/RuntimeCoordinator.cpp +++ b/apps/LoopThroughWithOpenGLCompositing/runtime/coordination/RuntimeCoordinator.cpp @@ -56,6 +56,11 @@ RuntimeCoordinatorResult RuntimeCoordinator::RemoveLayer(const std::string& laye } RuntimeCoordinatorResult result = ApplyStoreMutation(mRuntimeStore.DeleteStoredLayer(layerId, error), error, true, true, true); + if (result.accepted) + { + result.transientOscInvalidation = RuntimeCoordinatorTransientOscInvalidation::Layer; + result.transientOscLayerKey = layerId; + } PublishCoordinatorResult("RemoveLayer", result); return result; } @@ -205,7 +210,8 @@ RuntimeCoordinatorResult RuntimeCoordinator::ResetLayerParameters(const std::str return result; } - result.clearTransientOscState = true; + result.transientOscInvalidation = RuntimeCoordinatorTransientOscInvalidation::Layer; + result.transientOscLayerKey = layerId; result.renderResetScope = RuntimeCoordinatorRenderResetScope::TemporalHistoryAndFeedback; PublishCoordinatorResult("ResetLayerParameters", result); return result; @@ -538,7 +544,7 @@ void RuntimeCoordinator::PublishCoordinatorResult(const std::string& action, con mutation.runtimeStateBroadcastRequired = result.runtimeStateBroadcastRequired; mutation.shaderBuildRequested = result.shaderBuildRequested; mutation.persistenceRequested = result.persistenceRequested; - mutation.clearTransientOscState = result.clearTransientOscState; + mutation.clearTransientOscState = result.transientOscInvalidation != RuntimeCoordinatorTransientOscInvalidation::None; mutation.renderResetScope = ToRuntimeEventRenderResetScope(result.renderResetScope); mutation.errorMessage = result.errorMessage; mRuntimeEventDispatcher.PublishPayload(mutation, "RuntimeCoordinator"); diff --git a/apps/LoopThroughWithOpenGLCompositing/runtime/coordination/RuntimeCoordinator.h b/apps/LoopThroughWithOpenGLCompositing/runtime/coordination/RuntimeCoordinator.h index ad2bd4e..0ad8709 100644 --- a/apps/LoopThroughWithOpenGLCompositing/runtime/coordination/RuntimeCoordinator.h +++ b/apps/LoopThroughWithOpenGLCompositing/runtime/coordination/RuntimeCoordinator.h @@ -25,18 +25,26 @@ enum class RuntimeCoordinatorRenderResetScope TemporalHistoryAndFeedback }; +enum class RuntimeCoordinatorTransientOscInvalidation +{ + None, + Layer, + All +}; + struct RuntimeCoordinatorResult { bool accepted = false; bool runtimeStateBroadcastRequired = false; bool shaderBuildRequested = false; bool persistenceRequested = false; - bool clearTransientOscState = false; bool compileStatusChanged = false; bool compileStatusSucceeded = false; bool clearReloadRequest = false; RuntimeCoordinatorCommittedStateMode committedStateMode = RuntimeCoordinatorCommittedStateMode::Unchanged; RuntimeCoordinatorRenderResetScope renderResetScope = RuntimeCoordinatorRenderResetScope::None; + RuntimeCoordinatorTransientOscInvalidation transientOscInvalidation = RuntimeCoordinatorTransientOscInvalidation::None; + std::string transientOscLayerKey; std::string compileStatusMessage; std::string errorMessage; }; diff --git a/apps/LoopThroughWithOpenGLCompositing/runtime/live/RuntimeLiveState.cpp b/apps/LoopThroughWithOpenGLCompositing/runtime/live/RuntimeLiveState.cpp index f2124fb..6ee68ef 100644 --- a/apps/LoopThroughWithOpenGLCompositing/runtime/live/RuntimeLiveState.cpp +++ b/apps/LoopThroughWithOpenGLCompositing/runtime/live/RuntimeLiveState.cpp @@ -74,6 +74,7 @@ JsonValue BuildOscCommitValue(const ShaderParameterDefinition& definition, const return JsonValue(); } + } void RuntimeLiveState::Clear() @@ -81,6 +82,47 @@ void RuntimeLiveState::Clear() mOscOverlayStates.clear(); } +void RuntimeLiveState::ClearForLayerKey(const std::string& layerKey) +{ + for (auto it = mOscOverlayStates.begin(); it != mOscOverlayStates.end();) + { + if (OverlayMatchesLayerKey(it->second, layerKey)) + it = mOscOverlayStates.erase(it); + else + ++it; + } +} + +bool RuntimeLiveState::OverlayMatchesLayerKey(const OscOverlayState& overlay, const std::string& layerKey) +{ + return MatchesOscControlKey(overlay.layerKey, layerKey); +} + +bool RuntimeLiveState::TryResolveOverlayTarget( + const OscOverlayState& overlay, + const std::vector& states, + std::vector::const_iterator& stateIt, + std::vector::const_iterator& definitionIt) +{ + stateIt = std::find_if(states.begin(), states.end(), + [&overlay](const RuntimeRenderState& state) + { + return MatchesOscControlKey(state.layerId, overlay.layerKey) || + MatchesOscControlKey(state.shaderId, overlay.layerKey) || + MatchesOscControlKey(state.shaderName, overlay.layerKey); + }); + if (stateIt == states.end()) + return false; + + definitionIt = std::find_if(stateIt->parameterDefinitions.begin(), stateIt->parameterDefinitions.end(), + [&overlay](const ShaderParameterDefinition& definition) + { + return MatchesOscControlKey(definition.id, overlay.parameterKey) || + MatchesOscControlKey(definition.label, overlay.parameterKey); + }); + return definitionIt != stateIt->parameterDefinitions.end(); +} + std::size_t RuntimeLiveState::OverlayCount() const { return mOscOverlayStates.size(); @@ -131,6 +173,27 @@ void RuntimeLiveState::ApplyOscCommitCompletions(const std::vector& states) +{ + for (auto it = mOscOverlayStates.begin(); it != mOscOverlayStates.end();) + { + std::vector::const_iterator stateIt; + std::vector::const_iterator definitionIt; + if (TryResolveOverlayTarget(it->second, states, stateIt, definitionIt)) + { + ShaderParameterValue targetValue; + std::string normalizeError; + if (NormalizeAndValidateParameterValue(*definitionIt, it->second.targetValue, targetValue, normalizeError)) + { + ++it; + continue; + } + } + + it = mOscOverlayStates.erase(it); + } +} + void RuntimeLiveState::ApplyToLayerStates( std::vector& states, const RuntimeLiveStateApplyOptions& options, @@ -139,6 +202,10 @@ void RuntimeLiveState::ApplyToLayerStates( if (states.empty() || mOscOverlayStates.empty()) return; + PruneIncompatibleOverlays(states); + if (mOscOverlayStates.empty()) + return; + const auto now = options.now; const double clampedSmoothing = ClampOscAlpha(options.smoothing); std::vector overlayKeysToRemove; diff --git a/apps/LoopThroughWithOpenGLCompositing/runtime/live/RuntimeLiveState.h b/apps/LoopThroughWithOpenGLCompositing/runtime/live/RuntimeLiveState.h index 557019f..65f8edf 100644 --- a/apps/LoopThroughWithOpenGLCompositing/runtime/live/RuntimeLiveState.h +++ b/apps/LoopThroughWithOpenGLCompositing/runtime/live/RuntimeLiveState.h @@ -44,9 +44,11 @@ class RuntimeLiveState { public: void Clear(); + void ClearForLayerKey(const std::string& layerKey); std::size_t OverlayCount() const; void ApplyOscUpdates(const std::vector& updates); void ApplyOscCommitCompletions(const std::vector& completedCommits); + void PruneIncompatibleOverlays(const std::vector& states); void ApplyToLayerStates( std::vector& states, const RuntimeLiveStateApplyOptions& options, @@ -67,5 +69,12 @@ private: bool commitQueued = false; }; + static bool OverlayMatchesLayerKey(const OscOverlayState& overlay, const std::string& layerKey); + static bool TryResolveOverlayTarget( + const OscOverlayState& overlay, + const std::vector& states, + std::vector::const_iterator& stateIt, + std::vector::const_iterator& definitionIt); + std::map mOscOverlayStates; }; diff --git a/docs/PHASE_5_LIVE_STATE_LAYERING_DESIGN.md b/docs/PHASE_5_LIVE_STATE_LAYERING_DESIGN.md index 31eeb11..862380b 100644 --- a/docs/PHASE_5_LIVE_STATE_LAYERING_DESIGN.md +++ b/docs/PHASE_5_LIVE_STATE_LAYERING_DESIGN.md @@ -7,8 +7,8 @@ Phase 1 named the subsystems. Phase 2 added the typed event substrate. Phase 3 m ## Status - Phase 5 design package: proposed. -- Phase 5 implementation: Step 2 started. -- Current alignment: Phase 3 introduced the first pure composition boundary and transient OSC overlay owner. Phase 5 now has a small `RuntimeStateLayerModel` inventory that names the current state categories, and `RenderStateComposer` consumes a `LayeredRenderStateInput` whose fields make base persisted, committed live, and transient automation inputs explicit. Committed runtime values are still physically stored through `RuntimeStore`/`LayerStackStore`, and transient OSC overlay state is still applied through `RuntimeLiveState`. +- Phase 5 implementation: Step 3 complete. +- Current alignment: Phase 3 introduced the first pure composition boundary and transient OSC overlay owner. Phase 5 now has a small `RuntimeStateLayerModel` inventory that names the current state categories, `RenderStateComposer` consumes a `LayeredRenderStateInput` whose fields make base persisted, committed live, and transient automation inputs explicit, and `RuntimeLiveState` owns transient-overlay invalidation against current layer/parameter compatibility. Committed runtime values are still physically stored through `RuntimeStore`/`LayerStackStore`. Current live-state footholds: @@ -19,6 +19,7 @@ Current live-state footholds: - `RenderStateComposer` consumes `LayeredRenderStateInput`, chooses committed-live layer states over base-persisted layer states when both are supplied, applies transient automation on top, and returns final per-frame layer states plus settled commit requests. - `RuntimeServiceLiveBridge` drains OSC ingress/completion queues and applies them to render live state during frame preparation. - `RuntimeStateLayerModel` names the Phase 5 state categories and classifies current fields as base persisted, committed live, transient automation, render-local, or health/config state. +- `RuntimeCoordinator` can request layer-scoped transient OSC invalidation, while `RuntimeLiveState` prunes overlays that no longer map to the current render-facing layer/parameter definitions. ## Why Phase 5 Exists @@ -265,14 +266,21 @@ Move reset/reload transient-state decisions into one policy point. Initial target: -- layer removal clears matching transient overlays -- shader change clears incompatible overlays -- preset load clears incompatible overlays -- shader reload can preserve compatible overlays when requested -- temporal/feedback resets stay render-local and separate from parameter overlays +- [x] layer removal clears matching transient overlays +- [x] shader change clears incompatible overlays +- [x] preset load clears incompatible overlays +- [x] shader reload can preserve compatible overlays when requested +- [x] temporal/feedback resets stay render-local and separate from parameter overlays This is where Phase 5 should prevent "clear everything" and "preserve everything" from being scattered through unrelated code. +Current implementation: + +- `RuntimeCoordinatorResult` carries a named `RuntimeCoordinatorTransientOscInvalidation` request rather than a raw clear-all flag. +- `RuntimeUpdateController` applies layer-scoped invalidation to both render-owned overlay state and queued OSC service state. +- `RuntimeLiveState::PruneIncompatibleOverlays(...)` is the central compatibility policy for current render-facing layer/parameter definitions. +- `RuntimeLiveState::ApplyToLayerStates(...)` prunes incompatible overlays before applying transient values, so shader changes, preset loads, and layer removals stop carrying stale overlays once the current frame state no longer maps them. + ### Step 4. Clarify OSC Commit Semantics Make the transient-to-committed path explicit. @@ -368,7 +376,7 @@ Phase 5 can be considered complete once the project can say: - [x] persisted, committed-live, and transient automation layers are named in code or clear read models - [x] final render-value precedence is explicit and covered by tests - [x] `RenderStateComposer` or its replacement consumes a layered input contract -- [ ] reset/reload/preset behavior for transient overlays is centralized or clearly delegated +- [x] reset/reload/preset behavior for transient overlays is centralized or clearly delegated - [ ] OSC overlay settle/commit behavior is explicit, including persistence policy - [ ] `RuntimeStore` remains durable-state focused and does not absorb transient automation policy - [ ] render-local temporal/feedback state remains separate from live parameter layering diff --git a/docs/subsystems/RenderEngine.md b/docs/subsystems/RenderEngine.md index 204d0b4..d6f14f4 100644 --- a/docs/subsystems/RenderEngine.md +++ b/docs/subsystems/RenderEngine.md @@ -111,6 +111,8 @@ This state should remain render-local even when it influences visible output. Phase 5's `RuntimeStateLayerModel` explicitly keeps temporal history, feedback state, accepted input frames, staged output frames, preview staging, and screenshot/readback staging in the render-local category. These are deliberately outside the persisted/committed/transient-automation parameter composition rule. +`RuntimeLiveState` now owns transient automation invalidation for render-facing compatibility. It can clear overlays for a target layer/control key and prunes overlays that no longer resolve to the current layer and parameter definitions before applying them to a frame. This keeps shader reload, preset load, and layer removal behavior local to the live-state/composition boundary instead of scattering it through GL drawing code. + ### 5. Shader Build Application Compilation itself may eventually move into a separate build service, but once shader build outputs exist, `RenderEngine` owns: diff --git a/docs/subsystems/RuntimeCoordinator.md b/docs/subsystems/RuntimeCoordinator.md index 8cc3294..9c36e28 100644 --- a/docs/subsystems/RuntimeCoordinator.md +++ b/docs/subsystems/RuntimeCoordinator.md @@ -446,6 +446,8 @@ The coordinator should define explicit reset scopes such as: That allows later phases to stop encoding reset behavior implicitly in UI handlers or render rebuild code. +Phase 5 has made this more concrete for OSC overlays: coordinator results now carry a named transient OSC invalidation request, with layer-scoped invalidation used for layer removal and manual parameter reset. The render/live-state owner still decides compatibility details, but callers no longer infer transient reset behavior from a generic boolean. + ## Migration Plan From Current Code The coordinator should be introduced incrementally. diff --git a/tests/RuntimeLiveStateTests.cpp b/tests/RuntimeLiveStateTests.cpp index 37b72f6..9a1f745 100644 --- a/tests/RuntimeLiveStateTests.cpp +++ b/tests/RuntimeLiveStateTests.cpp @@ -349,6 +349,100 @@ void TestRuntimeLiveStateTriggerOverlayIncrementsAndClears() Expect(liveState.OverlayCount() == 0, "trigger overlay clears after apply"); } +void TestRuntimeLiveStateClearsOverlaysForLayerKey() +{ + RuntimeLiveState liveState; + + RuntimeLiveOscUpdate first; + first.routeKey = "layer-one\namount"; + first.layerKey = "layer-one"; + first.parameterKey = "amount"; + first.targetValue = JsonValue(0.5); + + RuntimeLiveOscUpdate second = first; + second.routeKey = "layer-two\namount"; + second.layerKey = "layer-two"; + second.targetValue = JsonValue(0.75); + + liveState.ApplyOscUpdates({ first, second }); + liveState.ClearForLayerKey("layer-one"); + + Expect(liveState.OverlayCount() == 1, "layer-scoped invalidation only removes matching overlays"); + + std::vector states = { MakeLayerState() }; + states[0].layerId = "layer-two"; + RuntimeLiveStateApplyOptions options; + options.smoothing = 0.0; + liveState.ApplyToLayerStates(states, options, nullptr); + + const auto valueIt = states[0].parameterValues.find("amount"); + Expect(valueIt != states[0].parameterValues.end() && + !valueIt->second.numberValues.empty() && + std::fabs(valueIt->second.numberValues[0] - 0.75) < 0.0001, + "unmatched layer invalidation preserves unrelated overlay"); +} + +void TestRuntimeLiveStatePrunesRemovedLayerOverlay() +{ + RuntimeLiveState liveState; + + RuntimeLiveOscUpdate update; + update.routeKey = "removed-layer\namount"; + update.layerKey = "removed-layer"; + update.parameterKey = "amount"; + update.targetValue = JsonValue(0.5); + liveState.ApplyOscUpdates({ update }); + + std::vector states = { MakeLayerState() }; + liveState.PruneIncompatibleOverlays(states); + + Expect(liveState.OverlayCount() == 0, "invalidation policy removes overlays for missing layers"); +} + +void TestRuntimeLiveStatePrunesIncompatibleParameterOverlay() +{ + RuntimeLiveState liveState; + + RuntimeLiveOscUpdate update; + update.routeKey = "layer-one\namount"; + update.layerKey = "layer-one"; + update.parameterKey = "amount"; + update.targetValue = JsonValue(0.5); + liveState.ApplyOscUpdates({ update }); + + std::vector states = { MakeLayerStateWithDefinitions({ FloatDefinition("other", "Other") }) }; + liveState.PruneIncompatibleOverlays(states); + + Expect(liveState.OverlayCount() == 0, "invalidation policy removes overlays for missing parameters"); +} + +void TestRuntimeLiveStatePreservesCompatibleOverlayAfterShaderReload() +{ + RuntimeLiveState liveState; + + RuntimeLiveOscUpdate update; + update.routeKey = "layer-one\namount"; + update.layerKey = "layer-one"; + update.parameterKey = "Amount"; + update.targetValue = JsonValue(0.5); + liveState.ApplyOscUpdates({ update }); + + std::vector states = { MakeLayerStateWithDefinitions({ FloatDefinition("amount-renamed", "Amount") }) }; + liveState.PruneIncompatibleOverlays(states); + + Expect(liveState.OverlayCount() == 1, "invalidation policy preserves overlays that still map by control key"); + + RuntimeLiveStateApplyOptions options; + options.smoothing = 0.0; + liveState.ApplyToLayerStates(states, options, nullptr); + + const auto valueIt = states[0].parameterValues.find("amount-renamed"); + Expect(valueIt != states[0].parameterValues.end() && + !valueIt->second.numberValues.empty() && + std::fabs(valueIt->second.numberValues[0] - 0.5) < 0.0001, + "compatible overlay applies to the reloaded parameter id"); +} + void TestRenderStateComposerBuildsFrameState() { RuntimeLiveState liveState; @@ -496,6 +590,10 @@ int main() TestRuntimeLiveStateSmoothingPartiallyConverges(); TestRuntimeLiveStateSmoothingVectorSizeMismatchUsesTargetShape(); TestRuntimeLiveStateTriggerOverlayIncrementsAndClears(); + TestRuntimeLiveStateClearsOverlaysForLayerKey(); + TestRuntimeLiveStatePrunesRemovedLayerOverlay(); + TestRuntimeLiveStatePrunesIncompatibleParameterOverlay(); + TestRuntimeLiveStatePreservesCompatibleOverlayAfterShaderReload(); TestRenderStateComposerBuildsFrameState(); TestRenderStateComposerUsesCommittedLayerOverBaseLayer(); TestRenderStateComposerUsesBaseLayerWhenCommittedLayerMissing(); diff --git a/tests/RuntimeSubsystemTests.cpp b/tests/RuntimeSubsystemTests.cpp index 110b7a3..f2c3ac9 100644 --- a/tests/RuntimeSubsystemTests.cpp +++ b/tests/RuntimeSubsystemTests.cpp @@ -278,6 +278,14 @@ void TestRuntimeCoordinatorPersistenceEvents() expectAcceptedPersistence(coordinator.UpdateLayerParameter(alphaLayerId, "gain", JsonValue(0.75)), "UpdateLayerParameter", "parameter changes are accepted"); + RuntimeCoordinatorResult resetResult = coordinator.ResetLayerParameters(alphaLayerId); + std::vector resetEvents = dispatchAndClear(); + Expect(resetResult.accepted, "parameter reset is accepted"); + Expect(resetResult.transientOscInvalidation == RuntimeCoordinatorTransientOscInvalidation::Layer, + "parameter reset requests layer-scoped transient OSC invalidation"); + Expect(resetResult.transientOscLayerKey == alphaLayerId, "parameter reset invalidates the target layer overlays"); + Expect(countEvents(resetEvents, RuntimeEventType::RuntimeMutationAccepted) == 1, "parameter reset publishes accepted fact"); + expectAcceptedPersistence(coordinator.AddLayer("beta"), "AddLayer", "stack edits are accepted"); layers = store.CopyLayerStates(); Expect(layers.size() == 2, "stack edit creates a second layer");