2 Commits

Author SHA1 Message Date
Aiden
ff10b66d1d Phase 5 step 5
Some checks failed
CI / React UI Build (push) Successful in 10s
CI / Native Windows Build And Tests (push) Successful in 2m42s
CI / Windows Release Package (push) Has been cancelled
2026-05-11 19:14:59 +10:00
Aiden
fdcc38c6ae Step 4 2026-05-11 19:09:01 +10:00
13 changed files with 127 additions and 22 deletions

View File

@@ -241,6 +241,7 @@ void ControlServices::PollLoop(RuntimeCoordinator& runtimeCoordinator)
completedCommit.generation = entry.second.generation; completedCommit.generation = entry.second.generation;
std::lock_guard<std::mutex> lock(mCompletedOscCommitMutex); std::lock_guard<std::mutex> lock(mCompletedOscCommitMutex);
mCompletedOscCommits.push_back(std::move(completedCommit)); mCompletedOscCommits.push_back(std::move(completedCommit));
PublishOscOverlaySettled(entry.second);
} }
else if (!result.errorMessage.empty()) else if (!result.errorMessage.empty())
{ {
@@ -321,3 +322,22 @@ void ControlServices::PublishOscCommitRequested(const PendingOscCommit& commit)
OutputDebugStringA("OscCommitRequested event publish threw.\n"); OutputDebugStringA("OscCommitRequested event publish threw.\n");
} }
} }
void ControlServices::PublishOscOverlaySettled(const PendingOscCommit& commit)
{
try
{
OscOverlayEvent event;
event.routeKey = commit.routeKey;
event.layerKey = commit.layerKey;
event.parameterKey = commit.parameterKey;
event.generation = commit.generation;
event.settled = true;
if (!mRuntimeEventDispatcher.PublishPayload(event, "ControlServices"))
OutputDebugStringA("OscOverlaySettled event publish failed.\n");
}
catch (...)
{
OutputDebugStringA("OscOverlaySettled event publish threw.\n");
}
}

View File

@@ -76,6 +76,7 @@ private:
void PublishRuntimeStateBroadcastRequested(const std::string& reason); void PublishRuntimeStateBroadcastRequested(const std::string& reason);
void PublishOscValueReceived(const PendingOscUpdate& update, const std::string& routeKey); void PublishOscValueReceived(const PendingOscUpdate& update, const std::string& routeKey);
void PublishOscCommitRequested(const PendingOscCommit& commit); void PublishOscCommitRequested(const PendingOscCommit& commit);
void PublishOscOverlaySettled(const PendingOscCommit& commit);
std::unique_ptr<ControlServer> mControlServer; std::unique_ptr<ControlServer> mControlServer;
std::unique_ptr<OscServer> mOscServer; std::unique_ptr<OscServer> mOscServer;

View File

@@ -178,9 +178,14 @@ RuntimeCoordinatorResult RuntimeCoordinator::UpdateLayerParameterByControlKey(co
RuntimeCoordinatorResult RuntimeCoordinator::CommitOscParameterByControlKey(const std::string& layerKey, const std::string& parameterKey, const JsonValue& newValue) RuntimeCoordinatorResult RuntimeCoordinator::CommitOscParameterByControlKey(const std::string& layerKey, const std::string& parameterKey, const JsonValue& newValue)
{ {
std::lock_guard<std::mutex> lock(mMutex); std::lock_guard<std::mutex> lock(mMutex);
constexpr RuntimeCoordinatorOscCommitPersistence kDefaultOscCommitPersistence =
RuntimeCoordinatorOscCommitPersistence::SessionOnly;
constexpr bool kPersistSettledOscCommits =
kDefaultOscCommitPersistence == RuntimeCoordinatorOscCommitPersistence::Persistent;
std::string error; std::string error;
ResolvedParameterMutation mutation; ResolvedParameterMutation mutation;
if (!BuildParameterMutationByControlKey(layerKey, parameterKey, newValue, true, mutation, error)) if (!BuildParameterMutationByControlKey(layerKey, parameterKey, newValue, kPersistSettledOscCommits, mutation, error))
{ {
RuntimeCoordinatorResult result = ApplyStoreMutation(false, error, false, false, false); RuntimeCoordinatorResult result = ApplyStoreMutation(false, error, false, false, false);
PublishCoordinatorResult("CommitOscParameterByControlKey", result); PublishCoordinatorResult("CommitOscParameterByControlKey", result);

View File

@@ -32,6 +32,12 @@ enum class RuntimeCoordinatorTransientOscInvalidation
All All
}; };
enum class RuntimeCoordinatorOscCommitPersistence
{
SessionOnly,
Persistent
};
struct RuntimeCoordinatorResult struct RuntimeCoordinatorResult
{ {
bool accepted = false; bool accepted = false;

View File

@@ -89,7 +89,7 @@ bool RenderSnapshotBuilder::TryBuildLayerRenderStates(unsigned outputWidth, unsi
bool RenderSnapshotBuilder::TryRefreshLayerParameters(std::vector<RuntimeRenderState>& states) const bool RenderSnapshotBuilder::TryRefreshLayerParameters(std::vector<RuntimeRenderState>& states) const
{ {
RefreshLayerParameters(mRuntimeStore.CopyLayerStates(), states); RefreshLayerParameters(mRuntimeStore.CopyCommittedLiveLayerStates(), states);
return true; return true;
} }
@@ -113,10 +113,10 @@ void RenderSnapshotBuilder::BuildLayerRenderStates(unsigned outputWidth, unsigne
{ {
states.clear(); states.clear();
for (const LayerStackStore::LayerPersistentState& layer : readModel.layers) for (const LayerStackStore::LayerPersistentState& layer : readModel.committedLiveState.layers)
{ {
auto shaderIt = readModel.packagesById.find(layer.shaderId); auto shaderIt = readModel.committedLiveState.packagesById.find(layer.shaderId);
if (shaderIt == readModel.packagesById.end()) if (shaderIt == readModel.committedLiveState.packagesById.end())
continue; continue;
const ShaderPackage& shaderPackage = shaderIt->second; const ShaderPackage& shaderPackage = shaderIt->second;

View File

@@ -576,24 +576,37 @@ ShaderCompilerInputs RuntimeStore::GetShaderCompilerInputs() const
return inputs; return inputs;
} }
CommittedLiveStateReadModel RuntimeStore::BuildCommittedLiveStateReadModel() const
{
CommittedLiveStateReadModel model;
std::lock_guard<std::mutex> lock(mMutex);
model.layers = mLayerStack.Layers();
model.packagesById = mShaderCatalog.CaptureSnapshot().packagesById;
return model;
}
RenderSnapshotReadModel RuntimeStore::BuildRenderSnapshotReadModel() const RenderSnapshotReadModel RuntimeStore::BuildRenderSnapshotReadModel() const
{ {
RenderSnapshotReadModel model; RenderSnapshotReadModel model;
model.signalStatus = mHealthTelemetry.GetSignalStatusSnapshot(); model.signalStatus = mHealthTelemetry.GetSignalStatusSnapshot();
model.committedLiveState = BuildCommittedLiveStateReadModel();
std::lock_guard<std::mutex> lock(mMutex); std::lock_guard<std::mutex> lock(mMutex);
model.layers = mLayerStack.Layers();
model.packagesById = mShaderCatalog.CaptureSnapshot().packagesById;
model.timing.startTime = mStartTime; model.timing.startTime = mStartTime;
model.timing.startupRandom = mStartupRandom; model.timing.startupRandom = mStartupRandom;
return model; return model;
} }
std::vector<RuntimeStore::LayerPersistentState> RuntimeStore::CopyLayerStates() const std::vector<RuntimeStore::LayerPersistentState> RuntimeStore::CopyCommittedLiveLayerStates() const
{ {
std::lock_guard<std::mutex> lock(mMutex); std::lock_guard<std::mutex> lock(mMutex);
return mLayerStack.Layers(); return mLayerStack.Layers();
} }
std::vector<RuntimeStore::LayerPersistentState> RuntimeStore::CopyLayerStates() const
{
return CopyCommittedLiveLayerStates();
}
RenderTimingSnapshot RuntimeStore::GetRenderTimingSnapshot() const RenderTimingSnapshot RuntimeStore::GetRenderTimingSnapshot() const
{ {
std::lock_guard<std::mutex> lock(mMutex); std::lock_guard<std::mutex> lock(mMutex);

View File

@@ -71,7 +71,9 @@ public:
void ClearReloadRequest(); void ClearReloadRequest();
bool CopyShaderPackageForStoredLayer(const std::string& layerId, ShaderPackage& shaderPackage, std::string& error) const; bool CopyShaderPackageForStoredLayer(const std::string& layerId, ShaderPackage& shaderPackage, std::string& error) const;
::ShaderCompilerInputs GetShaderCompilerInputs() const; ::ShaderCompilerInputs GetShaderCompilerInputs() const;
::CommittedLiveStateReadModel BuildCommittedLiveStateReadModel() const;
::RenderSnapshotReadModel BuildRenderSnapshotReadModel() const; ::RenderSnapshotReadModel BuildRenderSnapshotReadModel() const;
std::vector<LayerPersistentState> CopyCommittedLiveLayerStates() const;
std::vector<LayerPersistentState> CopyLayerStates() const; std::vector<LayerPersistentState> CopyLayerStates() const;
::RenderTimingSnapshot GetRenderTimingSnapshot() const; ::RenderTimingSnapshot GetRenderTimingSnapshot() const;
::RuntimeStatePresentationReadModel BuildRuntimeStatePresentationReadModel() const; ::RuntimeStatePresentationReadModel BuildRuntimeStatePresentationReadModel() const;

View File

@@ -27,10 +27,15 @@ struct RenderTimingSnapshot
double startupRandom = 0.0; double startupRandom = 0.0;
}; };
struct RenderSnapshotReadModel struct CommittedLiveStateReadModel
{ {
std::vector<LayerStackStore::LayerPersistentState> layers; std::vector<LayerStackStore::LayerPersistentState> layers;
std::map<std::string, ShaderPackage> packagesById; std::map<std::string, ShaderPackage> packagesById;
};
struct RenderSnapshotReadModel
{
CommittedLiveStateReadModel committedLiveState;
HealthTelemetry::SignalStatusSnapshot signalStatus; HealthTelemetry::SignalStatusSnapshot signalStatus;
RenderTimingSnapshot timing; RenderTimingSnapshot timing;
}; };

View File

@@ -7,8 +7,8 @@ Phase 1 named the subsystems. Phase 2 added the typed event substrate. Phase 3 m
## Status ## Status
- Phase 5 design package: proposed. - Phase 5 design package: proposed.
- Phase 5 implementation: Step 3 complete. - Phase 5 implementation: Step 5 complete.
- Current alignment: Phase 3 introduced the first pure composition boundary and transient OSC overlay owner. Phase 5 now has a small `RuntimeStateLayerModel` inventory that names the current state categories, `RenderStateComposer` consumes a `LayeredRenderStateInput` whose fields make base persisted, committed live, and transient automation inputs explicit, and `RuntimeLiveState` owns transient-overlay invalidation against current layer/parameter compatibility. Committed runtime values are still physically stored through `RuntimeStore`/`LayerStackStore`. - Current alignment: Phase 3 introduced the first pure composition boundary and transient OSC overlay owner. Phase 5 now has a small `RuntimeStateLayerModel` inventory that names the current state categories, `RenderStateComposer` consumes a `LayeredRenderStateInput` whose fields make base persisted, committed live, and transient automation inputs explicit, `RuntimeLiveState` owns transient-overlay invalidation against current layer/parameter compatibility, settled OSC commits have an explicit session-only persistence policy, and snapshot publication consumes a named `CommittedLiveStateReadModel`. Committed runtime values are still physically backed by `RuntimeStore`/`LayerStackStore` during this conservative migration step.
Current live-state footholds: Current live-state footholds:
@@ -20,6 +20,8 @@ Current live-state footholds:
- `RuntimeServiceLiveBridge` drains OSC ingress/completion queues and applies them to render live state during frame preparation. - `RuntimeServiceLiveBridge` drains OSC ingress/completion queues and applies them to render live state during frame preparation.
- `RuntimeStateLayerModel` names the Phase 5 state categories and classifies current fields as base persisted, committed live, transient automation, render-local, or health/config state. - `RuntimeStateLayerModel` names the Phase 5 state categories and classifies current fields as base persisted, committed live, transient automation, render-local, or health/config state.
- `RuntimeCoordinator` can request layer-scoped transient OSC invalidation, while `RuntimeLiveState` prunes overlays that no longer map to the current render-facing layer/parameter definitions. - `RuntimeCoordinator` can request layer-scoped transient OSC invalidation, while `RuntimeLiveState` prunes overlays that no longer map to the current render-facing layer/parameter definitions.
- `RuntimeCoordinator::CommitOscParameterByControlKey(...)` commits settled OSC values into session state without requesting persistence by default.
- `CommittedLiveStateReadModel` names the current committed/session read boundary that feeds render snapshot publication while remaining physically backed by `RuntimeStore`.
## Why Phase 5 Exists ## Why Phase 5 Exists
@@ -287,22 +289,31 @@ Make the transient-to-committed path explicit.
Initial target: Initial target:
- document and test whether settled OSC commits persist - [x] document and test whether settled OSC commits persist
- ensure stale generation completions are ignored - [x] ensure stale generation completions are ignored
- ensure one settled route does not clear unrelated overlay state - [x] ensure one settled route does not clear unrelated overlay state
- publish or preserve useful events for accepted overlay commits - [x] publish or preserve useful events for accepted overlay commits
Current Phase 3 behavior is a good base; Phase 5 should make the policy easier to reason about from the code. Current Phase 3 behavior is a good base; Phase 5 should make the policy easier to reason about from the code.
Current policy:
- settled OSC commits are `RuntimeCoordinatorOscCommitPersistence::SessionOnly` by default
- accepted settled OSC commits update the committed session value through `RuntimeStore::SetStoredParameterValue(..., persistState = false, ...)`
- accepted settled OSC commits publish runtime mutation/state-change observations, but no `RuntimePersistenceRequested` event
- accepted service-side commit completions publish `OscOverlaySettled`
- stale generation completions are ignored by `RuntimeLiveState::ApplyOscCommitCompletions(...)`
- unrelated routes remain untouched when a different route settles or completes
### Step 5. Separate Committed-Live Concept From Durable Storage ### Step 5. Separate Committed-Live Concept From Durable Storage
Decide whether to physically split committed-live state now or introduce a read/model boundary first. Decide whether to physically split committed-live state now or introduce a read/model boundary first.
Conservative option: Conservative option:
- leave storage physically in `RuntimeStore` - [x] leave storage physically in `RuntimeStore`
- add a named committed-live read model - [x] add a named committed-live read model
- keep persistence decisions in `RuntimeCoordinator` - [x] keep persistence decisions in `RuntimeCoordinator`
Stronger option: Stronger option:
@@ -312,6 +323,13 @@ Stronger option:
Phase 5 does not need a flag-day split. It needs the concept to stop being implicit. Phase 5 does not need a flag-day split. It needs the concept to stop being implicit.
Current implementation:
- `CommittedLiveStateReadModel` carries the current committed/session layer stack and shader package metadata used by snapshot publication.
- `RenderSnapshotReadModel` contains `committedLiveState` rather than exposing layer-stack fields directly.
- `RenderSnapshotBuilder` builds render snapshots and parameter refreshes from committed-live read APIs.
- `RuntimeStore` still provides the physical backing during this phase, but session-only committed changes can be observed through the committed-live read model without requiring durable persistence.
### Step 6. Update Docs And Exit Criteria ### Step 6. Update Docs And Exit Criteria
Before calling Phase 5 complete, update: Before calling Phase 5 complete, update:
@@ -377,8 +395,8 @@ Phase 5 can be considered complete once the project can say:
- [x] final render-value precedence is explicit and covered by tests - [x] final render-value precedence is explicit and covered by tests
- [x] `RenderStateComposer` or its replacement consumes a layered input contract - [x] `RenderStateComposer` or its replacement consumes a layered input contract
- [x] reset/reload/preset behavior for transient overlays is centralized or clearly delegated - [x] reset/reload/preset behavior for transient overlays is centralized or clearly delegated
- [ ] OSC overlay settle/commit behavior is explicit, including persistence policy - [x] OSC overlay settle/commit behavior is explicit, including persistence policy
- [ ] `RuntimeStore` remains durable-state focused and does not absorb transient automation policy - [x] `RuntimeStore` remains durable-state focused and does not absorb transient automation policy
- [ ] render-local temporal/feedback state remains separate from live parameter layering - [ ] render-local temporal/feedback state remains separate from live parameter layering
- [ ] subsystem docs and the architecture review reflect the final ownership model - [ ] subsystem docs and the architecture review reflect the final ownership model

View File

@@ -113,6 +113,8 @@ Phase 5's `RuntimeStateLayerModel` explicitly keeps temporal history, feedback s
`RuntimeLiveState` now owns transient automation invalidation for render-facing compatibility. It can clear overlays for a target layer/control key and prunes overlays that no longer resolve to the current layer and parameter definitions before applying them to a frame. This keeps shader reload, preset load, and layer removal behavior local to the live-state/composition boundary instead of scattering it through GL drawing code. `RuntimeLiveState` now owns transient automation invalidation for render-facing compatibility. It can clear overlays for a target layer/control key and prunes overlays that no longer resolve to the current layer and parameter definitions before applying them to a frame. This keeps shader reload, preset load, and layer removal behavior local to the live-state/composition boundary instead of scattering it through GL drawing code.
Render snapshots now flow through a named `CommittedLiveStateReadModel`, so render-facing committed state is distinct from durable storage even while both are physically backed by the same store during migration.
### 5. Shader Build Application ### 5. Shader Build Application
Compilation itself may eventually move into a separate build service, but once shader build outputs exist, `RenderEngine` owns: Compilation itself may eventually move into a separate build service, but once shader build outputs exist, `RenderEngine` owns:

View File

@@ -272,6 +272,10 @@ For OSC specifically, the coordinator should eventually decide:
- whether it should later commit into committed live state - whether it should later commit into committed live state
- what reset/reload actions invalidate it - what reset/reload actions invalidate it
Phase 5 sets the default settled OSC policy to session-only. `CommitOscParameterByControlKey(...)` updates committed session state through the store with persistence disabled, publishes ordinary mutation/state-change observations, and does not request a persistence write unless a future explicit policy opts into durable OSC commits.
The committed-live concept now has a named read model, `CommittedLiveStateReadModel`. The coordinator remains the owner of whether a mutation should be durable or session-only, while `RuntimeStore` temporarily backs the read model until a physical `CommittedLiveState` collaborator is worth extracting.
### Health and timing state ### Health and timing state
The coordinator may emit events like: The coordinator may emit events like:

View File

@@ -97,6 +97,8 @@ Those are coordinator concerns, not store concerns.
Phase 5 names this boundary in code through `RuntimeStateLayerModel`: persisted layer stack data, saved parameter values, and stack presets are classified as base persisted state. Operator/session values may still be backed by the store during migration, but their mutation policy is committed-live policy owned by the coordinator, not durable-store policy by default. Phase 5 names this boundary in code through `RuntimeStateLayerModel`: persisted layer stack data, saved parameter values, and stack presets are classified as base persisted state. Operator/session values may still be backed by the store during migration, but their mutation policy is committed-live policy owned by the coordinator, not durable-store policy by default.
Phase 5 also adds `CommittedLiveStateReadModel` as the named read boundary for current session/operator state. During the conservative migration, `RuntimeStore` still backs that model physically, but snapshot publication consumes the committed-live read model rather than treating render-facing state as raw durable storage.
### Runtime Configuration ### Runtime Configuration
Examples: Examples:

View File

@@ -9,6 +9,7 @@
#include <filesystem> #include <filesystem>
#include <fstream> #include <fstream>
#include <iostream> #include <iostream>
#include <map>
#include <string> #include <string>
#include <variant> #include <variant>
#include <windows.h> #include <windows.h>
@@ -311,8 +312,34 @@ void TestRuntimeCoordinatorPersistenceEvents()
Expect(countEvents(overlayEvents, RuntimeEventType::OscOverlayApplied) == 1, "transient OSC overlay is observable"); Expect(countEvents(overlayEvents, RuntimeEventType::OscOverlayApplied) == 1, "transient OSC overlay is observable");
Expect(countEvents(overlayEvents, RuntimeEventType::RuntimePersistenceRequested) == 0, "transient OSC overlay does not request persistence"); Expect(countEvents(overlayEvents, RuntimeEventType::RuntimePersistenceRequested) == 0, "transient OSC overlay does not request persistence");
expectAcceptedPersistence(coordinator.CommitOscParameterByControlKey("alpha", "gain", JsonValue(0.2)), "CommitOscParameterByControlKey", RuntimeCoordinatorResult oscCommitResult = coordinator.CommitOscParameterByControlKey("alpha", "gain", JsonValue(0.2));
"accepted OSC commit is persistent"); std::vector<RuntimeEvent> oscCommitEvents = dispatchAndClear();
Expect(oscCommitResult.accepted, "accepted OSC commit updates committed session state");
Expect(!oscCommitResult.persistenceRequested, "settled OSC commit does not request persistence by default");
Expect(countEvents(oscCommitEvents, RuntimeEventType::RuntimeMutationAccepted) == 1, "settled OSC commit publishes accepted fact");
Expect(countEvents(oscCommitEvents, RuntimeEventType::RuntimeStateChanged) == 1, "settled OSC commit publishes state change");
Expect(countEvents(oscCommitEvents, RuntimeEventType::RuntimePersistenceRequested) == 0, "settled OSC commit publishes no persistence request");
RuntimeStore::StoredParameterSnapshot oscCommitSnapshot;
Expect(store.TryGetStoredParameterByControlKey("alpha", "gain", oscCommitSnapshot, error), "settled OSC commit can be read back");
Expect(!oscCommitSnapshot.currentValue.numberValues.empty() &&
oscCommitSnapshot.currentValue.numberValues[0] == 0.2,
"settled OSC commit updates the committed session value");
CommittedLiveStateReadModel committedLiveState = store.BuildCommittedLiveStateReadModel();
Expect(!committedLiveState.layers.empty(), "committed live read model exposes current session layers");
const auto committedLayerIt = std::find_if(committedLiveState.layers.begin(), committedLiveState.layers.end(),
[&oscCommitSnapshot](const RuntimeStore::LayerPersistentState& layer) { return layer.id == oscCommitSnapshot.layerId; });
Expect(committedLayerIt != committedLiveState.layers.end(), "committed live read model preserves layer identity");
if (committedLayerIt != committedLiveState.layers.end())
{
const auto committedValueIt = committedLayerIt->parameterValues.find("gain");
Expect(committedValueIt != committedLayerIt->parameterValues.end() &&
!committedValueIt->second.numberValues.empty() &&
committedValueIt->second.numberValues[0] == 0.2,
"committed live read model includes session-only OSC commit value");
}
Expect(committedLiveState.packagesById.find("alpha") != committedLiveState.packagesById.end(),
"committed live read model carries package definitions for snapshot publication");
} }
std::filesystem::remove_all(root); std::filesystem::remove_all(root);