fix(rec): recordings played ~1.4x fast — CFR emission, drop -re

Root cause (measured, not guessed): the take-3 'rebase on overrun' reset
nextTick to wall-now, erasing every missed slot. Pump delivered 215-219/300
per 5s (~43fps) but rawvideo carries no timestamps — ffmpeg muxes by frame
count at -framerate 60, so every recording played fast with honest-looking
stats. A second pacer, ffmpeg -re, throttled the demux separately
('Resumed reading ... after a lag' 0.79s->4.82s).

Fix follows libobs video-io.c (https://github.com/obsproject/obs-studio)
- the video thread never resets its deadline; one frame per interval slot,
a late render repeats content (judder), never skips time:
1. FramePump: count-based emission, while(now>=nextTick){Submit; nextTick+=I}
2. FfmpegArgs: -re removed - the pump is the pacer

Muxed duration is now frame-count/fps == wall time by construction. Covers
game background + webcam (one shared pump). 0 warnings; suite 288/289 -
the one failure (Composite_FullScene_MasterPixels 1380,700) also fails with
this change stashed: pre-existing, untouched, recorded as follow-up.
This commit is contained in:
2026-09-10 08:35:30 -07:00
parent d212d5ae2c
commit bd396e488c
5 changed files with 117 additions and 62 deletions
+13 -3
View File
@@ -105,9 +105,19 @@ Both halves were solved by OBS/libyuv long ago; do not re-derive:
frame makes the period `render + submit + interval` — the producer can hit ≤ half
the declared rate even with a free render. OBS's `video_thread`
(`libobs/media-io/video-io.c`) advances an absolute `nextTick += intervalTicks` and
sleeps only the remainder; if the deadline blew, skip the wait AND the missed ticks
(rebase, no catch-up burst — a burst queues stale frames). Critical with rawvideo:
pts is stamped by ARRIVAL, so a starved producer silently time-lapses the file.
sleeps only the remainder. **NEVER "rebase" the deadline to wall-now when it blows** —
the take-3 note I originally wrote here ("skip … the missed ticks (rebase)") was the
PROVEN-wrong advice: resetting `nextTick` erases the missed slots, so a 43fps reality
was authored into a 60fps container and every recording played ~1.4x fast (take 12/13,
2026-09-10). The rebase even kept the stats looking honest (render+wait == period, no
loss) because a rebased frame is never "late". OBS keeps the counter MOVING and outputs
ONE frame per interval tick — a late render shows as REPEATED footage (judder; duration
stays correct), never a skip and never a wall-now reset. Critical with rawvideo:
pts is stamped by ARRIVAL, so the muxed duration is pure frame count ÷ fps — the only
way to make duration == wall time under ANY load is exactly one frame per interval slot.
Corollary: **never add a second pacer.** `ffmpeg -re` on the rawvideo input throttles
the pipe independently and fights the pump (its "Resumed reading … after a lag" grew
0.79s→4.82s in the same take). Removed; the pump IS the pacer.
2. **A 1080p frame is ~2.07M pixels — the hot path must be row-simple.** Per-pixel
`Math.Round` + float source-over in managed code costs ~100ns/px = the whole 258ms.
libyuv's pattern (https://chromium.googlesource.com/libyuv/libyuv/): branch per