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<DesktopRenderYieldException>(() => 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<BrowserBoxShadow> shadows, IReadOnlyList<BrowserCssGradient>? 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<byte> 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<byte> 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();
}
}
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<DesktopRenderYieldException>(() => 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<BrowserBoxShadow> shadows, IReadOnlyList<BrowserCssGradient>? 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<byte> 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<byte> 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();
}
}