using XFESystem.Browser; using XFESystem.Desktop; using XfePaintBaseline; namespace XFESystem.Browser.Tests; public sealed class BoxOcclusionPixelTests { [Fact] public void OpaqueInteriorAvoidsShadowWorkWithoutChangingAnyPixel() { var bounds = new BrowserRect(36.25, 30.75, 220.5, 180.25); var box = Decoration(bounds, new(12, 12, 12, 12), 1, [new(1.5, 4.25, 14.5, 2.25, new(0x112233, .85))]); var command = Command(box); var before = new Pixels(320, 260, 2); var after = new Pixels(320, 260, 2); BaselineBoxPainter.Paint(before, command, bounds, new(0, 0, 320, 260)); BrowserBoxRasterPainter.Paint(after, command, bounds, new(0, 0, 320, 260)); Assert.Equal(before.Frame, after.Frame); Assert.True(after.ShadowPixels < before.ShadowPixels * .45, $"Covered shadow mask pixels were not eliminated: {before.ShadowPixels} -> {after.ShadowPixels}."); } [Fact] public void FractionalRoundedLayersPreserveFullFramebufferAtBothScales() { var random = new Random(179198617); foreach (int scale in new[] { 1, 2 }) for (int sample = 0; sample < 100; sample++) { var bounds = new BrowserRect(6 + random.NextDouble() * 8, 7 + random.NextDouble() * 8, 10 + random.NextDouble() * 55, 8 + random.NextDouble() * 49); double maximum = Math.Min(bounds.Width, bounds.Height) / 2; var radius = new BrowserEdges(random.NextDouble() * maximum, random.NextDouble() * maximum, random.NextDouble() * maximum, random.NextDouble() * maximum); double alpha = sample % 3 == 0 ? 1 : random.NextDouble(); double opacity = sample % 4 == 0 ? 1 : random.NextDouble(); BrowserBoxShadow[] shadows = [ new(-2.75, 1.125, sample % 5 == 0 ? 0 : 3.25 + random.NextDouble() * 8, -3.5 + random.NextDouble() * 7, new(0x112233, .73)), new(4.5, -1.25, 8.75, 1.25, new(0x654321, .42)), new(1, 2, 3, 4, new(0xFF0000, 1), Inset: true) ]; var box = Decoration(bounds, radius, alpha, shadows, [Gradient(sample % 2 == 0, .24), Gradient(false, .6)]); box = box with { Border = new(.75, 1.25, 2.5, .25) }; var command = Command(box) with { Opacity = opacity }; BrowserRect visible = sample % 2 == 0 ? new(0, 0, 90, 85) : new(12.25, 15.75, 31.5, 29.25); var before = new Pixels(90, 85, scale); var after = new Pixels(90, 85, scale); BaselineBoxPainter.Paint(before, command, bounds, visible); BrowserBoxRasterPainter.Paint(after, command, bounds, visible); Assert.Equal(before.Frame, after.Frame); } } [Theory] [InlineData(1)] [InlineData(2)] public void TranslucentRoundedAncestorRetainsShadowBehindItsAntialiasedEdge(int scale) { // The ancestor clips both shadow and background. Even an opaque child // background does not occlude a shadow at a partially covered clip pixel. var bounds = new BrowserRect(6.25, 7.75, 68.5, 60.25); var box = Decoration(bounds, new(2.25, 5.5, 8.75, 3.5), 1, [new(0, 0, 6.25, 0, new(0x112233, 1))], [Gradient(true, .31)]); var command = Command(box); var before = new Pixels(90, 85, scale); var after = new Pixels(90, 85, scale); var oldClip = new BaselineClipCanvas(before); var newClip = new BrowserClipRasterCanvas(after); var first = new BrowserRoundedClip(new(10.125, 11.625, 59.25, 49.75), new(16.5, 13.25, 9.5, 18.75)); var second = new BrowserRoundedClip(new(9.375, 13.875, 63.5, 45.5), new(5.75, 11.5, 13.25, 7.625)); oldClip.Push(first, 0, 0); newClip.Push(first, 0, 0); oldClip.Push(second, 0, 0); newClip.Push(second, 0, 0); BaselineBoxPainter.Paint(oldClip, command, bounds, new(0, 0, 90, 85)); BrowserBoxRasterPainter.Paint(newClip, command, bounds, new(0, 0, 90, 85)); Assert.Equal(before.Frame, after.Frame); Assert.Contains(after.Frame, pixel => pixel != Pixels.Clear); } [Theory] [InlineData(false, 1)] [InlineData(true, 1)] [InlineData(false, 2)] [InlineData(true, 2)] public void TransparentGradientTailsSkipOnlyZeroAlphaSubmissions(bool radial, int scale) { var bounds = new BrowserRect(6.25, 7.75, 68.5, 60.25); var box = Decoration(bounds, new(9.25, 7.5, 13.25, 4.5), 0, [], [Gradient(radial, .24), new(false, 75, .5, .5, [new(new(0xABCDEF, 0), 0), new(new(0x123456, 0), 1)])]); var command = Command(box); var before = new Pixels(90, 85, scale); var after = new Pixels(90, 85, scale); BaselineBoxPainter.Paint(before, command, bounds, new(0, 0, 90, 85)); BrowserBoxRasterPainter.Paint(after, command, bounds, new(0, 0, 90, 85)); Assert.Equal(before.Frame, after.Frame); Assert.True(before.EmptyCalls > 0); Assert.Equal(0, after.EmptyCalls); Assert.True(after.BlendCalls < before.BlendCalls); } [Fact] public void FullyTransparentGradientStillChecksInteractiveYieldEveryRow() { var bounds = new BrowserRect(5, 5, 40, 40); var box = Decoration(bounds, default, 0, [], [new(false, 90, .5, .5, [new(new(0, 0), 0), new(new(0xFFFFFF, 0), 1)])]); var canvas = new YieldPixels(); Assert.Throws(() => BrowserBoxRasterPainter.Paint(canvas, Command(box), bounds, new(0, 0, 80, 80))); Assert.Equal(2, canvas.YieldChecks); Assert.Equal(0, canvas.BlendCalls); } private static BrowserBoxDecoration Decoration(BrowserRect bounds, BrowserEdges radii, double alpha, IReadOnlyList shadows, IReadOnlyList? gradients = null) => new(radii, default, new(0xACBEE3, alpha), new(0x906D25, .7), new(0xCDA3CC, .5), new(0xA6DC93, .9), new(0x31B8CF, .4), shadows, bounds, gradients ?? []); private static BrowserDrawCommand Command(BrowserBoxDecoration box) => new(BrowserDrawKind.Fill, box.Bounds, 0, null, Decoration: box); private static BrowserCssGradient Gradient(bool radial, double end) => new(radial, 112.5, .125, .75, [new(new(0xB59AE8, .73), 0), new(new(0x92ACDF, .12), end / 2), new(new(0x75B4D7, 0), end)]); private sealed class Pixels(int width, int height, int scale) : IDesktopCoverageCanvas { public const uint Clear = 0xFF203040; public int Width => width; public int Height => height; public int NativeWidth => width * scale; public int NativeHeight => height * scale; public int ScalePercent => scale * 100; public uint[] Frame { get; } = Enumerable.Repeat(Clear, width * height * scale * scale).ToArray(); public long ShadowPixels; public int EmptyCalls; public int BlendCalls; public void BlendCoverage(uint rgb, int x, int y, int maskWidth, int maskHeight, ReadOnlySpan source) { Assert.Equal(maskWidth * maskHeight, source.Length); BlendCalls++; if (rgb == 0x112233) ShadowPixels += source.Length; if (source.IndexOfAnyExcept((byte)0) < 0) EmptyCalls++; for (int row = 0; row < maskHeight; row++) for (int column = 0; column < maskWidth; column++) { int px = x + column, py = y + row; if (px < 0 || px >= NativeWidth || py < 0 || py >= NativeHeight) continue; uint alpha = source[row * maskWidth + column]; uint old = Frame[py * NativeWidth + px]; uint red = (((rgb >> 16) & 255) * alpha + ((old >> 16) & 255) * (255 - alpha) + 127) / 255; uint green = (((rgb >> 8) & 255) * alpha + ((old >> 8) & 255) * (255 - alpha) + 127) / 255; uint blue = ((rgb & 255) * alpha + (old & 255) * (255 - alpha) + 127) / 255; Frame[py * NativeWidth + px] = 0xFF000000 | red << 16 | green << 8 | blue; } } public void Fill(uint rgb, int x, int y, int fillWidth, int fillHeight) => throw new NotSupportedException(); public void Line(uint rgb, int x1, int y1, int x2, int y2) => throw new NotSupportedException(); public void Text(string text, uint rgb, int x, int y) => throw new NotSupportedException(); public void Present() => throw new NotSupportedException(); } private sealed class YieldPixels : IDesktopCoverageCanvas, IDesktopRenderYieldCanvas { public int Width => 80; public int Height => 80; public int YieldChecks; public int BlendCalls; public bool RenderYieldRequested => ++YieldChecks >= 2; public void BlendCoverage(uint rgb, int x, int y, int width, int height, ReadOnlySpan source) => BlendCalls++; public void Fill(uint rgb, int x, int y, int width, int height) => throw new NotSupportedException(); public void Line(uint rgb, int x1, int y1, int x2, int y2) => throw new NotSupportedException(); public void Text(string text, uint rgb, int x, int y) => throw new NotSupportedException(); public void Present() => throw new NotSupportedException(); } }