# Render Cadence Golden Rules

These are the non-negotiable rules for the new render-cadence architecture.

They exist because the old app drifted into a place where DeckLink timing, render work, shader build work, state coordination, readback, and recovery behavior all influenced each other. The new app should stay boring, explicit, and easy to reason about.

## 1. The Render Thread Owns Its GL Context

Only the render thread may bind and use its primary OpenGL context.

Allowed on the render thread:

- GL resource creation and destruction for resources it owns
- GL shader/program swap from an already-prepared GL program
- drawing the next frame
- async readback queueing and completion polling
- publishing completed system-memory frames

Not allowed on the render thread:

- Slang compiler invocation
- manifest scanning/parsing
- filesystem discovery
- image/font/LUT decoding
- persistence
- network/API/OSC handling
- DeckLink scheduling
- blocking console logging
- config file discovery or parsing

If GL preparation happens off-thread, use an explicit shared-context GL prepare thread. Do not smuggle non-render work back into the cadence loop.

## 2. Render Cadence Does Not Chase Buffers

The render thread runs at the selected render cadence.

It must not speed up to fill a DeckLink/system-memory buffer, and it must not slow down because a consumer is late. If the GPU is genuinely overloaded, record that as render overrun telemetry.

Buffers absorb timing differences. They do not control render cadence.

## 3. Video I/O Never Renders

DeckLink output consumes already-rendered system-memory frames.

The output/scheduling side may:

- schedule completed frames
- release frames after DeckLink completion
- report late/dropped/schedule telemetry
- record app-side poll misses
- conservatively realign the DeckLink schedule cursor after measured timing pressure

It must not:

- render fallback frames
- invoke GL
- compile shaders
- block the render cadence waiting for DeckLink
- continuously rewrite healthy scheduled timestamps

If no completed frame is available, record the miss and keep the ownership boundary intact.

DeckLink input is also an edge, not a renderer. It may capture/copy the latest supported CPU frame into an input mailbox and update input telemetry, but it must not call GL, schedule output, compile shaders, or drive render cadence. Format decode that requires GL belongs to the render-owned input upload path.

## 4. Runtime Build Work Produces Artifacts

Runtime shader work is split into two phases:

1. CPU/build phase outside the render thread
2. shared-context GL preparation outside the render thread where practical
3. GL program swap on the render thread

The CPU/build phase may parse manifests, invoke Slang, validate package shape, and prepare CPU-side data.

The render thread receives completed render-layer artifacts, asks the shared-context prepare worker to compile/link changed GL programs, and only swaps in prepared programs at a frame boundary. A failed artifact or failed GL preparation must not disturb the current renderer.

The display/render layer model is app-owned. It may track requested shaders, build state, display metadata, and render-ready artifacts, but it must not perform GL work or drive render cadence directly.

## 5. No Hidden Blocking In The Cadence Path

The render loop must not do work with unbounded or OS-dependent latency.

Examples to avoid:

- file reads
- directory scans
- image decoding
- process launches
- waits on worker threads
- blocking locks around slow code
- synchronous GPU readback waits
- console I/O

Short mutex use for exchanging small already-prepared objects is acceptable. Holding a lock while doing heavy work is not.

## 6. System Memory Frames Are A Handoff, Not A Render Driver

The system-memory frame exchange stores completed frames as a bounded FIFO reserve and protects frames scheduled to DeckLink.

Render acquire must not evict completed frames that are waiting for playout, and it must never force the render thread to wait for the output side to consume a frame.

If the completed reserve overflows, the exchange may drop the oldest completed, unscheduled frame and record `completedDrops`. That is an app-side reserve drop, not a DeckLink dropped frame.

## 7. Startup Uses Warmup, Not Burst Rendering

DeckLink playback starts only after the render thread has produced enough real frames for preroll.

Warmup should happen at normal render cadence. Do not temporarily accelerate the renderer to fill buffers.

## 8. Telemetry Must Name Ownership Clearly

Counters should say which subsystem had the event.

Good examples:

- `renderFps`
- `scheduleFps`
- `completedPollMisses`
- `scheduleFailures`
- `decklinkBuffered`
- `deckLinkScheduleLeadFrames`
- `deckLinkScheduleRealignments`
- `inputCaptureFps`
- `inputSubmitMs`
- `inputUploadMs`
- `inputConvertMs`
- `shaderCommitted`
- `shaderFailures`

Avoid ambiguous names like `underrun` unless it is clear whether it means app-ready underrun, DeckLink buffered-frame underrun, render overrun, or schedule failure.

## 9. Keep Files Small And Role-Based

A file should have one clear reason to change.

Preferred boundaries:

- app orchestration
- render cadence/thread ownership
- GL rendering
- runtime artifact build/bridge
- app-owned display/render layer model
- parameter packing
- system-memory frame exchange
- DeckLink output scheduling
- telemetry
- local control/API edge
- config loading
- JSON presentation/serialization
- logging

If a file starts coordinating multiple subsystems and doing detailed work for each of them, split it before it becomes the new old app.

## 10. Prefer Explicit Unsupported States

If a feature needs storage, timing behavior, or ownership we have not designed yet, reject it clearly.

For example, in the current new app it is better to reject texture/LUT/text/temporal/feedback shaders than to quietly load files or allocate history state on the render thread.

Unsupported is healthy when it protects the architecture.
