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:
@@ -376,42 +376,50 @@ public sealed class FramePump : IDisposable
|
||||
IFfmpegEncoder? encoder;
|
||||
lock (_gate) encoder = _encoder;
|
||||
if (encoder == null) break;
|
||||
|
||||
// Count-based CFR emission (libobs video-io.c — the frame interval
|
||||
// is a DEADLINE and the output stream holds its declared rate):
|
||||
// one frame per interval slot, whatever the render cost. When the
|
||||
// renderer falls behind, the SAME fresh composite is written again
|
||||
// for every slot that ticked past, so a slow render expresses as
|
||||
// duplicated footage (judder) — never as a skipped timestamp. The
|
||||
// deadline counter is NEVER reset to wall-now: the old rebase
|
||||
// erased every missed slot, so a 43fps reality was authored into a
|
||||
// 60fps container and every recording played ~1.4x fast (rawvideo
|
||||
// carries no timestamps — muxed duration is pure frame count).
|
||||
submitSw.Restart();
|
||||
await encoder.SubmitFrameAsync(frame, ct);
|
||||
while (!ct.IsCancellationRequested
|
||||
&& System.Diagnostics.Stopwatch.GetTimestamp() >= nextTick)
|
||||
{
|
||||
await encoder.SubmitFrameAsync(frame, ct);
|
||||
statFrames++;
|
||||
nextTick += intervalTicks;
|
||||
}
|
||||
submitSw.Stop();
|
||||
submitTicks += submitSw.ElapsedTicks;
|
||||
// Submit copied the bytes — everything from this tick is recyclable.
|
||||
// Release AFTER submit, and the free-list Contains guard makes the
|
||||
// Cut path (BlendFrame returns toFrame itself, aliasing scratch) safe.
|
||||
|
||||
// SubmitFrameAsync copied the bytes on every write above — the
|
||||
// tick's buffers are recyclable once each due slot consumed them.
|
||||
// The free-list Contains guard keeps the Cut path (BlendFrame
|
||||
// returns toFrame itself, aliasing scratch) safe.
|
||||
ReleaseScratch(frame.BgraPixels);
|
||||
ReleaseScratch(scratch);
|
||||
statFrames++;
|
||||
ReportStats();
|
||||
|
||||
// Advance the deadline; cost already spent is not slept again.
|
||||
// Blew the frame budget: skip the wait AND the missed ticks —
|
||||
// rebase rather than bursting a catch-up pile (OBS rewinds its
|
||||
// tick; a burst only queues stale frames). Otherwise sleep the
|
||||
// BULK and SPIN the 2ms tail — never hand a sub-tick remainder
|
||||
// to the sleep quantum (see the timeBeginPeriod note).
|
||||
nextTick += intervalTicks;
|
||||
// Sleep the BULK of the remainder, SPIN the 2ms tail — never hand
|
||||
// a sub-tick remainder to the sleep quantum (the timeBeginPeriod
|
||||
// note). If the renderer already ate the budget there is nothing
|
||||
// left to sleep and the loop renders the next frame straight away.
|
||||
waitSw.Restart();
|
||||
var ahead = nextTick - System.Diagnostics.Stopwatch.GetTimestamp();
|
||||
if (ahead <= 0)
|
||||
{
|
||||
nextTick = System.Diagnostics.Stopwatch.GetTimestamp() + intervalTicks;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (ahead > SpinTailTicks)
|
||||
await _pacingDelay(TimeSpan.FromSeconds(
|
||||
(ahead - SpinTailTicks) / (double)System.Diagnostics.Stopwatch.Frequency), ct);
|
||||
while (!ct.IsCancellationRequested
|
||||
&& System.Diagnostics.Stopwatch.GetTimestamp() < nextTick)
|
||||
Thread.SpinWait(400);
|
||||
}
|
||||
if (ahead > SpinTailTicks)
|
||||
await _pacingDelay(TimeSpan.FromSeconds(
|
||||
(ahead - SpinTailTicks) / (double)System.Diagnostics.Stopwatch.Frequency), ct);
|
||||
while (!ct.IsCancellationRequested
|
||||
&& System.Diagnostics.Stopwatch.GetTimestamp() < nextTick)
|
||||
Thread.SpinWait(400);
|
||||
waitSw.Stop();
|
||||
waitTicks += waitSw.ElapsedTicks;
|
||||
ReportStats();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user