From 5b3bf7d5e8295f15bf3f24daba9c8697352882b1 Mon Sep 17 00:00:00 2001 From: ZGaetano Date: Sat, 13 Jun 2026 00:24:47 -0400 Subject: [PATCH] fix(engine): guard scaler against zero-dimension source frames MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- .../ManagedNearestNeighborFrameScaler.cs | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/src/Dragon-ISO.Engine/Pipeline/ManagedNearestNeighborFrameScaler.cs b/src/Dragon-ISO.Engine/Pipeline/ManagedNearestNeighborFrameScaler.cs index cae2a6b..a6b62c7 100644 --- a/src/Dragon-ISO.Engine/Pipeline/ManagedNearestNeighborFrameScaler.cs +++ b/src/Dragon-ISO.Engine/Pipeline/ManagedNearestNeighborFrameScaler.cs @@ -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); } + /// Allocates a black, fully-opaque BGRA frame at the requested size. + 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)