fix(pump+compositor): take-3 starvation — deadline pacing + row-blit fast paths
Two defects made the producer 17x slow (37s record -> 2.1s/127-frame file, rawvideo stamps by arrival): FramePump slept the FULL interval after each render (period = render+submit+interval) and SceneCompositor did per-pixel float sampling + Math.Round blends over all 2.07M master pixels, scanning the whole destination per overlay (258ms avg render vs 1.5ms submit). Both solutions are established, not invented — researched before coding per the derivative-work rule: - deadline pacing: OBS libobs/media-io/video-io.c video_thread (nextTick += intervalTicks, sleep only the remainder, rebase on overrun, never burst) - row blits: libyuv pattern (BSD-3, chromium.googlesource.com/libyuv/libyuv) — 1:1 aligned identity fast path, per-pixel alpha branch, integer fixed-point blend, overlay clipped to the intersection rect, skip the dead black pre-fill when the backdrop covers ONE integration test: Pump_Paces_To_The_Deadline_Compensating_Render_Cost (lands after a fake-seam lesson: pacing fakes must await, not complete synchronously, or the pump loop runs inline on StartAsync and hangs vstest). Clean build 0 warnings; FramePumpTests 10/10, SceneCompositor/SceneGraph/ SocialBar/StretchMath 20/20. Docs same commit: ai.md pipeline section, TASKS.md TASK 18 (webcam-in-output + rename modal verified from take 3), MyMistakes recipe, HANDOFF rewritten. Take 4 pending on the user's machine.
This commit is contained in:
@@ -324,4 +324,67 @@ public class FramePumpTests
|
||||
Assert.Equal(StreamStatus.Streaming, h.Status);
|
||||
Assert.Equal(59.9, h.FPS, 1);
|
||||
}
|
||||
|
||||
/// <summary>The ONE integration test for the take-3 starvation fix (2026-09-03):
|
||||
/// the frame interval is a DEADLINE — render cost eats into it, never piles on
|
||||
/// top (the OBS libobs video-io.c pacing pattern). The old loop slept the full
|
||||
/// interval AFTER each render, so 30 wall-seconds of 258ms renders muxed into a
|
||||
/// 2.1s 60fps time-lapse (rawvideo stamps by arrival). Here the fake render
|
||||
/// costs ≥60ms of a 200ms interval and the pacing seam records every requested
|
||||
/// wait: no request may reach the full interval (that was the bug), none may be
|
||||
/// negative, and a machine slow enough to blow every deadline is still honest
|
||||
/// (zero waits < interval passes too).</summary>
|
||||
[Fact]
|
||||
public async Task Pump_Paces_To_The_Deadline_Compensating_Render_Cost()
|
||||
{
|
||||
const int fps = 5; // 200ms interval — generous margin over the fake cost
|
||||
const int renderCostMs = 60; // Thread.Sleep is a guaranteed lower bound
|
||||
var interval = TimeSpan.FromSeconds(1d / fps);
|
||||
|
||||
var encoder = new FakeEncoder();
|
||||
var scene = BackgroundScene();
|
||||
var delays = new List<TimeSpan>();
|
||||
using var pump = new FramePump(
|
||||
sceneProvider: () => scene,
|
||||
frameResolver: _ =>
|
||||
{
|
||||
System.Threading.Thread.Sleep(renderCostMs);
|
||||
return null;
|
||||
},
|
||||
compositorOptions: () => new CompositorOptions
|
||||
{
|
||||
SourceRectX = 0, SourceRectY = 0, SourceRectWidth = 64, SourceRectHeight = 48,
|
||||
OutputWidth = 64, OutputHeight = 48,
|
||||
},
|
||||
encoderOptions: () => new EncoderOptions
|
||||
{
|
||||
RtmpUrl = "rtmp://a.rtmp.youtube.com/live2/abc",
|
||||
Width = 64, Height = 48, Fps = fps,
|
||||
},
|
||||
encoderFactory: () => encoder,
|
||||
pacingDelay: async (d, ct) =>
|
||||
{
|
||||
lock (delays) delays.Add(d);
|
||||
await Task.Delay(d, ct); // honors the request, like the real Task.Delay default
|
||||
});
|
||||
|
||||
await pump.StartAsync();
|
||||
var deadline = DateTime.UtcNow.AddSeconds(10);
|
||||
while (DateTime.UtcNow < deadline)
|
||||
{
|
||||
lock (delays) if (delays.Count >= 3) break;
|
||||
await Task.Delay(10);
|
||||
}
|
||||
await pump.StopAsync();
|
||||
|
||||
List<TimeSpan> got;
|
||||
lock (delays) got = delays.ToList();
|
||||
Assert.True(got.Count >= 3, $"pump paced only {got.Count} frames in 10s");
|
||||
foreach (var d in got)
|
||||
{
|
||||
Assert.True(d >= TimeSpan.Zero, $"requested wait {d} is negative");
|
||||
Assert.True(d < interval,
|
||||
$"requested wait {d} reaches the full {interval} interval — render cost must subtract from the deadline (the take-3 bug)");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user