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}