using Xunit; using ytLive.Models; using ytLive.Services; using ytLive.Services.Compositor; namespace ytLive.Tests; /// /// TASK 31 — the baked-crust compositor optimization. Static layers below the split /// point (first Dynamic element) are baked and cached once; only the dynamic / /// above-split layers compose per frame. A static mutation invalidates the cache; /// a dynamic-only pixel change never does. /// public class SceneGraphTests { private 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); } private static void AssertColor(VideoFrame frame, int x, int y, byte r, byte g, byte b) { var i = (y * frame.Width + x) * 4; Assert.True( Math.Abs(frame.BgraPixels[i + 2] - r) <= 2 && Math.Abs(frame.BgraPixels[i + 1] - g) <= 2 && Math.Abs(frame.BgraPixels[i] - b) <= 2, $"pixel ({x},{y}): expected rgb({r},{g},{b}), got rgb({frame.BgraPixels[i + 2]}," + $"{frame.BgraPixels[i + 1]},{frame.BgraPixels[i]})"); } private static CompositorOptions Options() => new() { SourceRectX = 0, SourceRectY = 0, SourceRectWidth = 1920, SourceRectHeight = 1080, OutputWidth = 1920, OutputHeight = 1080, }; [Fact] public void BakedStaticBase_WithDynamicLayer_CompositesCorrectly() { var red = Solid(1920, 1080, 255, 0, 0); // static background art var cyan = Solid(200, 200, 0, 255, 255); // static image var green = Solid(1280, 720, 0, 255, 0); // dynamic webcam // Z-order: static background -> static image -> dynamic webcam. var background = new Source { Name = "Background", Type = SourceType.Background }; var image = new Source { Type = SourceType.Image, X = 1200, Y = 600, Width = 200, Height = 200 }; var webcam = new WebcamSceneConfig { X = 100, Y = 100, Width = 300, Height = 300 }; var scene = new Scene { Name = "Live" }; scene.Elements.Add(background); scene.Elements.Add(image); scene.Elements.Add(webcam); var graph = new SceneGraph(); graph.Scenes.Add(scene); // Split point: first dynamic element is the webcam at index 2. Assert.Equal(2, graph.GetSplitPoint(scene)); var staticResolves = 0; VideoFrame? FrameFor(SceneElement e) { switch (e) { case Source { Type: SourceType.Background }: staticResolves++; return red; case Source { Type: SourceType.Image }: staticResolves++; return cyan; case WebcamSceneConfig: return green; default: return null; } } // Ask twice. The static base must be baked/cached: its frameFor calls happen // once (staticResolves at the first-bake count), and GetBakedBase returns the // same instance the second time (cache hit — no re-bake). var base1 = graph.GetBakedBase(scene, FrameFor, Options); var resolvesAfterFirstBake = staticResolves; var base2 = graph.GetBakedBase(scene, FrameFor, Options); Assert.NotNull(base1); Assert.Same(base1, base2); // cached: identical instance on second call Assert.Equal(resolvesAfterFirstBake, staticResolves); // no re-bake on cache hit // The baked base contains the static background (red) + static image (cyan). AssertColor(base1!, 0, 0, 255, 0, 0); AssertColor(base1!, 1220, 700, 0, 255, 255); // Composite the dynamic layer on top: webcam covers its rect, image still below. var split = graph.GetSplitPoint(scene); var output = SceneCompositor.CompositeLayers(base1!, scene, split, FrameFor, Options()); AssertColor(output, 250, 250, 0, 255, 0); // webcam (dynamic) on top AssertColor(output, 0, 0, 255, 0, 0); // static background still shows AssertColor(output, 1220, 700, 0, 255, 255); // static image still shows // Dynamic-only pixel change must NOT invalidate the cache: the webcam's frame // identity is unchanged, so GetBakedBase still returns the same baked frame. var afterDynamicOnly = graph.GetBakedBase(scene, FrameFor, Options); Assert.Same(base1, afterDynamicOnly); // A static mutation (moving the image) invalidates the cache: a fresh base is // baked and the moved image lands at its new location. image.X = 500; image.Y = 700; graph.InvalidateBake(scene); var rebaked = graph.GetBakedBase(scene, FrameFor, Options); Assert.NotNull(rebaked); AssertColor(rebaked!, 520, 750, 0, 255, 255); // image now at new location } }