v0.1.0 d9218d6

syncobj_test.go RawHistoryFolder

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}

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 23

Tags 10

↑↓ moveenter openesc close