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}). " +
"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];
// 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);
}
/// <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)
{
switch (aspect)