fix(engine): guard scaler against zero-dimension source frames
All checks were successful
CI / build-and-test (push) Successful in 31s

A source frame reporting width or height == 0 (a malformed/glitched NDI
frame, or a sender mid-renegotiation) drove ComputeFitRect into a
division by zero: srcAspect = srcW/srcH with srcH==0 yields Infinity →
NaN width/height, and the Stretch path produced a degenerate copy loop.
Either way a single bad frame could throw out of Scale and bubble up to
the pipeline supervisor as a failure, costing a restart + reconnect.

Now a zero-area source short-circuits to a black, fully-opaque frame at
the target resolution — the same visual the slate path would show — so a
transient bad frame is absorbed silently instead of tearing down the ISO.
This commit is contained in:
Zac Gaetano 2026-06-13 00:24:47 -04:00
parent e36b928c69
commit 5b3bf7d5e8

View file

@ -15,6 +15,15 @@ public sealed class ManagedNearestNeighborFrameScaler : IFrameScaler
$"ManagedNearestNeighborFrameScaler only supports BGRA input (got {source.Format}). " + $"ManagedNearestNeighborFrameScaler only supports BGRA input (got {source.Format}). " +
"v1.0 receivers request BGRA from NDI; UYVY support comes in v1.5."); "v1.0 receivers request BGRA from NDI; UYVY support comes in v1.5.");
// Degenerate source guard: a frame reporting a zero (or negative) dimension
// would divide-by-zero in ComputeFitRect (srcW/srcH) and produce NaN extents,
// or drive a degenerate copy loop on the Stretch path. Rather than throw out
// of the hot path and cost the pipeline a supervisor restart, emit a black
// opaque frame at the target size — identical to what the no-signal slate
// would render — so a single bad frame is absorbed without a reconnect.
if (source.Width <= 0 || source.Height <= 0)
return BlackFrame(targetWidth, targetHeight, timestampTicks);
var output = new byte[targetWidth * targetHeight * 4]; var output = new byte[targetWidth * targetHeight * 4];
// Compute the destination rectangle within the target according to aspect mode. // Compute the destination rectangle within the target according to aspect mode.
@ -56,6 +65,14 @@ public sealed class ManagedNearestNeighborFrameScaler : IFrameScaler
return new ProcessedFrame(targetWidth, targetHeight, timestampTicks, output, PixelFormat.Bgra); return new ProcessedFrame(targetWidth, targetHeight, timestampTicks, output, PixelFormat.Bgra);
} }
/// <summary>Allocates a black, fully-opaque BGRA frame at the requested size.</summary>
private static ProcessedFrame BlackFrame(int width, int height, long timestampTicks)
{
var output = new byte[width * height * 4];
for (var i = 3; i < output.Length; i += 4) output[i] = 0xFF; // alpha; BGR stay 0
return new ProcessedFrame(width, height, timestampTicks, output, PixelFormat.Bgra);
}
private static (int X, int Y, int W, int H) ComputeFitRect(int srcW, int srcH, int dstW, int dstH, AspectMode aspect) private static (int X, int Y, int W, int H) ComputeFitRect(int srcW, int srcH, int dstW, int dstH, AspectMode aspect)
{ {
switch (aspect) switch (aspect)