From 5c46eaf18ac85be0f0862113694fca3f65a0b05e Mon Sep 17 00:00:00 2001 From: Aiden Date: Thu, 21 May 2026 16:51:00 +1000 Subject: [PATCH] More changes --- src/app/RuntimeLayerController.cpp | 5 + src/app/RuntimeLayerController.h | 7 + src/app/RuntimeLayerControllerBuild.cpp | 26 ++- src/app/RuntimeLayerControllerControls.cpp | 10 + src/runtime/RuntimeLayerModel.cpp | 3 + src/runtime/RuntimeStatePersistence.cpp | 206 ++++++++++++++++++ src/runtime/RuntimeStatePersistence.h | 43 ++++ tests/CMakeLists.txt | 7 + ...adenceCompositorRuntimeLayerModelTests.cpp | 43 +++- tests/RuntimeStatePersistenceTests.cpp | 157 +++++++++++++ 10 files changed, 505 insertions(+), 2 deletions(-) create mode 100644 src/runtime/RuntimeStatePersistence.cpp create mode 100644 src/runtime/RuntimeStatePersistence.h create mode 100644 tests/RuntimeStatePersistenceTests.cpp diff --git a/src/app/RuntimeLayerController.cpp b/src/app/RuntimeLayerController.cpp index 178a471..9397838 100644 --- a/src/app/RuntimeLayerController.cpp +++ b/src/app/RuntimeLayerController.cpp @@ -2,6 +2,8 @@ #include "../logging/Logger.h" +#include + namespace RenderCadenceCompositor { RuntimeLayerController::RuntimeLayerController(RenderLayerPublisher publisher) : @@ -23,6 +25,8 @@ void RuntimeLayerController::Initialize(const std::string& shaderLibrary, unsign { mShaderLibrary = shaderLibrary; mMaxTemporalHistoryFrames = maxTemporalHistoryFrames; + mRuntimeStatePath = ResolveRuntimeStatePath(); + mPersistenceWriter.Start(mRuntimeStatePath, std::chrono::milliseconds(250)); LoadSupportedShaderCatalog(shaderLibrary, maxTemporalHistoryFrames); InitializeLayerModel(runtimeShaderId); } @@ -57,6 +61,7 @@ void RuntimeLayerController::StartStartupBuild(const std::string& runtimeShaderI void RuntimeLayerController::Stop() { + mPersistenceWriter.Stop(true); StopAllRuntimeShaderBuilds(); } diff --git a/src/app/RuntimeLayerController.h b/src/app/RuntimeLayerController.h index fd03fb8..069af90 100644 --- a/src/app/RuntimeLayerController.h +++ b/src/app/RuntimeLayerController.h @@ -3,11 +3,13 @@ #include "../control/ControlActionResult.h" #include "../control/RuntimeControlCommand.h" #include "../runtime/RuntimeLayerModel.h" +#include "../runtime/RuntimeStatePersistence.h" #include "../runtime/RuntimeShaderBridge.h" #include "../runtime/SupportedShaderCatalog.h" #include "../telemetry/CadenceTelemetry.h" #include +#include #include #include #include @@ -42,6 +44,9 @@ private: bool LoadSupportedShaderCatalog(const std::string& shaderLibrary, unsigned maxTemporalHistoryFrames); void InitializeLayerModel(std::string& runtimeShaderId); bool InitializeLayerModelFromRuntimeState(); + void RequestRuntimeStatePersistence(); + void RequestRuntimeStatePersistenceLocked(); + std::filesystem::path ResolveRuntimeStatePath() const; void StartLayerShaderBuild(const std::string& layerId, const std::string& shaderId); void RetireLayerShaderBuild(const std::string& layerId); void CleanupRetiredShaderBuilds(); @@ -56,6 +61,8 @@ private: RenderLayerPublisher mPublisher; std::string mShaderLibrary; unsigned mMaxTemporalHistoryFrames = 0; + std::filesystem::path mRuntimeStatePath; + RuntimeStatePersistenceWriter mPersistenceWriter; SupportedShaderCatalog mShaderCatalog; mutable std::mutex mRuntimeLayerMutex; RuntimeLayerModel mRuntimeLayerModel; diff --git a/src/app/RuntimeLayerControllerBuild.cpp b/src/app/RuntimeLayerControllerBuild.cpp index 52be42a..8826f0a 100644 --- a/src/app/RuntimeLayerControllerBuild.cpp +++ b/src/app/RuntimeLayerControllerBuild.cpp @@ -36,6 +36,9 @@ void RuntimeLayerController::InitializeLayerModel(std::string& runtimeShaderId) if (InitializeLayerModelFromRuntimeState()) return; + if (!runtimeShaderId.empty()) + Log("runtime-state", "Falling back to configured runtime shader '" + runtimeShaderId + "'."); + std::lock_guard lock(mRuntimeLayerMutex); std::string error; if (!mRuntimeLayerModel.InitializeSingleLayer(mShaderCatalog, runtimeShaderId, error)) @@ -48,9 +51,11 @@ void RuntimeLayerController::InitializeLayerModel(std::string& runtimeShaderId) bool RuntimeLayerController::InitializeLayerModelFromRuntimeState() { - const std::filesystem::path runtimeStatePath = FindRepoPath("runtime/runtime_state.json"); + const std::filesystem::path runtimeStatePath = mRuntimeStatePath.empty() ? ResolveRuntimeStatePath() : mRuntimeStatePath; if (runtimeStatePath.empty()) return false; + if (!std::filesystem::exists(runtimeStatePath)) + return false; std::ifstream input(runtimeStatePath, std::ios::binary); if (!input) @@ -83,6 +88,25 @@ bool RuntimeLayerController::InitializeLayerModelFromRuntimeState() return true; } +std::filesystem::path RuntimeLayerController::ResolveRuntimeStatePath() const +{ + const std::filesystem::path runtimeDirectory = FindRepoPath("runtime"); + if (!runtimeDirectory.empty()) + return runtimeDirectory / "runtime_state.json"; + return std::filesystem::current_path() / "runtime" / "runtime_state.json"; +} + +void RuntimeLayerController::RequestRuntimeStatePersistence() +{ + std::lock_guard lock(mRuntimeLayerMutex); + RequestRuntimeStatePersistenceLocked(); +} + +void RuntimeLayerController::RequestRuntimeStatePersistenceLocked() +{ + mPersistenceWriter.RequestSave(mRuntimeLayerModel.Snapshot()); +} + void RuntimeLayerController::StartLayerShaderBuild(const std::string& layerId, const std::string& shaderId) { CleanupRetiredShaderBuilds(); diff --git a/src/app/RuntimeLayerControllerControls.cpp b/src/app/RuntimeLayerControllerControls.cpp index a32359f..4c93c9f 100644 --- a/src/app/RuntimeLayerControllerControls.cpp +++ b/src/app/RuntimeLayerControllerControls.cpp @@ -19,6 +19,7 @@ ControlActionResult RuntimeLayerController::HandleAddLayer(const std::string& bo std::lock_guard lock(mRuntimeLayerMutex); if (!mRuntimeLayerModel.AddLayer(mShaderCatalog, shaderId, layerId, error)) return { false, error }; + RequestRuntimeStatePersistenceLocked(); } Log("runtime-shader", "Layer added: " + layerId + " shader=" + shaderId); @@ -39,6 +40,7 @@ ControlActionResult RuntimeLayerController::HandleRemoveLayer(const std::string& std::lock_guard lock(mRuntimeLayerMutex); if (!mRuntimeLayerModel.RemoveLayer(layerId, error)) return { false, error }; + RequestRuntimeStatePersistenceLocked(); } Log("runtime-shader", "Layer removed: " + layerId); @@ -61,6 +63,7 @@ ControlActionResult RuntimeLayerController::HandleControlCommand(const RuntimeCo std::lock_guard lock(mRuntimeLayerMutex); if (!mRuntimeLayerModel.AddLayer(mShaderCatalog, command.shaderId, layerId, error)) return { false, error }; + RequestRuntimeStatePersistenceLocked(); } Log("runtime-shader", "Layer added: " + layerId + " shader=" + command.shaderId); StartLayerShaderBuild(layerId, command.shaderId); @@ -72,6 +75,7 @@ ControlActionResult RuntimeLayerController::HandleControlCommand(const RuntimeCo std::lock_guard lock(mRuntimeLayerMutex); if (!mRuntimeLayerModel.RemoveLayer(command.layerId, error)) return { false, error }; + RequestRuntimeStatePersistenceLocked(); } Log("runtime-shader", "Layer removed: " + command.layerId); RetireLayerShaderBuild(command.layerId); @@ -84,6 +88,7 @@ ControlActionResult RuntimeLayerController::HandleControlCommand(const RuntimeCo std::lock_guard lock(mRuntimeLayerMutex); if (!mRuntimeLayerModel.ReorderLayer(command.layerId, command.targetIndex, error)) return { false, error }; + RequestRuntimeStatePersistenceLocked(); } PublishRuntimeRenderLayers(); return { true, std::string() }; @@ -94,6 +99,7 @@ ControlActionResult RuntimeLayerController::HandleControlCommand(const RuntimeCo std::lock_guard lock(mRuntimeLayerMutex); if (!mRuntimeLayerModel.SetLayerBypass(command.layerId, command.bypass, error)) return { false, error }; + RequestRuntimeStatePersistenceLocked(); } PublishRuntimeRenderLayers(); return { true, std::string() }; @@ -104,6 +110,7 @@ ControlActionResult RuntimeLayerController::HandleControlCommand(const RuntimeCo std::lock_guard lock(mRuntimeLayerMutex); if (!mRuntimeLayerModel.SetLayerShader(mShaderCatalog, command.layerId, command.shaderId, error)) return { false, error }; + RequestRuntimeStatePersistenceLocked(); } Log("runtime-shader", "Layer shader changed: " + command.layerId + " shader=" + command.shaderId); StartLayerShaderBuild(command.layerId, command.shaderId); @@ -115,6 +122,7 @@ ControlActionResult RuntimeLayerController::HandleControlCommand(const RuntimeCo std::lock_guard lock(mRuntimeLayerMutex); if (!mRuntimeLayerModel.UpdateParameter(command.layerId, command.parameterId, command.value, error)) return { false, error }; + RequestRuntimeStatePersistenceLocked(); } PublishRuntimeRenderLayers(); return { true, std::string() }; @@ -125,6 +133,7 @@ ControlActionResult RuntimeLayerController::HandleControlCommand(const RuntimeCo std::lock_guard lock(mRuntimeLayerMutex); if (!mRuntimeLayerModel.ResetParameters(command.layerId, error)) return { false, error }; + RequestRuntimeStatePersistenceLocked(); } PublishRuntimeRenderLayers(); return { true, std::string() }; @@ -139,6 +148,7 @@ ControlActionResult RuntimeLayerController::HandleControlCommand(const RuntimeCo std::lock_guard lock(mRuntimeLayerMutex); if (!mRuntimeLayerModel.ReloadFromCatalog(mShaderCatalog, buildsToStart, error)) return { false, error }; + RequestRuntimeStatePersistenceLocked(); } for (const auto& build : buildsToStart) diff --git a/src/runtime/RuntimeLayerModel.cpp b/src/runtime/RuntimeLayerModel.cpp index 1a4a50a..9c198de 100644 --- a/src/runtime/RuntimeLayerModel.cpp +++ b/src/runtime/RuntimeLayerModel.cpp @@ -359,7 +359,10 @@ bool RuntimeLayerModel::ReloadFromCatalog(const SupportedShaderCatalog& shaderCa const std::string nextFingerprint = ShaderPackageFingerprint(*shaderPackage); if (layer.packageFingerprint == nextFingerprint) + { + buildsToStart.push_back({ layer.id, layer.shaderId }); continue; + } std::map previousDefinitions; for (const ShaderParameterDefinition& definition : layer.parameterDefinitions) diff --git a/src/runtime/RuntimeStatePersistence.cpp b/src/runtime/RuntimeStatePersistence.cpp new file mode 100644 index 0000000..944eefa --- /dev/null +++ b/src/runtime/RuntimeStatePersistence.cpp @@ -0,0 +1,206 @@ +#include "RuntimeStatePersistence.h" + +#include "../logging/Logger.h" + +#include + +namespace RenderCadenceCompositor +{ +namespace +{ +JsonValue ParameterValueToJson(const ShaderParameterDefinition& definition, const ShaderParameterValue& value) +{ + switch (definition.type) + { + case ShaderParameterType::Float: + return JsonValue(value.numberValues.empty() ? 0.0 : value.numberValues.front()); + case ShaderParameterType::Vec2: + case ShaderParameterType::Color: + { + JsonValue array = JsonValue::MakeArray(); + for (double number : value.numberValues) + array.pushBack(JsonValue(number)); + return array; + } + case ShaderParameterType::Boolean: + return JsonValue(value.booleanValue); + case ShaderParameterType::Enum: + return JsonValue(value.enumValue); + case ShaderParameterType::Text: + return JsonValue(value.textValue); + case ShaderParameterType::Trigger: + return JsonValue(value.numberValues.empty() ? 0.0 : value.numberValues.front()); + } + return JsonValue(); +} +} + +JsonValue RuntimeStateSnapshotToJson(const RuntimeLayerModelSnapshot& snapshot) +{ + JsonValue root = JsonValue::MakeObject(); + JsonValue layers = JsonValue::MakeArray(); + + for (const RuntimeLayerReadModel& layer : snapshot.displayLayers) + { + JsonValue layerJson = JsonValue::MakeObject(); + layerJson.set("id", JsonValue(layer.id)); + layerJson.set("shaderId", JsonValue(layer.shaderId)); + layerJson.set("bypass", JsonValue(layer.bypass)); + + JsonValue parameterValues = JsonValue::MakeObject(); + for (const ShaderParameterDefinition& definition : layer.parameterDefinitions) + { + const auto valueIt = layer.parameterValues.find(definition.id); + if (valueIt != layer.parameterValues.end()) + parameterValues.set(definition.id, ParameterValueToJson(definition, valueIt->second)); + } + layerJson.set("parameterValues", parameterValues); + layers.pushBack(layerJson); + } + + root.set("layers", layers); + return root; +} + +std::string SerializeRuntimeStateSnapshot(const RuntimeLayerModelSnapshot& snapshot) +{ + return SerializeJson(RuntimeStateSnapshotToJson(snapshot), true); +} + +RuntimeStatePersistenceWriter::~RuntimeStatePersistenceWriter() +{ + Stop(true); +} + +void RuntimeStatePersistenceWriter::Start(const std::filesystem::path& path, std::chrono::milliseconds debounce) +{ + Stop(true); + mPath = path; + mDebounce = debounce; + { + std::lock_guard lock(mMutex); + mStopping = false; + mHasPendingState = false; + mPendingState.clear(); + } + mThread = std::thread([this]() { ThreadMain(); }); +} + +void RuntimeStatePersistenceWriter::RequestSave(const RuntimeLayerModelSnapshot& snapshot) +{ + const std::string serializedState = SerializeRuntimeStateSnapshot(snapshot); + { + std::lock_guard lock(mMutex); + mPendingState = serializedState; + mHasPendingState = true; + } + mCondition.notify_one(); +} + +void RuntimeStatePersistenceWriter::Stop(bool flushPending) +{ + std::string finalState; + { + std::lock_guard lock(mMutex); + if (flushPending && mHasPendingState) + finalState = mPendingState; + mStopping = true; + } + mCondition.notify_one(); + if (mThread.joinable()) + mThread.join(); + + if (!finalState.empty()) + { + std::string error; + if (!WriteSnapshot(finalState, error)) + LogWarning("runtime-state", error); + } + + std::lock_guard lock(mMutex); + mStopping = false; + mHasPendingState = false; + mPendingState.clear(); +} + +void RuntimeStatePersistenceWriter::ThreadMain() +{ + for (;;) + { + std::unique_lock lock(mMutex); + mCondition.wait(lock, [this]() { + return mStopping || mHasPendingState; + }); + + if (mStopping) + return; + + const std::string pendingAtWake = mPendingState; + mCondition.wait_for(lock, mDebounce, [this, &pendingAtWake]() { + return mStopping || mPendingState != pendingAtWake; + }); + + if (mStopping) + return; + if (mPendingState != pendingAtWake) + continue; + + const std::string stateToWrite = mPendingState; + mHasPendingState = false; + lock.unlock(); + + std::string error; + if (!WriteSnapshot(stateToWrite, error)) + LogWarning("runtime-state", error); + } +} + +bool RuntimeStatePersistenceWriter::WriteSnapshot(const std::string& serializedState, std::string& error) const +{ + if (mPath.empty()) + { + error = "Runtime state path is empty."; + return false; + } + + std::error_code ec; + std::filesystem::create_directories(mPath.parent_path(), ec); + if (ec) + { + error = "Could not create runtime state directory: " + ec.message(); + return false; + } + + const std::filesystem::path tempPath = mPath.string() + ".tmp"; + { + std::ofstream output(tempPath, std::ios::binary | std::ios::trunc); + if (!output) + { + error = "Could not open runtime state temp file: " + tempPath.string(); + return false; + } + output << serializedState << "\n"; + if (!output) + { + error = "Could not write runtime state temp file: " + tempPath.string(); + return false; + } + } + + std::filesystem::rename(tempPath, mPath, ec); + if (ec) + { + std::filesystem::remove(mPath, ec); + ec.clear(); + std::filesystem::rename(tempPath, mPath, ec); + if (ec) + { + error = "Could not replace runtime state file: " + ec.message(); + return false; + } + } + + error.clear(); + return true; +} +} diff --git a/src/runtime/RuntimeStatePersistence.h b/src/runtime/RuntimeStatePersistence.h new file mode 100644 index 0000000..34a06fa --- /dev/null +++ b/src/runtime/RuntimeStatePersistence.h @@ -0,0 +1,43 @@ +#pragma once + +#include "RuntimeLayerModel.h" +#include "RuntimeJson.h" + +#include +#include +#include +#include +#include +#include + +namespace RenderCadenceCompositor +{ +JsonValue RuntimeStateSnapshotToJson(const RuntimeLayerModelSnapshot& snapshot); +std::string SerializeRuntimeStateSnapshot(const RuntimeLayerModelSnapshot& snapshot); + +class RuntimeStatePersistenceWriter +{ +public: + RuntimeStatePersistenceWriter() = default; + RuntimeStatePersistenceWriter(const RuntimeStatePersistenceWriter&) = delete; + RuntimeStatePersistenceWriter& operator=(const RuntimeStatePersistenceWriter&) = delete; + ~RuntimeStatePersistenceWriter(); + + void Start(const std::filesystem::path& path, std::chrono::milliseconds debounce); + void RequestSave(const RuntimeLayerModelSnapshot& snapshot); + void Stop(bool flushPending = true); + +private: + void ThreadMain(); + bool WriteSnapshot(const std::string& serializedState, std::string& error) const; + + std::filesystem::path mPath; + std::chrono::milliseconds mDebounce{ 250 }; + std::mutex mMutex; + std::condition_variable mCondition; + std::thread mThread; + std::string mPendingState; + bool mHasPendingState = false; + bool mStopping = false; +}; +} diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 4d637db..69ff1a3 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -49,6 +49,13 @@ add_video_shader_test(RenderCadenceCompositorRuntimeLayerModelTests "${TEST_DIR}/RenderCadenceCompositorRuntimeLayerModelTests.cpp" ) +add_video_shader_test(RuntimeStatePersistenceTests + "${SRC_DIR}/logging/Logger.cpp" + "${SRC_DIR}/runtime/RuntimeJson.cpp" + "${SRC_DIR}/runtime/RuntimeStatePersistence.cpp" + "${TEST_DIR}/RuntimeStatePersistenceTests.cpp" +) + add_video_shader_test(FontAtlasBuilderTests "${SRC_DIR}/runtime/FontAtlasBuilder.cpp" "${SRC_DIR}/runtime/RuntimeJson.cpp" diff --git a/tests/RenderCadenceCompositorRuntimeLayerModelTests.cpp b/tests/RenderCadenceCompositorRuntimeLayerModelTests.cpp index db85594..5c50775 100644 --- a/tests/RenderCadenceCompositorRuntimeLayerModelTests.cpp +++ b/tests/RenderCadenceCompositorRuntimeLayerModelTests.cpp @@ -252,6 +252,27 @@ void TestInitializeFromRuntimeStateRestoresLayerStack() std::filesystem::remove_all(root); } +void TestInvalidRuntimeStateCanFallBackToConfiguredShader() +{ + std::filesystem::path root; + RenderCadenceCompositor::SupportedShaderCatalog catalog = MakeCatalog(root); + + JsonValue invalidRuntimeState = JsonValue::MakeObject(); + invalidRuntimeState.set("layers", JsonValue("not-an-array")); + + RenderCadenceCompositor::RuntimeLayerModel model; + std::string error; + Expect(!model.InitializeFromRuntimeState(catalog, invalidRuntimeState, error), "invalid runtime state is rejected"); + Expect(model.InitializeSingleLayer(catalog, "solid", error), "configured default shader can initialize after invalid runtime state"); + + RenderCadenceCompositor::RuntimeLayerModelSnapshot snapshot = model.Snapshot(); + Expect(snapshot.displayLayers.size() == 1, "fallback startup creates one default layer"); + Expect(snapshot.displayLayers[0].shaderId == "solid", "fallback layer uses the configured shader id"); + Expect(snapshot.displayLayers[0].parameterValues.at("gain").numberValues.front() == 0.5, "fallback layer uses shader defaults"); + + std::filesystem::remove_all(root); +} + void TestLayerControlsUpdateDisplayAndRenderModels() { std::filesystem::path root; @@ -308,23 +329,42 @@ void TestReloadRefreshesChangedShaderMetadataAndPreservesValues() { std::filesystem::path root; RenderCadenceCompositor::SupportedShaderCatalog catalog = MakeCatalog(root); + WriteFile(root / "passthrough" / "shader.slang", "float4 shadeVideo(float2 uv) { return float4(uv, 1.0, 1.0); }\n"); + WriteFile(root / "passthrough" / "shader.json", R"({ + "id": "passthrough", + "name": "Passthrough", + "description": "Passthrough test shader", + "category": "Tests", + "entryPoint": "shadeVideo", + "parameters": [ + { "id": "opacity", "label": "Opacity", "type": "float", "default": 1.0 } + ] + })"); + catalog = LoadCatalog(root); RenderCadenceCompositor::RuntimeLayerModel model; std::string error; std::string layerId; + std::string unchangedLayerId; Expect(model.AddLayer(catalog, "solid", layerId, error), "reload test layer can be added"); + Expect(model.AddLayer(catalog, "passthrough", unchangedLayerId, error), "reload unchanged layer can be added"); Expect(model.UpdateParameter(layerId, "gain", JsonValue(0.75), error), "reload test parameter can be customized"); WriteFile(root / "solid" / "shader.json", SolidShaderManifest(0.1, true)); RenderCadenceCompositor::SupportedShaderCatalog reloadedCatalog = LoadCatalog(root); std::vector> buildsToStart; Expect(model.ReloadFromCatalog(reloadedCatalog, buildsToStart, error), "reload refreshes model from changed catalog"); - Expect(buildsToStart.size() == 1 && buildsToStart[0].first == layerId && buildsToStart[0].second == "solid", "changed shader queues a layer rebuild"); + Expect(buildsToStart.size() == 2 + && buildsToStart[0].first == layerId + && buildsToStart[0].second == "solid" + && buildsToStart[1].first == unchangedLayerId + && buildsToStart[1].second == "passthrough", "reload queues every available layer rebuild in order"); RenderCadenceCompositor::RuntimeLayerModelSnapshot snapshot = model.Snapshot(); Expect(snapshot.displayLayers[0].parameterDefinitions.size() == 3, "reload exposes new JSON parameter definitions to UI"); Expect(snapshot.displayLayers[0].parameterValues.at("gain").numberValues.front() == 0.75, "reload preserves compatible parameter values"); Expect(snapshot.displayLayers[0].parameterValues.at("mix").numberValues.front() == 0.25, "reload initializes newly added parameters"); + Expect(snapshot.displayLayers[1].parameterValues.at("opacity").numberValues.front() == 1.0, "reload keeps unchanged layer parameter values"); std::filesystem::remove_all(root); } @@ -337,6 +377,7 @@ int main() TestBuildFailureStaysDisplaySide(); TestAddAndRemoveLayers(); TestInitializeFromRuntimeStateRestoresLayerStack(); + TestInvalidRuntimeStateCanFallBackToConfiguredShader(); TestLayerControlsUpdateDisplayAndRenderModels(); TestReloadRefreshesChangedShaderMetadataAndPreservesValues(); diff --git a/tests/RuntimeStatePersistenceTests.cpp b/tests/RuntimeStatePersistenceTests.cpp new file mode 100644 index 0000000..9e275f7 --- /dev/null +++ b/tests/RuntimeStatePersistenceTests.cpp @@ -0,0 +1,157 @@ +#include "RuntimeStatePersistence.h" + +#include +#include +#include +#include +#include +#include +#include + +namespace +{ +int gFailures = 0; + +using RenderCadenceCompositor::RuntimeLayerModelSnapshot; +using RenderCadenceCompositor::RuntimeLayerReadModel; +using RenderCadenceCompositor::SerializeRuntimeStateSnapshot; + +void Expect(bool condition, const char* message) +{ + if (condition) + return; + + std::cerr << "FAIL: " << message << "\n"; + ++gFailures; +} + +std::filesystem::path MakeTestRoot() +{ + const auto stamp = std::chrono::steady_clock::now().time_since_epoch().count(); + std::filesystem::path root = std::filesystem::temp_directory_path() / ("render-cadence-runtime-state-persistence-tests-" + std::to_string(stamp)); + std::filesystem::create_directories(root); + return root; +} + +std::string ReadTextFile(const std::filesystem::path& path) +{ + std::ifstream input(path, std::ios::binary); + std::ostringstream buffer; + buffer << input.rdbuf(); + return buffer.str(); +} + +ShaderParameterDefinition FloatParameter() +{ + ShaderParameterDefinition definition; + definition.id = "gain"; + definition.type = ShaderParameterType::Float; + return definition; +} + +ShaderParameterDefinition BooleanParameter() +{ + ShaderParameterDefinition definition; + definition.id = "enabled"; + definition.type = ShaderParameterType::Boolean; + return definition; +} + +RuntimeLayerModelSnapshot MakeSnapshot(const std::string& layerId, double gain, bool enabled) +{ + RuntimeLayerReadModel layer; + layer.id = layerId; + layer.shaderId = "solid"; + layer.shaderName = "Solid"; + layer.bypass = !enabled; + layer.parameterDefinitions = { FloatParameter(), BooleanParameter() }; + + ShaderParameterValue gainValue; + gainValue.numberValues = { gain }; + layer.parameterValues["gain"] = gainValue; + + ShaderParameterValue enabledValue; + enabledValue.booleanValue = enabled; + layer.parameterValues["enabled"] = enabledValue; + + RuntimeLayerModelSnapshot snapshot; + snapshot.displayLayers.push_back(layer); + return snapshot; +} + +void TestRuntimeStateSerialization() +{ + const std::string serialized = SerializeRuntimeStateSnapshot(MakeSnapshot("layer-a", 0.75, false)); + JsonValue parsed; + std::string error; + Expect(ParseJson(serialized, parsed, error), "serialized runtime state parses"); + const JsonValue* layers = parsed.find("layers"); + Expect(layers && layers->isArray() && layers->asArray().size() == 1, "serialized runtime state contains one layer"); + const JsonValue& layer = layers->asArray().front(); + Expect(layer.find("id") && layer.find("id")->asString() == "layer-a", "serialized runtime state preserves layer id"); + Expect(layer.find("shaderId") && layer.find("shaderId")->asString() == "solid", "serialized runtime state preserves shader id"); + Expect(layer.find("bypass") && layer.find("bypass")->asBoolean() == true, "serialized runtime state preserves bypass flag"); + const JsonValue* values = layer.find("parameterValues"); + Expect(values && values->isObject(), "serialized runtime state contains parameter values"); + Expect(values->find("gain") && values->find("gain")->asNumber() == 0.75, "serialized runtime state preserves float values"); + Expect(values->find("enabled") && values->find("enabled")->asBoolean() == false, "serialized runtime state preserves boolean values"); +} + +void TestRuntimeStateWriterFlushesLatestSnapshot() +{ + const std::filesystem::path root = MakeTestRoot(); + const std::filesystem::path path = root / "runtime_state.json"; + + RenderCadenceCompositor::RuntimeStatePersistenceWriter writer; + writer.Start(path, std::chrono::milliseconds(1000)); + writer.RequestSave(MakeSnapshot("layer-old", 0.25, true)); + writer.RequestSave(MakeSnapshot("layer-new", 0.9, false)); + writer.Stop(true); + + JsonValue parsed; + std::string error; + Expect(ParseJson(ReadTextFile(path), parsed, error), "flushed runtime state parses"); + const JsonValue& layer = parsed.find("layers")->asArray().front(); + Expect(layer.find("id")->asString() == "layer-new", "writer flushes the latest pending snapshot"); + Expect(layer.find("parameterValues")->find("gain")->asNumber() == 0.9, "writer flush keeps latest parameter value"); + + std::filesystem::remove_all(root); +} + +void TestRuntimeStateWriterDebouncesAndWritesInBackground() +{ + const std::filesystem::path root = MakeTestRoot(); + const std::filesystem::path path = root / "nested" / "runtime_state.json"; + + { + RenderCadenceCompositor::RuntimeStatePersistenceWriter writer; + writer.Start(path, std::chrono::milliseconds(20)); + writer.RequestSave(MakeSnapshot("layer-bg", 0.5, true)); + std::this_thread::sleep_for(std::chrono::milliseconds(120)); + writer.Stop(true); + } + + JsonValue parsed; + std::string error; + Expect(ParseJson(ReadTextFile(path), parsed, error), "background runtime state write parses"); + Expect(parsed.find("layers")->asArray().front().find("id")->asString() == "layer-bg", "background writer creates the runtime state file"); + + std::filesystem::remove_all(root); +} +} + +int main() +{ + TestRuntimeStateSerialization(); + TestRuntimeStateWriterFlushesLatestSnapshot(); + TestRuntimeStateWriterDebouncesAndWritesInBackground(); + + if (gFailures != 0) + { + std::cerr << gFailures << " RuntimeStatePersistence test failure(s).\n"; + return 1; + } + + std::cout << "RuntimeStatePersistence tests passed.\n"; + return 0; +}