Phase 3 refactor in progress
This commit is contained in:
@@ -40,6 +40,8 @@ set(APP_SOURCES
|
|||||||
"${APP_DIR}/control/OscServer.h"
|
"${APP_DIR}/control/OscServer.h"
|
||||||
"${APP_DIR}/control/RuntimeControlBridge.cpp"
|
"${APP_DIR}/control/RuntimeControlBridge.cpp"
|
||||||
"${APP_DIR}/control/RuntimeControlBridge.h"
|
"${APP_DIR}/control/RuntimeControlBridge.h"
|
||||||
|
"${APP_DIR}/control/RuntimeServiceLiveBridge.cpp"
|
||||||
|
"${APP_DIR}/control/RuntimeServiceLiveBridge.h"
|
||||||
"${APP_DIR}/control/RuntimeServices.cpp"
|
"${APP_DIR}/control/RuntimeServices.cpp"
|
||||||
"${APP_DIR}/control/RuntimeServices.h"
|
"${APP_DIR}/control/RuntimeServices.h"
|
||||||
"${APP_DIR}/videoio/decklink/DeckLinkAPI_h.h"
|
"${APP_DIR}/videoio/decklink/DeckLinkAPI_h.h"
|
||||||
@@ -310,21 +312,34 @@ endif()
|
|||||||
add_test(NAME RuntimeLiveStateTests COMMAND RuntimeLiveStateTests)
|
add_test(NAME RuntimeLiveStateTests COMMAND RuntimeLiveStateTests)
|
||||||
|
|
||||||
add_executable(RuntimeSubsystemTests
|
add_executable(RuntimeSubsystemTests
|
||||||
|
"${APP_DIR}/runtime/coordination/RuntimeCoordinator.cpp"
|
||||||
|
"${APP_DIR}/runtime/snapshot/RenderSnapshotBuilder.cpp"
|
||||||
"${APP_DIR}/runtime/store/LayerStackStore.cpp"
|
"${APP_DIR}/runtime/store/LayerStackStore.cpp"
|
||||||
|
"${APP_DIR}/runtime/store/RuntimeConfigStore.cpp"
|
||||||
|
"${APP_DIR}/runtime/store/RuntimeStore.cpp"
|
||||||
"${APP_DIR}/runtime/store/ShaderPackageCatalog.cpp"
|
"${APP_DIR}/runtime/store/ShaderPackageCatalog.cpp"
|
||||||
"${APP_DIR}/runtime/presentation/RuntimeStateJson.cpp"
|
"${APP_DIR}/runtime/presentation/RuntimeStateJson.cpp"
|
||||||
|
"${APP_DIR}/runtime/presentation/RuntimeStatePresenter.cpp"
|
||||||
"${APP_DIR}/runtime/support/RuntimeJson.cpp"
|
"${APP_DIR}/runtime/support/RuntimeJson.cpp"
|
||||||
"${APP_DIR}/runtime/support/RuntimeParameterUtils.cpp"
|
"${APP_DIR}/runtime/support/RuntimeParameterUtils.cpp"
|
||||||
|
"${APP_DIR}/runtime/telemetry/HealthTelemetry.cpp"
|
||||||
|
"${APP_DIR}/runtime/telemetry/RuntimeClock.cpp"
|
||||||
|
"${APP_DIR}/shader/ShaderCompiler.cpp"
|
||||||
"${APP_DIR}/shader/ShaderPackageRegistry.cpp"
|
"${APP_DIR}/shader/ShaderPackageRegistry.cpp"
|
||||||
"${CMAKE_CURRENT_SOURCE_DIR}/tests/RuntimeSubsystemTests.cpp"
|
"${CMAKE_CURRENT_SOURCE_DIR}/tests/RuntimeSubsystemTests.cpp"
|
||||||
)
|
)
|
||||||
|
|
||||||
target_include_directories(RuntimeSubsystemTests PRIVATE
|
target_include_directories(RuntimeSubsystemTests PRIVATE
|
||||||
"${APP_DIR}"
|
"${APP_DIR}"
|
||||||
|
"${APP_DIR}/platform"
|
||||||
"${APP_DIR}/runtime"
|
"${APP_DIR}/runtime"
|
||||||
|
"${APP_DIR}/runtime/coordination"
|
||||||
|
"${APP_DIR}/runtime/events"
|
||||||
"${APP_DIR}/runtime/presentation"
|
"${APP_DIR}/runtime/presentation"
|
||||||
|
"${APP_DIR}/runtime/snapshot"
|
||||||
"${APP_DIR}/runtime/store"
|
"${APP_DIR}/runtime/store"
|
||||||
"${APP_DIR}/runtime/support"
|
"${APP_DIR}/runtime/support"
|
||||||
|
"${APP_DIR}/runtime/telemetry"
|
||||||
"${APP_DIR}/shader"
|
"${APP_DIR}/shader"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,77 @@
|
|||||||
|
#include "RuntimeServiceLiveBridge.h"
|
||||||
|
|
||||||
|
#include "RuntimeServices.h"
|
||||||
|
|
||||||
|
#include <windows.h>
|
||||||
|
|
||||||
|
void RuntimeServiceLiveBridge::DrainServiceEvents(RuntimeServices& runtimeServices, RenderEngine& renderEngine)
|
||||||
|
{
|
||||||
|
std::vector<RuntimeServices::AppliedOscUpdate> appliedOscUpdates;
|
||||||
|
std::vector<RuntimeServices::CompletedOscCommit> completedOscCommits;
|
||||||
|
|
||||||
|
std::string oscError;
|
||||||
|
if (!runtimeServices.ApplyPendingOscUpdates(appliedOscUpdates, oscError) && !oscError.empty())
|
||||||
|
OutputDebugStringA(("OSC apply failed: " + oscError + "\n").c_str());
|
||||||
|
runtimeServices.ConsumeCompletedOscCommits(completedOscCommits);
|
||||||
|
|
||||||
|
std::vector<RenderEngine::OscOverlayUpdate> overlayUpdates;
|
||||||
|
overlayUpdates.reserve(appliedOscUpdates.size());
|
||||||
|
for (const RuntimeServices::AppliedOscUpdate& update : appliedOscUpdates)
|
||||||
|
{
|
||||||
|
overlayUpdates.push_back({ update.routeKey, update.layerKey, update.parameterKey, update.targetValue });
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<RenderEngine::OscOverlayCommitCompletion> overlayCommitCompletions;
|
||||||
|
overlayCommitCompletions.reserve(completedOscCommits.size());
|
||||||
|
for (const RuntimeServices::CompletedOscCommit& completedCommit : completedOscCommits)
|
||||||
|
{
|
||||||
|
overlayCommitCompletions.push_back({ completedCommit.routeKey, completedCommit.generation });
|
||||||
|
}
|
||||||
|
|
||||||
|
renderEngine.UpdateOscOverlayState(overlayUpdates, overlayCommitCompletions);
|
||||||
|
}
|
||||||
|
|
||||||
|
void RuntimeServiceLiveBridge::QueueServiceCommitRequests(
|
||||||
|
RuntimeServices& runtimeServices,
|
||||||
|
const std::vector<RenderEngine::OscOverlayCommitRequest>& commitRequests)
|
||||||
|
{
|
||||||
|
for (const RenderEngine::OscOverlayCommitRequest& commitRequest : commitRequests)
|
||||||
|
{
|
||||||
|
std::string commitError;
|
||||||
|
if (!runtimeServices.QueueOscCommit(
|
||||||
|
commitRequest.routeKey,
|
||||||
|
commitRequest.layerKey,
|
||||||
|
commitRequest.parameterKey,
|
||||||
|
commitRequest.value,
|
||||||
|
commitRequest.generation,
|
||||||
|
commitError) &&
|
||||||
|
!commitError.empty())
|
||||||
|
{
|
||||||
|
OutputDebugStringA(("OSC commit queue failed: " + commitError + "\n").c_str());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool RuntimeServiceLiveBridge::PrepareLiveRenderLayerStates(
|
||||||
|
RuntimeServices& runtimeServices,
|
||||||
|
RenderEngine& renderEngine,
|
||||||
|
bool useCommittedLayerStates,
|
||||||
|
unsigned renderWidth,
|
||||||
|
unsigned renderHeight,
|
||||||
|
double oscSmoothing,
|
||||||
|
std::vector<RuntimeRenderState>& layerStates)
|
||||||
|
{
|
||||||
|
DrainServiceEvents(runtimeServices, renderEngine);
|
||||||
|
|
||||||
|
std::vector<RenderEngine::OscOverlayCommitRequest> commitRequests;
|
||||||
|
const bool resolved = renderEngine.ResolveRenderLayerStates(
|
||||||
|
useCommittedLayerStates,
|
||||||
|
renderWidth,
|
||||||
|
renderHeight,
|
||||||
|
oscSmoothing,
|
||||||
|
&commitRequests,
|
||||||
|
layerStates);
|
||||||
|
|
||||||
|
QueueServiceCommitRequests(runtimeServices, commitRequests);
|
||||||
|
return resolved;
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "RenderEngine.h"
|
||||||
|
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
class RuntimeServices;
|
||||||
|
|
||||||
|
class RuntimeServiceLiveBridge
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
static void DrainServiceEvents(RuntimeServices& runtimeServices, RenderEngine& renderEngine);
|
||||||
|
static void QueueServiceCommitRequests(
|
||||||
|
RuntimeServices& runtimeServices,
|
||||||
|
const std::vector<RenderEngine::OscOverlayCommitRequest>& commitRequests);
|
||||||
|
static bool PrepareLiveRenderLayerStates(
|
||||||
|
RuntimeServices& runtimeServices,
|
||||||
|
RenderEngine& renderEngine,
|
||||||
|
bool useCommittedLayerStates,
|
||||||
|
unsigned renderWidth,
|
||||||
|
unsigned renderHeight,
|
||||||
|
double oscSmoothing,
|
||||||
|
std::vector<RuntimeRenderState>& layerStates);
|
||||||
|
};
|
||||||
@@ -7,6 +7,7 @@
|
|||||||
#include "RuntimeCoordinator.h"
|
#include "RuntimeCoordinator.h"
|
||||||
#include "RuntimeEventDispatcher.h"
|
#include "RuntimeEventDispatcher.h"
|
||||||
#include "RuntimeParameterUtils.h"
|
#include "RuntimeParameterUtils.h"
|
||||||
|
#include "RuntimeServiceLiveBridge.h"
|
||||||
#include "RuntimeServices.h"
|
#include "RuntimeServices.h"
|
||||||
#include "RuntimeSnapshotProvider.h"
|
#include "RuntimeSnapshotProvider.h"
|
||||||
#include "RuntimeStore.h"
|
#include "RuntimeStore.h"
|
||||||
@@ -302,61 +303,30 @@ void OpenGLComposite::renderEffect()
|
|||||||
{
|
{
|
||||||
if (mRuntimeUpdateController)
|
if (mRuntimeUpdateController)
|
||||||
mRuntimeUpdateController->ProcessRuntimeWork();
|
mRuntimeUpdateController->ProcessRuntimeWork();
|
||||||
std::vector<RuntimeServices::AppliedOscUpdate> appliedOscUpdates;
|
|
||||||
std::vector<RuntimeServices::CompletedOscCommit> completedOscCommits;
|
|
||||||
if (mRuntimeServices)
|
|
||||||
{
|
|
||||||
std::string oscError;
|
|
||||||
if (!mRuntimeServices->ApplyPendingOscUpdates(appliedOscUpdates, oscError) && !oscError.empty())
|
|
||||||
OutputDebugStringA(("OSC apply failed: " + oscError + "\n").c_str());
|
|
||||||
mRuntimeServices->ConsumeCompletedOscCommits(completedOscCommits);
|
|
||||||
}
|
|
||||||
|
|
||||||
std::vector<RenderEngine::OscOverlayUpdate> overlayUpdates;
|
|
||||||
overlayUpdates.reserve(appliedOscUpdates.size());
|
|
||||||
for (const RuntimeServices::AppliedOscUpdate& update : appliedOscUpdates)
|
|
||||||
{
|
|
||||||
overlayUpdates.push_back({ update.routeKey, update.layerKey, update.parameterKey, update.targetValue });
|
|
||||||
}
|
|
||||||
|
|
||||||
std::vector<RenderEngine::OscOverlayCommitCompletion> overlayCommitCompletions;
|
|
||||||
overlayCommitCompletions.reserve(completedOscCommits.size());
|
|
||||||
for (const RuntimeServices::CompletedOscCommit& completedCommit : completedOscCommits)
|
|
||||||
{
|
|
||||||
overlayCommitCompletions.push_back({ completedCommit.routeKey, completedCommit.generation });
|
|
||||||
}
|
|
||||||
|
|
||||||
if (mRenderEngine)
|
|
||||||
mRenderEngine->UpdateOscOverlayState(overlayUpdates, overlayCommitCompletions);
|
|
||||||
|
|
||||||
const bool hasInputSource = mVideoBackend->HasInputSource();
|
const bool hasInputSource = mVideoBackend->HasInputSource();
|
||||||
std::vector<RuntimeRenderState> layerStates;
|
std::vector<RuntimeRenderState> layerStates;
|
||||||
std::vector<RenderEngine::OscOverlayCommitRequest> overlayCommitRequests;
|
|
||||||
const double smoothing = mRuntimeStore ? mRuntimeStore->GetConfiguredOscSmoothing() : 0.0;
|
const double smoothing = mRuntimeStore ? mRuntimeStore->GetConfiguredOscSmoothing() : 0.0;
|
||||||
|
if (mRuntimeServices)
|
||||||
|
{
|
||||||
|
RuntimeServiceLiveBridge::PrepareLiveRenderLayerStates(
|
||||||
|
*mRuntimeServices,
|
||||||
|
*mRenderEngine,
|
||||||
|
mRuntimeCoordinator && mRuntimeCoordinator->UseCommittedLayerStates(),
|
||||||
|
mVideoBackend->InputFrameWidth(),
|
||||||
|
mVideoBackend->InputFrameHeight(),
|
||||||
|
smoothing,
|
||||||
|
layerStates);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
mRenderEngine->ResolveRenderLayerStates(
|
mRenderEngine->ResolveRenderLayerStates(
|
||||||
mRuntimeCoordinator && mRuntimeCoordinator->UseCommittedLayerStates(),
|
mRuntimeCoordinator && mRuntimeCoordinator->UseCommittedLayerStates(),
|
||||||
mVideoBackend->InputFrameWidth(),
|
mVideoBackend->InputFrameWidth(),
|
||||||
mVideoBackend->InputFrameHeight(),
|
mVideoBackend->InputFrameHeight(),
|
||||||
smoothing,
|
smoothing,
|
||||||
&overlayCommitRequests,
|
nullptr,
|
||||||
layerStates);
|
layerStates);
|
||||||
if (mRuntimeServices)
|
|
||||||
{
|
|
||||||
for (const RenderEngine::OscOverlayCommitRequest& commitRequest : overlayCommitRequests)
|
|
||||||
{
|
|
||||||
std::string commitError;
|
|
||||||
if (!mRuntimeServices->QueueOscCommit(
|
|
||||||
commitRequest.routeKey,
|
|
||||||
commitRequest.layerKey,
|
|
||||||
commitRequest.parameterKey,
|
|
||||||
commitRequest.value,
|
|
||||||
commitRequest.generation,
|
|
||||||
commitError) &&
|
|
||||||
!commitError.empty())
|
|
||||||
{
|
|
||||||
OutputDebugStringA(("OSC commit queue failed: " + commitError + "\n").c_str());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
const unsigned historyCap = mRuntimeStore ? mRuntimeStore->GetConfiguredMaxTemporalHistoryFrames() : 0;
|
const unsigned historyCap = mRuntimeStore ? mRuntimeStore->GetConfiguredMaxTemporalHistoryFrames() : 0;
|
||||||
mRenderEngine->RenderLayerStack(
|
mRenderEngine->RenderLayerStack(
|
||||||
|
|||||||
@@ -259,8 +259,7 @@ bool RenderEngine::ResolveRenderLayerStates(
|
|||||||
layerStates.clear();
|
layerStates.clear();
|
||||||
if (useCommittedLayerStates)
|
if (useCommittedLayerStates)
|
||||||
{
|
{
|
||||||
layerStates = mShaderPrograms.CommittedLayerStates();
|
layerStates = ComposeRenderLayerStates(mShaderPrograms.CommittedLayerStates(), false, oscSmoothing, commitRequests);
|
||||||
ApplyOscOverlays(layerStates, false, oscSmoothing, commitRequests);
|
|
||||||
mRuntimeSnapshotProvider.RefreshDynamicRenderStateFields(layerStates);
|
mRuntimeSnapshotProvider.RefreshDynamicRenderStateFields(layerStates);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -281,12 +280,12 @@ bool RenderEngine::ResolveRenderLayerStates(
|
|||||||
renderSnapshot.versions.parameterStateVersion = mCachedParameterStateVersion;
|
renderSnapshot.versions.parameterStateVersion = mCachedParameterStateVersion;
|
||||||
renderSnapshot.states = mCachedLayerRenderStates;
|
renderSnapshot.states = mCachedLayerRenderStates;
|
||||||
|
|
||||||
ApplyOscOverlays(renderSnapshot.states, true, oscSmoothing, commitRequests);
|
renderSnapshot.states = ComposeRenderLayerStates(renderSnapshot.states, true, oscSmoothing, commitRequests);
|
||||||
if (mCachedParameterStateVersion != versions.parameterStateVersion &&
|
if (mCachedParameterStateVersion != versions.parameterStateVersion &&
|
||||||
mRuntimeSnapshotProvider.TryRefreshPublishedSnapshotParameters(renderSnapshot))
|
mRuntimeSnapshotProvider.TryRefreshPublishedSnapshotParameters(renderSnapshot))
|
||||||
{
|
{
|
||||||
mCachedParameterStateVersion = renderSnapshot.versions.parameterStateVersion;
|
mCachedParameterStateVersion = renderSnapshot.versions.parameterStateVersion;
|
||||||
ApplyOscOverlays(renderSnapshot.states, true, oscSmoothing, commitRequests);
|
renderSnapshot.states = ComposeRenderLayerStates(renderSnapshot.states, true, oscSmoothing, commitRequests);
|
||||||
}
|
}
|
||||||
|
|
||||||
mCachedLayerRenderStates = renderSnapshot.states;
|
mCachedLayerRenderStates = renderSnapshot.states;
|
||||||
@@ -303,36 +302,38 @@ bool RenderEngine::ResolveRenderLayerStates(
|
|||||||
mCachedParameterStateVersion = renderSnapshot.versions.parameterStateVersion;
|
mCachedParameterStateVersion = renderSnapshot.versions.parameterStateVersion;
|
||||||
mCachedRenderStateWidth = renderSnapshot.outputWidth;
|
mCachedRenderStateWidth = renderSnapshot.outputWidth;
|
||||||
mCachedRenderStateHeight = renderSnapshot.outputHeight;
|
mCachedRenderStateHeight = renderSnapshot.outputHeight;
|
||||||
ApplyOscOverlays(mCachedLayerRenderStates, true, oscSmoothing, commitRequests);
|
mCachedLayerRenderStates = ComposeRenderLayerStates(mCachedLayerRenderStates, true, oscSmoothing, commitRequests);
|
||||||
layerStates = mCachedLayerRenderStates;
|
layerStates = mCachedLayerRenderStates;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
ApplyOscOverlays(mCachedLayerRenderStates, true, oscSmoothing, commitRequests);
|
layerStates = ComposeRenderLayerStates(mCachedLayerRenderStates, true, oscSmoothing, commitRequests);
|
||||||
layerStates = mCachedLayerRenderStates;
|
|
||||||
mRuntimeSnapshotProvider.RefreshDynamicRenderStateFields(layerStates);
|
mRuntimeSnapshotProvider.RefreshDynamicRenderStateFields(layerStates);
|
||||||
return !layerStates.empty();
|
return !layerStates.empty();
|
||||||
}
|
}
|
||||||
|
|
||||||
void RenderEngine::ApplyOscOverlays(
|
std::vector<RuntimeRenderState> RenderEngine::ComposeRenderLayerStates(
|
||||||
std::vector<RuntimeRenderState>& states,
|
const std::vector<RuntimeRenderState>& baseStates,
|
||||||
bool allowCommit,
|
bool allowCommit,
|
||||||
double smoothing,
|
double smoothing,
|
||||||
std::vector<OscOverlayCommitRequest>* commitRequests)
|
std::vector<OscOverlayCommitRequest>* commitRequests)
|
||||||
{
|
{
|
||||||
std::vector<RuntimeLiveOscCommitRequest> liveCommitRequests;
|
RenderStateCompositionInput input;
|
||||||
RuntimeLiveStateApplyOptions options;
|
input.baseLayerStates = &baseStates;
|
||||||
options.allowCommit = allowCommit;
|
input.liveState = &mRuntimeLiveState;
|
||||||
options.smoothing = smoothing;
|
input.allowLiveCommits = allowCommit;
|
||||||
options.commitDelay = kOscOverlayCommitDelay;
|
input.collectLiveCommitRequests = commitRequests != nullptr;
|
||||||
options.now = std::chrono::steady_clock::now();
|
input.liveSmoothing = smoothing;
|
||||||
mRuntimeLiveState.ApplyToLayerStates(states, options, commitRequests ? &liveCommitRequests : nullptr);
|
input.liveCommitDelay = kOscOverlayCommitDelay;
|
||||||
|
input.now = std::chrono::steady_clock::now();
|
||||||
|
const RenderStateCompositionResult result = mRenderStateComposer.BuildFrameState(input);
|
||||||
|
|
||||||
if (!commitRequests)
|
if (!commitRequests)
|
||||||
return;
|
return result.layerStates;
|
||||||
|
|
||||||
for (const RuntimeLiveOscCommitRequest& request : liveCommitRequests)
|
for (const RuntimeLiveOscCommitRequest& request : result.commitRequests)
|
||||||
commitRequests->push_back({ request.routeKey, request.layerKey, request.parameterKey, request.value, request.generation });
|
commitRequests->push_back({ request.routeKey, request.layerKey, request.parameterKey, request.value, request.generation });
|
||||||
|
return result.layerStates;
|
||||||
}
|
}
|
||||||
|
|
||||||
void RenderEngine::RenderLayerStack(
|
void RenderEngine::RenderLayerStack(
|
||||||
|
|||||||
@@ -4,9 +4,9 @@
|
|||||||
#include "OpenGLRenderPipeline.h"
|
#include "OpenGLRenderPipeline.h"
|
||||||
#include "OpenGLRenderer.h"
|
#include "OpenGLRenderer.h"
|
||||||
#include "OpenGLShaderPrograms.h"
|
#include "OpenGLShaderPrograms.h"
|
||||||
|
#include "RenderStateComposer.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>
|
||||||
@@ -133,8 +133,8 @@ private:
|
|||||||
HDC mHdc;
|
HDC mHdc;
|
||||||
HGLRC mHglrc;
|
HGLRC mHglrc;
|
||||||
|
|
||||||
void ApplyOscOverlays(
|
std::vector<RuntimeRenderState> ComposeRenderLayerStates(
|
||||||
std::vector<RuntimeRenderState>& states,
|
const std::vector<RuntimeRenderState>& baseStates,
|
||||||
bool allowCommit,
|
bool allowCommit,
|
||||||
double smoothing,
|
double smoothing,
|
||||||
std::vector<OscOverlayCommitRequest>* commitRequests);
|
std::vector<OscOverlayCommitRequest>* commitRequests);
|
||||||
@@ -145,5 +145,6 @@ 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;
|
||||||
|
RenderStateComposer mRenderStateComposer;
|
||||||
RuntimeLiveState mRuntimeLiveState;
|
RuntimeLiveState mRuntimeLiveState;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -175,7 +175,7 @@ RuntimeCoordinatorResult RuntimeCoordinator::CommitOscParameterByControlKey(cons
|
|||||||
std::lock_guard<std::mutex> lock(mMutex);
|
std::lock_guard<std::mutex> lock(mMutex);
|
||||||
std::string error;
|
std::string error;
|
||||||
ResolvedParameterMutation mutation;
|
ResolvedParameterMutation mutation;
|
||||||
if (!BuildParameterMutationByControlKey(layerKey, parameterKey, newValue, false, mutation, error))
|
if (!BuildParameterMutationByControlKey(layerKey, parameterKey, newValue, true, mutation, error))
|
||||||
{
|
{
|
||||||
RuntimeCoordinatorResult result = ApplyStoreMutation(false, error, false, false, false);
|
RuntimeCoordinatorResult result = ApplyStoreMutation(false, error, false, false, false);
|
||||||
PublishCoordinatorResult("CommitOscParameterByControlKey", result);
|
PublishCoordinatorResult("CommitOscParameterByControlKey", result);
|
||||||
|
|||||||
@@ -3,8 +3,22 @@
|
|||||||
RenderStateCompositionResult RenderStateComposer::BuildFrameState(const RenderStateCompositionInput& input) const
|
RenderStateCompositionResult RenderStateComposer::BuildFrameState(const RenderStateCompositionInput& input) const
|
||||||
{
|
{
|
||||||
RenderStateCompositionResult result;
|
RenderStateCompositionResult result;
|
||||||
result.layerStates = input.baseLayerStates;
|
if (!input.baseLayerStates)
|
||||||
|
return result;
|
||||||
|
|
||||||
|
result.layerStates = *input.baseLayerStates;
|
||||||
|
result.hasLayerStates = !result.layerStates.empty();
|
||||||
if (input.liveState)
|
if (input.liveState)
|
||||||
input.liveState->ApplyToLayerStates(result.layerStates, input.liveStateOptions, &result.commitRequests);
|
{
|
||||||
|
RuntimeLiveStateApplyOptions options;
|
||||||
|
options.allowCommit = input.allowLiveCommits;
|
||||||
|
options.smoothing = input.liveSmoothing;
|
||||||
|
options.commitDelay = input.liveCommitDelay;
|
||||||
|
options.now = input.now;
|
||||||
|
input.liveState->ApplyToLayerStates(
|
||||||
|
result.layerStates,
|
||||||
|
options,
|
||||||
|
input.collectLiveCommitRequests ? &result.commitRequests : nullptr);
|
||||||
|
}
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,19 +2,25 @@
|
|||||||
|
|
||||||
#include "RuntimeLiveState.h"
|
#include "RuntimeLiveState.h"
|
||||||
|
|
||||||
|
#include <chrono>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
struct RenderStateCompositionInput
|
struct RenderStateCompositionInput
|
||||||
{
|
{
|
||||||
std::vector<RuntimeRenderState> baseLayerStates;
|
const std::vector<RuntimeRenderState>* baseLayerStates = nullptr;
|
||||||
RuntimeLiveState* liveState = nullptr;
|
RuntimeLiveState* liveState = nullptr;
|
||||||
RuntimeLiveStateApplyOptions liveStateOptions;
|
bool allowLiveCommits = false;
|
||||||
|
bool collectLiveCommitRequests = true;
|
||||||
|
double liveSmoothing = 0.0;
|
||||||
|
std::chrono::milliseconds liveCommitDelay = std::chrono::milliseconds(150);
|
||||||
|
std::chrono::steady_clock::time_point now = std::chrono::steady_clock::now();
|
||||||
};
|
};
|
||||||
|
|
||||||
struct RenderStateCompositionResult
|
struct RenderStateCompositionResult
|
||||||
{
|
{
|
||||||
std::vector<RuntimeRenderState> layerStates;
|
std::vector<RuntimeRenderState> layerStates;
|
||||||
std::vector<RuntimeLiveOscCommitRequest> commitRequests;
|
std::vector<RuntimeLiveOscCommitRequest> commitRequests;
|
||||||
|
bool hasLayerStates = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
class RenderStateComposer
|
class RenderStateComposer
|
||||||
|
|||||||
@@ -7,8 +7,8 @@ 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: groundwork started.
|
- Phase 3 implementation: initial parallel implementation batch integrated.
|
||||||
- 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 now has the live-state/composer building blocks and a service bridge. `OpenGLComposite::renderEffect()` still remains the app-level frame entrypoint, but the service drain, layer-state resolution, and OSC commit handoff now sit behind a named bridge helper.
|
||||||
|
|
||||||
Current footholds:
|
Current footholds:
|
||||||
|
|
||||||
@@ -17,8 +17,9 @@ Current footholds:
|
|||||||
- `RuntimeSnapshotProvider` publishes render snapshots from `RenderSnapshotBuilder`.
|
- `RuntimeSnapshotProvider` publishes render snapshots from `RenderSnapshotBuilder`.
|
||||||
- `RuntimeLiveState` owns transient OSC overlay bookkeeping and commit-settlement policy.
|
- `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.
|
- `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`.
|
- `RenderEngine` still owns snapshot cache selection and final render-layer resolution, but live overlay value composition now delegates to `RenderStateComposer` and `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.
|
||||||
|
- `RuntimeServiceLiveBridge` translates service OSC queues into render live-state updates and queues settled overlay commit requests.
|
||||||
- `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.
|
||||||
|
|
||||||
The current architecture is much better than the original `RuntimeHost` shape, but the render path still has too much coordination logic stitched through `OpenGLComposite`, `RuntimeServices`, `RuntimeCoordinator`, and `RenderEngine`.
|
The current architecture is much better than the original `RuntimeHost` shape, but the render path still has too much coordination logic stitched through `OpenGLComposite`, `RuntimeServices`, `RuntimeCoordinator`, and `RenderEngine`.
|
||||||
@@ -265,7 +266,7 @@ 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`.
|
Status: mostly complete for the current architecture. `RenderEngine` still exposes compatibility methods used by the service bridge, but it now delegates overlay updates, commit completions, smoothing, generation matching, and commit-request creation to `RuntimeLiveState`/`RenderStateComposer`.
|
||||||
|
|
||||||
### Step 3. Bridge Service Queues To Events Or Live-State Commands
|
### Step 3. Bridge Service Queues To Events Or Live-State Commands
|
||||||
|
|
||||||
@@ -278,6 +279,8 @@ Replace `OpenGLComposite::renderEffect()` queue draining with a bridge that publ
|
|||||||
|
|
||||||
This is where the remaining Phase 2 open question about transient OSC overlay event scope should be resolved for the current architecture.
|
This is where the remaining Phase 2 open question about transient OSC overlay event scope should be resolved for the current architecture.
|
||||||
|
|
||||||
|
Status: started. `RuntimeServiceLiveBridge` now drains pending OSC updates and completed OSC commits, applies them to render live state, and queues settled commit requests. It is still a source-local bridge rather than a fully dispatcher-driven event bridge.
|
||||||
|
|
||||||
### Step 4. Narrow `OpenGLComposite::renderEffect()`
|
### Step 4. Narrow `OpenGLComposite::renderEffect()`
|
||||||
|
|
||||||
Target shape:
|
Target shape:
|
||||||
@@ -293,6 +296,8 @@ void OpenGLComposite::renderEffect()
|
|||||||
|
|
||||||
The exact names can change. The goal is that render effect no longer manually drains services, settles overlay commits, and resolves layer values.
|
The exact names can change. The goal is that render effect no longer manually drains services, settles overlay commits, and resolves layer values.
|
||||||
|
|
||||||
|
Status: started. `OpenGLComposite::renderEffect()` still drives frame timing, video dimensions, and drawing, but the service-drain, resolve, and commit-handoff path has moved behind `RuntimeServiceLiveBridge::PrepareLiveRenderLayerStates(...)`.
|
||||||
|
|
||||||
### Step 5. Add Persistence Boundary Tests
|
### Step 5. Add Persistence Boundary Tests
|
||||||
|
|
||||||
Add behavior tests for:
|
Add behavior tests for:
|
||||||
@@ -348,12 +353,12 @@ The current groundwork is intended to let these lanes proceed in parallel with l
|
|||||||
|
|
||||||
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` (groundwork exists via `RenderStateComposer`; full snapshot/cache resolution still needs to move behind it)
|
- [x] final render-state composition has a named, testable owner outside `OpenGLComposite` (live value composition is covered by `RenderStateComposer`; full snapshot/cache selection still remains in `RenderEngine`)
|
||||||
- [x] 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
|
- [x] 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
|
- [x] `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
|
- [x] `RuntimeStore` remains durable-state focused and does not gain live overlay responsibilities
|
||||||
- [ ] persistence requests are explicit event outcomes for persisted mutations
|
- [x] persistence requests are explicit event outcomes for persisted mutations
|
||||||
- [ ] Phase 4 can define a render-thread input contract around immutable or near-immutable frame state
|
- [ ] Phase 4 can define a render-thread input contract around immutable or near-immutable frame state
|
||||||
|
|
||||||
## Open Questions
|
## Open Questions
|
||||||
|
|||||||
@@ -30,6 +30,35 @@ ShaderParameterDefinition FloatDefinition(const std::string& id, const std::stri
|
|||||||
return definition;
|
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 MakeLayerState()
|
||||||
{
|
{
|
||||||
RuntimeRenderState state;
|
RuntimeRenderState state;
|
||||||
@@ -43,6 +72,16 @@ RuntimeRenderState MakeLayerState()
|
|||||||
return state;
|
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()
|
void TestRuntimeLiveStateAppliesLatestOscOverlay()
|
||||||
{
|
{
|
||||||
RuntimeLiveState liveState;
|
RuntimeLiveState liveState;
|
||||||
@@ -71,6 +110,40 @@ void TestRuntimeLiveStateAppliesLatestOscOverlay()
|
|||||||
"overlay applies the latest target value");
|
"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()
|
void TestRuntimeLiveStateQueuesAndCompletesCommit()
|
||||||
{
|
{
|
||||||
RuntimeLiveState liveState;
|
RuntimeLiveState liveState;
|
||||||
@@ -99,6 +172,183 @@ void TestRuntimeLiveStateQueuesAndCompletesCommit()
|
|||||||
Expect(liveState.OverlayCount() == 0, "matching commit completion removes settled overlay");
|
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()
|
void TestRenderStateComposerBuildsFrameState()
|
||||||
{
|
{
|
||||||
RuntimeLiveState liveState;
|
RuntimeLiveState liveState;
|
||||||
@@ -110,27 +360,102 @@ void TestRenderStateComposerBuildsFrameState()
|
|||||||
liveState.ApplyOscUpdates({ update });
|
liveState.ApplyOscUpdates({ update });
|
||||||
|
|
||||||
RenderStateCompositionInput input;
|
RenderStateCompositionInput input;
|
||||||
input.baseLayerStates = { MakeLayerState() };
|
std::vector<RuntimeRenderState> baseLayerStates = { MakeLayerState() };
|
||||||
|
input.baseLayerStates = &baseLayerStates;
|
||||||
input.liveState = &liveState;
|
input.liveState = &liveState;
|
||||||
input.liveStateOptions.allowCommit = false;
|
input.allowLiveCommits = false;
|
||||||
input.liveStateOptions.smoothing = 0.0;
|
input.liveSmoothing = 0.0;
|
||||||
|
|
||||||
RenderStateComposer composer;
|
RenderStateComposer composer;
|
||||||
RenderStateCompositionResult result = composer.BuildFrameState(input);
|
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");
|
Expect(result.layerStates.size() == 1, "composer returns composed layer state");
|
||||||
const auto valueIt = result.layerStates[0].parameterValues.find("amount");
|
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 != 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,
|
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");
|
"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()
|
int main()
|
||||||
{
|
{
|
||||||
TestRuntimeLiveStateAppliesLatestOscOverlay();
|
TestRuntimeLiveStateAppliesLatestOscOverlay();
|
||||||
|
TestRuntimeLiveStateIgnoresStaleCommitCompletions();
|
||||||
TestRuntimeLiveStateQueuesAndCompletesCommit();
|
TestRuntimeLiveStateQueuesAndCompletesCommit();
|
||||||
|
TestRuntimeLiveStateQueuesOneCommitPerGeneration();
|
||||||
|
TestRuntimeLiveStateSmoothingZeroAppliesTargetImmediately();
|
||||||
|
TestRuntimeLiveStateSmoothingOneConvergesImmediately();
|
||||||
|
TestRuntimeLiveStateSmoothingPartiallyConverges();
|
||||||
|
TestRuntimeLiveStateSmoothingVectorSizeMismatchUsesTargetShape();
|
||||||
|
TestRuntimeLiveStateTriggerOverlayIncrementsAndClears();
|
||||||
TestRenderStateComposerBuildsFrameState();
|
TestRenderStateComposerBuildsFrameState();
|
||||||
|
TestRenderStateComposerQueuesCommitRequestsWhenEnabled();
|
||||||
|
TestRenderStateComposerSuppressesCommitCollection();
|
||||||
|
|
||||||
if (gFailures != 0)
|
if (gFailures != 0)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
#include "LayerStackStore.h"
|
#include "LayerStackStore.h"
|
||||||
|
#include "RuntimeCoordinator.h"
|
||||||
|
#include "RuntimeEventDispatcher.h"
|
||||||
#include "RuntimeStateJson.h"
|
#include "RuntimeStateJson.h"
|
||||||
|
#include "RuntimeStore.h"
|
||||||
#include "ShaderPackageCatalog.h"
|
#include "ShaderPackageCatalog.h"
|
||||||
|
|
||||||
#include <chrono>
|
#include <chrono>
|
||||||
@@ -8,6 +11,8 @@
|
|||||||
#include <iostream>
|
#include <iostream>
|
||||||
#include <map>
|
#include <map>
|
||||||
#include <string>
|
#include <string>
|
||||||
|
#include <variant>
|
||||||
|
#include <windows.h>
|
||||||
|
|
||||||
namespace
|
namespace
|
||||||
{
|
{
|
||||||
@@ -44,6 +49,31 @@ void WriteShaderPackage(const std::filesystem::path& root, const std::string& di
|
|||||||
WriteFile(packageRoot / "shader.slang", "float4 shadeVideo(float2 uv) { return float4(uv, 0.0, 1.0); }\n");
|
WriteFile(packageRoot / "shader.slang", "float4 shadeVideo(float2 uv) { return float4(uv, 0.0, 1.0); }\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
std::filesystem::path GetCurrentDirectoryPath()
|
||||||
|
{
|
||||||
|
char buffer[MAX_PATH] = {};
|
||||||
|
GetCurrentDirectoryA(MAX_PATH, buffer);
|
||||||
|
return std::filesystem::path(buffer);
|
||||||
|
}
|
||||||
|
|
||||||
|
class ScopedCurrentDirectory
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
explicit ScopedCurrentDirectory(const std::filesystem::path& path) :
|
||||||
|
mPrevious(GetCurrentDirectoryPath())
|
||||||
|
{
|
||||||
|
SetCurrentDirectoryA(path.string().c_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
~ScopedCurrentDirectory()
|
||||||
|
{
|
||||||
|
SetCurrentDirectoryA(mPrevious.string().c_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::filesystem::path mPrevious;
|
||||||
|
};
|
||||||
|
|
||||||
ShaderPackageCatalog BuildCatalog(const std::filesystem::path& root)
|
ShaderPackageCatalog BuildCatalog(const std::filesystem::path& root)
|
||||||
{
|
{
|
||||||
ShaderPackageCatalog catalog;
|
ShaderPackageCatalog catalog;
|
||||||
@@ -181,6 +211,105 @@ void TestRuntimeStateJsonReadModelSerialization()
|
|||||||
const JsonValue* value = parameters->asArray()[0].find("value");
|
const JsonValue* value = parameters->asArray()[0].find("value");
|
||||||
Expect(value && value->asNumber() == 0.8, "serialized parameter includes current value");
|
Expect(value && value->asNumber() == 0.8, "serialized parameter includes current value");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void TestRuntimeCoordinatorPersistenceEvents()
|
||||||
|
{
|
||||||
|
const std::filesystem::path root = MakeTestRoot();
|
||||||
|
WriteFile(root / "CMakeLists.txt", "cmake_minimum_required(VERSION 3.24)\n");
|
||||||
|
std::filesystem::create_directories(root / "apps" / "LoopThroughWithOpenGLCompositing");
|
||||||
|
std::filesystem::create_directories(root / "runtime" / "templates");
|
||||||
|
WriteShaderPackage(root / "shaders", "alpha", R"({
|
||||||
|
"id": "alpha",
|
||||||
|
"name": "Alpha",
|
||||||
|
"parameters": [{ "id": "gain", "label": "Gain", "type": "float", "default": 0.5, "min": 0, "max": 1 }]
|
||||||
|
})");
|
||||||
|
WriteShaderPackage(root / "shaders", "beta", R"({
|
||||||
|
"id": "beta",
|
||||||
|
"name": "Beta",
|
||||||
|
"parameters": [{ "id": "amount", "label": "Amount", "type": "float", "default": 0.25, "min": 0, "max": 1 }]
|
||||||
|
})");
|
||||||
|
|
||||||
|
{
|
||||||
|
ScopedCurrentDirectory scopedDirectory(root);
|
||||||
|
RuntimeStore store;
|
||||||
|
std::string error;
|
||||||
|
Expect(store.InitializeStore(error), "runtime store initializes in isolated fixture");
|
||||||
|
Expect(error.empty(), "runtime store initialization has no error");
|
||||||
|
|
||||||
|
RuntimeEventDispatcher dispatcher(64);
|
||||||
|
std::vector<RuntimeEvent> seenEvents;
|
||||||
|
dispatcher.SubscribeAll([&seenEvents](const RuntimeEvent& event) {
|
||||||
|
seenEvents.push_back(event);
|
||||||
|
});
|
||||||
|
|
||||||
|
RuntimeCoordinator coordinator(store, dispatcher);
|
||||||
|
auto dispatchAndClear = [&]() {
|
||||||
|
dispatcher.DispatchPending();
|
||||||
|
const std::vector<RuntimeEvent> events = seenEvents;
|
||||||
|
seenEvents.clear();
|
||||||
|
return events;
|
||||||
|
};
|
||||||
|
auto countEvents = [](const std::vector<RuntimeEvent>& events, RuntimeEventType type) {
|
||||||
|
return static_cast<std::size_t>(std::count_if(events.begin(), events.end(),
|
||||||
|
[type](const RuntimeEvent& event) { return event.type == type; }));
|
||||||
|
};
|
||||||
|
auto persistenceReason = [](const std::vector<RuntimeEvent>& events) {
|
||||||
|
for (const RuntimeEvent& event : events)
|
||||||
|
{
|
||||||
|
if (event.type != RuntimeEventType::RuntimePersistenceRequested)
|
||||||
|
continue;
|
||||||
|
const auto* payload = std::get_if<RuntimePersistenceRequestedEvent>(&event.payload);
|
||||||
|
return payload ? payload->reason : std::string();
|
||||||
|
}
|
||||||
|
return std::string();
|
||||||
|
};
|
||||||
|
auto expectAcceptedPersistence = [&](const RuntimeCoordinatorResult& result, const std::string& reason, const char* message) {
|
||||||
|
const std::vector<RuntimeEvent> events = dispatchAndClear();
|
||||||
|
Expect(result.accepted, message);
|
||||||
|
Expect(result.persistenceRequested, "accepted persistent mutation marks coordinator result");
|
||||||
|
Expect(countEvents(events, RuntimeEventType::RuntimeMutationAccepted) == 1, "persistent mutation publishes accepted fact");
|
||||||
|
Expect(countEvents(events, RuntimeEventType::RuntimePersistenceRequested) == 1, "persistent mutation publishes persistence request");
|
||||||
|
Expect(persistenceReason(events) == reason, "persistence request preserves coordinator action reason");
|
||||||
|
};
|
||||||
|
|
||||||
|
std::vector<RuntimeStore::LayerPersistentState> layers = store.CopyLayerStates();
|
||||||
|
Expect(layers.size() == 1, "isolated fixture starts with a default layer");
|
||||||
|
const std::string alphaLayerId = layers.empty() ? std::string() : layers[0].id;
|
||||||
|
|
||||||
|
expectAcceptedPersistence(coordinator.UpdateLayerParameter(alphaLayerId, "gain", JsonValue(0.75)), "UpdateLayerParameter",
|
||||||
|
"parameter changes are accepted");
|
||||||
|
|
||||||
|
expectAcceptedPersistence(coordinator.AddLayer("beta"), "AddLayer", "stack edits are accepted");
|
||||||
|
layers = store.CopyLayerStates();
|
||||||
|
Expect(layers.size() == 2, "stack edit creates a second layer");
|
||||||
|
const std::string betaLayerId = layers.size() > 1 ? layers[1].id : std::string();
|
||||||
|
expectAcceptedPersistence(coordinator.MoveLayer(betaLayerId, -1), "MoveLayer", "layer order edits are accepted");
|
||||||
|
|
||||||
|
expectAcceptedPersistence(coordinator.SaveStackPreset("Look One"), "SaveStackPreset", "preset save is accepted");
|
||||||
|
expectAcceptedPersistence(coordinator.LoadStackPreset("Look One"), "LoadStackPreset", "preset load is accepted");
|
||||||
|
|
||||||
|
RuntimeCoordinatorResult rejected = coordinator.UpdateLayerParameter(alphaLayerId, "missing", JsonValue(0.5));
|
||||||
|
std::vector<RuntimeEvent> rejectedEvents = dispatchAndClear();
|
||||||
|
Expect(!rejected.accepted, "invalid parameter mutation is rejected");
|
||||||
|
Expect(!rejected.persistenceRequested, "rejected mutation does not mark persistence");
|
||||||
|
Expect(countEvents(rejectedEvents, RuntimeEventType::RuntimeMutationRejected) == 1, "rejected mutation publishes rejection fact");
|
||||||
|
Expect(countEvents(rejectedEvents, RuntimeEventType::RuntimePersistenceRequested) == 0, "rejected mutation publishes no persistence request");
|
||||||
|
|
||||||
|
OscOverlayEvent overlay;
|
||||||
|
overlay.routeKey = "alpha\ngain";
|
||||||
|
overlay.layerKey = "alpha";
|
||||||
|
overlay.parameterKey = "gain";
|
||||||
|
Expect(dispatcher.PublishPayload(overlay, "RuntimeLiveState"), "OSC overlay event publishes");
|
||||||
|
std::vector<RuntimeEvent> overlayEvents = dispatchAndClear();
|
||||||
|
Expect(countEvents(overlayEvents, RuntimeEventType::OscOverlayApplied) == 1, "transient OSC overlay is observable");
|
||||||
|
Expect(countEvents(overlayEvents, RuntimeEventType::RuntimePersistenceRequested) == 0, "transient OSC overlay does not request persistence");
|
||||||
|
|
||||||
|
expectAcceptedPersistence(coordinator.CommitOscParameterByControlKey("alpha", "gain", JsonValue(0.2)), "CommitOscParameterByControlKey",
|
||||||
|
"accepted OSC commit is persistent");
|
||||||
|
}
|
||||||
|
|
||||||
|
std::filesystem::remove_all(root);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
int main()
|
int main()
|
||||||
@@ -188,6 +317,7 @@ int main()
|
|||||||
TestLayerDefaultsAndCrud();
|
TestLayerDefaultsAndCrud();
|
||||||
TestMoveClassificationAndPresetLoad();
|
TestMoveClassificationAndPresetLoad();
|
||||||
TestRuntimeStateJsonReadModelSerialization();
|
TestRuntimeStateJsonReadModelSerialization();
|
||||||
|
TestRuntimeCoordinatorPersistenceEvents();
|
||||||
|
|
||||||
if (gFailures != 0)
|
if (gFailures != 0)
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user