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:
+27
-3
@@ -307,9 +307,9 @@ Both halves were solved by OBS/libyuv long ago; do not re-derive:
|
||||
OBS's answering machinery is the encoder queue (`libobs/obs-encoder.c`): the encoder thread
|
||||
NEVER couples back into the video thread; overflow = dropped data, NEVER a frozen producer.
|
||||
Fixed as: bounded `Channel<byte[]>` (cap 120) + a dedicated drain task owning stdin,
|
||||
`SubmitFrameAsync` = copy-to-pool-array + `TryWrite` (drop-newest + count when full), pump
|
||||
emits ONE fresh composite per iteration (no burst re-write), stop flushes the queue then EOF.
|
||||
**Rule: verify with a clock-independent judge.** The WSL ticker that "proved" slice 9 has its
|
||||
`SubmitFrameAsync` = copy-to-pool-array + `TryWrite` (drop-newest + count when full), stop
|
||||
flushes the queue then EOF. **Rule: verify with a clock-independent judge.** The WSL ticker that
|
||||
"proved" slice 9 has its
|
||||
own Host-timer jitter under Windows load — so this slice burns a dot-matrix `_outputIndex`
|
||||
into the bottom-right of every composite; decoding the recording reads the honest sequence
|
||||
(+1/frame, jumps = counted drops) with no external clock involved. Take 17 must read
|
||||
@@ -317,6 +317,30 @@ Both halves were solved by OBS/libyuv long ago; do not re-derive:
|
||||
UNRELIABLE here: the scene is always animating — session elapsed timer + REC pulse — so
|
||||
no two frames are ever byte-identical.)
|
||||
|
||||
**SUPERSEDED by slice 15 (2026-09-14):** the last clause "pump emits ONE fresh composite per
|
||||
iteration (no burst re-write)" had it BACKWARDS for the deadline. Slice 10 chose a skip when
|
||||
the render overruns — an overrun's slots vanish from the file — and that AUTHORS ACCELERATED
|
||||
playback (my item-1 lesson already said it: "NEVER a skip … a late render shows as REPEATED
|
||||
footage"). Device takes proved it: one fresh frame per 35ms stall → `ty-20260914-1726` = 163
|
||||
video frames (2.72s) against 2.93s of audio, video ending 0.22s early ("audio speeds up then
|
||||
cuts off"). OBS's answer (docs.obsproject.com/backend-design: "If the video frame queue is
|
||||
full, it will duplicate the last frame"; `libobs/obs-output.c` counts "lagged frames due to
|
||||
rendering lag/stalls" — never a time-hole) is to fill each missed slot by DUPLICATING the
|
||||
newest frame; duration == wall, judder not fast-forward. That's what the pump's submit now
|
||||
does: a catch-up loop over the missed slots emitting this iteration's composite again, clamped
|
||||
to a `deadlineNow` captured once (bounded — the smear that justified slice 10 was the
|
||||
BLOCKING pipe-write re-copying during a long freeze; the queue makes each emit nanoseconds,
|
||||
so the burst is safe). The burned index moved inside the submit loop: EVERY emitted slot
|
||||
carries its own +1 (this also fixed the old unconditional `_outputIndex++` that gapped the
|
||||
sequence on non-submitting fast-render iterations). A/V sync must be re-clap-measured after
|
||||
this fix — the +0.6s audio-late reading on 1726 was confounded by the 1.1x acceleration.
|
||||
|
||||
**Latest A/V sync numbers (2026-09-14, pre-slice-15 pacing):** keep the recipe below honest on
|
||||
clap measures. ty-1723: 697 video frames (60fps) = 11.62s vs 11.84s audio. ty-1726 (clap): 163
|
||||
frames = 2.72s vs 2.93s audio; clap audio env peak 1.655s vs video motion cluster 0.75–1.03s →
|
||||
single-peak offset ≈ +0.64–0.89s, cross-correlation lag +0.667s (audio late) — BOTH confounded by
|
||||
the acceleration; re-measure after slice 15.
|
||||
|
||||
|
||||
**Take-4 follow-ups (2026-09-04) — the symptom needed a second pass, so cite again:**
|
||||
render was still 58.9ms after slice 1. Slice 2 (buffer pool + opaque-row memcpy +
|
||||
|
||||
Reference in New Issue
Block a user