-
Notifications
You must be signed in to change notification settings - Fork 643
[performance] Port single-value SKPMColor premultiply/unpremultiply to managed C# #4385
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
3 commits
Select commit
Hold shift + click to select a range
f3e37be
Port single-value SKPMColor premultiply/unpremultiply to managed C#
github-actions[bot] a36a22d
Merge branch 'main' into dev/perf-skpmcolor-managed-8920b4f8bb1ce155
mattleibow 541a512
Merge branch 'main' into dev/perf-skpmcolor-managed-8920b4f8bb1ce155
mattleibow File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
108 changes: 108 additions & 0 deletions
108
benchmarks/SkiaSharp.Benchmarks/Benchmarks/SKPMColorConvertBenchmark.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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; | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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); | ||
|
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); | ||
|
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; | ||
| } | ||
| } | ||
| } | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.