using Xunit; using ytLive.Models; using ytLive.Services; using ytLive.Services.Compositor; using ytLive.Services.Encoder; namespace ytLive.Tests; /// /// Per-element composite timing + the pump's scene dump (webcam-cadence unit, /// 2026-10-16). The judder diagnosis ended with a question the log could not answer: /// the compositor was doing 6.8 composites/s at ~141ms each, and nothing in the stats /// line said WHICH of the scene's six elements ate the budget — only that the render /// cost 141ms. This pins the two readings that close that gap: a real composite /// attributes wall time per element (slowest first) with the paste-cache rasterize /// split, and the scene dump records the shape a take was made from. /// public class CompositeTimingTests { /// The ONE integration test for this change: a REAL SceneCompositor.Render /// over a full-canvas element plus a small one, through the real timing collector — /// the parts line must name both elements, sort the full-canvas one first, and carry /// the raster sub-stage; the dump must name every element with its z-index, source /// type and rect, and echo the split point. No 5s stats window is waited on: the /// window logic only formats what the collector already accumulated. [Fact] public void Composite_Timings_Rank_The_Slowest_Element_And_The_Dump_Names_The_Scene_Shape() { const int w = 640, h = 480; var scene = new Scene { Name = "Live" }; var big = new Source { Type = SourceType.Image, Name = "FullCanvas", X = 0, Y = 0, Width = w, Height = h }; var small = new Source { Type = SourceType.Image, Name = "TinyBadge", X = 16, Y = 16, Width = 64, Height = 64 }; scene.Elements.Add(big); scene.Elements.Add(small); var bigFrame = SceneCompositorTests.Solid(w, h, 200, 40, 40); var smallFrame = SceneCompositorTests.Solid(64, 64, 40, 200, 40); VideoFrame? FrameFor(SceneElement e) => ReferenceEquals(e, big) ? bigFrame : smallFrame; var options = new CompositorOptions { SourceRectX = 0, SourceRectY = 0, SourceRectWidth = w, SourceRectHeight = h, OutputWidth = w, OutputHeight = h, }; var timings = new CompositeTimings(); new SceneCompositor().Render(scene, FrameFor, null, options, timings: timings); // minMs 0: a 640×480 test scene is sub-millisecond per stage, so the production // 0.1ms floor would hide exactly the entries being asserted. var parts = timings.Describe(1, minMs: 0); Assert.Contains("#0 FullCanvas", parts); Assert.Contains("#1 TinyBadge", parts); Assert.Contains("#0 FullCanvas raster", parts); // the paste-cache rasterize miss Assert.Contains("scale", parts); Assert.Contains("backdrop", parts); Assert.True( parts.IndexOf("#0 FullCanvas", StringComparison.Ordinal) < parts.IndexOf("#1 TinyBadge", StringComparison.Ordinal), $"the full-canvas element must rank above the 64×64 one: {parts}"); var dump = FramePump.FormatSceneDump(scene, split: 1); Assert.Contains("FramePump scene dump: 'Live' elements=2 split=1", dump[0]); Assert.Contains(dump, l => l.Contains("[0] FullCanvas") && l.Contains("type=Image") && l.Contains("vis=1") && l.Contains("rect=0,0 640x480")); Assert.Contains(dump, l => l.Contains("[1] TinyBadge") && l.Contains("rect=16,16 64x64")); // No SceneGraph wired (-1) — the dump says nothing about a split point. Assert.DoesNotContain("split=", FramePump.FormatSceneDump(scene, split: -1)[0]); } }