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

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

View File

@@ -30,6 +30,35 @@ ShaderParameterDefinition FloatDefinition(const std::string& id, const std::stri
return definition;
}
ShaderParameterDefinition Vec2Definition(const std::string& id, const std::string& label)
{
ShaderParameterDefinition definition;
definition.id = id;
definition.label = label;
definition.type = ShaderParameterType::Vec2;
definition.defaultNumbers = { 0.0, 0.0 };
definition.minNumbers = { 0.0, 0.0 };
definition.maxNumbers = { 1.0, 1.0 };
return definition;
}
ShaderParameterDefinition TriggerDefinition(const std::string& id, const std::string& label)
{
ShaderParameterDefinition definition;
definition.id = id;
definition.label = label;
definition.type = ShaderParameterType::Trigger;
return definition;
}
JsonValue NumberArray(std::initializer_list<double> numbers)
{
JsonValue value = JsonValue::MakeArray();
for (double number : numbers)
value.pushBack(JsonValue(number));
return value;
}
RuntimeRenderState MakeLayerState()
{
RuntimeRenderState state;
@@ -43,6 +72,16 @@ RuntimeRenderState MakeLayerState()
return state;
}
RuntimeRenderState MakeLayerStateWithDefinitions(const std::vector<ShaderParameterDefinition>& definitions)
{
RuntimeRenderState state;
state.layerId = "layer-one";
state.shaderId = "test-shader";
state.shaderName = "Test Shader";
state.parameterDefinitions = definitions;
return state;
}
void TestRuntimeLiveStateAppliesLatestOscOverlay()
{
RuntimeLiveState liveState;
@@ -71,6 +110,40 @@ void TestRuntimeLiveStateAppliesLatestOscOverlay()
"overlay applies the latest target value");
}
void TestRuntimeLiveStateIgnoresStaleCommitCompletions()
{
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, "initial commit request is queued");
liveState.ApplyOscCommitCompletions({ { "other-route", commitRequests[0].generation } });
Expect(liveState.OverlayCount() == 1, "completion for another route does not remove overlay");
liveState.ApplyOscCommitCompletions({ { commitRequests[0].routeKey, commitRequests[0].generation + 1 } });
Expect(liveState.OverlayCount() == 1, "completion for another generation does not remove overlay");
RuntimeLiveOscUpdate newerUpdate = update;
newerUpdate.targetValue = JsonValue(0.2);
liveState.ApplyOscUpdates({ newerUpdate });
liveState.ApplyOscCommitCompletions({ { commitRequests[0].routeKey, commitRequests[0].generation } });
Expect(liveState.OverlayCount() == 1, "stale completion for previous generation is ignored after newer update");
}
void TestRuntimeLiveStateQueuesAndCompletesCommit()
{
RuntimeLiveState liveState;
@@ -99,6 +172,183 @@ void TestRuntimeLiveStateQueuesAndCompletesCommit()
Expect(liveState.OverlayCount() == 0, "matching commit completion removes settled overlay");
}
void TestRuntimeLiveStateQueuesOneCommitPerGeneration()
{
RuntimeLiveState liveState;
RuntimeLiveOscUpdate update;
update.routeKey = "layer-one\namount";
update.layerKey = "layer-one";
update.parameterKey = "amount";
update.targetValue = JsonValue(0.8);
liveState.ApplyOscUpdates({ update });
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);
std::vector<RuntimeRenderState> states = { MakeLayerState() };
std::vector<RuntimeLiveOscCommitRequest> commitRequests;
liveState.ApplyToLayerStates(states, options, &commitRequests);
Expect(commitRequests.size() == 1, "first apply queues one commit for generation");
Expect(commitRequests[0].generation == 1, "first commit uses generation one");
commitRequests.clear();
options.now += std::chrono::milliseconds(1);
liveState.ApplyToLayerStates(states, options, &commitRequests);
Expect(commitRequests.empty(), "second apply does not duplicate commit for same generation");
RuntimeLiveOscUpdate newerUpdate = update;
newerUpdate.targetValue = JsonValue(0.4);
liveState.ApplyOscUpdates({ newerUpdate });
commitRequests.clear();
options.now += std::chrono::milliseconds(1);
liveState.ApplyToLayerStates(states, options, &commitRequests);
Expect(commitRequests.size() == 1, "newer update allows a new commit request");
Expect(commitRequests[0].generation == 2, "new commit uses newer generation");
}
void TestRuntimeLiveStateSmoothingZeroAppliesTargetImmediately()
{
RuntimeLiveState liveState;
RuntimeLiveOscUpdate update;
update.routeKey = "layer-one\namount";
update.layerKey = "layer-one";
update.parameterKey = "amount";
update.targetValue = JsonValue(1.0);
liveState.ApplyOscUpdates({ update });
std::vector<RuntimeRenderState> states = { MakeLayerState() };
RuntimeLiveStateApplyOptions options;
options.smoothing = 0.0;
liveState.ApplyToLayerStates(states, options, nullptr);
const auto valueIt = states[0].parameterValues.find("amount");
Expect(valueIt != states[0].parameterValues.end(), "smoothing zero writes amount");
Expect(!valueIt->second.numberValues.empty() && std::fabs(valueIt->second.numberValues[0] - 1.0) < 0.0001,
"smoothing zero applies target immediately");
}
void TestRuntimeLiveStateSmoothingOneConvergesImmediately()
{
RuntimeLiveState liveState;
RuntimeLiveOscUpdate update;
update.routeKey = "layer-one\namount";
update.layerKey = "layer-one";
update.parameterKey = "amount";
update.targetValue = JsonValue(1.0);
liveState.ApplyOscUpdates({ update });
std::vector<RuntimeRenderState> states = { MakeLayerState() };
RuntimeLiveStateApplyOptions options;
options.smoothing = 1.0;
options.now = std::chrono::steady_clock::now() + std::chrono::milliseconds(16);
liveState.ApplyToLayerStates(states, options, nullptr);
const auto valueIt = states[0].parameterValues.find("amount");
Expect(valueIt != states[0].parameterValues.end(), "smoothing one writes amount");
Expect(!valueIt->second.numberValues.empty() && std::fabs(valueIt->second.numberValues[0] - 1.0) < 0.0001,
"smoothing one converges immediately");
}
void TestRuntimeLiveStateSmoothingPartiallyConverges()
{
RuntimeLiveState liveState;
RuntimeLiveOscUpdate update;
update.routeKey = "layer-one\namount";
update.layerKey = "layer-one";
update.parameterKey = "amount";
update.targetValue = JsonValue(1.0);
liveState.ApplyOscUpdates({ update });
std::vector<RuntimeRenderState> states = { MakeLayerState() };
ShaderParameterValue amount;
amount.numberValues = { 0.0 };
states[0].parameterValues["amount"] = amount;
RuntimeLiveStateApplyOptions options;
options.smoothing = 0.5;
options.now = std::chrono::steady_clock::now() + std::chrono::milliseconds(16);
liveState.ApplyToLayerStates(states, options, nullptr);
const auto valueIt = states[0].parameterValues.find("amount");
Expect(valueIt != states[0].parameterValues.end(), "partial smoothing writes amount");
Expect(!valueIt->second.numberValues.empty() &&
valueIt->second.numberValues[0] > 0.0 &&
valueIt->second.numberValues[0] < 1.0,
"partial smoothing advances toward target without snapping");
}
void TestRuntimeLiveStateSmoothingVectorSizeMismatchUsesTargetShape()
{
RuntimeLiveState liveState;
RuntimeLiveOscUpdate update;
update.routeKey = "layer-one\noffset";
update.layerKey = "layer-one";
update.parameterKey = "offset";
update.targetValue = NumberArray({ 0.25, 0.75 });
liveState.ApplyOscUpdates({ update });
std::vector<RuntimeRenderState> states = { MakeLayerStateWithDefinitions({ Vec2Definition("offset", "Offset") }) };
ShaderParameterValue malformedOffset;
malformedOffset.numberValues = { 0.1 };
states[0].parameterValues["offset"] = malformedOffset;
RuntimeLiveStateApplyOptions options;
options.smoothing = 0.5;
options.now = std::chrono::steady_clock::now() + std::chrono::milliseconds(16);
liveState.ApplyToLayerStates(states, options, nullptr);
const auto valueIt = states[0].parameterValues.find("offset");
Expect(valueIt != states[0].parameterValues.end(), "vector mismatch writes offset");
Expect(valueIt->second.numberValues.size() == 2, "vector mismatch restores target vector size");
Expect(valueIt->second.numberValues.size() == 2 &&
std::fabs(valueIt->second.numberValues[0] - 0.25) < 0.0001 &&
std::fabs(valueIt->second.numberValues[1] - 0.75) < 0.0001,
"vector mismatch snaps to validated target shape");
}
void TestRuntimeLiveStateTriggerOverlayIncrementsAndClears()
{
RuntimeLiveState liveState;
RuntimeLiveOscUpdate update;
update.routeKey = "layer-one\npulse";
update.layerKey = "layer-one";
update.parameterKey = "pulse";
update.targetValue = JsonValue(true);
liveState.ApplyOscUpdates({ update });
std::vector<RuntimeRenderState> states = { MakeLayerStateWithDefinitions({ TriggerDefinition("pulse", "Pulse") }) };
states[0].timeSeconds = 42.0;
ShaderParameterValue pulse;
pulse.numberValues = { 2.0, 10.0 };
states[0].parameterValues["pulse"] = pulse;
std::vector<RuntimeLiveOscCommitRequest> commitRequests;
RuntimeLiveStateApplyOptions options;
options.allowCommit = true;
options.smoothing = 0.0;
options.commitDelay = std::chrono::milliseconds(0);
liveState.ApplyToLayerStates(states, options, &commitRequests);
const auto valueIt = states[0].parameterValues.find("pulse");
Expect(valueIt != states[0].parameterValues.end(), "trigger overlay writes pulse");
Expect(valueIt->second.numberValues.size() == 2 &&
std::fabs(valueIt->second.numberValues[0] - 3.0) < 0.0001 &&
std::fabs(valueIt->second.numberValues[1] - 42.0) < 0.0001,
"trigger overlay increments count and stamps layer time");
Expect(commitRequests.empty(), "trigger overlay does not queue commit");
Expect(liveState.OverlayCount() == 0, "trigger overlay clears after apply");
}
void TestRenderStateComposerBuildsFrameState()
{
RuntimeLiveState liveState;
@@ -110,27 +360,102 @@ void TestRenderStateComposerBuildsFrameState()
liveState.ApplyOscUpdates({ update });
RenderStateCompositionInput input;
input.baseLayerStates = { MakeLayerState() };
std::vector<RuntimeRenderState> baseLayerStates = { MakeLayerState() };
input.baseLayerStates = &baseLayerStates;
input.liveState = &liveState;
input.liveStateOptions.allowCommit = false;
input.liveStateOptions.smoothing = 0.0;
input.allowLiveCommits = false;
input.liveSmoothing = 0.0;
RenderStateComposer composer;
RenderStateCompositionResult result = composer.BuildFrameState(input);
Expect(result.hasLayerStates, "composer reports that it composed base layer states");
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");
const auto baseValueIt = baseLayerStates[0].parameterValues.find("amount");
Expect(baseValueIt != baseLayerStates[0].parameterValues.end() &&
!baseValueIt->second.numberValues.empty() &&
std::fabs(baseValueIt->second.numberValues[0] - 0.25) < 0.0001,
"composer leaves base layer states unchanged");
}
void TestRenderStateComposerQueuesCommitRequestsWhenEnabled()
{
RuntimeLiveState liveState;
RuntimeLiveOscUpdate update;
update.routeKey = "layer-one\namount";
update.layerKey = "layer-one";
update.parameterKey = "amount";
update.targetValue = JsonValue(0.8);
liveState.ApplyOscUpdates({ update });
std::vector<RuntimeRenderState> baseLayerStates = { MakeLayerState() };
RenderStateCompositionInput input;
input.baseLayerStates = &baseLayerStates;
input.liveState = &liveState;
input.allowLiveCommits = true;
input.collectLiveCommitRequests = true;
input.liveSmoothing = 0.0;
input.liveCommitDelay = std::chrono::milliseconds(0);
input.now = std::chrono::steady_clock::now() + std::chrono::milliseconds(1);
RenderStateComposer composer;
RenderStateCompositionResult result = composer.BuildFrameState(input);
Expect(result.commitRequests.size() == 1, "composer returns live commit requests when collection is enabled");
Expect(result.commitRequests[0].routeKey == "layer-one\namount", "composer commit request preserves route");
Expect(result.commitRequests[0].generation == 1, "composer commit request preserves generation");
}
void TestRenderStateComposerSuppressesCommitCollection()
{
RuntimeLiveState liveState;
RuntimeLiveOscUpdate update;
update.routeKey = "layer-one\namount";
update.layerKey = "layer-one";
update.parameterKey = "amount";
update.targetValue = JsonValue(0.7);
liveState.ApplyOscUpdates({ update });
std::vector<RuntimeRenderState> baseLayerStates = { MakeLayerState() };
RenderStateCompositionInput input;
input.baseLayerStates = &baseLayerStates;
input.liveState = &liveState;
input.allowLiveCommits = true;
input.collectLiveCommitRequests = false;
input.liveSmoothing = 0.0;
input.liveCommitDelay = std::chrono::milliseconds(0);
input.now = std::chrono::steady_clock::now() + std::chrono::milliseconds(1);
RenderStateComposer composer;
RenderStateCompositionResult result = composer.BuildFrameState(input);
Expect(result.commitRequests.empty(), "composer can apply overlays without collecting commit requests");
const auto valueIt = result.layerStates[0].parameterValues.find("amount");
Expect(valueIt != result.layerStates[0].parameterValues.end() &&
!valueIt->second.numberValues.empty() &&
std::fabs(valueIt->second.numberValues[0] - 0.7) < 0.0001,
"composer still applies overlays when commit collection is disabled");
}
}
int main()
{
TestRuntimeLiveStateAppliesLatestOscOverlay();
TestRuntimeLiveStateIgnoresStaleCommitCompletions();
TestRuntimeLiveStateQueuesAndCompletesCommit();
TestRuntimeLiveStateQueuesOneCommitPerGeneration();
TestRuntimeLiveStateSmoothingZeroAppliesTargetImmediately();
TestRuntimeLiveStateSmoothingOneConvergesImmediately();
TestRuntimeLiveStateSmoothingPartiallyConverges();
TestRuntimeLiveStateSmoothingVectorSizeMismatchUsesTargetShape();
TestRuntimeLiveStateTriggerOverlayIncrementsAndClears();
TestRenderStateComposerBuildsFrameState();
TestRenderStateComposerQueuesCommitRequestsWhenEnabled();
TestRenderStateComposerSuppressesCommitCollection();
if (gFailures != 0)
{