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}