1package wayland
2
3import (
4 "context"
5 "os"
6 "strings"
7 "sync"
8 "testing"
9 "time"
10 "unsafe"
11
12 "github.com/bnema/neferwl/internal/logging"
13 "github.com/bnema/neferwl/internal/ports"
14 "github.com/bnema/purego-libwayland/protocol/linuxdmabuf"
15 "github.com/bnema/purego-libwayland/protocol/linuxdrmsyncobj"
16 "github.com/bnema/purego-libwayland/protocol/wayland"
17 "github.com/bnema/wlturbo"
18 "github.com/stretchr/testify/mock"
19 "golang.org/x/sys/unix"
20)
21
22func TestSyncobjStructSizes(t *testing.T) {
23 for _, tc := range []struct {
24 name string
25 got uintptr
26 req uintptr
27 }{
28 {"destroy", unsafe.Sizeof(syncobjDestroy{}), ioctlSyncobjDestroy},
29 {"handle_to_fd", unsafe.Sizeof(syncobjHandle{}), ioctlSyncobjHandleToFD},
30 {"fd_to_handle", unsafe.Sizeof(syncobjHandle{}), ioctlSyncobjFDToHandle},
31 {"timeline_signal", unsafe.Sizeof(syncobjTimelineArray{}), ioctlSyncobjTimelineSignal},
32 {"eventfd", unsafe.Sizeof(syncobjEventfd{}), ioctlSyncobjEventfd},
33 {"get_cap", unsafe.Sizeof(drmCap{}), ioctlGetCap},
34 } {
35 if want := (tc.req >> 16) & 0x3fff; tc.got != want {
36 t.Errorf("%s: size %d, ioctl encodes %d", tc.name, tc.got, want)
37 }
38 }
39}
40
41// syncHarness is a server with a mocked syncobj device and a client with
42// a mapped window, a dmabuf buffer and the syncobj objects.
43type syncHarness struct {
44 t *testing.T
45 s *Server
46 c *wlturbo.Display
47 dev *mocksyncobjDevice
48 contents chan ports.SurfaceContent
49 presented chan ports.OutputPresented
50 surf uint32
51 syncSurf uint32
52 timeline uint32
53 win ports.WindowID
54 mu sync.Mutex
55 efds map[uint64]int // eventfd by acquire point
56 signals []uint64
57 fences []*os.File
58 releases map[uint32]*syncReleaseProxy
59 mgr uint32
60}
61
62func newSyncHarness(t *testing.T) *syncHarness {
63 h := &syncHarness{t: t, efds: map[uint64]int{}}
64 h.dev = newMocksyncobjDevice(t)
65 h.dev.EXPECT().fdToHandle(mock.Anything).Return(7, nil).Maybe()
66 h.dev.EXPECT().destroy(mock.Anything).Return(nil).Maybe()
67 h.dev.EXPECT().eventfd(uint32(7), mock.Anything, mock.Anything).RunAndReturn(func(_ uint32, point uint64, efd int) error {
68 // Keep a duplicate: the waiter closes its own when it fires.
69 dup, err := unix.Dup(efd)
70 if err != nil {
71 return err
72 }
73 h.mu.Lock()
74 h.efds[point] = dup
75 h.mu.Unlock()
76 return nil
77 }).Maybe()
78 h.dev.EXPECT().exportSyncFile(uint32(7), mock.Anything).RunAndReturn(func(uint32, uint64) (*os.File, error) {
79 r, w, err := os.Pipe()
80 if err != nil {
81 return nil, err
82 }
83 w.Close()
84 h.mu.Lock()
85 h.fences = append(h.fences, r)
86 h.mu.Unlock()
87 return r, nil
88 }).Maybe()
89 h.dev.EXPECT().signal(uint32(7), mock.Anything).RunAndReturn(func(_ uint32, point uint64) error {
90 h.mu.Lock()
91 h.signals = append(h.signals, point)
92 h.mu.Unlock()
93 return nil
94 }).Maybe()
95 dir := t.TempDir()
96 events := make(chan ports.ClientEvent, 16)
97 commands := make(chan ports.ClientCommand, 16)
98 h.contents = make(chan ports.SurfaceContent, 64)
99 h.presented = make(chan ports.OutputPresented, 16)
100 sup := ports.DMABufSupport{Formats: []ports.DMABufFormat{linearARGB}}
101 s, err := New(Options{RuntimeDir: dir, Outputs: testOutputs, DMABuf: sup, syncDev: h.dev}, Channels{Events: events, Commands: commands, Contents: h.contents, Presented: h.presented}, logging.For(context.Background(), "wayland"))
102 if err != nil {
103 t.Fatal(err)
104 }
105 ctx, cancel := context.WithCancel(context.Background())
106 done := make(chan error, 1)
107 go func() { done <- s.Run(ctx) }()
108 t.Cleanup(func() {
109 cancel()
110 <-done
111 h.mu.Lock()
112 for _, fd := range h.efds {
113 unix.Close(fd)
114 }
115 h.mu.Unlock()
116 })
117 h.s = s
118 h.c = protocolClient(t, s, dir)
119 w, surf, xdg := surfaceMapper(t, h.c, events)()
120 registerProtocol(t, h.c, xdg)
121 h.win, h.surf = w.ID, surf
122 commands <- ports.ConfigureWindow{ID: w.ID, Width: 64, Height: 16, Output: "HEADLESS-1"}
123 mgr := bindProtocol(t, h.c, "wp_linux_drm_syncobj_manager_v1")
124 h.syncSurf = h.c.AllocateID()
125 requestProtocol(t, h.c, mgr, linuxdrmsyncobj.WpLinuxDrmSyncobjManagerV1RequestGetSurface, h.syncSurf, surf)
126 registerProtocol(t, h.c, h.syncSurf)
127 fd, err := unix.MemfdCreate("timeline", 0)
128 if err != nil {
129 t.Fatal(err)
130 }
131 defer unix.Close(fd)
132 h.timeline = h.c.AllocateID()
133 registerProtocol(t, h.c, h.timeline)
134 if err := h.c.SendRequestWithFDs(mgr, uint16(linuxdrmsyncobj.WpLinuxDrmSyncobjManagerV1RequestImportTimeline), []int{fd}, h.timeline); err != nil {
135 t.Fatal(err)
136 }
137 for deadline := time.Now().Add(2 * time.Second); ; {
138 var out string
139 s.display.Do(func() { out = s.frameOutput(s.windows[w.ID].xdg.surface) })
140 if out != "" {
141 break
142 }
143 if time.Now().After(deadline) {
144 t.Fatal("configure not applied")
145 }
146 time.Sleep(2 * time.Millisecond)
147 }
148 return h
149}
150
151// dmabuf makes a client dmabuf wl_buffer (a memfd stands in for it).
152func (h *syncHarness) dmabuf() uint32 {
153 t, c := h.t, h.c
154 dm := bindVersion(t, c, "zwp_linux_dmabuf_v1", dmabufVersion)
155 fd, err := unix.MemfdCreate("buf", 0)
156 if err != nil {
157 t.Fatal(err)
158 }
159 defer unix.Close(fd)
160 if err := unix.Ftruncate(fd, 256*16); err != nil {
161 t.Fatal(err)
162 }
163 p := c.AllocateID()
164 requestProtocol(t, c, dm, linuxdmabuf.ZwpLinuxDmabufV1RequestCreateParams, p)
165 registerProtocol(t, c, p)
166 if err := c.SendRequestWithFDs(p, uint16(linuxdmabuf.ZwpLinuxBufferParamsV1RequestAdd), []int{fd}, uint32(0), uint32(0), uint32(256), uint32(0), uint32(0)); err != nil {
167 t.Fatal(err)
168 }
169 b := c.AllocateID()
170 rel := &syncReleaseProxy{released: make(chan struct{}, 4)}
171 rel.SetID(b)
172 c.Context().Register(rel)
173 requestProtocol(t, c, p, linuxdmabuf.ZwpLinuxBufferParamsV1RequestCreateImmed, b, int32(64), int32(16), linearARGB.Format, uint32(0))
174 h.releases[b] = rel
175 return b
176}
177
178type syncReleaseProxy struct {
179 wlturbo.BaseProxy
180 released chan struct{}
181}
182
183func (p *syncReleaseProxy) Dispatch(e *wlturbo.Event) {
184 if uint32(e.Opcode) == wayland.BufferEventRelease {
185 p.released <- struct{}{}
186 }
187}
188
189func (h *syncHarness) setPoints(acquire, release uint64) {
190 requestProtocol(h.t, h.c, h.syncSurf, linuxdrmsyncobj.WpLinuxDrmSyncobjSurfaceV1RequestSetAcquirePoint, h.timeline, uint32(acquire>>32), uint32(acquire))
191 requestProtocol(h.t, h.c, h.syncSurf, linuxdrmsyncobj.WpLinuxDrmSyncobjSurfaceV1RequestSetReleasePoint, h.timeline, uint32(release>>32), uint32(release))
192}
193
194func (h *syncHarness) commit(buf uint32, acquire, release uint64) {
195 t, c := h.t, h.c
196 requestProtocol(t, c, h.surf, wayland.SurfaceRequestAttach, buf, int32(0), int32(0))
197 h.setPoints(acquire, release)
198 requestProtocol(t, c, h.surf, wayland.SurfaceRequestCommit)
199 if err := c.Roundtrip(); err != nil {
200 t.Fatal(err)
201 }
202}
203
204// content waits for a DMABuf content of the window.
205func (h *syncHarness) content(wait time.Duration) (ports.SurfaceContent, bool) {
206 deadline := time.After(wait)
207 for {
208 select {
209 case c := <-h.contents:
210 if c.ID == h.win && c.DMABuf != nil {
211 return c, true
212 }
213 case <-deadline:
214 return ports.SurfaceContent{}, false
215 }
216 }
217}
218
219// fire makes the kernel's eventfd for an acquire point readable.
220func (h *syncHarness) fire(point uint64) {
221 h.t.Helper()
222 deadline := time.Now().Add(2 * time.Second)
223 for {
224 h.mu.Lock()
225 fd, ok := h.efds[point]
226 h.mu.Unlock()
227 if ok {
228 var one [8]byte
229 one[0] = 1
230 if _, err := unix.Write(fd, one[:]); err != nil {
231 h.t.Fatal(err)
232 }
233 return
234 }
235 if time.Now().After(deadline) {
236 h.t.Fatalf("no eventfd for point %d", point)
237 }
238 time.Sleep(2 * time.Millisecond)
239 }
240}
241
242func (h *syncHarness) signalled() []uint64 {
243 h.mu.Lock()
244 defer h.mu.Unlock()
245 return append([]uint64(nil), h.signals...)
246}
247
248// A commit waits for its acquire point; the content carries the fence;
249// replacing it signals the release point once outputs moved on, and
250// wl_buffer.release is never sent. The acquire fence closes only then.
251func TestSyncobjAcquireAndRelease(t *testing.T) {
252 h := newSyncHarness(t)
253 h.releases = map[uint32]*syncReleaseProxy{}
254 a, b := h.dmabuf(), h.dmabuf()
255 h.commit(a, 1, 2)
256 if _, ok := h.content(50 * time.Millisecond); ok {
257 t.Fatal("content before the acquire point had a fence")
258 }
259 h.fire(1)
260 first, ok := h.content(2 * time.Second)
261 if !ok || first.Acquire == nil {
262 t.Fatalf("content %+v", first)
263 }
264 h.commit(b, 3, 4)
265 h.fire(3)
266 second, ok := h.content(2 * time.Second)
267 if !ok {
268 t.Fatal("second content")
269 }
270 if _, err := first.Acquire.Stat(); err != nil {
271 t.Fatal("acquire fence closed before release")
272 }
273 // The output read the second content: the first buffer is released.
274 h.presented <- ports.OutputPresented{Output: "HEADLESS-1", Seen: map[ports.WindowID]uint64{h.win: second.Seq}}
275 deadline := time.Now().Add(2 * time.Second)
276 for len(h.signalled()) == 0 && time.Now().Before(deadline) {
277 if err := h.c.Roundtrip(); err != nil {
278 t.Fatal(err)
279 }
280 time.Sleep(2 * time.Millisecond)
281 }
282 if got := h.signalled(); len(got) != 1 || got[0] != 2 {
283 t.Fatalf("signalled %v", got)
284 }
285 if _, err := first.Acquire.Stat(); err == nil {
286 t.Fatal("acquire fence left open after release")
287 }
288 if err := h.c.Roundtrip(); err != nil {
289 t.Fatal(err)
290 }
291 select {
292 case <-h.releases[a].released:
293 t.Fatal("wl_buffer.release sent under explicit sync")
294 default:
295 }
296}
297
298func TestSyncobjProtocolErrors(t *testing.T) {
299 for _, tc := range []struct {
300 name string
301 run func(h *syncHarness)
302 obj func(h *syncHarness) uint32
303 code uint32
304 }{
305 {"conflicting points", func(h *syncHarness) {
306 b := h.dmabuf()
307 requestProtocol(t, h.c, h.surf, wayland.SurfaceRequestAttach, b, int32(0), int32(0))
308 requestProtocol(t, h.c, h.syncSurf, linuxdrmsyncobj.WpLinuxDrmSyncobjSurfaceV1RequestSetAcquirePoint, h.timeline, uint32(0), uint32(5))
309 requestProtocol(t, h.c, h.syncSurf, linuxdrmsyncobj.WpLinuxDrmSyncobjSurfaceV1RequestSetReleasePoint, h.timeline, uint32(0), uint32(5))
310 requestProtocol(t, h.c, h.surf, wayland.SurfaceRequestCommit)
311 }, func(h *syncHarness) uint32 { return h.syncSurf }, uint32(linuxdrmsyncobj.WpLinuxDrmSyncobjSurfaceV1ErrorConflictingPoints)},
312 {"no release point", func(h *syncHarness) {
313 b := h.dmabuf()
314 requestProtocol(t, h.c, h.surf, wayland.SurfaceRequestAttach, b, int32(0), int32(0))
315 requestProtocol(t, h.c, h.syncSurf, linuxdrmsyncobj.WpLinuxDrmSyncobjSurfaceV1RequestSetAcquirePoint, h.timeline, uint32(0), uint32(5))
316 requestProtocol(t, h.c, h.surf, wayland.SurfaceRequestCommit)
317 }, func(h *syncHarness) uint32 { return h.syncSurf }, uint32(linuxdrmsyncobj.WpLinuxDrmSyncobjSurfaceV1ErrorNoReleasePoint)},
318 {"buffer without points", func(h *syncHarness) {
319 b := h.dmabuf()
320 requestProtocol(t, h.c, h.surf, wayland.SurfaceRequestAttach, b, int32(0), int32(0))
321 requestProtocol(t, h.c, h.surf, wayland.SurfaceRequestCommit)
322 }, func(h *syncHarness) uint32 { return h.syncSurf }, uint32(linuxdrmsyncobj.WpLinuxDrmSyncobjSurfaceV1ErrorNoAcquirePoint)},
323 {"no acquire point", func(h *syncHarness) {
324 b := h.dmabuf()
325 requestProtocol(t, h.c, h.surf, wayland.SurfaceRequestAttach, b, int32(0), int32(0))
326 requestProtocol(t, h.c, h.syncSurf, linuxdrmsyncobj.WpLinuxDrmSyncobjSurfaceV1RequestSetReleasePoint, h.timeline, uint32(0), uint32(5))
327 requestProtocol(t, h.c, h.surf, wayland.SurfaceRequestCommit)
328 }, func(h *syncHarness) uint32 { return h.syncSurf }, uint32(linuxdrmsyncobj.WpLinuxDrmSyncobjSurfaceV1ErrorNoAcquirePoint)},
329 {"points without attach", func(h *syncHarness) {
330 b := h.dmabuf()
331 h.commit(b, 1, 2)
332 h.fire(1)
333 h.setPoints(3, 4)
334 requestProtocol(t, h.c, h.surf, wayland.SurfaceRequestCommit)
335 }, func(h *syncHarness) uint32 { return h.syncSurf }, uint32(linuxdrmsyncobj.WpLinuxDrmSyncobjSurfaceV1ErrorNoBuffer)},
336 {"points with null attach", func(h *syncHarness) {
337 requestProtocol(t, h.c, h.surf, wayland.SurfaceRequestAttach, uint32(0), int32(0), int32(0))
338 h.setPoints(1, 2)
339 requestProtocol(t, h.c, h.surf, wayland.SurfaceRequestCommit)
340 }, func(h *syncHarness) uint32 { return h.syncSurf }, uint32(linuxdrmsyncobj.WpLinuxDrmSyncobjSurfaceV1ErrorNoBuffer)},
341 {"wl_shm buffer", func(h *syncHarness) {
342 b := shmBuffer(t, h.c)
343 requestProtocol(t, h.c, h.surf, wayland.SurfaceRequestAttach, b, int32(0), int32(0))
344 h.setPoints(1, 2)
345 requestProtocol(t, h.c, h.surf, wayland.SurfaceRequestCommit)
346 }, func(h *syncHarness) uint32 { return h.syncSurf }, uint32(linuxdrmsyncobj.WpLinuxDrmSyncobjSurfaceV1ErrorUnsupportedBuffer)},
347 {"surface destroyed", func(h *syncHarness) {
348 requestProtocol(t, h.c, h.surf, wayland.SurfaceRequestDestroy)
349 h.setPoints(1, 2)
350 }, func(h *syncHarness) uint32 { return h.syncSurf }, uint32(linuxdrmsyncobj.WpLinuxDrmSyncobjSurfaceV1ErrorNoSurface)},
351 {"surface exists", func(h *syncHarness) {
352 mgr := bindProtocol(t, h.c, "wp_linux_drm_syncobj_manager_v1")
353 h.mgr = mgr
354 requestProtocol(t, h.c, mgr, linuxdrmsyncobj.WpLinuxDrmSyncobjManagerV1RequestGetSurface, h.c.AllocateID(), h.surf)
355 }, func(h *syncHarness) uint32 { return h.mgr }, uint32(linuxdrmsyncobj.WpLinuxDrmSyncobjManagerV1ErrorSurfaceExists)},
356 } {
357 t.Run(tc.name, func(t *testing.T) {
358 h := newSyncHarness(t)
359 h.releases = map[uint32]*syncReleaseProxy{}
360 h.t = t
361 tc.run(h)
362 expectProtocolError(t, h.c, tc.obj(h), tc.code)
363 })
364 }
365}
366
367// An invalid timeline fd is a protocol error.
368func TestSyncobjInvalidTimeline(t *testing.T) {
369 dev := newMocksyncobjDevice(t)
370 dev.EXPECT().fdToHandle(mock.Anything).Return(0, unix.EINVAL).Once()
371 dir := t.TempDir()
372 s, err := New(Options{RuntimeDir: dir, Outputs: testOutputs, syncDev: dev}, Channels{}, logging.For(context.Background(), "wayland"))
373 if err != nil {
374 t.Fatal(err)
375 }
376 ctx, cancel := context.WithCancel(context.Background())
377 done := make(chan error, 1)
378 go func() { done <- s.Run(ctx) }()
379 t.Cleanup(func() { cancel(); <-done })
380 c := protocolClient(t, s, dir)
381 mgr := bindProtocol(t, c, "wp_linux_drm_syncobj_manager_v1")
382 fd, _ := unix.MemfdCreate("bad", 0)
383 defer unix.Close(fd)
384 if err := c.SendRequestWithFDs(mgr, uint16(linuxdrmsyncobj.WpLinuxDrmSyncobjManagerV1RequestImportTimeline), []int{fd}, c.AllocateID()); err != nil {
385 t.Fatal(err)
386 }
387 expectProtocolError(t, c, mgr, uint32(linuxdrmsyncobj.WpLinuxDrmSyncobjManagerV1ErrorInvalidTimeline))
388}
389
390// A commit dropped before its acquire point has a fence signals nothing
391// (the client has not submitted that point) and sends no wl_buffer.release;
392// the waiter closes its eventfd and no acquire file is left open.
393func TestSyncobjDroppedCommit(t *testing.T) {
394 h := newSyncHarness(t)
395 h.releases = map[uint32]*syncReleaseProxy{}
396 a := h.dmabuf()
397 before := openFDs(t)
398 h.commit(a, 1, 2)
399 requestProtocol(t, h.c, h.surf, wayland.SurfaceRequestDestroy)
400 if err := h.c.Roundtrip(); err != nil {
401 t.Fatal(err)
402 }
403 // The point never gets a fence: the waiter must still stop polling it.
404 time.Sleep(250 * time.Millisecond)
405 if err := h.c.Roundtrip(); err != nil {
406 t.Fatal(err)
407 }
408 if got := h.signalled(); len(got) != 0 {
409 t.Fatalf("signalled %v for a commit whose acquire point never arrived", got)
410 }
411 select {
412 case <-h.releases[a].released:
413 t.Fatal("wl_buffer.release sent under explicit sync")
414 default:
415 }
416 // The harness keeps its own duplicate of the eventfd.
417 if after := openFDs(t); after > before+1 {
418 t.Fatalf("eventfds and sync files %d -> %d", before, after)
419 }
420}
421
422// A commit dropped after its acquire point fired signals its release point.
423func TestSyncobjDroppedReadyCommitReleases(t *testing.T) {
424 h := newSyncHarness(t)
425 h.releases = map[uint32]*syncReleaseProxy{}
426 a := h.dmabuf()
427 h.commit(a, 1, 2)
428 h.fire(1)
429 if _, ok := h.content(2 * time.Second); !ok {
430 t.Fatal("content")
431 }
432 requestProtocol(t, h.c, h.surf, wayland.SurfaceRequestDestroy)
433 deadline := time.Now().Add(2 * time.Second)
434 for len(h.signalled()) == 0 && time.Now().Before(deadline) {
435 if err := h.c.Roundtrip(); err != nil {
436 t.Fatal(err)
437 }
438 time.Sleep(2 * time.Millisecond)
439 }
440 if got := h.signalled(); len(got) != 1 || got[0] != 2 {
441 t.Fatalf("signalled %v", got)
442 }
443}
444
445// openFDs counts the eventfds and sync files (pipes stand in for them)
446// the process holds.
447func openFDs(t *testing.T) int {
448 t.Helper()
449 ents, err := os.ReadDir("/proc/self/fd")
450 if err != nil {
451 t.Fatal(err)
452 }
453 n := 0
454 for _, e := range ents {
455 l, _ := os.Readlink("/proc/self/fd/" + e.Name())
456 if strings.Contains(l, "eventfd") || strings.HasPrefix(l, "pipe:") {
457 n++
458 }
459 }
460 return n
461}