RenderCadenceCompositor
This app is the modular version of the working DeckLink render-cadence probe.
Its job is to prove the production-facing foundation before the current compositor's shader/runtime/control features are ported over.
Before adding features here, read the guardrails in Render Cadence Golden Rules.
Architecture
RenderThread
owns a hidden OpenGL context
polls latest input frames without waiting
uploads input frames into a render-owned GL texture
renders simple BGRA8 motion at selected cadence
queues async PBO readback
publishes completed frames into SystemFrameExchange
InputFrameMailbox
owns latest disposable CPU input slots
drops older unsampled input frames when newer frames arrive
protects the one frame currently being uploaded by render
SystemFrameExchange
owns Free / Rendering / Completed / Scheduled slots
drops old completed unscheduled frames when render needs space
protects scheduled frames until DeckLink completion
DeckLinkOutputThread
consumes completed system-memory frames
schedules them into DeckLink up to target depth
never renders
Startup warms up real rendered frames before DeckLink scheduled playback starts.
Current Scope
Included now:
- output-only DeckLink
- non-blocking startup when DeckLink output is unavailable
- hidden render-thread-owned OpenGL context
- simple smooth-motion renderer
- BGRA8-only output
- synthetic BGRA8 input producer
- non-blocking latest-frame input mailbox
- render-thread-owned input texture upload
- async PBO readback
- latest-N system-memory frame exchange
- rendered-frame warmup
- background Slang compile of
shaders/happy-accident - app-owned display/render layer model for shader build readiness
- app-owned submission of a completed shader artifact
- render-thread-owned runtime render scene for ready shader layers
- shared-context GL prepare worker for runtime shader program compile/link
- render-thread-only GL program swap once a prepared program is ready
- manifest-driven stateless single-pass shader packages
- manifest-driven stateless named-pass shader packages
- atomic render-plan swap after every pass program is prepared
- HTTP shader list populated from supported stateless full-frame shader packages
- default float, vec2, color, boolean, enum, and trigger parameters
- small JSON writer for future HTTP/WebSocket payloads
- JSON serialization for cadence telemetry snapshots
- background logging with
log,warning, anderrorlevels - local HTTP control server matching the OpenAPI route surface
- HTTP layer controls for add, remove, reorder, bypass, shader change, parameter update, and parameter reset
- trigger parameters as latest-pulse controls with shader-visible count/time
- startup config provider for
config/runtime-host.json - quiet telemetry health monitor
- non-GL frame-exchange tests
- non-GL input-mailbox tests
Intentionally not included yet:
- real DeckLink input capture
- input format conversion
- temporal/history/feedback shader storage
- texture/LUT asset upload
- text-parameter rasterization
- runtime state
- OSC control
- persistent control/state writes
- trigger event history for stacked repeated pulses
- preview
- screenshots
- persistence
Those features should be ported only after the cadence spine is stable.
V1 Feature Parity Checklist
This tracks parity with apps/LoopThroughWithOpenGLCompositing.
- Stable DeckLink output cadence
- BGRA8 system-memory output path
- Render thread owns its primary GL context
- Output startup warmup before scheduled playback
- Non-blocking startup when DeckLink output is unavailable
- Runtime shader package discovery
- Background Slang shader compile
- Shared-context GL shader/program preparation
- Render-thread program swap at a frame boundary
- Stateless single-pass shader rendering
- Stateless named-pass shader rendering
- Atomic multipass render-plan commit
- Shader add/remove control path
- Previous-layer texture handoff for stacked shaders
- Supported shader list in HTTP/UI state
- Local HTTP server
- WebSocket state updates for the UI
- OpenAPI document serving
- Static control UI serving
- Startup config loading from
config/runtime-host.json - Cadence telemetry JSON
- Health logging for schedule/drop/starvation events
- Runtime parameter updates from HTTP controls
- Layer reorder/bypass/set-shader/update-parameter/reset-parameter HTTP controls
- Trigger parameter pulse count/time for latest trigger events
- Synthetic BGRA8 frame input producer
- Latest-frame CPU input mailbox
- Render-owned input texture upload
- Runtime shaders receive input through
gVideoInput - DeckLink input capture
- Live DeckLink input bound to
gVideoInput - Input format conversion/scaling
- Temporal history buffers
- Feedback buffers
- Texture asset loading and upload
- LUT asset loading and upload
- Text parameter rasterization
- Trigger history/event buffers for overlapping repeated trigger effects
- Full runtime state store/read model
- Persistent layer stack/config writes
- OSC ingress
- Preview output
- Screenshot capture
- External keying support
- Full V1 health/runtime presentation model
Build
cmake --build --preset build-debug --target RenderCadenceCompositor -- /m:1
The executable is:
build\vs2022-x64-debug\Debug\RenderCadenceCompositor.exe
Run
Run from VS Code with:
Debug RenderCadenceCompositor
Or from a terminal:
build\vs2022-x64-debug\Debug\RenderCadenceCompositor.exe
Press Enter to stop.
To test a different compatible shader package:
build\vs2022-x64-debug\Debug\RenderCadenceCompositor.exe --shader solid-color
Use --no-shader to keep the simple motion fallback only.
Startup Config
On startup the app loads config/runtime-host.json through AppConfigProvider, then applies explicit CLI overrides.
Currently consumed fields:
serverPortshaderLibraryoscBindAddressoscPortoscSmoothinginputVideoFormatinputFrameRateoutputVideoFormatoutputFrameRateautoReloadmaxTemporalHistoryFramespreviewFpsenableExternalKeying
The loaded config is treated as a read-only startup snapshot. Subsystems that need config should receive this snapshot or a narrowed config struct from app orchestration; they should not reload files independently.
Supported CLI overrides:
--shader <shader-id>--no-shader--port <port>
Expected Telemetry
Startup, shutdown, shader-build, and render-thread event messages are written through the app logger. Telemetry is intentionally separate and remains a compact once-per-second cadence line.
The logger writes to the console, OutputDebugStringA, and logs/render-cadence-compositor.log by default. Render-thread log calls use the non-blocking path so diagnostics do not become cadence blockers.
HTTP Control Server
The app starts a local HTTP control server on http://127.0.0.1:8080 by default, searching nearby ports if that one is busy.
Current endpoints:
GET /and UI asset paths: serve the bundled control UI fromui/distGET /api/state: returns OpenAPI-shaped display data with cadence telemetry, supported shaders, output status, and a read-only current runtime layerGET /ws: upgrades to a WebSocket and streams state snapshots when they changeGET /docs/openapi.yamlandGET /openapi.yaml: serves the OpenAPI documentGET /docs: serves Swagger UIPOST /api/layers/add,/remove,/reorder,/set-bypass,/set-shader,/update-parameter, and/reset-parametersuse the shared runtime control-command path- other OpenAPI POST routes are present but return
{ "ok": false, "error": "Endpoint is not implemented in RenderCadenceCompositor yet." }
The HTTP server runs on its own thread. It serves static UI/docs files, samples/copies telemetry through callbacks, and translates POST bodies into runtime control commands. Command execution is app-owned, so future OSC ingress can create the same commands without depending on HTTP route code. Control commands may update the display layer model, start background shader builds, or publish an already-built render-layer snapshot, but they do not call render work or DeckLink scheduling directly.
Optional DeckLink Output
DeckLink output is an optional edge service in this app.
Startup order is:
- start render thread
- warm up rendered system-memory frames
- try to attach DeckLink output
- start telemetry and HTTP either way
If DeckLink discovery or output setup fails, the app logs a warning and continues running without starting the output scheduler or scheduled playback. This keeps render cadence, runtime shader testing, HTTP state, and logging available on machines without DeckLink hardware or drivers.
/api/state reports the output status in videoIO.statusMessage.
The app samples telemetry once per second.
Normal cadence samples are available through GET /api/state and are not printed to the console. The telemetry monitor only logs health events:
- warning when DeckLink late/dropped-frame counters increase
- warning when schedule failures increase
- error when the app/DeckLink output buffer is starved
Healthy first-run signs:
- visible DeckLink output is smooth
renderFpsis close to the selected cadencescheduleFpsis close to the selected cadence after warmupscheduledstays near 4decklinkBufferedstays near 4 when availablelateanddroppeddo not increase continuouslyscheduleFailuresdoes not increaseshaderCommittedbecomes1after the background Happy Accident compile completesshaderFailuresremains0
completedPollMisses means the DeckLink scheduling thread woke up before a completed frame was available. It is not a DeckLink playout underrun by itself. Treat it as healthy polling noise when scheduled, decklinkBuffered, late, dropped, and scheduleFailures remain stable.
Runtime Slang Shader Test
On startup the app begins compiling the selected shader package on a background thread owned by the app orchestration layer. The default is shaders/happy-accident.
The render thread keeps drawing the simple motion renderer while Slang compiles. It does not choose packages, launch Slang, or track build lifecycle. Once a completed shader artifact is published, the render-thread-owned runtime scene queues changed layers to a shared-context GL prepare worker. That worker compiles/links runtime shader programs off the cadence thread. The render thread only swaps in an already-prepared GL program at a frame boundary. If either the Slang build or GL preparation fails, the app keeps rendering the current renderer or simple motion fallback.
Current runtime shader support is deliberately limited to stateless full-frame packages:
- one or more named passes
- one sampled source input per pass
- named intermediate outputs routed by the pass manifest
- final visible output must be named
layerOutput - no temporal history
- no feedback storage
- no texture/LUT assets yet
- no text parameters yet
- manifest defaults initialize parameters
- HTTP controls can update runtime parameter values without rebuilding GL programs when the shader program is unchanged
- trigger parameters are treated as latest-pulse controls: each press increments the trigger count and records the current runtime time
- repeated trigger history is not stored yet, so effects such as
trigger-ripplerestart from the latest trigger rather than accumulating overlapping ripples - the first layer receives a small fallback source texture until DeckLink input is added
- the first layer receives the latest synthetic input texture through both
gVideoInputandgLayerInputwhen input frames are available - stacked layers receive the original input through
gVideoInputand the previous ready layer output throughgLayerInput
The /api/state shader list uses the same support rules as runtime shader compilation and reports only packages this app can run today. Unsupported manifest feature sets such as temporal, feedback, texture-backed, font-backed, or text-parameter shaders are hidden from the control UI for now.
Runtime shaders are exposed through RuntimeLayerModel as display layers with manifest parameter definitions, current parameter values, build status, and render-ready artifacts. POST controls mutate this app-owned model and may start background shader builds when the selected shader changes.
When a layer becomes render-ready, the app publishes the ready render-layer snapshot to the render thread. The render thread owns the GL-side RuntimeRenderScene, diffs that snapshot at a frame boundary, queues new or changed pass programs to the shared-context prepare worker, swaps in a full prepared render plan only after every pass is ready, removes obsolete GL programs, and renders ready layers in order. Stacked stateless full-frame shaders render through internal ping-pong targets so each layer can sample the previous layer through gLayerInput; multipass shaders route named intermediate outputs through their manifest-declared pass inputs, and the final ready layer renders to the output target.
Successful handoff signs:
- telemetry shows
shaderCommitted=1 - output changes from the simple motion pattern to the Happy Accident shader
- render/schedule cadence remains near 60 fps during and after the handoff
- DeckLink buffer remains stable
Baseline Result
Date: 2026-05-12
User-visible result:
- output was smooth
- DeckLink held a 4-frame buffer
Representative telemetry:
renderFps=59.9 scheduleFps=59.9 free=8 completed=0 scheduled=4 completedPollMisses=30 scheduleFailures=0 completions=720 late=0 dropped=0 decklinkBuffered=4 scheduleCallMs=1.2
renderFps=59.8 scheduleFps=59.8 free=7 completed=1 scheduled=4 completedPollMisses=36 scheduleFailures=0 completions=1080 late=0 dropped=0 decklinkBuffered=4 scheduleCallMs=4.7
renderFps=59.9 scheduleFps=59.9 free=7 completed=1 scheduled=4 completedPollMisses=86 scheduleFailures=0 completions=1381 late=0 dropped=0 decklinkBuffered=4 scheduleCallMs=2.1
Read:
- render cadence and DeckLink schedule cadence both held roughly 60 fps
- app scheduled depth stayed at 4
- actual DeckLink buffered depth stayed at 4
- no late frames, dropped frames, or schedule failures were observed
- completed poll misses were benign because playout remained fully fed
Tests
cmake --build --preset build-debug --target RenderCadenceCompositorFrameExchangeTests -- /m:1
ctest --test-dir build\vs2022-x64-debug -C Debug -R RenderCadenceCompositorFrameExchangeTests --output-on-failure
Relationship To The Probe
apps/DeckLinkRenderCadenceProbe proved the timing model in one compact file.
This app keeps the same core behavior but splits it into modules that can grow:
frames/: system-memory handoffplatform/: COM/Win32/hidden GL context supportrender/: cadence thread, clock, and simple rendererframes/InputFrameMailbox: non-blocking latest-frame CPU input handoffvideo/SyntheticInputProducer: temporary BGRA8 test-pattern producer for proving the frame-input pathrender/InputFrameTexture: render-thread-owned upload of the latest CPU input frame into GLrender/readback/: PBO-backed BGRA8 readback and completed-frame publicationrender/runtime/RuntimeRenderScene: render-thread-owned GL scene for ready runtime shader layersrender/runtime/RuntimeShaderPrepareWorker: shared-context runtime shader program compile/link workerruntime/: app-owned shader layer readiness model, runtime Slang build bridge, and completed artifact handoffcontrol/: control action results and runtime-state JSON presentationcontrol/http/: local HTTP API, static UI serving, OpenAPI serving, and WebSocket updatesjson/: compact JSON serialization helpersvideo/: DeckLink output wrapper and scheduling threadtelemetry/: cadence telemetrytelemetry/TelemetryHealthMonitor: quiet health event logging from telemetry samplesapp/: startup/shutdown orchestrationapp/AppConfigProvider: startup config loading and CLI overrides
Next Porting Steps
Only after this app matches the probe's smooth output:
- replace
SimpleMotionRendererwith a render-scene interface - port shader package rendering
- port runtime snapshots/live state
- add control services
- add preview/screenshot from system-memory frames
- replace synthetic input with DeckLink input capture into the existing CPU latest-frame mailbox