Skip to content
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

API Proposal: Add blittable Color to System.Numerics #48615

Open
Tracked by #47244
JeremyKuhne opened this issue Feb 22, 2021 · 71 comments · May be fixed by #106575
Open
Tracked by #47244

API Proposal: Add blittable Color to System.Numerics #48615

JeremyKuhne opened this issue Feb 22, 2021 · 71 comments · May be fixed by #106575
Assignees
Labels
api-approved API was approved in API review, it can be implemented area-System.Numerics in-pr There is an active PR which will close this issue when it is merged
Milestone

Comments

@JeremyKuhne
Copy link
Member

JeremyKuhne commented Feb 22, 2021

Background and Motivation

System.Drawing.Color contains extra metadata that makes it unusable in interop scenarios and inefficient in arrays.

namespace System.Drawing
{
    public readonly struct Color : IEquatable<Color>
    {
        private readonly string? name;
        private readonly long value;
        private readonly short knownColor;
        private readonly short state;
    }
}

Color is also mutable, despite being readonly. If it is constructed from a system KnownColor value, it always looks up the value on every access.

In order to facilitate exchange of raw color data throughout the .NET ecosystem and external libraries we should expose common color types that only contain raw color data.

The intent is to follow up with these base types to provide a basic set of the most common functionality needed around colors as described here: #48615 (comment)

The intent is NOT to start building a general purpose image manipulation library. Libraries such as ImageSharp are the right answer for this.

WinForms and System.Drawing will be able to leverage this for performance, which was the original driver for this request.

// Native Win32 definitions

// GDI
typedef DWORD COLORREF;
// GDI+
typedef DWORD ARGB;

Proposed API

using System;

namespace System.Drawing
{
    partial struct Color : IEquatable<Color>
    {
        public static Color FromArgb(System.Numerics.Colors.Argb<byte> argb);
        public static implicit operator Color(System.Numerics.Colors.Argb<byte> argb);

        // ToNumericsArgb? ToArgbNumerics?
        public System.Numerics.Colors.Argb<byte> ToArgbValue();
        public static explicit operator System.Numerics.Colors.Argb<byte>(in Color color);
    }
}

// WPF
namespace System.Windows.Media
{
    public struct Color : IEquatable<Color>, IFormattable
    {
        public static Color FromArgb(System.Numerics.Colors.Argb<byte> argb);
        public static implicit operator Color(System.Numerics.Colors.Argb<byte> argb);
        public static Color FromArgb(System.Numerics.Colors.Argb<float> argb);
        public static implicit operator Color(System.Numerics.Colors.Argb<float> argb);

        public System.Numerics.Colors.Argb<byte> ToArgbValue();
        public static explicit operator System.Numerics.Colors.Argb<byte>(in Color color);
        public System.Numerics.Colors.Argb<float> ToArgbValue();
        public static explicit operator System.Numerics.Colors.Argb<float>(in Color color);
    }
}

namespace System.Numerics.Colors
{
    public static class Argb
    {
        public static Argb<byte> CreateBigEndian(uint color);
        public static Argb<byte> CreateLittleEndian(uint color);
        public static uint ToUInt32LittleEndian(this Argb<byte> color);
        public static uint ToUInt32BigEndian(this Argb<byte> color);
    }

    public readonly struct Argb<T> : IEquatable<Argb<T>>, IFormattable, ISpanFormattable where T : struct
    {
        public T A { get; }
        public T R { get; }
        public T G { get; }
        public T B { get; }

        public Argb(T a, T r, T g, T b);
        public Argb(ReadOnlySpan<T> values);
        public void CopyTo(Span<T> destination);
        public bool Equals(Argb<T> other);
        public string ToString(string format, IFormatProvider formatProvider);
        // whatever ISpanFormattable says
        public Rgba<T> ToRgba();
    }

    public static class Rgba
    {
        public static Rgba<byte> CreateLittleEndian(uint color);
        public static Rgba<byte> CreateBigEndian(uint color);
        public static uint ToUInt32LittleEndian(this Rgba<byte> color);
        public static uint ToUInt32BigEndian(this Rgba<byte> color);
    }

    public readonly struct Rgba<T> : IEquatable<Rgba<T>>, IFormattable, ISpanFormattable where T : struct
    {
        public T R { get; }
        public T G { get; }
        public T B { get; }
        public T A { get; }

        public Rgba(T r, T g, T b, T a);
        public Rgba(ReadOnlySpan<T> values);
        public void CopyTo(Span<T> destination);
        public bool Equals(Rgba<T> other);
        public string ToString(string format, IFormatProvider formatProvider);
        // whatever ISpanFormattable says
        public Argb<T> ToArgb();
    }
}

Original Proposal

    public readonly struct ColorArgb
    {
        // Layout matches GDI COLORREF and GDI+ ARGB
        public uint Value { get; }
        public byte A => (byte)(Value >> 24);
        public byte R => (byte)(Value >> 16);
        public byte G => (byte)(Value >> 8);
        public byte B => (byte)(Value);

        public ColorArgb(uint value) => Value = value;
        public ColorArgb(byte r, byte g, byte b) : this(r, g, b, byte.MaxValue) { }
        public ColorArgb(byte r, byte g, byte b, byte a) => Value = (uint)(a << 24 | r << 16 | g << 8 | b);
        public ColorArgb(KnownColor knownColor) => Value = KnownColorTable.KnownColorToArgb(knownColor);

        public ColorArgb(Color color) => Value = (uint)color.ToArgb();

        // Color has these 3 methods.
        public float GetHue() => 0;
        public float GetSaturation() => 0;
        public float GetBrightness() => 0;

        // Frequently checked as only GDI+ supports transparency.
        public bool HasTransparency => A != byte.MaxValue;
        public bool FullyTransparent => A == 0;

        public static implicit operator Color(ColorArgb color) => Color.FromArgb((int)color.Value);
        public static implicit operator ARGB(ColorArgb color) => (ARGB)color.Value;
        public static implicit operator ColorArgb(ARGB argb) => new ColorArgb((uint)argb);
        public static implicit operator ColorArgb(Color color) => new ColorArgb(color);
        public static implicit operator ColorArgb(KnownColor knownColor) => new ColorArgb(knownColor);
    }

    // For ease of use in interop definitions (P/Invoke, function pointers, COM)
    // (as close as we get to a typedef in C#)
    public enum ARGB : uint { }

Alternative Designs

We could drop a System.Drawing specific color type in System.Drawing for this purpose (the original proposal). Discussing this with @pgovind and @tannergooding we feel there is value in defining more broadly available color types in System.Numerics.

Using Vector4 is possible for this purpose, but that makes data interchange and just general usage difficult. (Was this ARGB or RGBA, etc?) The intent is also to add additional methods in the future that are dependent on specific layout (such as conversion to HSL/HSV, etc.).

Risks

No known risks.

@JeremyKuhne JeremyKuhne added the api-suggestion Early API idea and discussion, it is NOT ready for implementation label Feb 22, 2021
@dotnet-issue-labeler dotnet-issue-labeler bot added area-System.Drawing untriaged New issue has not been triaged by the area owner labels Feb 22, 2021
@ghost
Copy link

ghost commented Feb 22, 2021

Tagging subscribers to this area: @safern, @tarekgh
See info in area-owners.md if you want to be subscribed.

Issue Details

Background and Motivation

System.Drawing.Color contains extra metadata that makes it unusable in interop scenarios and inefficient in arrays.

namespace System.Drawing
{
    public readonly struct Color : IEquatable<Color>
    {
        private readonly string? name; // Do not rename (binary serialization)
        private readonly long value; // Do not rename (binary serialization)
        private readonly short knownColor; // Do not rename (binary serialization)
        private readonly short state; // Do not rename (binary serialization)
    }
}

Color is also mutable, despite being readonly. If it is constructed from a system KnownColor value, it always looks up the value on every access.

In order to facilitate GDI and GDI+ interop scenarios and creation of more performant overloads for System.Drawing and Windows Forms, we should expose a type only contains the raw color data.

// Native definitions

// GDI
typedef DWORD COLORREF;
// GDI+
typedef DWORD ARGB;

Proposed API

    public readonly struct ColorArgb
    {
        // Layout matches GDI COLORREF and GDI+ ARGB
        public uint Value { get; }
        public byte A => (byte)(Value >> 24);
        public byte R => (byte)(Value >> 16);
        public byte G => (byte)(Value >> 8);
        public byte B => (byte)(Value);

        public ColorArgb(uint value) => Value = value;
        public ColorArgb(byte r, byte g, byte b) : this(r, g, b, byte.MaxValue) { }
        public ColorArgb(byte r, byte g, byte b, byte a) => Value = (uint)(a << 24 | r << 16 | g << 8 | b);
        public ColorArgb(KnownColor knownColor) => Value = KnownColorTable.KnownColorToArgb(knownColor);

        public ColorArgb(Color color) => Value = (uint)color.ToArgb();

        // Color has these 3 methods.
        public float GetHue() => 0;
        public float GetSaturation() => 0;
        public float GetBrightness() => 0;

        // Frequently checked as only GDI+ supports transparency.
        public bool HasTransparency => A != byte.MaxValue;
        public bool FullyTransparent => A == 0;

        public static implicit operator Color(ColorArgb color) => Color.FromArgb((int)color.Value);
        public static implicit operator ARGB(ColorArgb color) => (ARGB)color.Value;
        public static implicit operator ColorArgb(ARGB argb) => new ColorArgb((uint)argb);
        public static implicit operator ColorArgb(Color color) => new ColorArgb(color);
        public static implicit operator ColorArgb(KnownColor knownColor) => new ColorArgb(knownColor);
    }

    // For ease of use in interop definitions (P/Invoke, function pointers, COM)
    // (as close as we get to a typedef in C#)
    public enum ARGB : uint { }

Alternative Designs

We could make a more generic set of color structs to handle all typical color usage scenarios, but that isn't the intent here. #32418 tracks such a request. This type is specifically to address the existing space that System.Drawing exists in, and as such lives in System.Drawing.

Risks

No known risks.

Author: JeremyKuhne
Assignees: -
Labels:

api-suggestion, area-System.Drawing, untriaged

Milestone: -

@charlesroddie
Copy link

Wow what horrors. ColorArgb is clearly better for the reasons given. But then Color8 in the linked thread solves the same problem. So apart from the namespace it's the same proposal.

@aaronfranke
Copy link

aaronfranke commented Apr 3, 2021

I dislike this proposal for a few reasons:

  • The name does not give any information about color depth, while Color8 does. It's <T> now so that's very clear.

  • I think we should be using and encouraging RGBA instead of ARGB. RGBA is the most used format, while ARGB is mostly a just thing in Microsoft products. The proposal has both RGBA and ARGB now, but I still think that RGBA should be presented as the "default" option by naming it just Color.

@JeremyKuhne
Copy link
Member Author

So apart from the namespace it's the same proposal.

@charlesroddie Yes, it is very close. This is the native color, and this is specifically for direct interop. Color8 is not the same format and I don't want to muddy that space with a layout that doesn't match the "standard".

@aaronfranke

The name does not give any information about color depth, while Color8 does.

True, and I think Color8 is a much better "new" color. This is explicitly for perf and direct interop scenarios.

I think we should be using and encouraging RGBA instead of ARGB.

I agree, but it doesn't address the specific interop need here.

I want to be super clear- this is not intended to be the "new" Color. This is meant to allow us to have a shared native definition for GDI and GDI+ scenarios, which is what System.Drawing is specifically written to. We can take multiple steps to push people to use the "new" Color types when they become available. Type naming, subnamespacing, hiding from intellisense, etc. are options we can lean on.

@charlesroddie
Copy link

charlesroddie commented Apr 6, 2021

This is meant to allow us to have a shared native definition for GDI and GDI+ scenarios, which is what System.Drawing is specifically written to.

Sorry I didn't see the interop part. If it's a Windows-specific feature then I don't think it's a candidate for dotnet/runtime. Perhaps a WindowsGraphicsPrimitives library that WPF/WinForms can depend on? Or a GraphicsPrimitives library that does the same thing for a broader set of consumers?

@JeremyKuhne
Copy link
Member Author

If it's a Windows-specific feature then I don't think it's a candidate for dotnet/runtime.

Well, it is Windows specific in the sense that it is GDI/GDI+ specific, which is what System.Drawing is a projection/mapping of. So generally I agree with you, but this assembly is really a legacy Windows thing we're carrying and not the future of drawing/graphics support for .NET I think.

@safern
Copy link
Member

safern commented Apr 8, 2021

Given that this is needed by Winforms to improve perf and System.Drawing.Common could benefit from it, let's mark is as api-ready-for-review and discuss it in the review meeting.

@safern safern added api-ready-for-review API is ready for review, it is NOT ready for implementation and removed untriaged New issue has not been triaged by the area owner labels Apr 8, 2021
@safern safern added this to the 6.0.0 milestone Apr 8, 2021
@aaronfranke
Copy link

aaronfranke commented Apr 8, 2021

If this is reviewed, then #32418 should also be reviewed. Since so many things need a Color type, it makes sense to include one (or two) in .NET for everyone to use, including Winforms and System.Drawing and Maui and much more.

@bartonjs
Copy link
Member

In API Review we feel that this is just setting us up for an API bifurcation between Color and ColorArgb, and that there doesn't seem to be a compelling scenario for that bifurcation.

With a compelling scenario we could have a different discussion.

@bartonjs
Copy link
Member

bartonjs commented May 25, 2021

Video

It seems like there may be scenarios, but we should reconcile the layering (above or below Color) and determine if we need this, or a companion, for MAUI.

@bartonjs bartonjs reopened this May 25, 2021
@bartonjs bartonjs added api-needs-work API needs work before it is approved, it is NOT ready for implementation and removed api-ready-for-review API is ready for review, it is NOT ready for implementation labels May 25, 2021
@JeremyKuhne JeremyKuhne changed the title API Proposal: Add blittable Color to System.Drawing API Proposal: Add blittable Color to System.Numerics May 27, 2021
@JeremyKuhne
Copy link
Member Author

Updated the proposal based on offline discussions with @pgovind and @tannergooding.

@ghost
Copy link

ghost commented May 28, 2021

Tagging subscribers to this area: @tannergooding, @pgovind
See info in area-owners.md if you want to be subscribed.

Issue Details

Background and Motivation

System.Drawing.Color contains extra metadata that makes it unusable in interop scenarios and inefficient in arrays.

namespace System.Drawing
{
    public readonly struct Color : IEquatable<Color>
    {
        private readonly string? name; // Do not rename (binary serialization)
        private readonly long value; // Do not rename (binary serialization)
        private readonly short knownColor; // Do not rename (binary serialization)
        private readonly short state; // Do not rename (binary serialization)
    }
}

Color is also mutable, despite being readonly. If it is constructed from a system KnownColor value, it always looks up the value on every access.

In order to facilitate GDI and GDI+ interop scenarios and creation of more performant overloads for System.Drawing and Windows Forms, we should expose a type only contains the raw color data.

// Native definitions

// GDI
typedef DWORD COLORREF;
// GDI+
typedef DWORD ARGB;

Proposed API

using System;

namespace System.Drawing
{
    public readonly struct Color : IEquatable<Color>
    {
        public static implicit operator Color(System.Numerics.Colors.Argb<byte> argb);
        public static explicit operator System.Numerics.Colors.Argb<byte>(in Color color);
        public static implicit operator Color(System.Numerics.Colors.Rgba<byte> rgba);
        public static explicit operator System.Numerics.Colors.Rgba<byte>(in Color color);
    }
}

namespace System.Numerics.Colors
{
    public static class Argb
    {
        public static Argb<byte> CreateLittleEndian(uint color);
        public static uint ToUInt32LittleEndian(this Argb<byte> color);
    }

    public readonly struct Argb<T> : IEquatable<Argb<T>>, IFormattable where T : struct
    {
        public T A { get; }
        public T R { get; }
        public T G { get; }
        public T B { get; }

        public Argb(T a, T r, T g, T b);
        public Argb(ReadOnlySpan<T> values);
        public readonly void CopyTo(Span<T> destination);
        public bool Equals(Argb<T> other);
        public string ToString(string format, IFormatProvider formatProvider);
    }

    public static class Rgba
    {
        public static Rgba<byte> CreateLittleEndian(uint color);
        public static uint ToUInt32LittleEndian(this Rgba<byte> color);
    }

    public readonly struct Rgba<T> : IEquatable<Rgba<T>>, IFormattable where T : struct
    {
        public T R { get; }
        public T G { get; }
        public T B { get; }
        public T A { get; }

        public Rgba(T r, T g, T b, T a);
        public Rgba(ReadOnlySpan<T> values);
        public readonly void CopyTo(Span<T> destination);
        public bool Equals(Rgba<T> other);
        public string ToString(string format, IFormatProvider formatProvider);
    }
}

Original Proposal

``` C# public readonly struct ColorArgb { // Layout matches GDI COLORREF and GDI+ ARGB public uint Value { get; } public byte A => (byte)(Value >> 24); public byte R => (byte)(Value >> 16); public byte G => (byte)(Value >> 8); public byte B => (byte)(Value);
    public ColorArgb(uint value) => Value = value;
    public ColorArgb(byte r, byte g, byte b) : this(r, g, b, byte.MaxValue) { }
    public ColorArgb(byte r, byte g, byte b, byte a) => Value = (uint)(a << 24 | r << 16 | g << 8 | b);
    public ColorArgb(KnownColor knownColor) => Value = KnownColorTable.KnownColorToArgb(knownColor);

    public ColorArgb(Color color) => Value = (uint)color.ToArgb();

    // Color has these 3 methods.
    public float GetHue() => 0;
    public float GetSaturation() => 0;
    public float GetBrightness() => 0;

    // Frequently checked as only GDI+ supports transparency.
    public bool HasTransparency => A != byte.MaxValue;
    public bool FullyTransparent => A == 0;

    public static implicit operator Color(ColorArgb color) => Color.FromArgb((int)color.Value);
    public static implicit operator ARGB(ColorArgb color) => (ARGB)color.Value;
    public static implicit operator ColorArgb(ARGB argb) => new ColorArgb((uint)argb);
    public static implicit operator ColorArgb(Color color) => new ColorArgb(color);
    public static implicit operator ColorArgb(KnownColor knownColor) => new ColorArgb(knownColor);
}

// For ease of use in interop definitions (P/Invoke, function pointers, COM)
// (as close as we get to a typedef in C#)
public enum ARGB : uint { }
</details>

## Alternative Designs

We could drop a System.Drawing specific color type in System.Drawing for this purpose (the original proposal). Discussing this with @pgovind and @tannergooding we feel there is value in defining more broadly available color types in System.Numerics.

Using Vector4 is _possible_ for this purpose, but that makes data interchange and just general usage difficult. (Was this ARGB or RGBA, etc?) The intent is also to add additional methods in the future that are dependent on specific layout (such as conversion to HSL/HSV, etc.).

## Risks

No known risks.


<table>
  <tr>
    <th align="left">Author:</th>
    <td>JeremyKuhne</td>
  </tr>
  <tr>
    <th align="left">Assignees:</th>
    <td>-</td>
  </tr>
  <tr>
    <th align="left">Labels:</th>
    <td>

`api-needs-work`, `api-suggestion`, `area-System.Drawing`, `area-System.Numerics`

</td>
  </tr>
  <tr>
    <th align="left">Milestone:</th>
    <td>6.0.0</td>
  </tr>
</table>
</details>

@terrajobst terrajobst added api-approved API was approved in API review, it can be implemented and removed api-ready-for-review API is ready for review, it is NOT ready for implementation labels Mar 8, 2022
@deeprobin
Copy link
Contributor

I think we can label this "easy". I think new contributors would do well with this issue.

@tannergooding
Copy link
Member

This one is being primarily driven by WinForms and need in other areas. It's going to likely be done by that team to ensure it and its usages are going in at the same time/release

@rickbrew
Copy link
Contributor

rickbrew commented Mar 11, 2022

So why are they named Rgba and Argb ? I thought the standard, as per WIC, was BGRA and RGBA? Both of those have A in the same position (byte index 3). I'm not familiar with moving A to byte index 0.

Argb<T> in particular does not appear compatible with Win32/WIC-style BGRA ordering. This means that to use Argb<T>, swizzling will always be required, which has overhead and will limit throughput in a very common scenario, including WinForms.

@tannergooding
Copy link
Member

Argb in particular does not appear compatible with Win32/WIC-style BGRA ordering.

This is a complex space and depends on native surface format of a given OS vs what is used everywhere.

The native Win32 surface format is often referred to as B8G8R8A8 but that is most often carried about as a typedef of uint32_t and so whether it matches a Bgra<T> or Argb<T> struct can depend on endianness among other factors.

It may be that we want/need to eventually expose Bgra<T> as well, but that doesn't necessarily fit with the immediate needs of WinForms, WPF, or common usages in games.

The conversion between formats can also be done as part of the copy or write operation in a majority of these cases, leading to no real additional cost in terms of the operation.

@rickbrew
Copy link
Contributor

rickbrew commented Mar 11, 2022

I disagree -- the swizzling definitely has a cost, especially when it has to be done on both ends of an operation, especially if has to be done multiple times as a buffer is passed around internally and has various operations performed on it that only operate on the swizzled format. So in my app/library, imagine I'm passing a BGRA bitmap from A to B to C, and each one wants to execute a filter on the bitmap that is supported by these primitives (but only in ARGB format!). That means I have to swizzle 6 times to get the job done, unless I add a way for the components to communicate the component ordering of the buffer. This affects architecture of apps and libraries, which may not be malleable enough to reasonably accommodate this.

It may be that we want/need to eventually expose Bgra as well, but that doesn't necessarily fit with the immediate needs of WinForms, WPF, or common usages in games.

That sounds completely contradictory. You're stating that a key target scenario here is WinForms and WPF, which will be dealing with BGRA because that's what the platform (Win32) uses, so therefore BGRA doesn't fit with their immediate needs? 😕 Paint.NET would also be hamstrung, performance-wise, if I wanted to use these primitives -- I have seen good performance improvements by optimizing and eliminating things like swizzling, copying, and format conversions.

To be blunt, if BGRA isn't supported, these primitives are completely useless for Paint.NET and for a large number of Windows/desktop scenarios. The performance cost of swizzling is not super high but it does lower the upper bound of performance, especially for real-time/interactive scenarios.

@tannergooding
Copy link
Member

the swizzling definitely has a cost, especially when it has to be done on both ends of an operation, especially if has to be done multiple times as a buffer is passed around internally and has various operations performed on it that only operate on the swizzled format.

I think you're misunderstanding. In terms of basic operations, the swizzle is performed as part of the load/store. So if you require a copy, then you can convert as part of that copy at no-additional cost (there is no instruction latency difference between a load or a load + swizzle). This is true of both individual pixel read-writes and more vectorized algorithms. You can achieve theoretically higher throughput for a 1-to-1 copy using non-temporal loads/stores, but that's something a bit different and isn't as common.

If you are having to swizzle, do an operation, and then re-swizzle; then yes there can be a cost for vectorized code since you can't do the swizzle as part of a vectorized store.

You're stating that a key target scenario here is WinForms and WPF, which will be dealing with BGRA because that's what the platform (Win32) uses, so therefore BGRA doesn't fit with their immediate needs

This is a complex area and what the docs describe vs how its implemented can differ a bit.

  • COLORREF is a uint32 and is 0x00BBGGRR
    • This is equivalent to struct COLORREF { byte R, G, B, A; }, where A is always 0, on a little-endian platform
    • This is described in docs as RGB
  • Gdiplus.Color is a uint32 and is 0xAARRGGBB.
    • This is equivalent to struct Color { byte B, G, R, A; } on a little-endian platform
    • This is described in docs as RGBA
    • The ordering in the struct, however, is BGRA
  • System.Windows.Media.Color is struct Color { byte a, r, g, b; } or struct Color { float a, r, g, b; }
    • This is equivalent to 0xBBGGRRAA on a little endian platform
    • This is described in the docs as ARGB
  • CSS and other standard colors are or in the form #RRGGBBAA
    • These are described in the docs as RGB and RGBA
  • DirectX Math are interpreted as struct Color { float r, g, b, a; }
    • These are described in the docs as RGBA
    • Most games likewise order their surface formats and other bits as RGBA
  • Direct2D exposes ColorF which is a uint32 and is 0xAARRGGBB
    • Direct2D recommends targeting B8G8R8A8 as this ends up matching the layout of ColorF and Gdiplus.Color on a little-endian system

The actual underlying format for GDI+ and the recommended format for D2D1 is really equivalent to BGRA since Windows only supports little-endian systems (however, it would be ARGB on a big-endian system if ever supported in the future). However, most higher level libraries are working with RGBA or ARGB instead and then "normalize" to the actual surface format in the compositor or elsewhere. This is particularly relevant when the surface format for other platforms/systems can differ as well and so picking one major format to standardize on for the "core" functionality is desirable. This does sometimes have a cost for converting, but those are often relegated to "one time" operations that happen as part of the start/end of the transform pipeline.

  • Also noting that BGRA is strictly just the "native" surface format. If you have a monitor that supports it, you are completely able to opt into other formats as well including HDR, 10 or 12-bit color formats, etc. In this case, the DWM will handle fixing up the difference for apps that don't support this (and it can lead to a noticeable "washed out" appearance in some cases).

@rickbrew
Copy link
Contributor

Hmm, well that seems to clear up the confusion on my part (along with the conversation we had on Discord).

@dakersnar dakersnar modified the milestones: 7.0.0, Future Aug 8, 2022
@AlexRadch AlexRadch linked a pull request Aug 17, 2024 that will close this issue
@dotnet-policy-service dotnet-policy-service bot added the in-pr There is an active PR which will close this issue when it is merged label Aug 17, 2024
@AlexRadch
Copy link
Contributor

AlexRadch commented Aug 18, 2024

I implemented this API in PR #106575. I think that the methods CreateBigEndian(uint color) and CreateLittleEndian(uint color) have inappropriate and confusing names. These methods create a color from an integer value with a big-endian/little-endian byte order.

I suggest renaming them to FromBigEndian and FromLittleEndian. Such names are more consistent with reverse methods ToUInt32BigEndian and ToUInt32LittleEndian. It is convenient for reverse methods to have reverse names.

@PavielKraskouski
Copy link

Will it be possible to convert float colors to vector types and back? For example, when reading normal maps, colors need to be converted to vector.

@michael-hawker
Copy link

Does CMYK factor as an alternate schema/form in here at all as something to think about? Just trying to think beyond the base cases here and what could be useful to other industries/use-cases beyond the traditional graphics rendering.

@rickbrew
Copy link
Contributor

rickbrew commented Dec 6, 2024

Worth noting is that colors composed of different primitive types, e.g. byte vs float, often use different ranges for their values. An Rgba<byte> will use the full [0, 255] range of a byte whereas an Rgba<float> would use a normalized range of [0, 1]. Values outside of that range are either invalid or used for things like HDR. So this design should consider that a "primitive schema" might be necessary in the future, particularly if generic conversions between e.g. Rgba<byte> and Rgba<float> is desired functionality.

Put another way, we can't just constrain to INumber<T>, IMinMaxValue<T> and use IMinMaxValue<T>.MinValue/MaxValue for the range of color components.

Some imaging toolkits, notably WIC, also have a default policy that pixel formats using unsigned integers (e.g. Rgba<byte>) use companded gamma (e.g. sRGB ~2.2ɣ), while floating point formats (Rgba<float>) use linear gamma (1.0ɣ). This gets into color spaces, color profiles, and color "contexts", which is a huge mess of complexity on its own, but is worth keeping an eye on so that these APIs don't get painted in a corner with respect to future expansion.

@DrkWzrd
Copy link

DrkWzrd commented Dec 30, 2024

Sorry, but IMHO this makes no sense with this small surface/scope.

First. If you are going to reinvent the wheel to make an unification of code, the reinvented wheel should be better than the current wheel. That means to implement a wellformed api surface around the color theory, color spaces, conversions, etc... Even if you don't implement it, create an good structure of interfaces and let the people cook. If this is only "create a new small, non extrapolable struct to make it a little bit better to interop and help us to improve performance without encouraging the developers to use these things" is ok, but is an enormous missed opportunity.

Second. Json serialization. If this is looking for make the interoperability better, why not the serialization?. Look third statement.

Third. Create a logical framework around the concept of color (again, first point, color theory, color spaces, conversions). Create an "agnostic color-color space tuple" or "color in space" (naming is hard) struct, and work around there. Is it a pain in the *ss? Yeah, but it is "the right way". In a pure way, a color is just a vector of values (I think this must be make with SIMD in mind), each one represeting something in one concrete color space or representation system.

Fourth. More ahead, you'll never break binary compatibility in order to remove Media.Color and Drawing.Color, and use the new Color, adding a layer that will be "just a new layer which is common" for the mantainers of the libraries, but not to the users. That way the users will never use this work. You have to encourage the users to work in "the new color api space" and just make System.Windows.Media.Color.FromAgnosticColor(AgnosticColor xxxx, ....) and System.Drawing.Color.FromAgnosticColor(AgnosticColor xxxx, ....) in the end stage.

The color animations in WPF could be improved a lot with this work, a lot of other things too.

Fifth. Generic Math interfaces!

And, if this become bigger and has the sufficient scope, just create namespace for it System.Colors

PS: I understand that the great majority of developers just use sRGB (or any other permutation of the (A)RGB model). And that's the core funcionality you should implement. But you could let the API more open to expansion for other developers.

@tannergooding
Copy link
Member

Any API the BCL exposes has to consider the Framework Design Guidelines (FDG) and the general scope of the proposal. We don't just expose APIs unnecessarily, we don't try and expose foundational things without good reason, and extensibility support or lack there of is an explicit part of the design considerations.

All the points you've raised have already been considered and were rejected for various reasons such as going directly against the FDG, coupling things together that should not be coupled or which may only be "point in time", not being "foundational" but rather encroaching into incredibly complex spaces that are better handled by explicit 3rd party libraries dedicated to the scenario, etc.

The APIs being exposed here are explicitly scoped, they are done the way they are to provide a means of simple primitive interop, to allow exposing some baseline functionality that is common to other graphics oriented vector math libraries (such as DirectX Math or GLM), and to provide a means of accelerating/improving WinForms/WPF.

@DrkWzrd
Copy link

DrkWzrd commented Jan 1, 2025

What about something like this?

public readonly struct Color: IEquatable<Color> //etc...
{
    public enum ColorFormat
    {
        Rgba,
        Argb,
        Rgb,
        Bgr,
        Xyz,
        Xyy,
        Lab,
        //and so on
    }

    // Color components. Meaningless, the ColorFormat will be the element that gives them the meaning by the order.
    public float Component1 { get; set; }
    public float Component2 { get; set; }
    public float Component3 { get; set; }
    public float Component4 { get; set; }
    //future proofing?
    //public float Component5 { get; set; }
    //public float Component6 { get; set; }

    public ColorFormat Format { get; set; }

    public Color(ColorFormat format, float c1, float c2, float c3, float c4 = 0.0f)
    {
        Format = format;
        Component1 = c1;
        Component2 = c2;
        Component3 = c3;
        Component4 = c4;
    }
}

//among all the other methods and static classes in the api proposal

This is still blittable, it's based on floating point (which is more relatable to something like the color) and is the most generic way a 3-4 components color can be represented, without restraints. To make use of vectorization, just convert it to Vector4. Instead a full range float, it could be considered something more restrictive, or even make it generic (but that defeats the purpose).

This could be the basic element, and then use the proposed Rgba or Argb as the "usable in everyday computing things".

@tannergooding
Copy link
Member

This is still blittable

It's blittable in the strictest sense that it contains all unmanaged data and would not require marshalling.

It is not blittable in the useful sense in that it allows bit-blitting of the data in the common scenarios that require it. So you wouldn't be able to use such a Color[1080, 1920] array to represent a bitmap, for example. You wouldn't be able to have a Span<Color> or Color* to the underlying buffer of a drawing surface, etc.

The design here is very intentional and considerate of the real world needs and use-cases; not some generic shape that tries to do everything at once.

@aaronfranke
Copy link

aaronfranke commented Jan 2, 2025

I will reiterate what I've stated before: The most important color type to include in .NET is the one already used in the C# ecosystem, such as Unity, Godot, Stride, and MAUI (see dotnet/maui#16903). Namely, a Color made out of 32-bit float values in an RGBA layout.

This is specifically what would provide the most benefit for interoperability scenarios in the general case, so that code can use a general color type that has seamless blittability with these others. For the non-general case, such as a need for multiple layouts, multiple color spaces, compact representations, and so on, these are what should be left to libraries. I urge people to not get distracted by legacy Microsoft-specific technologies with ARGB, which is not generally useful in the .NET ecosystem.

@tannergooding
Copy link
Member

Namely, a Color made out of 32-bit float values in an RGBA layout.

This is covered in the existing proposal via the Rgba<T> type.

I urge people to not get distracted by legacy Microsoft-specific technologies with ARGB

This is not a legacy or Microsoft specific thing, it's actually rather the standard name for the BGRA32 format that many windowing systems default to. This has historically been the default because it was more efficient to handle data as a single 32-bit unit rather than as 4 separate 8-bit units, so you had XRGB and then later ARGB. When stored as little-endian this then means blue was the first byte, being the least significant unit.

This historical start to the "layout" is still visible in almost every system used to talk about colors today. It's used in the industry standard hex representation of colors, such as #FF0000 being red, #00FF00 being green, and #0000FF being blue. Placing the alpha at the most significant byte then used the otherwise "unused" bits without needing to otherwise reposition the data on little-endian systems (unlike RGBA32 which shifts everything down one byte).

There are still many scenarios that require or otherwise benefit (such as perf wise) from BGRA32 compatible formats and so it remains a key scenario to support.

-- This isn't universally the default and some systems like OpenGL have historically preferred RGBA32, likewise when working with 4x float components it is also very typical to use a RGBA128 format, but that's still all the reason why the base proposal covers both Rgba<T> and Argb<T>

@aaronfranke
Copy link

aaronfranke commented Jan 2, 2025

@tannergooding Industry-standard color hex codes are RGB(A): #FF0000FF is opaque red (not opaque blue).

Placing alpha at the end has the benefit of making it easier to handle cases where the alpha is optional. With an array of floats that can be 3 or 4 long, read index 0 as red, index 1 as green, index 2 as blue, and if index 3 exists, read it as alpha.

But yeah, the proposal has Rgba<T> so that's good, my argument is that it should be called Color and presented as the default color format to avoid confusion for people needing a color format and not knowing which is the recommended one. Ultimately I would be happy with Rgba<T> even if it's not the optimal name in my opinion.

@tannergooding
Copy link
Member

Industry-standard color hex codes are RGB(A): #FF0000FF is opaque red (not opaque blue).

I was giving a history lesson on how/why BGRA32 (aka B8G8R8X8) was and continues to be a common and prevalent color format. I was not making a statement about the current interpretation of web-color codes.

What was being stated is that prior to alpha existing, it was simply #RRGGBB where that was commonly interpreted directly as a uint32 for performance and therefore produced a value 0x00RRGGBB. Since little-endian machines were (and continue to be) prevalent and the data was typically stored in memory as a 0xBB, 0xGG, 0xRR, 0x-- where the fourth byte was unused (known as B8G8R8X8 or BGRX32-BE or XRGB32-LE). When alpha was then introduced, the storage format then simply placed alpha in the unused space, so the actual integer was 0xAARRGGBB (known as B8G8R8A8 or BGRA32-BE or ARGB32-LE).

This is distinct from what the web standardized on (#RRGGBBAA), where it decided to go its own direction upon introducing alpha. This new direction caused a deviation where it defined a format that no longer represents the raw integer. Which is to say that #RRGGBBAA (such that #00FF00FF is opaque green) is not stored as 0xRRGGBBAA which would be 0xAA, 0xBB, 0xGG, 0xRR; it's rather stored as 0xRR, 0xGG, 0xBB, 0xAA (R8G8B8A8 or RGBA32-BE or ABGR32-LE) which would be loaded on a little-endian machine as the integer 0xAABBGGRR. Shuffling was (and remains) an expensive step, so its avoided where possible.

This is entirely from how things work when working with bytes, not with floats, and from it being far more efficient for data to be manipulated as a single uint32 rather than as 4x byte. Modern computers have evolved and now have the ability to work with 4x float efficiently regardless of layout; the same goes for 16x byte so the distinction is less relevant for things like games where you do often see 4x elements and no longer see it "compressed" into a single endian impacted load/store, but that doesn't change what the underlying window manager or native display format is for the OS (or what hardware has optimizations around), which often remains or defaults to BGRA32 (and you can often see better frame rates and other considerations by utilizing such a format for your ultimate render target output, which is distinct from what you use at earlier pipeline stages where R32G32B32A32 is more prevalent).

But yeah, the proposal has Rgba so that's good, my argument is that it should be called Color and presented as the default color format to avoid confusion for people needing a color format and not knowing which is the recommended one. Ultimately I would be happy with Rgba even if it's not the optimal name in my opinion.

Calling it Color is non starter, not only is that significantly conflicting with many other types in the scenarios that would be common for these new types to be used, but it is then also inconsistent with the other color types we want to expose because they are foundational and necessary.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
api-approved API was approved in API review, it can be implemented area-System.Numerics in-pr There is an active PR which will close this issue when it is merged
Projects
None yet
Development

Successfully merging a pull request may close this issue.