Browse Source

Use associated alpha blending with coverage

Route BlendWithCoverage calls through AssociatedAlphaPorterDuffFunctions when TOperator.IsAssociatedAlpha is true, across all SIMD widths (scalar, Vector256, Vector512) and all BlendWithCoverageFunction overloads.
pull/3162/head
James Jackson-South 2 weeks ago
parent
commit
b81b9a2792
  1. 52
      src/ImageSharp/PixelFormats/PixelBlenders/PixelBlender{TPixel,TOperator}.cs
  2. 96
      tests/ImageSharp.Tests/PixelFormats/PixelBlenderTests.cs

52
src/ImageSharp/PixelFormats/PixelBlenders/PixelBlender{TPixel,TOperator}.cs

@ -315,7 +315,9 @@ internal sealed class PixelBlender<TPixel, TOperator> : PixelBlender<TPixel>
Vector512<float> coverageVector = CreateClampedVector512(ref coverageBase);
Vector512<float> blended = TOperator.Invoke(backgroundVector, Unsafe.Add(ref sourceBase, i), amountVector);
Unsafe.Add(ref destinationBase, i) = PorterDuffFunctions.BlendWithCoverage(backgroundVector, blended, coverageVector);
Unsafe.Add(ref destinationBase, i) = TOperator.IsAssociatedAlpha
? AssociatedAlphaPorterDuffFunctions.BlendWithCoverage(backgroundVector, blended, coverageVector)
: PorterDuffFunctions.BlendWithCoverage(backgroundVector, blended, coverageVector);
}
scalarStart = vectorCount * 4;
@ -335,7 +337,9 @@ internal sealed class PixelBlender<TPixel, TOperator> : PixelBlender<TPixel>
Vector256<float> coverageVector = CreateClampedVector256(ref coverageBase);
Vector256<float> blended = TOperator.Invoke(backgroundVector, Unsafe.Add(ref sourceBase, i), amountVector);
Unsafe.Add(ref destinationBase, i) = PorterDuffFunctions.BlendWithCoverage(backgroundVector, blended, coverageVector);
Unsafe.Add(ref destinationBase, i) = TOperator.IsAssociatedAlpha
? AssociatedAlphaPorterDuffFunctions.BlendWithCoverage(backgroundVector, blended, coverageVector)
: PorterDuffFunctions.BlendWithCoverage(backgroundVector, blended, coverageVector);
}
scalarStart = vectorCount * 2;
@ -346,7 +350,10 @@ internal sealed class PixelBlender<TPixel, TOperator> : PixelBlender<TPixel>
Vector4 backgroundPixel = Unsafe.Add(ref backgroundRef, (uint)i);
Vector4 blended = TOperator.Invoke(backgroundPixel, Unsafe.Add(ref sourceRef, (uint)i), amount);
Unsafe.Add(ref destinationRef, (uint)i) = PorterDuffFunctions.BlendWithCoverage(backgroundPixel, blended, Numerics.Clamp(Unsafe.Add(ref coverageRef, (uint)i), 0, 1F));
float clampedCoverage = Numerics.Clamp(Unsafe.Add(ref coverageRef, (uint)i), 0, 1F);
Unsafe.Add(ref destinationRef, (uint)i) = TOperator.IsAssociatedAlpha
? AssociatedAlphaPorterDuffFunctions.BlendWithCoverage(backgroundPixel, blended, clampedCoverage)
: PorterDuffFunctions.BlendWithCoverage(backgroundPixel, blended, clampedCoverage);
}
}
@ -376,7 +383,9 @@ internal sealed class PixelBlender<TPixel, TOperator> : PixelBlender<TPixel>
Vector512<float> coverageVector = CreateClampedVector512(ref coverageBase);
Vector512<float> blended = TOperator.Invoke(backgroundVector, sourceVector, amountVector);
Unsafe.Add(ref destinationBase, i) = PorterDuffFunctions.BlendWithCoverage(backgroundVector, blended, coverageVector);
Unsafe.Add(ref destinationBase, i) = TOperator.IsAssociatedAlpha
? AssociatedAlphaPorterDuffFunctions.BlendWithCoverage(backgroundVector, blended, coverageVector)
: PorterDuffFunctions.BlendWithCoverage(backgroundVector, blended, coverageVector);
}
scalarStart = vectorCount * 4;
@ -396,7 +405,9 @@ internal sealed class PixelBlender<TPixel, TOperator> : PixelBlender<TPixel>
Vector256<float> coverageVector = CreateClampedVector256(ref coverageBase);
Vector256<float> blended = TOperator.Invoke(backgroundVector, sourceVector, amountVector);
Unsafe.Add(ref destinationBase, i) = PorterDuffFunctions.BlendWithCoverage(backgroundVector, blended, coverageVector);
Unsafe.Add(ref destinationBase, i) = TOperator.IsAssociatedAlpha
? AssociatedAlphaPorterDuffFunctions.BlendWithCoverage(backgroundVector, blended, coverageVector)
: PorterDuffFunctions.BlendWithCoverage(backgroundVector, blended, coverageVector);
}
scalarStart = vectorCount * 2;
@ -407,7 +418,10 @@ internal sealed class PixelBlender<TPixel, TOperator> : PixelBlender<TPixel>
Vector4 backgroundPixel = Unsafe.Add(ref backgroundRef, (uint)i);
Vector4 blended = TOperator.Invoke(backgroundPixel, source, amount);
Unsafe.Add(ref destinationRef, (uint)i) = PorterDuffFunctions.BlendWithCoverage(backgroundPixel, blended, Numerics.Clamp(Unsafe.Add(ref coverageRef, (uint)i), 0, 1F));
float clampedCoverage = Numerics.Clamp(Unsafe.Add(ref coverageRef, (uint)i), 0, 1F);
Unsafe.Add(ref destinationRef, (uint)i) = TOperator.IsAssociatedAlpha
? AssociatedAlphaPorterDuffFunctions.BlendWithCoverage(backgroundPixel, blended, clampedCoverage)
: PorterDuffFunctions.BlendWithCoverage(backgroundPixel, blended, clampedCoverage);
}
}
@ -439,7 +453,9 @@ internal sealed class PixelBlender<TPixel, TOperator> : PixelBlender<TPixel>
Vector512<float> coverageVector = CreateClampedVector512(ref coverageBase);
Vector512<float> blended = TOperator.Invoke(backgroundVector, Unsafe.Add(ref sourceBase, i), amountVector);
Unsafe.Add(ref destinationBase, i) = PorterDuffFunctions.BlendWithCoverage(backgroundVector, blended, coverageVector);
Unsafe.Add(ref destinationBase, i) = TOperator.IsAssociatedAlpha
? AssociatedAlphaPorterDuffFunctions.BlendWithCoverage(backgroundVector, blended, coverageVector)
: PorterDuffFunctions.BlendWithCoverage(backgroundVector, blended, coverageVector);
}
scalarStart = vectorCount * 4;
@ -460,7 +476,9 @@ internal sealed class PixelBlender<TPixel, TOperator> : PixelBlender<TPixel>
Vector256<float> coverageVector = CreateClampedVector256(ref coverageBase);
Vector256<float> blended = TOperator.Invoke(backgroundVector, Unsafe.Add(ref sourceBase, i), amountVector);
Unsafe.Add(ref destinationBase, i) = PorterDuffFunctions.BlendWithCoverage(backgroundVector, blended, coverageVector);
Unsafe.Add(ref destinationBase, i) = TOperator.IsAssociatedAlpha
? AssociatedAlphaPorterDuffFunctions.BlendWithCoverage(backgroundVector, blended, coverageVector)
: PorterDuffFunctions.BlendWithCoverage(backgroundVector, blended, coverageVector);
}
scalarStart = vectorCount * 2;
@ -471,7 +489,10 @@ internal sealed class PixelBlender<TPixel, TOperator> : PixelBlender<TPixel>
Vector4 backgroundPixel = Unsafe.Add(ref backgroundRef, (uint)i);
Vector4 blended = TOperator.Invoke(backgroundPixel, Unsafe.Add(ref sourceRef, (uint)i), Numerics.Clamp(Unsafe.Add(ref amountRef, (uint)i), 0, 1F));
Unsafe.Add(ref destinationRef, (uint)i) = PorterDuffFunctions.BlendWithCoverage(backgroundPixel, blended, Numerics.Clamp(Unsafe.Add(ref coverageRef, (uint)i), 0, 1F));
float clampedCoverage = Numerics.Clamp(Unsafe.Add(ref coverageRef, (uint)i), 0, 1F);
Unsafe.Add(ref destinationRef, (uint)i) = TOperator.IsAssociatedAlpha
? AssociatedAlphaPorterDuffFunctions.BlendWithCoverage(backgroundPixel, blended, clampedCoverage)
: PorterDuffFunctions.BlendWithCoverage(backgroundPixel, blended, clampedCoverage);
}
}
@ -502,7 +523,9 @@ internal sealed class PixelBlender<TPixel, TOperator> : PixelBlender<TPixel>
Vector512<float> coverageVector = CreateClampedVector512(ref coverageBase);
Vector512<float> blended = TOperator.Invoke(backgroundVector, sourceVector, amountVector);
Unsafe.Add(ref destinationBase, i) = PorterDuffFunctions.BlendWithCoverage(backgroundVector, blended, coverageVector);
Unsafe.Add(ref destinationBase, i) = TOperator.IsAssociatedAlpha
? AssociatedAlphaPorterDuffFunctions.BlendWithCoverage(backgroundVector, blended, coverageVector)
: PorterDuffFunctions.BlendWithCoverage(backgroundVector, blended, coverageVector);
}
scalarStart = vectorCount * 4;
@ -523,7 +546,9 @@ internal sealed class PixelBlender<TPixel, TOperator> : PixelBlender<TPixel>
Vector256<float> coverageVector = CreateClampedVector256(ref coverageBase);
Vector256<float> blended = TOperator.Invoke(backgroundVector, sourceVector, amountVector);
Unsafe.Add(ref destinationBase, i) = PorterDuffFunctions.BlendWithCoverage(backgroundVector, blended, coverageVector);
Unsafe.Add(ref destinationBase, i) = TOperator.IsAssociatedAlpha
? AssociatedAlphaPorterDuffFunctions.BlendWithCoverage(backgroundVector, blended, coverageVector)
: PorterDuffFunctions.BlendWithCoverage(backgroundVector, blended, coverageVector);
}
scalarStart = vectorCount * 2;
@ -534,7 +559,10 @@ internal sealed class PixelBlender<TPixel, TOperator> : PixelBlender<TPixel>
Vector4 backgroundPixel = Unsafe.Add(ref backgroundRef, (uint)i);
Vector4 blended = TOperator.Invoke(backgroundPixel, source, Numerics.Clamp(Unsafe.Add(ref amountRef, (uint)i), 0, 1F));
Unsafe.Add(ref destinationRef, (uint)i) = PorterDuffFunctions.BlendWithCoverage(backgroundPixel, blended, Numerics.Clamp(Unsafe.Add(ref coverageRef, (uint)i), 0, 1F));
float clampedCoverage = Numerics.Clamp(Unsafe.Add(ref coverageRef, (uint)i), 0, 1F);
Unsafe.Add(ref destinationRef, (uint)i) = TOperator.IsAssociatedAlpha
? AssociatedAlphaPorterDuffFunctions.BlendWithCoverage(backgroundPixel, blended, clampedCoverage)
: PorterDuffFunctions.BlendWithCoverage(backgroundPixel, blended, clampedCoverage);
}
}

96
tests/ImageSharp.Tests/PixelFormats/PixelBlenderTests.cs

@ -243,6 +243,102 @@ public class PixelBlenderTests
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<Rgba32P> blender = PixelOperations<Rgba32P>.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<Rgba32P>(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<Rgba32P>(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<Rgba32> straightBlender = PixelOperations<Rgba32>.Instance.GetPixelBlender(PixelColorBlendingMode.Normal, PixelAlphaCompositionMode.SrcOver);
PixelBlender<Rgba32P> associatedBlender = PixelOperations<Rgba32P>.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()
{

Loading…
Cancel
Save