1package vulkan
2
3import (
4 "image/color"
5 "math"
6 "os"
7 "slices"
8 "testing"
9 "unsafe"
10
11 "github.com/bnema/neferwl/internal/ports"
12 "golang.org/x/sys/unix"
13)
14
15// Each plane can be backed by a separate DMA-BUF, as with VAAPI exports.
16func yuvTestBuffer(t *testing.T, p010 bool, y, u, v int, rows ...[3]int) *ports.DMABuf {
17 t.Helper()
18 format := fourcc('N', 'V', '1', '2')
19 if p010 {
20 format = fourcc('P', '0', '1', '0')
21 }
22 planes := make([]ports.DMABufPlane, 2)
23 dev, err := os.OpenFile("/dev/udmabuf", os.O_RDWR, 0)
24 if err != nil {
25 t.Skipf("udmabuf unavailable: %v", err)
26 }
27 defer dev.Close()
28 // Use a stride supported by the modifier on the device; the test
29 // populates the exact client-provided row pitch.
30 stride := uint32(256)
31
32 ySize := int(stride) * 16
33 uvSize := int(stride) * 8
34 pages := (ySize + uvSize + os.Getpagesize() - 1) / os.Getpagesize() * os.Getpagesize()
35 mem, err := unix.MemfdCreate("yuv-shared", unix.MFD_ALLOW_SEALING)
36 if err != nil {
37 t.Fatal(err)
38 }
39 defer unix.Close(mem)
40 if err := unix.Ftruncate(mem, int64(pages)); err != nil {
41 t.Fatal(err)
42 }
43 data := make([]byte, pages)
44 for i := 0; i < 2; i++ {
45 start, end := 0, ySize
46 values := []int{y}
47 if i == 1 {
48 start, end, values = ySize, ySize+uvSize, []int{u, v}
49 }
50 for n, ch := start, 0; n < end; ch++ {
51 val := values[ch%len(values)]
52 if len(rows) > 0 {
53 if i == 0 && (n-start)/int(stride) < 4 {
54 val = rows[0][0]
55 } else if i == 1 && (n-start)/int(stride) < 2 {
56 val = rows[0][ch%len(values)+1]
57 }
58 }
59 if p010 {
60 code := uint16(val << 6)
61 data[n], data[n+1] = byte(code), byte(code>>8)
62 n += 2
63 } else {
64 data[n] = byte(val)
65 n++
66 }
67 }
68 }
69 if _, err := unix.Pwrite(mem, data, 0); err != nil {
70 t.Fatal(err)
71 }
72 if _, err := unix.FcntlInt(uintptr(mem), unix.F_ADD_SEALS, unix.F_SEAL_SHRINK); err != nil {
73 t.Fatal(err)
74 }
75 arg := struct {
76 memfd, flags uint32
77 offset, size uint64
78 }{memfd: uint32(mem), flags: 1, size: uint64(pages)}
79 fd, _, errno := unix.Syscall(unix.SYS_IOCTL, dev.Fd(), 0x40187542, uintptr(unsafe.Pointer(&arg)))
80 if errno != 0 {
81 t.Skipf("udmabuf create: %v", errno)
82 }
83 f := os.NewFile(fd, "yuv-shared")
84 t.Cleanup(func() { f.Close() })
85 dup, err := unix.Dup(int(f.Fd()))
86 if err != nil {
87 t.Fatal(err)
88 }
89 f2 := os.NewFile(uintptr(dup), "yuv-shared-dup")
90 t.Cleanup(func() { f2.Close() })
91 planes[0] = ports.DMABufPlane{File: f, Stride: stride}
92 planes[1] = ports.DMABufPlane{File: f2, Offset: uint32(ySize), Stride: stride}
93 return &ports.DMABuf{ID: uint64(format) + uint64(y), Width: 64, Height: 16, Format: format, Planes: planes}
94}
95
96func TestYUVComposition(t *testing.T) {
97 for _, tc := range []struct {
98 name string
99 p010, hdr bool
100 coeff uint8
101 y, u, v int
102 rgb [3]float64
103 }{
104 {"NV12 BT709 limited white", false, false, 2, 235, 128, 128, [3]float64{1, 1, 1}},
105 {"NV12 BT709 limited red", false, false, 2, 63, 102, 240, [3]float64{1, 0, 0}},
106 {"NV12 BT601 limited red", false, false, 4, 81, 90, 240, [3]float64{1, 0, 0}},
107 {"P010 BT2020 limited white", true, false, 6, 940, 512, 512, [3]float64{1, 1, 1}},
108 {"P010 BT2020 PQ 1000 nits", true, true, 6, 756, 512, 512, [3]float64{0, 0, 0}},
109 } {
110 t.Run(tc.name, func(t *testing.T) {
111 var r *Renderer
112 if tc.hdr {
113 r = hdrTestRenderer(t)
114 } else {
115 var err error
116 r, err = New(64, 16)
117 if err != nil {
118 t.Skipf("Vulkan unavailable: %v", err)
119 }
120 defer r.Close()
121 }
122 b := yuvTestBuffer(t, tc.p010, tc.y, tc.u, tc.v)
123 if !slices.Contains(r.DMABuf().Formats, ports.DMABufFormat{Format: b.Format}) {
124 t.Skipf("linear YUV %#x not importable", b.Format)
125 }
126 if distinct, err := differentBacking(b.Planes); err != nil || distinct {
127 t.Fatalf("expected duplicate plane fds, distinct=%t err=%v", distinct, err)
128 }
129 if _, err := r.importDMABuf(b); err != nil {
130 t.Fatalf("YUV import: %v", err)
131 }
132 c := ports.SurfaceContent{ID: 1, Width: 64, Height: 16, Opaque: true, DMABuf: b, Color: ports.SurfaceColor{Coefficients: tc.coeff, Range: 2, Chroma: 1}}
133 if tc.hdr {
134 c.Color.TF, c.Color.Primaries = ports.ColorTFPQ, ports.ColorPrimariesBT2020
135 }
136 scene := ports.Scene{Windows: []ports.SceneWindow{{ID: 1, Rect: ports.Rect{W: 64, H: 16}}}}
137 if err := render(r, scene, map[ports.WindowID]ports.SurfaceContent{1: c}); err != nil {
138 t.Fatal(err)
139 }
140 if tc.hdr {
141 // Limited-range P010: PQ code is (Y-64)/876. Round-trip
142 // through fp16 composition and HDR output without clipping.
143 code := (float64(tc.y) - 64) / 876
144 want := pqEncode(pqDecode(code))
145 got := hdrTargetAt(t, r, 8, 8)
146 for _, channel := range got {
147 if math.Abs(channel-want) > .015 {
148 t.Fatalf("PQ %v want %v", got, want)
149 }
150 }
151 // SDR capture tone-maps the 1000-nit white onto the shoulder.
152 linear := pqDecode(code) / 203
153 sdr := captureSDR([3]float64{linear, linear, linear})
154 if px := readPixels(t, r).RGBAAt(8, 8); !near(px, color.RGBA{sdr[0], sdr[1], sdr[2], 255}, 2) || px.A != 255 {
155 t.Fatalf("capture %v want %v", px, sdr)
156 }
157 return
158 }
159 got := readPixels(t, r).RGBAAt(8, 8)
160 want := color.RGBA{uint8(math.Round(tc.rgb[0] * 255)), uint8(math.Round(tc.rgb[1] * 255)), uint8(math.Round(tc.rgb[2] * 255)), 255}
161 if !near(got, want, 8) {
162 t.Errorf("got %v want %v", got, want)
163 }
164 })
165 }
166}
167
168func pqDecode(code float64) float64 {
169 p := math.Pow(code, 32.0/2523)
170 return math.Pow(math.Max(p-3424.0/4096, 0)/(2413.0/128-p*2392.0/128), 16384.0/2610) * 10000
171}
172
173// Luma and chroma rows outside the crop must not bleed into its edge.
174func TestYUVSourceCropEdges(t *testing.T) {
175 r, err := New(64, 16)
176 if err != nil {
177 t.Skipf("Vulkan unavailable: %v", err)
178 }
179 defer r.Close()
180 b := yuvTestBuffer(t, false, 235, 128, 128, [3]int{16, 16, 240})
181 if !slices.Contains(r.DMABuf().Formats, ports.DMABufFormat{Format: b.Format}) {
182 t.Skipf("linear NV12 not importable")
183 }
184 c := ports.SurfaceContent{ID: 1, Width: 64, Height: 16, LogicalW: 64, LogicalH: 16, Source: [4]float32{0, 4, 64, 8}, Opaque: true, DMABuf: b, Color: ports.SurfaceColor{Coefficients: 2, Range: 2, Chroma: 1}}
185 scene := ports.Scene{Windows: []ports.SceneWindow{{ID: 1, Rect: ports.Rect{W: 64, H: 16}}}}
186 if err := render(r, scene, map[ports.WindowID]ports.SurfaceContent{1: c}); err != nil {
187 t.Fatal(err)
188 }
189 for _, y := range []int{0, 1, 8, 15} {
190 if got := readPixels(t, r).RGBAAt(8, y); !near(got, color.RGBA{255, 255, 255, 255}, 8) {
191 t.Errorf("cropped edge y=%d: %v, want white", y, got)
192 }
193 }
194}