step 2
All checks were successful
CI / React UI Build (push) Successful in 11s
CI / Native Windows Build And Tests (push) Successful in 2m41s
CI / Windows Release Package (push) Successful in 2m46s

This commit is contained in:
Aiden
2026-05-11 19:49:05 +10:00
parent 68503256dc
commit ab38bfad24
7 changed files with 113 additions and 43 deletions

View File

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

View File

@@ -0,0 +1,44 @@
#include "PersistenceWriter.h"
#include <windows.h>
#include <filesystem>
#include <fstream>
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;
}

View File

@@ -0,0 +1,11 @@
#pragma once
#include "PersistenceRequest.h"
#include <string>
class PersistenceWriter
{
public:
bool WriteSnapshot(const PersistenceSnapshot& snapshot, std::string& error) const;
};

View File

@@ -7,7 +7,6 @@
#include <mutex>
#include <random>
#include <sstream>
#include <windows.h>
namespace
{
@@ -99,6 +98,23 @@ std::string RuntimeStore::BuildPersistentStateJson() const
return RuntimeStatePresenter::BuildRuntimeStateJson(*this);
}
PersistenceSnapshot RuntimeStore::BuildRuntimeStatePersistenceSnapshot(const PersistenceRequest& request) const
{
std::lock_guard<std::mutex> 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<std::string> RuntimeStore::GetStackPresetNamesLocked() const
{
std::vector<std::string> presetNames;

View File

@@ -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<std::string> GetStackPresetNamesLocked() const;
void MarkRenderStateDirtyLocked();
void MarkParameterStateDirtyLocked();
RenderSnapshotBuilder mRenderSnapshotBuilder;
PersistenceWriter mPersistenceWriter;
RuntimeConfigStore mConfigStore;
ShaderPackageCatalog mShaderCatalog;
CommittedLiveState mCommittedLiveState;

View File

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

View File

@@ -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<RuntimeEvent> seenEvents;
dispatcher.SubscribeAll([&seenEvents](const RuntimeEvent& event) {