using Xunit; using ytLive.Models; using ytLive.Services; using ytLive.Services.Compositor; namespace ytLive.Tests; /// /// The output compositor (TASK 4 ship step 1): renders a scene into the encoder's /// master frame, mirroring the XAML preview minus the editing chrome. The integration /// test composites a full scene (background + round webcam + images + mirror + border) /// and asserts per-layer probe pixels; the vertical-tier test asserts the 9:16 crop /// and upscale. Frames are pushed by the test — no capture managers involved. /// public class SceneCompositorTests { public static VideoFrame Solid(int w, int h, byte r, byte g, byte b) { var pixels = new byte[w * h * 4]; for (var i = 0; i < pixels.Length; i += 4) { pixels[i] = b; pixels[i + 1] = g; pixels[i + 2] = r; pixels[i + 3] = 255; } return new VideoFrame(w, h, pixels); } /// Left half = leftColor, right half = rightColor. private static VideoFrame Split(int w, int h, byte lr, byte lg, byte lb, byte rr, byte rg, byte rb) { var pixels = new byte[w * h * 4]; var half = w / 2; for (var y = 0; y < h; y++) { for (var x = 0; x < w; x++) { var i = (y * w + x) * 4; pixels[i] = x < half ? lb : rb; pixels[i + 1] = x < half ? lg : rg; pixels[i + 2] = x < half ? lr : rr; pixels[i + 3] = 255; } } return new VideoFrame(w, h, pixels); } private static void AssertColor(VideoFrame frame, int x, int y, byte r, byte g, byte b) { var i = (y * frame.Width + x) * 4; var br = frame.BgraPixels[i + 2]; var bg = frame.BgraPixels[i + 1]; var bb = frame.BgraPixels[i]; Assert.True( Math.Abs(br - r) <= 2 && Math.Abs(bg - g) <= 2 && Math.Abs(bb - b) <= 2, $"pixel ({x},{y}): expected rgb({r},{g},{b}), got rgb({br},{bg},{bb})"); } [Fact] public void Composite_FullScene_MasterPixels() { var red = Solid(1920, 1080, 255, 0, 0); // background var green = Solid(1280, 720, 0, 255, 0); // webcam var split = Split(100, 100, 0, 255, 255, 255, 0, 255); // image: left cyan, right magenta var background = new Source { Type = SourceType.DisplayCapture, IsBackground = true, CaptureKey = "monitor:0" }; var roundWebcam = new WebcamSceneConfig { X = 100, Y = 100, Width = 300, Height = 300, ClipShape = ClipShape.Round }; var imageA = new Source { Type = SourceType.Image, X = 1200, Y = 600, Width = 200, Height = 200 }; var imageB = new Source { Type = SourceType.Image, X = 500, Y = 700, Width = 100, Height = 100, IsMirrored = true, BorderColor = "#ffffff", BorderOpacity = 1, BorderWidth = 4, }; var scene = new Scene { Name = "Live" }; scene.Elements.Add(background); scene.Elements.Add(roundWebcam); scene.Elements.Add(imageA); scene.Elements.Add(imageB); VideoFrame? FrameFor(SceneElement e) => e switch { WebcamSceneConfig => green, Source { IsBackground: true } => red, Source { Type: SourceType.Image } => split, _ => null, }; var options = new CompositorOptions { SourceRectX = 0, SourceRectY = 0, SourceRectWidth = 1920, SourceRectHeight = 1080, OutputWidth = 1920, OutputHeight = 1080, }; var output = new SceneCompositor().Render(scene, FrameFor, null, options); Assert.Equal(1920, output.Width); Assert.Equal(1080, output.Height); // background at the frame corner AssertColor(output, 0, 0, 255, 0, 0); // round webcam: center is the (square-cropped) webcam feed AssertColor(output, 250, 250, 0, 255, 0); // round webcam: element-square corner is OUTSIDE the circle -> background shows AssertColor(output, 101, 101, 255, 0, 0); // imageA (unmirrored): left half cyan, right half magenta AssertColor(output, 1220, 700, 0, 255, 255); AssertColor(output, 1380, 700, 255, 0, 255); // imageB (mirrored): halves swap — element left shows the source's right (magenta) AssertColor(output, 520, 750, 255, 0, 255); AssertColor(output, 580, 750, 0, 255, 255); // imageB border: white ring at the top edge (drawn over the content) AssertColor(output, 550, 700, 255, 255, 255); } /// The take-4 memcpy fast path: a full-cover opaque backdrop (the live /// capture's contract) must reproduce the source EXACTLY, and when supplied via the /// scratch pool parameter the render must overwrite every byte of the reused /// buffer — stale sentinels from a previous frame may not survive (this is what /// makes buffer recycling safe at the pump level). [Fact] public void Composite_OpaqueFullCover_Backdrop_CopiesEveryPixel_Into_Scratch() { const int w = 128, h = 64; var src = new byte[w * h * 4]; for (var y = 0; y < h; y++) for (var x = 0; x < w; x++) { var i = (y * w + x) * 4; src[i] = (byte)(x * 2); src[i + 1] = (byte)(y * 4); src[i + 2] = (byte)(x + y); src[i + 3] = 255; // the IsOpaque contract } var opaque = new VideoFrame(w, h, src) { IsOpaque = true }; var background = new Source { Type = SourceType.DisplayCapture, IsBackground = true, CaptureKey = "monitor:0" }; var scene = new Scene { Name = "Live" }; scene.Elements.Add(background); var options = new CompositorOptions { SourceRectX = 0, SourceRectY = 0, SourceRectWidth = w, SourceRectHeight = h, OutputWidth = w, OutputHeight = h, }; var scratch = new byte[w * h * 4]; Array.Fill(scratch, (byte)0xFD); // sentinel: x*2 (even), y*4 (×4), x+y (≤190) can never produce 253 var output = new SceneCompositor().Render(scene, _ => opaque, null, options, scratch: scratch); Assert.Equal(src, output.BgraPixels); // byte-exact: the memcpy path must not round Assert.DoesNotContain((byte)0xFD, output.BgraPixels); // full cover — nothing stale } /// The ONE integration test for the paste cache (take-8): a second render of /// the SAME frames must produce byte-identical output (the hit path pastes a cached /// element-space raster — position/opacity/margins/round-clip must survive it), and a /// NEW source array must propagate (the cache keys on frame identity, so a stale /// raster can never outlive its content). Covers the margins the FullScene probe /// pixels don't: outside-the-circle alpha and the black pre-fill interaction. [Fact] public void PasteCache_RepeatRender_IsByteIdentical_And_ContentChangePropagates() { var red = Solid(1920, 1080, 255, 0, 0); var green = Solid(1280, 720, 0, 255, 0); var blue = Solid(1280, 720, 0, 0, 255); var background = new Source { Type = SourceType.DisplayCapture, IsBackground = true, CaptureKey = "monitor:0" }; var roundWebcam = new WebcamSceneConfig { X = 100, Y = 100, Width = 300, Height = 300, ClipShape = ClipShape.Round }; var scene = new Scene { Name = "Live" }; scene.Elements.Add(background); scene.Elements.Add(roundWebcam); VideoFrame? FrameFor(SceneElement e) => e switch { WebcamSceneConfig => green, Source { IsBackground: true } => red, _ => null, }; var options = new CompositorOptions { SourceRectX = 0, SourceRectY = 0, SourceRectWidth = 1920, SourceRectHeight = 1080, OutputWidth = 1920, OutputHeight = 1080, }; var compositor = new SceneCompositor(); var first = compositor.Render(scene, FrameFor, null, options); var second = compositor.Render(scene, FrameFor, null, options); // paste-cache hit Assert.Equal(first.BgraPixels, second.BgraPixels); // margins: inside the circle = webcam, outside = backdrop (both renders) foreach (var f in new[] { first, second }) { AssertColor(f, 250, 250, 0, 255, 0); AssertColor(f, 101, 101, 255, 0, 0); Assert.Equal((byte)255, f.BgraPixels[(250 * 1920 + 250) * 4 + 3]); // alpha stays opaque } // Content change (new array, same geometry) must show through the cache. VideoFrame? FrameForBlue(SceneElement e) => e switch { WebcamSceneConfig => blue, Source { IsBackground: true } => red, _ => null, }; var third = compositor.Render(scene, FrameForBlue, null, options); AssertColor(third, 250, 250, 0, 0, 255); AssertColor(third, 101, 101, 255, 0, 0); } /// The ONE integration test for the capture buffer ring + paste-cache epoch /// (take-11 spike fix): a recycled capture buffer hands out the SAME byte[] with /// different content; only VideoFrame.Epoch can keep the paste cache honest. Without /// the epoch in the key, frame 2 false-hits frame 1's raster and the recording shows /// stale pixels for as long as the array keeps cycling — the exact bug this pins. [Fact] public void PasteCache_RecycledArrayWithNewEpoch_ReRasterizes_NotStaleHits() { var pixels = new byte[64 * 32 * 4]; for (var i = 0; i < pixels.Length; i += 4) { pixels[i] = 0; pixels[i + 1] = 0; pixels[i + 2] = 255; pixels[i + 3] = 255; // red } var gen1 = new VideoFrame(64, 32, pixels) { IsOpaque = true, Epoch = 1 }; var webcam = new WebcamSceneConfig { X = 4, Y = 4, Width = 32, Height = 16 }; var scene = new Scene { Name = "Live" }; scene.Elements.Add(webcam); var options = new CompositorOptions { SourceRectX = 0, SourceRectY = 0, SourceRectWidth = 128, SourceRectHeight = 64, OutputWidth = 128, OutputHeight = 64, }; var compositor = new SceneCompositor(); var out1 = compositor.Render(scene, _ => gen1, null, options); AssertColor(out1, 20, 12, 255, 0, 0); // Generation 2: the SAME array, new content, bumped epoch (what the capture // ring does 60 times a second). for (var i = 0; i < pixels.Length; i += 4) { pixels[i] = 0; pixels[i + 1] = 255; pixels[i + 2] = 0; pixels[i + 3] = 255; // green } var gen2 = new VideoFrame(64, 32, pixels) { IsOpaque = true, Epoch = 2 }; var out2 = compositor.Render(scene, _ => gen2, null, options); AssertColor(out2, 20, 12, 0, 255, 0); // a stale hit would still be red // And the same epoch twice still hits the cache (idempotent paste). var out3 = compositor.Render(scene, _ => gen2, null, options); Assert.Equal(out2.BgraPixels, out3.BgraPixels); } [Fact] public void Render_VerticalTier_Outputs_1080x1920_From_The_Center_Crop() { var red = Solid(1920, 1080, 255, 0, 0); var green = Solid(1280, 720, 0, 255, 0); var background = new Source { Type = SourceType.DisplayCapture, IsBackground = true, CaptureKey = "monitor:0" }; var roundWebcam = new WebcamSceneConfig { X = 656, Y = 100, Width = 300, Height = 300, ClipShape = ClipShape.Round }; var scene = new Scene { Name = "Live" }; scene.Elements.Add(background); scene.Elements.Add(roundWebcam); VideoFrame? FrameFor(SceneElement e) => e switch { WebcamSceneConfig => green, Source { IsBackground: true } => red, _ => null, }; var options = new CompositorOptions { SourceRectX = 656, SourceRectY = 0, SourceRectWidth = 607, SourceRectHeight = 1080, OutputWidth = 1080, OutputHeight = 1920, }; var output = new SceneCompositor().Render(scene, FrameFor, null, options); Assert.Equal(1080, output.Width); Assert.Equal(1920, output.Height); // top-left of the crop is pure background (webcam starts at crop y=100, inset from the corner) AssertColor(output, 0, 0, 255, 0, 0); // the webcam center (crop 150,250) scales to output ~(267,444) and stays green AssertColor(output, 267, 444, 0, 255, 0); // far from the webcam, still background AssertColor(output, 978, 1688, 255, 0, 0); } [Fact] public void Composite_WithFlash_BlendsOverContent() { var red = Solid(1920, 1080, 255, 0, 0); var background = new Source { Type = SourceType.DisplayCapture, IsBackground = true, CaptureKey = "monitor:0" }; var scene = new Scene { Name = "Live" }; scene.Elements.Add(background); // flash: a semi-transparent white pixel at the center of a master-sized frame var flash = new VideoFrame(1920, 1080, new byte[1920 * 1080 * 4]); var ci = (1080 / 2 * 1920 + 1920 / 2) * 4; flash.BgraPixels[ci] = 255; flash.BgraPixels[ci + 1] = 255; flash.BgraPixels[ci + 2] = 255; flash.BgraPixels[ci + 3] = 64; // ~25% alpha var options = new CompositorOptions { SourceRectX = 0, SourceRectY = 0, SourceRectWidth = 1920, SourceRectHeight = 1080, OutputWidth = 1920, OutputHeight = 1080, }; var output = new SceneCompositor().Render(scene, _ => red, flash, options); // red lightened by 25% white: ~(255, 63, 63) AssertColor(output, 960, 540, 255, 64, 64); // untouched corner stays pure red AssertColor(output, 0, 0, 255, 0, 0); } [Fact] public void Composite_WithSocialBar_OverlaysAboveFlash_AtTopOrBottom() { var red = Solid(1920, 1080, 255, 0, 0); var background = new Source { Type = SourceType.DisplayCapture, IsBackground = true, CaptureKey = "monitor:0" }; var scene = new Scene { Name = "Live" }; scene.Elements.Add(background); var options = new CompositorOptions { SourceRectX = 0, SourceRectY = 0, SourceRectWidth = 1920, SourceRectHeight = 1080, OutputWidth = 1920, OutputHeight = 1080, }; var compositor = new SceneCompositor(); // master-sized overlay: opaque green at (0,0), 50%-white at the center var bar = new VideoFrame(1920, 1080, new byte[1920 * 1080 * 4]); var g = 0; bar.BgraPixels[g] = 0; bar.BgraPixels[g + 1] = 255; bar.BgraPixels[g + 2] = 0; bar.BgraPixels[g + 3] = 255; var w = (540 * 1920 + 960) * 4; bar.BgraPixels[w] = 255; bar.BgraPixels[w + 1] = 255; bar.BgraPixels[w + 2] = 255; bar.BgraPixels[w + 3] = 128; // flash: opaque magenta at (0,0) — the bar must cover it var flash = new VideoFrame(1920, 1080, new byte[1920 * 1080 * 4]); flash.BgraPixels[0] = 255; flash.BgraPixels[1] = 0; flash.BgraPixels[2] = 255; flash.BgraPixels[3] = 255; var top = compositor.Render(scene, _ => red, flash, options, bar, 0); AssertColor(top, 0, 0, 0, 255, 0); // bar above flash at the top-left AssertColor(top, 960, 540, 255, 127, 127); // 50% white over red AssertColor(top, 100, 100, 255, 0, 0); // empty overlay area: background var bottom = compositor.Render(scene, _ => red, flash, options, bar, 1040); AssertColor(bottom, 0, 1040, 0, 255, 0); // bar drawn at the bottom edge AssertColor(bottom, 0, 1039, 255, 0, 0); // background just above the bar AssertColor(bottom, 960, 540, 255, 0, 0); // bar region moved away from center } } public class StretchMathTests { [Fact] public void UniformToFill_SameAspect_Is_Exact_Fit_With_No_Offset() { var (scale, ox, oy) = StretchMath.UniformToFill(400, 300, 800, 600); Assert.Equal(0.5f, scale, 3); Assert.Equal(0f, ox, 3); Assert.Equal(0f, oy, 3); } [Fact] public void UniformToFill_WiderSource_Crops_And_Centers_Vertically() { var (scale, ox, oy) = StretchMath.UniformToFill(400, 200, 800, 600); Assert.Equal(0.5f, scale, 3); Assert.Equal(0f, ox, 3); Assert.Equal(-50f, oy, 3); // drawn 400x300 into 400x200 -> 50px crop top and bottom } [Fact] public void BilinearScale_SameSize_ReturnsTheInput() { var src = SceneCompositorTests.Solid(4, 4, 10, 20, 30); Assert.Same(src, StretchMath.BilinearScale(src, 4, 4)); } [Fact] public void BilinearScale_Downscales_To_Target_Size() { var src = SceneCompositorTests.Solid(16, 16, 0, 255, 0); var scaled = StretchMath.BilinearScale(src, 8, 8); Assert.Equal(8, scaled.Width); Assert.Equal(8, scaled.Height); var i = 0; Assert.Equal(255, scaled.BgraPixels[i + 1]); // solid green survives the scale } }