Files
video-shader-toys/src/README.md
Aiden 56883439a6
All checks were successful
CI / React UI Build (push) Successful in 12s
CI / Native Windows Build And Tests (push) Successful in 2m14s
CI / Windows Release Package (push) Has been skipped
clean split
2026-05-30 21:22:03 +10:00

31 KiB

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 the oldest ready input frame without waiting
  uploads input frames into a render-owned GL texture
  renders at selected cadence
  optionally GPU-packs the output target into UYVY8
  queues async PBO readback for the selected system-frame format
  publishes completed frames into SystemFrameExchange

InputFrameMailbox
  owns bounded FIFO CPU input slots
  keeps a bounded three-ready-frame input buffer for render
  trims frames beyond that bound to avoid runaway input latency
  protects the one frame currently being uploaded by render
  uses a single contiguous copy when capture row stride matches mailbox row stride

SystemFrameExchange
  owns Free / Rendering / Completed / Scheduled slots
  preserves completed output frames as a bounded FIFO reserve once they are waiting for playout
  protects scheduled frames until DeckLink completion

VideoOutputThread
  consumes completed system-memory frames
  schedules them into the active video output edge up to target depth
  never renders

PreviewWindowThread
  optionally owns a Win32/GDI preview window
  copies the latest completed or scheduled system-memory frame without consuming it
  skips preview ticks instead of waiting for the frame exchange lock
  never calls GL, DeckLink, shader build, or render cadence code

Startup builds a small output preroll reserve before DeckLink scheduled playback starts. When DeckLink input is available, startup also waits briefly for three ready input frames before the render thread starts so the first render ticks are deliberate rather than lucky.

Current Scope

Included now:

  • generic video output edge contract with DeckLink and NDI output implementations
  • optional DeckLink input edge with BGRA8 capture or raw UYVY8 capture decoded on the render thread
  • non-blocking startup when DeckLink output is unavailable
  • hidden render-thread-owned OpenGL context
  • simple smooth-motion renderer
  • BGRA8 and render-thread-packed UYVY8 system-memory output
  • non-blocking three-frame FIFO input mailbox for render
  • fast contiguous mailbox copy path for matching input row strides
  • bounded three-frame input warmup before render cadence starts
  • render-thread-owned input texture upload
  • async PBO readback for the configured system-frame format
  • bounded FIFO system-memory frame exchange
  • bounded completed-frame output preroll reserve before DeckLink playback, with DeckLink scheduled depth still targeted at four
  • conservative DeckLink schedule-lead telemetry and recovery
  • optional background Slang compile of configured or restored runtime shader layers
  • app-owned display/render layer model for shader build readiness
  • app-owned submission of a completed shader artifact
  • render-thread-owned render-content interface with runtime shader content as the default implementation
  • app-owned runtime-content controller interface with the Slang shader stack as the default implementation
  • render-thread-owned runtime render scene for ready shader layers inside the default render-content adapter
  • 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, and error levels
  • 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 restore from runtime/runtime_state.json
  • debounced background autosave for durable layer-stack UI/API changes
  • startup config provider for config/runtime-host.json
  • quiet telemetry health monitor
  • optional preview window fed from completed system-memory frames on its own thread
  • non-GL frame-exchange tests
  • non-GL input-mailbox tests

Intentionally not included yet:

  • additional input format conversion/scaling
  • temporal/history/feedback shader storage
  • texture/LUT asset upload
  • manual stack preset save/load
  • OSC control
  • trigger event history for stacked repeated pulses
  • screenshots

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 and UYVY8 system-memory output paths
  • 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
  • Optional DeckLink input capture
  • UYVY8 input capture with render-thread GPU decode to shader input texture
  • Three-frame FIFO CPU input mailbox for render
  • Fast contiguous input mailbox copy when source/destination stride matches
  • Bounded three-frame input warmup before render cadence starts
  • Render-owned input texture upload
  • Runtime shaders receive input through gVideoInput
  • 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
  • CPU-side MSDF/MTSDF font atlas generation cache
  • Single-line text parameter rasterization and GL binding
  • Trigger history/event buffers for overlapping repeated trigger effects
  • Startup restore from latest runtime layer state
  • Debounced background autosave for durable layer-stack changes
  • Manual stack preset save/load
  • Persistent config writes
  • OSC ingress
  • Preview output from a non-consuming system-memory tap
  • 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:

  • serverPort
  • shaderLibrary
  • oscBindAddress (reported through the OSC service stub; UDP ingress is not wired yet)
  • oscPort (use 0 to mark the OSC stub disabled; UDP ingress is not wired yet)
  • oscSmoothing (reported through the OSC service stub; smoothing is reserved for future OSC ingress)
  • input.backend
  • input.device
  • input.resolution
  • input.frameRate
  • output.backend
  • output.device
  • output.resolution
  • output.frameRate
  • output.pixelFormat (auto, bgra8, or uyvy8)
  • output.keying.external (DeckLink external-keyer backing field)
  • output.keying.alphaRequired (general output alpha request)
  • autoReload
  • maxTemporalHistoryFrames
  • previewEnabled
  • previewFps

input.backend and output.backend currently support decklink, ndi, and none. Backend creation is routed through the app-side video backend factory, so new concrete backends can be added without making main or the render cadence path own their startup details.

output.pixelFormat=auto chooses UYVY8 for DeckLink/NDI output unless output alpha is enabled, in which case it uses BGRA8. Explicit uyvy8 requests are rejected back to BGRA8 when alpha is required. V210/YUVA remain explicit unsupported render-readback states until matching render-thread packers exist.

The control UI exposes keying as a single Output alpha control. It writes output.keying.alphaRequired=true for any alpha-capable output path and also writes output.keying.external=true when the selected output backend is DeckLink, because DeckLink external keying is the current hardware path that consumes that alpha. NDI ignores external keying and only uses the alpha requirement to select an alpha-carrying system-frame format.

The control UI includes a host-config editor backed by GET /api/config and POST /api/config/save. When input backend is NDI, the editor also calls GET /api/ndi/sources to populate the input device field from NDI discovery while preserving manual source-name entry. Saving rewrites config/runtime-host.json and marks the app as needing restart. Startup-owned services such as render dimensions, video backend selection, frame rate, output pixel format, HTTP port, and preview settings apply after POST /api/app/restart starts a fresh native host process.

When previewEnabled is true, the preview window runs on PreviewWindowThread. It paints BGRA8 system-memory frames directly and decodes UYVY8 frames to BGRA for Win32/GDI after render readback has already completed, so it does not bind GL and does not consume frames from video output. previewFps controls the preview repaint cadence; the default is 60 fps and config/runtime-host.json tracks the shipped 59.94 output cadence.

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.

autoReload is exposed in config and state for compatibility, but automatic file watching is not currently wired. Use POST /api/reload or the control UI reload button to rescan shader files and manifests.

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 from ui/dist
  • GET /api/state: returns OpenAPI-shaped display data with cadence telemetry, supported shaders, output status, and a read-only current runtime layer
  • GET /api/config: returns active and saved startup host config
  • GET /api/ndi/sources: returns currently discoverable NDI source names for host-config input selection
  • GET /ws: upgrades to a WebSocket and streams state snapshots when they change
  • GET /docs/openapi.yaml and GET /openapi.yaml: serves the OpenAPI document
  • GET /docs: serves Swagger UI
  • POST /api/layers/add, /remove, /reorder, /set-bypass, /set-shader, /update-parameter, and /reset-parameters use the shared runtime control-command path
  • POST /api/reload: rescans the shader library, refreshes manifests, and queues recompilation for every catalog-valid layer in the active stack
  • 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. control/http/HttpControlServer owns socket lifetime, HTTP parsing, static UI/docs helpers, and WebSocket transport. The Render Cadence endpoint map lives in app/RenderCadenceHttpRoutes, which samples/copies telemetry through callbacks and translates POST bodies into runtime control commands. A fork can keep the HTTP/WebSocket shell and install a different route callback without inheriting this app's /api/... surface. 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, request debounced runtime-state persistence, start background shader builds, or publish an already-built render-layer snapshot, but they do not call render work or DeckLink scheduling directly.

control/osc/OscControlServer is currently a lifecycle/status stub. It consumes the startup OSC config and reports whether OSC is configured or disabled, but it deliberately does not open a UDP socket or dispatch parameter changes yet.

Runtime Content Boundary

app/RenderCadenceApp owns startup, video output, preview, telemetry, OSC status, and HTTP server lifetime. It does not own the Slang shader stack directly. Runtime content is supplied through IRuntimeContentController in app/RuntimeContentController.h.

The default implementation is ShaderRuntimeContentController. It wraps RuntimeLayerController, exposes shader catalog/layer state to /api/state, handles the shader layer POST commands, and publishes render-ready shader layers to the current render thread. A fork that uses a D3D engine should replace this controller with a D3D engine controller and decide separately whether to keep the current GL render/readback thread as a bridge or replace it with a D3D-native frame publisher.

DeckLink output is an optional edge service in this app.

Startup order is:

  1. start render thread
  2. build a bounded completed-frame output preroll reserve at normal render cadence
  3. try to attach DeckLink output
  4. 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 backend-neutral output status in videoOutput. Portable fields live at videoOutput.enabled, videoOutput.backend, and videoOutput.scheduleFailures; backend-specific counters such as DeckLink buffered depth and schedule lead live under videoOutput.backendMetrics. The older videoIO and decklink status objects are retained for compatibility while clients migrate.

DeckLink input is an optional edge service in this app.

Startup order is:

  1. create InputFrameMailbox
  2. try to attach DeckLink input for the configured input mode
  3. prefer BGRA8 capture, otherwise accept raw UYVY8 capture and configure the mailbox for UYVY8 bytes
  4. start DeckLinkInputThread
  5. wait briefly for three ready input warmup frames before starting render cadence
  6. leave input absent if discovery, setup, format support, or stream startup fails

DeckLinkInput and DeckLinkInputThread are deliberately narrow. They capture BGRA8 frames directly or raw UYVY8 frames into InputFrameMailbox; they do not call GL, render, preview, screenshot, shader, or output scheduling code. UYVY8-to-RGBA decode happens later inside the render-thread-owned input texture upload path, so the DeckLink callback stays a capture/copy edge only. The render upload path consumes the oldest ready input frame from a bounded three-ready-frame queue, so the input behaves like a small jitter buffer instead of a latest-frame preview mailbox. The mailbox trims older frames beyond that bound to avoid runaway latency, uses one contiguous copy when the capture row stride matches the configured mailbox row stride, and falls back to row-by-row copy only for padded or mismatched frames. Unsupported input modes or formats outside BGRA8/UYVY8 are reported explicitly and treated as an unavailable edge rather than silently converted.

Input warmup is startup-only and bounded. It may delay render-thread startup for a short window, but it does not add waits to the steady-state render cadence loop.

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, including schedule lead and recovery count
  • warning when schedule failures increase
  • error when the app/DeckLink output buffer is starved

Render cadence telemetry:

  • clockOverruns: render cadence overruns where missed time was detected
  • clockSkippedFrames: selected-cadence frame intervals skipped instead of catch-up rendering
  • clockOveruns / clockSkipped: compatibility aliases for quick polling scripts

Input telemetry:

  • inputFramesReceived: frames accepted into InputFrameMailbox
  • inputFramesDropped: ready input frames dropped or missed because the mailbox was full
  • renderFrameMs: most recent render-thread draw duration, excluding completed-readback copy and readback queue work
  • renderFrameBudgetUsedPercent: most recent render draw time as a percentage of the selected frame budget
  • renderFrameMaxMs: maximum observed render-thread draw duration for this process
  • readbackQueueMs: time spent queueing the most recent async PBO readback for the selected system-frame format
  • completedReadbackCopyMs: time spent mapping/copying the most recent completed readback into system-memory frame storage
  • completedDrops: oldest completed unscheduled system-memory frames dropped because the bounded completed reserve overflowed; this is an app-side reserve drop, not a DeckLink dropped-frame report
  • acquireMisses: times render/readback could not acquire a writable system-memory frame slot; completed frames waiting for playout are preserved instead of being displaced
  • deckLinkScheduleLeadAvailable: whether DeckLink schedule-lead telemetry was available for the latest sample
  • deckLinkScheduleLeadFrames: estimated distance between the DeckLink playback cursor and the next scheduled stream time
  • deckLinkPlaybackFrameIndex: latest sampled DeckLink playback frame index
  • deckLinkNextScheduleFrameIndex: next stream frame index the app intends to schedule
  • deckLinkPlaybackStreamTime: latest sampled DeckLink playback stream time in DeckLink time units
  • deckLinkScheduleRealignments: conservative schedule-cursor recoveries triggered by late/drop pressure or dangerously low lead
  • inputConsumeMisses: render ticks where no ready input frame was available to upload
  • inputUploadMisses: input texture upload attempts that reused the previous GL input texture
  • inputReadyFrames: ready input frames currently queued in InputFrameMailbox
  • inputReadingFrames: input frames currently protected while render uploads them
  • inputLatestAgeMs: age of the newest submitted input frame
  • inputUploadMs: render-thread GL upload/decode submission time for the latest uploaded input frame
  • inputFormatSupported: whether the latest frame reaching the render upload path was BGRA8 or UYVY8 compatible
  • inputSignalPresent: whether any input frame has reached the mailbox
  • inputCaptureFps: DeckLink input callback capture rate
  • inputConvertMs: input-edge CPU conversion time; expected to remain 0 for BGRA8 and raw UYVY8 capture because UYVY8 decode is render-thread GPU work
  • inputSubmitMs: time spent copying/submitting the latest captured input frame to InputFrameMailbox
  • inputCaptureFormat: selected DeckLink input format (BGRA8, UYVY8, or none)
  • inputNoSignalFrames: DeckLink callbacks reporting no input source
  • inputUnsupportedFrames: input frames rejected before mailbox submission
  • inputSubmitMisses: input frames that could not be submitted to the mailbox

Runtime shaders continue rendering when input is missing. If no mailbox frame has been uploaded yet, shader samplers use the runtime fallback source texture; once DeckLink input is flowing, shaders such as CRT and trigger-ripple sample the current input through gVideoInput.

Healthy first-run signs:

  • visible DeckLink output is smooth
  • renderFps is close to the selected cadence
  • scheduleFps is close to the selected cadence after warmup
  • scheduled stays near 4
  • decklinkBuffered stays near 4 when available
  • deckLinkScheduleLeadFrames remains positive and stable when available
  • deckLinkScheduleRealignments does not increase continuously
  • late and dropped do not increase continuously
  • scheduleFailures does not increase
  • shaderCommitted increases after the restored/default runtime layer compiles and commits
  • shaderFailures remains 0

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 Stack

On startup the app first tries to restore runtime/runtime_state.json. Valid saved layers are rebuilt in saved order, including shader id, bypass state, and parameter values. Missing shader packages are skipped, invalid saved parameter values fall back to manifest defaults, and if the runtime-state file is missing or unusable the app falls back to the optional configured startup shader. The checked-in config leaves runtimeShaderId empty so a fresh host keeps the simple fallback renderer until layers are added or a saved stack exists.

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.

The Slang compiler lookup matches the CMake third-party layout: SLANG_ROOT first, then THIRD_PARTY_ROOT, then repo video-io-3rdParty/, then repo or packaged 3rdParty/. Packaged builds use 3rdParty/slang/bin/slangc.exe; development bundles can use slang-2026.8-windows-x86_64/bin/slangc.exe.

POST /api/reload rescans shaders/, re-reads manifests, refreshes supported shader metadata, reconciles active layer parameters against changed definitions, and queues recompilation for every catalog-valid layer in the active stack. It does not compile every package in the shader library; packages are compiled when they are part of the active stack.

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
  • text parameters are single-line ASCII masks backed by prepared MTSDF font atlases
  • 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-ripple restart from the latest trigger rather than accumulating overlapping ripples
  • the first layer receives a small fallback source texture until DeckLink input is available
  • the first layer receives the latest DeckLink input texture through both gVideoInput and gLayerInput when input frames are available
  • stacked layers receive the original input through gVideoInput and the previous ready layer output through gLayerInput

Shader source semantics:

  • gVideoInput means the latest decoded shader-visible video input for every layer.
  • gLayerInput means the previous layer output.
  • the first layer may receive gLayerInput = gVideoInput.
  • later layers receive gVideoInput = original input and gLayerInput = previous layer.
  • named intermediate pass inputs inside a multipass layer are still routed through the selected pass-source slot; layer stacking should use gLayerInput.

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, and texture-backed 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 or when /api/reload is requested. Durable UI/API layer changes request a debounced background write to runtime/runtime_state.json. OSC ingress is not wired yet; when it is added, OSC-driven changes should stay out of autosave unless an explicit persistence policy is introduced.

When a layer becomes render-ready, the app publishes the ready render-layer snapshot to the render thread. The render thread forwards those snapshots to RuntimeShaderRenderContent, the default IRenderContent implementation. That adapter owns the GL-side RuntimeRenderScene, diffs snapshots 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 restored or configured runtime shader stack
  • 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
  • DeckLink schedule lead remained positive during healthy playback
  • 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 handoff
  • platform/: COM/Win32/hidden GL context support
  • render/: cadence thread, clock, render-content boundary, and default runtime shader content
  • frames/InputFrameMailbox: non-blocking bounded FIFO CPU input handoff with contiguous-copy fast path for matching row strides
  • render/InputFrameTexture: render-thread-owned upload of the currently acquired CPU input frame into GL, including raw UYVY8 decode into the shader-visible input texture
  • render/readback/: PBO-backed BGRA8/UYVY8 readback and completed-frame publication
  • render/thread/: render thread lifecycle, GL startup/cadence loop, metrics, and runtime layer commit mailbox
  • render/RenderContent: narrow draw-call boundary used by the cadence/readback shell
  • render/RuntimeShaderRenderContent: default content adapter that wraps runtime shader rendering and fallback presentation
  • render/runtime/RuntimeRenderScene: render-thread-owned GL scene for ready runtime shader layers
  • render/runtime/RuntimeShaderPrepareWorker: shared-context runtime shader program compile/link worker
  • runtime/catalog/: supported shader catalog and package filtering
  • runtime/layers/: app-owned shader layer readiness model, restore/reload reconciliation, and render snapshots
  • runtime/shader/: runtime Slang build bridge and completed artifact handoff
  • runtime/state/: runtime JSON helpers, parameter normalization, and debounced runtime-state persistence
  • runtime/text/: font atlas build and prepared text texture composition
  • control/: control action results and runtime-state JSON presentation
  • control/http/: local HTTP transport, static UI/OpenAPI serving helpers, and WebSocket updates
  • control/osc/: OSC service lifecycle/status stub; no UDP listener or dispatch yet
  • app/RenderCadenceHttpRoutes: this app's /api/... endpoint map behind the reusable HTTP server route callback
  • json/: compact JSON serialization helpers
  • video/: DeckLink output wrapper and scheduling thread
  • telemetry/: cadence telemetry
  • telemetry/TelemetryHealthMonitor: quiet health event logging from telemetry samples
  • app/: startup/shutdown orchestration
  • app/AppConfigProvider: startup config loading and CLI overrides

Next Porting Steps

Only after this app matches the probe's smooth output:

  1. broaden runtime snapshots/live state toward OSC and presets
  2. add screenshot capture from system-memory frames
  3. add scaling and additional input format support after the BGRA8/raw-UYVY8 input edge is stable