1package vulkan
2
3import (
4 "image/color"
5 "math"
6 "slices"
7 "testing"
8
9 "github.com/bnema/neferwl/internal/ports"
10)
11
12// A PQ 2101010 client is decoded before the SDR intermediate, which clips
13// highlights above reference white. The GPU and udmabuf are optional.
14func TestPQClientComposition(t *testing.T) {
15 r, err := New(64, 4)
16 if err != nil {
17 t.Skipf("Vulkan unavailable: %v", err)
18 }
19 defer r.Close()
20 type sample struct {
21 rgb [3]float64
22 alpha float64
23 }
24 // Neutral values test absolute luminance; saturated colors exercise the
25 // BT.2020 -> BT.709 matrix (and the clipping of out-of-gamut channels).
26 samples := []sample{
27 {[3]float64{100, 100, 100}, 1}, {[3]float64{203, 203, 203}, 1}, {[3]float64{1000, 1000, 1000}, 1},
28 {[3]float64{100, 0, 0}, 1}, {[3]float64{0, 0, 100}, 1}, {[3]float64{0, 100, 0}, .6666667},
29 }
30 for _, name := range [][4]byte{{'X', 'R', '3', '0'}, {'X', 'B', '3', '0'}, {'A', 'B', '3', '0'}} {
31 t.Run(string(name[:]), func(t *testing.T) {
32 format := ports.DMABufFormat{Format: fourcc(name[0], name[1], name[2], name[3]), Modifier: 0}
33 if !slices.Contains(r.DMABuf().Formats, format) {
34 t.Skipf("linear %s unavailable", name)
35 }
36 // Store premultiplied PQ codes for alpha buffers. Alpha uses two bits.
37 f := udmabuf(t, 64, 4, func(x, _ int) [4]byte {
38 s := samples[min(x/10, len(samples)-1)]
39 a := uint32(math.Round(s.alpha * 3))
40 if name[0] == 'X' {
41 a = 0 // X bits are undefined, and must be ignored.
42 s.alpha = 1
43 }
44 var ch [3]uint32
45 for i, n := range s.rgb {
46 ch[i] = uint32(math.Round(pqEncode(n) * s.alpha * 1023))
47 }
48 var packed uint32
49 if name[1] == 'R' {
50 packed = ch[2] | ch[1]<<10 | ch[0]<<20
51 } else {
52 packed = ch[0] | ch[1]<<10 | ch[2]<<20
53 }
54 packed |= a << 30
55 return [4]byte{byte(packed), byte(packed >> 8), byte(packed >> 16), byte(packed >> 24)}
56 })
57 buf := &ports.DMABuf{ID: uint64(format.Format), Width: 64, Height: 4, Format: format.Format, Planes: []ports.DMABufPlane{{File: f, Stride: 256}}}
58 c := ports.SurfaceContent{ID: 1, Width: 64, Height: 4, Opaque: name[0] == 'X', Color: ports.SurfaceColor{TF: ports.ColorTFPQ, Primaries: ports.ColorPrimariesBT2020}, DMABuf: buf}
59 scene := ports.Scene{Background: "#000000", Windows: []ports.SceneWindow{{ID: 1, Rect: ports.Rect{W: 64, H: 4}}}}
60 if err := render(r, scene, map[ports.WindowID]ports.SurfaceContent{1: c}); err != nil {
61 t.Fatal(err)
62 }
63 for i, s := range samples {
64 alpha := s.alpha
65 if name[0] == 'X' {
66 alpha = 1
67 }
68 var nits [3]float64
69 for j, n := range s.rgb {
70 sourceAlpha := s.alpha
71 if name[0] == 'X' {
72 sourceAlpha = 1
73 }
74 code := math.Round(pqEncode(n)*sourceAlpha*1023) / 1023
75 p := math.Pow(code/sourceAlpha, 32.0/2523)
76 nits[j] = math.Pow(math.Max(p-3424.0/4096, 0)/(2413.0/128-p*2392.0/128), 16384.0/2610) * 10000
77 }
78 linear := [3]float64{
79 1.660491*nits[0] - 0.587641*nits[1] - 0.072850*nits[2],
80 -0.124550*nits[0] + 1.132900*nits[1] - 0.008349*nits[2],
81 -0.018151*nits[0] - 0.100579*nits[1] + 1.118730*nits[2],
82 }
83 want := color.RGBA{A: 255}
84 channels := []*uint8{&want.R, &want.G, &want.B}
85 for j, n := range linear {
86 v := math.Max(0, math.Min(n/203, 1))
87 if v <= .0031308 {
88 v *= 12.92
89 } else {
90 v = 1.055*math.Pow(v, 1/2.4) - .055
91 }
92 // Composition blends premultiplied source onto a black opaque fill.
93 *channels[j] = uint8(math.Round(v * alpha * 255))
94 }
95 got := readPixels(t, r).RGBAAt(i*10+5, 2)
96 if !near(got, want, 4) {
97 t.Errorf("sample %d: got %v want %v", i, got, want)
98 }
99 }
100 })
101 }
102}