GROUND WORK PHASE 3
This commit is contained in:
@@ -110,6 +110,10 @@ set(APP_SOURCES
|
|||||||
"${APP_DIR}/runtime/events/RuntimeEventPayloads.h"
|
"${APP_DIR}/runtime/events/RuntimeEventPayloads.h"
|
||||||
"${APP_DIR}/runtime/events/RuntimeEventQueue.h"
|
"${APP_DIR}/runtime/events/RuntimeEventQueue.h"
|
||||||
"${APP_DIR}/runtime/events/RuntimeEventType.h"
|
"${APP_DIR}/runtime/events/RuntimeEventType.h"
|
||||||
|
"${APP_DIR}/runtime/live/RenderStateComposer.cpp"
|
||||||
|
"${APP_DIR}/runtime/live/RenderStateComposer.h"
|
||||||
|
"${APP_DIR}/runtime/live/RuntimeLiveState.cpp"
|
||||||
|
"${APP_DIR}/runtime/live/RuntimeLiveState.h"
|
||||||
"${APP_DIR}/runtime/presentation/RuntimeStateJson.cpp"
|
"${APP_DIR}/runtime/presentation/RuntimeStateJson.cpp"
|
||||||
"${APP_DIR}/runtime/presentation/RuntimeStateJson.h"
|
"${APP_DIR}/runtime/presentation/RuntimeStateJson.h"
|
||||||
"${APP_DIR}/runtime/presentation/RuntimeStatePresenter.cpp"
|
"${APP_DIR}/runtime/presentation/RuntimeStatePresenter.cpp"
|
||||||
@@ -165,6 +169,7 @@ target_include_directories(LoopThroughWithOpenGLCompositing PRIVATE
|
|||||||
"${APP_DIR}/runtime"
|
"${APP_DIR}/runtime"
|
||||||
"${APP_DIR}/runtime/coordination"
|
"${APP_DIR}/runtime/coordination"
|
||||||
"${APP_DIR}/runtime/events"
|
"${APP_DIR}/runtime/events"
|
||||||
|
"${APP_DIR}/runtime/live"
|
||||||
"${APP_DIR}/runtime/presentation"
|
"${APP_DIR}/runtime/presentation"
|
||||||
"${APP_DIR}/runtime/snapshot"
|
"${APP_DIR}/runtime/snapshot"
|
||||||
"${APP_DIR}/runtime/store"
|
"${APP_DIR}/runtime/store"
|
||||||
@@ -282,6 +287,28 @@ endif()
|
|||||||
|
|
||||||
add_test(NAME RuntimeEventTypeTests COMMAND RuntimeEventTypeTests)
|
add_test(NAME RuntimeEventTypeTests COMMAND RuntimeEventTypeTests)
|
||||||
|
|
||||||
|
add_executable(RuntimeLiveStateTests
|
||||||
|
"${APP_DIR}/runtime/live/RenderStateComposer.cpp"
|
||||||
|
"${APP_DIR}/runtime/live/RuntimeLiveState.cpp"
|
||||||
|
"${APP_DIR}/runtime/support/RuntimeJson.cpp"
|
||||||
|
"${APP_DIR}/runtime/support/RuntimeParameterUtils.cpp"
|
||||||
|
"${CMAKE_CURRENT_SOURCE_DIR}/tests/RuntimeLiveStateTests.cpp"
|
||||||
|
)
|
||||||
|
|
||||||
|
target_include_directories(RuntimeLiveStateTests PRIVATE
|
||||||
|
"${APP_DIR}"
|
||||||
|
"${APP_DIR}/runtime"
|
||||||
|
"${APP_DIR}/runtime/live"
|
||||||
|
"${APP_DIR}/runtime/support"
|
||||||
|
"${APP_DIR}/shader"
|
||||||
|
)
|
||||||
|
|
||||||
|
if(MSVC)
|
||||||
|
target_compile_options(RuntimeLiveStateTests PRIVATE /W3)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
add_test(NAME RuntimeLiveStateTests COMMAND RuntimeLiveStateTests)
|
||||||
|
|
||||||
add_executable(RuntimeSubsystemTests
|
add_executable(RuntimeSubsystemTests
|
||||||
"${APP_DIR}/runtime/store/LayerStackStore.cpp"
|
"${APP_DIR}/runtime/store/LayerStackStore.cpp"
|
||||||
"${APP_DIR}/runtime/store/ShaderPackageCatalog.cpp"
|
"${APP_DIR}/runtime/store/ShaderPackageCatalog.cpp"
|
||||||
|
|||||||
@@ -1,83 +1,14 @@
|
|||||||
#include "RenderEngine.h"
|
#include "RenderEngine.h"
|
||||||
|
|
||||||
#include "RuntimeParameterUtils.h"
|
|
||||||
#include "ShaderBuildQueue.h"
|
#include "ShaderBuildQueue.h"
|
||||||
|
|
||||||
#include <gl/gl.h>
|
#include <gl/gl.h>
|
||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <cctype>
|
|
||||||
#include <cmath>
|
|
||||||
#include <cstddef>
|
|
||||||
|
|
||||||
namespace
|
namespace
|
||||||
{
|
{
|
||||||
constexpr auto kOscOverlayCommitDelay = std::chrono::milliseconds(150);
|
constexpr auto kOscOverlayCommitDelay = std::chrono::milliseconds(150);
|
||||||
constexpr double kOscSmoothingReferenceFps = 60.0;
|
|
||||||
constexpr double kOscSmoothingMaxStepSeconds = 0.25;
|
|
||||||
|
|
||||||
std::string SimplifyOscControlKey(const std::string& text)
|
|
||||||
{
|
|
||||||
std::string simplified;
|
|
||||||
for (unsigned char ch : text)
|
|
||||||
{
|
|
||||||
if (std::isalnum(ch))
|
|
||||||
simplified.push_back(static_cast<char>(std::tolower(ch)));
|
|
||||||
}
|
|
||||||
return simplified;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool MatchesOscControlKey(const std::string& candidate, const std::string& key)
|
|
||||||
{
|
|
||||||
return candidate == key || SimplifyOscControlKey(candidate) == SimplifyOscControlKey(key);
|
|
||||||
}
|
|
||||||
|
|
||||||
double ClampOscAlpha(double value)
|
|
||||||
{
|
|
||||||
return (std::max)(0.0, (std::min)(1.0, value));
|
|
||||||
}
|
|
||||||
|
|
||||||
double ComputeTimeBasedOscAlpha(double smoothing, double deltaSeconds)
|
|
||||||
{
|
|
||||||
const double clampedSmoothing = ClampOscAlpha(smoothing);
|
|
||||||
if (clampedSmoothing <= 0.0)
|
|
||||||
return 0.0;
|
|
||||||
if (clampedSmoothing >= 1.0)
|
|
||||||
return 1.0;
|
|
||||||
|
|
||||||
const double clampedDeltaSeconds = (std::max)(0.0, (std::min)(kOscSmoothingMaxStepSeconds, deltaSeconds));
|
|
||||||
if (clampedDeltaSeconds <= 0.0)
|
|
||||||
return 0.0;
|
|
||||||
|
|
||||||
const double frameScale = clampedDeltaSeconds * kOscSmoothingReferenceFps;
|
|
||||||
return ClampOscAlpha(1.0 - std::pow(1.0 - clampedSmoothing, frameScale));
|
|
||||||
}
|
|
||||||
|
|
||||||
JsonValue BuildOscCommitValue(const ShaderParameterDefinition& definition, const ShaderParameterValue& value)
|
|
||||||
{
|
|
||||||
switch (definition.type)
|
|
||||||
{
|
|
||||||
case ShaderParameterType::Boolean:
|
|
||||||
return JsonValue(value.booleanValue);
|
|
||||||
case ShaderParameterType::Enum:
|
|
||||||
return JsonValue(value.enumValue);
|
|
||||||
case ShaderParameterType::Text:
|
|
||||||
return JsonValue(value.textValue);
|
|
||||||
case ShaderParameterType::Trigger:
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return JsonValue();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
RenderEngine::RenderEngine(
|
RenderEngine::RenderEngine(
|
||||||
@@ -229,51 +160,24 @@ void RenderEngine::ApplyRuntimeCoordinatorRenderReset(RuntimeCoordinatorRenderRe
|
|||||||
|
|
||||||
void RenderEngine::ClearOscOverlayState()
|
void RenderEngine::ClearOscOverlayState()
|
||||||
{
|
{
|
||||||
mOscOverlayStates.clear();
|
mRuntimeLiveState.Clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
void RenderEngine::UpdateOscOverlayState(
|
void RenderEngine::UpdateOscOverlayState(
|
||||||
const std::vector<OscOverlayUpdate>& updates,
|
const std::vector<OscOverlayUpdate>& updates,
|
||||||
const std::vector<OscOverlayCommitCompletion>& completedCommits)
|
const std::vector<OscOverlayCommitCompletion>& completedCommits)
|
||||||
{
|
{
|
||||||
|
std::vector<RuntimeLiveOscCommitCompletion> liveCompletions;
|
||||||
|
liveCompletions.reserve(completedCommits.size());
|
||||||
for (const OscOverlayCommitCompletion& completedCommit : completedCommits)
|
for (const OscOverlayCommitCompletion& completedCommit : completedCommits)
|
||||||
{
|
liveCompletions.push_back({ completedCommit.routeKey, completedCommit.generation });
|
||||||
auto overlayIt = mOscOverlayStates.find(completedCommit.routeKey);
|
mRuntimeLiveState.ApplyOscCommitCompletions(liveCompletions);
|
||||||
if (overlayIt == mOscOverlayStates.end())
|
|
||||||
continue;
|
|
||||||
|
|
||||||
OscOverlayState& overlay = overlayIt->second;
|
std::vector<RuntimeLiveOscUpdate> liveUpdates;
|
||||||
if (overlay.commitQueued &&
|
liveUpdates.reserve(updates.size());
|
||||||
overlay.pendingCommitGeneration == completedCommit.generation &&
|
|
||||||
overlay.generation == completedCommit.generation)
|
|
||||||
{
|
|
||||||
mOscOverlayStates.erase(overlayIt);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const auto now = std::chrono::steady_clock::now();
|
|
||||||
for (const OscOverlayUpdate& update : updates)
|
for (const OscOverlayUpdate& update : updates)
|
||||||
{
|
liveUpdates.push_back({ update.routeKey, update.layerKey, update.parameterKey, update.targetValue });
|
||||||
auto overlayIt = mOscOverlayStates.find(update.routeKey);
|
mRuntimeLiveState.ApplyOscUpdates(liveUpdates);
|
||||||
if (overlayIt == mOscOverlayStates.end())
|
|
||||||
{
|
|
||||||
OscOverlayState overlay;
|
|
||||||
overlay.layerKey = update.layerKey;
|
|
||||||
overlay.parameterKey = update.parameterKey;
|
|
||||||
overlay.targetValue = update.targetValue;
|
|
||||||
overlay.lastUpdatedTime = now;
|
|
||||||
overlay.lastAppliedTime = now;
|
|
||||||
overlay.generation = 1;
|
|
||||||
mOscOverlayStates[update.routeKey] = std::move(overlay);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
overlayIt->second.targetValue = update.targetValue;
|
|
||||||
overlayIt->second.lastUpdatedTime = now;
|
|
||||||
overlayIt->second.generation += 1;
|
|
||||||
overlayIt->second.commitQueued = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void RenderEngine::ResizeView(int width, int height)
|
void RenderEngine::ResizeView(int width, int height)
|
||||||
@@ -416,129 +320,19 @@ void RenderEngine::ApplyOscOverlays(
|
|||||||
double smoothing,
|
double smoothing,
|
||||||
std::vector<OscOverlayCommitRequest>* commitRequests)
|
std::vector<OscOverlayCommitRequest>* commitRequests)
|
||||||
{
|
{
|
||||||
if (states.empty() || mOscOverlayStates.empty())
|
std::vector<RuntimeLiveOscCommitRequest> liveCommitRequests;
|
||||||
|
RuntimeLiveStateApplyOptions options;
|
||||||
|
options.allowCommit = allowCommit;
|
||||||
|
options.smoothing = smoothing;
|
||||||
|
options.commitDelay = kOscOverlayCommitDelay;
|
||||||
|
options.now = std::chrono::steady_clock::now();
|
||||||
|
mRuntimeLiveState.ApplyToLayerStates(states, options, commitRequests ? &liveCommitRequests : nullptr);
|
||||||
|
|
||||||
|
if (!commitRequests)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
const auto now = std::chrono::steady_clock::now();
|
for (const RuntimeLiveOscCommitRequest& request : liveCommitRequests)
|
||||||
const double clampedSmoothing = ClampOscAlpha(smoothing);
|
commitRequests->push_back({ request.routeKey, request.layerKey, request.parameterKey, request.value, request.generation });
|
||||||
std::vector<std::string> overlayKeysToRemove;
|
|
||||||
|
|
||||||
for (auto& item : mOscOverlayStates)
|
|
||||||
{
|
|
||||||
const std::string& routeKey = item.first;
|
|
||||||
OscOverlayState& overlay = item.second;
|
|
||||||
auto stateIt = std::find_if(states.begin(), states.end(),
|
|
||||||
[&overlay](const RuntimeRenderState& state)
|
|
||||||
{
|
|
||||||
return MatchesOscControlKey(state.layerId, overlay.layerKey) ||
|
|
||||||
MatchesOscControlKey(state.shaderId, overlay.layerKey) ||
|
|
||||||
MatchesOscControlKey(state.shaderName, overlay.layerKey);
|
|
||||||
});
|
|
||||||
if (stateIt == states.end())
|
|
||||||
continue;
|
|
||||||
|
|
||||||
auto definitionIt = std::find_if(stateIt->parameterDefinitions.begin(), stateIt->parameterDefinitions.end(),
|
|
||||||
[&overlay](const ShaderParameterDefinition& definition)
|
|
||||||
{
|
|
||||||
return MatchesOscControlKey(definition.id, overlay.parameterKey) ||
|
|
||||||
MatchesOscControlKey(definition.label, overlay.parameterKey);
|
|
||||||
});
|
|
||||||
if (definitionIt == stateIt->parameterDefinitions.end())
|
|
||||||
continue;
|
|
||||||
|
|
||||||
ShaderParameterValue targetValue;
|
|
||||||
std::string normalizeError;
|
|
||||||
if (!NormalizeAndValidateParameterValue(*definitionIt, overlay.targetValue, targetValue, normalizeError))
|
|
||||||
continue;
|
|
||||||
|
|
||||||
if (definitionIt->type == ShaderParameterType::Trigger)
|
|
||||||
{
|
|
||||||
ShaderParameterValue& value = stateIt->parameterValues[definitionIt->id];
|
|
||||||
const double previousCount = value.numberValues.empty() ? 0.0 : value.numberValues[0];
|
|
||||||
const double triggerTime = stateIt->timeSeconds;
|
|
||||||
value.numberValues = { previousCount + 1.0, triggerTime };
|
|
||||||
overlayKeysToRemove.push_back(routeKey);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const bool smoothable =
|
|
||||||
clampedSmoothing > 0.0 &&
|
|
||||||
(definitionIt->type == ShaderParameterType::Float ||
|
|
||||||
definitionIt->type == ShaderParameterType::Vec2 ||
|
|
||||||
definitionIt->type == ShaderParameterType::Color);
|
|
||||||
if (!smoothable)
|
|
||||||
{
|
|
||||||
overlay.currentValue = targetValue;
|
|
||||||
overlay.hasCurrentValue = true;
|
|
||||||
stateIt->parameterValues[definitionIt->id] = overlay.currentValue;
|
|
||||||
if (allowCommit &&
|
|
||||||
!overlay.commitQueued &&
|
|
||||||
now - overlay.lastUpdatedTime >= kOscOverlayCommitDelay &&
|
|
||||||
commitRequests)
|
|
||||||
{
|
|
||||||
commitRequests->push_back({ routeKey, overlay.layerKey, overlay.parameterKey, overlay.targetValue, overlay.generation });
|
|
||||||
overlay.pendingCommitGeneration = overlay.generation;
|
|
||||||
overlay.commitQueued = true;
|
|
||||||
}
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!overlay.hasCurrentValue)
|
|
||||||
{
|
|
||||||
overlay.currentValue = DefaultValueForDefinition(*definitionIt);
|
|
||||||
auto currentIt = stateIt->parameterValues.find(definitionIt->id);
|
|
||||||
if (currentIt != stateIt->parameterValues.end())
|
|
||||||
overlay.currentValue = currentIt->second;
|
|
||||||
overlay.hasCurrentValue = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (overlay.currentValue.numberValues.size() != targetValue.numberValues.size())
|
|
||||||
overlay.currentValue.numberValues = targetValue.numberValues;
|
|
||||||
|
|
||||||
double smoothingAlpha = clampedSmoothing;
|
|
||||||
if (overlay.lastAppliedTime != std::chrono::steady_clock::time_point())
|
|
||||||
{
|
|
||||||
const double deltaSeconds =
|
|
||||||
std::chrono::duration_cast<std::chrono::duration<double>>(now - overlay.lastAppliedTime).count();
|
|
||||||
smoothingAlpha = ComputeTimeBasedOscAlpha(clampedSmoothing, deltaSeconds);
|
|
||||||
}
|
|
||||||
overlay.lastAppliedTime = now;
|
|
||||||
|
|
||||||
ShaderParameterValue nextValue = targetValue;
|
|
||||||
bool converged = true;
|
|
||||||
for (std::size_t index = 0; index < targetValue.numberValues.size(); ++index)
|
|
||||||
{
|
|
||||||
const double currentNumber = overlay.currentValue.numberValues[index];
|
|
||||||
const double targetNumber = targetValue.numberValues[index];
|
|
||||||
const double delta = targetNumber - currentNumber;
|
|
||||||
double nextNumber = currentNumber + delta * smoothingAlpha;
|
|
||||||
if (std::fabs(delta) <= 0.0005)
|
|
||||||
nextNumber = targetNumber;
|
|
||||||
else
|
|
||||||
converged = false;
|
|
||||||
nextValue.numberValues[index] = nextNumber;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (converged)
|
|
||||||
nextValue.numberValues = targetValue.numberValues;
|
|
||||||
|
|
||||||
overlay.currentValue = nextValue;
|
|
||||||
overlay.hasCurrentValue = true;
|
|
||||||
stateIt->parameterValues[definitionIt->id] = overlay.currentValue;
|
|
||||||
if (allowCommit &&
|
|
||||||
converged &&
|
|
||||||
!overlay.commitQueued &&
|
|
||||||
now - overlay.lastUpdatedTime >= kOscOverlayCommitDelay &&
|
|
||||||
commitRequests)
|
|
||||||
{
|
|
||||||
commitRequests->push_back({ routeKey, overlay.layerKey, overlay.parameterKey, BuildOscCommitValue(*definitionIt, overlay.currentValue), overlay.generation });
|
|
||||||
overlay.pendingCommitGeneration = overlay.generation;
|
|
||||||
overlay.commitQueued = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const std::string& overlayKey : overlayKeysToRemove)
|
|
||||||
mOscOverlayStates.erase(overlayKey);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void RenderEngine::RenderLayerStack(
|
void RenderEngine::RenderLayerStack(
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
#include "OpenGLShaderPrograms.h"
|
#include "OpenGLShaderPrograms.h"
|
||||||
#include "HealthTelemetry.h"
|
#include "HealthTelemetry.h"
|
||||||
#include "RuntimeCoordinator.h"
|
#include "RuntimeCoordinator.h"
|
||||||
|
#include "RuntimeLiveState.h"
|
||||||
#include "RuntimeSnapshotProvider.h"
|
#include "RuntimeSnapshotProvider.h"
|
||||||
|
|
||||||
#include <windows.h>
|
#include <windows.h>
|
||||||
@@ -132,20 +133,6 @@ private:
|
|||||||
HDC mHdc;
|
HDC mHdc;
|
||||||
HGLRC mHglrc;
|
HGLRC mHglrc;
|
||||||
|
|
||||||
struct OscOverlayState
|
|
||||||
{
|
|
||||||
std::string layerKey;
|
|
||||||
std::string parameterKey;
|
|
||||||
JsonValue targetValue;
|
|
||||||
ShaderParameterValue currentValue;
|
|
||||||
bool hasCurrentValue = false;
|
|
||||||
std::chrono::steady_clock::time_point lastUpdatedTime;
|
|
||||||
std::chrono::steady_clock::time_point lastAppliedTime;
|
|
||||||
uint64_t generation = 0;
|
|
||||||
uint64_t pendingCommitGeneration = 0;
|
|
||||||
bool commitQueued = false;
|
|
||||||
};
|
|
||||||
|
|
||||||
void ApplyOscOverlays(
|
void ApplyOscOverlays(
|
||||||
std::vector<RuntimeRenderState>& states,
|
std::vector<RuntimeRenderState>& states,
|
||||||
bool allowCommit,
|
bool allowCommit,
|
||||||
@@ -158,5 +145,5 @@ private:
|
|||||||
unsigned mCachedRenderStateWidth = 0;
|
unsigned mCachedRenderStateWidth = 0;
|
||||||
unsigned mCachedRenderStateHeight = 0;
|
unsigned mCachedRenderStateHeight = 0;
|
||||||
std::chrono::steady_clock::time_point mLastPreviewPresentTime;
|
std::chrono::steady_clock::time_point mLastPreviewPresentTime;
|
||||||
std::map<std::string, OscOverlayState> mOscOverlayStates;
|
RuntimeLiveState mRuntimeLiveState;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
#include "RenderStateComposer.h"
|
||||||
|
|
||||||
|
RenderStateCompositionResult RenderStateComposer::BuildFrameState(const RenderStateCompositionInput& input) const
|
||||||
|
{
|
||||||
|
RenderStateCompositionResult result;
|
||||||
|
result.layerStates = input.baseLayerStates;
|
||||||
|
if (input.liveState)
|
||||||
|
input.liveState->ApplyToLayerStates(result.layerStates, input.liveStateOptions, &result.commitRequests);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "RuntimeLiveState.h"
|
||||||
|
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
struct RenderStateCompositionInput
|
||||||
|
{
|
||||||
|
std::vector<RuntimeRenderState> baseLayerStates;
|
||||||
|
RuntimeLiveState* liveState = nullptr;
|
||||||
|
RuntimeLiveStateApplyOptions liveStateOptions;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct RenderStateCompositionResult
|
||||||
|
{
|
||||||
|
std::vector<RuntimeRenderState> layerStates;
|
||||||
|
std::vector<RuntimeLiveOscCommitRequest> commitRequests;
|
||||||
|
};
|
||||||
|
|
||||||
|
class RenderStateComposer
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
RenderStateCompositionResult BuildFrameState(const RenderStateCompositionInput& input) const;
|
||||||
|
};
|
||||||
@@ -0,0 +1,262 @@
|
|||||||
|
#include "RuntimeLiveState.h"
|
||||||
|
|
||||||
|
#include "RuntimeParameterUtils.h"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cctype>
|
||||||
|
#include <cmath>
|
||||||
|
#include <cstddef>
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
constexpr double kOscSmoothingReferenceFps = 60.0;
|
||||||
|
constexpr double kOscSmoothingMaxStepSeconds = 0.25;
|
||||||
|
|
||||||
|
std::string SimplifyOscControlKey(const std::string& text)
|
||||||
|
{
|
||||||
|
std::string simplified;
|
||||||
|
for (unsigned char ch : text)
|
||||||
|
{
|
||||||
|
if (std::isalnum(ch))
|
||||||
|
simplified.push_back(static_cast<char>(std::tolower(ch)));
|
||||||
|
}
|
||||||
|
return simplified;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool MatchesOscControlKey(const std::string& candidate, const std::string& key)
|
||||||
|
{
|
||||||
|
return candidate == key || SimplifyOscControlKey(candidate) == SimplifyOscControlKey(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
double ClampOscAlpha(double value)
|
||||||
|
{
|
||||||
|
return (std::max)(0.0, (std::min)(1.0, value));
|
||||||
|
}
|
||||||
|
|
||||||
|
double ComputeTimeBasedOscAlpha(double smoothing, double deltaSeconds)
|
||||||
|
{
|
||||||
|
const double clampedSmoothing = ClampOscAlpha(smoothing);
|
||||||
|
if (clampedSmoothing <= 0.0)
|
||||||
|
return 0.0;
|
||||||
|
if (clampedSmoothing >= 1.0)
|
||||||
|
return 1.0;
|
||||||
|
|
||||||
|
const double clampedDeltaSeconds = (std::max)(0.0, (std::min)(kOscSmoothingMaxStepSeconds, deltaSeconds));
|
||||||
|
if (clampedDeltaSeconds <= 0.0)
|
||||||
|
return 0.0;
|
||||||
|
|
||||||
|
const double frameScale = clampedDeltaSeconds * kOscSmoothingReferenceFps;
|
||||||
|
return ClampOscAlpha(1.0 - std::pow(1.0 - clampedSmoothing, frameScale));
|
||||||
|
}
|
||||||
|
|
||||||
|
JsonValue BuildOscCommitValue(const ShaderParameterDefinition& definition, const ShaderParameterValue& value)
|
||||||
|
{
|
||||||
|
switch (definition.type)
|
||||||
|
{
|
||||||
|
case ShaderParameterType::Boolean:
|
||||||
|
return JsonValue(value.booleanValue);
|
||||||
|
case ShaderParameterType::Enum:
|
||||||
|
return JsonValue(value.enumValue);
|
||||||
|
case ShaderParameterType::Text:
|
||||||
|
return JsonValue(value.textValue);
|
||||||
|
case ShaderParameterType::Trigger:
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return JsonValue();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void RuntimeLiveState::Clear()
|
||||||
|
{
|
||||||
|
mOscOverlayStates.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
std::size_t RuntimeLiveState::OverlayCount() const
|
||||||
|
{
|
||||||
|
return mOscOverlayStates.size();
|
||||||
|
}
|
||||||
|
|
||||||
|
void RuntimeLiveState::ApplyOscUpdates(const std::vector<RuntimeLiveOscUpdate>& updates)
|
||||||
|
{
|
||||||
|
const auto now = std::chrono::steady_clock::now();
|
||||||
|
for (const RuntimeLiveOscUpdate& update : updates)
|
||||||
|
{
|
||||||
|
auto overlayIt = mOscOverlayStates.find(update.routeKey);
|
||||||
|
if (overlayIt == mOscOverlayStates.end())
|
||||||
|
{
|
||||||
|
OscOverlayState overlay;
|
||||||
|
overlay.layerKey = update.layerKey;
|
||||||
|
overlay.parameterKey = update.parameterKey;
|
||||||
|
overlay.targetValue = update.targetValue;
|
||||||
|
overlay.lastUpdatedTime = now;
|
||||||
|
overlay.lastAppliedTime = now;
|
||||||
|
overlay.generation = 1;
|
||||||
|
mOscOverlayStates[update.routeKey] = std::move(overlay);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
overlayIt->second.targetValue = update.targetValue;
|
||||||
|
overlayIt->second.lastUpdatedTime = now;
|
||||||
|
overlayIt->second.generation += 1;
|
||||||
|
overlayIt->second.commitQueued = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void RuntimeLiveState::ApplyOscCommitCompletions(const std::vector<RuntimeLiveOscCommitCompletion>& completedCommits)
|
||||||
|
{
|
||||||
|
for (const RuntimeLiveOscCommitCompletion& completedCommit : completedCommits)
|
||||||
|
{
|
||||||
|
auto overlayIt = mOscOverlayStates.find(completedCommit.routeKey);
|
||||||
|
if (overlayIt == mOscOverlayStates.end())
|
||||||
|
continue;
|
||||||
|
|
||||||
|
OscOverlayState& overlay = overlayIt->second;
|
||||||
|
if (overlay.commitQueued &&
|
||||||
|
overlay.pendingCommitGeneration == completedCommit.generation &&
|
||||||
|
overlay.generation == completedCommit.generation)
|
||||||
|
{
|
||||||
|
mOscOverlayStates.erase(overlayIt);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void RuntimeLiveState::ApplyToLayerStates(
|
||||||
|
std::vector<RuntimeRenderState>& states,
|
||||||
|
const RuntimeLiveStateApplyOptions& options,
|
||||||
|
std::vector<RuntimeLiveOscCommitRequest>* commitRequests)
|
||||||
|
{
|
||||||
|
if (states.empty() || mOscOverlayStates.empty())
|
||||||
|
return;
|
||||||
|
|
||||||
|
const auto now = options.now;
|
||||||
|
const double clampedSmoothing = ClampOscAlpha(options.smoothing);
|
||||||
|
std::vector<std::string> overlayKeysToRemove;
|
||||||
|
|
||||||
|
for (auto& item : mOscOverlayStates)
|
||||||
|
{
|
||||||
|
const std::string& routeKey = item.first;
|
||||||
|
OscOverlayState& overlay = item.second;
|
||||||
|
auto stateIt = std::find_if(states.begin(), states.end(),
|
||||||
|
[&overlay](const RuntimeRenderState& state)
|
||||||
|
{
|
||||||
|
return MatchesOscControlKey(state.layerId, overlay.layerKey) ||
|
||||||
|
MatchesOscControlKey(state.shaderId, overlay.layerKey) ||
|
||||||
|
MatchesOscControlKey(state.shaderName, overlay.layerKey);
|
||||||
|
});
|
||||||
|
if (stateIt == states.end())
|
||||||
|
continue;
|
||||||
|
|
||||||
|
auto definitionIt = std::find_if(stateIt->parameterDefinitions.begin(), stateIt->parameterDefinitions.end(),
|
||||||
|
[&overlay](const ShaderParameterDefinition& definition)
|
||||||
|
{
|
||||||
|
return MatchesOscControlKey(definition.id, overlay.parameterKey) ||
|
||||||
|
MatchesOscControlKey(definition.label, overlay.parameterKey);
|
||||||
|
});
|
||||||
|
if (definitionIt == stateIt->parameterDefinitions.end())
|
||||||
|
continue;
|
||||||
|
|
||||||
|
ShaderParameterValue targetValue;
|
||||||
|
std::string normalizeError;
|
||||||
|
if (!NormalizeAndValidateParameterValue(*definitionIt, overlay.targetValue, targetValue, normalizeError))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
if (definitionIt->type == ShaderParameterType::Trigger)
|
||||||
|
{
|
||||||
|
ShaderParameterValue& value = stateIt->parameterValues[definitionIt->id];
|
||||||
|
const double previousCount = value.numberValues.empty() ? 0.0 : value.numberValues[0];
|
||||||
|
const double triggerTime = stateIt->timeSeconds;
|
||||||
|
value.numberValues = { previousCount + 1.0, triggerTime };
|
||||||
|
overlayKeysToRemove.push_back(routeKey);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const bool smoothable =
|
||||||
|
clampedSmoothing > 0.0 &&
|
||||||
|
(definitionIt->type == ShaderParameterType::Float ||
|
||||||
|
definitionIt->type == ShaderParameterType::Vec2 ||
|
||||||
|
definitionIt->type == ShaderParameterType::Color);
|
||||||
|
if (!smoothable)
|
||||||
|
{
|
||||||
|
overlay.currentValue = targetValue;
|
||||||
|
overlay.hasCurrentValue = true;
|
||||||
|
stateIt->parameterValues[definitionIt->id] = overlay.currentValue;
|
||||||
|
if (options.allowCommit &&
|
||||||
|
!overlay.commitQueued &&
|
||||||
|
now - overlay.lastUpdatedTime >= options.commitDelay &&
|
||||||
|
commitRequests)
|
||||||
|
{
|
||||||
|
commitRequests->push_back({ routeKey, overlay.layerKey, overlay.parameterKey, overlay.targetValue, overlay.generation });
|
||||||
|
overlay.pendingCommitGeneration = overlay.generation;
|
||||||
|
overlay.commitQueued = true;
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!overlay.hasCurrentValue)
|
||||||
|
{
|
||||||
|
overlay.currentValue = DefaultValueForDefinition(*definitionIt);
|
||||||
|
auto currentIt = stateIt->parameterValues.find(definitionIt->id);
|
||||||
|
if (currentIt != stateIt->parameterValues.end())
|
||||||
|
overlay.currentValue = currentIt->second;
|
||||||
|
overlay.hasCurrentValue = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (overlay.currentValue.numberValues.size() != targetValue.numberValues.size())
|
||||||
|
overlay.currentValue.numberValues = targetValue.numberValues;
|
||||||
|
|
||||||
|
double smoothingAlpha = clampedSmoothing;
|
||||||
|
if (overlay.lastAppliedTime != std::chrono::steady_clock::time_point())
|
||||||
|
{
|
||||||
|
const double deltaSeconds =
|
||||||
|
std::chrono::duration_cast<std::chrono::duration<double>>(now - overlay.lastAppliedTime).count();
|
||||||
|
smoothingAlpha = ComputeTimeBasedOscAlpha(clampedSmoothing, deltaSeconds);
|
||||||
|
}
|
||||||
|
overlay.lastAppliedTime = now;
|
||||||
|
|
||||||
|
ShaderParameterValue nextValue = targetValue;
|
||||||
|
bool converged = true;
|
||||||
|
for (std::size_t index = 0; index < targetValue.numberValues.size(); ++index)
|
||||||
|
{
|
||||||
|
const double currentNumber = overlay.currentValue.numberValues[index];
|
||||||
|
const double targetNumber = targetValue.numberValues[index];
|
||||||
|
const double delta = targetNumber - currentNumber;
|
||||||
|
double nextNumber = currentNumber + delta * smoothingAlpha;
|
||||||
|
if (std::fabs(delta) <= 0.0005)
|
||||||
|
nextNumber = targetNumber;
|
||||||
|
else
|
||||||
|
converged = false;
|
||||||
|
nextValue.numberValues[index] = nextNumber;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (converged)
|
||||||
|
nextValue.numberValues = targetValue.numberValues;
|
||||||
|
|
||||||
|
overlay.currentValue = nextValue;
|
||||||
|
overlay.hasCurrentValue = true;
|
||||||
|
stateIt->parameterValues[definitionIt->id] = overlay.currentValue;
|
||||||
|
if (options.allowCommit &&
|
||||||
|
converged &&
|
||||||
|
!overlay.commitQueued &&
|
||||||
|
now - overlay.lastUpdatedTime >= options.commitDelay &&
|
||||||
|
commitRequests)
|
||||||
|
{
|
||||||
|
commitRequests->push_back({ routeKey, overlay.layerKey, overlay.parameterKey, BuildOscCommitValue(*definitionIt, overlay.currentValue), overlay.generation });
|
||||||
|
overlay.pendingCommitGeneration = overlay.generation;
|
||||||
|
overlay.commitQueued = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const std::string& overlayKey : overlayKeysToRemove)
|
||||||
|
mOscOverlayStates.erase(overlayKey);
|
||||||
|
}
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "RuntimeJson.h"
|
||||||
|
#include "ShaderTypes.h"
|
||||||
|
|
||||||
|
#include <chrono>
|
||||||
|
#include <cstdint>
|
||||||
|
#include <map>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
struct RuntimeLiveOscUpdate
|
||||||
|
{
|
||||||
|
std::string routeKey;
|
||||||
|
std::string layerKey;
|
||||||
|
std::string parameterKey;
|
||||||
|
JsonValue targetValue;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct RuntimeLiveOscCommitCompletion
|
||||||
|
{
|
||||||
|
std::string routeKey;
|
||||||
|
uint64_t generation = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct RuntimeLiveOscCommitRequest
|
||||||
|
{
|
||||||
|
std::string routeKey;
|
||||||
|
std::string layerKey;
|
||||||
|
std::string parameterKey;
|
||||||
|
JsonValue value;
|
||||||
|
uint64_t generation = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct RuntimeLiveStateApplyOptions
|
||||||
|
{
|
||||||
|
bool allowCommit = false;
|
||||||
|
double smoothing = 0.0;
|
||||||
|
std::chrono::milliseconds commitDelay = std::chrono::milliseconds(150);
|
||||||
|
std::chrono::steady_clock::time_point now = std::chrono::steady_clock::now();
|
||||||
|
};
|
||||||
|
|
||||||
|
class RuntimeLiveState
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
void Clear();
|
||||||
|
std::size_t OverlayCount() const;
|
||||||
|
void ApplyOscUpdates(const std::vector<RuntimeLiveOscUpdate>& updates);
|
||||||
|
void ApplyOscCommitCompletions(const std::vector<RuntimeLiveOscCommitCompletion>& completedCommits);
|
||||||
|
void ApplyToLayerStates(
|
||||||
|
std::vector<RuntimeRenderState>& states,
|
||||||
|
const RuntimeLiveStateApplyOptions& options,
|
||||||
|
std::vector<RuntimeLiveOscCommitRequest>* commitRequests);
|
||||||
|
|
||||||
|
private:
|
||||||
|
struct OscOverlayState
|
||||||
|
{
|
||||||
|
std::string layerKey;
|
||||||
|
std::string parameterKey;
|
||||||
|
JsonValue targetValue;
|
||||||
|
ShaderParameterValue currentValue;
|
||||||
|
bool hasCurrentValue = false;
|
||||||
|
std::chrono::steady_clock::time_point lastUpdatedTime;
|
||||||
|
std::chrono::steady_clock::time_point lastAppliedTime;
|
||||||
|
uint64_t generation = 0;
|
||||||
|
uint64_t pendingCommitGeneration = 0;
|
||||||
|
bool commitQueued = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
std::map<std::string, OscOverlayState> mOscOverlayStates;
|
||||||
|
};
|
||||||
@@ -7,7 +7,7 @@ Phase 1 split runtime responsibilities into named subsystems. Phase 2 added the
|
|||||||
## Status
|
## Status
|
||||||
|
|
||||||
- Phase 3 design package: proposed.
|
- Phase 3 design package: proposed.
|
||||||
- Phase 3 implementation: not started.
|
- Phase 3 implementation: groundwork started.
|
||||||
- Current alignment: the repo has the right building blocks, but `OpenGLComposite::renderEffect()` still manually reconciles transient OSC overlays, completed OSC commits, committed/live snapshot selection, and render-state resolution on the render path.
|
- Current alignment: the repo has the right building blocks, but `OpenGLComposite::renderEffect()` still manually reconciles transient OSC overlays, completed OSC commits, committed/live snapshot selection, and render-state resolution on the render path.
|
||||||
|
|
||||||
Current footholds:
|
Current footholds:
|
||||||
@@ -15,7 +15,9 @@ Current footholds:
|
|||||||
- `RuntimeStore` is split into durable state collaborators: `RuntimeConfigStore`, `LayerStackStore`, `ShaderPackageCatalog`, `RenderSnapshotBuilder`, presentation read models, and `HealthTelemetry`.
|
- `RuntimeStore` is split into durable state collaborators: `RuntimeConfigStore`, `LayerStackStore`, `ShaderPackageCatalog`, `RenderSnapshotBuilder`, presentation read models, and `HealthTelemetry`.
|
||||||
- `RuntimeCoordinator` owns mutation validation/classification and publishes accepted/rejected/follow-up events.
|
- `RuntimeCoordinator` owns mutation validation/classification and publishes accepted/rejected/follow-up events.
|
||||||
- `RuntimeSnapshotProvider` publishes render snapshots from `RenderSnapshotBuilder`.
|
- `RuntimeSnapshotProvider` publishes render snapshots from `RenderSnapshotBuilder`.
|
||||||
- `RenderEngine` owns render-local OSC overlay state and final render-layer resolution.
|
- `RuntimeLiveState` owns transient OSC overlay bookkeeping and commit-settlement policy.
|
||||||
|
- `RenderStateComposer` exists as the first pure composition boundary for combining base layer state with live overlays.
|
||||||
|
- `RenderEngine` still owns final render-layer resolution, but its OSC overlay bookkeeping now delegates to `RuntimeLiveState`.
|
||||||
- `ControlServices` owns OSC ingress, pending OSC updates, completed OSC commit notifications, and service start/stop.
|
- `ControlServices` owns OSC ingress, pending OSC updates, completed OSC commit notifications, and service start/stop.
|
||||||
- `RuntimeEventDispatcher` now routes accepted mutations, reloads, snapshots, shader build events, backend observations, and health observations.
|
- `RuntimeEventDispatcher` now routes accepted mutations, reloads, snapshots, shader build events, backend observations, and health observations.
|
||||||
|
|
||||||
@@ -250,6 +252,8 @@ Introduce `RuntimeLiveState`, `RenderStateComposer`, or an equivalent pair of cl
|
|||||||
|
|
||||||
Start by moving pure data operations out of `RenderEngine::ResolveRenderLayerStates(...)` without changing behavior.
|
Start by moving pure data operations out of `RenderEngine::ResolveRenderLayerStates(...)` without changing behavior.
|
||||||
|
|
||||||
|
Status: started. `runtime/live/RuntimeLiveState` and `runtime/live/RenderStateComposer` now exist, are included in the build, and have a focused `RuntimeLiveStateTests` target.
|
||||||
|
|
||||||
### Step 2. Move OSC Overlay Bookkeeping Behind The Boundary
|
### Step 2. Move OSC Overlay Bookkeeping Behind The Boundary
|
||||||
|
|
||||||
Move these responsibilities out of the current frame orchestration:
|
Move these responsibilities out of the current frame orchestration:
|
||||||
@@ -261,6 +265,8 @@ Move these responsibilities out of the current frame orchestration:
|
|||||||
|
|
||||||
The first implementation can still be called synchronously from the current render path. The important part is that the behavior has a named owner and tests.
|
The first implementation can still be called synchronously from the current render path. The important part is that the behavior has a named owner and tests.
|
||||||
|
|
||||||
|
Status: started. `RenderEngine` still exposes the compatibility methods used by `OpenGLComposite`, but it now delegates overlay updates, commit completions, smoothing, generation matching, and commit-request creation to `RuntimeLiveState`.
|
||||||
|
|
||||||
### Step 3. Bridge Service Queues To Events Or Live-State Commands
|
### Step 3. Bridge Service Queues To Events Or Live-State Commands
|
||||||
|
|
||||||
Replace `OpenGLComposite::renderEffect()` queue draining with a bridge that publishes or applies:
|
Replace `OpenGLComposite::renderEffect()` queue draining with a bridge that publishes or applies:
|
||||||
@@ -324,14 +330,26 @@ Existing useful homes:
|
|||||||
|
|
||||||
- `RuntimeSubsystemTests` for pure state/composer behavior
|
- `RuntimeSubsystemTests` for pure state/composer behavior
|
||||||
- `RuntimeEventTypeTests` for event bridge behavior
|
- `RuntimeEventTypeTests` for event bridge behavior
|
||||||
- a new `RuntimeLiveStateTests.cpp` target if the live-state code grows enough
|
- `RuntimeLiveStateTests` for the new live-state/composer boundary
|
||||||
|
|
||||||
|
## Parallel Work Lanes
|
||||||
|
|
||||||
|
The current groundwork is intended to let these lanes proceed in parallel with low overlap:
|
||||||
|
|
||||||
|
| Lane | Primary files | Goal |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| A. Live-state behavior | `runtime/live/RuntimeLiveState.*`, `tests/RuntimeLiveStateTests.cpp` | Finish stale completion tests, smoothing edge cases, trigger behavior, and overlay settle policy. |
|
||||||
|
| B. Render-state composition | `runtime/live/RenderStateComposer.*`, `gl/RenderEngine.*` | Move more of `RenderEngine::ResolveRenderLayerStates(...)` value composition behind the pure composer while keeping GL calls in `RenderEngine`. |
|
||||||
|
| C. Service bridge | `control/RuntimeServices.*`, `control/ControlServices.*`, possible new bridge class | Stop `OpenGLComposite::renderEffect()` from draining OSC update/completion queues directly. |
|
||||||
|
| D. App-frame orchestration | `gl/OpenGLComposite.*`, `gl/RuntimeUpdateController.*` | Replace render-effect glue with a narrow frame-state preparation call and commit-request handoff. |
|
||||||
|
| E. Persistence boundary | `runtime/coordination/RuntimeCoordinator.*`, `runtime/store/*`, event tests | Keep persistence request publication explicit and prepare for a later background writer without changing storage behavior yet. |
|
||||||
|
|
||||||
## Phase 3 Exit Criteria
|
## Phase 3 Exit Criteria
|
||||||
|
|
||||||
Phase 3 can be considered complete once the project can say:
|
Phase 3 can be considered complete once the project can say:
|
||||||
|
|
||||||
- [ ] final render-state composition has a named, testable owner outside `OpenGLComposite`
|
- [ ] final render-state composition has a named, testable owner outside `OpenGLComposite` (groundwork exists via `RenderStateComposer`; full snapshot/cache resolution still needs to move behind it)
|
||||||
- [ ] transient OSC overlay state has a named owner and tests
|
- [x] transient OSC overlay state has a named owner and tests
|
||||||
- [ ] overlay commit requests and completions no longer require `OpenGLComposite` to drain service queues directly
|
- [ ] overlay commit requests and completions no longer require `OpenGLComposite` to drain service queues directly
|
||||||
- [ ] `RenderEngine` is closer to GL/render resource ownership and less responsible for value composition
|
- [ ] `RenderEngine` is closer to GL/render resource ownership and less responsible for value composition
|
||||||
- [ ] `RuntimeStore` remains durable-state focused and does not gain live overlay responsibilities
|
- [ ] `RuntimeStore` remains durable-state focused and does not gain live overlay responsibilities
|
||||||
|
|||||||
143
tests/RuntimeLiveStateTests.cpp
Normal file
143
tests/RuntimeLiveStateTests.cpp
Normal file
@@ -0,0 +1,143 @@
|
|||||||
|
#include "RenderStateComposer.h"
|
||||||
|
#include "RuntimeLiveState.h"
|
||||||
|
|
||||||
|
#include <chrono>
|
||||||
|
#include <cmath>
|
||||||
|
#include <iostream>
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
int gFailures = 0;
|
||||||
|
|
||||||
|
void Expect(bool condition, const char* message)
|
||||||
|
{
|
||||||
|
if (condition)
|
||||||
|
return;
|
||||||
|
|
||||||
|
std::cerr << "FAIL: " << message << "\n";
|
||||||
|
++gFailures;
|
||||||
|
}
|
||||||
|
|
||||||
|
ShaderParameterDefinition FloatDefinition(const std::string& id, const std::string& label)
|
||||||
|
{
|
||||||
|
ShaderParameterDefinition definition;
|
||||||
|
definition.id = id;
|
||||||
|
definition.label = label;
|
||||||
|
definition.type = ShaderParameterType::Float;
|
||||||
|
definition.defaultNumbers = { 0.0 };
|
||||||
|
definition.minNumbers = { 0.0 };
|
||||||
|
definition.maxNumbers = { 1.0 };
|
||||||
|
return definition;
|
||||||
|
}
|
||||||
|
|
||||||
|
RuntimeRenderState MakeLayerState()
|
||||||
|
{
|
||||||
|
RuntimeRenderState state;
|
||||||
|
state.layerId = "layer-one";
|
||||||
|
state.shaderId = "test-shader";
|
||||||
|
state.shaderName = "Test Shader";
|
||||||
|
state.parameterDefinitions.push_back(FloatDefinition("amount", "Amount"));
|
||||||
|
ShaderParameterValue amount;
|
||||||
|
amount.numberValues = { 0.25 };
|
||||||
|
state.parameterValues["amount"] = amount;
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
|
||||||
|
void TestRuntimeLiveStateAppliesLatestOscOverlay()
|
||||||
|
{
|
||||||
|
RuntimeLiveState liveState;
|
||||||
|
|
||||||
|
RuntimeLiveOscUpdate first;
|
||||||
|
first.routeKey = "layer-one\namount";
|
||||||
|
first.layerKey = "layer-one";
|
||||||
|
first.parameterKey = "amount";
|
||||||
|
first.targetValue = JsonValue(0.5);
|
||||||
|
|
||||||
|
RuntimeLiveOscUpdate second = first;
|
||||||
|
second.targetValue = JsonValue(0.75);
|
||||||
|
|
||||||
|
liveState.ApplyOscUpdates({ first, second });
|
||||||
|
Expect(liveState.OverlayCount() == 1, "live state keeps one overlay per route");
|
||||||
|
|
||||||
|
std::vector<RuntimeRenderState> states = { MakeLayerState() };
|
||||||
|
RuntimeLiveStateApplyOptions options;
|
||||||
|
options.allowCommit = false;
|
||||||
|
options.smoothing = 0.0;
|
||||||
|
liveState.ApplyToLayerStates(states, options, nullptr);
|
||||||
|
|
||||||
|
const auto valueIt = states[0].parameterValues.find("amount");
|
||||||
|
Expect(valueIt != states[0].parameterValues.end(), "overlay writes the target parameter");
|
||||||
|
Expect(!valueIt->second.numberValues.empty() && std::fabs(valueIt->second.numberValues[0] - 0.75) < 0.0001,
|
||||||
|
"overlay applies the latest target value");
|
||||||
|
}
|
||||||
|
|
||||||
|
void TestRuntimeLiveStateQueuesAndCompletesCommit()
|
||||||
|
{
|
||||||
|
RuntimeLiveState liveState;
|
||||||
|
|
||||||
|
RuntimeLiveOscUpdate update;
|
||||||
|
update.routeKey = "layer-one\namount";
|
||||||
|
update.layerKey = "layer-one";
|
||||||
|
update.parameterKey = "amount";
|
||||||
|
update.targetValue = JsonValue(0.9);
|
||||||
|
liveState.ApplyOscUpdates({ update });
|
||||||
|
|
||||||
|
std::vector<RuntimeRenderState> states = { MakeLayerState() };
|
||||||
|
std::vector<RuntimeLiveOscCommitRequest> commitRequests;
|
||||||
|
RuntimeLiveStateApplyOptions options;
|
||||||
|
options.allowCommit = true;
|
||||||
|
options.smoothing = 0.0;
|
||||||
|
options.commitDelay = std::chrono::milliseconds(0);
|
||||||
|
options.now = std::chrono::steady_clock::now() + std::chrono::milliseconds(1);
|
||||||
|
liveState.ApplyToLayerStates(states, options, &commitRequests);
|
||||||
|
|
||||||
|
Expect(commitRequests.size() == 1, "live state queues a commit request once the overlay can settle");
|
||||||
|
Expect(commitRequests[0].routeKey == "layer-one\namount", "commit request preserves route");
|
||||||
|
Expect(commitRequests[0].generation == 1, "commit request carries overlay generation");
|
||||||
|
|
||||||
|
liveState.ApplyOscCommitCompletions({ { commitRequests[0].routeKey, commitRequests[0].generation } });
|
||||||
|
Expect(liveState.OverlayCount() == 0, "matching commit completion removes settled overlay");
|
||||||
|
}
|
||||||
|
|
||||||
|
void TestRenderStateComposerBuildsFrameState()
|
||||||
|
{
|
||||||
|
RuntimeLiveState liveState;
|
||||||
|
RuntimeLiveOscUpdate update;
|
||||||
|
update.routeKey = "layer-one\namount";
|
||||||
|
update.layerKey = "Test Shader";
|
||||||
|
update.parameterKey = "Amount";
|
||||||
|
update.targetValue = JsonValue(0.6);
|
||||||
|
liveState.ApplyOscUpdates({ update });
|
||||||
|
|
||||||
|
RenderStateCompositionInput input;
|
||||||
|
input.baseLayerStates = { MakeLayerState() };
|
||||||
|
input.liveState = &liveState;
|
||||||
|
input.liveStateOptions.allowCommit = false;
|
||||||
|
input.liveStateOptions.smoothing = 0.0;
|
||||||
|
|
||||||
|
RenderStateComposer composer;
|
||||||
|
RenderStateCompositionResult result = composer.BuildFrameState(input);
|
||||||
|
|
||||||
|
Expect(result.layerStates.size() == 1, "composer returns composed layer state");
|
||||||
|
const auto valueIt = result.layerStates[0].parameterValues.find("amount");
|
||||||
|
Expect(valueIt != result.layerStates[0].parameterValues.end(), "composer applies live overlay through live state");
|
||||||
|
Expect(!valueIt->second.numberValues.empty() && std::fabs(valueIt->second.numberValues[0] - 0.6) < 0.0001,
|
||||||
|
"composer uses OSC key matching against shader names and labels");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int main()
|
||||||
|
{
|
||||||
|
TestRuntimeLiveStateAppliesLatestOscOverlay();
|
||||||
|
TestRuntimeLiveStateQueuesAndCompletesCommit();
|
||||||
|
TestRenderStateComposerBuildsFrameState();
|
||||||
|
|
||||||
|
if (gFailures != 0)
|
||||||
|
{
|
||||||
|
std::cerr << gFailures << " RuntimeLiveState test failure(s).\n";
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::cout << "RuntimeLiveState tests passed.\n";
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user