diff --git a/apps/LoopThroughWithOpenGLCompositing/runtime/snapshot/RenderSnapshotBuilder.cpp b/apps/LoopThroughWithOpenGLCompositing/runtime/snapshot/RenderSnapshotBuilder.cpp index a8fe6c5..14b5826 100644 --- a/apps/LoopThroughWithOpenGLCompositing/runtime/snapshot/RenderSnapshotBuilder.cpp +++ b/apps/LoopThroughWithOpenGLCompositing/runtime/snapshot/RenderSnapshotBuilder.cpp @@ -89,7 +89,7 @@ bool RenderSnapshotBuilder::TryBuildLayerRenderStates(unsigned outputWidth, unsi bool RenderSnapshotBuilder::TryRefreshLayerParameters(std::vector& states) const { - RefreshLayerParameters(mRuntimeStore.CopyLayerStates(), states); + RefreshLayerParameters(mRuntimeStore.CopyCommittedLiveLayerStates(), states); return true; } @@ -113,10 +113,10 @@ void RenderSnapshotBuilder::BuildLayerRenderStates(unsigned outputWidth, unsigne { states.clear(); - for (const LayerStackStore::LayerPersistentState& layer : readModel.layers) + for (const LayerStackStore::LayerPersistentState& layer : readModel.committedLiveState.layers) { - auto shaderIt = readModel.packagesById.find(layer.shaderId); - if (shaderIt == readModel.packagesById.end()) + auto shaderIt = readModel.committedLiveState.packagesById.find(layer.shaderId); + if (shaderIt == readModel.committedLiveState.packagesById.end()) continue; const ShaderPackage& shaderPackage = shaderIt->second; diff --git a/apps/LoopThroughWithOpenGLCompositing/runtime/store/RuntimeStore.cpp b/apps/LoopThroughWithOpenGLCompositing/runtime/store/RuntimeStore.cpp index 12eab05..1d9e750 100644 --- a/apps/LoopThroughWithOpenGLCompositing/runtime/store/RuntimeStore.cpp +++ b/apps/LoopThroughWithOpenGLCompositing/runtime/store/RuntimeStore.cpp @@ -576,24 +576,37 @@ ShaderCompilerInputs RuntimeStore::GetShaderCompilerInputs() const return inputs; } +CommittedLiveStateReadModel RuntimeStore::BuildCommittedLiveStateReadModel() const +{ + CommittedLiveStateReadModel model; + std::lock_guard lock(mMutex); + model.layers = mLayerStack.Layers(); + model.packagesById = mShaderCatalog.CaptureSnapshot().packagesById; + return model; +} + RenderSnapshotReadModel RuntimeStore::BuildRenderSnapshotReadModel() const { RenderSnapshotReadModel model; model.signalStatus = mHealthTelemetry.GetSignalStatusSnapshot(); + model.committedLiveState = BuildCommittedLiveStateReadModel(); std::lock_guard lock(mMutex); - model.layers = mLayerStack.Layers(); - model.packagesById = mShaderCatalog.CaptureSnapshot().packagesById; model.timing.startTime = mStartTime; model.timing.startupRandom = mStartupRandom; return model; } -std::vector RuntimeStore::CopyLayerStates() const +std::vector RuntimeStore::CopyCommittedLiveLayerStates() const { std::lock_guard lock(mMutex); return mLayerStack.Layers(); } +std::vector RuntimeStore::CopyLayerStates() const +{ + return CopyCommittedLiveLayerStates(); +} + RenderTimingSnapshot RuntimeStore::GetRenderTimingSnapshot() const { std::lock_guard lock(mMutex); diff --git a/apps/LoopThroughWithOpenGLCompositing/runtime/store/RuntimeStore.h b/apps/LoopThroughWithOpenGLCompositing/runtime/store/RuntimeStore.h index 4ebcea7..4678a30 100644 --- a/apps/LoopThroughWithOpenGLCompositing/runtime/store/RuntimeStore.h +++ b/apps/LoopThroughWithOpenGLCompositing/runtime/store/RuntimeStore.h @@ -71,7 +71,9 @@ public: void ClearReloadRequest(); bool CopyShaderPackageForStoredLayer(const std::string& layerId, ShaderPackage& shaderPackage, std::string& error) const; ::ShaderCompilerInputs GetShaderCompilerInputs() const; + ::CommittedLiveStateReadModel BuildCommittedLiveStateReadModel() const; ::RenderSnapshotReadModel BuildRenderSnapshotReadModel() const; + std::vector CopyCommittedLiveLayerStates() const; std::vector CopyLayerStates() const; ::RenderTimingSnapshot GetRenderTimingSnapshot() const; ::RuntimeStatePresentationReadModel BuildRuntimeStatePresentationReadModel() const; diff --git a/apps/LoopThroughWithOpenGLCompositing/runtime/store/RuntimeStoreReadModels.h b/apps/LoopThroughWithOpenGLCompositing/runtime/store/RuntimeStoreReadModels.h index 26ba8d2..e38af92 100644 --- a/apps/LoopThroughWithOpenGLCompositing/runtime/store/RuntimeStoreReadModels.h +++ b/apps/LoopThroughWithOpenGLCompositing/runtime/store/RuntimeStoreReadModels.h @@ -27,10 +27,15 @@ struct RenderTimingSnapshot double startupRandom = 0.0; }; -struct RenderSnapshotReadModel +struct CommittedLiveStateReadModel { std::vector layers; std::map packagesById; +}; + +struct RenderSnapshotReadModel +{ + CommittedLiveStateReadModel committedLiveState; HealthTelemetry::SignalStatusSnapshot signalStatus; RenderTimingSnapshot timing; }; diff --git a/docs/PHASE_5_LIVE_STATE_LAYERING_DESIGN.md b/docs/PHASE_5_LIVE_STATE_LAYERING_DESIGN.md index 33e8621..13df73f 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 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`. +- Phase 5 implementation: Step 5 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, settled OSC commits have an explicit session-only persistence policy, and snapshot publication consumes a named `CommittedLiveStateReadModel`. Committed runtime values are still physically backed by `RuntimeStore`/`LayerStackStore` during this conservative migration step. Current live-state footholds: @@ -21,6 +21,7 @@ Current live-state footholds: - `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. +- `CommittedLiveStateReadModel` names the current committed/session read boundary that feeds render snapshot publication while remaining physically backed by `RuntimeStore`. ## Why Phase 5 Exists @@ -310,9 +311,9 @@ Decide whether to physically split committed-live state now or introduce a read/ Conservative option: -- leave storage physically in `RuntimeStore` -- add a named committed-live read model -- keep persistence decisions in `RuntimeCoordinator` +- [x] leave storage physically in `RuntimeStore` +- [x] add a named committed-live read model +- [x] keep persistence decisions in `RuntimeCoordinator` Stronger option: @@ -322,6 +323,13 @@ Stronger option: Phase 5 does not need a flag-day split. It needs the concept to stop being implicit. +Current implementation: + +- `CommittedLiveStateReadModel` carries the current committed/session layer stack and shader package metadata used by snapshot publication. +- `RenderSnapshotReadModel` contains `committedLiveState` rather than exposing layer-stack fields directly. +- `RenderSnapshotBuilder` builds render snapshots and parameter refreshes from committed-live read APIs. +- `RuntimeStore` still provides the physical backing during this phase, but session-only committed changes can be observed through the committed-live read model without requiring durable persistence. + ### Step 6. Update Docs And Exit Criteria Before calling Phase 5 complete, update: @@ -388,7 +396,7 @@ Phase 5 can be considered complete once the project can say: - [x] `RenderStateComposer` or its replacement consumes a layered input contract - [x] reset/reload/preset behavior for transient overlays is centralized or clearly delegated - [x] OSC overlay settle/commit behavior is explicit, including persistence policy -- [ ] `RuntimeStore` remains durable-state focused and does not absorb transient automation policy +- [x] `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/RenderEngine.md b/docs/subsystems/RenderEngine.md index d6f14f4..8f5d468 100644 --- a/docs/subsystems/RenderEngine.md +++ b/docs/subsystems/RenderEngine.md @@ -113,6 +113,8 @@ Phase 5's `RuntimeStateLayerModel` explicitly keeps temporal history, feedback s `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. +Render snapshots now flow through a named `CommittedLiveStateReadModel`, so render-facing committed state is distinct from durable storage even while both are physically backed by the same store during migration. + ### 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 893cb23..fceea75 100644 --- a/docs/subsystems/RuntimeCoordinator.md +++ b/docs/subsystems/RuntimeCoordinator.md @@ -274,6 +274,8 @@ For OSC specifically, the coordinator should eventually decide: 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. +The committed-live concept now has a named read model, `CommittedLiveStateReadModel`. The coordinator remains the owner of whether a mutation should be durable or session-only, while `RuntimeStore` temporarily backs the read model until a physical `CommittedLiveState` collaborator is worth extracting. + ### Health and timing state The coordinator may emit events like: diff --git a/docs/subsystems/RuntimeStore.md b/docs/subsystems/RuntimeStore.md index b8038d9..994117d 100644 --- a/docs/subsystems/RuntimeStore.md +++ b/docs/subsystems/RuntimeStore.md @@ -97,6 +97,8 @@ Those are coordinator concerns, not store concerns. Phase 5 names this boundary in code through `RuntimeStateLayerModel`: persisted layer stack data, saved parameter values, and stack presets are classified as base persisted state. Operator/session values may still be backed by the store during migration, but their mutation policy is committed-live policy owned by the coordinator, not durable-store policy by default. +Phase 5 also adds `CommittedLiveStateReadModel` as the named read boundary for current session/operator state. During the conservative migration, `RuntimeStore` still backs that model physically, but snapshot publication consumes the committed-live read model rather than treating render-facing state as raw durable storage. + ### Runtime Configuration Examples: diff --git a/tests/RuntimeSubsystemTests.cpp b/tests/RuntimeSubsystemTests.cpp index 27fd28d..d03061b 100644 --- a/tests/RuntimeSubsystemTests.cpp +++ b/tests/RuntimeSubsystemTests.cpp @@ -9,6 +9,7 @@ #include #include #include +#include #include #include #include @@ -323,6 +324,22 @@ void TestRuntimeCoordinatorPersistenceEvents() Expect(!oscCommitSnapshot.currentValue.numberValues.empty() && oscCommitSnapshot.currentValue.numberValues[0] == 0.2, "settled OSC commit updates the committed session value"); + + CommittedLiveStateReadModel committedLiveState = store.BuildCommittedLiveStateReadModel(); + Expect(!committedLiveState.layers.empty(), "committed live read model exposes current session layers"); + const auto committedLayerIt = std::find_if(committedLiveState.layers.begin(), committedLiveState.layers.end(), + [&oscCommitSnapshot](const RuntimeStore::LayerPersistentState& layer) { return layer.id == oscCommitSnapshot.layerId; }); + Expect(committedLayerIt != committedLiveState.layers.end(), "committed live read model preserves layer identity"); + if (committedLayerIt != committedLiveState.layers.end()) + { + const auto committedValueIt = committedLayerIt->parameterValues.find("gain"); + Expect(committedValueIt != committedLayerIt->parameterValues.end() && + !committedValueIt->second.numberValues.empty() && + committedValueIt->second.numberValues[0] == 0.2, + "committed live read model includes session-only OSC commit value"); + } + Expect(committedLiveState.packagesById.find("alpha") != committedLiveState.packagesById.end(), + "committed live read model carries package definitions for snapshot publication"); } std::filesystem::remove_all(root);