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:
@@ -996,6 +996,29 @@ Full suite 290/291 passing, the sole failure the pre-existing compositor pixel t
|
||||
- Full suite **293/293 green, 0 warnings**. Audio untouched. **NOT YET VERIFIED ON DEVICE** — the
|
||||
composition path needs a real 60fps-widget run (open item; see HANDOFF). Web work commits stay
|
||||
LOCAL (no push) until the user greenlights.
|
||||
- **Slice 15 — one frame per deadline SLOT: duplicate-on-lag, never skip (2026-09-14, device takes
|
||||
ty-1723/1726):** slice 10's freshness choice — a render overrun VANISHES the missed slots from the
|
||||
file — authored ACCELERATED playback: ~35ms render cost vs the 16.6ms slot, one fresh frame per
|
||||
35ms (log wall-1726: `FramePump stall: iteration 33ms (> 2× the 17ms interval): worst render
|
||||
33-46ms`), and a 60fps container muxed on arrival → 1723: 697 frames = 11.62s video vs 11.84s
|
||||
audio; 1726: 163 frames = 2.72s vs 2.93s. The video also ended 0.21–0.24s before the audio ("audio
|
||||
speeds up, cuts off at the end"). OBS's answer is duplicate-on-lag: `libobs/media-io/video-io.c`
|
||||
emits one frame per tick and a lagger DUPLICATES the newest frame, counted as "lagged frames due
|
||||
to rendering lag/stalls" (obs-output.c) — a wall-time hole never exists (docs.obsproject.com/
|
||||
backend-design: "If the video frame queue is full, it will duplicate the last frame"). The pump's
|
||||
submit is now a bounded catch-up: `while (now >= nextTick) { submit; nextTick += intervalTicks; }`
|
||||
over a `deadlineNow` captured once per iteration — the LATEST composite, fresh on the first missed
|
||||
slot, repeated (OBS's duplication) for the rest. Duration == wall under any render load, at the
|
||||
cost of a short judder during a stall. The burst is safe because `Channel.TryWrite`
|
||||
never blocks (slice 10's queue — the take-9 smear was the BLOCKING pipe-write re-copying a stale buffer
|
||||
during a long freeze; here each emit is nanoseconds). The burned `_outputIndex` (slice 10 judge)
|
||||
moved INSIDE the submit loop so every emitted slot carries its own +1 (and the old unconditional
|
||||
pre-gate bump no longer gaps the sequence on non-submitting fast-render iterations). **Good Dog
|
||||
test** `Pump_Overrun_Renders_EmitsEverySlot_NotSkipped`: 60fps, resolver sleeps 35ms, asserts ≥0.65
|
||||
of the wall slots are emitted (the skip-pump wrote ~1/35ms ≈ 200 in 7s; the slot pump ~415). Full
|
||||
suite **294/294 green, 0 warnings**. The +0.6s audio-late clap reading on 1726 was confounded by
|
||||
the 1.1x acceleration — re-measure on device; if a real residual remains it is the audio pipeline.
|
||||
No push (web/A/V work stays local).
|
||||
- **Stop ordering matters:** `StopAsync` stops the encoder — since slice 10 it FLUSHES the pending
|
||||
queue (`Channel.TryComplete` → drain writes the leftovers, closes stdin → EOF → ffmpeg finalizes+exits;
|
||||
an accepted frame is never lost) — **before** awaiting the loop. The old reverse-order deadlock was
|
||||
|
||||
Reference in New Issue
Block a user