Phase 3 docs
All checks were successful
CI / React UI Build (push) Successful in 10s
CI / Native Windows Build And Tests (push) Successful in 2m27s
CI / Windows Release Package (push) Successful in 2m46s

This commit is contained in:
Aiden
2026-05-11 16:24:52 +10:00
parent d4f6a4a268
commit 00b6ad4c36
2 changed files with 363 additions and 0 deletions

View File

@@ -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`

View File

@@ -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.