// Copyright (c) Six Labors. // Licensed under the Six Labors Split License. using System.Numerics; using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.PixelFormats.PixelBlenders; using SixLabors.ImageSharp.Tests.TestUtilities; namespace SixLabors.ImageSharp.Tests.PixelFormats; [Trait("Category", "PixelFormats")] public class PixelBlenderTests { public static TheoryData BlenderMappings = new() { { new TestPixel(), DefaultPixelBlenders.NormalSrcOver.GetType(), PixelColorBlendingMode.Normal }, { new TestPixel(), DefaultPixelBlenders.ScreenSrcOver.GetType(), PixelColorBlendingMode.Screen }, { new TestPixel(), DefaultPixelBlenders.HardLightSrcOver.GetType(), PixelColorBlendingMode.HardLight }, { new TestPixel(), DefaultPixelBlenders.OverlaySrcOver.GetType(), PixelColorBlendingMode.Overlay }, { new TestPixel(), DefaultPixelBlenders.DarkenSrcOver.GetType(), PixelColorBlendingMode.Darken }, { new TestPixel(), DefaultPixelBlenders.LightenSrcOver.GetType(), PixelColorBlendingMode.Lighten }, { new TestPixel(), DefaultPixelBlenders.AddSrcOver.GetType(), PixelColorBlendingMode.Add }, { new TestPixel(), DefaultPixelBlenders.SubtractSrcOver.GetType(), PixelColorBlendingMode.Subtract }, { new TestPixel(), DefaultPixelBlenders.MultiplySrcOver.GetType(), PixelColorBlendingMode.Multiply }, { new TestPixel(), DefaultPixelBlenders.NormalSrcOver.GetType(), PixelColorBlendingMode.Normal }, { new TestPixel(), DefaultPixelBlenders.ScreenSrcOver.GetType(), PixelColorBlendingMode.Screen }, { new TestPixel(), DefaultPixelBlenders.HardLightSrcOver.GetType(), PixelColorBlendingMode.HardLight }, { new TestPixel(), DefaultPixelBlenders.OverlaySrcOver.GetType(), PixelColorBlendingMode.Overlay }, { new TestPixel(), DefaultPixelBlenders.DarkenSrcOver.GetType(), PixelColorBlendingMode.Darken }, { new TestPixel(), DefaultPixelBlenders.LightenSrcOver.GetType(), PixelColorBlendingMode.Lighten }, { new TestPixel(), DefaultPixelBlenders.AddSrcOver.GetType(), PixelColorBlendingMode.Add }, { new TestPixel(), DefaultPixelBlenders.SubtractSrcOver.GetType(), PixelColorBlendingMode.Subtract }, { new TestPixel(), DefaultPixelBlenders.MultiplySrcOver.GetType(), PixelColorBlendingMode.Multiply }, }; public static TheoryData BlenderModeMappings { get { TheoryData data = new(); AddBlenderModeMappings( data, PixelAlphaCompositionMode.Clear, DefaultPixelBlenders.NormalClear.GetType(), DefaultPixelBlenders.MultiplyClear.GetType(), DefaultPixelBlenders.AddClear.GetType(), DefaultPixelBlenders.SubtractClear.GetType(), DefaultPixelBlenders.ScreenClear.GetType(), DefaultPixelBlenders.DarkenClear.GetType(), DefaultPixelBlenders.LightenClear.GetType(), DefaultPixelBlenders.OverlayClear.GetType(), DefaultPixelBlenders.HardLightClear.GetType(), DefaultPixelBlenders.ColorDodgeClear.GetType(), DefaultPixelBlenders.ColorBurnClear.GetType(), DefaultPixelBlenders.SoftLightClear.GetType(), DefaultPixelBlenders.DifferenceClear.GetType(), DefaultPixelBlenders.ExclusionClear.GetType(), DefaultPixelBlenders.HueClear.GetType(), DefaultPixelBlenders.SaturationClear.GetType(), DefaultPixelBlenders.ColorClear.GetType(), DefaultPixelBlenders.LuminosityClear.GetType()); AddBlenderModeMappings( data, PixelAlphaCompositionMode.Xor, DefaultPixelBlenders.NormalXor.GetType(), DefaultPixelBlenders.MultiplyXor.GetType(), DefaultPixelBlenders.AddXor.GetType(), DefaultPixelBlenders.SubtractXor.GetType(), DefaultPixelBlenders.ScreenXor.GetType(), DefaultPixelBlenders.DarkenXor.GetType(), DefaultPixelBlenders.LightenXor.GetType(), DefaultPixelBlenders.OverlayXor.GetType(), DefaultPixelBlenders.HardLightXor.GetType(), DefaultPixelBlenders.ColorDodgeXor.GetType(), DefaultPixelBlenders.ColorBurnXor.GetType(), DefaultPixelBlenders.SoftLightXor.GetType(), DefaultPixelBlenders.DifferenceXor.GetType(), DefaultPixelBlenders.ExclusionXor.GetType(), DefaultPixelBlenders.HueXor.GetType(), DefaultPixelBlenders.SaturationXor.GetType(), DefaultPixelBlenders.ColorXor.GetType(), DefaultPixelBlenders.LuminosityXor.GetType()); AddBlenderModeMappings( data, PixelAlphaCompositionMode.Plus, DefaultPixelBlenders.NormalPlus.GetType(), DefaultPixelBlenders.MultiplyPlus.GetType(), DefaultPixelBlenders.AddPlus.GetType(), DefaultPixelBlenders.SubtractPlus.GetType(), DefaultPixelBlenders.ScreenPlus.GetType(), DefaultPixelBlenders.DarkenPlus.GetType(), DefaultPixelBlenders.LightenPlus.GetType(), DefaultPixelBlenders.OverlayPlus.GetType(), DefaultPixelBlenders.HardLightPlus.GetType(), DefaultPixelBlenders.ColorDodgePlus.GetType(), DefaultPixelBlenders.ColorBurnPlus.GetType(), DefaultPixelBlenders.SoftLightPlus.GetType(), DefaultPixelBlenders.DifferencePlus.GetType(), DefaultPixelBlenders.ExclusionPlus.GetType(), DefaultPixelBlenders.HuePlus.GetType(), DefaultPixelBlenders.SaturationPlus.GetType(), DefaultPixelBlenders.ColorPlus.GetType(), DefaultPixelBlenders.LuminosityPlus.GetType()); AddBlenderModeMappings( data, PixelAlphaCompositionMode.Src, DefaultPixelBlenders.NormalSrc.GetType(), DefaultPixelBlenders.MultiplySrc.GetType(), DefaultPixelBlenders.AddSrc.GetType(), DefaultPixelBlenders.SubtractSrc.GetType(), DefaultPixelBlenders.ScreenSrc.GetType(), DefaultPixelBlenders.DarkenSrc.GetType(), DefaultPixelBlenders.LightenSrc.GetType(), DefaultPixelBlenders.OverlaySrc.GetType(), DefaultPixelBlenders.HardLightSrc.GetType(), DefaultPixelBlenders.ColorDodgeSrc.GetType(), DefaultPixelBlenders.ColorBurnSrc.GetType(), DefaultPixelBlenders.SoftLightSrc.GetType(), DefaultPixelBlenders.DifferenceSrc.GetType(), DefaultPixelBlenders.ExclusionSrc.GetType(), DefaultPixelBlenders.HueSrc.GetType(), DefaultPixelBlenders.SaturationSrc.GetType(), DefaultPixelBlenders.ColorSrc.GetType(), DefaultPixelBlenders.LuminositySrc.GetType()); AddBlenderModeMappings( data, PixelAlphaCompositionMode.SrcAtop, DefaultPixelBlenders.NormalSrcAtop.GetType(), DefaultPixelBlenders.MultiplySrcAtop.GetType(), DefaultPixelBlenders.AddSrcAtop.GetType(), DefaultPixelBlenders.SubtractSrcAtop.GetType(), DefaultPixelBlenders.ScreenSrcAtop.GetType(), DefaultPixelBlenders.DarkenSrcAtop.GetType(), DefaultPixelBlenders.LightenSrcAtop.GetType(), DefaultPixelBlenders.OverlaySrcAtop.GetType(), DefaultPixelBlenders.HardLightSrcAtop.GetType(), DefaultPixelBlenders.ColorDodgeSrcAtop.GetType(), DefaultPixelBlenders.ColorBurnSrcAtop.GetType(), DefaultPixelBlenders.SoftLightSrcAtop.GetType(), DefaultPixelBlenders.DifferenceSrcAtop.GetType(), DefaultPixelBlenders.ExclusionSrcAtop.GetType(), DefaultPixelBlenders.HueSrcAtop.GetType(), DefaultPixelBlenders.SaturationSrcAtop.GetType(), DefaultPixelBlenders.ColorSrcAtop.GetType(), DefaultPixelBlenders.LuminositySrcAtop.GetType()); AddBlenderModeMappings( data, PixelAlphaCompositionMode.SrcIn, DefaultPixelBlenders.NormalSrcIn.GetType(), DefaultPixelBlenders.MultiplySrcIn.GetType(), DefaultPixelBlenders.AddSrcIn.GetType(), DefaultPixelBlenders.SubtractSrcIn.GetType(), DefaultPixelBlenders.ScreenSrcIn.GetType(), DefaultPixelBlenders.DarkenSrcIn.GetType(), DefaultPixelBlenders.LightenSrcIn.GetType(), DefaultPixelBlenders.OverlaySrcIn.GetType(), DefaultPixelBlenders.HardLightSrcIn.GetType(), DefaultPixelBlenders.ColorDodgeSrcIn.GetType(), DefaultPixelBlenders.ColorBurnSrcIn.GetType(), DefaultPixelBlenders.SoftLightSrcIn.GetType(), DefaultPixelBlenders.DifferenceSrcIn.GetType(), DefaultPixelBlenders.ExclusionSrcIn.GetType(), DefaultPixelBlenders.HueSrcIn.GetType(), DefaultPixelBlenders.SaturationSrcIn.GetType(), DefaultPixelBlenders.ColorSrcIn.GetType(), DefaultPixelBlenders.LuminositySrcIn.GetType()); AddBlenderModeMappings( data, PixelAlphaCompositionMode.SrcOut, DefaultPixelBlenders.NormalSrcOut.GetType(), DefaultPixelBlenders.MultiplySrcOut.GetType(), DefaultPixelBlenders.AddSrcOut.GetType(), DefaultPixelBlenders.SubtractSrcOut.GetType(), DefaultPixelBlenders.ScreenSrcOut.GetType(), DefaultPixelBlenders.DarkenSrcOut.GetType(), DefaultPixelBlenders.LightenSrcOut.GetType(), DefaultPixelBlenders.OverlaySrcOut.GetType(), DefaultPixelBlenders.HardLightSrcOut.GetType(), DefaultPixelBlenders.ColorDodgeSrcOut.GetType(), DefaultPixelBlenders.ColorBurnSrcOut.GetType(), DefaultPixelBlenders.SoftLightSrcOut.GetType(), DefaultPixelBlenders.DifferenceSrcOut.GetType(), DefaultPixelBlenders.ExclusionSrcOut.GetType(), DefaultPixelBlenders.HueSrcOut.GetType(), DefaultPixelBlenders.SaturationSrcOut.GetType(), DefaultPixelBlenders.ColorSrcOut.GetType(), DefaultPixelBlenders.LuminositySrcOut.GetType()); AddBlenderModeMappings( data, PixelAlphaCompositionMode.Dest, DefaultPixelBlenders.NormalDest.GetType(), DefaultPixelBlenders.MultiplyDest.GetType(), DefaultPixelBlenders.AddDest.GetType(), DefaultPixelBlenders.SubtractDest.GetType(), DefaultPixelBlenders.ScreenDest.GetType(), DefaultPixelBlenders.DarkenDest.GetType(), DefaultPixelBlenders.LightenDest.GetType(), DefaultPixelBlenders.OverlayDest.GetType(), DefaultPixelBlenders.HardLightDest.GetType(), DefaultPixelBlenders.ColorDodgeDest.GetType(), DefaultPixelBlenders.ColorBurnDest.GetType(), DefaultPixelBlenders.SoftLightDest.GetType(), DefaultPixelBlenders.DifferenceDest.GetType(), DefaultPixelBlenders.ExclusionDest.GetType(), DefaultPixelBlenders.HueDest.GetType(), DefaultPixelBlenders.SaturationDest.GetType(), DefaultPixelBlenders.ColorDest.GetType(), DefaultPixelBlenders.LuminosityDest.GetType()); AddBlenderModeMappings( data, PixelAlphaCompositionMode.DestAtop, DefaultPixelBlenders.NormalDestAtop.GetType(), DefaultPixelBlenders.MultiplyDestAtop.GetType(), DefaultPixelBlenders.AddDestAtop.GetType(), DefaultPixelBlenders.SubtractDestAtop.GetType(), DefaultPixelBlenders.ScreenDestAtop.GetType(), DefaultPixelBlenders.DarkenDestAtop.GetType(), DefaultPixelBlenders.LightenDestAtop.GetType(), DefaultPixelBlenders.OverlayDestAtop.GetType(), DefaultPixelBlenders.HardLightDestAtop.GetType(), DefaultPixelBlenders.ColorDodgeDestAtop.GetType(), DefaultPixelBlenders.ColorBurnDestAtop.GetType(), DefaultPixelBlenders.SoftLightDestAtop.GetType(), DefaultPixelBlenders.DifferenceDestAtop.GetType(), DefaultPixelBlenders.ExclusionDestAtop.GetType(), DefaultPixelBlenders.HueDestAtop.GetType(), DefaultPixelBlenders.SaturationDestAtop.GetType(), DefaultPixelBlenders.ColorDestAtop.GetType(), DefaultPixelBlenders.LuminosityDestAtop.GetType()); AddBlenderModeMappings( data, PixelAlphaCompositionMode.DestIn, DefaultPixelBlenders.NormalDestIn.GetType(), DefaultPixelBlenders.MultiplyDestIn.GetType(), DefaultPixelBlenders.AddDestIn.GetType(), DefaultPixelBlenders.SubtractDestIn.GetType(), DefaultPixelBlenders.ScreenDestIn.GetType(), DefaultPixelBlenders.DarkenDestIn.GetType(), DefaultPixelBlenders.LightenDestIn.GetType(), DefaultPixelBlenders.OverlayDestIn.GetType(), DefaultPixelBlenders.HardLightDestIn.GetType(), DefaultPixelBlenders.ColorDodgeDestIn.GetType(), DefaultPixelBlenders.ColorBurnDestIn.GetType(), DefaultPixelBlenders.SoftLightDestIn.GetType(), DefaultPixelBlenders.DifferenceDestIn.GetType(), DefaultPixelBlenders.ExclusionDestIn.GetType(), DefaultPixelBlenders.HueDestIn.GetType(), DefaultPixelBlenders.SaturationDestIn.GetType(), DefaultPixelBlenders.ColorDestIn.GetType(), DefaultPixelBlenders.LuminosityDestIn.GetType()); AddBlenderModeMappings( data, PixelAlphaCompositionMode.DestOut, DefaultPixelBlenders.NormalDestOut.GetType(), DefaultPixelBlenders.MultiplyDestOut.GetType(), DefaultPixelBlenders.AddDestOut.GetType(), DefaultPixelBlenders.SubtractDestOut.GetType(), DefaultPixelBlenders.ScreenDestOut.GetType(), DefaultPixelBlenders.DarkenDestOut.GetType(), DefaultPixelBlenders.LightenDestOut.GetType(), DefaultPixelBlenders.OverlayDestOut.GetType(), DefaultPixelBlenders.HardLightDestOut.GetType(), DefaultPixelBlenders.ColorDodgeDestOut.GetType(), DefaultPixelBlenders.ColorBurnDestOut.GetType(), DefaultPixelBlenders.SoftLightDestOut.GetType(), DefaultPixelBlenders.DifferenceDestOut.GetType(), DefaultPixelBlenders.ExclusionDestOut.GetType(), DefaultPixelBlenders.HueDestOut.GetType(), DefaultPixelBlenders.SaturationDestOut.GetType(), DefaultPixelBlenders.ColorDestOut.GetType(), DefaultPixelBlenders.LuminosityDestOut.GetType()); AddBlenderModeMappings( data, PixelAlphaCompositionMode.DestOver, DefaultPixelBlenders.NormalDestOver.GetType(), DefaultPixelBlenders.MultiplyDestOver.GetType(), DefaultPixelBlenders.AddDestOver.GetType(), DefaultPixelBlenders.SubtractDestOver.GetType(), DefaultPixelBlenders.ScreenDestOver.GetType(), DefaultPixelBlenders.DarkenDestOver.GetType(), DefaultPixelBlenders.LightenDestOver.GetType(), DefaultPixelBlenders.OverlayDestOver.GetType(), DefaultPixelBlenders.HardLightDestOver.GetType(), DefaultPixelBlenders.ColorDodgeDestOver.GetType(), DefaultPixelBlenders.ColorBurnDestOver.GetType(), DefaultPixelBlenders.SoftLightDestOver.GetType(), DefaultPixelBlenders.DifferenceDestOver.GetType(), DefaultPixelBlenders.ExclusionDestOver.GetType(), DefaultPixelBlenders.HueDestOver.GetType(), DefaultPixelBlenders.SaturationDestOver.GetType(), DefaultPixelBlenders.ColorDestOver.GetType(), DefaultPixelBlenders.LuminosityDestOver.GetType()); AddBlenderModeMappings( data, PixelAlphaCompositionMode.SrcOver, DefaultPixelBlenders.NormalSrcOver.GetType(), DefaultPixelBlenders.MultiplySrcOver.GetType(), DefaultPixelBlenders.AddSrcOver.GetType(), DefaultPixelBlenders.SubtractSrcOver.GetType(), DefaultPixelBlenders.ScreenSrcOver.GetType(), DefaultPixelBlenders.DarkenSrcOver.GetType(), DefaultPixelBlenders.LightenSrcOver.GetType(), DefaultPixelBlenders.OverlaySrcOver.GetType(), DefaultPixelBlenders.HardLightSrcOver.GetType(), DefaultPixelBlenders.ColorDodgeSrcOver.GetType(), DefaultPixelBlenders.ColorBurnSrcOver.GetType(), DefaultPixelBlenders.SoftLightSrcOver.GetType(), DefaultPixelBlenders.DifferenceSrcOver.GetType(), DefaultPixelBlenders.ExclusionSrcOver.GetType(), DefaultPixelBlenders.HueSrcOver.GetType(), DefaultPixelBlenders.SaturationSrcOver.GetType(), DefaultPixelBlenders.ColorSrcOver.GetType(), DefaultPixelBlenders.LuminositySrcOver.GetType()); return data; } } [Theory] [MemberData(nameof(BlenderMappings))] public void ReturnsCorrectBlender(TestPixel pixel, Type type, PixelColorBlendingMode mode) where TPixel : unmanaged, IPixel { PixelBlender blender = PixelOperations.Instance.GetPixelBlender(mode, PixelAlphaCompositionMode.SrcOver); Assert.IsType(type, blender); } [Theory] [MemberData(nameof(BlenderModeMappings))] public void ReturnsCorrectBlenderForAllModeCombinations(PixelColorBlendingMode colorMode, PixelAlphaCompositionMode alphaMode, Type type) { PixelBlender blender = PixelOperations.Instance.GetPixelBlender(colorMode, alphaMode); Assert.IsType(type, blender); } [Fact] public void BlendFunctionsAreCalledForAllModeCombinations() => FeatureTestRunner.RunWithHwIntrinsicsFeature( ExerciseAllBlenderModeCombinations, HwIntrinsics.AllowAll | HwIntrinsics.DisableAVX512F | HwIntrinsics.DisableAVX | HwIntrinsics.DisableHWIntrinsic); [Fact] public void AssociatedAlphaBlendFunctionsAreCalledForAllModeCombinations() => FeatureTestRunner.RunWithHwIntrinsicsFeature( ExerciseAllAssociatedAlphaBlenderModeCombinations, HwIntrinsics.AllowAll | HwIntrinsics.DisableAVX512F | HwIntrinsics.DisableAVX | HwIntrinsics.DisableHWIntrinsic); [Theory] [InlineData(PixelColorBlendingMode.ColorDodge)] [InlineData(PixelColorBlendingMode.ColorBurn)] [InlineData(PixelColorBlendingMode.SoftLight)] [InlineData(PixelColorBlendingMode.Difference)] [InlineData(PixelColorBlendingMode.Exclusion)] [InlineData(PixelColorBlendingMode.Hue)] [InlineData(PixelColorBlendingMode.Saturation)] [InlineData(PixelColorBlendingMode.Color)] [InlineData(PixelColorBlendingMode.Luminosity)] public void NewSrcOverModesMatchAcrossAlphaRepresentations(PixelColorBlendingMode colorMode) { Rgba32 background = new(220, 80, 40, 160); Rgba32 source = new(20, 180, 120, 96); PixelBlender straightBlender = PixelOperations.Instance.GetPixelBlender(colorMode, PixelAlphaCompositionMode.SrcOver); PixelBlender associatedBlender = PixelOperations.Instance.GetPixelBlender(colorMode, PixelAlphaCompositionMode.SrcOver); Rgba32 straightResult = straightBlender.Blend(background, source, .625F); Rgba32P expected = Rgba32P.FromRgba32(straightResult); Rgba32P actual = associatedBlender.Blend(Rgba32P.FromRgba32(background), Rgba32P.FromRgba32(source), .625F); // Associated storage introduces one extra byte quantization, but both paths must describe the same composite. Assert.True(Math.Abs(expected.R - actual.R) <= 1, $"{colorMode}: expected {expected}, actual {actual}"); Assert.True(Math.Abs(expected.G - actual.G) <= 1, $"{colorMode}: expected {expected}, actual {actual}"); Assert.True(Math.Abs(expected.B - actual.B) <= 1, $"{colorMode}: expected {expected}, actual {actual}"); Assert.Equal(expected.A, actual.A); } [Fact] public void PlusMatchesAcrossAlphaRepresentations() { Rgba32 background = new(220, 80, 40, 160); Rgba32 source = new(20, 180, 120, 96); PixelBlender straightBlender = PixelOperations.Instance.GetPixelBlender(PixelColorBlendingMode.Normal, PixelAlphaCompositionMode.Plus); PixelBlender associatedBlender = PixelOperations.Instance.GetPixelBlender(PixelColorBlendingMode.Normal, PixelAlphaCompositionMode.Plus); Rgba32P expected = Rgba32P.FromRgba32(straightBlender.Blend(background, source, .625F)); Rgba32P actual = associatedBlender.Blend(Rgba32P.FromRgba32(background), Rgba32P.FromRgba32(source), .625F); Assert.True(Math.Abs(expected.R - actual.R) <= 1, $"expected {expected}, actual {actual}"); Assert.True(Math.Abs(expected.G - actual.G) <= 1, $"expected {expected}, actual {actual}"); Assert.True(Math.Abs(expected.B - actual.B) <= 1, $"expected {expected}, actual {actual}"); Assert.Equal(expected.A, actual.A); } [Fact] public void AssociatedHardLightDestAtopRoundsExactMidpointAwayFromZero() => FeatureTestRunner.RunWithHwIntrinsicsFeature( RunAssociatedHardLightDestAtopRoundsExactMidpointAwayFromZero, HwIntrinsics.AllowAll | HwIntrinsics.DisableAVX512F | HwIntrinsics.DisableAVX | HwIntrinsics.DisableHWIntrinsic); private static void RunAssociatedHardLightDestAtopRoundsExactMidpointAwayFromZero() { Rgba32P background = Rgba32P.FromRgba32(new Rgba32(220, 80, 40, 160)); Rgba32P source = Rgba32P.FromRgba32(new Rgba32(20, 180, 120, 96)); PixelBlender blender = PixelOperations.Instance.GetPixelBlender(PixelColorBlendingMode.HardLight, PixelAlphaCompositionMode.DestAtop); Assert.Equal(new Rgba32P(30, 33, 16, 60), blender.Blend(background, source, .625F)); } [Fact] public void AssociatedBlendWithCoverageAppliesCoverageToColorAndAlpha() => FeatureTestRunner.RunWithHwIntrinsicsFeature( RunAssociatedBlendWithCoverageAppliesCoverageToColorAndAlpha, HwIntrinsics.AllowAll | HwIntrinsics.DisableAVX512F | HwIntrinsics.DisableAVX | HwIntrinsics.DisableHWIntrinsic); [Fact] public void BlendWithCoverageMatchesAcrossAlphaRepresentations() => FeatureTestRunner.RunWithHwIntrinsicsFeature( RunBlendWithCoverageMatchesAcrossAlphaRepresentations, HwIntrinsics.AllowAll | HwIntrinsics.DisableAVX512F | HwIntrinsics.DisableAVX | HwIntrinsics.DisableHWIntrinsic); private static void RunAssociatedBlendWithCoverageAppliesCoverageToColorAndAlpha() { PixelBlender blender = PixelOperations.Instance.GetPixelBlender(PixelColorBlendingMode.Normal, PixelAlphaCompositionMode.SrcOver); Rgba32P source = Rgba32P.FromRgba32(new Rgba32(37, 211, 89, 173)); Vector4 sourceScaled = source.ToScaledVector4(); // Seven pixels exercise one AVX-512 batch plus three tails, or three AVX2 batches plus one tail. float[] coverage = [1F, .8F, .6F, .4F, .2F, 0F, .5F]; float[] amounts = [1F, 1F, 1F, 1F, 1F, 1F, 1F]; Rgba32P[] background = new Rgba32P[7]; Rgba32P[] sourceSpan = [source, source, source, source, source, source, source]; Rgba32P[] destination = new Rgba32P[7]; Rgba32P[] expected = new Rgba32P[7]; Vector4[] sourceSpanBuffer = new Vector4[destination.Length * 3]; Vector4[] constantSourceBuffer = new Vector4[destination.Length * 2]; // Compositing over transparency reduces source-over to the source itself, so coverage must // scale the associated color and alpha lanes together: the premultiplied source times coverage. // An unassociated coverage lerp instead premultiplies and unassociates around the mix, which // cancels coverage out of the color lanes entirely at zero backdrop alpha. for (int i = 0; i < expected.Length; i++) { expected[i] = Rgba32P.FromScaledVector4(sourceScaled * coverage[i]); } blender.BlendWithCoverage(Configuration.Default, destination, background, sourceSpan, 1F, coverage, sourceSpanBuffer); Assert.Equal(expected, destination); blender.BlendWithCoverage(Configuration.Default, destination, background, source, 1F, coverage, constantSourceBuffer); Assert.Equal(expected, destination); blender.BlendWithCoverage(Configuration.Default, destination, background, sourceSpan, amounts, coverage, sourceSpanBuffer); Assert.Equal(expected, destination); blender.BlendWithCoverage(Configuration.Default, destination, background, source, amounts, coverage, constantSourceBuffer); Assert.Equal(expected, destination); } private static void RunBlendWithCoverageMatchesAcrossAlphaRepresentations() { PixelBlender straightBlender = PixelOperations.Instance.GetPixelBlender(PixelColorBlendingMode.Normal, PixelAlphaCompositionMode.SrcOver); PixelBlender associatedBlender = PixelOperations.Instance.GetPixelBlender(PixelColorBlendingMode.Normal, PixelAlphaCompositionMode.SrcOver); Rgba32[] background = [ new(0, 0, 0, 0), new(29, 83, 137, 107), new(255, 255, 255, 255), new(220, 80, 40, 160), new(10, 20, 30, 40), new(40, 200, 100, 192), new(180, 160, 20, 128), ]; Rgba32 source = new(37, 211, 89, 173); float[] coverage = [1F, .8F, .6F, .4F, .2F, 0F, .5F]; Rgba32[] straightDestination = new Rgba32[background.Length]; Rgba32P[] associatedDestination = new Rgba32P[background.Length]; Rgba32P[] associatedBackground = new Rgba32P[background.Length]; Vector4[] workingBuffer = new Vector4[background.Length * 2]; for (int i = 0; i < background.Length; i++) { associatedBackground[i] = Rgba32P.FromRgba32(background[i]); } straightBlender.BlendWithCoverage(Configuration.Default, straightDestination, background, source, 1F, coverage, workingBuffer); associatedBlender.BlendWithCoverage(Configuration.Default, associatedDestination, associatedBackground, Rgba32P.FromRgba32(source), 1F, coverage, workingBuffer); // Both destinations must describe the same composite: canonical associated bytes may differ // by one quantization step per channel while alpha is representation-independent. for (int i = 0; i < background.Length; i++) { Rgba32P expected = Rgba32P.FromRgba32(straightDestination[i]); Rgba32P actual = associatedDestination[i]; Assert.True(Math.Abs(expected.R - actual.R) <= 1, $"[{i}] expected {expected}, actual {actual}"); Assert.True(Math.Abs(expected.G - actual.G) <= 1, $"[{i}] expected {expected}, actual {actual}"); Assert.True(Math.Abs(expected.B - actual.B) <= 1, $"[{i}] expected {expected}, actual {actual}"); Assert.Equal(expected.A, actual.A); } } [Fact] public void Blend_WithConstantSourceAndSingleAmount() { PixelBlender blender = DefaultPixelBlenders.NormalSrcOver; Rgba32[] destination = new Rgba32[2]; Rgba32[] background = [ Color.Red.ToPixel(), Color.Green.ToPixel() ]; Rgba32 source = Color.Blue.ToPixel(); blender.Blend(Configuration.Default, destination, background, source, 1F); Assert.Equal(source, destination[0]); Assert.Equal(source, destination[1]); } [Fact] public void Blend_WithConstantSourceSingleAmountAndWorkingBuffer() { PixelBlender blender = DefaultPixelBlenders.NormalSrcOver; Rgba32[] destination = new Rgba32[2]; Rgba32[] background = [ Color.Red.ToPixel(), Color.Green.ToPixel() ]; Rgba32 source = Color.Blue.ToPixel(); Vector4[] workingBuffer = new Vector4[destination.Length * 2]; blender.Blend(Configuration.Default, destination, background, source, 1F, workingBuffer); Assert.Equal(source, destination[0]); Assert.Equal(source, destination[1]); } [Fact] public void Blend_WithConstantSourceAndAmountSpan() { PixelBlender blender = DefaultPixelBlenders.NormalSrcOver; Rgba32[] destination = new Rgba32[2]; Rgba32[] background = [ Color.Red.ToPixel(), Color.Green.ToPixel() ]; Rgba32 source = Color.Blue.ToPixel(); float[] amount = [1F, 1F]; blender.Blend(Configuration.Default, destination, background, source, amount); Assert.Equal(source, destination[0]); Assert.Equal(source, destination[1]); } [Fact] public void Blend_WithConstantSourceAmountSpanAndWorkingBuffer() { PixelBlender blender = DefaultPixelBlenders.NormalSrcOver; Rgba32[] destination = new Rgba32[2]; Rgba32[] background = [ Color.Red.ToPixel(), Color.Green.ToPixel() ]; Rgba32 source = Color.Blue.ToPixel(); float[] amount = [1F, 1F]; Vector4[] workingBuffer = new Vector4[destination.Length * 2]; blender.Blend(Configuration.Default, destination, background, source, amount, workingBuffer); Assert.Equal(source, destination[0]); Assert.Equal(source, destination[1]); } [Fact] public void Blend_WithSourceSpanAmountSpanAndWorkingBuffer() { PixelBlender blender = DefaultPixelBlenders.NormalSrcOver; Rgba32[] destination = new Rgba32[2]; Rgba32[] background = [ Color.Red.ToPixel(), Color.Green.ToPixel() ]; Rgba32[] source = [ Color.Blue.ToPixel(), Color.Yellow.ToPixel() ]; float[] amount = [1F, 1F]; Vector4[] workingBuffer = new Vector4[destination.Length * 3]; blender.Blend(Configuration.Default, destination, background, source, amount, workingBuffer); Assert.Equal(source[0], destination[0]); Assert.Equal(source[1], destination[1]); } [Fact] public void BlendWithCoverage_WithConstantSourceAndSingleAmount() { PixelBlender blender = DefaultPixelBlenders.NormalSrc; Rgba32[] destination = new Rgba32[3]; Rgba32[] background = [ new(255, 0, 0), new(255, 0, 0), new(255, 0, 0) ]; Rgba32 source = new(0, 0, 255); float[] coverage = [0F, .5F, 1F]; blender.BlendWithCoverage(Configuration.Default, destination, background, source, 1F, coverage); Assert.Equal(background[0], destination[0]); Assert.Equal(new Rgba32(128, 0, 128), destination[1]); Assert.Equal(source, destination[2]); } [Fact] public void BlendWithCoverage_WithConstantSourceSingleAmountAndWorkingBuffer() { PixelBlender blender = DefaultPixelBlenders.NormalSrc; Rgba32[] destination = new Rgba32[3]; Rgba32[] background = [ new(255, 0, 0), new(255, 0, 0), new(255, 0, 0) ]; Rgba32 source = new(0, 0, 255); float[] coverage = [0F, .5F, 1F]; Vector4[] workingBuffer = new Vector4[destination.Length * 2]; blender.BlendWithCoverage(Configuration.Default, destination, background, source, 1F, coverage, workingBuffer); Assert.Equal(background[0], destination[0]); Assert.Equal(new Rgba32(128, 0, 128), destination[1]); Assert.Equal(source, destination[2]); } [Fact] public void BlendWithCoverage_WithSourceSpanAndSingleAmount() { PixelBlender blender = DefaultPixelBlenders.NormalSrc; Rgba32[] destination = new Rgba32[3]; Rgba32[] background = [ new(255, 0, 0), new(0, 255, 0), new(0, 0, 255) ]; Rgba32[] source = [ new(0, 0, 255), new(255, 0, 0), new(0, 255, 0) ]; float[] coverage = [0F, .5F, 1F]; blender.BlendWithCoverage(Configuration.Default, destination, background, source, 1F, coverage); Assert.Equal(background[0], destination[0]); Assert.Equal(new Rgba32(128, 128, 0), destination[1]); Assert.Equal(source[2], destination[2]); } [Fact] public void BlendWithCoverage_WithSourceSpanSingleAmountAndWorkingBuffer() { PixelBlender blender = DefaultPixelBlenders.NormalSrc; Rgba32[] destination = new Rgba32[3]; Rgba32[] background = [ new(255, 0, 0), new(0, 255, 0), new(0, 0, 255) ]; Rgba32[] source = [ new(0, 0, 255), new(255, 0, 0), new(0, 255, 0) ]; float[] coverage = [0F, .5F, 1F]; Vector4[] workingBuffer = new Vector4[destination.Length * 3]; blender.BlendWithCoverage(Configuration.Default, destination, background, source, 1F, coverage, workingBuffer); Assert.Equal(background[0], destination[0]); Assert.Equal(new Rgba32(128, 128, 0), destination[1]); Assert.Equal(source[2], destination[2]); } [Fact] public void BlendWithCoverage_WithConstantSourceAndAmountSpan() { PixelBlender blender = DefaultPixelBlenders.NormalSrc; Rgba32[] destination = new Rgba32[3]; Rgba32[] background = [ new(255, 0, 0), new(255, 0, 0), new(255, 0, 0) ]; Rgba32 source = new(0, 0, 255); float[] amount = [1F, 1F, 1F]; float[] coverage = [0F, .5F, 1F]; blender.BlendWithCoverage(Configuration.Default, destination, background, source, amount, coverage); Assert.Equal(background[0], destination[0]); Assert.Equal(new Rgba32(128, 0, 128), destination[1]); Assert.Equal(source, destination[2]); } [Fact] public void BlendWithCoverage_WithConstantSourceAmountSpanAndWorkingBuffer() { PixelBlender blender = DefaultPixelBlenders.NormalSrc; Rgba32[] destination = new Rgba32[3]; Rgba32[] background = [ new(255, 0, 0), new(255, 0, 0), new(255, 0, 0) ]; Rgba32 source = new(0, 0, 255); float[] amount = [1F, 1F, 1F]; float[] coverage = [0F, .5F, 1F]; Vector4[] workingBuffer = new Vector4[destination.Length * 2]; blender.BlendWithCoverage(Configuration.Default, destination, background, source, amount, coverage, workingBuffer); Assert.Equal(background[0], destination[0]); Assert.Equal(new Rgba32(128, 0, 128), destination[1]); Assert.Equal(source, destination[2]); } [Fact] public void BlendWithCoverage_WithSourceSpanAndAmountSpan() { PixelBlender blender = DefaultPixelBlenders.NormalSrc; Rgba32[] destination = new Rgba32[3]; Rgba32[] background = [ new(255, 0, 0), new(0, 255, 0), new(0, 0, 255) ]; Rgba32[] source = [ new(0, 0, 255), new(255, 0, 0), new(0, 255, 0) ]; float[] amount = [1F, 1F, 1F]; float[] coverage = [0F, .5F, 1F]; blender.BlendWithCoverage(Configuration.Default, destination, background, source, amount, coverage); Assert.Equal(background[0], destination[0]); Assert.Equal(new Rgba32(128, 128, 0), destination[1]); Assert.Equal(source[2], destination[2]); } [Fact] public void BlendWithCoverage_WithSourceSpanAmountSpanAndWorkingBuffer() { PixelBlender blender = DefaultPixelBlenders.NormalSrc; Rgba32[] destination = new Rgba32[3]; Rgba32[] background = [ new(255, 0, 0), new(0, 255, 0), new(0, 0, 255) ]; Rgba32[] source = [ new(0, 0, 255), new(255, 0, 0), new(0, 255, 0) ]; float[] amount = [1F, 1F, 1F]; float[] coverage = [0F, .5F, 1F]; Vector4[] workingBuffer = new Vector4[destination.Length * 3]; blender.BlendWithCoverage(Configuration.Default, destination, background, source, amount, coverage, workingBuffer); Assert.Equal(background[0], destination[0]); Assert.Equal(new Rgba32(128, 128, 0), destination[1]); Assert.Equal(source[2], destination[2]); } public static TheoryData ColorBlendingExpectedResults = new() { { Color.MistyRose.ToPixel(), Color.MidnightBlue.ToPixel(), 1, PixelColorBlendingMode.Normal, Color.MidnightBlue.ToPixel() }, { Color.MistyRose.ToPixel(), Color.MidnightBlue.ToPixel(), 1, PixelColorBlendingMode.Screen, new Rgba32(0xFFEEE7FF) }, { Color.MistyRose.ToPixel(), Color.MidnightBlue.ToPixel(), 1, PixelColorBlendingMode.HardLight, new Rgba32(0xFFC62D32) }, { Color.MistyRose.ToPixel(), Color.MidnightBlue.ToPixel(), 1, PixelColorBlendingMode.Overlay, new Rgba32(0xFFDDCEFF) }, { Color.MistyRose.ToPixel(), Color.MidnightBlue.ToPixel(), 1, PixelColorBlendingMode.Darken, new Rgba32(0xFF701919) }, { Color.MistyRose.ToPixel(), Color.MidnightBlue.ToPixel(), 1, PixelColorBlendingMode.Lighten, new Rgba32(0xFFE1E4FF) }, { Color.MistyRose.ToPixel(), Color.MidnightBlue.ToPixel(), 1, PixelColorBlendingMode.Add, new Rgba32(0xFFFFFDFF) }, { Color.MistyRose.ToPixel(), Color.MidnightBlue.ToPixel(), 1, PixelColorBlendingMode.Subtract, new Rgba32(0xFF71CBE6) }, { Color.MistyRose.ToPixel(), Color.MidnightBlue.ToPixel(), 1, PixelColorBlendingMode.Multiply, new Rgba32(0xFF631619) }, }; [Theory] [MemberData(nameof(ColorBlendingExpectedResults))] public void TestColorBlendingModes(Rgba32 backdrop, Rgba32 source, float opacity, PixelColorBlendingMode mode, Rgba32 expectedResult) { PixelBlender blender = PixelOperations.Instance.GetPixelBlender(mode, PixelAlphaCompositionMode.SrcOver); Rgba32 actualResult = blender.Blend(backdrop, source, opacity); // var str = actualResult.Rgba.ToString("X8"); // used to extract expectedResults Assert.Equal(actualResult.ToVector4(), expectedResult.ToVector4()); } public static TheoryData AlphaCompositionExpectedResults = new() { { Color.MistyRose.ToPixel(), Color.MidnightBlue.ToPixel(), 1, PixelAlphaCompositionMode.Clear, new Rgba32(0) }, { Color.MistyRose.ToPixel(), Color.MidnightBlue.ToPixel(), 1, PixelAlphaCompositionMode.Xor, new Rgba32(0) }, { Color.MistyRose.ToPixel(), Color.MidnightBlue.ToPixel(), 1, PixelAlphaCompositionMode.Dest, Color.MistyRose.ToPixel() }, { Color.MistyRose.ToPixel(), Color.MidnightBlue.ToPixel(), 1, PixelAlphaCompositionMode.DestAtop, Color.MistyRose.ToPixel() }, { Color.MistyRose.ToPixel(), Color.MidnightBlue.ToPixel(), 1, PixelAlphaCompositionMode.DestIn, Color.MistyRose.ToPixel() }, { Color.MistyRose.ToPixel(), Color.MidnightBlue.ToPixel(), 1, PixelAlphaCompositionMode.DestOut, new Rgba32(0) }, { Color.MistyRose.ToPixel(), Color.MidnightBlue.ToPixel(), 1, PixelAlphaCompositionMode.DestOver, Color.MistyRose.ToPixel() }, { Color.MistyRose.ToPixel(), Color.MidnightBlue.ToPixel(), 1, PixelAlphaCompositionMode.Src, Color.MidnightBlue.ToPixel() }, { Color.MistyRose.ToPixel(), Color.MidnightBlue.ToPixel(), 1, PixelAlphaCompositionMode.SrcAtop, Color.MidnightBlue.ToPixel() }, { Color.MistyRose.ToPixel(), Color.MidnightBlue.ToPixel(), 1, PixelAlphaCompositionMode.SrcIn, Color.MidnightBlue.ToPixel() }, { Color.MistyRose.ToPixel(), Color.MidnightBlue.ToPixel(), 1, PixelAlphaCompositionMode.SrcOut, new Rgba32(0) }, { Color.MistyRose.ToPixel(), Color.MidnightBlue.ToPixel(), 1, PixelAlphaCompositionMode.SrcOver, Color.MidnightBlue.ToPixel() }, { new Rgba32(51, 102, 153, 77), new Rgba32(204, 51, 26, 102), .5F, PixelAlphaCompositionMode.Plus, new Rgba32(112, 82, 102, 128) }, { new Rgba32(230, 51, 26, 191), new Rgba32(204, 230, 77, 191), 1F, PixelAlphaCompositionMode.Plus, new Rgba32(255, 210, 77, 255) }, }; [Theory] [MemberData(nameof(AlphaCompositionExpectedResults))] public void TestAlphaCompositionModes(Rgba32 backdrop, Rgba32 source, float opacity, PixelAlphaCompositionMode mode, Rgba32 expectedResult) { PixelBlender blender = PixelOperations.Instance.GetPixelBlender(PixelColorBlendingMode.Normal, mode); Rgba32 actualResult = blender.Blend(backdrop, source, opacity); // var str = actualResult.Rgba.ToString("X8"); // used to extract expectedResults Assert.Equal(actualResult.ToVector4(), expectedResult.ToVector4()); } private static void AddBlenderModeMappings( TheoryData data, PixelAlphaCompositionMode alphaMode, Type normal, Type multiply, Type add, Type subtract, Type screen, Type darken, Type lighten, Type overlay, Type hardLight, Type colorDodge, Type colorBurn, Type softLight, Type difference, Type exclusion, Type hue, Type saturation, Type color, Type luminosity) { data.Add(PixelColorBlendingMode.Normal, alphaMode, normal); data.Add(PixelColorBlendingMode.Multiply, alphaMode, multiply); data.Add(PixelColorBlendingMode.Add, alphaMode, add); data.Add(PixelColorBlendingMode.Subtract, alphaMode, subtract); data.Add(PixelColorBlendingMode.Screen, alphaMode, screen); data.Add(PixelColorBlendingMode.Darken, alphaMode, darken); data.Add(PixelColorBlendingMode.Lighten, alphaMode, lighten); data.Add(PixelColorBlendingMode.Overlay, alphaMode, overlay); data.Add(PixelColorBlendingMode.HardLight, alphaMode, hardLight); data.Add(PixelColorBlendingMode.ColorDodge, alphaMode, colorDodge); data.Add(PixelColorBlendingMode.ColorBurn, alphaMode, colorBurn); data.Add(PixelColorBlendingMode.SoftLight, alphaMode, softLight); data.Add(PixelColorBlendingMode.Difference, alphaMode, difference); data.Add(PixelColorBlendingMode.Exclusion, alphaMode, exclusion); data.Add(PixelColorBlendingMode.Hue, alphaMode, hue); data.Add(PixelColorBlendingMode.Saturation, alphaMode, saturation); data.Add(PixelColorBlendingMode.Color, alphaMode, color); data.Add(PixelColorBlendingMode.Luminosity, alphaMode, luminosity); } private static void ExerciseAllBlenderModeCombinations() { foreach (PixelAlphaCompositionMode alphaMode in Enum.GetValues()) { foreach (PixelColorBlendingMode colorMode in Enum.GetValues()) { PixelBlender blender = PixelOperations.Instance.GetPixelBlender(colorMode, alphaMode); ExerciseBlender(blender); } } } private static void ExerciseAllAssociatedAlphaBlenderModeCombinations() { Rgba32P[] background = [ Rgba32P.FromRgba32(new Rgba32(220, 80, 40, 160)), Rgba32P.FromRgba32(new Rgba32(40, 200, 100, 192)), Rgba32P.FromRgba32(new Rgba32(120, 60, 230, 224)), Rgba32P.FromRgba32(new Rgba32(180, 160, 20, 128)), Rgba32P.FromRgba32(new Rgba32(30, 140, 210, 96)), ]; Rgba32P[] source = [ Rgba32P.FromRgba32(new Rgba32(20, 180, 120, 96)), Rgba32P.FromRgba32(new Rgba32(210, 30, 150, 144)), Rgba32P.FromRgba32(new Rgba32(80, 220, 40, 176)), Rgba32P.FromRgba32(new Rgba32(240, 110, 60, 208)), Rgba32P.FromRgba32(new Rgba32(100, 50, 200, 112)), ]; foreach (PixelAlphaCompositionMode alphaMode in Enum.GetValues()) { foreach (PixelColorBlendingMode colorMode in Enum.GetValues()) { PixelBlender blender = PixelOperations.Instance.GetPixelBlender(colorMode, alphaMode); AssertAssociatedBlenderMatchesScalar(blender, background, source, colorMode, alphaMode); } } } private static void AssertAssociatedBlenderMatchesScalar( PixelBlender blender, Rgba32P[] associatedBackground, Rgba32P[] associatedSource, PixelColorBlendingMode colorMode, PixelAlphaCompositionMode alphaMode) { const float amount = .625F; float[] amounts = [.125F, .375F, .625F, .875F, 1F]; float[] coverage = [.2F, .4F, .6F, .8F, 1F]; Rgba32P constantAssociatedSource = associatedSource[2]; Rgba32P[] expected = new Rgba32P[associatedBackground.Length]; Rgba32P[] actual = new Rgba32P[associatedBackground.Length]; Vector4[] actualSourceSpanBuffer = new Vector4[associatedBackground.Length * 3]; Vector4[] actualConstantSourceBuffer = new Vector4[associatedBackground.Length * 2]; for (int i = 0; i < expected.Length; i++) { expected[i] = blender.Blend(associatedBackground[i], associatedSource[i], amount); } blender.Blend(Configuration.Default, actual, associatedBackground, associatedSource, amount, actualSourceSpanBuffer); AssertRgba32PEqual(expected, actual, colorMode, alphaMode, "SourceSpanSingleAmount"); for (int i = 0; i < expected.Length; i++) { expected[i] = blender.Blend(associatedBackground[i], constantAssociatedSource, amount); } blender.Blend(Configuration.Default, actual, associatedBackground, constantAssociatedSource, amount, actualConstantSourceBuffer); AssertRgba32PEqual(expected, actual, colorMode, alphaMode, "ConstantSourceSingleAmount"); for (int i = 0; i < expected.Length; i++) { expected[i] = blender.Blend(associatedBackground[i], associatedSource[i], amounts[i]); } blender.Blend(Configuration.Default, actual, associatedBackground, associatedSource, amounts, actualSourceSpanBuffer); AssertRgba32PEqual(expected, actual, colorMode, alphaMode, "SourceSpanAmountSpan"); for (int i = 0; i < expected.Length; i++) { expected[i] = blender.Blend(associatedBackground[i], constantAssociatedSource, amounts[i]); } blender.Blend(Configuration.Default, actual, associatedBackground, constantAssociatedSource, amounts, actualConstantSourceBuffer); AssertRgba32PEqual(expected, actual, colorMode, alphaMode, "ConstantSourceAmountSpan"); for (int i = 0; i < expected.Length; i++) { expected[i] = BlendWithCoverageScalar(blender, associatedBackground[i], associatedSource[i], amount, coverage[i]); } blender.BlendWithCoverage(Configuration.Default, actual, associatedBackground, associatedSource, amount, coverage, actualSourceSpanBuffer); AssertRgba32PEqual(expected, actual, colorMode, alphaMode, "SourceSpanSingleAmountCoverage"); for (int i = 0; i < expected.Length; i++) { expected[i] = BlendWithCoverageScalar(blender, associatedBackground[i], constantAssociatedSource, amount, coverage[i]); } blender.BlendWithCoverage(Configuration.Default, actual, associatedBackground, constantAssociatedSource, amount, coverage, actualConstantSourceBuffer); AssertRgba32PEqual(expected, actual, colorMode, alphaMode, "ConstantSourceSingleAmountCoverage"); for (int i = 0; i < expected.Length; i++) { expected[i] = BlendWithCoverageScalar(blender, associatedBackground[i], associatedSource[i], amounts[i], coverage[i]); } blender.BlendWithCoverage(Configuration.Default, actual, associatedBackground, associatedSource, amounts, coverage, actualSourceSpanBuffer); AssertRgba32PEqual(expected, actual, colorMode, alphaMode, "SourceSpanAmountSpanCoverage"); for (int i = 0; i < expected.Length; i++) { expected[i] = BlendWithCoverageScalar(blender, associatedBackground[i], constantAssociatedSource, amounts[i], coverage[i]); } blender.BlendWithCoverage(Configuration.Default, actual, associatedBackground, constantAssociatedSource, amounts, coverage, actualConstantSourceBuffer); AssertRgba32PEqual(expected, actual, colorMode, alphaMode, "ConstantSourceAmountSpanCoverage"); } private static Rgba32P BlendWithCoverageScalar(PixelBlender blender, Rgba32P background, Rgba32P source, float amount, float coverage) { Span destination = stackalloc Rgba32P[1]; Span backgroundSpan = stackalloc Rgba32P[1] { background }; Span sourceSpan = stackalloc Rgba32P[1] { source }; Span coverageSpan = stackalloc float[1] { coverage }; Span buffer = stackalloc Vector4[3]; // A one-pixel span takes the scalar remainder path, providing an exact oracle for each SIMD coverage result. blender.BlendWithCoverage(Configuration.Default, destination, backgroundSpan, sourceSpan, amount, coverageSpan, buffer); return destination[0]; } private static void AssertRgba32PEqual( ReadOnlySpan expected, ReadOnlySpan actual, PixelColorBlendingMode colorMode, PixelAlphaCompositionMode alphaMode, string scenario) { for (int i = 0; i < expected.Length; i++) { Assert.True(expected[i] == actual[i], $"{colorMode}/{alphaMode}/{scenario}[{i}]: expected {expected[i]}, actual {actual[i]}"); } } private static void ExerciseBlender(PixelBlender blender) { Rgba32 background = Color.MistyRose.ToPixel(); Rgba32 source = Color.MidnightBlue.ToPixel(); ExerciseBlender(blender, background, source); } /// /// Exercises every bulk overload across vector-width boundaries and compares it with single-pixel composition. /// /// The pixel format. /// The blender under test. /// The background pixel. /// The source pixel. private static void ExerciseBlender(PixelBlender blender, TPixel background, TPixel source) where TPixel : unmanaged, IPixel { const float amount = .625F; float[] amounts = [0F, .125F, .375F, .625F, .875F, 1F, .5F]; float[] coverage = [1F, .8F, .6F, .4F, .2F, 0F, .5F]; // Seven pixels exercise one AVX-512 batch plus three tails, or three AVX2 batches plus one tail. TPixel[] destination = new TPixel[7]; TPixel[] expected = new TPixel[7]; TPixel[] backgroundSpan = [background, background, background, background, background, background, background]; TPixel[] sourceSpan = [source, source, source, source, source, source, source]; Vector4[] sourceSpanBuffer = new Vector4[destination.Length * 3]; Vector4[] constantSourceBuffer = new Vector4[destination.Length * 2]; Array.Fill(expected, blender.Blend(background, source, amount)); blender.Blend(Configuration.Default, destination, backgroundSpan, sourceSpan, amount, sourceSpanBuffer); Assert.Equal(expected, destination); blender.Blend(Configuration.Default, destination, backgroundSpan, source, amount, constantSourceBuffer); Assert.Equal(expected, destination); for (int i = 0; i < expected.Length; i++) { expected[i] = blender.Blend(background, source, amounts[i]); } blender.Blend(Configuration.Default, destination, backgroundSpan, sourceSpan, amounts, sourceSpanBuffer); Assert.Equal(expected, destination); blender.Blend(Configuration.Default, destination, backgroundSpan, source, amounts, constantSourceBuffer); Assert.Equal(expected, destination); for (int i = 0; i < expected.Length; i++) { expected[i] = BlendWithCoverageScalar(blender, background, source, amount, coverage[i]); } blender.BlendWithCoverage(Configuration.Default, destination, backgroundSpan, sourceSpan, amount, coverage, sourceSpanBuffer); Assert.Equal(expected, destination); blender.BlendWithCoverage(Configuration.Default, destination, backgroundSpan, source, amount, coverage, constantSourceBuffer); Assert.Equal(expected, destination); for (int i = 0; i < expected.Length; i++) { expected[i] = BlendWithCoverageScalar(blender, background, source, amounts[i], coverage[i]); } blender.BlendWithCoverage(Configuration.Default, destination, backgroundSpan, sourceSpan, amounts, coverage, sourceSpanBuffer); Assert.Equal(expected, destination); blender.BlendWithCoverage(Configuration.Default, destination, backgroundSpan, source, amounts, coverage, constantSourceBuffer); Assert.Equal(expected, destination); } /// /// Computes a one-pixel coverage result through the scalar remainder path. /// /// The pixel format. /// The blender under test. /// The background pixel. /// The source pixel. /// The source opacity. /// The pixel coverage. /// The blended pixel. private static TPixel BlendWithCoverageScalar(PixelBlender blender, TPixel background, TPixel source, float amount, float coverage) where TPixel : unmanaged, IPixel { Span destination = stackalloc TPixel[1]; Span backgroundSpan = stackalloc TPixel[1] { background }; Span sourceSpan = stackalloc TPixel[1] { source }; Span coverageSpan = stackalloc float[1] { coverage }; Span buffer = stackalloc Vector4[3]; blender.BlendWithCoverage(Configuration.Default, destination, backgroundSpan, sourceSpan, amount, coverageSpan, buffer); return destination[0]; } }