step 3
All checks were successful
CI / React UI Build (push) Successful in 11s
CI / Native Windows Build And Tests (push) Successful in 2m40s
CI / Windows Release Package (push) Successful in 2m44s

This commit is contained in:
Aiden
2026-05-11 19:05:29 +10:00
parent 7740fe209c
commit 718e4dcadd
16 changed files with 282 additions and 13 deletions

View File

@@ -155,6 +155,40 @@ void ControlServices::ClearOscState()
} }
} }
void ControlServices::ClearOscStateForLayerKey(const std::string& layerKey)
{
{
std::lock_guard<std::mutex> lock(mPendingOscMutex);
for (auto it = mPendingOscUpdates.begin(); it != mPendingOscUpdates.end();)
{
if (it->second.layerKey == layerKey)
it = mPendingOscUpdates.erase(it);
else
++it;
}
}
{
std::lock_guard<std::mutex> lock(mPendingOscCommitMutex);
for (auto it = mPendingOscCommits.begin(); it != mPendingOscCommits.end();)
{
if (it->second.layerKey == layerKey)
it = mPendingOscCommits.erase(it);
else
++it;
}
}
{
std::lock_guard<std::mutex> 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<CompletedOscCommit>& completedCommits) void ControlServices::ConsumeCompletedOscCommits(std::vector<CompletedOscCommit>& completedCommits)
{ {
completedCommits.clear(); completedCommits.clear();

View File

@@ -49,6 +49,7 @@ public:
bool ApplyPendingOscUpdates(std::vector<AppliedOscUpdate>& appliedUpdates, std::string& error); bool ApplyPendingOscUpdates(std::vector<AppliedOscUpdate>& 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); 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 ClearOscState();
void ClearOscStateForLayerKey(const std::string& layerKey);
void ConsumeCompletedOscCommits(std::vector<CompletedOscCommit>& completedCommits); void ConsumeCompletedOscCommits(std::vector<CompletedOscCommit>& completedCommits);
private: private:

View File

@@ -68,6 +68,12 @@ void RuntimeServices::ClearOscState()
mControlServices->ClearOscState(); mControlServices->ClearOscState();
} }
void RuntimeServices::ClearOscStateForLayerKey(const std::string& layerKey)
{
if (mControlServices)
mControlServices->ClearOscStateForLayerKey(layerKey);
}
void RuntimeServices::ConsumeCompletedOscCommits(std::vector<CompletedOscCommit>& completedCommits) void RuntimeServices::ConsumeCompletedOscCommits(std::vector<CompletedOscCommit>& completedCommits)
{ {
if (!mControlServices) if (!mControlServices)

View File

@@ -27,6 +27,7 @@ public:
bool ApplyPendingOscUpdates(std::vector<AppliedOscUpdate>& appliedUpdates, std::string& error); bool ApplyPendingOscUpdates(std::vector<AppliedOscUpdate>& 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); 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 ClearOscState();
void ClearOscStateForLayerKey(const std::string& layerKey);
void ConsumeCompletedOscCommits(std::vector<CompletedOscCommit>& completedCommits); void ConsumeCompletedOscCommits(std::vector<CompletedOscCommit>& completedCommits);
private: private:

View File

@@ -415,7 +415,16 @@ void RenderEngine::ProcessScreenshotCaptureCommandsOnRenderThread()
void RenderEngine::ClearOscOverlayState() 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( void RenderEngine::UpdateOscOverlayState(

View File

@@ -95,6 +95,7 @@ public:
void ResetShaderFeedbackState(); void ResetShaderFeedbackState();
void ApplyRuntimeCoordinatorRenderReset(RuntimeCoordinatorRenderResetScope resetScope); void ApplyRuntimeCoordinatorRenderReset(RuntimeCoordinatorRenderResetScope resetScope);
void ClearOscOverlayState(); void ClearOscOverlayState();
void ClearOscOverlayStateForLayerKey(const std::string& layerKey);
void UpdateOscOverlayState( void UpdateOscOverlayState(
const std::vector<OscOverlayUpdate>& updates, const std::vector<OscOverlayUpdate>& updates,
const std::vector<OscOverlayCommitCompletion>& completedCommits); const std::vector<OscOverlayCommitCompletion>& completedCommits);

View File

@@ -85,10 +85,19 @@ bool RuntimeUpdateController::ApplyRuntimeCoordinatorResult(const RuntimeCoordin
mRuntimeCoordinator.ApplyCommittedStateMode(result.committedStateMode); mRuntimeCoordinator.ApplyCommittedStateMode(result.committedStateMode);
if (result.clearTransientOscState) switch (result.transientOscInvalidation)
{ {
case RuntimeCoordinatorTransientOscInvalidation::All:
mRenderEngine.ClearOscOverlayState(); mRenderEngine.ClearOscOverlayState();
mRuntimeServices.ClearOscState(); mRuntimeServices.ClearOscState();
break;
case RuntimeCoordinatorTransientOscInvalidation::Layer:
mRenderEngine.ClearOscOverlayStateForLayerKey(result.transientOscLayerKey);
mRuntimeServices.ClearOscStateForLayerKey(result.transientOscLayerKey);
break;
case RuntimeCoordinatorTransientOscInvalidation::None:
default:
break;
} }
mRenderEngine.ApplyRuntimeCoordinatorRenderReset(result.renderResetScope); mRenderEngine.ApplyRuntimeCoordinatorRenderReset(result.renderResetScope);

View File

@@ -56,6 +56,11 @@ RuntimeCoordinatorResult RuntimeCoordinator::RemoveLayer(const std::string& laye
} }
RuntimeCoordinatorResult result = ApplyStoreMutation(mRuntimeStore.DeleteStoredLayer(layerId, error), error, true, true, true); RuntimeCoordinatorResult result = ApplyStoreMutation(mRuntimeStore.DeleteStoredLayer(layerId, error), error, true, true, true);
if (result.accepted)
{
result.transientOscInvalidation = RuntimeCoordinatorTransientOscInvalidation::Layer;
result.transientOscLayerKey = layerId;
}
PublishCoordinatorResult("RemoveLayer", result); PublishCoordinatorResult("RemoveLayer", result);
return result; return result;
} }
@@ -205,7 +210,8 @@ RuntimeCoordinatorResult RuntimeCoordinator::ResetLayerParameters(const std::str
return result; return result;
} }
result.clearTransientOscState = true; result.transientOscInvalidation = RuntimeCoordinatorTransientOscInvalidation::Layer;
result.transientOscLayerKey = layerId;
result.renderResetScope = RuntimeCoordinatorRenderResetScope::TemporalHistoryAndFeedback; result.renderResetScope = RuntimeCoordinatorRenderResetScope::TemporalHistoryAndFeedback;
PublishCoordinatorResult("ResetLayerParameters", result); PublishCoordinatorResult("ResetLayerParameters", result);
return result; return result;
@@ -538,7 +544,7 @@ void RuntimeCoordinator::PublishCoordinatorResult(const std::string& action, con
mutation.runtimeStateBroadcastRequired = result.runtimeStateBroadcastRequired; mutation.runtimeStateBroadcastRequired = result.runtimeStateBroadcastRequired;
mutation.shaderBuildRequested = result.shaderBuildRequested; mutation.shaderBuildRequested = result.shaderBuildRequested;
mutation.persistenceRequested = result.persistenceRequested; mutation.persistenceRequested = result.persistenceRequested;
mutation.clearTransientOscState = result.clearTransientOscState; mutation.clearTransientOscState = result.transientOscInvalidation != RuntimeCoordinatorTransientOscInvalidation::None;
mutation.renderResetScope = ToRuntimeEventRenderResetScope(result.renderResetScope); mutation.renderResetScope = ToRuntimeEventRenderResetScope(result.renderResetScope);
mutation.errorMessage = result.errorMessage; mutation.errorMessage = result.errorMessage;
mRuntimeEventDispatcher.PublishPayload(mutation, "RuntimeCoordinator"); mRuntimeEventDispatcher.PublishPayload(mutation, "RuntimeCoordinator");

View File

@@ -25,18 +25,26 @@ enum class RuntimeCoordinatorRenderResetScope
TemporalHistoryAndFeedback TemporalHistoryAndFeedback
}; };
enum class RuntimeCoordinatorTransientOscInvalidation
{
None,
Layer,
All
};
struct RuntimeCoordinatorResult struct RuntimeCoordinatorResult
{ {
bool accepted = false; bool accepted = false;
bool runtimeStateBroadcastRequired = false; bool runtimeStateBroadcastRequired = false;
bool shaderBuildRequested = false; bool shaderBuildRequested = false;
bool persistenceRequested = false; bool persistenceRequested = false;
bool clearTransientOscState = false;
bool compileStatusChanged = false; bool compileStatusChanged = false;
bool compileStatusSucceeded = false; bool compileStatusSucceeded = false;
bool clearReloadRequest = false; bool clearReloadRequest = false;
RuntimeCoordinatorCommittedStateMode committedStateMode = RuntimeCoordinatorCommittedStateMode::Unchanged; RuntimeCoordinatorCommittedStateMode committedStateMode = RuntimeCoordinatorCommittedStateMode::Unchanged;
RuntimeCoordinatorRenderResetScope renderResetScope = RuntimeCoordinatorRenderResetScope::None; RuntimeCoordinatorRenderResetScope renderResetScope = RuntimeCoordinatorRenderResetScope::None;
RuntimeCoordinatorTransientOscInvalidation transientOscInvalidation = RuntimeCoordinatorTransientOscInvalidation::None;
std::string transientOscLayerKey;
std::string compileStatusMessage; std::string compileStatusMessage;
std::string errorMessage; std::string errorMessage;
}; };

View File

@@ -74,6 +74,7 @@ JsonValue BuildOscCommitValue(const ShaderParameterDefinition& definition, const
return JsonValue(); return JsonValue();
} }
} }
void RuntimeLiveState::Clear() void RuntimeLiveState::Clear()
@@ -81,6 +82,47 @@ void RuntimeLiveState::Clear()
mOscOverlayStates.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<RuntimeRenderState>& states,
std::vector<RuntimeRenderState>::const_iterator& stateIt,
std::vector<ShaderParameterDefinition>::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 std::size_t RuntimeLiveState::OverlayCount() const
{ {
return mOscOverlayStates.size(); return mOscOverlayStates.size();
@@ -131,6 +173,27 @@ void RuntimeLiveState::ApplyOscCommitCompletions(const std::vector<RuntimeLiveOs
} }
} }
void RuntimeLiveState::PruneIncompatibleOverlays(const std::vector<RuntimeRenderState>& states)
{
for (auto it = mOscOverlayStates.begin(); it != mOscOverlayStates.end();)
{
std::vector<RuntimeRenderState>::const_iterator stateIt;
std::vector<ShaderParameterDefinition>::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( void RuntimeLiveState::ApplyToLayerStates(
std::vector<RuntimeRenderState>& states, std::vector<RuntimeRenderState>& states,
const RuntimeLiveStateApplyOptions& options, const RuntimeLiveStateApplyOptions& options,
@@ -139,6 +202,10 @@ void RuntimeLiveState::ApplyToLayerStates(
if (states.empty() || mOscOverlayStates.empty()) if (states.empty() || mOscOverlayStates.empty())
return; return;
PruneIncompatibleOverlays(states);
if (mOscOverlayStates.empty())
return;
const auto now = options.now; const auto now = options.now;
const double clampedSmoothing = ClampOscAlpha(options.smoothing); const double clampedSmoothing = ClampOscAlpha(options.smoothing);
std::vector<std::string> overlayKeysToRemove; std::vector<std::string> overlayKeysToRemove;

View File

@@ -44,9 +44,11 @@ class RuntimeLiveState
{ {
public: public:
void Clear(); void Clear();
void ClearForLayerKey(const std::string& layerKey);
std::size_t OverlayCount() const; std::size_t OverlayCount() const;
void ApplyOscUpdates(const std::vector<RuntimeLiveOscUpdate>& updates); void ApplyOscUpdates(const std::vector<RuntimeLiveOscUpdate>& updates);
void ApplyOscCommitCompletions(const std::vector<RuntimeLiveOscCommitCompletion>& completedCommits); void ApplyOscCommitCompletions(const std::vector<RuntimeLiveOscCommitCompletion>& completedCommits);
void PruneIncompatibleOverlays(const std::vector<RuntimeRenderState>& states);
void ApplyToLayerStates( void ApplyToLayerStates(
std::vector<RuntimeRenderState>& states, std::vector<RuntimeRenderState>& states,
const RuntimeLiveStateApplyOptions& options, const RuntimeLiveStateApplyOptions& options,
@@ -67,5 +69,12 @@ private:
bool commitQueued = false; bool commitQueued = false;
}; };
static bool OverlayMatchesLayerKey(const OscOverlayState& overlay, const std::string& layerKey);
static bool TryResolveOverlayTarget(
const OscOverlayState& overlay,
const std::vector<RuntimeRenderState>& states,
std::vector<RuntimeRenderState>::const_iterator& stateIt,
std::vector<ShaderParameterDefinition>::const_iterator& definitionIt);
std::map<std::string, OscOverlayState> mOscOverlayStates; std::map<std::string, OscOverlayState> mOscOverlayStates;
}; };

View File

@@ -7,8 +7,8 @@ Phase 1 named the subsystems. Phase 2 added the typed event substrate. Phase 3 m
## Status ## Status
- Phase 5 design package: proposed. - Phase 5 design package: proposed.
- Phase 5 implementation: Step 2 started. - 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, 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`. - 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: 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. - `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. - `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. - `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 ## Why Phase 5 Exists
@@ -265,14 +266,21 @@ Move reset/reload transient-state decisions into one policy point.
Initial target: Initial target:
- layer removal clears matching transient overlays - [x] layer removal clears matching transient overlays
- shader change clears incompatible overlays - [x] shader change clears incompatible overlays
- preset load clears incompatible overlays - [x] preset load clears incompatible overlays
- shader reload can preserve compatible overlays when requested - [x] shader reload can preserve compatible overlays when requested
- temporal/feedback resets stay render-local and separate from parameter overlays - [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. 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 ### Step 4. Clarify OSC Commit Semantics
Make the transient-to-committed path explicit. 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] 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] final render-value precedence is explicit and covered by tests
- [x] `RenderStateComposer` or its replacement consumes a layered input contract - [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 - [ ] OSC overlay settle/commit behavior is explicit, including persistence policy
- [ ] `RuntimeStore` remains durable-state focused and does not absorb transient automation policy - [ ] `RuntimeStore` remains durable-state focused and does not absorb transient automation policy
- [ ] render-local temporal/feedback state remains separate from live parameter layering - [ ] render-local temporal/feedback state remains separate from live parameter layering

View File

@@ -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. 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 ### 5. Shader Build Application
Compilation itself may eventually move into a separate build service, but once shader build outputs exist, `RenderEngine` owns: Compilation itself may eventually move into a separate build service, but once shader build outputs exist, `RenderEngine` owns:

View File

@@ -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. 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 ## Migration Plan From Current Code
The coordinator should be introduced incrementally. The coordinator should be introduced incrementally.

View File

@@ -349,6 +349,100 @@ void TestRuntimeLiveStateTriggerOverlayIncrementsAndClears()
Expect(liveState.OverlayCount() == 0, "trigger overlay clears after apply"); Expect(liveState.OverlayCount() == 0, "trigger overlay clears after apply");
} }
void TestRuntimeLiveStateClearsOverlaysForLayerKey()
{
RuntimeLiveState liveState;
RuntimeLiveOscUpdate first;
first.routeKey = "layer-one\namount";
first.layerKey = "layer-one";
first.parameterKey = "amount";
first.targetValue = JsonValue(0.5);
RuntimeLiveOscUpdate second = first;
second.routeKey = "layer-two\namount";
second.layerKey = "layer-two";
second.targetValue = JsonValue(0.75);
liveState.ApplyOscUpdates({ first, second });
liveState.ClearForLayerKey("layer-one");
Expect(liveState.OverlayCount() == 1, "layer-scoped invalidation only removes matching overlays");
std::vector<RuntimeRenderState> states = { MakeLayerState() };
states[0].layerId = "layer-two";
RuntimeLiveStateApplyOptions options;
options.smoothing = 0.0;
liveState.ApplyToLayerStates(states, options, nullptr);
const auto valueIt = states[0].parameterValues.find("amount");
Expect(valueIt != states[0].parameterValues.end() &&
!valueIt->second.numberValues.empty() &&
std::fabs(valueIt->second.numberValues[0] - 0.75) < 0.0001,
"unmatched layer invalidation preserves unrelated overlay");
}
void TestRuntimeLiveStatePrunesRemovedLayerOverlay()
{
RuntimeLiveState liveState;
RuntimeLiveOscUpdate update;
update.routeKey = "removed-layer\namount";
update.layerKey = "removed-layer";
update.parameterKey = "amount";
update.targetValue = JsonValue(0.5);
liveState.ApplyOscUpdates({ update });
std::vector<RuntimeRenderState> states = { MakeLayerState() };
liveState.PruneIncompatibleOverlays(states);
Expect(liveState.OverlayCount() == 0, "invalidation policy removes overlays for missing layers");
}
void TestRuntimeLiveStatePrunesIncompatibleParameterOverlay()
{
RuntimeLiveState liveState;
RuntimeLiveOscUpdate update;
update.routeKey = "layer-one\namount";
update.layerKey = "layer-one";
update.parameterKey = "amount";
update.targetValue = JsonValue(0.5);
liveState.ApplyOscUpdates({ update });
std::vector<RuntimeRenderState> states = { MakeLayerStateWithDefinitions({ FloatDefinition("other", "Other") }) };
liveState.PruneIncompatibleOverlays(states);
Expect(liveState.OverlayCount() == 0, "invalidation policy removes overlays for missing parameters");
}
void TestRuntimeLiveStatePreservesCompatibleOverlayAfterShaderReload()
{
RuntimeLiveState liveState;
RuntimeLiveOscUpdate update;
update.routeKey = "layer-one\namount";
update.layerKey = "layer-one";
update.parameterKey = "Amount";
update.targetValue = JsonValue(0.5);
liveState.ApplyOscUpdates({ update });
std::vector<RuntimeRenderState> states = { MakeLayerStateWithDefinitions({ FloatDefinition("amount-renamed", "Amount") }) };
liveState.PruneIncompatibleOverlays(states);
Expect(liveState.OverlayCount() == 1, "invalidation policy preserves overlays that still map by control key");
RuntimeLiveStateApplyOptions options;
options.smoothing = 0.0;
liveState.ApplyToLayerStates(states, options, nullptr);
const auto valueIt = states[0].parameterValues.find("amount-renamed");
Expect(valueIt != states[0].parameterValues.end() &&
!valueIt->second.numberValues.empty() &&
std::fabs(valueIt->second.numberValues[0] - 0.5) < 0.0001,
"compatible overlay applies to the reloaded parameter id");
}
void TestRenderStateComposerBuildsFrameState() void TestRenderStateComposerBuildsFrameState()
{ {
RuntimeLiveState liveState; RuntimeLiveState liveState;
@@ -496,6 +590,10 @@ int main()
TestRuntimeLiveStateSmoothingPartiallyConverges(); TestRuntimeLiveStateSmoothingPartiallyConverges();
TestRuntimeLiveStateSmoothingVectorSizeMismatchUsesTargetShape(); TestRuntimeLiveStateSmoothingVectorSizeMismatchUsesTargetShape();
TestRuntimeLiveStateTriggerOverlayIncrementsAndClears(); TestRuntimeLiveStateTriggerOverlayIncrementsAndClears();
TestRuntimeLiveStateClearsOverlaysForLayerKey();
TestRuntimeLiveStatePrunesRemovedLayerOverlay();
TestRuntimeLiveStatePrunesIncompatibleParameterOverlay();
TestRuntimeLiveStatePreservesCompatibleOverlayAfterShaderReload();
TestRenderStateComposerBuildsFrameState(); TestRenderStateComposerBuildsFrameState();
TestRenderStateComposerUsesCommittedLayerOverBaseLayer(); TestRenderStateComposerUsesCommittedLayerOverBaseLayer();
TestRenderStateComposerUsesBaseLayerWhenCommittedLayerMissing(); TestRenderStateComposerUsesBaseLayerWhenCommittedLayerMissing();

View File

@@ -278,6 +278,14 @@ void TestRuntimeCoordinatorPersistenceEvents()
expectAcceptedPersistence(coordinator.UpdateLayerParameter(alphaLayerId, "gain", JsonValue(0.75)), "UpdateLayerParameter", expectAcceptedPersistence(coordinator.UpdateLayerParameter(alphaLayerId, "gain", JsonValue(0.75)), "UpdateLayerParameter",
"parameter changes are accepted"); "parameter changes are accepted");
RuntimeCoordinatorResult resetResult = coordinator.ResetLayerParameters(alphaLayerId);
std::vector<RuntimeEvent> resetEvents = dispatchAndClear();
Expect(resetResult.accepted, "parameter reset is accepted");
Expect(resetResult.transientOscInvalidation == RuntimeCoordinatorTransientOscInvalidation::Layer,
"parameter reset requests layer-scoped transient OSC invalidation");
Expect(resetResult.transientOscLayerKey == alphaLayerId, "parameter reset invalidates the target layer overlays");
Expect(countEvents(resetEvents, RuntimeEventType::RuntimeMutationAccepted) == 1, "parameter reset publishes accepted fact");
expectAcceptedPersistence(coordinator.AddLayer("beta"), "AddLayer", "stack edits are accepted"); expectAcceptedPersistence(coordinator.AddLayer("beta"), "AddLayer", "stack edits are accepted");
layers = store.CopyLayerStates(); layers = store.CopyLayerStates();
Expect(layers.size() == 2, "stack edit creates a second layer"); Expect(layers.size() == 2, "stack edit creates a second layer");