Compare commits
3 Commits
c2de2c3738
...
5c46eaf18a
| Author | SHA1 | Date | |
|---|---|---|---|
| 5c46eaf18a | |||
| d68cf9b1a0 | |||
| bda9a9dc22 |
@@ -2,6 +2,8 @@
|
||||
|
||||
#include "../logging/Logger.h"
|
||||
|
||||
#include <chrono>
|
||||
|
||||
namespace RenderCadenceCompositor
|
||||
{
|
||||
RuntimeLayerController::RuntimeLayerController(RenderLayerPublisher publisher) :
|
||||
@@ -23,26 +25,43 @@ 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);
|
||||
}
|
||||
|
||||
void RuntimeLayerController::StartStartupBuild(const std::string& runtimeShaderId)
|
||||
{
|
||||
if (runtimeShaderId.empty())
|
||||
std::vector<std::pair<std::string, std::string>> buildsToStart;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mRuntimeLayerMutex);
|
||||
buildsToStart = mRuntimeLayerModel.PendingLayerBuilds();
|
||||
}
|
||||
|
||||
if (buildsToStart.empty() && runtimeShaderId.empty())
|
||||
{
|
||||
Log("runtime-shader", "Runtime shader build disabled.");
|
||||
return;
|
||||
}
|
||||
|
||||
Log("runtime-shader", "Starting background Slang build for shader '" + runtimeShaderId + "'.");
|
||||
const std::string layerId = FirstRuntimeLayerId();
|
||||
if (!layerId.empty())
|
||||
StartLayerShaderBuild(layerId, runtimeShaderId);
|
||||
if (buildsToStart.empty())
|
||||
{
|
||||
const std::string layerId = FirstRuntimeLayerId();
|
||||
if (!layerId.empty())
|
||||
buildsToStart.push_back({ layerId, runtimeShaderId });
|
||||
}
|
||||
|
||||
for (const auto& build : buildsToStart)
|
||||
{
|
||||
Log("runtime-shader", "Starting background Slang build for layer '" + build.first + "' shader '" + build.second + "'.");
|
||||
StartLayerShaderBuild(build.first, build.second);
|
||||
}
|
||||
}
|
||||
|
||||
void RuntimeLayerController::Stop()
|
||||
{
|
||||
mPersistenceWriter.Stop(true);
|
||||
StopAllRuntimeShaderBuilds();
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
@@ -41,6 +43,10 @@ public:
|
||||
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();
|
||||
@@ -55,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;
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
#include "RuntimeLayerController.h"
|
||||
|
||||
#include "AppConfigProvider.h"
|
||||
#include "RuntimeJson.h"
|
||||
#include "../logging/Logger.h"
|
||||
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <sstream>
|
||||
|
||||
namespace RenderCadenceCompositor
|
||||
{
|
||||
@@ -30,6 +33,12 @@ bool RuntimeLayerController::LoadSupportedShaderCatalog(const std::string& shade
|
||||
|
||||
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))
|
||||
@@ -40,6 +49,64 @@ void RuntimeLayerController::InitializeLayerModel(std::string& runtimeShaderId)
|
||||
}
|
||||
}
|
||||
|
||||
bool RuntimeLayerController::InitializeLayerModelFromRuntimeState()
|
||||
{
|
||||
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)
|
||||
{
|
||||
LogWarning("runtime-state", "Could not open runtime state file: " + runtimeStatePath.string());
|
||||
return false;
|
||||
}
|
||||
|
||||
std::ostringstream buffer;
|
||||
buffer << input.rdbuf();
|
||||
|
||||
JsonValue runtimeState;
|
||||
std::string error;
|
||||
if (!ParseJson(buffer.str(), runtimeState, error))
|
||||
{
|
||||
LogWarning("runtime-state", "Could not parse runtime state file: " + error);
|
||||
return false;
|
||||
}
|
||||
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mRuntimeLayerMutex);
|
||||
if (!mRuntimeLayerModel.InitializeFromRuntimeState(mShaderCatalog, runtimeState, error))
|
||||
{
|
||||
LogWarning("runtime-state", "Could not restore runtime state: " + error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
Log("runtime-state", "Restored runtime layer stack from " + runtimeStatePath.string() + ".");
|
||||
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();
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
|
||||
#include <algorithm>
|
||||
#include <chrono>
|
||||
#include <cctype>
|
||||
#include <set>
|
||||
#include <utility>
|
||||
|
||||
namespace RenderCadenceCompositor
|
||||
@@ -35,6 +37,38 @@ JsonValue ParameterValueToJson(const ShaderParameterDefinition& definition, cons
|
||||
}
|
||||
return JsonValue();
|
||||
}
|
||||
|
||||
bool ParseRuntimeLayerNumber(const std::string& layerId, uint64_t& number)
|
||||
{
|
||||
const std::string prefix = "runtime-layer-";
|
||||
if (layerId.compare(0, prefix.size(), prefix) != 0)
|
||||
return false;
|
||||
|
||||
const std::string suffix = layerId.substr(prefix.size());
|
||||
if (suffix.empty())
|
||||
return false;
|
||||
|
||||
uint64_t parsed = 0;
|
||||
for (char character : suffix)
|
||||
{
|
||||
if (!std::isdigit(static_cast<unsigned char>(character)))
|
||||
return false;
|
||||
parsed = parsed * 10 + static_cast<uint64_t>(character - '0');
|
||||
}
|
||||
|
||||
number = parsed;
|
||||
return true;
|
||||
}
|
||||
|
||||
std::string AllocateRestoredLayerId(std::set<std::string>& usedLayerIds, uint64_t& nextLayerNumber)
|
||||
{
|
||||
for (;;)
|
||||
{
|
||||
std::string candidate = "runtime-layer-" + std::to_string(nextLayerNumber++);
|
||||
if (usedLayerIds.insert(candidate).second)
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool RuntimeLayerModel::InitializeSingleLayer(const SupportedShaderCatalog& shaderCatalog, const std::string& shaderId, std::string& error)
|
||||
@@ -227,6 +261,89 @@ bool RuntimeLayerModel::ResetParameters(const std::string& layerId, std::string&
|
||||
return true;
|
||||
}
|
||||
|
||||
bool RuntimeLayerModel::InitializeFromRuntimeState(const SupportedShaderCatalog& shaderCatalog, const JsonValue& runtimeState, std::string& error)
|
||||
{
|
||||
if (!runtimeState.isObject())
|
||||
{
|
||||
error = "Runtime state root must be a JSON object.";
|
||||
return false;
|
||||
}
|
||||
|
||||
const JsonValue* layersValue = runtimeState.find("layers");
|
||||
if (!layersValue || !layersValue->isArray())
|
||||
{
|
||||
error = "Runtime state must contain a layers array.";
|
||||
return false;
|
||||
}
|
||||
|
||||
std::vector<Layer> restoredLayers;
|
||||
std::set<std::string> usedLayerIds;
|
||||
uint64_t nextLayerNumber = 1;
|
||||
|
||||
for (const JsonValue& layerValue : layersValue->asArray())
|
||||
{
|
||||
if (!layerValue.isObject())
|
||||
continue;
|
||||
|
||||
const JsonValue* shaderIdValue = layerValue.find("shaderId");
|
||||
if (!shaderIdValue || !shaderIdValue->isString() || shaderIdValue->asString().empty())
|
||||
continue;
|
||||
|
||||
const ShaderPackage* shaderPackage = shaderCatalog.FindPackage(shaderIdValue->asString());
|
||||
if (!shaderPackage)
|
||||
continue;
|
||||
|
||||
Layer layer;
|
||||
const JsonValue* layerIdValue = layerValue.find("id");
|
||||
if (layerIdValue && layerIdValue->isString() && !layerIdValue->asString().empty() && usedLayerIds.insert(layerIdValue->asString()).second)
|
||||
layer.id = layerIdValue->asString();
|
||||
else
|
||||
layer.id = AllocateRestoredLayerId(usedLayerIds, nextLayerNumber);
|
||||
|
||||
uint64_t restoredLayerNumber = 0;
|
||||
if (ParseRuntimeLayerNumber(layer.id, restoredLayerNumber) && restoredLayerNumber >= nextLayerNumber)
|
||||
nextLayerNumber = restoredLayerNumber + 1;
|
||||
|
||||
layer.shaderId = shaderPackage->id;
|
||||
layer.packageFingerprint = ShaderPackageFingerprint(*shaderPackage);
|
||||
layer.shaderName = shaderPackage->displayName.empty() ? shaderPackage->id : shaderPackage->displayName;
|
||||
const JsonValue* bypassValue = layerValue.find("bypass");
|
||||
layer.bypass = bypassValue && bypassValue->isBoolean() ? bypassValue->asBoolean() : false;
|
||||
layer.buildState = RuntimeLayerBuildState::Pending;
|
||||
layer.message = "Runtime Slang build is waiting to start.";
|
||||
InitializeDefaultParameterValues(layer, *shaderPackage);
|
||||
|
||||
const JsonValue* parameterValues = layerValue.find("parameterValues");
|
||||
if (parameterValues && parameterValues->isObject())
|
||||
{
|
||||
for (const ShaderParameterDefinition& definition : layer.parameterDefinitions)
|
||||
{
|
||||
const JsonValue* value = parameterValues->find(definition.id);
|
||||
if (!value)
|
||||
continue;
|
||||
|
||||
ShaderParameterValue normalizedValue;
|
||||
std::string normalizeError;
|
||||
if (NormalizeAndValidateParameterValue(definition, *value, normalizedValue, normalizeError))
|
||||
layer.parameterValues[definition.id] = normalizedValue;
|
||||
}
|
||||
}
|
||||
|
||||
restoredLayers.push_back(std::move(layer));
|
||||
}
|
||||
|
||||
if (restoredLayers.empty())
|
||||
{
|
||||
error = "Runtime state did not contain any supported layers.";
|
||||
return false;
|
||||
}
|
||||
|
||||
mLayers = std::move(restoredLayers);
|
||||
mNextLayerNumber = nextLayerNumber;
|
||||
error.clear();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool RuntimeLayerModel::ReloadFromCatalog(const SupportedShaderCatalog& shaderCatalog, std::vector<std::pair<std::string, std::string>>& buildsToStart, std::string& error)
|
||||
{
|
||||
buildsToStart.clear();
|
||||
@@ -242,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)
|
||||
@@ -392,6 +512,17 @@ std::string RuntimeLayerModel::FirstLayerId() const
|
||||
return mLayers.empty() ? std::string() : mLayers.front().id;
|
||||
}
|
||||
|
||||
std::vector<std::pair<std::string, std::string>> RuntimeLayerModel::PendingLayerBuilds() const
|
||||
{
|
||||
std::vector<std::pair<std::string, std::string>> builds;
|
||||
for (const Layer& layer : mLayers)
|
||||
{
|
||||
if (layer.buildState == RuntimeLayerBuildState::Pending)
|
||||
builds.push_back({ layer.id, layer.shaderId });
|
||||
}
|
||||
return builds;
|
||||
}
|
||||
|
||||
RuntimeLayerModel::Layer* RuntimeLayerModel::FindLayer(const std::string& layerId)
|
||||
{
|
||||
for (Layer& layer : mLayers)
|
||||
|
||||
@@ -53,6 +53,7 @@ class RuntimeLayerModel
|
||||
{
|
||||
public:
|
||||
bool InitializeSingleLayer(const SupportedShaderCatalog& shaderCatalog, const std::string& shaderId, std::string& error);
|
||||
bool InitializeFromRuntimeState(const SupportedShaderCatalog& shaderCatalog, const JsonValue& runtimeState, std::string& error);
|
||||
void Clear();
|
||||
|
||||
bool AddLayer(const SupportedShaderCatalog& shaderCatalog, const std::string& shaderId, std::string& layerId, std::string& error);
|
||||
@@ -71,6 +72,7 @@ public:
|
||||
|
||||
RuntimeLayerModelSnapshot Snapshot() const;
|
||||
std::string FirstLayerId() const;
|
||||
std::vector<std::pair<std::string, std::string>> PendingLayerBuilds() const;
|
||||
|
||||
private:
|
||||
struct Layer
|
||||
|
||||
206
src/runtime/RuntimeStatePersistence.cpp
Normal file
206
src/runtime/RuntimeStatePersistence.cpp
Normal 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;
|
||||
}
|
||||
}
|
||||
43
src/runtime/RuntimeStatePersistence.h
Normal file
43
src/runtime/RuntimeStatePersistence.h
Normal 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;
|
||||
};
|
||||
}
|
||||
@@ -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"
|
||||
|
||||
@@ -54,6 +54,30 @@ std::string SolidShaderManifest(double gainDefault, bool includeMix)
|
||||
})";
|
||||
}
|
||||
|
||||
std::string AllParametersShaderManifest()
|
||||
{
|
||||
return R"({
|
||||
"id": "all-params",
|
||||
"name": "All Params",
|
||||
"description": "All parameter restore test shader",
|
||||
"category": "Tests",
|
||||
"entryPoint": "shadeVideo",
|
||||
"fonts": [{ "id": "inter", "path": "Inter.ttf" }],
|
||||
"parameters": [
|
||||
{ "id": "gain", "label": "Gain", "type": "float", "default": 0.5, "min": 0.0, "max": 1.0 },
|
||||
{ "id": "offset", "label": "Offset", "type": "vec2", "default": [0.0, 0.0], "min": [-1.0, -1.0], "max": [1.0, 1.0] },
|
||||
{ "id": "tint", "label": "Tint", "type": "color", "default": [1.0, 1.0, 1.0, 1.0], "min": [0.0, 0.0, 0.0, 0.0], "max": [1.0, 1.0, 1.0, 1.0] },
|
||||
{ "id": "enabled", "label": "Enabled", "type": "bool", "default": true },
|
||||
{ "id": "mode", "label": "Mode", "type": "enum", "default": "soft", "options": [
|
||||
{ "value": "soft", "label": "Soft" },
|
||||
{ "value": "hard", "label": "Hard" }
|
||||
] },
|
||||
{ "id": "titleText", "label": "Title", "type": "text", "default": "DEFAULT", "font": "inter", "maxLength": 8 },
|
||||
{ "id": "drop", "label": "Drop", "type": "trigger" }
|
||||
]
|
||||
})";
|
||||
}
|
||||
|
||||
RenderCadenceCompositor::SupportedShaderCatalog LoadCatalog(const std::filesystem::path& root)
|
||||
{
|
||||
RenderCadenceCompositor::SupportedShaderCatalog catalog;
|
||||
@@ -159,6 +183,96 @@ void TestAddAndRemoveLayers()
|
||||
std::filesystem::remove_all(root);
|
||||
}
|
||||
|
||||
void TestInitializeFromRuntimeStateRestoresLayerStack()
|
||||
{
|
||||
std::filesystem::path root = MakeTestRoot();
|
||||
WriteFile(root / "solid" / "shader.slang", "float4 shadeVideo(float2 uv) { return float4(uv, 0.0, 1.0); }\n");
|
||||
WriteFile(root / "solid" / "shader.json", SolidShaderManifest(0.5, false));
|
||||
WriteFile(root / "all-params" / "shader.slang", "float4 shadeVideo(float2 uv) { return float4(uv, 0.0, 1.0); }\n");
|
||||
WriteFile(root / "all-params" / "Inter.ttf", "not a real font, but enough for restore catalog support checks");
|
||||
WriteFile(root / "all-params" / "shader.json", AllParametersShaderManifest());
|
||||
RenderCadenceCompositor::SupportedShaderCatalog catalog = LoadCatalog(root);
|
||||
|
||||
JsonValue runtimeState;
|
||||
std::string error;
|
||||
Expect(ParseJson(R"({
|
||||
"layers": [
|
||||
{
|
||||
"id": "layer-31",
|
||||
"shaderId": "all-params",
|
||||
"bypass": true,
|
||||
"parameterValues": {
|
||||
"gain": 0.75,
|
||||
"offset": [0.25, -0.5],
|
||||
"tint": [0.1, 0.2, 0.3, 0.4],
|
||||
"enabled": false,
|
||||
"mode": "hard",
|
||||
"titleText": "RESTORED-TEXT",
|
||||
"drop": 4
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "layer-32",
|
||||
"shaderId": "missing",
|
||||
"parameterValues": {
|
||||
"gain": 0.9
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "layer-33",
|
||||
"shaderId": "solid",
|
||||
"parameterValues": {
|
||||
"gain": "bad"
|
||||
}
|
||||
}
|
||||
]
|
||||
})", runtimeState, error), "runtime state fixture parses");
|
||||
|
||||
RenderCadenceCompositor::RuntimeLayerModel model;
|
||||
Expect(model.InitializeFromRuntimeState(catalog, runtimeState, error), "runtime state can initialize the layer model");
|
||||
RenderCadenceCompositor::RuntimeLayerModelSnapshot snapshot = model.Snapshot();
|
||||
Expect(snapshot.displayLayers.size() == 2, "restore keeps supported layers and skips missing shaders");
|
||||
Expect(snapshot.displayLayers[0].id == "layer-31", "restore preserves saved layer id");
|
||||
Expect(snapshot.displayLayers[0].shaderId == "all-params", "restore preserves shader id");
|
||||
Expect(snapshot.displayLayers[0].bypass, "restore preserves bypass state");
|
||||
Expect(snapshot.displayLayers[0].parameterValues.at("gain").numberValues.front() == 0.75, "restore preserves valid parameter values");
|
||||
Expect(snapshot.displayLayers[0].parameterValues.at("offset").numberValues == std::vector<double>({ 0.25, -0.5 }), "restore preserves vec2 parameter values");
|
||||
Expect(snapshot.displayLayers[0].parameterValues.at("tint").numberValues == std::vector<double>({ 0.1, 0.2, 0.3, 0.4 }), "restore preserves color parameter values");
|
||||
Expect(!snapshot.displayLayers[0].parameterValues.at("enabled").booleanValue, "restore preserves boolean parameter values");
|
||||
Expect(snapshot.displayLayers[0].parameterValues.at("mode").enumValue == "hard", "restore preserves enum parameter values");
|
||||
Expect(snapshot.displayLayers[0].parameterValues.at("titleText").textValue == "RESTORED", "restore normalizes and preserves text parameter values");
|
||||
Expect(snapshot.displayLayers[0].parameterValues.at("drop").numberValues.front() == 4.0, "restore preserves trigger counts");
|
||||
Expect(snapshot.displayLayers[1].id == "layer-33", "restore preserves later supported layer order");
|
||||
Expect(snapshot.displayLayers[1].shaderId == "solid", "restore preserves later layer shader id");
|
||||
Expect(snapshot.displayLayers[1].parameterValues.at("gain").numberValues.front() == 0.5, "restore falls back to defaults for invalid parameter values");
|
||||
|
||||
const std::vector<std::pair<std::string, std::string>> builds = model.PendingLayerBuilds();
|
||||
Expect(builds.size() == 2 && builds[0].first == "layer-31" && builds[0].second == "all-params" && builds[1].first == "layer-33" && builds[1].second == "solid", "restore queues startup builds for every restored layer in order");
|
||||
|
||||
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;
|
||||
@@ -215,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);
|
||||
}
|
||||
@@ -243,6 +376,8 @@ int main()
|
||||
TestRejectsUnsupportedStartupShader();
|
||||
TestBuildFailureStaysDisplaySide();
|
||||
TestAddAndRemoveLayers();
|
||||
TestInitializeFromRuntimeStateRestoresLayerStack();
|
||||
TestInvalidRuntimeStateCanFallBackToConfiguredShader();
|
||||
TestLayerControlsUpdateDisplayAndRenderModels();
|
||||
TestReloadRefreshesChangedShaderMetadataAndPreservesValues();
|
||||
|
||||
|
||||
157
tests/RuntimeStatePersistenceTests.cpp
Normal file
157
tests/RuntimeStatePersistenceTests.cpp
Normal 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;
|
||||
}
|
||||
Reference in New Issue
Block a user