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)
{
completedCommits.clear();

View File

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

View File

@@ -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<CompletedOscCommit>& completedCommits)
{
if (!mControlServices)

View File

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

View File

@@ -415,7 +415,16 @@ void RenderEngine::ProcessScreenshotCaptureCommandsOnRenderThread()
void RenderEngine::ClearOscOverlayState()
{
InvokeOnRenderThread([this]() {
mRuntimeLiveState.Clear();
});
}
void RenderEngine::ClearOscOverlayStateForLayerKey(const std::string& layerKey)
{
InvokeOnRenderThread([this, layerKey]() {
mRuntimeLiveState.ClearForLayerKey(layerKey);
});
}
void RenderEngine::UpdateOscOverlayState(

View File

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

View File

@@ -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);

View File

@@ -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");

View File

@@ -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;
};

View File

@@ -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<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
{
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(
std::vector<RuntimeRenderState>& 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<std::string> overlayKeysToRemove;

View File

@@ -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<RuntimeLiveOscUpdate>& updates);
void ApplyOscCommitCompletions(const std::vector<RuntimeLiveOscCommitCompletion>& completedCommits);
void PruneIncompatibleOverlays(const std::vector<RuntimeRenderState>& states);
void ApplyToLayerStates(
std::vector<RuntimeRenderState>& 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<RuntimeRenderState>& states,
std::vector<RuntimeRenderState>::const_iterator& stateIt,
std::vector<ShaderParameterDefinition>::const_iterator& definitionIt);
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
- 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

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.
`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:

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.
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.

View File

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

View File

@@ -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<RuntimeEvent> resetEvents = dispatchAndClear();
Expect(resetResult.accepted, "parameter reset is accepted");
Expect(resetResult.transientOscInvalidation == RuntimeCoordinatorTransientOscInvalidation::Layer,
"parameter reset requests layer-scoped transient OSC invalidation");
Expect(resetResult.transientOscLayerKey == alphaLayerId, "parameter reset invalidates the target layer overlays");
Expect(countEvents(resetEvents, RuntimeEventType::RuntimeMutationAccepted) == 1, "parameter reset publishes accepted fact");
expectAcceptedPersistence(coordinator.AddLayer("beta"), "AddLayer", "stack edits are accepted");
layers = store.CopyLayerStates();
Expect(layers.size() == 2, "stack edit creates a second layer");