X11 SRGB Clamp

EDID ops

Grab the Extended Display Identifier Data there are numerous of ways to get this but I just used xrandr --props here, and stored it in a file. It’ll be used to get the color-characteristics but it is theoretically better to get these color characteristics from an ICC profile if possible.

DP-0 connected primary 2560x1440+0+0 (normal left inverted right x axis y ax, s) 59x 336mm
  _MUTTER_PRESENTATION_OUTPUT: 0
  CTM: -714877597 0 72646571 0 11403438 -2147483648 199691239 0 -221842746 0 22085969 0 53019433 0 213060787 0 -266080221 0
  CscMatrix: 54627 11083 -174 0 3047 62150 337 0 809 3251 61475 0
  EDID:
    00ffffffffffff001e6d7f5b22580300
    041f0104b53c22789f8cb5af4f43ab26
    0e5054254b007140818081c0a9c0b300
    d1c08100d1cf09ec00a0a0a067503020
    3a0055502100001a000000fd003090e6
    e63c010a202020202020000000fc004c
    4720554c545241474541520a000000ff
    003130344e54445636463137300a01c7
    02030f712309060746100403011f136f
    c200a0a0a0555030203a005550210000
    1a565e00a0a0a0295030203500555021
    00001a5aa000a0a0a0465030203a0055
    50210000000000000000000000000000
    00000000000000000000000000000000
    00000000000000000000000000000000
    00000000000000000000000000000060
  BorderDimensions: 4
  	supported: 4
  Border: 0 0 0 0
  	range: (0, 65535)
  SignalFormat: DisplayPort
  	supported: DisplayPort
  ConnectorType: DisplayPort
  ConnectorNumber: 2
  _ConnectorLocation: 2
  non-desktop: 0
  	supported: 0, 1
   2560x1440    143.97*+ 120.00    99.95    59.95
   1920x1080     74.91    60.00    59.94    50.00
   1680x1050     59.95
   1600x900      60.00
   1280x1024     75.02    60.02
   1280x800      59.81
   1280x720      60.00    59.94    50.00
   1152x864      59.96
   1024x768      75.03    60.00
   800x600       75.00    60.32
   720x480       59.94
   640x480       75.00    59.94    59.93

Decode the file using any edid-decoder. The one here called edid-decode has an online version.

edid-decode [path-to-file]

The decoded edid should result in something like this

edid-decode (hex):
00 ff ff ff ff ff ff 00 1e 6d 7f 5b 22 58 03 00
04 1f 01 04 b5 3c 22 78 9f 8c b5 af 4f 43 ab 26
0e 50 54 25 4b 00 71 40 81 80 81 c0 a9 c0 b3 00
d1 c0 81 00 d1 cf 09 ec 00 a0 a0 a0 67 50 30 20
3a 00 55 50 21 00 00 1a 00 00 00 fd 00 30 90 e6
e6 3c 01 0a 20 20 20 20 20 20 00 00 00 fc 00 4c
47 20 55 4c 54 52 41 47 45 41 52 0a 00 00 00 ff
00 31 30 34 4e 54 44 56 36 46 31 37 30 0a 01 c7

02 03 0f 71 23 09 06 07 46 10 04 03 01 1f 13 6f
c2 00 a0 a0 a0 55 50 30 20 3a 00 55 50 21 00 00
1a 56 5e 00 a0 a0 a0 29 50 30 20 35 00 55 50 21
00 00 1a 5a a0 00 a0 a0 a0 46 50 30 20 3a 00 55
50 21 00 00 00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 60

----------------

Block 0, Base EDID:
  EDID Structure Version & Revision: 1.4
  Vendor & Product Identification:
    Manufacturer: GSM
    Model: 23423
    Serial Number: 219170
    Made in: week 4 of 2021
  Basic Display Parameters & Features:
    Digital display
    Bits per primary color channel: 10
    DisplayPort interface
    Maximum image size: 60 cm x 34 cm
    Gamma: 2.20
    DPMS levels: Standby
    Supported color formats: RGB 4:4:4, YCrCb 4:4:4, YCrCb 4:2:2
    Default (sRGB) color space is primary color space
    First detailed timing includes the native pixel format and preferred refresh rate
    Display is continuous frequency
  Color Characteristics:
    Red  : 0.6855, 0.3085
    Green: 0.2646, 0.6679
    Blue : 0.1503, 0.0576
    White: 0.3134, 0.3291
  Established Timings I & II:
    DMT 0x04:   640x480    59.940476 Hz   4:3     31.469 kHz     25.175000 MHz
    DMT 0x06:   640x480    75.000000 Hz   4:3     37.500 kHz     31.500000 MHz
    DMT 0x09:   800x600    60.316541 Hz   4:3     37.879 kHz     40.000000 MHz
    DMT 0x0b:   800x600    75.000000 Hz   4:3     46.875 kHz     49.500000 MHz
    DMT 0x10:  1024x768    60.003840 Hz   4:3     48.363 kHz     65.000000 MHz
    DMT 0x12:  1024x768    75.028582 Hz   4:3     60.023 kHz     78.750000 MHz
    DMT 0x24:  1280x1024   75.024675 Hz   5:4     79.976 kHz    135.000000 MHz
  Standard Timings:
    GTF     :  1152x864    60.000000 Hz   4:3     53.700 kHz     81.624000 MHz
    DMT 0x23:  1280x1024   60.019740 Hz   5:4     63.981 kHz    108.000000 MHz
    DMT 0x55:  1280x720    60.000000 Hz  16:9     45.000 kHz     74.250000 MHz
    DMT 0x53:  1600x900    60.000000 Hz  16:9     60.000 kHz    108.000000 MHz (RB)
    DMT 0x3a:  1680x1050   59.954250 Hz  16:10    65.290 kHz    146.250000 MHz
    DMT 0x52:  1920x1080   60.000000 Hz  16:9     67.500 kHz    148.500000 MHz
    DMT 0x1c:  1280x800    59.810326 Hz  16:10    49.702 kHz     83.500000 MHz
    GTF     :  1920x1080   75.000068 Hz  16:9     84.600 kHz    220.637000 MHz
  Detailed Timing Descriptors:
    DTD 1:  2560x1440  143.973257 Hz  16:9    222.151 kHz    604.250000 MHz (597 mm x 336 mm)
                 Hfront   48 Hsync  32 Hback   80 Hpol P
                 Vfront    3 Vsync  10 Vback   90 Vpol N
    Display Range Limits:
      Monitor ranges (Bare Limits): 48-144 Hz V, 230-230 kHz H, max dotclock 600 MHz
    Display Product Name: 'LG ULTRAGEAR'
    Display Product Serial Number: '104NTDV6F170'
  Extension blocks: 1
Checksum: 0xc7

----------------

Block 1, CTA-861 Extension Block:
  Revision: 3
  Basic audio support
  Supports YCbCr 4:4:4
  Supports YCbCr 4:2:2
  Native detailed modes: 1
  Audio Data Block:
    Linear PCM:
      Max channels: 2
      Supported sample rates (kHz): 48 44.1
      Supported sample sizes (bits): 24 20 16
  Video Data Block:
    VIC  16:  1920x1080   60.000000 Hz  16:9     67.500 kHz    148.500000 MHz
    VIC   4:  1280x720    60.000000 Hz  16:9     45.000 kHz     74.250000 MHz
    VIC   3:   720x480    59.940060 Hz  16:9     31.469 kHz     27.000000 MHz
    VIC   1:   640x480    59.940476 Hz   4:3     31.469 kHz     25.175000 MHz
    VIC  31:  1920x1080   50.000000 Hz  16:9     56.250 kHz    148.500000 MHz
    VIC  19:  1280x720    50.000000 Hz  16:9     37.500 kHz     74.250000 MHz
  Detailed Timing Descriptors:
    DTD 2:  2560x1440  119.997589 Hz  16:9    182.996 kHz    497.750000 MHz (597 mm x 336 mm)
                 Hfront   48 Hsync  32 Hback   80 Hpol P
                 Vfront    3 Vsync  10 Vback   72 Vpol N
    DTD 3:  2560x1440   59.950550 Hz  16:9     88.787 kHz    241.500000 MHz (597 mm x 336 mm)
                 Hfront   48 Hsync  32 Hback   80 Hpol P
                 Vfront    3 Vsync   5 Vback   33 Vpol N
    DTD 4:  2560x1440   99.946436 Hz  16:9    150.919 kHz    410.500000 MHz (analog composite, sync-on-green, 597 mm x 336 mm)
                 Hfront   48 Hsync  32 Hback   80 Hpol N
                 Vfront    3 Vsync  10 Vback   57 Vpol N
Checksum: 0x60  Unused space in Extension Block: 58 bytes

Relevant bit is the Color-Characteristics values:

Color Characteristics:
        Red  : 0.6855, 0.3085
        Green: 0.2646, 0.6679
        Blue : 0.1503, 0.0576
        White: 0.3134, 0.3291

Subsequent program.cs script converts the provided matrix to an RGB matrix, the math is here. The input format is Rxy Gxy Bxy.

dotnet run 0.6855 0.3085 0.2646 0.6679 0.1503 0.0576
// program.cs
using System;
using System.Globalization;
using System.Threading;
using MathNet.Numerics.LinearAlgebra;

namespace CscMatrixCalculator {
class Program {
  // credit to  for the math
  public struct Point {
    public double X;
    public double Y;
  }

  public struct ColorSpace {
    public Point Red;
    public Point Green;
    public Point Blue;
    public Point White;
  }

  public static Point D65 = new (){ X = 0.312713, Y = 0.329016 };

  public static ColorSpace sRGB
      = new (){ Red = new Point{ X = 0.64, Y = 0.33 },
          Green = new Point{ X = 0.3, Y = 0.6 },
          Blue = new Point{ X = 0.15, Y = 0.06 }, White = D65 };

  public static ColorSpace P3Display
      = new (){ Red = new Point{ X = 0.68, Y = 0.32 },
          Green = new Point{ X = 0.265, Y = 0.69 },
          Blue = new Point{ X = 0.15, Y = 0.06 }, White = D65 };

  public static Matrix<double> RGBToXYZ(ColorSpace colorSpace)
  {
    var red = colorSpace.Red;
    var green = colorSpace.Green;
    var blue = colorSpace.Blue;
    var white = colorSpace.White;
    var whiteXYZ = Matrix<double>.Build.DenseOfArray(
        new[, ]{ { white.X / white.Y }, { 1 },
            { (1 - white.X - white.Y) / white.Y } });

    var Mprime = Matrix<double>.Build.DenseOfArray(new[, ]{
        { red.X / red.Y, green.X / green.Y, blue.X / blue.Y },
        { 1, 1, 1 },
        { (1 - red.X - red.Y) / red.Y,
            (1 - green.X - green.Y) / green.Y,
            (1 - blue.X - blue.Y) / blue.Y } });

    return Mprime
        * Matrix<double>.Build.DiagonalOfDiagonalVector(
            (Mprime.Inverse() * whiteXYZ).Column(0));
  }

  public static Matrix<double> XYZToRGB(ColorSpace colorSpace)
  {
    return RGBToXYZ(colorSpace).Inverse();
  }

  public static Matrix<double> RGBToRGB(
      ColorSpace from, ColorSpace to)
  {
    var result = XYZToRGB(to) \* RGBToXYZ(from);
    result.CoerceZero(1e-14);
    return result;
  }

  static void Main(string\[] args)
  {
    double\[] coords;
    try {
      coords = Array.ConvertAll(
          args, x => double.Parse(x, CultureInfo.InvariantCulture));
      if (coords.Length != 6)
        throw new Exception();
    } catch {
      Console.WriteLine(
          "Arguments must be: red_x red_y green_x green_y blue_x blue_y");
      return;
    }

    var colorSpace = new ColorSpace{ Red
      = new Point{ X = coords[0], Y = coords[1] },
      Green = new Point{ X = coords[2], Y = coords[3] },
      Blue = new Point{ X = coords[4], Y = coords[5] }, White = D65 };

    var matrix = RGBToRGB(sRGB, colorSpace);

    Thread.CurrentThread.CurrentCulture
        = CultureInfo.InvariantCulture;
    for (var i = 0; i < 3; i++) {
      for (var j = 0; j < 3; j++) {
        Console.Write(matrix[i, j]);
        if (!(i == 2 && j == 2)) {
          Console.Write(':');
        }
      }
    }
  }
}
}

The output should be something like this

0.8335444411825749:0.16911422824331526:-0.0026586694258896593:0.04650138955110145:0.9483488982583312:0.005149712190567302:0.012353331090545688:0.04961038521975711:0.9380362836896972

Two ways to use this color-transform-matrix, from what I found the cmdemo method should work on both AMD & Nvidia, whereas the --set CscMatrix is Nvidia only.

cmdemo

The output is already in the proper format for the color-demo-app so all that needs to be done is compile the color-demo-app

git://people.freedesktop.org/~hwentland/color-demo-app
cd color-demo-app
make

Get the display name by running xrandr and run the executable

./cmdemo -o  -d srgb -c 0.8335444411825749:0.16911422824331526:-0.0026586694258896593:0.04650138955110145:0.9483488982583312:0.005149712190567302:0.012353331090545688:0.04961038521975711:0.9380362836896972 -r srgb"

CscMatrix

Have to use xrandr for novideo folks. No need to compile a program for this its just multiplication. Each entry needs to be multiplied by 65536.

For example if we were multiplying the red channel the red_x value would be 0.8335444411825749 * 65536 = 55460 and red_y would be 0.16911422824331526 * 65536 = 11083. The format for the --set CscMatrix is [R,G,B,1]

xrandr --output DP-0 --set CscMatrix 54627,11083,-174,0,3047,62150,337,0,809,3251,61475,0

Limitations

Might happen to just be related to the GPU vendor and OS combination.