More changes
Some checks failed
CI / React UI Build (push) Successful in 11s
CI / Native Windows Build And Tests (push) Failing after 2m12s
CI / Windows Release Package (push) Has been skipped

This commit is contained in:
2026-05-21 16:51:00 +10:00
parent d68cf9b1a0
commit 5c46eaf18a
10 changed files with 505 additions and 2 deletions

View File

@@ -2,6 +2,8 @@
#include "../logging/Logger.h"
#include <chrono>
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();
}

View File

@@ -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 <functional>
#include <filesystem>
#include <map>
#include <memory>
#include <mutex>
@@ -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;

View File

@@ -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<std::mutex> 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<std::mutex> lock(mRuntimeLayerMutex);
RequestRuntimeStatePersistenceLocked();
}
void RuntimeLayerController::RequestRuntimeStatePersistenceLocked()
{
mPersistenceWriter.RequestSave(mRuntimeLayerModel.Snapshot());
}
void RuntimeLayerController::StartLayerShaderBuild(const std::string& layerId, const std::string& shaderId)
{
CleanupRetiredShaderBuilds();

View File

@@ -19,6 +19,7 @@ ControlActionResult RuntimeLayerController::HandleAddLayer(const std::string& bo
std::lock_guard<std::mutex> 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<std::mutex> 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<std::mutex> 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<std::mutex> 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<std::mutex> 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<std::mutex> 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<std::mutex> 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<std::mutex> 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<std::mutex> 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<std::mutex> lock(mRuntimeLayerMutex);
if (!mRuntimeLayerModel.ReloadFromCatalog(mShaderCatalog, buildsToStart, error))
return { false, error };
RequestRuntimeStatePersistenceLocked();
}
for (const auto& build : buildsToStart)

View File

@@ -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<std::string, ShaderParameterDefinition> previousDefinitions;
for (const ShaderParameterDefinition& definition : layer.parameterDefinitions)

View File

@@ -0,0 +1,206 @@
#include "RuntimeStatePersistence.h"
#include "../logging/Logger.h"
#include <fstream>
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<std::mutex> 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<std::mutex> lock(mMutex);
mPendingState = serializedState;
mHasPendingState = true;
}
mCondition.notify_one();
}
void RuntimeStatePersistenceWriter::Stop(bool flushPending)
{
std::string finalState;
{
std::lock_guard<std::mutex> 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<std::mutex> lock(mMutex);
mStopping = false;
mHasPendingState = false;
mPendingState.clear();
}
void RuntimeStatePersistenceWriter::ThreadMain()
{
for (;;)
{
std::unique_lock<std::mutex> 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;
}
}

View File

@@ -0,0 +1,43 @@
#pragma once
#include "RuntimeLayerModel.h"
#include "RuntimeJson.h"
#include <chrono>
#include <condition_variable>
#include <filesystem>
#include <mutex>
#include <string>
#include <thread>
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;
};
}

View File

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

View File

@@ -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<std::pair<std::string, std::string>> 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();

View File

@@ -0,0 +1,157 @@
#include "RuntimeStatePersistence.h"
#include <chrono>
#include <filesystem>
#include <fstream>
#include <iostream>
#include <sstream>
#include <string>
#include <thread>
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;
}