Frame timing
All checks were successful
CI / React UI Build (push) Successful in 10s
CI / Native Windows Build And Tests (push) Successful in 2m53s
CI / Windows Release Package (push) Successful in 3m6s

This commit is contained in:
Aiden
2026-05-12 01:08:32 +10:00
parent ac729dc2b9
commit f1f4e3421b
6 changed files with 141 additions and 39 deletions

View File

@@ -2,7 +2,15 @@
## Status
Proposed.
In progress.
Implemented so far:
- real DeckLink buffered-frame telemetry is exposed separately from synthetic scheduler lead
- pure `RenderCadenceController` exists with non-GL tests
- `SystemOutputFramePool` now exposes the Phase 7.7 state vocabulary: `Free`, `Rendering`, `Completed`, `Scheduled`
- the output producer now uses `RenderCadenceController` to render one output frame per cadence tick
- DeckLink scheduling remains a separate top-up pass capped by the configured preroll target
Phase 7.5 and 7.6 proved useful pieces individually:
@@ -38,6 +46,16 @@ DeckLink playout scheduler
The system-memory frame buffer becomes the contract between render timing and device timing.
Core principle:
- The render cadence should be stable and boring.
- If the selected output mode is 59.94 fps, the render producer should attempt to render at 59.94 fps.
- It should not speed up just because the DeckLink buffer is empty.
- It should not slow down because DeckLink is full or because completed frames have not drained.
- Completed-but-unscheduled frames are a latest-N cache. Old completed frames may be dropped/recycled to keep rendering at cadence.
- Scheduled frames are protected until DeckLink completes them.
- The only normal reason for the render cadence to deviate is that rendering/GPU work itself overruns the frame budget.
## Non-Goals
- Do not hide failure by repeating frames as the primary strategy.
@@ -64,6 +82,14 @@ That means the system can be full and still look wrong, because "full" is not ti
### Target Shape
```text
Startup / warmup
render cadence starts first
render thread produces warmup frames at the selected cadence
completed system-memory queue reaches warmup target
DeckLink preroll is scheduled from completed frames
DeckLink playback starts with a filled buffer
Steady state
RenderCadenceController
owns output frame tick: frame 0, 1, 2...
owns render target time
@@ -73,7 +99,8 @@ RenderCadenceController
PlayoutFrameStore
owns free / rendering / completed / scheduled slots
tracks frame number, render time, completion time, and schedule state
exposes completed frames to DeckLink scheduler
exposes latest completed frames to DeckLink scheduler
may drop/recycle oldest unscheduled completed frames when render cadence needs space
DeckLinkPlayoutScheduler
owns DeckLink schedule time
@@ -111,14 +138,28 @@ Rules:
- If the render thread is early, it waits/yields.
- If it is slightly late, it renders the next frame immediately and records lateness.
- If it is badly late, policy may skip render ticks before rendering the newest frame.
- Skipping render ticks is a render-cadence decision, not a DeckLink stream-time jump.
- If it is badly late because render/GPU work overran the frame budget, policy may skip render ticks before rendering the newest frame.
- Skipping render ticks is an overrun policy, not a buffer-fill strategy.
- DeckLink schedule time should remain continuous unless a deliberate device recovery policy says otherwise.
Non-rule:
- The render producer must not render faster than the selected cadence to refill DeckLink.
- DeckLink should start only after warmup/preroll has filled enough completed frames.
- If the DeckLink buffer drains in steady state, that is a real timing failure to measure, not a signal for the render thread to sprint.
## Buffer Model
Use a fixed system-memory slot pool.
The completed portion of the pool is not a strict consume-before-render queue. It is a latest-N rendered-frame cache:
- render cadence writes one frame per selected output tick
- if completed-but-unscheduled frames are full, the oldest completed frame is disposable
- DeckLink scheduling consumes from the completed cache when it needs frames
- frames already scheduled to DeckLink are never recycled until completion
- if all slots are scheduled/in flight, cadence may miss because there is genuinely no safe system-memory target
Suggested starting values:
- completed-frame target: 2-4 frames
@@ -266,14 +307,14 @@ Before more scheduling changes, measure the real device buffer.
Deliverables:
- call DeckLink `GetBufferedVideoFrameCount()` after schedule/completion where available
- expose `actualDeckLinkBufferedFrames`
- keep `scheduledLeadFrames` but label it synthetic/internal
- record schedule-call duration and failures
- [x] call DeckLink `GetBufferedVideoFrameCount()` after schedule/completion where available
- [x] expose `actualDeckLinkBufferedFrames`
- [x] keep `scheduledLeadFrames` but label it synthetic/internal
- [x] record schedule-call duration and failures
Exit criteria:
- runtime telemetry distinguishes app completed queue, system scheduled slots, synthetic lead, and actual DeckLink buffer depth
- [x] runtime telemetry distinguishes app completed queue, system scheduled slots, synthetic lead, and actual DeckLink buffer depth
### Step 2: Rename Existing Queues To Match Their Roles
@@ -295,17 +336,17 @@ Add a pure timing helper first.
Responsibilities:
- compute next render tick
- track frame duration
- report early/late/drift
- decide whether to render, wait, or skip render ticks
- [x] compute next render tick
- [x] track frame duration
- [x] report early/late/drift
- [x] decide whether to render, wait, or skip render ticks
Tests:
- exact cadence advances
- late ticks are measured
- large lateness can skip according to policy
- no dependency on GL or DeckLink
- [x] exact cadence advances
- [x] late ticks are measured
- [x] large lateness can skip according to policy
- [x] no dependency on GL or DeckLink
### Step 4: Move Output Production To Cadence Ticks
@@ -313,15 +354,36 @@ Replace queue-pressure-only production with cadence-driven production.
Initial behavior:
- render at selected output cadence
- produce into system-memory slots
- publish completed frames
- pause when completed queue is at max depth
- [x] render at selected output cadence
- [x] produce into system-memory slots
- [x] publish completed frames
- [x] recycle/drop oldest unscheduled completed frames when cadence needs a slot
- [ ] only wait when every safe slot is scheduled/in flight
Exit criteria:
- output rendering continues without DeckLink completions
- output rendering does not schedule DeckLink directly
- completed-frame buffering behaves as latest-N, not consume-before-render
### Step 4a: Add Warmup Before DeckLink Playback
DeckLink output should not start consuming before the render cadence has prepared an initial cushion.
Initial behavior:
- configure DeckLink output without starting scheduled playback
- start the render cadence producer
- render warmup frames at the selected cadence, not faster
- wait until completed-frame depth reaches `targetWarmupFrames`
- schedule those completed frames as DeckLink preroll
- call `StartScheduledPlayback()`
Exit criteria:
- startup does not require the render producer to catch up by rendering faster than cadence
- DeckLink begins playback with a real completed-frame buffer
- if warmup cannot fill within a bounded timeout, startup enters degraded state with telemetry
### Step 5: Make DeckLink Scheduler A Separate Top-Up Loop