# Phase 5 Design: Live State Layering And Composition This document expands Phase 5 of [ARCHITECTURE_RESILIENCE_REVIEW.md](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/docs/ARCHITECTURE_RESILIENCE_REVIEW.md) into a concrete design target. Phase 1 named the subsystems. Phase 2 added the typed event substrate. Phase 3 made render-facing live state explicit through `RuntimeLiveState`, `RenderStateComposer`, `RenderFrameInput`, `RenderFrameState`, `RenderFrameStateResolver`, and `RuntimeServiceLiveBridge`. Phase 4 made one render thread the owner of normal runtime GL work. Phase 5 should now make the live parameter model itself explicit: persisted truth, operator/session truth, and transient automation should be separate layers with one predictable composition rule. ## Status - Phase 5 design package: complete. - Phase 5 implementation: 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, `RuntimeLiveState` owns transient-overlay invalidation against current layer/parameter compatibility, settled OSC commits have an explicit session-only persistence policy, and `CommittedLiveState` physically owns current session layer state. `RuntimeStore` still owns file IO, config, package metadata, preset persistence, and persistence requests. Current live-state footholds: - `RuntimeStore` owns file IO, config, package metadata, preset persistence, persistent-state serialization, and persistence requests. - `CommittedLiveState` physically owns the current committed/session layer stack and parameter values. - `RuntimeCoordinator` owns mutation validation, classification, accepted/rejected event publication, snapshot/reload follow-ups, and the policy switch between committed states and live snapshots. - `RuntimeSnapshotProvider` publishes render-facing snapshots from committed runtime state. - `RuntimeLiveState` owns transient OSC overlay bookkeeping, smoothing, generation tracking, and commit-settlement policy. - `RenderStateComposer` consumes `LayeredRenderStateInput`, chooses committed-live layer states over base-persisted layer states when both are supplied, applies transient automation on top, and returns final per-frame layer states plus settled commit requests. - `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. - `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. - `CommittedLiveState` owns current committed/session layer state and exposes `CommittedLiveStateReadModel` for render snapshot publication. ## Why Phase 5 Exists The resilience review identifies live OSC overlay and persisted state as separate concepts that still do not have a fully formal model. The app now has better boundaries, but several policies are still implicit: - whether a value is durable, committed for the current session, or transient automation - whether an OSC value should merely influence the current frame or eventually commit - what reload, preset load, layer removal, shader change, and reset should do to transient values - which layer wins when UI/operator changes race with OSC automation - which state changes should publish snapshots, request persistence, or only affect render frames Without a formal layering model, these rules can leak across `RuntimeStore`, `RuntimeCoordinator`, `RuntimeLiveState`, `RenderStateComposer`, and service bridges. Phase 5 should make those rules boring and testable. ## Goals Phase 5 should establish: - explicit state layers for persisted, committed/session, and transient automation values - one named composition contract for final render values - clear ownership for layer-specific mutation policy - explicit reset/reload/preset behavior for transient and committed state - a clean path for OSC automation to remain high-rate without becoming durable state by accident - tests for layer precedence, lifecycle, invalidation, and commit policy without GL or DeckLink - documentation that distinguishes render-local temporal/feedback state from parameter/live-state overlays ## Non-Goals Phase 5 should not require: - a background persistence writer implementation - a DeckLink producer/consumer playout queue - a full cue/timeline/preset performance system - a new UI state-management framework - replacing every synchronous coordinator API - moving temporal history or shader feedback into the runtime state model Those are later phases or separate feature work. Phase 5 is about parameter and live-value layering. ## Target State Model Phase 5 should formalize three layers: | Layer | Owner | Lifetime | Persistence | Render role | | --- | --- | --- | --- | --- | | Base persisted state | `RuntimeStore` plus durable serialization/preset IO | survives restart | written to disk | default saved layer stack, shader selections, saved parameter values | | Committed live state | `CommittedLiveState` with policy owned by `RuntimeCoordinator` | current running session | may request persistence depending on mutation type | operator/UI/current truth until changed again | | Transient automation overlay | `RuntimeLiveState` or a new automation overlay collaborator | high-rate/short-lived | not persisted directly | temporary OSC/automation target applied over committed truth | The target composition rule is: ```text final render state = base persisted state + committed live state + transient automation overlay ``` The actual implementation may continue using render snapshots as the base transport. The important part is that each layer has named ownership, documented lifetime, and tested precedence. ## Current Composition Shape Today, final frame state is prepared through this path: 1. `OpenGLComposite::renderEffect()` processes runtime work. 2. `OpenGLComposite` builds `RenderFrameInput`. 3. `RuntimeServiceLiveBridge` drains OSC updates and completed commits. 4. `RenderEngine` updates `RuntimeLiveState`. 5. `RenderFrameStateResolver` chooses committed states or live snapshot states. 6. `RenderStateComposer` applies transient overlay values. 7. `RenderEngine::RenderPreparedFrame(...)` consumes `RenderFrameState`. That is a good Phase 3/4 foundation. Phase 5 should make the hidden assumptions in steps 5 and 6 explicit enough that reset/reload/preset and future UI automation behavior are not scattered across those collaborators. ## Proposed Collaborators ### `RuntimeStateLayerModel` Optional pure model that names the layers and composition metadata. Responsibilities: - represent base, committed, and transient layer state inputs - define precedence and invalidation categories - expose a pure composition function or input object - keep GL, services, persistence, and device callbacks out of the model Non-responsibilities: - disk IO - OSC socket handling - render-thread scheduling - shader compilation This may be a small set of structs rather than a large class. The value is in naming the contract. ### `CommittedLiveState` Runtime/session collaborator for committed current-session state that has moved out of `RuntimeStore` physical ownership. Responsibilities: - hold operator/UI committed values that are true for the current session - distinguish persistence-required commits from session-only commits - expose a read model for snapshot publication - provide reset/load behavior separate from durable storage Non-responsibilities: - transient OSC smoothing - disk writes - GL resources Phase 5 now uses this physical split. `RuntimeStore` still wraps it for compatibility and persistence IO, but committed values no longer live directly as store fields. ### `AutomationOverlayState` Possible evolution of `RuntimeLiveState`. Responsibilities: - hold transient automation values keyed by route/layer/parameter identity - track generation, commit-in-flight, and completion - apply smoothing and settle policy - decide whether an overlay is render-only, commit-requesting, stale, or invalidated Non-responsibilities: - owning committed truth - persistent state mutation - snapshot publication This can start by renaming or narrowing current `RuntimeLiveState` responsibilities rather than replacing it outright. ### `LayeredStateComposer` Possible evolution of `RenderStateComposer`. Responsibilities: - apply the target precedence rule - produce final `RuntimeRenderState` values for a frame - return commit requests or overlay observations when policy says a transient value settled - keep value composition testable without OpenGL Non-responsibilities: - frame rendering - service queue draining - storage mutation ## Layering Rules ### Precedence Default precedence should be: 1. base persisted/snapshot value 2. committed live/session value 3. transient automation overlay The topmost valid layer wins for discrete values. Numeric/vector values may be smoothed by overlay policy before they win. ### Identity Layering should use stable render-facing identity: - layer id for persisted structural identity - layer key/control key for OSC-facing identity - parameter id for shader-defined identity - parameter control key for external-control identity Current policy treats render-facing layer identity plus parameter/control-key compatibility as authoritative. Incompatible transient overlays are pruned before composition, so stale OSC routes do not migrate onto unrelated controls after layer removal, preset load, shader change, or incompatible reload. ### Invalidations The following should have explicit behavior: - layer removed: clear committed and transient state for that layer - layer shader changed: clear or remap parameter overlays according to compatible control keys - preset loaded: replace base/committed state and clear incompatible transient overlays - shader reload with same controls: preserve compatible transient overlays where safe - manual reset parameters: clear committed overrides and transient overlays for that layer - no input/source changes: should not affect parameter layers ### Commit Policy Transient automation may: - remain render-only - settle and request a committed mutation - commit without persistence - commit with persistence only when the control path explicitly requests it The policy should be explicit per ingress path or parameter category. Phase 5 does not need a full UI for it, but the default behavior should be documented and tested. ## Event And Snapshot Contract Phase 5 should clarify which changes publish which effects: | Change | Snapshot publication | Persistence request | Render reset | Runtime event | | --- | --- | --- | --- | --- | | persisted layer stack mutation | yes | yes | maybe | accepted mutation + persistence requested | | operator live parameter change | yes | maybe | no, unless structural | accepted mutation | | transient OSC overlay update | no committed snapshot by default | no | no | optional overlay observation | | overlay settled commit | yes if accepted | usually no for OSC | no | accepted mutation or overlay-settled observation | | preset load | yes | maybe | temporal/feedback policy dependent | accepted mutation + reload/reset observations | | shader change/reload | yes after build | maybe | temporal/feedback policy dependent | shader build/reload events | This table should evolve with implementation, but Phase 5 should prevent transient overlay updates from masquerading as durable committed state. ## Migration Plan ### Step 1. Inventory Current State Layers Document and/or encode where each current state category lives: - persisted layer stack and parameter values - committed current-session parameter values - runtime compile/reload flags - transient OSC overlays - render-local temporal history and feedback state Initial target: - [x] identify which fields are durable, committed-live, transient automation, render-local, or health/config - [x] update subsystem docs where the current ownership is misleading - [x] add small tests for classification if a pure helper exists ### Step 2. Name The Layered Composition Input Introduce a named composition input model around the previous `RenderStateCompositionInput`. Initial target: - [x] make base/committed/transient inputs visible in type names or field names - [x] keep `RenderStateComposer` behavior unchanged at first - [x] add tests that assert precedence with no GL Possible outcomes: - [x] add a new `LayeredRenderStateInput` - [x] no adapter was needed; callers now use the layered input shape directly ### Step 3. Make Reset And Reload Policy Explicit Move reset/reload transient-state decisions into one policy point. Initial target: - [x] layer removal clears matching transient overlays - [x] shader change clears incompatible overlays - [x] preset load clears incompatible overlays - [x] shader reload can preserve compatible overlays when requested - [x] temporal/feedback resets stay render-local and separate from parameter overlays This is where Phase 5 should prevent "clear everything" and "preserve everything" from being scattered through unrelated code. Current implementation: - `RuntimeCoordinatorResult` carries a named `RuntimeCoordinatorTransientOscInvalidation` request rather than a raw clear-all flag. - `RuntimeUpdateController` applies layer-scoped invalidation to both render-owned overlay state and queued OSC service state. - `RuntimeLiveState::PruneIncompatibleOverlays(...)` is the central compatibility policy for current render-facing layer/parameter definitions. - `RuntimeLiveState::ApplyToLayerStates(...)` prunes incompatible overlays before applying transient values, so shader changes, preset loads, and layer removals stop carrying stale overlays once the current frame state no longer maps them. ### Step 4. Clarify OSC Commit Semantics Make the transient-to-committed path explicit. Initial target: - [x] document and test whether settled OSC commits persist - [x] ensure stale generation completions are ignored - [x] ensure one settled route does not clear unrelated overlay state - [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 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 Separate the committed-live concept from durable storage with both a physical owner and a read/model boundary. Earlier conservative option: - [x] add a named committed-live read model - [x] keep persistence decisions in `RuntimeCoordinator` Stronger option: - [x] introduce `CommittedLiveState` - [x] make `RuntimeSnapshotProvider` consume committed live state through a read model - [x] leave durable writes in `RuntimeStore` The implementation now has the stronger split while keeping `RuntimeStore` as the compatibility facade for existing callers. Current implementation: - `CommittedLiveState` physically owns the current committed/session layer stack. - `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 owns config, package metadata, disk IO, preset files, and persistent-state serialization, but delegates current-session layer mutations to `CommittedLiveState`. ### Step 6. Update Docs And Exit Criteria Before calling Phase 5 complete, update: - [x] architecture review checklist - [x] `RuntimeCoordinator`, `RuntimeStore`, `RuntimeSnapshotProvider`, `RenderEngine`, and `ControlServices` subsystem docs - [x] Phase 6 assumptions about persistence inputs - [x] Phase 7 assumptions about what render/backend state is not part of live parameter layering ## Testing Strategy Phase 5 tests should avoid GL, DeckLink, sockets, and filesystem writes where possible. Recommended tests: - base value is used when no committed or transient value exists - committed value overrides base value - transient overlay overrides committed value - numeric smoothing applies only to transient overlay values - trigger/bool/discrete overlay behavior is explicit - layer removal clears matching transient state - shader change preserves only compatible overlays if policy allows - preset load clears or replaces committed/transient state according to policy - settled OSC overlay creates the expected commit request - settled OSC commit does not request persistence unless policy says so - stale commit completion does not clear a newer overlay - render-local temporal/feedback resets do not mutate parameter layers Existing useful homes: - `RuntimeLiveStateTests` for overlay generation, smoothing, settle, and invalidation behavior - `RuntimeSubsystemTests` for coordinator mutation, persistence request, and reset/reload policy - `RuntimeEventTypeTests` for any new observations or accepted mutation events - a possible new `RuntimeStateLayeringTests` target if the composition model gets a pure helper ## Risks ### Over-Abstraction Risk It would be easy to introduce too many state containers. Phase 5 should add names where they clarify behavior, not create an elaborate framework. ### Persistence Confusion Risk Committed live state and persisted state are related but not identical. If Phase 5 blurs them, Phase 6's background persistence writer will inherit ambiguous inputs. ### Automation Surprise Risk OSC automation can be high-rate and transient, but users may expect settled values to become "real." The commit policy needs to be explicit enough that UI, OSC, presets, and reloads behave predictably. ### Identity/Compatibility Risk Shader changes and preset loads can invalidate layer/parameter identities. Phase 5 should prefer conservative clearing over accidental application of an old automation value to the wrong control. ### Render Coupling Risk Render-local resources such as temporal history, feedback buffers, readback caches, and playout queues are not parameter layers. Keeping them out of this model avoids turning Phase 5 into a render-resource refactor. ## Phase 5 Exit Criteria Phase 5 can be considered complete once the project can say: - [x] persisted, committed-live, and transient automation layers are named in code or clear read models - [x] final render-value precedence is explicit and covered by tests - [x] `RenderStateComposer` or its replacement consumes a layered input contract - [x] reset/reload/preset behavior for transient overlays is centralized or clearly delegated - [x] OSC overlay settle/commit behavior is explicit, including persistence policy - [x] `RuntimeStore` remains durable-state focused and does not absorb transient automation policy - [x] render-local temporal/feedback state remains separate from live parameter layering - [x] subsystem docs and the architecture review reflect the final ownership model ## Open Questions - Should transient OSC overlay updates become app-level typed events, or stay source-local through `RuntimeServiceLiveBridge`? - Should overlay commit persistence be global, ingress-specific, or parameter-definition-driven? - What compatibility rule should apply when shader reload preserves a control key but changes parameter shape? - Should preset load clear all transient automation, or only automation that no longer maps to the loaded stack? - Should UI slider drags use the committed-live layer directly, or a short-lived transient layer that commits on release? ## Short Version Phase 5 should make live values boring and explicit. Persisted state is durable truth. Committed live state is current-session/operator truth. Transient automation is high-rate overlay truth. Render consumes the composed result, and each layer has clear ownership, lifetime, persistence behavior, and reset/reload rules.