# Phase 3 Design: Live State And Service Coordination

This document expands Phase 3 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 split runtime responsibilities into named subsystems. Phase 2 added the typed internal event model those subsystems can coordinate through. Phase 3 should now finish the service-facing and live-state cleanup needed before the app attempts sole-owner GL rendering.

## Status

- Phase 3 design package: accepted.
- Phase 3 implementation: exit criteria satisfied for the current architecture.
- Current alignment: the repo now has the live-state/composer building blocks, a service bridge, and a named frame-state handoff. `OpenGLComposite::renderEffect()` still remains the app-level frame entrypoint, but the service drain, layer-state resolution, and OSC commit handoff now sit behind named helpers and frame-state data.

Current footholds:

- `RuntimeStore` is split into durable state collaborators: `RuntimeConfigStore`, `LayerStackStore`, `ShaderPackageCatalog`, `RenderSnapshotBuilder`, presentation read models, and `HealthTelemetry`.
- `RuntimeCoordinator` owns mutation validation/classification and publishes accepted/rejected/follow-up events.
- `RuntimeSnapshotProvider` publishes render snapshots from `RenderSnapshotBuilder`.
- `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.
- `RenderFrameInput` / `RenderFrameState` now provide a named frame-facing handoff model for preparing layer state and render inputs before drawing.
- `RenderFrameStateResolver` now owns snapshot cache selection, parameter refresh decisions, and final frame-state resolution before drawing.
- `RenderEngine` owns GL/render resources and delegates frame-state preparation to the resolver.
- `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.

The current architecture is much better than the original `RuntimeHost` shape. Phase 4 has since moved normal runtime GL work onto the `RenderEngine` render thread, so the remaining render-facing risk is no longer shared context ownership; it is the later producer/consumer playout work needed to keep DeckLink callbacks from synchronously waiting on output production.

## Why Phase 3 Exists

The resilience review says render-thread isolation should come after state access and control coordination are no longer centered on a large mutable runtime object. Phase 2 gives us the event substrate; Phase 3 should make the data flowing into render explicit enough that Phase 4 can make the render thread the sole GL owner without dragging service coordination and state reconciliation with it.

The main problems Phase 3 addressed:

- transient OSC overlay state and persisted committed state needed a named reconciliation boundary
- `RenderEngine` needed to move final frame-state selection and value composition out of drawing code
- service-side queues for pending OSC updates and completed OSC commits needed a bridge outside `OpenGLComposite`
- Phase 6 has since moved runtime-state persistence requests onto a debounced background writer
- `RuntimeUpdateController` still exists partly as compatibility glue between synchronous coordinator results and event-driven effects

## Goals

Phase 3 should establish:

- an explicit live-state model separating persisted state, committed runtime state, and transient automation overlay
- service-facing event bridges for OSC overlay updates and overlay commit completions
- a narrower `OpenGLComposite::renderEffect()` that renders a prepared read model instead of orchestrating runtime/service state
- a clear owner for final render-layer state resolution before it reaches GL drawing
- a contained persistence request model that later Phase 6 connected to the background writer
- tests for live-state composition, overlay settlement, and service-to-runtime event behavior without GL or DeckLink

## Non-Goals

Phase 3 should not require:

- a dedicated render thread
- moving all GL calls off the current callback path
- a background persistence writer implementation
- a final DeckLink lifecycle state machine
- replacing every direct synchronous command API
- a final cue/preset/timeline system

Those are later phases. Phase 3 is about making state and service coordination clean enough for those later phases.

## Current Coordination Shape

`OpenGLComposite::renderEffect()` is now the app-level frame entrypoint, but it is intentionally narrow:

1. pumps `RuntimeUpdateController::ProcessRuntimeWork()`
2. builds a `RenderFrameInput`
3. renders the frame through `RuntimeServiceLiveBridge`, `RenderFrameStateResolver`, and `RenderEngine`

The bridge now owns service queue draining, live automation settlement, committed/live state selection, and OSC commit handoff. `RenderFrameStateResolver` owns snapshot cache selection, parameter refresh decisions, and dynamic render-field refresh before handing a prepared frame state to `RenderEngine`.

## Target State Model

Phase 3 should formalize three state categories:

| State category | Owner | Lifetime | Render role |
| --- | --- | --- | --- |
| Persisted layer state | `LayerStackStore` behind `RuntimeStore` | saved durable state | base layer stack and saved parameter values |
| Committed runtime state | `RuntimeCoordinator` / snapshot publication | accepted operator/UI/OSC commits | stable render snapshot selected for rendering |
| Transient automation overlay | new live-state collaborator or narrowed render-side owner | high-rate OSC automation between commits | temporary per-route override blended into final values |

Render should eventually consume:

```text
final render state = published snapshot + committed live selection + transient overlay
```

The important change is not the exact formula name. The important change is that final render-state composition has one named owner and can be tested without GL.

## Phase 3 Collaborators

### `RuntimeLiveState`

Small runtime collaborator for transient automation state.

Responsibilities:

- keep transient OSC overlay values keyed by route
- track overlay generation and pending commit generation
- apply overlay commit completions
- decide when an overlay value has settled enough to request a commit
- build a `LiveStateOverlaySnapshot` for final render-state composition

Non-responsibilities:

- persistent state mutation
- shader package lookup
- GL resources
- OSC socket ownership

### `RenderStateComposer`

Pure or mostly pure collaborator for frame value composition.

Responsibilities:

- combine published render snapshots with live overlay state
- apply smoothing/time-based automation policy
- return final `RuntimeRenderState` values plus any commit requests
- stay testable without OpenGL

Non-responsibilities:

- drawing
- service queue draining
- disk persistence
- OSC packet parsing

### `RuntimeServiceLiveBridge`

`RuntimeServiceLiveBridge` is the current source-local bridge between services, live state, and render-state preparation.

Responsibilities:

- translate service-side OSC ingress into typed events or live-state commands
- publish overlay applied/settled events where useful
- route overlay commit requests to `RuntimeCoordinator`
- keep `OpenGLComposite` out of service queue draining

Non-responsibilities:

- final GL rendering
- persistent store mutation outside coordinator APIs

## Event Bridge Targets

| Current flow | Phase 3 bridge target | Notes |
| --- | --- | --- |
| pending OSC updates drained by `OpenGLComposite` | `OscValueReceived` -> live-state overlay update handler | Phase 2 already has the event type; Phase 3 decides whether transient overlay updates enter the app dispatcher or a source-local bridge. |
| render asks for overlay commit requests | `OscOverlaySettled` or direct coordinator command plus event publication | Commit request creation should leave `renderEffect()` and live near the live-state owner. |
| completed OSC commits drained by `OpenGLComposite` | `RuntimeMutationAccepted` / completion event -> live-state commit completion | Completed commit routing should be event-driven or owned by live-state service bridge. |
| `RenderFrameStateResolver::Resolve(...)` | `RenderStateComposer::BuildFrameState(...)` | Keep final state composition testable without GL. |
| direct persistence writes from store mutations | `RuntimePersistenceRequested` as the durable write trigger | Phase 6 later connected this request boundary to the background writer. |
| runtime-state broadcast side effects | `RuntimeStateBroadcastRequested` plus optional completed/failed observations | Keep broadcast delivery in services and presentation ownership in runtime presentation. |

## Runtime Store Scope In Phase 3

`RuntimeStore` is already much smaller than the original host, but Phase 3 should keep narrowing it toward durable state and read-model publishing.

Target responsibilities:

- initialize runtime config and persistent state
- expose durable layer/package/config read models
- own saved layer stack and preset serialization while exposing snapshots for the background writer
- publish or support immutable render/presentation snapshots

Avoid adding:

- transient OSC overlay state
- frame-local render composition decisions
- service queue coordination
- background worker policy

## Runtime Coordinator Scope In Phase 3

`RuntimeCoordinator` should remain the command/mutation policy owner.

Keep:

- validation/classification
- accepted/rejected mutation publication
- reload/build/persistence follow-up events
- synchronous command results for UI/API callers that need immediate success or error

Narrow:

- any behavior that looks like render-frame state composition
- any direct service queue interpretation
- any persistence timing policy beyond publishing `RuntimePersistenceRequested`

## Render Engine Scope In Phase 3

`RenderEngine` should move closer to being a GL/render-local owner.

Keep:

- GL resources
- shader programs
- render passes
- preview/output rendering
- temporal history and feedback resources

Move or narrow:

- transient OSC overlay bookkeeping
- final layer-state composition from snapshot plus overlay
- creation of commit requests from smoothed overlay values

Some transient render-only state may remain in `RenderEngine` if it truly belongs to GL or temporal resources. But value composition should be separable from drawing.

## OpenGLComposite Scope In Phase 3

`OpenGLComposite` should remain the current composition root, but not the runtime-service coordinator.

Target:

- wire collaborators
- own app-level lifecycle
- initialize GL/backend/runtime services
- call narrow render/update entrypoints

Avoid:

- draining OSC queues directly
- converting service DTOs into render DTOs
- deciding final layer-state composition
- coordinating commit completion settlement

## Persistence Position

Phase 3 did not implement the background writer, but it prepared the request boundary that Phase 6 now uses.

Target behavior by Phase 3 exit:

- state mutations publish `RuntimePersistenceRequested`
- persistence can be observed and tested as an event side effect
- disk writes are not inferred by callers; later Phase 6 routes accepted durable mutations through `RuntimePersistenceRequested` and the background writer
- callers outside the store/coordinator should not infer disk writes from mutation categories

This kept Phase 6 smaller: the background snapshot writer consumes persistence requests and stored-state snapshots rather than rediscovering mutation policy.

## Migration Plan

### Step 1. Name The Live State Boundary

Introduce `RuntimeLiveState`, `RenderStateComposer`, or an equivalent pair of classes.

Start by moving pure data operations out of frame rendering without changing behavior.

Status: complete for Phase 3. `runtime/live/RuntimeLiveState` and `runtime/live/RenderStateComposer` exist, are included in the build, and have a focused `RuntimeLiveStateTests` target.

### Step 2. Move OSC Overlay Bookkeeping Behind The Boundary

Move these responsibilities out of the current frame orchestration:

- overlay updates by route
- commit completion tracking
- generation matching
- settle/commit request creation

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: complete for Phase 3. `RenderEngine` still exposes compatibility methods used by the service bridge, but it 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

Replace `OpenGLComposite::renderEffect()` queue draining with a bridge that publishes or applies:

- `OscValueReceived`
- `OscOverlayApplied`
- `OscOverlaySettled`
- overlay commit completion observations

This is where the remaining Phase 2 open question about transient OSC overlay event scope should be resolved for the current architecture.

Status: complete for Phase 3. `RuntimeServiceLiveBridge` now drains pending OSC updates and completed OSC commits, applies them to render live state, and queues settled commit requests. It remains a source-local bridge by design until later live-state layering decides whether transient automation should enter the app-level dispatcher.

### Step 4. Narrow `OpenGLComposite::renderEffect()`

Target shape:

```cpp
void OpenGLComposite::renderEffect()
{
    mRuntimeUpdateController->ProcessRuntimeWork();
    const RenderFrameInput frameInput = BuildRenderFrameInput();
    RenderFrame(frameInput);
}
```

The exact names can change. The goal is that render effect no longer manually drains services, settles overlay commits, and resolves layer values.

Status: complete for Phase 3. `OpenGLComposite::renderEffect()` now processes runtime work, builds `RenderFrameInput`, and calls a narrow frame-render helper. Service draining, state resolution, and commit handoff sit behind `RuntimeServiceLiveBridge::PrepareLiveRenderFrameState(...)`, `RenderFrameStateResolver`, and `RenderFrameState`.

### Step 5. Add Persistence Boundary Tests

Add behavior tests for:

- accepted persisted mutations publish `RuntimePersistenceRequested`
- transient OSC commits do not force immediate persistence
- preset load/save persistence requests remain explicit
- rejected mutations do not publish persistence work

Status: complete for Phase 3. `RuntimeSubsystemTests` and `RuntimeEventTypeTests` cover accepted mutation persistence requests, rejected mutations, and transient OSC overlay behavior that does not request persistence.

### Step 6. Update Docs And Phase 4 Readiness

Before calling Phase 3 complete, update:

- subsystem docs for new live-state/composer collaborators
- architecture review checklist
- Phase 4 assumptions about render thread input state

Status: complete. The Phase 4 design note started from the `RenderFrameInput` / `RenderFrameState` contract and has now completed the shared-GL ownership migration.

## Testing Strategy

Phase 3 tests should avoid GL, DeckLink, and sockets.

Recommended tests:

- final layer-state composition applies snapshot values when no overlay exists
- transient overlay overrides the matching parameter by route
- smoothing moves toward target values over time
- overlay settle creates one commit request per route/generation
- completed commits clear pending overlay commit state
- stale commit completions are ignored by generation
- accepted mutations publish persistence requests where expected
- rejected mutations do not publish persistence or render follow-ups
- `OpenGLComposite` no longer needs to drain service result queues for runtime effects

Existing useful homes:

- `RuntimeSubsystemTests` for pure state/composer behavior
- `RuntimeEventTypeTests` for event bridge behavior
- `RuntimeLiveStateTests` for the new live-state/composer boundary

## Parallel Work Lanes

The current groundwork is intended to let these lanes proceed in parallel with low overlap:

| Lane | Primary files | Goal |
| --- | --- | --- |
| A. Live-state behavior | `runtime/live/RuntimeLiveState.*`, `tests/RuntimeLiveStateTests.cpp` | Implemented for Phase 3: stale completion, smoothing, trigger behavior, and overlay settle policy are covered by focused tests. |
| B. Render-state composition | `runtime/live/RenderStateComposer.*`, `gl/frame/RenderFrameStateResolver.*`, `gl/RenderEngine.*` | Implemented for Phase 3: value composition and frame-state selection sit outside GL drawing while GL calls remain in `RenderEngine`. |
| C. Service bridge | `control/RuntimeServices.*`, `control/RuntimeServiceLiveBridge.*`, `control/ControlServices.*` | Implemented for Phase 3: `OpenGLComposite::renderEffect()` no longer drains OSC update/completion queues directly. |
| D. App-frame orchestration | `gl/composite/OpenGLComposite.*`, `gl/frame/RuntimeUpdateController.*` | Implemented for Phase 3: render-effect glue is a narrow runtime-work, frame-input, render-frame sequence. |
| E. Persistence boundary | `runtime/coordination/RuntimeCoordinator.*`, `runtime/store/*`, event tests | Implemented for Phase 3: persistence request publication is explicit. Phase 6 later wired those requests to the background writer. |

## Phase 3 Exit Criteria

Phase 3 can be considered complete once the project can say:

- [x] final render-state composition has named owners outside `OpenGLComposite` (`RenderStateComposer` covers live value composition; `RenderFrameStateResolver` covers snapshot/cache selection and frame-state resolution)
- [x] transient OSC overlay state has a named owner and tests
- [x] overlay commit requests and completions no longer require `OpenGLComposite` to drain service queues directly
- [x] `RenderEngine` is closer to GL/render resource ownership and less responsible for value composition
- [x] `RuntimeStore` remains durable-state focused and does not gain live overlay responsibilities
- [x] persistence requests are explicit event outcomes for persisted mutations
- [x] Phase 4 can define a render-thread input contract around immutable or near-immutable frame state

## Open Questions

- Should transient OSC overlay values enter the app-level event dispatcher, or should they use a dedicated source-local latest-value bridge until live-state layering is finalized?
- Should the new live-state owner live under `runtime/`, `gl/`, or a new `renderstate/` boundary?
- Should smoothing policy be owned by live state, render-state composition, or render settings?
- Should overlay commit completion be represented as a new typed event, or derived from existing accepted mutation events with route/generation metadata?
- Should preset save remain synchronous after Phase 6, or eventually move behind a completion-based async request?

## Short Version

Phase 3 should make the app's live state boring and explicit.

- persisted state stays in the store
- accepted command policy stays in the coordinator
- transient automation gets a named owner
- final render-state composition becomes testable without GL
- `OpenGLComposite` stops manually reconciling service queues and layer values

Once that is true, Phase 4 can make the render thread the sole GL owner without also having to invent a clean state model at the same time.
