diff --git a/apps/LoopThroughWithOpenGLCompositing/gl/frame/RuntimeUpdateController.cpp b/apps/LoopThroughWithOpenGLCompositing/gl/frame/RuntimeUpdateController.cpp index 6ab870e..b8a2c45 100644 --- a/apps/LoopThroughWithOpenGLCompositing/gl/frame/RuntimeUpdateController.cpp +++ b/apps/LoopThroughWithOpenGLCompositing/gl/frame/RuntimeUpdateController.cpp @@ -48,6 +48,9 @@ RuntimeUpdateController::RuntimeUpdateController( mRuntimeEventDispatcher.Subscribe( RuntimeEventType::RuntimeReloadRequested, [this](const RuntimeEvent& event) { HandleRuntimeReloadRequested(event); }); + mRuntimeEventDispatcher.Subscribe( + RuntimeEventType::RuntimePersistenceRequested, + [this](const RuntimeEvent& event) { HandleRuntimePersistenceRequested(event); }); mRuntimeEventDispatcher.Subscribe( RuntimeEventType::ShaderBuildRequested, [this](const RuntimeEvent& event) { HandleShaderBuildRequested(event); }); @@ -158,6 +161,16 @@ void RuntimeUpdateController::HandleRuntimeReloadRequested(const RuntimeEvent& e mRuntimeStore.ClearReloadRequest(); } +void RuntimeUpdateController::HandleRuntimePersistenceRequested(const RuntimeEvent& event) +{ + const RuntimePersistenceRequestedEvent* payload = std::get_if(&event.payload); + if (!payload) + return; + + std::string error; + mRuntimeStore.RequestPersistence(payload->request, error); +} + void RuntimeUpdateController::HandleShaderBuildRequested(const RuntimeEvent& event) { const ShaderBuildEvent* payload = std::get_if(&event.payload); diff --git a/apps/LoopThroughWithOpenGLCompositing/gl/frame/RuntimeUpdateController.h b/apps/LoopThroughWithOpenGLCompositing/gl/frame/RuntimeUpdateController.h index e8be6bb..69428a4 100644 --- a/apps/LoopThroughWithOpenGLCompositing/gl/frame/RuntimeUpdateController.h +++ b/apps/LoopThroughWithOpenGLCompositing/gl/frame/RuntimeUpdateController.h @@ -36,6 +36,7 @@ public: private: void HandleRuntimeStateBroadcastRequested(const RuntimeEvent& event); void HandleRuntimeReloadRequested(const RuntimeEvent& event); + void HandleRuntimePersistenceRequested(const RuntimeEvent& event); void HandleShaderBuildRequested(const RuntimeEvent& event); void HandleShaderBuildPrepared(const RuntimeEvent& event); void HandleShaderBuildFailed(const RuntimeEvent& event); diff --git a/apps/LoopThroughWithOpenGLCompositing/runtime/store/RuntimeStore.cpp b/apps/LoopThroughWithOpenGLCompositing/runtime/store/RuntimeStore.cpp index bc4a730..39abd35 100644 --- a/apps/LoopThroughWithOpenGLCompositing/runtime/store/RuntimeStore.cpp +++ b/apps/LoopThroughWithOpenGLCompositing/runtime/store/RuntimeStore.cpp @@ -43,7 +43,6 @@ std::string PersistenceTargetKindName(PersistenceTargetKind targetKind) RuntimeStore::RuntimeStore() : mRenderSnapshotBuilder(*this), - mHealthTelemetry(), mReloadRequested(false), mCompileSucceeded(false), mStartupRandom(GenerateStartupRandom()), @@ -128,6 +127,35 @@ PersistenceSnapshot RuntimeStore::BuildRuntimeStatePersistenceSnapshot(const Per return BuildRuntimeStatePersistenceSnapshotLocked(request); } +bool RuntimeStore::RequestPersistence(const PersistenceRequest& request, std::string& error) +{ + if (request.targetKind != PersistenceTargetKind::RuntimeState) + { + error = "Unsupported persistence request target: " + PersistenceTargetKindName(request.targetKind); + mHealthTelemetry.RecordPersistenceWriteResult( + false, + PersistenceTargetKindName(request.targetKind), + std::string(), + request.reason, + error, + false); + return false; + } + + const PersistenceSnapshot snapshot = BuildRuntimeStatePersistenceSnapshot(request); + if (mPersistenceWriter.EnqueueSnapshot(snapshot, error)) + return true; + + mHealthTelemetry.RecordPersistenceWriteResult( + false, + PersistenceTargetKindName(request.targetKind), + snapshot.targetPath.string(), + request.reason, + error, + false); + return false; +} + PersistenceSnapshot RuntimeStore::BuildRuntimeStatePersistenceSnapshotLocked(const PersistenceRequest& request) const { PersistenceSnapshot snapshot; @@ -211,7 +239,7 @@ bool RuntimeStore::CreateStoredLayer(const std::string& shaderId, std::string& e mReloadRequested = true; MarkRenderStateDirtyLocked(); - return SavePersistentState(error); + return true; } bool RuntimeStore::DeleteStoredLayer(const std::string& layerId, std::string& error) @@ -222,7 +250,7 @@ bool RuntimeStore::DeleteStoredLayer(const std::string& layerId, std::string& er mReloadRequested = true; MarkRenderStateDirtyLocked(); - return SavePersistentState(error); + return true; } bool RuntimeStore::MoveStoredLayer(const std::string& layerId, int direction, std::string& error) @@ -239,7 +267,7 @@ bool RuntimeStore::MoveStoredLayer(const std::string& layerId, int direction, st mReloadRequested = true; MarkRenderStateDirtyLocked(); - return SavePersistentState(error); + return true; } bool RuntimeStore::MoveStoredLayerToIndex(const std::string& layerId, std::size_t targetIndex, std::string& error) @@ -256,7 +284,7 @@ bool RuntimeStore::MoveStoredLayerToIndex(const std::string& layerId, std::size_ mReloadRequested = true; MarkRenderStateDirtyLocked(); - return SavePersistentState(error); + return true; } bool RuntimeStore::SetStoredLayerBypassState(const std::string& layerId, bool bypassed, std::string& error) @@ -267,7 +295,7 @@ bool RuntimeStore::SetStoredLayerBypassState(const std::string& layerId, bool by mReloadRequested = true; MarkParameterStateDirtyLocked(); - return SavePersistentState(error); + return true; } bool RuntimeStore::SetStoredLayerShaderSelection(const std::string& layerId, const std::string& shaderId, std::string& error) @@ -278,18 +306,19 @@ bool RuntimeStore::SetStoredLayerShaderSelection(const std::string& layerId, con mReloadRequested = true; MarkRenderStateDirtyLocked(); - return SavePersistentState(error); + return true; } bool RuntimeStore::SetStoredParameterValue(const std::string& layerId, const std::string& parameterId, const ShaderParameterValue& value, bool persistState, std::string& error) { + (void)persistState; std::lock_guard lock(mMutex); if (!mCommittedLiveState.SetParameterValue(layerId, parameterId, value, error)) return false; MarkParameterStateDirtyLocked(); - return !persistState || SavePersistentState(error); + return true; } bool RuntimeStore::ResetStoredLayerParameterValues(const std::string& layerId, std::string& error) @@ -300,7 +329,7 @@ bool RuntimeStore::ResetStoredLayerParameterValues(const std::string& layerId, s return false; MarkParameterStateDirtyLocked(); - return SavePersistentState(error); + return true; } bool RuntimeStore::SaveStackPresetSnapshot(const std::string& presetName, std::string& error) const @@ -340,7 +369,7 @@ bool RuntimeStore::LoadStackPresetSnapshot(const std::string& presetName, std::s mReloadRequested = true; MarkRenderStateDirtyLocked(); - return SavePersistentState(error); + return true; } bool RuntimeStore::HasStoredLayer(const std::string& layerId) const @@ -503,11 +532,6 @@ bool RuntimeStore::LoadPersistentState(std::string& error) return mCommittedLiveState.LoadPersistentStateValue(root); } -bool RuntimeStore::SavePersistentState(std::string& error) const -{ - return mPersistenceWriter.EnqueueSnapshot(BuildRuntimeStatePersistenceSnapshotLocked(PersistenceRequest::RuntimeStateRequest("SavePersistentState")), error); -} - PersistenceSnapshot RuntimeStore::BuildStackPresetPersistenceSnapshot(const std::string& presetName) const { const std::string safeStem = LayerStackStore::MakeSafePresetFileStem(presetName); diff --git a/apps/LoopThroughWithOpenGLCompositing/runtime/store/RuntimeStore.h b/apps/LoopThroughWithOpenGLCompositing/runtime/store/RuntimeStore.h index 477366e..ffc0351 100644 --- a/apps/LoopThroughWithOpenGLCompositing/runtime/store/RuntimeStore.h +++ b/apps/LoopThroughWithOpenGLCompositing/runtime/store/RuntimeStore.h @@ -32,6 +32,7 @@ public: bool InitializeStore(std::string& error); std::string BuildPersistentStateJson() const; PersistenceSnapshot BuildRuntimeStatePersistenceSnapshot(const PersistenceRequest& request) const; + bool RequestPersistence(const PersistenceRequest& request, std::string& error); bool PollStoredFileChanges(bool& registryChanged, bool& reloadRequested, std::string& error); bool CreateStoredLayer(const std::string& shaderId, std::string& error); @@ -83,7 +84,6 @@ public: private: bool LoadPersistentState(std::string& error); - bool SavePersistentState(std::string& error) const; PersistenceSnapshot BuildRuntimeStatePersistenceSnapshotLocked(const PersistenceRequest& request) const; PersistenceSnapshot BuildStackPresetPersistenceSnapshot(const std::string& presetName) const; bool ScanShaderPackages(std::string& error); @@ -93,11 +93,11 @@ private: void MarkParameterStateDirtyLocked(); RenderSnapshotBuilder mRenderSnapshotBuilder; - mutable PersistenceWriter mPersistenceWriter; RuntimeConfigStore mConfigStore; ShaderPackageCatalog mShaderCatalog; CommittedLiveState mCommittedLiveState; HealthTelemetry mHealthTelemetry; + mutable PersistenceWriter mPersistenceWriter; mutable std::mutex mMutex; bool mReloadRequested; bool mCompileSucceeded; diff --git a/docs/PHASE_6_BACKGROUND_PERSISTENCE_DESIGN.md b/docs/PHASE_6_BACKGROUND_PERSISTENCE_DESIGN.md index f3fa1c8..f4c926d 100644 --- a/docs/PHASE_6_BACKGROUND_PERSISTENCE_DESIGN.md +++ b/docs/PHASE_6_BACKGROUND_PERSISTENCE_DESIGN.md @@ -7,8 +7,8 @@ Phases 1-5 separate durable state, coordination policy, render-facing snapshots, ## Status - Phase 6 design package: proposed. -- Phase 6 implementation: Step 4 complete. -- Current alignment: `RuntimeStore` owns durable serialization, config, package metadata, preset IO, and persistence requests; `CommittedLiveState` owns the current committed/session layer state; and `RuntimeCoordinator` publishes typed persistence requests for persisted mutations. The remaining issue is that actual disk writes are still synchronous store work rather than queued, debounced, atomic background writes. +- Phase 6 implementation: Step 5 complete. +- Current alignment: `RuntimeStore` owns durable serialization, config, package metadata, preset IO, and persistence request execution; `CommittedLiveState` owns the current committed/session layer state; and `RuntimeCoordinator` publishes typed persistence requests for persisted mutations. Runtime-state persistence is now requested through the coordinator/event path and executed by the background writer. Current persistence footholds: @@ -252,9 +252,17 @@ Route `RuntimePersistenceRequested` or coordinator persistence outcomes into the Initial target: -- accepted durable mutations request persistence -- transient-only mutations do not -- runtime reload/preset policies remain explicit +- [x] accepted durable mutations request persistence +- [x] transient-only mutations do not +- [x] runtime reload/preset policies remain explicit + +Current implementation: + +- Store mutation methods update committed durable/session state and mark render state dirty, but no longer enqueue runtime-state writes directly. +- `RuntimeCoordinator` remains the owner of the persistence decision and publishes `RuntimePersistenceRequested` only for accepted durable mutations. +- `RuntimeUpdateController` handles `RuntimePersistenceRequested` and calls `RuntimeStore::RequestPersistence(...)`. +- `RuntimeStore::RequestPersistence(...)` validates the request target, builds the runtime-state snapshot, enqueues it on `PersistenceWriter`, and records enqueue failures in `HealthTelemetry`. +- Stack preset save remains a synchronous preset-file write; preset load updates state and relies on the coordinator persistence request for runtime-state persistence. ### Step 6. Define Shutdown Flush @@ -307,14 +315,14 @@ Operator-triggered preset save often feels like it should complete before report Phase 6 can be considered complete once the project can say: -- [ ] durable mutations enqueue persistence instead of directly writing from mutation paths -- [ ] runtime-state writes are debounced/coalesced -- [ ] writes use temp-file/replace or equivalent atomic policy -- [ ] persistence failures are reported through structured health/events -- [ ] transient/live-only mutations do not request persistence +- [x] durable mutations enqueue persistence instead of directly writing from mutation paths +- [x] runtime-state writes are debounced/coalesced +- [x] writes use temp-file/replace or equivalent atomic policy +- [x] persistence failures are reported through structured health/events +- [x] transient/live-only mutations do not request persistence - [ ] shutdown flush behavior is explicit and tested -- [ ] `RuntimeStore` remains durable-state/serialization owner, not worker policy owner -- [ ] persistence behavior has focused non-render tests +- [x] `RuntimeStore` remains durable-state/serialization owner, not worker policy owner +- [x] persistence behavior has focused non-render tests ## Open Questions diff --git a/tests/RuntimeSubsystemTests.cpp b/tests/RuntimeSubsystemTests.cpp index 15bdc87..d5fc3b5 100644 --- a/tests/RuntimeSubsystemTests.cpp +++ b/tests/RuntimeSubsystemTests.cpp @@ -242,6 +242,12 @@ void TestRuntimeCoordinatorPersistenceEvents() Expect(snapshot.reason == "unit-test", "runtime-state persistence snapshot preserves request reason"); Expect(snapshot.targetPath.filename().string() == "runtime_state.json", "runtime-state persistence snapshot targets the runtime state file"); Expect(snapshot.contents.find("\"layers\"") != std::string::npos, "runtime-state persistence snapshot contains serialized layer state"); + Expect(store.RequestPersistence(PersistenceRequest::RuntimeStateRequest("unit-test-request"), error), + "runtime store accepts runtime-state persistence requests"); + PersistenceRequest unsupportedRequest; + unsupportedRequest.targetKind = PersistenceTargetKind::StackPreset; + unsupportedRequest.reason = "unsupported-unit-test"; + Expect(!store.RequestPersistence(unsupportedRequest, error), "runtime store rejects unsupported persistence request targets"); RuntimeEventDispatcher dispatcher(64); std::vector seenEvents;