fifo_test.go RawHistoryFolder

1package wayland
2
3import (
4	"testing"
5	"time"
6
7	"github.com/bnema/go-wayland-bindings/server/committiming"
8	"github.com/bnema/go-wayland-bindings/server/fifo"
9	"github.com/bnema/go-wayland-bindings/server/wayland"
10	"github.com/bnema/neferwl/internal/ports"
11	"github.com/bnema/wlturbo"
12	"golang.org/x/sys/unix"
13)
14
15// nextContent waits for a content of window id.
16func nextContent(t *testing.T, contents <-chan ports.SurfaceContent, id ports.WindowID) time.Time {
17	t.Helper()
18	deadline := time.After(2 * time.Second)
19	for {
20		select {
21		case c := <-contents:
22			if c.ID == id && !c.Empty() {
23				return time.Now()
24			}
25		case <-deadline:
26			t.Fatal("no content")
27		}
28	}
29}
30
31// shmBuffer creates a 1x1 wl_shm buffer.
32func shmBuffer(t *testing.T, c *wlturbo.Display) uint32 { return shmBufferSized(t, c, 1, 1) }
33
34// shmBufferSized is a w×h ARGB8888 wl_shm buffer.
35func shmBufferSized(t *testing.T, c *wlturbo.Display, w, h int32) uint32 {
36	t.Helper()
37	shm := bindProtocol(t, c, "wl_shm")
38	registerProtocol(t, c, shm)
39	fd, err := unix.MemfdCreate("fifo-buffer", 0)
40	if err != nil {
41		t.Fatal(err)
42	}
43	defer unix.Close(fd)
44	size := w * h * 4
45	if err := unix.Ftruncate(fd, int64(size)); err != nil {
46		t.Fatal(err)
47	}
48	pool, buffer := c.AllocateID(), c.AllocateID()
49	registerProtocol(t, c, pool)
50	registerProtocol(t, c, buffer)
51	if err := wireRequest(c, shm, uint16(wayland.ShmRequestCreatePool), []int{fd}, pool, size); err != nil {
52		t.Fatal(err)
53	}
54	requestProtocol(t, c, pool, wayland.ShmPoolRequestCreateBuffer, buffer, int32(0), w, h, w*4, uint32(0))
55	return buffer
56}
57
58func drainContents(contents <-chan ports.SurfaceContent) {
59	for {
60		select {
61		case <-contents:
62		case <-time.After(20 * time.Millisecond):
63			return
64		}
65	}
66}
67
68func TestFifoBarrierHoldsCommitOneRefresh(t *testing.T) {
69	s, events, _, contents, dir := contentServer(t)
70	c := protocolClient(t, s, dir)
71	w, surf, _ := surfaceMapper(t, c, events)()
72	manager := bindProtocol(t, c, "wp_fifo_manager_v1")
73	f := c.AllocateID()
74	registerProtocol(t, c, f)
75	requestProtocol(t, c, manager, fifo.WpFifoManagerV1RequestGetFifo, f, surf)
76	buf := shmBuffer(t, c)
77	drainContents(contents)
78
79	// Vulkan FIFO: every frame sets a barrier and waits on the last one.
80	// Headless outputs never flip: the barrier clears after one 60 Hz
81	// refresh, so three frames take at least two refreshes.
82	start := time.Now()
83	for range 3 {
84		requestProtocol(t, c, f, fifo.WpFifoV1RequestSetBarrier)
85		requestProtocol(t, c, f, fifo.WpFifoV1RequestWaitBarrier)
86		requestProtocol(t, c, surf, wayland.SurfaceRequestAttach, buf, int32(0), int32(0))
87		requestProtocol(t, c, surf, wayland.SurfaceRequestCommit)
88	}
89	if err := c.Roundtrip(); err != nil {
90		t.Fatal(err)
91	}
92	for range 3 {
93		nextContent(t, contents, w.ID)
94	}
95	if d := time.Since(start); d < 2*defaultFramePeriod-2*time.Millisecond {
96		t.Fatalf("three fifo frames in %v", d)
97	}
98}
99
100func TestCommitTimingDelaysCommit(t *testing.T) {
101	s, events, _, contents, dir := contentServer(t)
102	c := protocolClient(t, s, dir)
103	w, surf, _ := surfaceMapper(t, c, events)()
104	manager := bindProtocol(t, c, "wp_commit_timing_manager_v1")
105	timer := c.AllocateID()
106	registerProtocol(t, c, timer)
107	requestProtocol(t, c, manager, committiming.WpCommitTimingManagerV1RequestGetTimer, timer, surf)
108	buf := shmBuffer(t, c)
109	drainContents(contents)
110
111	var now unix.Timespec
112	if err := unix.ClockGettime(unix.CLOCK_MONOTONIC, &now); err != nil {
113		t.Fatal(err)
114	}
115	target := time.Duration(now.Nano()) + 100*time.Millisecond
116	start := time.Now()
117	sec := uint64(target / time.Second)
118	requestProtocol(t, c, timer, committiming.WpCommitTimerV1RequestSetTimestamp, uint32(sec>>32), uint32(sec), uint32(target%time.Second))
119	requestProtocol(t, c, surf, wayland.SurfaceRequestAttach, buf, int32(0), int32(0))
120	requestProtocol(t, c, surf, wayland.SurfaceRequestCommit)
121	if err := c.Roundtrip(); err != nil {
122		t.Fatal(err)
123	}
124	// Applied one refresh before the target, so it shows at the target.
125	at := nextContent(t, contents, w.ID).Sub(start)
126	if at < 100*time.Millisecond-defaultFramePeriod-2*time.Millisecond || at > time.Second {
127		t.Fatalf("timed commit applied after %v", at)
128	}
129
130	// A second timestamp before the commit is an error.
131	requestProtocol(t, c, timer, committiming.WpCommitTimerV1RequestSetTimestamp, uint32(0), uint32(1), uint32(0))
132	requestProtocol(t, c, timer, committiming.WpCommitTimerV1RequestSetTimestamp, uint32(0), uint32(1), uint32(0))
133	expectProtocolError(t, c, timer, uint32(committiming.WpCommitTimerV1ErrorTimestampExists))
134}
135
136// Far or past timestamps never hold a surface longer than a second.
137func TestMonotonicTimeClamps(t *testing.T) {
138	var now unix.Timespec
139	if err := unix.ClockGettime(unix.CLOCK_MONOTONIC, &now); err != nil {
140		t.Fatal(err)
141	}
142	for _, tc := range []struct {
143		sec    uint64
144		nsec   int64
145		lo, hi time.Duration
146	}{
147		{^uint64(0), 0, maxTimestampAhead - 10*time.Millisecond, maxTimestampAhead},
148		{0, 0, -10 * time.Millisecond, 10 * time.Millisecond},
149		{uint64(now.Sec), now.Nsec, -10 * time.Millisecond, 10 * time.Millisecond},
150	} {
151		got := time.Until(monotonicTime(tc.sec, tc.nsec))
152		if got < tc.lo || got > tc.hi {
153			t.Errorf("sec %d: %v not in [%v, %v]", tc.sec, got, tc.lo, tc.hi)
154		}
155	}
156}
157
158// A barrier on an output that flips clears on its next flip.
159func TestTickFifoClearsBarrierOnFlip(t *testing.T) {
160	out := &output{place: ports.OutputPlacement{Info: ports.OutputInfo{Name: "DP-2", RefreshMilli: 60000}}}
161	s := &Server{outputs: []*output{out}, fifoSurfaces: map[*surface]struct{}{}, lastFlip: map[string]time.Time{}}
162	surf := &surface{server: s, xdg: &xdgSurface{window: &window{last: ports.ConfigureWindow{Output: "DP-2"}}}}
163	now := time.Unix(100, 0)
164	s.lastFlip["DP-2"] = now.Add(-time.Millisecond)
165	surf.setBarrier(now)
166	if _, waiting := s.tickFifo(now.Add(time.Millisecond), nil); !waiting || !surf.barrier {
167		t.Fatal("barrier cleared before the flip")
168	}
169	// A flipping output waits up to 1.5 refreshes for a missed flip.
170	if w, _ := s.tickFifo(now.Add(20*time.Millisecond), nil); !surf.barrier || w < 4*time.Millisecond {
171		t.Fatalf("barrier %v wait %v", surf.barrier, w)
172	}
173	if _, waiting := s.tickFifo(now.Add(21*time.Millisecond), map[string]bool{"DP-2": true}); waiting || surf.barrier {
174		t.Fatal("barrier kept after the flip")
175	}
176}
177
178// A queued commit whose buffer is destroyed (a swapchain resize) is skipped
179// without unmapping the window.
180func TestFifoQueuedBufferDestroyed(t *testing.T) {
181	s, events, _, contents, dir := contentServer(t)
182	c := protocolClient(t, s, dir)
183	_, surf, _ := surfaceMapper(t, c, events)()
184	manager := bindProtocol(t, c, "wp_fifo_manager_v1")
185	f := c.AllocateID()
186	registerProtocol(t, c, f)
187	requestProtocol(t, c, manager, fifo.WpFifoManagerV1RequestGetFifo, f, surf)
188	buf := shmBuffer(t, c)
189	requestProtocol(t, c, f, fifo.WpFifoV1RequestSetBarrier)
190	requestProtocol(t, c, surf, wayland.SurfaceRequestAttach, buf, int32(0), int32(0))
191	requestProtocol(t, c, surf, wayland.SurfaceRequestCommit)
192	// Queued behind the barrier, then its buffer goes away.
193	requestProtocol(t, c, f, fifo.WpFifoV1RequestWaitBarrier)
194	requestProtocol(t, c, surf, wayland.SurfaceRequestAttach, buf, int32(0), int32(0))
195	requestProtocol(t, c, surf, wayland.SurfaceRequestCommit)
196	requestProtocol(t, c, buf, wayland.BufferRequestDestroy)
197	if err := c.Roundtrip(); err != nil {
198		t.Fatal(err)
199	}
200	drainContents(contents)
201	time.Sleep(3 * defaultFramePeriod)
202	for {
203		select {
204		case ev := <-events:
205			if _, ok := ev.(ports.WindowUnmapped); ok {
206				t.Fatal("window unmapped by a dead queued buffer")
207			}
208		default:
209			return
210		}
211	}
212}
213
214// A parent can keep a dependency after the child's queue is dropped or its
215// update is applied independently. Neither path may recycle that dependency
216// into a new commit while the parent still holds it.
217func TestQueuedParentRetainsRetiredChild(t *testing.T) {
218	for _, dropped := range []bool{true, false} {
219		name := "applied"
220		if dropped {
221			name = "dropped"
222		}
223		t.Run(name, func(t *testing.T) {
224			srv := &Server{fifoSurfaces: make(map[*surface]struct{})}
225			parent := &surface{server: srv}
226			child := &surface{server: srv}
227			child.sub.parent, child.sub.synced = parent, true
228			parent.sub.children = []*surface{child}
229			child.next.scale = 2
230			child.queueUpdate()
231			old := child.queue[0]
232			parent.next.scale = 3
233			parent.queueUpdate()
234			pu := parent.queue[0]
235			if old.refs != 1 || len(pu.deps) != 1 || pu.deps[0] != old {
236				t.Fatal("parent did not capture child's update")
237			}
238			if dropped {
239				child.dropQueue()
240			} else {
241				old.applyGraph()
242			}
243			if !old.retired || old.refs != 1 {
244				t.Fatalf("retired child ref count: retired=%v refs=%d", old.retired, old.refs)
245			}
246			child.next.scale = 7
247			child.queueUpdate()
248			if child.queue[0] == old || old.scale != 2 || pu.deps[0] != old {
249				t.Fatal("live dependency was recycled as a new child commit")
250			}
251			if !pu.graphReady(time.Now()) {
252				t.Fatal("retired dependency kept parent waiting")
253			}
254			pu.applyGraph()
255			if parent.bufferScale != 3 || len(parent.queue) != 0 || len(child.queue) != 1 || child.queue[0].scale != 7 {
256				t.Fatal("parent replayed old dependency or changed new child commit")
257			}
258			child.dropQueue()
259		})
260	}
261}
262
263// Rechecking a shared predecessor in a traversal must reuse its readiness.
264func TestGraphReadyMemoizesPredecessors(t *testing.T) {
265	s := &Server{readinessGeneration: 1}
266	surf := &surface{server: s}
267	first := &update{owner: surf}
268	second := &update{owner: surf}
269	third := &update{owner: surf}
270	surf.queue = []*update{first, second, third}
271	second.prev = first
272	third.prev = second
273	now := time.Now()
274	if !third.graphReady(now) {
275		t.Fatal("ready chain rejected")
276	}
277	if first.readyGeneration != s.readinessGeneration || second.readyGeneration != s.readinessGeneration {
278		t.Fatal("predecessors not memoized")
279	}
280}
281
282func TestInvisibleFifoPeriod(t *testing.T) {
283	out := &output{place: ports.OutputPlacement{Info: ports.OutputInfo{Name: "DP-2", RefreshMilli: 60000}}}
284	s := &Server{outputs: []*output{out}, fifoSurfaces: map[*surface]struct{}{}, lastFlip: map[string]time.Time{}}
285	w := &window{hasLast: true, last: ports.ConfigureWindow{Output: "DP-2"}}
286	surf := &surface{server: s, xdg: &xdgSurface{window: w}}
287	now := time.Unix(100, 0)
288	surf.setBarrier(now)
289	if _, _ = s.tickFifo(now.Add(100*time.Millisecond), map[string]bool{"DP-2": true}); !surf.barrier || s.outputPeriod(s.fifoOutput(surf)) != time.Second {
290		t.Fatalf("hidden barrier cleared or period wrong: %v %v", surf.barrier, s.outputPeriod(s.fifoOutput(surf)))
291	}
292	w.last.Visible = true
293	if _, waiting := s.tickFifo(now.Add(101*time.Millisecond), nil); surf.barrier || waiting {
294		t.Fatalf("visible barrier not resumed: %v waiting %v", surf.barrier, waiting)
295	}
296}

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