1package wayland
2
3import (
4 "context"
5 "encoding/binary"
6 "strings"
7 "testing"
8 "time"
9
10 "github.com/bnema/go-wayland-bindings/server/fifo"
11 "github.com/bnema/go-wayland-bindings/server/wayland"
12 "github.com/bnema/neferwl/internal/adapters/logging"
13 portsmocks "github.com/bnema/neferwl/internal/mocks/ports"
14 "github.com/bnema/neferwl/internal/ports"
15 "github.com/bnema/wlturbo"
16 "github.com/bnema/zerowrap"
17 "golang.org/x/sys/unix"
18)
19
20// slowOutputs has two outputs refreshing once a second: their deadlines never
21// fire inside a test, so only flips pace their frame callbacks.
22var slowOutputs = ports.Layout{
23 {Info: ports.OutputInfo{Name: "HEADLESS-1", Width: 1920, Height: 1080, RefreshMilli: 1000}, Width: 1920, Height: 1080, Scale: 1},
24 {Info: ports.OutputInfo{Name: "HEADLESS-2", Width: 1920, Height: 1080, RefreshMilli: 1000}, X: 1920, Width: 1920, Height: 1080, Scale: 1},
25}
26
27// frameDoneProxy records the time of a wl_callback.done.
28type frameDoneProxy struct {
29 wlturbo.BaseProxy
30 done chan uint32
31}
32
33func (p *frameDoneProxy) Dispatch(e *wlturbo.Event) {
34 if e.Opcode == uint16(wayland.CallbackEventDone) {
35 p.done <- binary.NativeEndian.Uint32(e.Data())
36 }
37}
38
39// requestFrame commits the surface with a new frame callback.
40func requestFrame(t *testing.T, c *wlturbo.Display, surf uint32) *frameDoneProxy {
41 t.Helper()
42 cb := c.AllocateID()
43 p := &frameDoneProxy{done: make(chan uint32, 2)}
44 p.SetID(cb)
45 registerWireProxy(c, p)
46 requestProtocol(t, c, surf, wayland.SurfaceRequestFrame, cb)
47 requestProtocol(t, c, surf, wayland.SurfaceRequestCommit)
48 if err := c.Roundtrip(); err != nil {
49 t.Fatal(err)
50 }
51 return p
52}
53
54// awaitFrame waits for the callback to be answered and returns its time.
55func awaitFrame(t *testing.T, c *wlturbo.Display, p *frameDoneProxy) uint32 {
56 t.Helper()
57 for deadline := time.Now().Add(2 * time.Second); time.Now().Before(deadline); {
58 if err := c.Roundtrip(); err != nil {
59 t.Fatal(err)
60 }
61 select {
62 case ms := <-p.done:
63 return ms
64 case <-time.After(2 * time.Millisecond):
65 }
66 }
67 t.Fatal("frame callback not answered")
68 return 0
69}
70
71// placeWindow puts the window on an output and waits until wayland applied it.
72func placeWindow(t *testing.T, s *Server, commands chan<- ports.ClientCommand, w ports.WindowMapped, output string) {
73 t.Helper()
74 commands <- ports.ConfigureWindow{ID: w.ID, Width: 100, Height: 100, Output: output, Visible: true}
75 for deadline := time.Now().Add(2 * time.Second); ; {
76 var got string
77 s.display.Do(func() { got = s.frameOutput(s.windows[w.ID].xdg.surface) })
78 if got == output {
79 return
80 }
81 if time.Now().After(deadline) {
82 t.Fatalf("window not placed on %s", output)
83 }
84 time.Sleep(time.Millisecond)
85 }
86}
87
88// Flips pushed while the display goroutine is busy are paced by one tick per
89// output, not one per stale flip. The frame callback of each output is
90// answered once, with the time of that output's last flip: a tick per flip
91// would have answered it with the first flip's time.
92func TestPresentationPushCoalescesWhileDisplayBusy(t *testing.T) {
93 s, events, commands, _, sink, dir := presentServerOn(t, slowOutputs)
94 c := protocolClient(t, s, dir)
95 mk := surfaceMapper(t, c, events)
96 outputs := []string{"HEADLESS-1", "HEADLESS-2"}
97 surfs := make([]uint32, len(outputs))
98 for i, name := range outputs {
99 w, surf, _ := mk()
100 placeWindow(t, s, commands, w, name)
101 surfs[i] = surf
102 // The first callback of an idle output is answered at once and
103 // starts its one-second period: the next one waits for a flip.
104 awaitFrame(t, c, requestFrame(t, c, surf))
105 }
106 waiting := make([]*frameDoneProxy, len(outputs))
107 for i := range outputs {
108 waiting[i] = requestFrame(t, c, surfs[i])
109 select {
110 case ms := <-waiting[i].done:
111 t.Fatalf("%s answered before any flip (at %d ms)", outputs[i], ms)
112 default:
113 }
114 }
115 var base time.Duration
116 release, started, finished := make(chan struct{}), make(chan struct{}), make(chan struct{})
117 go func() {
118 s.display.Do(func() { base = monotonic(s.started); close(started); <-release })
119 close(finished)
120 }()
121 <-started
122 const flips = 5
123 const step = 100 * time.Millisecond
124 for i := range flips {
125 for _, name := range outputs {
126 sink.Push(ports.OutputPresented{Output: name, Flip: &ports.FlipInfo{When: base + time.Duration(i+1)*step, Seq: uint64(i + 1), HardwareClock: true}})
127 }
128 }
129 close(release)
130 <-finished
131 last := uint32((flips * step).Milliseconds())
132 for i, name := range outputs {
133 ms := awaitFrame(t, c, waiting[i])
134 // base is read on the display goroutine a little before the server
135 // answers: a few ms of slack, far below one step.
136 if ms < last-5 || ms > last+5 {
137 t.Fatalf("%s answered at %d ms, want the last flip's %d ms (the first flip is at %d)", name, ms, last, step.Milliseconds())
138 }
139 }
140 var seq uint64
141 var reported bool
142 s.display.Do(func() {
143 seq = s.flips["HEADLESS-1"].Seq
144 _, reported = s.reports["HEADLESS-1"]
145 })
146 if !reported || seq != flips {
147 t.Fatalf("last flip %d, reported %v: every flip must still be answered in order", seq, reported)
148 }
149}
150
151func ringOf(outputs ...string) *presentSink {
152 p := &presentSink{ring: make([]ports.OutputPresented, 0, maxPresentRing+1)}
153 for _, o := range outputs {
154 p.ring = append(p.ring, ports.OutputPresented{Output: o})
155 }
156 return p
157}
158
159// fill pads the ring with reports of distinct outputs up to the bound.
160func (p *presentSink) fill() {
161 for i := 0; len(p.ring) < maxPresentRing; i++ {
162 p.ring = append(p.ring, ports.OutputPresented{Output: "pad" + string(rune('A'+i/26)) + string(rune('a'+i%26))})
163 }
164}
165
166func flipOf(seq uint64, shows ...ports.WindowID) *ports.FlipInfo {
167 f := &ports.FlipInfo{Seq: seq}
168 if len(shows) > 0 {
169 f.Shows = map[ports.WindowID]uint64{}
170 for _, id := range shows {
171 f.Shows[id] = seq
172 }
173 }
174 return f
175}
176
177// makeRoomLocked on a full ring: which report it merges or moves, and what it
178// reports.
179func TestPresentationRingMakeRoom(t *testing.T) {
180 tests := []struct {
181 name string
182 ring func() *presentSink
183 in ports.OutputPresented
184 wantMerged bool
185 check func(t *testing.T, p *presentSink, in *ports.OutputPresented)
186 }{
187 {
188 // A full ring merges the oldest flip of an output into its next flip.
189 name: "merges past the bound",
190 ring: func() *presentSink {
191 p := ringOf()
192 for i := range maxPresentRing {
193 p.ring = append(p.ring, ports.OutputPresented{Output: "A", Flip: flipOf(uint64(i + 1))})
194 }
195 return p
196 },
197 in: ports.OutputPresented{Output: "A", Flip: flipOf(maxPresentRing + 1)},
198 wantMerged: true,
199 check: func(t *testing.T, p *presentSink, _ *ports.OutputPresented) {
200 if len(p.ring) != maxPresentRing-1 || p.ring[0].Flip.Seq != 2 || p.ring[0].Flip.Merged != 1 {
201 t.Fatalf("ring %d, first seq %d merged %d", len(p.ring), p.ring[0].Flip.Seq, p.ring[0].Flip.Merged)
202 }
203 },
204 },
205 {
206 // A flip followed by a flip-less report of the same output is not
207 // lost: the later report carries it.
208 name: "keeps a flip before a flip-less report",
209 ring: func() *presentSink {
210 p := ringOf("A", "A")
211 p.ring[0].Flip = flipOf(7, 3)
212 p.fill()
213 p.ring = p.ring[:maxPresentRing]
214 return p
215 },
216 in: ports.OutputPresented{Output: "B", Flip: flipOf(9)},
217 check: func(t *testing.T, p *presentSink, _ *ports.OutputPresented) {
218 if len(p.ring) != maxPresentRing-1 || p.ring[0].Output != "A" || p.ring[0].Flip == nil || p.ring[0].Flip.Seq != 7 || p.ring[0].Flip.Merged != 0 {
219 t.Fatalf("flip lost: ring %d, first %+v", len(p.ring), p.ring[0])
220 }
221 },
222 },
223 {
224 // With several outputs interleaved, the victim is the oldest report
225 // that has a successor of its own output; other outputs' reports are
226 // untouched.
227 name: "interleaved outputs",
228 ring: func() *presentSink {
229 p := ringOf("A", "B", "A", "B")
230 p.ring[0].Flip, p.ring[1].Flip = flipOf(1, 10), flipOf(2, 20)
231 p.ring[2].Flip, p.ring[3].Flip = flipOf(3, 11), nil
232 p.fill()
233 return p
234 },
235 in: ports.OutputPresented{Output: "C"},
236 wantMerged: true,
237 check: func(t *testing.T, p *presentSink, _ *ports.OutputPresented) {
238 if p.ring[0].Output != "B" || p.ring[0].Flip.Seq != 2 || p.ring[0].Flip.Merged != 0 {
239 t.Fatalf("B's flip touched: %+v", p.ring[0])
240 }
241 a := p.ring[1]
242 if a.Output != "A" || a.Flip.Seq != 3 || a.Flip.Merged != 1 || a.Flip.Shows[10] != 1 || a.Flip.Shows[11] != 3 {
243 t.Fatalf("A's flips not merged: %+v", a.Flip)
244 }
245 if p.ring[2].Output != "B" || p.ring[2].Flip != nil {
246 t.Fatalf("B's flip-less report moved: %+v", p.ring[2])
247 }
248 },
249 },
250 {
251 // The incoming report counts as the successor of a ring report.
252 name: "incoming takes over a flip",
253 ring: func() *presentSink {
254 p := ringOf("A")
255 p.ring[0].Flip = flipOf(4)
256 p.fill()
257 return p
258 },
259 in: ports.OutputPresented{Output: "A"},
260 check: func(t *testing.T, p *presentSink, in *ports.OutputPresented) {
261 if in.Flip == nil || in.Flip.Seq != 4 || len(p.ring) != maxPresentRing-1 {
262 t.Fatalf("incoming %+v, ring %d", in.Flip, len(p.ring))
263 }
264 },
265 },
266 {
267 // With no successor for any report (more outputs than ring slots)
268 // nothing is dropped: the ring grows by one and every flip is still
269 // delivered.
270 name: "without a successor keeps everything",
271 ring: func() *presentSink {
272 p := ringOf()
273 for i := range maxPresentRing {
274 p.ring = append(p.ring, ports.OutputPresented{Output: "o" + string(rune('A'+i/26)) + string(rune('a'+i%26)), Flip: flipOf(uint64(i + 1))})
275 }
276 return p
277 },
278 in: ports.OutputPresented{Output: "other", Flip: flipOf(99)},
279 check: func(t *testing.T, p *presentSink, _ *ports.OutputPresented) {
280 if len(p.ring) != maxPresentRing {
281 t.Fatalf("merged or dropped: ring %d", len(p.ring))
282 }
283 },
284 },
285 }
286 for _, tt := range tests {
287 t.Run(tt.name, func(t *testing.T) {
288 p := tt.ring()
289 in := tt.in
290 if merged := p.makeRoomLocked(&in); merged != tt.wantMerged {
291 t.Fatalf("merged %v, want %v", merged, tt.wantMerged)
292 }
293 tt.check(t, p, &in)
294 })
295 }
296}
297
298// Merges are counted, and warned about at most once per mergeWarnEvery.
299func TestPresentationRingWarnsRateLimited(t *testing.T) {
300 now := time.Unix(1000, 0)
301 clk := portsmocks.NewMockClock(t)
302 clk.EXPECT().Now().RunAndReturn(func() time.Time { return now })
303 p := ringOf()
304 p.s = &Server{clock: clk}
305 for i := range maxPresentRing {
306 p.ring = append(p.ring, ports.OutputPresented{Output: "A", Flip: flipOf(uint64(i + 1))})
307 }
308 push := func(seq uint64) (bool, int) {
309 return p.enqueueLocked(ports.OutputPresented{Output: "A", Flip: flipOf(seq)})
310 }
311 if warn, n := push(100); !warn || n != 1 {
312 t.Fatalf("first merge: warn %v merged %d", warn, n)
313 }
314 for i := range 3 {
315 now = now.Add(time.Second)
316 if warn, _ := push(uint64(101 + i)); warn {
317 t.Fatalf("warned again after %v", time.Duration(i+1)*time.Second)
318 }
319 }
320 now = now.Add(mergeWarnEvery)
321 if warn, n := push(200); !warn || n != 4 {
322 t.Fatalf("after the interval: warn %v merged %d, want the 4 merges since", warn, n)
323 }
324}
325
326// A stalled display loop makes outputs merge flips; wayland says so once.
327func TestPresentationMergeWarnsInLog(t *testing.T) {
328 var out lockedBuffer
329 log := zerowrap.New(zerowrap.Config{Level: "info", Format: "json", Output: &out}).WithField("component", "wayland")
330 s, _, _, _, _ := contentServerWith(t, Options{}, log)
331 release, started, finished := make(chan struct{}), make(chan struct{}), make(chan struct{})
332 go func() {
333 s.display.Do(func() { close(started); <-release })
334 close(finished)
335 }()
336 <-started
337 for i := range 3 * maxPresentRing {
338 s.Presentation().Push(ports.OutputPresented{Output: "HEADLESS-1", Flip: &ports.FlipInfo{Seq: uint64(i + 1)}})
339 }
340 close(release)
341 <-finished
342 s.display.Do(func() {})
343 got := out.String()
344 if n := strings.Count(got, `"level":"warn"`); n != 1 {
345 t.Fatalf("%d warnings: %s", n, got)
346 }
347 if !strings.Contains(got, `"component":"wayland"`) || !strings.Contains(got, `"merged":`) {
348 t.Fatalf("warning lacks fields: %s", got)
349 }
350}
351
352// Pushing a report and draining it on the display goroutine allocates nothing
353// in steady state. The count includes the display goroutine: AllocsPerRun
354// reads the global malloc counter.
355func TestPresentationPushAllocations(t *testing.T) {
356 if raceEnabled {
357 t.Skip("the race detector makes the purego wake callback allocate")
358 }
359 s, _, _, _, sink, _ := presentServer(t)
360 r := ports.OutputPresented{Output: "HEADLESS-1"}
361 // Posts run in order: once the marker ran, the drain before it did.
362 // The marker is bound once and the channel is reused, so waiting for it
363 // allocates nothing.
364 ran := make(chan struct{}, 1)
365 marker := func() { ran <- struct{}{} }
366 for range 4 {
367 sink.Push(r)
368 s.display.Post(marker)
369 <-ran
370 }
371 allocs := testing.AllocsPerRun(200, func() {
372 sink.Push(r)
373 s.display.Post(marker)
374 <-ran
375 })
376 if allocs != 0 {
377 t.Errorf("push + drain: %.1f allocs, want 0", allocs)
378 }
379}
380
381// A fifo barrier on an output that does not flip clears at barrierAt+period,
382// driven by the pace timerfd. On a one-second output (so nothing expires during
383// the test) the timerfd must be armed, no later than that deadline (other waits,
384// like held buffers, may be earlier); and its callback, run once the deadline
385// has passed, applies the queued commit.
386func TestPaceTimerfdArmedForBarrierDeadline(t *testing.T) {
387 s, events, commands, _, dir := contentServerWith(t, Options{Outputs: slowOutputs}, testLog())
388 c := protocolClient(t, s, dir)
389 w, surf, _ := surfaceMapper(t, c, events)()
390 placeWindow(t, s, commands, w, "HEADLESS-1")
391 fifoQueue(t, c, surf)
392 var remaining, want, period time.Duration
393 var queued, left int
394 var barrier bool
395 var err error
396 s.display.Do(func() {
397 root := s.windows[w.ID].xdg.surface
398 period, queued = s.outputPeriod(s.fifoOutput(root)), len(root.queue)
399 var spec unix.ItimerSpec
400 err = unix.TimerfdGettime(s.paceFD, &spec)
401 remaining = time.Duration(spec.Value.Nano())
402 want = time.Until(root.barrierAt.Add(period))
403 // The deadline passes: the timerfd callback clears the barrier.
404 root.barrierAt = time.Now().Add(-period)
405 s.onPaceTimer()
406 barrier, left = root.barrier, len(root.queue)
407 })
408 if err != nil {
409 t.Fatal(err)
410 }
411 if queued != 1 || period != time.Second {
412 t.Fatalf("queued %d commits, period %v: the second commit must wait for the barrier", queued, period)
413 }
414 if remaining <= 0 || remaining > want+50*time.Millisecond {
415 t.Fatalf("pace timerfd fires in %v, want armed at or before barrierAt+period (%v)", remaining, want)
416 }
417 if barrier || left != 0 {
418 t.Fatalf("timer callback after the deadline: barrier %v, %d queued", barrier, left)
419 }
420}
421
422func testLog() zerowrap.Logger {
423 return logging.For(context.Background(), "wayland")
424}
425
426// fifoQueue sets a barrier with one commit and queues a second commit behind
427// wait_barrier.
428func fifoQueue(t *testing.T, c *wlturbo.Display, surf uint32) {
429 t.Helper()
430 manager := bindProtocol(t, c, "wp_fifo_manager_v1")
431 f := c.AllocateID()
432 registerProtocol(t, c, f)
433 requestProtocol(t, c, manager, fifo.WpFifoManagerV1RequestGetFifo, f, surf)
434 buf := shmBuffer(t, c)
435 requestProtocol(t, c, f, fifo.WpFifoV1RequestSetBarrier)
436 requestProtocol(t, c, surf, wayland.SurfaceRequestAttach, buf, int32(0), int32(0))
437 requestProtocol(t, c, surf, wayland.SurfaceRequestCommit)
438 requestProtocol(t, c, f, fifo.WpFifoV1RequestWaitBarrier)
439 requestProtocol(t, c, surf, wayland.SurfaceRequestAttach, buf, int32(0), int32(0))
440 requestProtocol(t, c, surf, wayland.SurfaceRequestCommit)
441 if err := c.Roundtrip(); err != nil {
442 t.Fatal(err)
443 }
444}