4.6 KiB
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 commit from an already-prepared artifact
- 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
If future GL preparation needs to happen 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
It must not:
- render fallback frames
- invoke GL
- compile shaders
- block the render cadence waiting for DeckLink
If no completed frame is available, record the miss and keep the ownership boundary intact.
4. Runtime Build Work Produces Artifacts
Runtime shader work is split into two phases:
- CPU/build phase outside the render thread
- GL commit phase 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 a completed artifact and either commits it at a frame boundary or rejects it. A failed artifact must not disturb the current renderer.
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 the latest rendered frames and protects frames scheduled to DeckLink.
It may drop old completed, unscheduled frames when the render thread needs a free slot. It must never force the render thread to wait for the output side to consume a 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:
renderFpsscheduleFpscompletedPollMissesscheduleFailuresdecklinkBufferedshaderCommittedshaderFailures
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
- parameter packing
- system-memory frame exchange
- DeckLink output scheduling
- telemetry
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.