1//go:build linux
2
3package syncobj
4
5import (
6 "errors"
7 "os"
8 "testing"
9 "unsafe"
10
11 "golang.org/x/sys/unix"
12)
13
14func TestUAPILayouts(t *testing.T) {
15 for _, tc := range []struct {
16 name string
17 got, want uintptr
18 }{
19 {"create", unsafe.Sizeof(createArg{}), 8},
20 {"destroy", unsafe.Sizeof(destroyArg{}), 8},
21 {"handle", unsafe.Sizeof(handleArg{}), 24},
22 {"transfer", unsafe.Sizeof(transferArg{}), 32},
23 {"wait", unsafe.Sizeof(timelineWaitArg{}), 48},
24 {"query", unsafe.Sizeof(queryArg{}), 24},
25 {"eventfd", unsafe.Sizeof(eventFDArg{}), 24},
26 } {
27 if tc.got != tc.want {
28 t.Errorf("%s size: got %d want %d", tc.name, tc.got, tc.want)
29 }
30 }
31}
32
33func TestRenderNodeIoctls(t *testing.T) {
34 path := os.Getenv("NEFERGUI_RENDER_NODE")
35 if path == "" {
36 t.Skip("opt in with NEFERGUI_RENDER_NODE=/dev/dri/renderD... (matching feedback main_device)")
37 }
38 n, err := Open(path)
39 if err != nil {
40 t.Fatal(err)
41 }
42 defer n.Close()
43 acquire, err := n.Create()
44 if err != nil {
45 t.Fatalf("SYNCOBJ_CREATE acquire: %v", err)
46 }
47 defer n.Destroy(acquire)
48 release, err := n.Create()
49 if err != nil {
50 t.Fatalf("SYNCOBJ_CREATE release: %v", err)
51 }
52 defer n.Destroy(release)
53 fd, err := n.ExportTimeline(acquire)
54 if err != nil {
55 t.Fatalf("HANDLE_TO_FD timeline: %v", err)
56 }
57 defer unix.Close(fd)
58 // Import as another handle to prove this FD is a DRM syncobj, not a
59 // Vulkan OPAQUE_FD. FD_TO_HANDLE without IMPORT_SYNC_FILE does not consume fd.
60 a := handleArg{FD: int32(fd)}
61 if err = n.ioctl(ioctlFDToHandle, unsafe.Pointer(&a)); err != nil {
62 t.Fatalf("FD_TO_HANDLE timeline: %v", err)
63 }
64 defer n.Destroy(a.Handle)
65 if a.Handle == 0 {
66 t.Fatal("zero imported handle")
67 }
68 before, err := n.Query(release)
69 if err != nil {
70 t.Fatalf("QUERY release: %v", err)
71 }
72 t.Logf("render node %s: CREATE=%d,%d HANDLE_TO_FD=%d FD_TO_HANDLE=%d QUERY=%d", path, acquire, release, fd, a.Handle, before)
73 // An unsignaled point cannot be used for image recycling. TIMELINE_WAIT
74 // must time out without a CPU fence fallback.
75 ready, err := n.Signaled(release, 1)
76 if err != nil {
77 t.Fatalf("TIMELINE_WAIT: %v", err)
78 }
79 if ready {
80 t.Fatal("unsignaled point reported ready")
81 }
82 // TRANSFER may be rejected for a point not yet submitted. A valid
83 // SYNC_FD import is exercised by an actual Vulkan submission test.
84 if err = n.Transfer(acquire, 1, release, 2); err != nil {
85 t.Logf("TRANSFER unsubmitted point (expected kernel-dependent rejection): %v", err)
86 } else {
87 t.Log("TRANSFER accepted unsubmitted point")
88 }
89}
90
91func TestFailedImportRetainsFD(t *testing.T) {
92 // No render node needed: EBADF leaves the caller's descriptor owned.
93 fd, err := unix.Eventfd(0, unix.EFD_CLOEXEC)
94 if err != nil {
95 t.Fatal(err)
96 }
97 defer unix.Close(fd)
98 n := &Node{fd: -1}
99 if _, err = n.ImportFence(fd); !errors.Is(err, unix.EBADF) {
100 t.Fatalf("want EBADF, got %v", err)
101 }
102 if _, err = unix.FcntlInt(uintptr(fd), unix.F_GETFD, 0); err != nil {
103 t.Fatalf("lost caller FD after failed import: %v", err)
104 }
105}
106
107// DRM_IOWR(type 'd', nr, size): direction 3<<30, size<<16, type<<8, nr.
108func TestEventFDRequestNumber(t *testing.T) {
109 want := uintptr(3<<30 | unsafe.Sizeof(eventFDArg{})<<16 | 'd'<<8 | 0xCF)
110 if ioctlEventFD != want {
111 t.Fatalf("DRM_IOCTL_SYNCOBJ_EVENTFD: got %#x want %#x", ioctlEventFD, want)
112 }
113 want = uintptr(3<<30 | unsafe.Sizeof(queryArg{})<<16 | 'd'<<8 | 0xCD)
114 if ioctlTimelineSig != want {
115 t.Fatalf("DRM_IOCTL_SYNCOBJ_TIMELINE_SIGNAL: got %#x want %#x", ioctlTimelineSig, want)
116 }
117}
118
119func TestEventFDBadFDFailsWithErrno(t *testing.T) {
120 n := &Node{fd: -1}
121 if err := n.EventFD(1, 1, 0); !errors.Is(err, unix.EBADF) {
122 t.Fatalf("want EBADF, got %v", err)
123 }
124}
125
126// A point signaled from the CPU makes the registered eventfd readable without
127// any waiter thread; the registration is one-shot.
128func TestEventFDSignaledPointHardware(t *testing.T) {
129 path := os.Getenv("NEFERGUI_RENDER_NODE")
130 if path == "" {
131 t.Skip("opt in with NEFERGUI_RENDER_NODE=/dev/dri/renderD...")
132 }
133 n, err := Open(path)
134 if err != nil {
135 t.Fatal(err)
136 }
137 defer n.Close()
138 handle, err := n.Create()
139 if err != nil {
140 t.Fatal(err)
141 }
142 defer n.Destroy(handle)
143 efd, err := unix.Eventfd(0, unix.EFD_CLOEXEC|unix.EFD_NONBLOCK)
144 if err != nil {
145 t.Fatal(err)
146 }
147 defer unix.Close(efd)
148 if err = n.EventFD(handle, 7, efd); err != nil {
149 t.Fatalf("EventFD: %v", err)
150 }
151 readable := func(timeout int) bool {
152 fds := []unix.PollFd{{Fd: int32(efd), Events: unix.POLLIN}}
153 r, err := unix.Poll(fds, timeout)
154 if err != nil {
155 t.Fatal(err)
156 }
157 return r > 0
158 }
159 if readable(0) {
160 t.Fatal("eventfd readable before the point signaled")
161 }
162 if err = n.Signal(handle, 7); err != nil {
163 t.Fatalf("Signal: %v", err)
164 }
165 if !readable(1000) {
166 t.Fatal("eventfd not readable after the point signaled")
167 }
168 var buf [8]byte
169 if _, err = unix.Read(efd, buf[:]); err != nil {
170 t.Fatal(err)
171 }
172 if readable(0) {
173 t.Fatal("eventfd still readable after read")
174 }
175 // Re-arming for an already signaled point fires immediately.
176 if err = n.EventFD(handle, 7, efd); err != nil {
177 t.Fatal(err)
178 }
179 if !readable(1000) {
180 t.Fatal("re-armed eventfd for a signaled point stayed silent")
181 }
182}