// 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()); 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()); 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()); 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()); 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()); 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()); 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()); 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()); 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()); 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()); 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()); 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()); 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); [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() }, }; [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) { 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); } 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]; } }