1package vulkan
2
3import (
4 "image"
5 "image/color"
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// udmabuf turns memfd pages into a real dmabuf: a linear buffer any GPU
16// driver imports, like a client's.
17func udmabuf(t *testing.T, w, h int, fill func(x, y int) [4]byte) *os.File {
18 t.Helper()
19 dev, err := os.OpenFile("/dev/udmabuf", os.O_RDWR, 0)
20 if err != nil {
21 t.Skipf("udmabuf unavailable: %v", err)
22 }
23 defer dev.Close()
24 size := (w*h*4 + os.Getpagesize() - 1) / os.Getpagesize() * os.Getpagesize()
25 mem, err := unix.MemfdCreate("dmabuf-test", unix.MFD_ALLOW_SEALING)
26 if err != nil {
27 t.Fatal(err)
28 }
29 defer unix.Close(mem)
30 if err := unix.Ftruncate(mem, int64(size)); err != nil {
31 t.Fatal(err)
32 }
33 data, err := unix.Mmap(mem, 0, size, unix.PROT_READ|unix.PROT_WRITE, unix.MAP_SHARED)
34 if err != nil {
35 t.Fatal(err)
36 }
37 for y := range h {
38 for x := range w {
39 p := fill(x, y)
40 copy(data[(y*w+x)*4:], p[:])
41 }
42 }
43 _ = unix.Munmap(data)
44 if _, err := unix.FcntlInt(uintptr(mem), unix.F_ADD_SEALS, unix.F_SEAL_SHRINK); err != nil {
45 t.Fatal(err)
46 }
47 // struct udmabuf_create { u32 memfd; u32 flags; u64 offset; u64 size; }
48 arg := struct {
49 memfd, flags uint32
50 offset, size uint64
51 }{memfd: uint32(mem), flags: 1 /* CLOEXEC */, size: uint64(size)}
52 const udmabufCreate = 0x40187542 // _IOW('u', 0x42, struct udmabuf_create)
53 fd, _, errno := unix.Syscall(unix.SYS_IOCTL, dev.Fd(), udmabufCreate, uintptr(unsafe.Pointer(&arg)))
54 if errno != 0 {
55 t.Skipf("udmabuf create: %v", errno)
56 }
57 f := os.NewFile(fd, "dmabuf")
58 t.Cleanup(func() { f.Close() })
59 return f
60}
61
62// A linear ARGB8888 dmabuf is imported and drawn like wl_shm pixels, and
63// the import is freed once no frame draws it.
64func TestRendererImportsDMABuf(t *testing.T) {
65 r, err := New(80, 32)
66 if err != nil {
67 t.Skipf("Vulkan unavailable: %v", err)
68 }
69 defer r.Close()
70 linear := ports.DMABufFormat{Format: fourcc('A', 'R', '2', '4'), Modifier: 0}
71 if !slices.Contains(r.DMABuf().Formats, linear) {
72 t.Skipf("linear ARGB8888 not importable: %+v", r.DMABuf())
73 }
74 // Left half red, right half blue (B,G,R,A in memory). 64 pixels wide:
75 // drivers want linear rows aligned (256 bytes on AMD).
76 f := udmabuf(t, 64, 16, func(x, _ int) [4]byte {
77 if x < 32 {
78 return [4]byte{0, 0, 255, 255}
79 }
80 return [4]byte{255, 0, 0, 255}
81 })
82 buf := &ports.DMABuf{ID: 7, Width: 64, Height: 16, Format: linear.Format, Planes: []ports.DMABufPlane{{File: f, Stride: 256}}}
83 scene := ports.Scene{Background: "#000000", Windows: []ports.SceneWindow{{ID: 1, Rect: ports.Rect{X: 4, Y: 4, W: 64, H: 16}}}}
84 contents := map[ports.WindowID]ports.SurfaceContent{1: {ID: 1, Width: 64, Height: 16, DMABuf: buf}}
85 if err := render(r, scene, contents); err != nil {
86 t.Fatal(err)
87 }
88 px := readPixels(t, r)
89 for _, c := range []struct {
90 x, y int
91 want color.RGBA
92 }{{4, 4, color.RGBA{255, 0, 0, 255}}, {35, 19, color.RGBA{255, 0, 0, 255}}, {36, 4, color.RGBA{0, 0, 255, 255}}, {67, 19, color.RGBA{0, 0, 255, 255}}, {2, 2, color.RGBA{0, 0, 0, 255}}, {70, 10, color.RGBA{0, 0, 0, 255}}} {
93 if got := px.At(c.x, c.y); got != c.want {
94 t.Errorf("At(%d,%d)=%v want %v", c.x, c.y, got, c.want)
95 }
96 }
97 if len(r.imports) != 1 {
98 t.Fatalf("imports %d", len(r.imports))
99 }
100 // Frames without the buffer: its import is released after importTTL.
101 for range importTTL + 1 {
102 if err := render(r, scene, nil); err != nil {
103 t.Fatal(err)
104 }
105 }
106 if len(r.imports) != 0 {
107 t.Fatalf("import kept: %d", len(r.imports))
108 }
109}
110
111// The HDR swapchain's DRM 2101010 formats use packed UNORM views, not
112// automatic sRGB decoding. The same imports work in SDR composition.
113func TestTenBitDMABufFormats(t *testing.T) {
114 for _, tc := range []struct {
115 name [4]byte
116 opaque bool
117 }{
118 {[4]byte{'A', 'R', '3', '0'}, false},
119 {[4]byte{'X', 'R', '3', '0'}, true},
120 {[4]byte{'A', 'B', '3', '0'}, false},
121 {[4]byte{'X', 'B', '3', '0'}, true},
122 } {
123 format, opaque, ok := vkFormat(fourcc(tc.name[0], tc.name[1], tc.name[2], tc.name[3]))
124 if !ok || format == 0 || opaque != tc.opaque {
125 t.Errorf("%q: format %v opaque %t available %t", tc.name, format, opaque, ok)
126 }
127 }
128}
129
130// A subsurface dmabuf is read after its own acquire fence, not the root's.
131func TestRendererSubsurfaceAcquire(t *testing.T) {
132 r, err := New(80, 32)
133 if err != nil {
134 t.Skipf("Vulkan unavailable: %v", err)
135 }
136 defer r.Close()
137 linear := ports.DMABufFormat{Format: fourcc('A', 'R', '2', '4'), Modifier: 0}
138 if !slices.Contains(r.DMABuf().Formats, linear) {
139 t.Skipf("linear ARGB8888 not importable: %+v", r.DMABuf())
140 }
141 f := udmabuf(t, 64, 16, func(int, int) [4]byte { return [4]byte{0, 0, 255, 255} })
142 fence, w, _ := os.Pipe()
143 defer fence.Close()
144 defer w.Close()
145 child := ports.SurfaceContent{ID: 1, Width: 64, Height: 16, Acquire: fence,
146 DMABuf: &ports.DMABuf{ID: 8, Width: 64, Height: 16, Format: linear.Format, Planes: []ports.DMABufPlane{{File: f, Stride: 256}}}}
147 scene := ports.Scene{Background: "#000000", Windows: []ports.SceneWindow{{ID: 1, Rect: ports.Rect{W: 64, H: 16}}}}
148 contents := map[ports.WindowID]ports.SurfaceContent{1: {ID: 1, Width: 64, Height: 16, Children: []ports.Subsurface{{SurfaceContent: child}}}}
149 ds := r.draws(scene, contents, newDamage(r.target(), scene, image.Rect(0, 0, 80, 32)))
150 for _, d := range ds {
151 if d.im != nil && d.acquire == fence {
152 return
153 }
154 }
155 t.Fatal("subsurface draw has no acquire fence")
156}