1// Package pattern defines the colour patches of the testpattern client and
2// the transfer functions that say what each patch must look like. It has no
3// Wayland imports so tests can reuse it as an oracle; it is deliberately
4// independent of the renderer's own colour code.
5package pattern
6
7import (
8 "image"
9 "math"
10)
11
12const (
13 // Columns and Rows give the patch grid.
14 Columns = 6
15 Rows = 2
16)
17
18// Patch is one solid rectangle of the test pattern. SRGB is set in "sdr"
19// mode, Nits (linear BT.2020, absolute luminance) in "hdr" mode.
20type Patch struct {
21 Rect image.Rectangle
22 SRGB [3]uint8
23 Nits [3]float64
24}
25
26// Centre returns the middle of the patch.
27func (p Patch) Centre() image.Point {
28 return image.Pt((p.Rect.Min.X+p.Rect.Max.X)/2, (p.Rect.Min.Y+p.Rect.Max.Y)/2)
29}
30
31// sdrColours are the sRGB patches in row-major order.
32var sdrColours = [Columns * Rows][3]uint8{
33 {0, 0, 0}, {64, 64, 64}, {128, 128, 128}, {192, 192, 192}, {255, 255, 255}, {255, 128, 0},
34 {255, 0, 0}, {0, 255, 0}, {0, 0, 255}, {0, 255, 255}, {255, 0, 255}, {255, 255, 0},
35}
36
37// BT709ToBT2020 converts linear BT.709 RGB to linear BT.2020 RGB.
38var BT709ToBT2020 = [3][3]float64{
39 {0.627404, 0.329283, 0.043313},
40 {0.069097, 0.919540, 0.011362},
41 {0.016391, 0.088013, 0.895595},
42}
43
44// ReferenceWhite is the luminance of SDR white in an HDR signal, in nits.
45const ReferenceWhite = 203
46
47// hdrNits are the BT.2020 linear patches in row-major order.
48var hdrNits = func() (n [Columns * Rows][3]float64) {
49 for i, v := range []float64{0, 1, 100, ReferenceWhite, 400, 1000} {
50 n[i] = [3]float64{v, v, v}
51 }
52 n[6] = [3]float64{ReferenceWhite, 0, 0}
53 n[7] = [3]float64{0, ReferenceWhite, 0}
54 n[8] = [3]float64{0, 0, ReferenceWhite}
55 for c := range 3 {
56 var v [3]float64
57 for r := range 3 {
58 v[r] = BT709ToBT2020[r][c] * ReferenceWhite
59 }
60 n[9+c] = v
61 }
62 return
63}()
64
65// Patches lays the mode's patches out as a 6×2 grid filling w×h. Cell sizes
66// use integer division; the last column and row take the remainder. Mode is
67// "sdr" or "hdr"; any other value yields nil.
68func Patches(mode string, w, h int) []Patch {
69 if mode != "sdr" && mode != "hdr" {
70 return nil
71 }
72 patches := make([]Patch, 0, Columns*Rows)
73 for i := range Columns * Rows {
74 col, row := i%Columns, i/Columns
75 x0, x1 := col*(w/Columns), (col+1)*(w/Columns)
76 y0, y1 := row*(h/Rows), (row+1)*(h/Rows)
77 if col == Columns-1 {
78 x1 = w
79 }
80 if row == Rows-1 {
81 y1 = h
82 }
83 p := Patch{Rect: image.Rect(x0, y0, x1, y1)}
84 if mode == "sdr" {
85 p.SRGB = sdrColours[i]
86 } else {
87 p.Nits = hdrNits[i]
88 }
89 patches = append(patches, p)
90 }
91 return patches
92}
93
94// PQEncode encodes absolute luminance in nits with SMPTE ST 2084, clamped to
95// 0–10000 nits, and returns a unit-range signal.
96func PQEncode(nits float64) float64 {
97 if nits <= 0 {
98 return 0
99 }
100 x := math.Pow(min(nits, 10000)/10000, 2610.0/16384)
101 return math.Pow((3424.0/4096+2413.0/128*x)/(1+2392.0/128*x), 2523.0/32)
102}
103
104// PQDecode is the SMPTE ST 2084 EOTF: it turns a unit-range PQ signal into
105// absolute luminance in nits (0–10000). It inverts PQEncode.
106func PQDecode(signal float64) float64 {
107 if signal <= 0 {
108 return 0
109 }
110 e := math.Pow(min(signal, 1), 32.0/2523)
111 num := max(e-3424.0/4096, 0)
112 den := 2413.0/128 - 2392.0/128*e
113 return 10000 * math.Pow(num/den, 16384.0/2610)
114}
115
116// SRGBToLinear decodes a unit-range sRGB component.
117func SRGBToLinear(v float64) float64 {
118 if v <= 0.04045 {
119 return v / 12.92
120 }
121 return math.Pow((v+0.055)/1.055, 2.4)
122}