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/go-wayland-bindings/server/committiming"
13	"github.com/bnema/go-wayland-bindings/server/fifo"
14	"github.com/bnema/go-wayland-bindings/server/linuxdmabuf"
15	"github.com/bnema/go-wayland-bindings/server/linuxdrmsyncobj"
16	"github.com/bnema/go-wayland-bindings/server/wayland"
17	"github.com/bnema/neferwl/internal/adapters/logging"
18	"github.com/bnema/neferwl/internal/ports"
19	"github.com/bnema/wlturbo"
20	"github.com/stretchr/testify/mock"
21	"golang.org/x/sys/unix"
22)
23
24func TestSyncobjStructSizes(t *testing.T) {
25	for _, tc := range []struct {
26		name string
27		got  uintptr
28		req  uintptr
29	}{
30		{"destroy", unsafe.Sizeof(syncobjDestroy{}), ioctlSyncobjDestroy},
31		{"handle_to_fd", unsafe.Sizeof(syncobjHandle{}), ioctlSyncobjHandleToFD},
32		{"fd_to_handle", unsafe.Sizeof(syncobjHandle{}), ioctlSyncobjFDToHandle},
33		{"timeline_signal", unsafe.Sizeof(syncobjTimelineArray{}), ioctlSyncobjTimelineSignal},
34		{"timeline_wait", unsafe.Sizeof(syncobjTimelineWait{}), ioctlSyncobjTimelineWait},
35		{"eventfd", unsafe.Sizeof(syncobjEventfd{}), ioctlSyncobjEventfd},
36		{"get_cap", unsafe.Sizeof(drmCap{}), ioctlGetCap},
37	} {
38		if want := (tc.req >> 16) & 0x3fff; tc.got != want {
39			t.Errorf("%s: size %d, ioctl encodes %d", tc.name, tc.got, want)
40		}
41	}
42}
43
44// syncHarness is a server with a mocked syncobj device and a client with
45// a mapped window, a dmabuf buffer and the syncobj objects.
46type syncHarness struct {
47	t         *testing.T
48	s         *Server
49	c         *wlturbo.Display
50	dev       *mocksyncobjDevice
51	contents  <-chan ports.SurfaceContent
52	presented ports.PresentationSink
53	surf      uint32
54	syncSurf  uint32
55	timeline  uint32
56	win       ports.WindowID
57	mu        sync.Mutex
58	efds      map[uint64]int // eventfd by acquire point
59	signals   []uint64
60	fences    []*os.File
61	releases  map[uint32]*syncReleaseProxy
62	mgr       uint32
63}
64
65func newSyncHarness(t *testing.T) *syncHarness {
66	h := &syncHarness{t: t, efds: map[uint64]int{}}
67	h.dev = newMocksyncobjDevice(t)
68	h.dev.EXPECT().fdToHandle(mock.Anything).Return(7, nil).Maybe()
69	h.dev.EXPECT().destroy(mock.Anything).Return(nil).Maybe()
70	h.dev.EXPECT().signalled(uint32(7), mock.Anything).Return(false, nil).Maybe()
71	h.dev.EXPECT().eventfd(uint32(7), mock.Anything, mock.Anything).RunAndReturn(func(_ uint32, point uint64, efd int) error {
72		// Keep a duplicate: the waiter closes its own when it fires.
73		dup, err := unix.Dup(efd)
74		if err != nil {
75			return err
76		}
77		h.mu.Lock()
78		h.efds[point] = dup
79		h.mu.Unlock()
80		return nil
81	}).Maybe()
82	h.dev.EXPECT().exportSyncFile(uint32(7), mock.Anything).RunAndReturn(func(uint32, uint64) (*os.File, error) {
83		r, w, err := os.Pipe()
84		if err != nil {
85			return nil, err
86		}
87		w.Close()
88		h.mu.Lock()
89		h.fences = append(h.fences, r)
90		h.mu.Unlock()
91		return r, nil
92	}).Maybe()
93	h.dev.EXPECT().signal(uint32(7), mock.Anything).RunAndReturn(func(_ uint32, point uint64) error {
94		h.mu.Lock()
95		h.signals = append(h.signals, point)
96		h.mu.Unlock()
97		return nil
98	}).Maybe()
99	dir := t.TempDir()
100	events := make(chan ports.ClientEvent, 16)
101	commands := make(chan ports.ClientCommand, 16)
102	sup := ports.DMABufSupport{Formats: []ports.DMABufFormat{linearARGB}}
103	s, err := New(Options{RuntimeDir: dir, Outputs: testOutputs, DMABuf: sup, syncDev: h.dev}, Channels{Events: events, Commands: commands}, logging.For(context.Background(), "wayland"))
104	if err != nil {
105		t.Fatal(err)
106	}
107	h.contents = relayContents(t, s)
108	ctx, cancel := context.WithCancel(context.Background())
109	done := make(chan error, 1)
110	go func() { done <- s.Run(ctx) }()
111	t.Cleanup(func() {
112		cancel()
113		<-done
114		h.mu.Lock()
115		for _, fd := range h.efds {
116			unix.Close(fd)
117		}
118		h.mu.Unlock()
119	})
120	h.s, h.presented = s, s.Presentation()
121	h.c = protocolClient(t, s, dir)
122	w, surf, xdg := surfaceMapper(t, h.c, events)()
123	registerProtocol(t, h.c, xdg)
124	h.win, h.surf = w.ID, surf
125	commands <- ports.ConfigureWindow{ID: w.ID, Width: 64, Height: 16, Output: "HEADLESS-1", Visible: true}
126	mgr := bindProtocol(t, h.c, "wp_linux_drm_syncobj_manager_v1")
127	h.syncSurf = h.c.AllocateID()
128	requestProtocol(t, h.c, mgr, linuxdrmsyncobj.WpLinuxDrmSyncobjManagerV1RequestGetSurface, h.syncSurf, surf)
129	registerProtocol(t, h.c, h.syncSurf)
130	fd, err := unix.MemfdCreate("timeline", 0)
131	if err != nil {
132		t.Fatal(err)
133	}
134	defer unix.Close(fd)
135	h.timeline = h.c.AllocateID()
136	registerProtocol(t, h.c, h.timeline)
137	if err := wireRequest(h.c, mgr, uint16(linuxdrmsyncobj.WpLinuxDrmSyncobjManagerV1RequestImportTimeline), []int{fd}, h.timeline); err != nil {
138		t.Fatal(err)
139	}
140	for deadline := time.Now().Add(2 * time.Second); ; {
141		var out string
142		s.display.Do(func() { out = s.frameOutput(s.windows[w.ID].xdg.surface) })
143		if out != "" {
144			break
145		}
146		if time.Now().After(deadline) {
147			t.Fatal("configure not applied")
148		}
149		time.Sleep(2 * time.Millisecond)
150	}
151	return h
152}
153
154// dmabuf makes a client dmabuf wl_buffer (a memfd stands in for it).
155func (h *syncHarness) dmabuf() uint32 {
156	t, c := h.t, h.c
157	dm := bindVersion(t, c, "zwp_linux_dmabuf_v1", dmabufVersion)
158	fd, err := unix.MemfdCreate("buf", 0)
159	if err != nil {
160		t.Fatal(err)
161	}
162	defer unix.Close(fd)
163	if err := unix.Ftruncate(fd, 256*16); err != nil {
164		t.Fatal(err)
165	}
166	p := c.AllocateID()
167	requestProtocol(t, c, dm, linuxdmabuf.ZwpLinuxDmabufV1RequestCreateParams, p)
168	registerProtocol(t, c, p)
169	if err := wireRequest(c, p, uint16(linuxdmabuf.ZwpLinuxBufferParamsV1RequestAdd), []int{fd}, uint32(0), uint32(0), uint32(256), uint32(0), uint32(0)); err != nil {
170		t.Fatal(err)
171	}
172	b := c.AllocateID()
173	rel := &syncReleaseProxy{released: make(chan struct{}, 4)}
174	rel.SetID(b)
175	registerWireProxy(c, rel)
176	requestProtocol(t, c, p, linuxdmabuf.ZwpLinuxBufferParamsV1RequestCreateImmed, b, int32(64), int32(16), linearARGB.Format, uint32(0))
177	h.releases[b] = rel
178	return b
179}
180
181type syncReleaseProxy struct {
182	wlturbo.BaseProxy
183	released chan struct{}
184}
185
186func (p *syncReleaseProxy) Dispatch(e *wlturbo.Event) {
187	if uint32(e.Opcode) == wayland.BufferEventRelease {
188		p.released <- struct{}{}
189	}
190}
191
192func (h *syncHarness) setPoints(acquire, release uint64) {
193	requestProtocol(h.t, h.c, h.syncSurf, linuxdrmsyncobj.WpLinuxDrmSyncobjSurfaceV1RequestSetAcquirePoint, h.timeline, uint32(acquire>>32), uint32(acquire))
194	requestProtocol(h.t, h.c, h.syncSurf, linuxdrmsyncobj.WpLinuxDrmSyncobjSurfaceV1RequestSetReleasePoint, h.timeline, uint32(release>>32), uint32(release))
195}
196
197func (h *syncHarness) commit(buf uint32, acquire, release uint64) {
198	t, c := h.t, h.c
199	requestProtocol(t, c, h.surf, wayland.SurfaceRequestAttach, buf, int32(0), int32(0))
200	h.setPoints(acquire, release)
201	requestProtocol(t, c, h.surf, wayland.SurfaceRequestCommit)
202	if err := c.Roundtrip(); err != nil {
203		t.Fatal(err)
204	}
205}
206
207// content waits for a DMABuf content of the window.
208func (h *syncHarness) content(wait time.Duration) (ports.SurfaceContent, bool) {
209	deadline := time.After(wait)
210	for {
211		select {
212		case c := <-h.contents:
213			if c.ID == h.win && c.DMABuf != nil {
214				return c, true
215			}
216		case <-deadline:
217			return ports.SurfaceContent{}, false
218		}
219	}
220}
221
222// fire makes the kernel's eventfd for an acquire point readable.
223func (h *syncHarness) fire(point uint64) {
224	h.t.Helper()
225	deadline := time.Now().Add(2 * time.Second)
226	for {
227		h.mu.Lock()
228		fd, ok := h.efds[point]
229		h.mu.Unlock()
230		if ok {
231			var one [8]byte
232			one[0] = 1
233			if _, err := unix.Write(fd, one[:]); err != nil {
234				h.t.Fatal(err)
235			}
236			return
237		}
238		if time.Now().After(deadline) {
239			h.t.Fatalf("no eventfd for point %d", point)
240		}
241		time.Sleep(2 * time.Millisecond)
242	}
243}
244
245func (h *syncHarness) signalled() []uint64 {
246	h.mu.Lock()
247	defer h.mu.Unlock()
248	return append([]uint64(nil), h.signals...)
249}
250
251// A commit waits for its acquire point; the content carries the fence;
252// replacing it signals the release point once outputs moved on, and
253// wl_buffer.release is never sent. The acquire fence closes only then.
254func TestSyncobjAcquireAndRelease(t *testing.T) {
255	h := newSyncHarness(t)
256	h.releases = map[uint32]*syncReleaseProxy{}
257	a, b := h.dmabuf(), h.dmabuf()
258	h.commit(a, 1, 2)
259	if _, ok := h.content(50 * time.Millisecond); ok {
260		t.Fatal("content before the acquire point had a fence")
261	}
262	h.fire(1)
263	first, ok := h.content(2 * time.Second)
264	if !ok || first.Acquire == nil {
265		t.Fatalf("content %+v", first)
266	}
267	h.commit(b, 3, 4)
268	h.fire(3)
269	second, ok := h.content(2 * time.Second)
270	if !ok {
271		t.Fatal("second content")
272	}
273	if _, err := first.Acquire.Stat(); err != nil {
274		t.Fatal("acquire fence closed before release")
275	}
276	// The output read the second content: the first buffer is released.
277	h.presented.Push(ports.OutputPresented{Output: "HEADLESS-1", Seen: map[ports.WindowID]uint64{h.win: second.Seq}})
278	deadline := time.Now().Add(2 * time.Second)
279	for len(h.signalled()) == 0 && time.Now().Before(deadline) {
280		if err := h.c.Roundtrip(); err != nil {
281			t.Fatal(err)
282		}
283		time.Sleep(2 * time.Millisecond)
284	}
285	if got := h.signalled(); len(got) != 1 || got[0] != 2 {
286		t.Fatalf("signalled %v", got)
287	}
288	if _, err := first.Acquire.Stat(); err == nil {
289		t.Fatal("acquire fence left open after release")
290	}
291	if err := h.c.Roundtrip(); err != nil {
292		t.Fatal(err)
293	}
294	select {
295	case <-h.releases[a].released:
296		t.Fatal("wl_buffer.release sent under explicit sync")
297	default:
298	}
299}
300
301// FIFO, commit timing and acquire are independent gates on the same ordered
302// update: clearing the first two cannot publish before its acquire fires.
303func TestQueuedCommitReadinessGatesInOrder(t *testing.T) {
304	h := newSyncHarness(t)
305	h.releases = map[uint32]*syncReleaseProxy{}
306	first, second := h.dmabuf(), h.dmabuf()
307	fm := bindProtocol(t, h.c, "wp_fifo_manager_v1")
308	f := h.c.AllocateID()
309	registerProtocol(t, h.c, f)
310	requestProtocol(t, h.c, fm, fifo.WpFifoManagerV1RequestGetFifo, f, h.surf)
311	tm := bindProtocol(t, h.c, "wp_commit_timing_manager_v1")
312	timer := h.c.AllocateID()
313	registerProtocol(t, h.c, timer)
314	requestProtocol(t, h.c, tm, committiming.WpCommitTimingManagerV1RequestGetTimer, timer, h.surf)
315
316	requestProtocol(t, h.c, f, fifo.WpFifoV1RequestSetBarrier)
317	h.commit(first, 1, 2)
318	h.fire(1)
319	one, ok := h.content(2 * time.Second)
320	if !ok {
321		t.Fatal("first update did not publish")
322	}
323	var now unix.Timespec
324	if err := unix.ClockGettime(unix.CLOCK_MONOTONIC, &now); err != nil {
325		t.Fatal(err)
326	}
327	target := time.Duration(now.Nano()) + 180*time.Millisecond
328	sec := uint64(target / time.Second)
329	requestProtocol(t, h.c, timer, committiming.WpCommitTimerV1RequestSetTimestamp, uint32(sec>>32), uint32(sec), uint32(target%time.Second))
330	requestProtocol(t, h.c, f, fifo.WpFifoV1RequestWaitBarrier)
331	h.commit(second, 3, 4)
332	// Even with an acquire fence available, timestamp and barrier hold it.
333	var waiting bool
334	h.s.display.Do(func() {
335		surf := h.s.windows[h.win].xdg.surface
336		waiting = len(surf.queue) == 1 && surf.barrier
337	})
338	if !waiting {
339		t.Fatal("update did not wait behind the FIFO barrier")
340	}
341	if _, ok := h.content(200 * time.Millisecond); ok {
342		t.Fatal("update published before its acquire fence")
343	}
344	var gatedByAcquire bool
345	h.s.display.Do(func() {
346		surf := h.s.windows[h.win].xdg.surface
347		gatedByAcquire = len(surf.queue) == 1 && !surf.barrier && !surf.tooEarly(surf.queue[0].at, time.Now()) && !surf.syncReady(surf.queue[0].sync)
348	})
349	if !gatedByAcquire {
350		t.Fatal("FIFO/timing did not clear while acquire still waited")
351	}
352	h.fire(3)
353	two, ok := h.content(2 * time.Second)
354	if !ok || two.Seq <= one.Seq || two.DMABuf.ID == one.DMABuf.ID {
355		t.Fatalf("ordered publication: first=%+v second=%+v", one, two)
356	}
357}
358
359// A destroyed queued buffer must not hold later commits behind its acquire.
360// Its release point cannot be advanced before the acquire is ready.
361func TestSyncobjDestroyedQueuedAcquire(t *testing.T) {
362	h := newSyncHarness(t)
363	h.releases = map[uint32]*syncReleaseProxy{}
364	first, dead, next := h.dmabuf(), h.dmabuf(), h.dmabuf()
365	h.commit(first, 1, 2)
366	h.fire(1)
367	if _, ok := h.content(2 * time.Second); !ok {
368		t.Fatal("initial content missing")
369	}
370	fm := bindProtocol(t, h.c, "wp_fifo_manager_v1")
371	f := h.c.AllocateID()
372	registerProtocol(t, h.c, f)
373	requestProtocol(t, h.c, fm, fifo.WpFifoManagerV1RequestGetFifo, f, h.surf)
374	requestProtocol(t, h.c, f, fifo.WpFifoV1RequestSetBarrier)
375	h.commit(dead, 3, 4)
376	requestProtocol(t, h.c, dead, wayland.BufferRequestDestroy)
377	if err := h.c.Roundtrip(); err != nil {
378		t.Fatal(err)
379	}
380	// The dead attachment is skipped; it does not replace the current one.
381	// A later attachment is queued while the old fence remains unsignalled.
382	h.commit(next, 5, 6)
383	h.fire(5)
384	got, ok := h.content(2 * time.Second)
385	if !ok || got.DMABuf == nil {
386		t.Fatalf("later content blocked: %+v", got)
387	}
388	for _, p := range h.signalled() {
389		if p == 4 {
390			t.Fatal("skipped buffer release signalled before acquire")
391		}
392	}
393}
394
395func TestSyncobjProtocolErrors(t *testing.T) {
396	for _, tc := range []struct {
397		name string
398		run  func(h *syncHarness)
399		obj  func(h *syncHarness) uint32
400		code uint32
401	}{
402		{"conflicting points", func(h *syncHarness) {
403			b := h.dmabuf()
404			requestProtocol(t, h.c, h.surf, wayland.SurfaceRequestAttach, b, int32(0), int32(0))
405			requestProtocol(t, h.c, h.syncSurf, linuxdrmsyncobj.WpLinuxDrmSyncobjSurfaceV1RequestSetAcquirePoint, h.timeline, uint32(0), uint32(5))
406			requestProtocol(t, h.c, h.syncSurf, linuxdrmsyncobj.WpLinuxDrmSyncobjSurfaceV1RequestSetReleasePoint, h.timeline, uint32(0), uint32(5))
407			requestProtocol(t, h.c, h.surf, wayland.SurfaceRequestCommit)
408		}, func(h *syncHarness) uint32 { return h.syncSurf }, uint32(linuxdrmsyncobj.WpLinuxDrmSyncobjSurfaceV1ErrorConflictingPoints)},
409		{"no release point", func(h *syncHarness) {
410			b := h.dmabuf()
411			requestProtocol(t, h.c, h.surf, wayland.SurfaceRequestAttach, b, int32(0), int32(0))
412			requestProtocol(t, h.c, h.syncSurf, linuxdrmsyncobj.WpLinuxDrmSyncobjSurfaceV1RequestSetAcquirePoint, h.timeline, uint32(0), uint32(5))
413			requestProtocol(t, h.c, h.surf, wayland.SurfaceRequestCommit)
414		}, func(h *syncHarness) uint32 { return h.syncSurf }, uint32(linuxdrmsyncobj.WpLinuxDrmSyncobjSurfaceV1ErrorNoReleasePoint)},
415		{"buffer without points", func(h *syncHarness) {
416			b := h.dmabuf()
417			requestProtocol(t, h.c, h.surf, wayland.SurfaceRequestAttach, b, int32(0), int32(0))
418			requestProtocol(t, h.c, h.surf, wayland.SurfaceRequestCommit)
419		}, func(h *syncHarness) uint32 { return h.syncSurf }, uint32(linuxdrmsyncobj.WpLinuxDrmSyncobjSurfaceV1ErrorNoAcquirePoint)},
420		{"no acquire point", func(h *syncHarness) {
421			b := h.dmabuf()
422			requestProtocol(t, h.c, h.surf, wayland.SurfaceRequestAttach, b, int32(0), int32(0))
423			requestProtocol(t, h.c, h.syncSurf, linuxdrmsyncobj.WpLinuxDrmSyncobjSurfaceV1RequestSetReleasePoint, h.timeline, uint32(0), uint32(5))
424			requestProtocol(t, h.c, h.surf, wayland.SurfaceRequestCommit)
425		}, func(h *syncHarness) uint32 { return h.syncSurf }, uint32(linuxdrmsyncobj.WpLinuxDrmSyncobjSurfaceV1ErrorNoAcquirePoint)},
426		{"points without attach", func(h *syncHarness) {
427			b := h.dmabuf()
428			h.commit(b, 1, 2)
429			h.fire(1)
430			h.setPoints(3, 4)
431			requestProtocol(t, h.c, h.surf, wayland.SurfaceRequestCommit)
432		}, func(h *syncHarness) uint32 { return h.syncSurf }, uint32(linuxdrmsyncobj.WpLinuxDrmSyncobjSurfaceV1ErrorNoBuffer)},
433		{"points with null attach", func(h *syncHarness) {
434			requestProtocol(t, h.c, h.surf, wayland.SurfaceRequestAttach, uint32(0), int32(0), int32(0))
435			h.setPoints(1, 2)
436			requestProtocol(t, h.c, h.surf, wayland.SurfaceRequestCommit)
437		}, func(h *syncHarness) uint32 { return h.syncSurf }, uint32(linuxdrmsyncobj.WpLinuxDrmSyncobjSurfaceV1ErrorNoBuffer)},
438		{"wl_shm buffer", func(h *syncHarness) {
439			b := shmBuffer(t, h.c)
440			requestProtocol(t, h.c, h.surf, wayland.SurfaceRequestAttach, b, int32(0), int32(0))
441			h.setPoints(1, 2)
442			requestProtocol(t, h.c, h.surf, wayland.SurfaceRequestCommit)
443		}, func(h *syncHarness) uint32 { return h.syncSurf }, uint32(linuxdrmsyncobj.WpLinuxDrmSyncobjSurfaceV1ErrorUnsupportedBuffer)},
444		{"surface destroyed", func(h *syncHarness) {
445			requestProtocol(t, h.c, h.surf, wayland.SurfaceRequestDestroy)
446			// The first request already raises the error and the server
447			// then closes the connection: a second write could hit EPIPE.
448			requestProtocol(t, h.c, h.syncSurf, linuxdrmsyncobj.WpLinuxDrmSyncobjSurfaceV1RequestSetAcquirePoint, h.timeline, uint32(0), uint32(1))
449		}, func(h *syncHarness) uint32 { return h.syncSurf }, uint32(linuxdrmsyncobj.WpLinuxDrmSyncobjSurfaceV1ErrorNoSurface)},
450		{"surface exists", func(h *syncHarness) {
451			mgr := bindProtocol(t, h.c, "wp_linux_drm_syncobj_manager_v1")
452			h.mgr = mgr
453			requestProtocol(t, h.c, mgr, linuxdrmsyncobj.WpLinuxDrmSyncobjManagerV1RequestGetSurface, h.c.AllocateID(), h.surf)
454		}, func(h *syncHarness) uint32 { return h.mgr }, uint32(linuxdrmsyncobj.WpLinuxDrmSyncobjManagerV1ErrorSurfaceExists)},
455	} {
456		t.Run(tc.name, func(t *testing.T) {
457			h := newSyncHarness(t)
458			h.releases = map[uint32]*syncReleaseProxy{}
459			h.t = t
460			tc.run(h)
461			expectProtocolError(t, h.c, tc.obj(h), tc.code)
462		})
463	}
464}
465
466// An invalid timeline fd is a protocol error.
467func TestSyncobjInvalidTimeline(t *testing.T) {
468	dev := newMocksyncobjDevice(t)
469	dev.EXPECT().fdToHandle(mock.Anything).Return(0, unix.EINVAL).Once()
470	dir := t.TempDir()
471	s, err := New(Options{RuntimeDir: dir, Outputs: testOutputs, syncDev: dev}, Channels{}, logging.For(context.Background(), "wayland"))
472	if err != nil {
473		t.Fatal(err)
474	}
475	ctx, cancel := context.WithCancel(context.Background())
476	done := make(chan error, 1)
477	go func() { done <- s.Run(ctx) }()
478	t.Cleanup(func() { cancel(); <-done })
479	c := protocolClient(t, s, dir)
480	mgr := bindProtocol(t, c, "wp_linux_drm_syncobj_manager_v1")
481	fd, _ := unix.MemfdCreate("bad", 0)
482	defer unix.Close(fd)
483	if err := wireRequest(c, mgr, uint16(linuxdrmsyncobj.WpLinuxDrmSyncobjManagerV1RequestImportTimeline), []int{fd}, c.AllocateID()); err != nil {
484		t.Fatal(err)
485	}
486	expectProtocolError(t, c, mgr, uint32(linuxdrmsyncobj.WpLinuxDrmSyncobjManagerV1ErrorInvalidTimeline))
487}
488
489// A synchronized subsurface dropped before its acquire is ready must
490// retire its release point without sending wl_buffer.release.
491func TestSyncobjDroppedSubsurfaceCommit(t *testing.T) {
492	h := newSyncHarness(t)
493	h.releases = map[uint32]*syncReleaseProxy{}
494	c := h.c
495	comp := bindProtocol(t, c, "wl_compositor")
496	subc := bindProtocol(t, c, "wl_subcompositor")
497	mgr := bindProtocol(t, c, "wp_linux_drm_syncobj_manager_v1")
498	child := c.AllocateID()
499	requestProtocol(t, c, comp, wayland.CompositorRequestCreateSurface, child)
500	registerProtocol(t, c, child)
501	sub := c.AllocateID()
502	requestProtocol(t, c, subc, wayland.SubcompositorRequestGetSubsurface, sub, child, h.surf)
503	registerProtocol(t, c, sub)
504	syncChild := c.AllocateID()
505	requestProtocol(t, c, mgr, linuxdrmsyncobj.WpLinuxDrmSyncobjManagerV1RequestGetSurface, syncChild, child)
506	registerProtocol(t, c, syncChild)
507	b := h.dmabuf()
508	requestProtocol(t, c, child, wayland.SurfaceRequestAttach, b, int32(0), int32(0))
509	requestProtocol(t, c, syncChild, linuxdrmsyncobj.WpLinuxDrmSyncobjSurfaceV1RequestSetAcquirePoint, h.timeline, uint32(0), uint32(21))
510	requestProtocol(t, c, syncChild, linuxdrmsyncobj.WpLinuxDrmSyncobjSurfaceV1RequestSetReleasePoint, h.timeline, uint32(0), uint32(22))
511	requestProtocol(t, c, child, wayland.SurfaceRequestCommit)
512	requestProtocol(t, c, sub, wayland.SubsurfaceRequestDestroy)
513	if err := c.Roundtrip(); err != nil {
514		t.Fatal(err)
515	}
516	h.fire(21) // dropping before acquire means the later signal must not release
517	if err := c.Roundtrip(); err != nil {
518		t.Fatal(err)
519	}
520	if got := h.signalled(); len(got) != 0 {
521		t.Fatalf("premature release point: %v", got)
522	}
523	select {
524	case <-h.releases[b].released:
525		t.Fatal("explicit sync sent wl_buffer.release")
526	default:
527	}
528}
529
530// A commit dropped before its acquire point has a fence signals nothing
531// (the client has not submitted that point) and sends no wl_buffer.release;
532// the waiter closes its eventfd and no acquire file is left open.
533func TestSyncobjDroppedCommit(t *testing.T) {
534	h := newSyncHarness(t)
535	h.releases = map[uint32]*syncReleaseProxy{}
536	a := h.dmabuf()
537	before := openFDs(t)
538	h.commit(a, 1, 2)
539	requestProtocol(t, h.c, h.surf, wayland.SurfaceRequestDestroy)
540	if err := h.c.Roundtrip(); err != nil {
541		t.Fatal(err)
542	}
543	// The point never gets a fence: the waiter must still stop polling it.
544	time.Sleep(250 * time.Millisecond)
545	if err := h.c.Roundtrip(); err != nil {
546		t.Fatal(err)
547	}
548	if got := h.signalled(); len(got) != 0 {
549		t.Fatalf("signalled %v for a commit whose acquire point never arrived", got)
550	}
551	select {
552	case <-h.releases[a].released:
553		t.Fatal("wl_buffer.release sent under explicit sync")
554	default:
555	}
556	// The harness keeps its own duplicate of the eventfd.
557	if after := openFDs(t); after > before+1 {
558		t.Fatalf("eventfds and sync files %d -> %d", before, after)
559	}
560}
561
562// A commit dropped after its acquire point fired signals its release point.
563func TestSyncobjDroppedReadyCommitReleases(t *testing.T) {
564	h := newSyncHarness(t)
565	h.releases = map[uint32]*syncReleaseProxy{}
566	a := h.dmabuf()
567	h.commit(a, 1, 2)
568	h.fire(1)
569	if _, ok := h.content(2 * time.Second); !ok {
570		t.Fatal("content")
571	}
572	requestProtocol(t, h.c, h.surf, wayland.SurfaceRequestDestroy)
573	deadline := time.Now().Add(2 * time.Second)
574	for len(h.signalled()) == 0 && time.Now().Before(deadline) {
575		if err := h.c.Roundtrip(); err != nil {
576			t.Fatal(err)
577		}
578		time.Sleep(2 * time.Millisecond)
579	}
580	if got := h.signalled(); len(got) != 1 || got[0] != 2 {
581		t.Fatalf("signalled %v", got)
582	}
583}
584
585// openFDs counts the eventfds and sync files (pipes stand in for them)
586// the process holds.
587func openFDs(t *testing.T) int {
588	t.Helper()
589	ents, err := os.ReadDir("/proc/self/fd")
590	if err != nil {
591		t.Fatal(err)
592	}
593	n := 0
594	for _, e := range ents {
595		l, _ := os.Readlink("/proc/self/fd/" + e.Name())
596		if strings.Contains(l, "eventfd") || strings.HasPrefix(l, "pipe:") {
597			n++
598		}
599	}
600	return n
601}

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