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

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