From ab38bfad240900f12e2f8046ed0c555a9d357e0b Mon Sep 17 00:00:00 2001 From: Aiden <68633820+awils27@users.noreply.github.com> Date: Mon, 11 May 2026 19:49:05 +1000 Subject: [PATCH] step 2 --- CMakeLists.txt | 3 + .../runtime/persistence/PersistenceWriter.cpp | 44 ++++++++++++ .../runtime/persistence/PersistenceWriter.h | 11 +++ .../runtime/store/RuntimeStore.cpp | 70 +++++++++---------- .../runtime/store/RuntimeStore.h | 6 +- docs/PHASE_6_BACKGROUND_PERSISTENCE_DESIGN.md | 15 ++-- tests/RuntimeSubsystemTests.cpp | 7 ++ 7 files changed, 113 insertions(+), 43 deletions(-) create mode 100644 apps/LoopThroughWithOpenGLCompositing/runtime/persistence/PersistenceWriter.cpp create mode 100644 apps/LoopThroughWithOpenGLCompositing/runtime/persistence/PersistenceWriter.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 0ac36b3..aa688da 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -126,6 +126,8 @@ set(APP_SOURCES "${APP_DIR}/runtime/live/RuntimeLiveState.cpp" "${APP_DIR}/runtime/live/RuntimeLiveState.h" "${APP_DIR}/runtime/persistence/PersistenceRequest.h" + "${APP_DIR}/runtime/persistence/PersistenceWriter.cpp" + "${APP_DIR}/runtime/persistence/PersistenceWriter.h" "${APP_DIR}/runtime/presentation/RuntimeStateJson.cpp" "${APP_DIR}/runtime/presentation/RuntimeStateJson.h" "${APP_DIR}/runtime/presentation/RuntimeStatePresenter.cpp" @@ -347,6 +349,7 @@ add_executable(RuntimeSubsystemTests "${APP_DIR}/runtime/coordination/RuntimeCoordinator.cpp" "${APP_DIR}/runtime/live/CommittedLiveState.cpp" "${APP_DIR}/runtime/snapshot/RenderSnapshotBuilder.cpp" + "${APP_DIR}/runtime/persistence/PersistenceWriter.cpp" "${APP_DIR}/runtime/store/LayerStackStore.cpp" "${APP_DIR}/runtime/store/RuntimeConfigStore.cpp" "${APP_DIR}/runtime/store/RuntimeStore.cpp" diff --git a/apps/LoopThroughWithOpenGLCompositing/runtime/persistence/PersistenceWriter.cpp b/apps/LoopThroughWithOpenGLCompositing/runtime/persistence/PersistenceWriter.cpp new file mode 100644 index 0000000..961d5f0 --- /dev/null +++ b/apps/LoopThroughWithOpenGLCompositing/runtime/persistence/PersistenceWriter.cpp @@ -0,0 +1,44 @@ +#include "PersistenceWriter.h" + +#include + +#include +#include + +bool PersistenceWriter::WriteSnapshot(const PersistenceSnapshot& snapshot, std::string& error) const +{ + if (snapshot.targetPath.empty()) + { + error = "Persistence snapshot target path is empty."; + return false; + } + + std::error_code fsError; + std::filesystem::create_directories(snapshot.targetPath.parent_path(), fsError); + + const std::filesystem::path temporaryPath = snapshot.targetPath.string() + ".tmp"; + std::ofstream output(temporaryPath, std::ios::binary | std::ios::trunc); + if (!output) + { + error = "Could not write file: " + temporaryPath.string(); + return false; + } + + output << snapshot.contents; + output.close(); + if (!output.good()) + { + error = "Could not finish writing file: " + temporaryPath.string(); + return false; + } + + if (!MoveFileExA(temporaryPath.string().c_str(), snapshot.targetPath.string().c_str(), MOVEFILE_REPLACE_EXISTING | MOVEFILE_WRITE_THROUGH)) + { + const DWORD lastError = GetLastError(); + std::filesystem::remove(temporaryPath, fsError); + error = "Could not replace file: " + snapshot.targetPath.string() + " (Win32 error " + std::to_string(lastError) + ")"; + return false; + } + + return true; +} diff --git a/apps/LoopThroughWithOpenGLCompositing/runtime/persistence/PersistenceWriter.h b/apps/LoopThroughWithOpenGLCompositing/runtime/persistence/PersistenceWriter.h new file mode 100644 index 0000000..1e92713 --- /dev/null +++ b/apps/LoopThroughWithOpenGLCompositing/runtime/persistence/PersistenceWriter.h @@ -0,0 +1,11 @@ +#pragma once + +#include "PersistenceRequest.h" + +#include + +class PersistenceWriter +{ +public: + bool WriteSnapshot(const PersistenceSnapshot& snapshot, std::string& error) const; +}; diff --git a/apps/LoopThroughWithOpenGLCompositing/runtime/store/RuntimeStore.cpp b/apps/LoopThroughWithOpenGLCompositing/runtime/store/RuntimeStore.cpp index 720968d..ffb62a3 100644 --- a/apps/LoopThroughWithOpenGLCompositing/runtime/store/RuntimeStore.cpp +++ b/apps/LoopThroughWithOpenGLCompositing/runtime/store/RuntimeStore.cpp @@ -7,7 +7,6 @@ #include #include #include -#include namespace { @@ -99,6 +98,23 @@ std::string RuntimeStore::BuildPersistentStateJson() const return RuntimeStatePresenter::BuildRuntimeStateJson(*this); } +PersistenceSnapshot RuntimeStore::BuildRuntimeStatePersistenceSnapshot(const PersistenceRequest& request) const +{ + std::lock_guard lock(mMutex); + return BuildRuntimeStatePersistenceSnapshotLocked(request); +} + +PersistenceSnapshot RuntimeStore::BuildRuntimeStatePersistenceSnapshotLocked(const PersistenceRequest& request) const +{ + PersistenceSnapshot snapshot; + snapshot.targetKind = PersistenceTargetKind::RuntimeState; + snapshot.targetPath = mConfigStore.GetRuntimeStatePath(); + snapshot.contents = SerializeJson(mCommittedLiveState.BuildPersistentStateValue(mShaderCatalog), true); + snapshot.reason = request.reason; + snapshot.generation = request.sequence; + return snapshot; +} + bool RuntimeStore::PollStoredFileChanges(bool& registryChanged, bool& reloadRequested, std::string& error) { try @@ -270,10 +286,7 @@ bool RuntimeStore::SaveStackPresetSnapshot(const std::string& presetName, std::s return false; } - JsonValue root = JsonValue::MakeObject(); - root = mCommittedLiveState.BuildStackPresetValue(mShaderCatalog, presetName); - - return WriteTextFile(mConfigStore.GetPresetRoot() / (safeStem + ".json"), SerializeJson(root, true), error); + return mPersistenceWriter.WriteSnapshot(BuildStackPresetPersistenceSnapshot(presetName), error); } bool RuntimeStore::LoadStackPresetSnapshot(const std::string& presetName, std::string& error) @@ -465,7 +478,20 @@ bool RuntimeStore::LoadPersistentState(std::string& error) bool RuntimeStore::SavePersistentState(std::string& error) const { - return WriteTextFile(mConfigStore.GetRuntimeStatePath(), SerializeJson(mCommittedLiveState.BuildPersistentStateValue(mShaderCatalog), true), error); + return mPersistenceWriter.WriteSnapshot(BuildRuntimeStatePersistenceSnapshotLocked(PersistenceRequest::RuntimeStateRequest("SavePersistentState")), error); +} + +PersistenceSnapshot RuntimeStore::BuildStackPresetPersistenceSnapshot(const std::string& presetName) const +{ + const std::string safeStem = LayerStackStore::MakeSafePresetFileStem(presetName); + + PersistenceSnapshot snapshot; + snapshot.targetKind = PersistenceTargetKind::StackPreset; + snapshot.targetPath = mConfigStore.GetPresetRoot() / (safeStem + ".json"); + snapshot.contents = SerializeJson(mCommittedLiveState.BuildStackPresetValue(mShaderCatalog, presetName), true); + snapshot.reason = "SaveStackPreset"; + snapshot.generation = 0; + return snapshot; } bool RuntimeStore::ScanShaderPackages(std::string& error) @@ -493,38 +519,6 @@ std::string RuntimeStore::ReadTextFile(const std::filesystem::path& path, std::s return buffer.str(); } -bool RuntimeStore::WriteTextFile(const std::filesystem::path& path, const std::string& contents, std::string& error) const -{ - std::error_code fsError; - std::filesystem::create_directories(path.parent_path(), fsError); - - const std::filesystem::path temporaryPath = path.string() + ".tmp"; - std::ofstream output(temporaryPath, std::ios::binary | std::ios::trunc); - if (!output) - { - error = "Could not write file: " + temporaryPath.string(); - return false; - } - - output << contents; - output.close(); - if (!output.good()) - { - error = "Could not finish writing file: " + temporaryPath.string(); - return false; - } - - if (!MoveFileExA(temporaryPath.string().c_str(), path.string().c_str(), MOVEFILE_REPLACE_EXISTING | MOVEFILE_WRITE_THROUGH)) - { - const DWORD lastError = GetLastError(); - std::filesystem::remove(temporaryPath, fsError); - error = "Could not replace file: " + path.string() + " (Win32 error " + std::to_string(lastError) + ")"; - return false; - } - - return true; -} - std::vector RuntimeStore::GetStackPresetNamesLocked() const { std::vector presetNames; diff --git a/apps/LoopThroughWithOpenGLCompositing/runtime/store/RuntimeStore.h b/apps/LoopThroughWithOpenGLCompositing/runtime/store/RuntimeStore.h index 36eab92..e95a0fb 100644 --- a/apps/LoopThroughWithOpenGLCompositing/runtime/store/RuntimeStore.h +++ b/apps/LoopThroughWithOpenGLCompositing/runtime/store/RuntimeStore.h @@ -3,6 +3,7 @@ #include "HealthTelemetry.h" #include "CommittedLiveState.h" #include "LayerStackStore.h" +#include "PersistenceWriter.h" #include "RenderSnapshotBuilder.h" #include "RuntimeConfigStore.h" #include "RuntimeJson.h" @@ -30,6 +31,7 @@ public: bool InitializeStore(std::string& error); std::string BuildPersistentStateJson() const; + PersistenceSnapshot BuildRuntimeStatePersistenceSnapshot(const PersistenceRequest& request) const; bool PollStoredFileChanges(bool& registryChanged, bool& reloadRequested, std::string& error); bool CreateStoredLayer(const std::string& shaderId, std::string& error); @@ -82,14 +84,16 @@ 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); std::string ReadTextFile(const std::filesystem::path& path, std::string& error) const; - bool WriteTextFile(const std::filesystem::path& path, const std::string& contents, std::string& error) const; std::vector GetStackPresetNamesLocked() const; void MarkRenderStateDirtyLocked(); void MarkParameterStateDirtyLocked(); RenderSnapshotBuilder mRenderSnapshotBuilder; + PersistenceWriter mPersistenceWriter; RuntimeConfigStore mConfigStore; ShaderPackageCatalog mShaderCatalog; CommittedLiveState mCommittedLiveState; diff --git a/docs/PHASE_6_BACKGROUND_PERSISTENCE_DESIGN.md b/docs/PHASE_6_BACKGROUND_PERSISTENCE_DESIGN.md index efcfd11..a021da4 100644 --- a/docs/PHASE_6_BACKGROUND_PERSISTENCE_DESIGN.md +++ b/docs/PHASE_6_BACKGROUND_PERSISTENCE_DESIGN.md @@ -7,7 +7,7 @@ Phases 1-5 separate durable state, coordination policy, render-facing snapshots, ## Status - Phase 6 design package: proposed. -- Phase 6 implementation: Step 1 complete. +- Phase 6 implementation: Step 2 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. Current persistence footholds: @@ -201,9 +201,16 @@ Move file-write mechanics behind a helper while keeping serialization ownership Initial target: -- `RuntimeStore` can build serialized runtime-state snapshots -- `PersistenceWriter` writes the snapshot -- existing synchronous save path can call through the writer/helper during transition +- [x] `RuntimeStore` can build serialized runtime-state snapshots +- [x] `PersistenceWriter` writes the snapshot +- [x] existing synchronous save path can call through the writer/helper during transition + +Current implementation: + +- `RuntimeStore::BuildRuntimeStatePersistenceSnapshot(...)` captures serialized runtime-state content and target path. +- `PersistenceWriter::WriteSnapshot(...)` owns the temp-file and replace write mechanics. +- `RuntimeStore::SavePersistentState(...)` still behaves synchronously, but now writes through `PersistenceWriter`. +- Stack preset saves also use `PersistenceWriter` synchronously; preset async policy remains a later decision. ### Step 3. Add Debounced Background Worker diff --git a/tests/RuntimeSubsystemTests.cpp b/tests/RuntimeSubsystemTests.cpp index 3cc95bd..15bdc87 100644 --- a/tests/RuntimeSubsystemTests.cpp +++ b/tests/RuntimeSubsystemTests.cpp @@ -236,6 +236,13 @@ void TestRuntimeCoordinatorPersistenceEvents() Expect(store.InitializeStore(error), "runtime store initializes in isolated fixture"); Expect(error.empty(), "runtime store initialization has no error"); + const PersistenceRequest snapshotRequest = PersistenceRequest::RuntimeStateRequest("unit-test"); + const PersistenceSnapshot snapshot = store.BuildRuntimeStatePersistenceSnapshot(snapshotRequest); + Expect(snapshot.targetKind == PersistenceTargetKind::RuntimeState, "runtime store builds a runtime-state persistence snapshot"); + 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"); + RuntimeEventDispatcher dispatcher(64); std::vector seenEvents; dispatcher.SubscribeAll([&seenEvents](const RuntimeEvent& event) {