Files
ytLlive/Services/Compositor/StretchMath.cs
T
gramps 18a21010bb Scene compositor (TASK 4 ship step 1): encoder master-frame scaffolding
Software compositor that renders a scene into the encoder's master VideoFrame,
mirroring the XAML preview minus editing chrome: backdrop -> background ->
elements (UniformToFill cover-crop, round clip, mirror, opacity, border) ->
branding flash.

NOTE FOR USERS: this change shows NO difference in the app's UI — it is pure
backend scaffolding laying the groundwork for live video capture/streaming.
The preview you see is unchanged.

- Services/Compositor/: SceneCompositor (Render(scene, frameFor resolver,
  flashFrame, CompositorOptions)), CompositorOptions (source rect + output
  size; 16:9 full master, vertical 607x1080 -> 1080x1920), StretchMath (pure
  UniformToFill + bilinear), StaticPixelCache (asset bytes -> BGRA8 frame)
- Frame sources injected via Func<SceneElement, VideoFrame?> resolver, so the
  compositor is pure, WPF-free, and hermetic to test (D3D11 upgrade behind the
  same seam later)
- SceneElement.TryGetBorderColor public (shared hex parse), stale
  MainViewModel comment fixed, pre-existing CS1998 in YouTubeAuthServiceTests
  cleaned up
- Tests: SceneCompositorTests integration (full scene + vertical tier + flash)
  + StretchMath units, docs updated (72 tests passing, 0 warnings)
2026-08-10 10:15:58 -07:00

77 lines
3.2 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.
namespace ytLive.Services.Compositor;
/// <summary>
/// Pure pixel math shared by the compositor: the WPF "UniformToFill" cover-crop
/// (what the preview's <c>Stretch="UniformToFill"</c> does) and a clamped bilinear
/// sample/scale. Pure and deterministic — the unit-tested half of the compositor.
/// </summary>
public static class StretchMath
{
/// <summary>
/// UniformToFill: the drawn content covers the destination while preserving the
/// source aspect, centered; overflow is cropped. Returns the scale and the
/// source-origin offset (in destination pixels) of the drawn content within the
/// destination rect.
/// </summary>
public static (float Scale, float OffsetX, float OffsetY) UniformToFill(float dstW, float dstH, int srcW, int srcH)
{
var scale = Math.Max(dstW / srcW, dstH / srcH);
var drawnW = srcW * scale;
var drawnH = srcH * scale;
return (scale, (dstW - drawnW) / 2f, (dstH - drawnH) / 2f);
}
/// <summary>Clamped bilinear scale into a new tightly-packed BGRA8 frame; returns the input unchanged when the sizes already match.</summary>
public static VideoFrame BilinearScale(VideoFrame src, int outW, int outH)
{
if (outW == src.Width && outH == src.Height) return src;
var outPixels = new byte[outW * outH * 4];
for (var y = 0; y < outH; y++)
{
var sy = (y + 0.5f) / outH * src.Height - 0.5f;
for (var x = 0; x < outW; x++)
{
var sx = (x + 0.5f) / outW * src.Width - 0.5f;
var (b, g, r, a) = SampleBgra(src.BgraPixels, src.Width, src.Height, sx, sy);
var di = (y * outW + x) * 4;
outPixels[di] = b;
outPixels[di + 1] = g;
outPixels[di + 2] = r;
outPixels[di + 3] = a;
}
}
return new VideoFrame(outW, outH, outPixels);
}
/// <summary>
/// Clamped bilinear sample of one BGRA8 pixel at fractional (sx, sy). Coordinates
/// outside [0, w-1]×[0, h-1] clamp to the edge, so a caller can sample freely
/// without bounds checks.
/// </summary>
public static (byte B, byte G, byte R, byte A) SampleBgra(byte[] src, int w, int h, float sx, float sy)
{
sx = Math.Clamp(sx, 0, w - 1);
sy = Math.Clamp(sy, 0, h - 1);
var x0 = (int)sx;
var y0 = (int)sy;
var x1 = Math.Min(x0 + 1, w - 1);
var y1 = Math.Min(y0 + 1, h - 1);
var fx = sx - x0;
var fy = sy - y0;
var p00 = (y0 * w + x0) * 4;
var p10 = (y0 * w + x1) * 4;
var p01 = (y1 * w + x0) * 4;
var p11 = (y1 * w + x1) * 4;
float r = Lerp(Lerp(src[p00 + 2], src[p10 + 2], fx), Lerp(src[p01 + 2], src[p11 + 2], fx), fy);
float g = Lerp(Lerp(src[p00 + 1], src[p10 + 1], fx), Lerp(src[p01 + 1], src[p11 + 1], fx), fy);
float b = Lerp(Lerp(src[p00], src[p10], fx), Lerp(src[p01], src[p11], fx), fy);
float a = Lerp(Lerp(src[p00 + 3], src[p10 + 3], fx), Lerp(src[p01 + 3], src[p11 + 3], fx), fy);
return ((byte)Math.Round(b), (byte)Math.Round(g), (byte)Math.Round(r), (byte)Math.Round(a));
}
private static float Lerp(float a, float b, float t) => a + (b - a) * t;
}