From 00b6ad4c369616237a038696246d70dedbae9a76 Mon Sep 17 00:00:00 2001 From: Aiden <68633820+awils27@users.noreply.github.com> Date: Mon, 11 May 2026 16:24:52 +1000 Subject: [PATCH] Phase 3 docs --- docs/ARCHITECTURE_RESILIENCE_REVIEW.md | 4 + ..._LIVE_STATE_SERVICE_COORDINATION_DESIGN.md | 359 ++++++++++++++++++ 2 files changed, 363 insertions(+) create mode 100644 docs/PHASE_3_LIVE_STATE_SERVICE_COORDINATION_DESIGN.md diff --git a/docs/ARCHITECTURE_RESILIENCE_REVIEW.md b/docs/ARCHITECTURE_RESILIENCE_REVIEW.md index 7fe5f62..fde5224 100644 --- a/docs/ARCHITECTURE_RESILIENCE_REVIEW.md +++ b/docs/ARCHITECTURE_RESILIENCE_REVIEW.md @@ -462,6 +462,10 @@ Suggested outcome: After the event model exists, finish separating live committed state and service-facing coordination from the runtime facades. +Dedicated design note: + +- [PHASE_3_LIVE_STATE_SERVICE_COORDINATION_DESIGN.md](/c:/Users/Aiden/Documents/GitHub/video-shader-toys/docs/PHASE_3_LIVE_STATE_SERVICE_COORDINATION_DESIGN.md) + Recommended split: - `RuntimeStore` diff --git a/docs/PHASE_3_LIVE_STATE_SERVICE_COORDINATION_DESIGN.md b/docs/PHASE_3_LIVE_STATE_SERVICE_COORDINATION_DESIGN.md new file mode 100644 index 0000000..a371d7f --- /dev/null +++ b/docs/PHASE_3_LIVE_STATE_SERVICE_COORDINATION_DESIGN.md @@ -0,0 +1,359 @@ +# 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: proposed. +- Phase 3 implementation: not started. +- 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 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`. +- `RenderEngine` owns render-local OSC overlay state and final render-layer resolution. +- `ControlServices` owns OSC ingress, pending OSC updates, completed OSC commit notifications, and service start/stop. +- `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`. + +## 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 addresses: + +- transient OSC overlay state and persisted committed state are still reconciled by hand in `OpenGLComposite::renderEffect()` +- `RenderEngine` both stores transient OSC overlay state and resolves final layer states +- `ControlServices` exposes service-side queues for pending OSC updates and completed OSC commits +- `RuntimeStore` still performs synchronous persistence directly from many state mutation paths +- `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 prepares for the later background writer phase +- 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()` currently performs a lot of cross-subsystem coordination: + +1. pumps `RuntimeUpdateController::ProcessRuntimeWork()` +2. asks `RuntimeServices` to apply pending OSC updates +3. asks `RuntimeServices` for completed OSC commits +4. converts service read models into `RenderEngine::OscOverlayUpdate` and `OscOverlayCommitCompletion` +5. applies those overlay changes to `RenderEngine` +6. asks `RenderEngine` to resolve final render layer states +7. queues OSC commit requests back into `RuntimeServices` +8. asks `RenderEngine` to draw the layer stack + +That works, but it keeps the current frame path responsible for service queue draining, live automation settlement, committed/live state selection, and final render-state composition. Phase 3 should turn that into explicit read models and event bridges. + +## 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. + +## Proposed Collaborators + +### `RuntimeLiveState` + +New small runtime collaborator or equivalent read model builder. + +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` + +New pure or mostly pure collaborator. + +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 + +### `RuntimeServiceEventBridge` + +This may be a new class or a narrowing of `RuntimeUpdateController`/`RuntimeServices`. + +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. | +| `RenderEngine::ResolveRenderLayerStates(...)` | `RenderStateComposer::BuildFrameState(...)` | Keep final state composition testable without GL. | +| direct persistence writes from store mutations | `RuntimePersistenceRequested` as the durable write trigger | Background writer lands later; Phase 3 should make request boundaries clear. | +| 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 until the background writer phase +- 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 should not implement the background writer, but it should prepare for it. + +Target behavior by Phase 3 exit: + +- state mutations publish `RuntimePersistenceRequested` +- persistence can be observed and tested as an event side effect +- synchronous `SavePersistentState()` remains allowed as an implementation detail inside `RuntimeStore` +- callers outside the store/coordinator should not infer disk writes from mutation categories + +This keeps Phase 6 smaller: the background snapshot writer can subscribe to persistence requests and consume a stored-state snapshot 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 `RenderEngine::ResolveRenderLayerStates(...)` without changing behavior. + +### 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. + +### 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. + +### Step 4. Narrow `OpenGLComposite::renderEffect()` + +Target shape: + +```cpp +void OpenGLComposite::renderEffect() +{ + mRuntimeUpdateController->ProcessRuntimeWork(); + RenderFrameState frameState = mRenderFrameCoordinator->BuildFrameState(...); + mRenderEngine->RenderLayerStack(frameState); +} +``` + +The exact names can change. The goal is that render effect no longer manually drains services, settles overlay commits, and resolves layer values. + +### 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 + +### 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 + +## 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 +- a new `RuntimeLiveStateTests.cpp` target if the live-state code grows enough + +## Phase 3 Exit Criteria + +Phase 3 can be considered complete once the project can say: + +- [ ] final render-state composition has a named, testable owner outside `OpenGLComposite` +- [ ] transient OSC overlay state has a named owner and tests +- [ ] 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 +- [ ] `RuntimeStore` remains durable-state focused and does not gain live overlay responsibilities +- [ ] 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 + +## 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? +- How much of persistence should remain synchronous until Phase 6? + +## 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.