-
Notifications
You must be signed in to change notification settings - Fork 642
[performance] Port SKColor -> SKColorF conversion to managed C# #4370
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
mattleibow
merged 3 commits into
main
from
dev/perf-skcolorf-managed-convert-bab87374f015c647
Jul 20, 2026
+177
−3
Merged
Changes from all commits
Commits
Show all changes
3 commits
Select commit
Hold shift + click to select a range
920c12e
perf: port SKColor to SKColorF conversion to managed code
github-actions[bot] 93f3330
Merge branch 'main' into dev/perf-skcolorf-managed-convert-bab87374f0…
mattleibow 4f32921
test: fix SKColorF equivalence test build on .NET Framework (net48)
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
69 changes: 69 additions & 0 deletions
69
benchmarks/SkiaSharp.Benchmarks/Benchmarks/SKColorFConvertBenchmark.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,69 @@ | ||
| using System; | ||
| using BenchmarkDotNet.Attributes; | ||
|
|
||
| namespace SkiaSharp.Benchmarks; | ||
|
|
||
| // The implicit SKColor -> SKColorF conversion turns a packed 8-bit BGRA color into four normalized | ||
| // floats. It sits on hot paths for anyone feeding colors into the float pipeline: setting an | ||
| // SKRuntimeEffect color uniform (SKRuntimeEffect.cs converts SKColor -> SKColorF via this implicit | ||
| // operator) does it every frame for animated shaders, and gradient/paint code converts batches of | ||
| // colors. Because it is an implicit operator it is trivially hit inside per-frame / per-item loops. | ||
| // | ||
| // The shipped implementation used to round-trip through the native sk_color4f_from_color P/Invoke | ||
| // (an extra managed->native transition, plus an output-pointer pin, per single color). This | ||
| // benchmark compares that previous native path (Old, baseline) against the current managed port | ||
| // (New) which is a plain `byte * (1f/255f)` per channel - proven bit-identical to the native result | ||
| // by SKColorFConvertEquivalenceTest. | ||
| // | ||
| // Both variants convert the same representative batch of colors so the per-call difference is | ||
| // amortized over a realistic loop rather than lost in harness overhead. | ||
| [MemoryDiagnoser] | ||
| public unsafe class SKColorFConvertBenchmark | ||
| { | ||
| private SKColor[] colors; | ||
|
|
||
| [Params(1024)] | ||
| public int Count { get; set; } | ||
|
|
||
| [GlobalSetup] | ||
| public void Setup() | ||
| { | ||
| colors = new SKColor[Count]; | ||
| // Spread across the full 32-bit color space so no channel is constant-folded. | ||
| var seed = 0x9E3779B9u; | ||
| for (var i = 0; i < colors.Length; i++) | ||
| { | ||
| seed = seed * 1664525u + 1013904223u; | ||
| colors[i] = (SKColor)seed; | ||
| } | ||
| } | ||
|
|
||
| // New: the shipped managed conversion (implicit operator, pure managed math). | ||
| [Benchmark] | ||
| public float New() | ||
| { | ||
| var sink = 0f; | ||
| var local = colors; | ||
| for (var i = 0; i < local.Length; i++) | ||
| { | ||
| SKColorF f = local[i]; | ||
| sink += f.Red + f.Green + f.Blue + f.Alpha; | ||
| } | ||
| return sink; | ||
| } | ||
|
|
||
| // Baseline: the previous implementation that round-tripped through the native P/Invoke. | ||
| [Benchmark(Baseline = true)] | ||
| public float Old() | ||
| { | ||
| var sink = 0f; | ||
| var local = colors; | ||
| for (var i = 0; i < local.Length; i++) | ||
| { | ||
| SKColorF f; | ||
| SkiaApi.sk_color4f_from_color((uint)local[i], &f); | ||
| sink += f.Red + f.Green + f.Blue + f.Alpha; | ||
| } | ||
| 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,96 @@ | ||
| using System.Runtime.CompilerServices; | ||
| using Xunit; | ||
|
|
||
| namespace SkiaSharp.Tests | ||
| { | ||
| // Equivalence coverage for the managed port of the implicit SKColor -> SKColorF | ||
| // conversion operator (previously sk_color4f_from_color via P/Invoke). Every | ||
| // assertion compares the managed operator bit-for-bit against the native oracle. | ||
| public class SKColorFConvertEquivalenceTest : SKTest | ||
| { | ||
| // The original native path the managed operator replaces. | ||
| private static unsafe SKColorF NativeFromColor (SKColor color) | ||
| { | ||
| SKColorF f; | ||
| SkiaApi.sk_color4f_from_color ((uint)color, &f); | ||
| return f; | ||
| } | ||
|
|
||
| // BitConverter.SingleToInt32Bits is not available on .NET Framework (net48), so | ||
| // reinterpret the raw float bits directly. This is bit-identical on every runtime | ||
| // and keeps the equivalence checks exact rather than tolerance-based. | ||
| private static unsafe int SingleToBits (float value) => *(int*)&value; | ||
|
|
||
| private static void AssertBitExact (SKColor src) | ||
| { | ||
| var native = NativeFromColor (src); | ||
| SKColorF managed = src; // managed implicit operator under test | ||
|
|
||
| Assert.True ( | ||
| SingleToBits (native.Red) == SingleToBits (managed.Red) && | ||
| SingleToBits (native.Green) == SingleToBits (managed.Green) && | ||
| SingleToBits (native.Blue) == SingleToBits (managed.Blue) && | ||
| SingleToBits (native.Alpha) == SingleToBits (managed.Alpha), | ||
| $"SKColor {src}: managed ({managed.Red:R},{managed.Green:R},{managed.Blue:R},{managed.Alpha:R}) " + | ||
| $"!= native ({native.Red:R},{native.Green:R},{native.Blue:R},{native.Alpha:R})"); | ||
|
mattleibow marked this conversation as resolved.
|
||
| } | ||
|
|
||
| [Fact] | ||
| public void ManagedFromColorMatchesNativeForAllGrayscale () | ||
| { | ||
| for (var c = 0; c <= 255; c++) | ||
| AssertBitExact (new SKColor ((byte)c, (byte)c, (byte)c, (byte)c)); | ||
| } | ||
|
|
||
| [Fact] | ||
| public void ManagedFromColorMatchesNativeForEachChannelSweep () | ||
| { | ||
| // Vary one channel across its full range while holding the others at | ||
| // distinct values, so a swapped-channel port would diverge somewhere. | ||
| for (var v = 0; v <= 255; v++) { | ||
| AssertBitExact (new SKColor ((byte)v, 17, 200, 99)); | ||
| AssertBitExact (new SKColor (17, (byte)v, 200, 99)); | ||
| AssertBitExact (new SKColor (17, 200, (byte)v, 99)); | ||
| AssertBitExact (new SKColor (17, 200, 99, (byte)v)); | ||
| } | ||
| } | ||
|
|
||
| [Fact] | ||
| public void ManagedFromColorMatchesNativeForDistinctChannels () | ||
| { | ||
| var samples = new (byte r, byte g, byte b, byte a)[] { | ||
| (0, 0, 0, 0), (255, 255, 255, 255), (255, 0, 0, 255), | ||
| (0, 255, 0, 255), (0, 0, 255, 255), (10, 20, 30, 40), | ||
| (1, 2, 3, 4), (128, 64, 32, 16), (127, 129, 126, 200), | ||
| (254, 1, 253, 2), (100, 150, 200, 250), | ||
| }; | ||
| foreach (var s in samples) | ||
| AssertBitExact (new SKColor (s.r, s.g, s.b, s.a)); | ||
| } | ||
|
|
||
| [MethodImpl (MethodImplOptions.NoInlining)] | ||
| private static float NaiveDivide (int c) => c / 255f; | ||
|
|
||
| [Fact] | ||
| public void EquivalenceComparisonDetectsAWrongPort () | ||
| { | ||
| // Guard: prove the bit-exact comparison the tests above rely on has teeth. | ||
|
|
||
| // (a) A swapped R/B port (a plausible mistake given SkColor's BGRA memory | ||
| // order) produces different bits and is caught. | ||
| var src = new SKColor (10, 20, 30, 40); | ||
| var native = NativeFromColor (src); | ||
| var swapped = new SKColorF (native.Blue, native.Green, native.Red, native.Alpha); | ||
| Assert.NotEqual ( | ||
| SingleToBits (native.Red), | ||
| SingleToBits (swapped.Red)); | ||
|
|
||
| // (b) A naive divide-by-255 differs from the native *(1/255) scale at some | ||
| // inputs (e.g. 127), confirming the exact formula matters. | ||
| var grayNative = NativeFromColor (new SKColor (127, 127, 127, 127)); | ||
| Assert.NotEqual ( | ||
| SingleToBits (grayNative.Red), | ||
| SingleToBits (NaiveDivide (127))); | ||
| } | ||
| } | ||
| } | ||
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.