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,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;
}
}
15 changes: 12 additions & 3 deletions binding/SkiaSharp/SKColorF.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 Down Expand Up @@ -250,11 +251,19 @@ public readonly void ToHsv (out float h, out float s, out float v)
public readonly override string ToString () =>
((SKColor)this).ToString ();

[MethodImpl (MethodImplOptions.AggressiveInlining)]
public static implicit operator SKColorF (SKColor color)
{
SKColorF colorF;
SkiaApi.sk_color4f_from_color ((uint)color, &colorF);
return colorF;
// Ported from the native sk_color4f_from_color (SkColor4f::FromColor):
// each 8-bit channel is scaled by 1/255 with no gamma applied. This is a
// single multiply per channel, so the managed result is bit-identical to the
// native path on every runtime (including x87) and avoids a P/Invoke per call.
const float scale = 1f / 255f;
return new SKColorF (
color.Red * scale,
color.Green * scale,
color.Blue * scale,
color.Alpha * scale);
}

public static explicit operator SKColor (SKColorF color) =>
Expand Down
96 changes: 96 additions & 0 deletions tests/Tests/SkiaSharp/SKColorFConvertEquivalenceTest.cs
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;
Comment thread
mattleibow marked this conversation as resolved.
}

// 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})");
Comment thread
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)));
}
}
}
Loading