XFE Git
XFE Studio Git
Git 首页 全局搜索
XFE 主站 文档 NuGet

XFESystem

【Rust】XFE系统,基于Rust和汇编

公开
关注 0 Fork 0 Star 0
UTF-8
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();
    }
}