feat/data-dnd e05cb15

yuv_test.go RawHistoryFolder

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}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 25

Tags 10

↑↓ moveenter openesc close