Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
using BenchmarkDotNet.Attributes;

namespace SkiaSharp.Benchmarks;

// SKPMColor.PreMultiply(SKColor) / UnPreMultiply(SKPMColor) convert a single colour to and from
// premultiplied form. Callers that composite or convert pixels one colour at a time (gradient
// ramps, palette expansion, per-pixel software blends) hit these in a tight loop, so the per-call
// cost dominates.
//
// The shipped implementation used to round-trip every single conversion through a native P/Invoke
// (SkiaApi.sk_color_premultiply / sk_color_unpremultiply) even though the work is a few integer
// ops. New is the managed port (integer-only, bit-exact with the native result — proven by
// SKPMColorEquivalenceTest); Old is the previous native call, reproduced verbatim as the baseline
// so both are measured in the same process/TFM.
//
// [Params] covers a small palette (256) and a 64x64 pixel tile (4096); alpha varies across the
// batch so the a != 255 premultiply branch is exercised realistically.
[MemoryDiagnoser]
public class SKPMColorPreMultiplyBenchmark
{
[Params(256, 4096)]
public int N { get; set; }

private SKColor[] colors;

[GlobalSetup]
public void GlobalSetup()
{
colors = new SKColor[N];
for (var i = 0; i < N; i++)
{
// Spread alpha and channels so most colours take the a != 255 (real premultiply) path.
var a = (byte)((i * 7) & 0xFF);
var r = (byte)((i * 13) & 0xFF);
var g = (byte)((i * 29) & 0xFF);
var b = (byte)((i * 53) & 0xFF);
colors[i] = new SKColor(r, g, b, a);
}
}

// New: the shipped managed integer implementation.
[Benchmark]
public uint New()
{
uint sink = 0;
var src = colors;
for (var i = 0; i < src.Length; i++)
sink ^= (uint)SKPMColor.PreMultiply(src[i]);
return sink;
}

// Baseline: the previous path — one native P/Invoke per colour.
[Benchmark(Baseline = true)]
public uint Old()
{
uint sink = 0;
var src = colors;
for (var i = 0; i < src.Length; i++)
sink ^= SkiaApi.sk_color_premultiply((uint)src[i]);
return sink;
}
}

[MemoryDiagnoser]
public class SKPMColorUnPreMultiplyBenchmark
{
[Params(256, 4096)]
public int N { get; set; }

private SKPMColor[] pmcolors;

[GlobalSetup]
public void GlobalSetup()
{
pmcolors = new SKPMColor[N];
for (var i = 0; i < N; i++)
{
var a = (byte)((i * 7) & 0xFF);
var r = (byte)((i * 13) & 0xFF);
var g = (byte)((i * 29) & 0xFF);
var b = (byte)((i * 53) & 0xFF);
// Build a valid premultiplied colour to unpremultiply back.
pmcolors[i] = SKPMColor.PreMultiply(new SKColor(r, g, b, a));
}
}

// New: the shipped managed integer implementation.
[Benchmark]
public uint New()
{
uint sink = 0;
var src = pmcolors;
for (var i = 0; i < src.Length; i++)
sink ^= (uint)(SKColor)SKPMColor.UnPreMultiply(src[i]);
return sink;
}

// Baseline: the previous path — one native P/Invoke per colour.
[Benchmark(Baseline = true)]
public uint Old()
{
uint sink = 0;
var src = pmcolors;
for (var i = 0; i < src.Length; i++)
sink ^= SkiaApi.sk_color_unpremultiply((uint)src[i]);
return sink;
}
}
67 changes: 63 additions & 4 deletions binding/SkiaSharp/SKPMColor.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
#nullable disable

using System;
using System.Runtime.CompilerServices;

namespace SkiaSharp
{
Expand All @@ -20,8 +21,27 @@ public SKPMColor (uint value)

// PreMultiply

public static SKPMColor PreMultiply (SKColor color) =>
SkiaApi.sk_color_premultiply ((uint)color);
public static SKPMColor PreMultiply (SKColor color)
{
uint a = color.Alpha;
uint r = color.Red;
uint g = color.Green;
uint b = color.Blue;

// Opaque colors are unchanged; MulDiv255Round (c, 255) == c for every c.
if (a != 255) {
r = MulDiv255Round (r, a);
g = MulDiv255Round (g, a);
b = MulDiv255Round (b, a);
}

var pmcolor =
(a << SKImageInfo.PlatformColorAlphaShift) |
(r << SKImageInfo.PlatformColorRedShift) |
(g << SKImageInfo.PlatformColorGreenShift) |
(b << SKImageInfo.PlatformColorBlueShift);
return new SKPMColor (pmcolor);
}

public static SKPMColor[] PreMultiply (SKColor[] colors)
{
Expand All @@ -35,8 +55,16 @@ public static SKPMColor[] PreMultiply (SKColor[] colors)

// UnPreMultiply

public static SKColor UnPreMultiply (SKPMColor pmcolor) =>
SkiaApi.sk_color_unpremultiply ((uint)pmcolor);
public static SKColor UnPreMultiply (SKPMColor pmcolor)
{
uint a = pmcolor.Alpha;
var scale = UnPreMultiplyScale (a);
return new SKColor (
(byte)ApplyUnPreMultiplyScale (scale, pmcolor.Red),
(byte)ApplyUnPreMultiplyScale (scale, pmcolor.Green),
(byte)ApplyUnPreMultiplyScale (scale, pmcolor.Blue),
(byte)a);
}

public static SKColor[] UnPreMultiply (SKPMColor[] pmcolors)
{
Expand All @@ -54,6 +82,37 @@ public static explicit operator SKPMColor (SKColor color) =>
public static explicit operator SKColor (SKPMColor color) =>
SKPMColor.UnPreMultiply (color);

// Managed replicas of Skia's SkMulDiv255Round / SkUnPreMultiply::ApplyScale
// (skia/src/core/SkColorData.h and skia/src/core/SkUnPreMultiply.cpp),
// kept bit-exact with the native sk_color_(un)premultiply for every input.

[MethodImpl (MethodImplOptions.AggressiveInlining)]
private static uint MulDiv255Round (uint value, uint alpha)
{
var prod = value * alpha + 128;
return (prod + (prod >> 8)) >> 8;
}

private static readonly uint[] unpremultiplyScale = CreateUnPreMultiplyScaleTable ();

private static uint[] CreateUnPreMultiplyScaleTable ()
{
// Mirrors Skia's SkUnPreMultiply::gTable: round((255 << 24) / alpha), computed once.
// Replaces a per-call 32-bit divide with a table lookup, matching the native path.
var table = new uint[256];
for (uint a = 1; a < 256; a++)
table[a] = ((255u << 24) + (a >> 1)) / a;
Comment thread
mattleibow marked this conversation as resolved.
return table;
}
Comment thread
mattleibow marked this conversation as resolved.

[MethodImpl (MethodImplOptions.AggressiveInlining)]
private static uint UnPreMultiplyScale (uint alpha) =>
unpremultiplyScale[alpha & 0xff];
Comment thread
mattleibow marked this conversation as resolved.

[MethodImpl (MethodImplOptions.AggressiveInlining)]
private static uint ApplyUnPreMultiplyScale (uint scale, uint component) =>
unchecked ((scale * component + (1u << 23)) >> 24);

public readonly override string ToString () =>
$"#{Alpha:x2}{Red:x2}{Green:x2}{Blue:x2}";

Expand Down
184 changes: 184 additions & 0 deletions tests/Tests/SkiaSharp/SKPMColorEquivalenceTest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
using System;
using System.Collections.Generic;
using Xunit;

namespace SkiaSharp.Tests
{
// Equivalence proof for the managed port of the single-value SKPMColor premultiply /
// unpremultiply conversions (see SKPMColor.PreMultiply(SKColor) / UnPreMultiply(SKPMColor)).
//
// The native oracle is SkiaApi.sk_color_premultiply / sk_color_unpremultiply (the exact code
// the binding called before the port). Both conversions are per-channel maps parameterised
// only by alpha, so sweeping (alpha 0..255) x (channel 0..255) for each of the three colour
// channel positions is a COMPLETE, bit-exact enumeration of every reachable behaviour — with
// distinct sentinels in the other channels so a swapped/mis-shifted channel is also caught.
// The exhaustive tests drive the PUBLIC API (SKPMColor.PreMultiply / UnPreMultiply) so they
// guard the shipped managed implementation directly, not a copy of it.
public class SKPMColorEquivalenceTest : SKTest
{
// Distinct sentinels (all different) placed in the non-swept channels so the test also
// pins the packing order, not just the per-channel scale.
private const uint R0 = 0x11;
private const uint G0 = 0x77;
private const uint B0 = 0xCC;

// ---- Helpers used only by the deliberately-wrong "teeth" guards below ----

private static uint UnPreMultiplyScale (uint alpha) =>
// Skia stores round((255 << 24) / alpha), not the floor: the +(alpha >> 1) bias makes
// the port bit-exact even for out-of-gamut inputs where a colour channel exceeds alpha.
alpha == 0 ? 0u : ((255u << 24) + (alpha >> 1)) / alpha;

private static uint ApplyUnPreMultiplyScale (uint scale, uint component) =>
// 32-bit multiply wraps exactly like the native uint32_t path; the >> 24 of a 32-bit
// value is always <= 255, so the packing never bleeds across channels.
unchecked ((scale * component + (1u << 23)) >> 24);

// ---- Exhaustive equivalence against the native oracle ----

[Fact]
public void ManagedPreMultiplyMatchesNativeForEveryAlphaAndChannel ()
{
var failures = new List<string> ();

for (uint a = 0; a <= 255; a++) {
for (uint v = 0; v <= 255; v++) {
CheckPreMultiply (failures, (a << 24) | (v << 16) | (G0 << 8) | B0); // sweep R
CheckPreMultiply (failures, (a << 24) | (R0 << 16) | (v << 8) | B0); // sweep G
CheckPreMultiply (failures, (a << 24) | (R0 << 16) | (G0 << 8) | v); // sweep B
}
}

Assert.True (failures.Count == 0, FormatFailures ("premultiply", failures));
}

[Fact]
public void ManagedUnPreMultiplyMatchesNativeForEveryAlphaAndChannel ()
{
var aShift = SKImageInfo.PlatformColorAlphaShift;
var rShift = SKImageInfo.PlatformColorRedShift;
var gShift = SKImageInfo.PlatformColorGreenShift;
var bShift = SKImageInfo.PlatformColorBlueShift;

var failures = new List<string> ();

for (uint a = 0; a <= 255; a++) {
for (uint v = 0; v <= 255; v++) {
CheckUnPreMultiply (failures, (a << aShift) | (v << rShift) | (G0 << gShift) | (B0 << bShift));
CheckUnPreMultiply (failures, (a << aShift) | (R0 << rShift) | (v << gShift) | (B0 << bShift));
CheckUnPreMultiply (failures, (a << aShift) | (R0 << rShift) | (G0 << gShift) | (v << bShift));
}
}

Assert.True (failures.Count == 0, FormatFailures ("unpremultiply", failures));
}

private static void CheckPreMultiply (List<string> failures, uint color)
{
var expected = SkiaApi.sk_color_premultiply (color);
var actual = (uint)SKPMColor.PreMultiply ((SKColor)color);
if (expected != actual && failures.Count < 16)
failures.Add ($"color=0x{color:X8}: native=0x{expected:X8} managed=0x{actual:X8}");
}

private static void CheckUnPreMultiply (List<string> failures, uint pmcolor)
{
var expected = SkiaApi.sk_color_unpremultiply (pmcolor);
var actual = (uint)(SKColor)SKPMColor.UnPreMultiply ((SKPMColor)pmcolor);
if (expected != actual && failures.Count < 16)
failures.Add ($"pm=0x{pmcolor:X8}: native=0x{expected:X8} managed=0x{actual:X8}");
}

private static string FormatFailures (string op, List<string> failures) =>
failures.Count == 0
? string.Empty
: $"{op} diverged from native on {failures.Count}+ inputs:\n " + string.Join ("\n ", failures);
Comment thread
mattleibow marked this conversation as resolved.

// ---- Round trip: premultiply then unpremultiply recovers the original (opaque) colour ----

[Fact]
public void PreMultiplyRoundTripsForOpaqueColors ()
{
for (uint r = 0; r <= 255; r += 7) {
for (uint g = 0; g <= 255; g += 11) {
for (uint b = 0; b <= 255; b += 13) {
var color = new SKColor ((byte)r, (byte)g, (byte)b, 255);
var pm = SKPMColor.PreMultiply (color);
Assert.Equal (color, SKPMColor.UnPreMultiply (pm));
}
}
}
}

// ---- Guard: prove the exhaustive comparison actually has teeth ----
//
// A deliberately-wrong premultiply (drops the rounding bias) and a deliberately-wrong
// unpremultiply (swaps two channels) must BOTH be detected by the same oracle comparison,
// otherwise a green run above would be meaningless.

[Fact]
public void ExhaustiveComparisonCatchesAWrongPreMultiply ()
{
var caught = false;
for (uint a = 0; a <= 255 && !caught; a++) {
for (uint v = 0; v <= 255 && !caught; v++) {
var color = (a << 24) | (v << 16) | (G0 << 8) | B0;
if (SkiaApi.sk_color_premultiply (color) != WrongPreMultiply (color))
caught = true;
}
}
Assert.True (caught, "A knowingly-wrong premultiply was NOT detected — the oracle comparison lacks teeth.");
}

[Fact]
public void ExhaustiveComparisonCatchesAWrongUnPreMultiply ()
{
var aShift = SKImageInfo.PlatformColorAlphaShift;
var rShift = SKImageInfo.PlatformColorRedShift;
var gShift = SKImageInfo.PlatformColorGreenShift;
var bShift = SKImageInfo.PlatformColorBlueShift;

var caught = false;
for (uint a = 1; a <= 255 && !caught; a++) {
for (uint v = 0; v <= 255 && !caught; v++) {
var pm = (a << aShift) | (v << rShift) | (Math.Min (G0, a) << gShift) | (Math.Min (B0, a) << bShift);
Comment thread
mattleibow marked this conversation as resolved.
if (SkiaApi.sk_color_unpremultiply (pm) != WrongUnPreMultiply (pm))
caught = true;
}
}
Assert.True (caught, "A knowingly-wrong unpremultiply was NOT detected — the oracle comparison lacks teeth.");
}

private static uint WrongPreMultiply (uint color)
{
// The correct premultiply but with the +128 rounding bias dropped -> off-by-one on many inputs.
uint a = (color >> 24) & 0xff;
uint r = (color >> 16) & 0xff;
uint g = (color >> 8) & 0xff;
uint b = color & 0xff;
if (a != 255) {
r = (r * a) / 255;
g = (g * a) / 255;
b = (b * a) / 255;
}
return (a << SKImageInfo.PlatformColorAlphaShift) |
(r << SKImageInfo.PlatformColorRedShift) |
(g << SKImageInfo.PlatformColorGreenShift) |
(b << SKImageInfo.PlatformColorBlueShift);
}

private static uint WrongUnPreMultiply (uint pmcolor)
{
// The correct unpremultiply but with red and blue swapped on output.
uint a = (pmcolor >> SKImageInfo.PlatformColorAlphaShift) & 0xff;
uint r = (pmcolor >> SKImageInfo.PlatformColorRedShift) & 0xff;
uint g = (pmcolor >> SKImageInfo.PlatformColorGreenShift) & 0xff;
uint b = (pmcolor >> SKImageInfo.PlatformColorBlueShift) & 0xff;
uint scale = UnPreMultiplyScale (a);
r = ApplyUnPreMultiplyScale (scale, r);
g = ApplyUnPreMultiplyScale (scale, g);
b = ApplyUnPreMultiplyScale (scale, b);
return (a << 24) | (b << 16) | (g << 8) | r;
}
}
}
Loading