Phase 3 refactor in progress
Some checks failed
CI / React UI Build (push) Successful in 10s
CI / Native Windows Build And Tests (push) Successful in 2m33s
CI / Windows Release Package (push) Has been cancelled

This commit is contained in:
Aiden
2026-05-11 16:48:52 +10:00
parent 0808171677
commit 06f3dd4942
12 changed files with 655 additions and 87 deletions

View File

@@ -1,5 +1,8 @@
#include "LayerStackStore.h"
#include "RuntimeCoordinator.h"
#include "RuntimeEventDispatcher.h"
#include "RuntimeStateJson.h"
#include "RuntimeStore.h"
#include "ShaderPackageCatalog.h"
#include <chrono>
@@ -8,6 +11,8 @@
#include <iostream>
#include <map>
#include <string>
#include <variant>
#include <windows.h>
namespace
{
@@ -44,6 +49,31 @@ void WriteShaderPackage(const std::filesystem::path& root, const std::string& di
WriteFile(packageRoot / "shader.slang", "float4 shadeVideo(float2 uv) { return float4(uv, 0.0, 1.0); }\n");
}
std::filesystem::path GetCurrentDirectoryPath()
{
char buffer[MAX_PATH] = {};
GetCurrentDirectoryA(MAX_PATH, buffer);
return std::filesystem::path(buffer);
}
class ScopedCurrentDirectory
{
public:
explicit ScopedCurrentDirectory(const std::filesystem::path& path) :
mPrevious(GetCurrentDirectoryPath())
{
SetCurrentDirectoryA(path.string().c_str());
}
~ScopedCurrentDirectory()
{
SetCurrentDirectoryA(mPrevious.string().c_str());
}
private:
std::filesystem::path mPrevious;
};
ShaderPackageCatalog BuildCatalog(const std::filesystem::path& root)
{
ShaderPackageCatalog catalog;
@@ -181,6 +211,105 @@ void TestRuntimeStateJsonReadModelSerialization()
const JsonValue* value = parameters->asArray()[0].find("value");
Expect(value && value->asNumber() == 0.8, "serialized parameter includes current value");
}
void TestRuntimeCoordinatorPersistenceEvents()
{
const std::filesystem::path root = MakeTestRoot();
WriteFile(root / "CMakeLists.txt", "cmake_minimum_required(VERSION 3.24)\n");
std::filesystem::create_directories(root / "apps" / "LoopThroughWithOpenGLCompositing");
std::filesystem::create_directories(root / "runtime" / "templates");
WriteShaderPackage(root / "shaders", "alpha", R"({
"id": "alpha",
"name": "Alpha",
"parameters": [{ "id": "gain", "label": "Gain", "type": "float", "default": 0.5, "min": 0, "max": 1 }]
})");
WriteShaderPackage(root / "shaders", "beta", R"({
"id": "beta",
"name": "Beta",
"parameters": [{ "id": "amount", "label": "Amount", "type": "float", "default": 0.25, "min": 0, "max": 1 }]
})");
{
ScopedCurrentDirectory scopedDirectory(root);
RuntimeStore store;
std::string error;
Expect(store.InitializeStore(error), "runtime store initializes in isolated fixture");
Expect(error.empty(), "runtime store initialization has no error");
RuntimeEventDispatcher dispatcher(64);
std::vector<RuntimeEvent> seenEvents;
dispatcher.SubscribeAll([&seenEvents](const RuntimeEvent& event) {
seenEvents.push_back(event);
});
RuntimeCoordinator coordinator(store, dispatcher);
auto dispatchAndClear = [&]() {
dispatcher.DispatchPending();
const std::vector<RuntimeEvent> events = seenEvents;
seenEvents.clear();
return events;
};
auto countEvents = [](const std::vector<RuntimeEvent>& events, RuntimeEventType type) {
return static_cast<std::size_t>(std::count_if(events.begin(), events.end(),
[type](const RuntimeEvent& event) { return event.type == type; }));
};
auto persistenceReason = [](const std::vector<RuntimeEvent>& events) {
for (const RuntimeEvent& event : events)
{
if (event.type != RuntimeEventType::RuntimePersistenceRequested)
continue;
const auto* payload = std::get_if<RuntimePersistenceRequestedEvent>(&event.payload);
return payload ? payload->reason : std::string();
}
return std::string();
};
auto expectAcceptedPersistence = [&](const RuntimeCoordinatorResult& result, const std::string& reason, const char* message) {
const std::vector<RuntimeEvent> events = dispatchAndClear();
Expect(result.accepted, message);
Expect(result.persistenceRequested, "accepted persistent mutation marks coordinator result");
Expect(countEvents(events, RuntimeEventType::RuntimeMutationAccepted) == 1, "persistent mutation publishes accepted fact");
Expect(countEvents(events, RuntimeEventType::RuntimePersistenceRequested) == 1, "persistent mutation publishes persistence request");
Expect(persistenceReason(events) == reason, "persistence request preserves coordinator action reason");
};
std::vector<RuntimeStore::LayerPersistentState> layers = store.CopyLayerStates();
Expect(layers.size() == 1, "isolated fixture starts with a default layer");
const std::string alphaLayerId = layers.empty() ? std::string() : layers[0].id;
expectAcceptedPersistence(coordinator.UpdateLayerParameter(alphaLayerId, "gain", JsonValue(0.75)), "UpdateLayerParameter",
"parameter changes are accepted");
expectAcceptedPersistence(coordinator.AddLayer("beta"), "AddLayer", "stack edits are accepted");
layers = store.CopyLayerStates();
Expect(layers.size() == 2, "stack edit creates a second layer");
const std::string betaLayerId = layers.size() > 1 ? layers[1].id : std::string();
expectAcceptedPersistence(coordinator.MoveLayer(betaLayerId, -1), "MoveLayer", "layer order edits are accepted");
expectAcceptedPersistence(coordinator.SaveStackPreset("Look One"), "SaveStackPreset", "preset save is accepted");
expectAcceptedPersistence(coordinator.LoadStackPreset("Look One"), "LoadStackPreset", "preset load is accepted");
RuntimeCoordinatorResult rejected = coordinator.UpdateLayerParameter(alphaLayerId, "missing", JsonValue(0.5));
std::vector<RuntimeEvent> rejectedEvents = dispatchAndClear();
Expect(!rejected.accepted, "invalid parameter mutation is rejected");
Expect(!rejected.persistenceRequested, "rejected mutation does not mark persistence");
Expect(countEvents(rejectedEvents, RuntimeEventType::RuntimeMutationRejected) == 1, "rejected mutation publishes rejection fact");
Expect(countEvents(rejectedEvents, RuntimeEventType::RuntimePersistenceRequested) == 0, "rejected mutation publishes no persistence request");
OscOverlayEvent overlay;
overlay.routeKey = "alpha\ngain";
overlay.layerKey = "alpha";
overlay.parameterKey = "gain";
Expect(dispatcher.PublishPayload(overlay, "RuntimeLiveState"), "OSC overlay event publishes");
std::vector<RuntimeEvent> overlayEvents = dispatchAndClear();
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");
}
std::filesystem::remove_all(root);
}
}
int main()
@@ -188,6 +317,7 @@ int main()
TestLayerDefaultsAndCrud();
TestMoveClassificationAndPresetLoad();
TestRuntimeStateJsonReadModelSerialization();
TestRuntimeCoordinatorPersistenceEvents();
if (gFailures != 0)
{