1package wayland
2
3import (
4 "testing"
5 "time"
6
7 "github.com/bnema/go-wayland-bindings/server/wayland"
8 "github.com/bnema/neferwl/internal/ports"
9 "github.com/bnema/purego-libwayland/server"
10 "golang.org/x/sys/unix"
11)
12
13// paceFixture is a server whose surface waits on an output whose next
14// frame is an hour away: a commit queues its callback and nothing fires. It
15// has what New gives a server for pacing: a display (never run, so posted
16// ticks stay queued) and the registered pace timer.
17func paceFixture(t *testing.T) (*Server, *surface) {
18 t.Helper()
19 display, err := server.NewDisplay()
20 if err != nil {
21 t.Fatal(err)
22 }
23 t.Cleanup(display.Close)
24 out := &output{place: ports.OutputPlacement{Info: ports.OutputInfo{Name: "DP-2", RefreshMilli: 60000}}}
25 s := &Server{
26 display: display,
27 outputs: []*output{out},
28 awaiting: map[string][]*wayland.Callback{},
29 frameDue: map[string]time.Time{"DP-2": time.Now().Add(time.Hour)},
30 fifoSurfaces: map[*surface]struct{}{},
31 lastFlip: map[string]time.Time{},
32 flips: map[string]ports.FlipInfo{},
33 reports: map[string]ports.OutputPresented{},
34 }
35 s.paceSignalFn = s.paceSignalled
36 if err := s.openPaceTimer(); err != nil {
37 t.Fatal(err)
38 }
39 t.Cleanup(func() { unix.Close(s.paceFD) })
40 w := &window{hasLast: true, last: ports.ConfigureWindow{Output: "DP-2", Visible: true}}
41 surf := &surface{server: s, xdg: &xdgSurface{window: w}}
42 surf.next.callbacks = []*wayland.Callback{new(wayland.Callback)}
43 return s, surf
44}
45
46// A commit on the display goroutine ticks the pacer inline: it arms the
47// pace timer itself and posts no tick, which would only run later on the
48// display goroutine it is already on.
49func TestCommitTicksPacerInline(t *testing.T) {
50 s, surf := paceFixture(t)
51 surf.queueAndTick()
52 if len(s.awaiting["DP-2"]) != 1 {
53 t.Fatalf("callback not queued: %+v", s.awaiting)
54 }
55 if s.paceSignal.Load() {
56 t.Fatal("commit posted a tick")
57 }
58 waitArmed(t, s, "inline tick did not arm the pacer timer for the next deadline")
59 if s.paceDepth != 0 || s.paceDirty {
60 t.Fatalf("pace state leaked: depth %d dirty %v", s.paceDepth, s.paceDirty)
61 }
62}
63
64// Wakes inside a tick coalesce into one more pass, not one posted tick each.
65func TestWakePacerDefersInsideTick(t *testing.T) {
66 s, _ := paceFixture(t)
67 s.paceDepth = 1
68 s.wakePacer()
69 s.wakePacer()
70 if s.paceSignal.Load() || !s.paceDirty {
71 t.Fatalf("wake inside a tick: signalled %v dirty %v", s.paceSignal.Load(), s.paceDirty)
72 }
73 s.paceDepth = 0
74 s.wakePacer()
75 if !s.paceSignal.Load() {
76 t.Fatal("wake outside a tick lost")
77 }
78}
79
80// A commit that waits (a fifo barrier) is timed by the inline tick.
81func TestCommitWaitingOnBarrierArmsTimer(t *testing.T) {
82 s, surf := paceFixture(t)
83 surf.next.callbacks = nil
84 surf.barrier, surf.barrierAt = true, time.Now()
85 surf.next.wait = true
86 surf.queueAndTick()
87 if len(surf.queue) != 1 || s.paceSignal.Load() {
88 t.Fatalf("queued %d, tick signalled %v", len(surf.queue), s.paceSignal.Load())
89 }
90 waitArmed(t, s, "timer not armed for the barrier deadline")
91}
92
93// waitArmed asserts the timerfd was armed: it is pending, or it already
94// fired and holds its expiry.
95func waitArmed(t *testing.T, s *Server, msg string) {
96 t.Helper()
97 var cur unix.ItimerSpec
98 if err := unix.TimerfdGettime(s.paceFD, &cur); err != nil {
99 t.Fatal(err)
100 }
101 if cur.Value != (unix.Timespec{}) {
102 return
103 }
104 var buf [8]byte
105 if n, _ := unix.Read(s.paceFD, buf[:]); n != 8 {
106 t.Fatal(msg)
107 }
108}
109
110// A synchronized subsurface commit waits for its parent's commit: it does
111// not tick the pacer, so a burst of child commits costs no ticks.
112func TestSyncedCommitDoesNotTick(t *testing.T) {
113 s, surf := paceFixture(t)
114 surf.sub.parent = &surface{server: s}
115 surf.sub.synced = true
116 surf.queueAndTick()
117 if len(surf.queue) != 1 || !surf.queue[0].synced {
118 t.Fatalf("synced commit not queued: %d", len(surf.queue))
119 }
120 var cur unix.ItimerSpec
121 if err := unix.TimerfdGettime(s.paceFD, &cur); err != nil {
122 t.Fatal(err)
123 }
124 if s.paceSignal.Load() || cur.Value != (unix.Timespec{}) {
125 t.Fatal("synced commit ticked the pacer")
126 }
127}
128
129// An arm for the deadline already armed (within paceTolerance) keeps the
130// timer; another deadline re-arms it; an expiry consumed by onPaceTimer, or a
131// stop, leaves it disarmed, and a stop of a disarmed timer does nothing.
132func TestArmPaceSkipsUnchangedDeadline(t *testing.T) {
133 s, _ := paceFixture(t)
134 remaining := func() time.Duration {
135 var cur unix.ItimerSpec
136 if err := unix.TimerfdGettime(s.paceFD, &cur); err != nil {
137 t.Fatal(err)
138 }
139 return time.Duration(cur.Value.Nano())
140 }
141 s.stopPace()
142 if s.paceArmed {
143 t.Fatal("stop of a never armed timer armed it")
144 }
145 now := time.Now()
146 s.armPaceFrom(now, time.Hour)
147 first := s.paceDeadline
148 if !s.paceArmed || remaining() <= 59*time.Minute {
149 t.Fatalf("not armed: %v", remaining())
150 }
151 // A deadline a little before the armed one keeps the timer.
152 s.armPaceFrom(now, time.Hour-paceTolerance/2)
153 if !s.paceDeadline.Equal(first) {
154 t.Fatalf("an earlier deadline within tolerance re-armed the timer: %v -> %v", first, s.paceDeadline)
155 }
156 // A later deadline, however close, re-arms: the timer must never fire
157 // before the requested deadline.
158 s.armPaceFrom(now, time.Hour+time.Nanosecond)
159 if !s.paceDeadline.After(first) {
160 t.Fatalf("a later deadline kept the earlier timer: %v -> %v", first, s.paceDeadline)
161 }
162 first = s.paceDeadline
163 s.armPace(time.Hour - time.Minute)
164 if s.paceDeadline.Equal(first) || remaining() > 59*time.Minute {
165 t.Fatalf("a new deadline kept the old timer: remaining %v", remaining())
166 }
167 s.stopPace()
168 if s.paceArmed || remaining() != 0 {
169 t.Fatalf("stop left the timer armed: %v", remaining())
170 }
171 // One-shot expiry: the fired timer is disarmed, so a stop is a no-op and
172 // the next arm sets the timer again even for the same deadline.
173 s.armPace(time.Nanosecond)
174 fds := []unix.PollFd{{Fd: int32(s.paceFD), Events: unix.POLLIN}}
175 if n, err := unix.Poll(fds, 5000); n != 1 {
176 t.Fatalf("the timer did not fire: %d, %v", n, err)
177 }
178 s.onPaceTimer()
179 if s.paceArmed {
180 t.Fatal("expiry did not disarm")
181 }
182 s.stopPace() // no syscall to observe: it must not re-arm anything
183 if remaining() != 0 {
184 t.Fatalf("stop after the expiry: %v", remaining())
185 }
186 s.armPace(time.Hour)
187 if !s.paceArmed || remaining() <= 59*time.Minute {
188 t.Fatalf("not re-armed after the expiry: %v", remaining())
189 }
190}