Files
LlamaCasty/ytLive.Tests/SceneCompositorTests.cs
T
gramps 6af2026906 perf(compositor): slice 5 — paste cache for non-opaque layers (take-7/8 data)
The stamped build settled what slices 3-4 could not: chat cache works (resolve
~0.0ms) but render stayed 26-27ms -> 124-135/300. The cost was the compositor
re-rasterizing EVERY layer every tick: this Live scene re-samples chat (159k) +
web widget (271k) + image (95k) + cam (156k) ~ 680k px @ ~38ns — for layers
whose pixels do not change between chat/web/cam updates.

OBS shape: cache the surface, paste per tick. BlitCachedLayer rasterizes a
non-opaque layer ONCE into an element-space, transparent-based frame keyed by
(source-array identity, src W/H, ceil'd dst rect, round, mirror), then pastes:
integer position, row alpha-blend, opacity applied at paste. Producers hand out
fresh immutable arrays -> array-identity keys cannot serve stale content; dict
bounded (48, clears whole). Drag/opacity live in paste params, not keys, so
editing stops triggering resamples too. Opaque backdrop keeps the memcpy path;
the webcam keeps the direct path via its IsOpaque flag (revisit if take 9 is
borderline).

ONE integration test: PasteCache_RepeatRender_IsByteIdentical_And_ContentChange-
Propagates (byte-exact raster-vs-paste incl. round-clip margins, new-array
propagation); existing pixel suite guards sampler semantics. 85/85 across
compositor/pump/chat/capture/session classes, clean build 0 warnings. Also:
BuildStampTests.cs was written last commit but never staged — its own scope-check
slip, added here (the run had used the on-disk file; tracked now).

Docs same commit: ai.md slice 5 + stale 'general path only 130k' claim corrected,
TASKS.md take-9 gate, MyMistakes recipe (prove the stage; a fix that doesn't move
the stat wasn't the bottleneck), HANDOFF. take 9 expectation: 300/300, render
<= ~8ms -> saga closes, Unit B starts.
2026-09-04 11:44:19 -07:00

357 lines
15 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using Xunit;
using ytLive.Models;
using ytLive.Services;
using ytLive.Services.Compositor;
namespace ytLive.Tests;
/// <summary>
/// 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.
/// </summary>
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);
}
/// <summary>Left half = leftColor, right half = rightColor.</summary>
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);
}
/// <summary>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
/// <c>scratch</c> 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).</summary>
[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
}
/// <summary>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.</summary>
[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);
}
[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
}
}