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// A full ring merges the oldest flip of an output into its next flip.
178func TestPresentationRingMergesPastBound(t *testing.T) {
179 p := ringOf()
180 for i := range maxPresentRing {
181 p.ring = append(p.ring, ports.OutputPresented{Output: "A", Flip: flipOf(uint64(i + 1))})
182 }
183 in := ports.OutputPresented{Output: "A", Flip: flipOf(maxPresentRing + 1)}
184 if !p.makeRoomLocked(&in) {
185 t.Fatal("two flips merged but not reported")
186 }
187 if len(p.ring) != maxPresentRing-1 || p.ring[0].Flip.Seq != 2 || p.ring[0].Flip.Merged != 1 {
188 t.Fatalf("ring %d, first seq %d merged %d", len(p.ring), p.ring[0].Flip.Seq, p.ring[0].Flip.Merged)
189 }
190}
191
192// A flip followed by a flip-less report of the same output is not lost: the
193// later report carries it.
194func TestPresentationRingKeepsFlipBeforeFliplessReport(t *testing.T) {
195 p := ringOf("A", "A")
196 p.ring[0].Flip = flipOf(7, 3)
197 p.fill()
198 p.ring = p.ring[:maxPresentRing]
199 in := ports.OutputPresented{Output: "B", Flip: flipOf(9)}
200 if p.makeRoomLocked(&in) {
201 t.Fatal("no flips merged, yet reported")
202 }
203 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 {
204 t.Fatalf("flip lost: ring %d, first %+v", len(p.ring), p.ring[0])
205 }
206}
207
208// With several outputs interleaved, the victim is the oldest report that has
209// a successor of its own output; other outputs' reports are untouched.
210func TestPresentationRingInterleavedOutputs(t *testing.T) {
211 p := ringOf("A", "B", "A", "B")
212 p.ring[0].Flip, p.ring[1].Flip = flipOf(1, 10), flipOf(2, 20)
213 p.ring[2].Flip, p.ring[3].Flip = flipOf(3, 11), nil
214 p.fill()
215 in := ports.OutputPresented{Output: "C"}
216 if !p.makeRoomLocked(&in) {
217 t.Fatal("merge not reported")
218 }
219 if p.ring[0].Output != "B" || p.ring[0].Flip.Seq != 2 || p.ring[0].Flip.Merged != 0 {
220 t.Fatalf("B's flip touched: %+v", p.ring[0])
221 }
222 a := p.ring[1]
223 if a.Output != "A" || a.Flip.Seq != 3 || a.Flip.Merged != 1 || a.Flip.Shows[10] != 1 || a.Flip.Shows[11] != 3 {
224 t.Fatalf("A's flips not merged: %+v", a.Flip)
225 }
226 if p.ring[2].Output != "B" || p.ring[2].Flip != nil {
227 t.Fatalf("B's flip-less report moved: %+v", p.ring[2])
228 }
229}
230
231// The incoming report counts as the successor of a ring report.
232func TestPresentationRingIncomingTakesOverFlip(t *testing.T) {
233 p := ringOf("A")
234 p.ring[0].Flip = flipOf(4)
235 p.fill()
236 in := ports.OutputPresented{Output: "A"}
237 if p.makeRoomLocked(&in) {
238 t.Fatal("no merge expected")
239 }
240 if in.Flip == nil || in.Flip.Seq != 4 || len(p.ring) != maxPresentRing-1 {
241 t.Fatalf("incoming %+v, ring %d", in.Flip, len(p.ring))
242 }
243}
244
245// With no successor for any report (more outputs than ring slots) nothing is
246// dropped: the ring grows by one and every flip is still delivered.
247func TestPresentationRingWithoutSuccessorKeepsEverything(t *testing.T) {
248 p := ringOf()
249 for i := range maxPresentRing {
250 p.ring = append(p.ring, ports.OutputPresented{Output: "o" + string(rune('A'+i/26)) + string(rune('a'+i%26)), Flip: flipOf(uint64(i + 1))})
251 }
252 in := ports.OutputPresented{Output: "other", Flip: flipOf(99)}
253 if p.makeRoomLocked(&in) || len(p.ring) != maxPresentRing {
254 t.Fatalf("merged or dropped: ring %d", len(p.ring))
255 }
256}
257
258// Merges are counted, and warned about at most once per mergeWarnEvery.
259func TestPresentationRingWarnsRateLimited(t *testing.T) {
260 now := time.Unix(1000, 0)
261 clk := portsmocks.NewMockClock(t)
262 clk.EXPECT().Now().RunAndReturn(func() time.Time { return now })
263 p := ringOf()
264 p.s = &Server{clock: clk}
265 for i := range maxPresentRing {
266 p.ring = append(p.ring, ports.OutputPresented{Output: "A", Flip: flipOf(uint64(i + 1))})
267 }
268 push := func(seq uint64) (bool, int) {
269 return p.enqueueLocked(ports.OutputPresented{Output: "A", Flip: flipOf(seq)})
270 }
271 if warn, n := push(100); !warn || n != 1 {
272 t.Fatalf("first merge: warn %v merged %d", warn, n)
273 }
274 for i := range 3 {
275 now = now.Add(time.Second)
276 if warn, _ := push(uint64(101 + i)); warn {
277 t.Fatalf("warned again after %v", time.Duration(i+1)*time.Second)
278 }
279 }
280 now = now.Add(mergeWarnEvery)
281 if warn, n := push(200); !warn || n != 4 {
282 t.Fatalf("after the interval: warn %v merged %d, want the 4 merges since", warn, n)
283 }
284}
285
286// A stalled display loop makes outputs merge flips; wayland says so once.
287func TestPresentationMergeWarnsInLog(t *testing.T) {
288 var out lockedBuffer
289 log := zerowrap.New(zerowrap.Config{Level: "info", Format: "json", Output: &out}).WithField("component", "wayland")
290 s, _, _, _, _ := contentServerWith(t, Options{}, log)
291 release, started, finished := make(chan struct{}), make(chan struct{}), make(chan struct{})
292 go func() {
293 s.display.Do(func() { close(started); <-release })
294 close(finished)
295 }()
296 <-started
297 for i := range 3 * maxPresentRing {
298 s.Presentation().Push(ports.OutputPresented{Output: "HEADLESS-1", Flip: &ports.FlipInfo{Seq: uint64(i + 1)}})
299 }
300 close(release)
301 <-finished
302 s.display.Do(func() {})
303 got := out.String()
304 if n := strings.Count(got, "outputs report faster than the display loop drains; flips merged"); n != 1 {
305 t.Fatalf("%d warnings: %s", n, got)
306 }
307 if !strings.Contains(got, `"component":"wayland"`) || !strings.Contains(got, `"merged":`) {
308 t.Fatalf("warning lacks fields: %s", got)
309 }
310}
311
312// Pushing a report and draining it on the display goroutine allocates nothing
313// in steady state. The count includes the display goroutine: AllocsPerRun
314// reads the global malloc counter.
315func TestPresentationPushAllocations(t *testing.T) {
316 if raceEnabled {
317 t.Skip("the race detector makes the purego wake callback allocate")
318 }
319 s, _, _, _, sink, _ := presentServer(t)
320 r := ports.OutputPresented{Output: "HEADLESS-1"}
321 // Posts run in order: once the marker ran, the drain before it did.
322 // The marker is bound once and the channel is reused, so waiting for it
323 // allocates nothing.
324 ran := make(chan struct{}, 1)
325 marker := func() { ran <- struct{}{} }
326 for range 4 {
327 sink.Push(r)
328 s.display.Post(marker)
329 <-ran
330 }
331 allocs := testing.AllocsPerRun(200, func() {
332 sink.Push(r)
333 s.display.Post(marker)
334 <-ran
335 })
336 if allocs != 0 {
337 t.Errorf("push + drain: %.1f allocs, want 0", allocs)
338 }
339}
340
341// A fifo barrier on an output that does not flip clears at barrierAt+period,
342// driven by the pace timerfd. On a one-second output (so nothing expires during
343// the test) the timerfd must be armed, no later than that deadline (other waits,
344// like held buffers, may be earlier); and its callback, run once the deadline
345// has passed, applies the queued commit.
346func TestPaceTimerfdArmedForBarrierDeadline(t *testing.T) {
347 s, events, commands, _, dir := contentServerWith(t, Options{Outputs: slowOutputs}, testLog())
348 c := protocolClient(t, s, dir)
349 w, surf, _ := surfaceMapper(t, c, events)()
350 placeWindow(t, s, commands, w, "HEADLESS-1")
351 fifoQueue(t, c, surf)
352 var remaining, want, period time.Duration
353 var queued, left int
354 var barrier bool
355 var err error
356 s.display.Do(func() {
357 root := s.windows[w.ID].xdg.surface
358 period, queued = s.outputPeriod(s.fifoOutput(root)), len(root.queue)
359 var spec unix.ItimerSpec
360 err = unix.TimerfdGettime(s.paceFD, &spec)
361 remaining = time.Duration(spec.Value.Nano())
362 want = time.Until(root.barrierAt.Add(period))
363 // The deadline passes: the timerfd callback clears the barrier.
364 root.barrierAt = time.Now().Add(-period)
365 s.onPaceTimer()
366 barrier, left = root.barrier, len(root.queue)
367 })
368 if err != nil {
369 t.Fatal(err)
370 }
371 if queued != 1 || period != time.Second {
372 t.Fatalf("queued %d commits, period %v: the second commit must wait for the barrier", queued, period)
373 }
374 if remaining <= 0 || remaining > want+50*time.Millisecond {
375 t.Fatalf("pace timerfd fires in %v, want armed at or before barrierAt+period (%v)", remaining, want)
376 }
377 if barrier || left != 0 {
378 t.Fatalf("timer callback after the deadline: barrier %v, %d queued", barrier, left)
379 }
380}
381
382// End to end on a 20 Hz output: the queued commit is applied at barrierAt+50 ms
383// (the timerfd), not at the next 100 ms held-buffer tick; the bound is generous
384// (half a period) against scheduler noise.
385func TestPaceTimerfdClearsBarrierOnDeadline(t *testing.T) {
386 out := ports.Layout{{Info: ports.OutputInfo{Name: "HEADLESS-1", Width: 1920, Height: 1080, RefreshMilli: 20000}, Width: 1920, Height: 1080, Scale: 1}}
387 s, events, commands, contents, dir := contentServerWith(t, Options{Outputs: out}, testLog())
388 c := protocolClient(t, s, dir)
389 w, surf, _ := surfaceMapper(t, c, events)()
390 placeWindow(t, s, commands, w, "HEADLESS-1")
391 nextContent(t, contents, w.ID) // the mapping commit
392 fifoQueue(t, c, surf)
393 var barrierAt time.Time
394 var period time.Duration
395 s.display.Do(func() {
396 root := s.windows[w.ID].xdg.surface
397 barrierAt, period = root.barrierAt, s.outputPeriod(s.fifoOutput(root))
398 })
399 nextContent(t, contents, w.ID) // the commit that set the barrier
400 late := nextContent(t, contents, w.ID).Sub(barrierAt.Add(period))
401 // Never early; late by far less than the period (not by a polling tick).
402 if late < -time.Millisecond || late > period/2 {
403 t.Fatalf("queued commit applied %v after barrierAt+%v", late, period)
404 }
405}
406
407func testLog() zerowrap.Logger {
408 return logging.For(context.Background(), "wayland")
409}
410
411// fifoQueue sets a barrier with one commit and queues a second commit behind
412// wait_barrier.
413func fifoQueue(t *testing.T, c *wlturbo.Display, surf uint32) {
414 t.Helper()
415 manager := bindProtocol(t, c, "wp_fifo_manager_v1")
416 f := c.AllocateID()
417 registerProtocol(t, c, f)
418 requestProtocol(t, c, manager, fifo.WpFifoManagerV1RequestGetFifo, f, surf)
419 buf := shmBuffer(t, c)
420 requestProtocol(t, c, f, fifo.WpFifoV1RequestSetBarrier)
421 requestProtocol(t, c, surf, wayland.SurfaceRequestAttach, buf, int32(0), int32(0))
422 requestProtocol(t, c, surf, wayland.SurfaceRequestCommit)
423 requestProtocol(t, c, f, fifo.WpFifoV1RequestWaitBarrier)
424 requestProtocol(t, c, surf, wayland.SurfaceRequestAttach, buf, int32(0), int32(0))
425 requestProtocol(t, c, surf, wayland.SurfaceRequestCommit)
426 if err := c.Roundtrip(); err != nil {
427 t.Fatal(err)
428 }
429}