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:
@@ -395,6 +395,41 @@ public class FramePumpTests
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>The ONE integration test for the slice-15 pacing fix (2026-09-14):
|
||||
/// a render that overruns the deadline must NOT skip the missed slots — a skip
|
||||
/// authors fast playback: ty-1726 rendered one frame per 35ms overrun into a
|
||||
/// 60fps container (163 frames for 2.93s of audio) and cut the audio tail off.
|
||||
/// OBS fills an overrun's slots by DUPLICATING the newest frame (libobs
|
||||
/// media-io/video-io.c; docs.obsproject.com/backend-design: "If the video frame
|
||||
/// queue is full, it will duplicate the last frame") so recording duration stays
|
||||
/// equal to wall duration. Here a 60fps pump renders every frame at ~35ms cost
|
||||
/// (a genuine overrun): the slice-10 keep-fresh loop authored ~1 per 35ms; the
|
||||
/// slot-filling loop must emit a frame for ~every 16.6ms of wall time.</summary>
|
||||
[Fact]
|
||||
public async Task Pump_Overrun_Renders_EmitsEverySlot_NotSkipped()
|
||||
{
|
||||
const int fps = 60;
|
||||
var encoder = new FakeEncoder();
|
||||
using var pump = NewPump(encoder, resolve: _ =>
|
||||
{
|
||||
System.Threading.Thread.Sleep(35);
|
||||
return null;
|
||||
});
|
||||
|
||||
await pump.StartAsync();
|
||||
await Task.Delay(7000); // ~420 slots of wall time
|
||||
await pump.StopAsync();
|
||||
|
||||
// Slot-faithful: ~1 frame per 16.6ms -> ~415 in the window. Skipping (the
|
||||
// slice-10 behavior): ~1 per 35ms -> ~200. 0.65 * fps * (elapsed seconds)
|
||||
// is a middle line a skipped pump can never cross, a filling one only fails
|
||||
// on a pathological run.
|
||||
Assert.True(encoder.Frames.Count >= (int)(7.0 * fps * 0.65),
|
||||
$"pump emitted {encoder.Frames.Count} frames in ~7s wall at {fps}fps — "
|
||||
+ $"{7.0 / encoder.Frames.Count * fps:F1}x playback — an overrun must "
|
||||
+ "DUPLICATE the newest frame per missed slot (OBS), not skip it");
|
||||
}
|
||||
|
||||
/// <summary>The ONE integration test for the take-4 scratch pool: the pump recycles
|
||||
/// its master buffers across frames (the 8.3MB-per-tick LOH churn that cost GC
|
||||
/// stalls inside "render") while EVERY frame's content stays correct — stale bytes
|
||||
|
||||
Reference in New Issue
Block a user