fix: FramePump paces one frame per deadline slot — duplicate-on-lag, never skip (slice 15)
ty-1723/1726 device takes showed accelerated playback + audio tail cut-off:
a render overrun (~35ms vs the 16.6ms slot) SKIPPED the missed slots (slice 10's
freshness choice), so a 60fps-authoring pump wrote one frame per 35ms into a
60fps container — 1723: 697 frames/11.62s vs 11.84s audio; 1726: 163/2.72s vs
2.93s, video ending 0.21-0.24s early.
OBS never leaves a wall-time hole: the video thread emits one frame per tick and
a lagging producer DUPLICATES the newest frame ("lagged frames due to rendering
lag/stalls" — obs-output.c; "If the video frame queue is full, it will duplicate
the last frame" — docs.obsproject.com/backend-design). The pump's submit is now a
bounded catch-up over the missed slots (while now >= nextTick), fresh on the first,
repeated after — duration == wall, judder not fast-forward. Safe because Channel.
TryWrite never blocks (the take-9 smear was the blocking pipe-write; each emit is
nanoseconds). Burned frame index moved inside the loop: every emitted slot carries
its own +1 (also fixes the old unconditional pre-gate bump that gapped the judge
sequence on non-submitting fast-render iterations).
Good Dog test: Pump_Overrun_Renders_EmitsEverySlot_NotSkipped (60fps, 35ms render
cost, asserts >=0.65 of the wall slots emitted). 294/294 green, 0 warnings.
No push — web/A/V work is commit-local until greenlight.
This commit is contained in:
@@ -394,34 +394,38 @@ public sealed class FramePump : IDisposable
|
||||
lock (_gate) encoder = _encoder;
|
||||
if (encoder == null) break;
|
||||
|
||||
// Burned-in frame counter (slice 10 judge): a clock-independent
|
||||
// pacing witness burned literally into the composite. Decode the
|
||||
// recording and read the bottom-right strip: the number must advance
|
||||
// +1 per frame and jump only by counted drops (queue overflow or a
|
||||
// render-overrun's skipped slots). The WSL ticker replaced as the
|
||||
// judge because its own timers can smear under Windows host load —
|
||||
// this can't lie.
|
||||
_outputIndex++;
|
||||
BurnFrameIndex(frame.BgraPixels, frame.Width, frame.Height, _outputIndex);
|
||||
|
||||
// Deadline pacing (slice 10 reshape): ONE fresh composite per iteration, submitted
|
||||
// only when the clock has reached the next deadline. Two changes from
|
||||
// the take-9 counting loop, both derived from where the stale-content
|
||||
// bug actually lived:
|
||||
// 1. SubmitFrameAsync no longer blocks on the pipe — it ENQUEUES into
|
||||
// the encoder's bounded queue (the OBS video-thread model), so the
|
||||
// pump can never stall behind ffmpeg, and overflow DROPS the
|
||||
// newest frame. The queue wasn't here in the take-9 loop — a
|
||||
// lagging ffmpeg made the pump's submit block, and the burst
|
||||
// while-loop (below) then re-wrote the SAME stale composite for
|
||||
// every slot that ticked past, which is why the ticker smeared.
|
||||
// 2. One submission max per iteration: each catch-up slot now gets a
|
||||
// FRESH render instead of a repeat of the stale one. Missed slots
|
||||
// vanish from the file (a count-based gap, like the queue drop) —
|
||||
// never duplicated frozen frames.
|
||||
if (!ct.IsCancellationRequested
|
||||
&& System.Diagnostics.Stopwatch.GetTimestamp() >= nextTick)
|
||||
// Deadline pacing, OBS duplicate-on-lag (slice 15): emit ONE frame per
|
||||
// deadline slot — a fresh composite when the render kept up, a REPEAT
|
||||
// of this iteration's latest composite for every slot the render
|
||||
// overran. The OBS model never leaves a wall-time hole: libobs
|
||||
// media-io/video-io.c runs on its own clock and duplicates the latest
|
||||
// frame when the video thread lags, logging "lagged frames due to
|
||||
// rendering lag/stalls" — never skipped time (docs.obsproject.com/
|
||||
// backend-design: "If the video frame queue is full, it will duplicate
|
||||
// the last frame"). Slice 10 chose the opposite for freshness: an
|
||||
// overrun SKIPPED the missed slots, so a 60fps-authoring pump emitting
|
||||
// one frame per 35ms overrun authored ~1.7x playback (ty-1726: 163
|
||||
// video frames for 2.93s of audio) and ended the video 0.22s before the
|
||||
// audio tail. Counting duplicates instead of skips keeps recording
|
||||
// duration == wall duration (no acceleration, no audio tail cut) at the
|
||||
// cost of a short judder during a stall — the accepted trade. The burst
|
||||
// is microseconds (the enqueue never blocks, and the loop is clamped to
|
||||
// deadlineNow) so it can't smear the way the take-9 blocking burst did.
|
||||
var deadlineNow = System.Diagnostics.Stopwatch.GetTimestamp();
|
||||
while (!ct.IsCancellationRequested && deadlineNow >= nextTick)
|
||||
{
|
||||
// Burned-in frame counter (slice 10 judge): a clock-independent
|
||||
// pacing witness burned literally into the composite. Decode the
|
||||
// recording and read the bottom-right strip: the number must
|
||||
// advance +1 per authored frame and jump only by counted drops
|
||||
// (queue overflow). Burned here — on the buffer finally submitted,
|
||||
// which the enqueue's snapshot copies before the next iteration
|
||||
// rewrites it — so every file frame carries its own index and a
|
||||
// fast-render iteration that submitted nothing leaves no phantom
|
||||
// gap (the slice-10 unconditional bump before the gate could).
|
||||
_outputIndex++;
|
||||
BurnFrameIndex(frame.BgraPixels, frame.Width, frame.Height, _outputIndex);
|
||||
|
||||
submitSw.Restart();
|
||||
await encoder.SubmitFrameAsync(frame, ct);
|
||||
submitSw.Stop();
|
||||
|
||||
Reference in New Issue
Block a user