From fdcc38c6ae1351cdd490410e03b436f1a2293f60 Mon Sep 17 00:00:00 2001 From: Aiden <68633820+awils27@users.noreply.github.com> Date: Mon, 11 May 2026 19:09:01 +1000 Subject: [PATCH] Step 4 --- .../control/ControlServices.cpp | 20 ++++++++++++++++ .../control/ControlServices.h | 1 + .../coordination/RuntimeCoordinator.cpp | 7 +++++- .../runtime/coordination/RuntimeCoordinator.h | 6 +++++ docs/PHASE_5_LIVE_STATE_LAYERING_DESIGN.md | 24 +++++++++++++------ docs/subsystems/RuntimeCoordinator.md | 2 ++ tests/RuntimeSubsystemTests.cpp | 14 +++++++++-- 7 files changed, 64 insertions(+), 10 deletions(-) diff --git a/apps/LoopThroughWithOpenGLCompositing/control/ControlServices.cpp b/apps/LoopThroughWithOpenGLCompositing/control/ControlServices.cpp index c55d5ec..96923e8 100644 --- a/apps/LoopThroughWithOpenGLCompositing/control/ControlServices.cpp +++ b/apps/LoopThroughWithOpenGLCompositing/control/ControlServices.cpp @@ -241,6 +241,7 @@ void ControlServices::PollLoop(RuntimeCoordinator& runtimeCoordinator) completedCommit.generation = entry.second.generation; std::lock_guard lock(mCompletedOscCommitMutex); mCompletedOscCommits.push_back(std::move(completedCommit)); + PublishOscOverlaySettled(entry.second); } else if (!result.errorMessage.empty()) { @@ -321,3 +322,22 @@ void ControlServices::PublishOscCommitRequested(const PendingOscCommit& commit) OutputDebugStringA("OscCommitRequested event publish threw.\n"); } } + +void ControlServices::PublishOscOverlaySettled(const PendingOscCommit& commit) +{ + try + { + OscOverlayEvent event; + event.routeKey = commit.routeKey; + event.layerKey = commit.layerKey; + event.parameterKey = commit.parameterKey; + event.generation = commit.generation; + event.settled = true; + if (!mRuntimeEventDispatcher.PublishPayload(event, "ControlServices")) + OutputDebugStringA("OscOverlaySettled event publish failed.\n"); + } + catch (...) + { + OutputDebugStringA("OscOverlaySettled event publish threw.\n"); + } +} diff --git a/apps/LoopThroughWithOpenGLCompositing/control/ControlServices.h b/apps/LoopThroughWithOpenGLCompositing/control/ControlServices.h index 1620d15..991916d 100644 --- a/apps/LoopThroughWithOpenGLCompositing/control/ControlServices.h +++ b/apps/LoopThroughWithOpenGLCompositing/control/ControlServices.h @@ -76,6 +76,7 @@ private: void PublishRuntimeStateBroadcastRequested(const std::string& reason); void PublishOscValueReceived(const PendingOscUpdate& update, const std::string& routeKey); void PublishOscCommitRequested(const PendingOscCommit& commit); + void PublishOscOverlaySettled(const PendingOscCommit& commit); std::unique_ptr mControlServer; std::unique_ptr mOscServer; diff --git a/apps/LoopThroughWithOpenGLCompositing/runtime/coordination/RuntimeCoordinator.cpp b/apps/LoopThroughWithOpenGLCompositing/runtime/coordination/RuntimeCoordinator.cpp index a5c73c2..89fe1a6 100644 --- a/apps/LoopThroughWithOpenGLCompositing/runtime/coordination/RuntimeCoordinator.cpp +++ b/apps/LoopThroughWithOpenGLCompositing/runtime/coordination/RuntimeCoordinator.cpp @@ -178,9 +178,14 @@ RuntimeCoordinatorResult RuntimeCoordinator::UpdateLayerParameterByControlKey(co RuntimeCoordinatorResult RuntimeCoordinator::CommitOscParameterByControlKey(const std::string& layerKey, const std::string& parameterKey, const JsonValue& newValue) { std::lock_guard lock(mMutex); + constexpr RuntimeCoordinatorOscCommitPersistence kDefaultOscCommitPersistence = + RuntimeCoordinatorOscCommitPersistence::SessionOnly; + constexpr bool kPersistSettledOscCommits = + kDefaultOscCommitPersistence == RuntimeCoordinatorOscCommitPersistence::Persistent; + std::string error; ResolvedParameterMutation mutation; - if (!BuildParameterMutationByControlKey(layerKey, parameterKey, newValue, true, mutation, error)) + if (!BuildParameterMutationByControlKey(layerKey, parameterKey, newValue, kPersistSettledOscCommits, mutation, error)) { RuntimeCoordinatorResult result = ApplyStoreMutation(false, error, false, false, false); PublishCoordinatorResult("CommitOscParameterByControlKey", result); diff --git a/apps/LoopThroughWithOpenGLCompositing/runtime/coordination/RuntimeCoordinator.h b/apps/LoopThroughWithOpenGLCompositing/runtime/coordination/RuntimeCoordinator.h index 0ad8709..d5af51e 100644 --- a/apps/LoopThroughWithOpenGLCompositing/runtime/coordination/RuntimeCoordinator.h +++ b/apps/LoopThroughWithOpenGLCompositing/runtime/coordination/RuntimeCoordinator.h @@ -32,6 +32,12 @@ enum class RuntimeCoordinatorTransientOscInvalidation All }; +enum class RuntimeCoordinatorOscCommitPersistence +{ + SessionOnly, + Persistent +}; + struct RuntimeCoordinatorResult { bool accepted = false; diff --git a/docs/PHASE_5_LIVE_STATE_LAYERING_DESIGN.md b/docs/PHASE_5_LIVE_STATE_LAYERING_DESIGN.md index 862380b..33e8621 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 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`. +- Phase 5 implementation: Step 4 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, `RuntimeLiveState` owns transient-overlay invalidation against current layer/parameter compatibility, and settled OSC commits have an explicit session-only persistence policy. Committed runtime values are still physically stored through `RuntimeStore`/`LayerStackStore`. Current live-state footholds: @@ -20,6 +20,7 @@ Current live-state footholds: - `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. +- `RuntimeCoordinator::CommitOscParameterByControlKey(...)` commits settled OSC values into session state without requesting persistence by default. ## Why Phase 5 Exists @@ -287,13 +288,22 @@ Make the transient-to-committed path explicit. Initial target: -- document and test whether settled OSC commits persist -- ensure stale generation completions are ignored -- ensure one settled route does not clear unrelated overlay state -- publish or preserve useful events for accepted overlay commits +- [x] document and test whether settled OSC commits persist +- [x] ensure stale generation completions are ignored +- [x] ensure one settled route does not clear unrelated overlay state +- [x] publish or preserve useful events for accepted overlay commits Current Phase 3 behavior is a good base; Phase 5 should make the policy easier to reason about from the code. +Current policy: + +- settled OSC commits are `RuntimeCoordinatorOscCommitPersistence::SessionOnly` by default +- accepted settled OSC commits update the committed session value through `RuntimeStore::SetStoredParameterValue(..., persistState = false, ...)` +- accepted settled OSC commits publish runtime mutation/state-change observations, but no `RuntimePersistenceRequested` event +- accepted service-side commit completions publish `OscOverlaySettled` +- stale generation completions are ignored by `RuntimeLiveState::ApplyOscCommitCompletions(...)` +- unrelated routes remain untouched when a different route settles or completes + ### Step 5. Separate Committed-Live Concept From Durable Storage Decide whether to physically split committed-live state now or introduce a read/model boundary first. @@ -377,7 +387,7 @@ Phase 5 can be considered complete once the project can say: - [x] final render-value precedence is explicit and covered by tests - [x] `RenderStateComposer` or its replacement consumes a layered input contract - [x] reset/reload/preset behavior for transient overlays is centralized or clearly delegated -- [ ] OSC overlay settle/commit behavior is explicit, including persistence policy +- [x] 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 - [ ] subsystem docs and the architecture review reflect the final ownership model diff --git a/docs/subsystems/RuntimeCoordinator.md b/docs/subsystems/RuntimeCoordinator.md index 9c36e28..893cb23 100644 --- a/docs/subsystems/RuntimeCoordinator.md +++ b/docs/subsystems/RuntimeCoordinator.md @@ -272,6 +272,8 @@ For OSC specifically, the coordinator should eventually decide: - whether it should later commit into committed live state - what reset/reload actions invalidate it +Phase 5 sets the default settled OSC policy to session-only. `CommitOscParameterByControlKey(...)` updates committed session state through the store with persistence disabled, publishes ordinary mutation/state-change observations, and does not request a persistence write unless a future explicit policy opts into durable OSC commits. + ### Health and timing state The coordinator may emit events like: diff --git a/tests/RuntimeSubsystemTests.cpp b/tests/RuntimeSubsystemTests.cpp index f2c3ac9..27fd28d 100644 --- a/tests/RuntimeSubsystemTests.cpp +++ b/tests/RuntimeSubsystemTests.cpp @@ -311,8 +311,18 @@ void TestRuntimeCoordinatorPersistenceEvents() Expect(countEvents(overlayEvents, RuntimeEventType::OscOverlayApplied) == 1, "transient OSC overlay is observable"); Expect(countEvents(overlayEvents, RuntimeEventType::RuntimePersistenceRequested) == 0, "transient OSC overlay does not request persistence"); - expectAcceptedPersistence(coordinator.CommitOscParameterByControlKey("alpha", "gain", JsonValue(0.2)), "CommitOscParameterByControlKey", - "accepted OSC commit is persistent"); + RuntimeCoordinatorResult oscCommitResult = coordinator.CommitOscParameterByControlKey("alpha", "gain", JsonValue(0.2)); + std::vector oscCommitEvents = dispatchAndClear(); + Expect(oscCommitResult.accepted, "accepted OSC commit updates committed session state"); + Expect(!oscCommitResult.persistenceRequested, "settled OSC commit does not request persistence by default"); + Expect(countEvents(oscCommitEvents, RuntimeEventType::RuntimeMutationAccepted) == 1, "settled OSC commit publishes accepted fact"); + Expect(countEvents(oscCommitEvents, RuntimeEventType::RuntimeStateChanged) == 1, "settled OSC commit publishes state change"); + Expect(countEvents(oscCommitEvents, RuntimeEventType::RuntimePersistenceRequested) == 0, "settled OSC commit publishes no persistence request"); + RuntimeStore::StoredParameterSnapshot oscCommitSnapshot; + Expect(store.TryGetStoredParameterByControlKey("alpha", "gain", oscCommitSnapshot, error), "settled OSC commit can be read back"); + Expect(!oscCommitSnapshot.currentValue.numberValues.empty() && + oscCommitSnapshot.currentValue.numberValues[0] == 0.2, + "settled OSC commit updates the committed session value"); } std::filesystem::remove_all(root);