1package wayland
2
3import (
4 "fmt"
5 "slices"
6 "sync"
7 "testing"
8 "time"
9
10 "github.com/bnema/go-wayland-bindings/server/inputtimestamps"
11 "github.com/bnema/go-wayland-bindings/server/wayland"
12 "github.com/bnema/neferwl/internal/ports"
13 "github.com/bnema/wlturbo"
14)
15
16// stampRecorder is the ordered log shared by the proxies of one client, so a
17// test sees whether a timestamp comes right before its event.
18type stampRecorder struct {
19 mu sync.Mutex
20 lines []string
21}
22
23func (r *stampRecorder) add(format string, args ...any) {
24 r.mu.Lock()
25 defer r.mu.Unlock()
26 r.lines = append(r.lines, fmt.Sprintf(format, args...))
27}
28
29func (r *stampRecorder) snapshot() []string {
30 r.mu.Lock()
31 defer r.mu.Unlock()
32 return slices.Clone(r.lines)
33}
34
35// wait round-trips until the log holds n lines.
36func (r *stampRecorder) wait(t *testing.T, c *wlturbo.Display, n int) []string {
37 t.Helper()
38 deadline := time.Now().Add(2 * time.Second)
39 for {
40 if err := c.Roundtrip(); err != nil {
41 t.Fatal(err)
42 }
43 if got := r.snapshot(); len(got) >= n {
44 return got
45 }
46 if time.Now().After(deadline) {
47 t.Fatalf("log has %v, want %d lines", r.snapshot(), n)
48 }
49 time.Sleep(5 * time.Millisecond)
50 }
51}
52
53// stampedKeyboard logs the key events of one wl_keyboard under its tag.
54type stampedKeyboard struct {
55 wlturbo.BaseProxy
56 tag string
57 log *stampRecorder
58}
59
60func (p *stampedKeyboard) Dispatch(e *wlturbo.Event) {
61 if e.Opcode == uint16(wayland.KeyboardEventKey) {
62 _ = e.Uint32() // serial
63 p.log.add("%s key %d", p.tag, e.Uint32())
64 }
65}
66
67// stampedPointer logs the motion, button and axis events of a wl_pointer.
68type stampedPointer struct {
69 wlturbo.BaseProxy
70 log *stampRecorder
71}
72
73func (p *stampedPointer) Dispatch(e *wlturbo.Event) {
74 switch e.Opcode {
75 case uint16(wayland.PointerEventMotion):
76 p.log.add("motion %d", e.Uint32())
77 case uint16(wayland.PointerEventButton):
78 _ = e.Uint32() // serial
79 p.log.add("button %d", e.Uint32())
80 case uint16(wayland.PointerEventAxis):
81 p.log.add("axis %d", e.Uint32())
82 case uint16(wayland.PointerEventAxisStop):
83 p.log.add("axis_stop %d", e.Uint32())
84 }
85}
86
87// stampProxy logs zwp_input_timestamps_v1.timestamp events.
88type stampProxy struct {
89 wlturbo.BaseProxy
90 log *stampRecorder
91}
92
93func (p *stampProxy) Dispatch(e *wlturbo.Event) {
94 if e.Opcode == uint16(inputtimestamps.ZwpInputTimestampsV1EventTimestamp) {
95 hi, lo, ns := e.Uint32(), e.Uint32(), e.Uint32()
96 p.log.add("ts %d %d %d", hi, lo, ns)
97 }
98}
99
100func stampSubscription(t *testing.T, c *wlturbo.Display, log *stampRecorder, manager uint32, request uint32, device uint32) uint32 {
101 t.Helper()
102 id := c.AllocateID()
103 p := &stampProxy{log: log}
104 p.SetID(id)
105 registerWireProxy(c, p)
106 requestProtocol(t, c, manager, request, id, device)
107 return id
108}
109
110func TestInputTimestampsGlobal(t *testing.T) {
111 s, _, _, dir := keyboardServer(t)
112 c := protocolClient(t, s, dir)
113 g, ok := c.Registry().FindGlobal("zwp_input_timestamps_manager_v1")
114 if !ok || g.Version != 1 {
115 t.Fatalf("zwp_input_timestamps_manager_v1: %+v, %v", g, ok)
116 }
117}
118
119func TestInputTimestampsKeyboard(t *testing.T) {
120 s, events, commands, dir := keyboardServer(t)
121 c := protocolClient(t, s, dir)
122 log := &stampRecorder{}
123 seat := bindProtocol(t, c, "wl_seat")
124 registerProtocol(t, c, seat)
125 var keyboards [2]uint32
126 for i, tag := range []string{"subscribed", "plain"} {
127 keyboards[i] = c.AllocateID()
128 kp := &stampedKeyboard{tag: tag, log: log}
129 kp.SetID(keyboards[i])
130 registerWireProxy(c, kp)
131 requestProtocol(t, c, seat, wayland.SeatRequestGetKeyboard, keyboards[i])
132 }
133 manager := bindProtocol(t, c, "zwp_input_timestamps_manager_v1")
134 ts := stampSubscription(t, c, log, manager, inputtimestamps.ZwpInputTimestampsManagerV1RequestGetKeyboardTimestamps, keyboards[0])
135 w := toplevelMapper(t, c, events)()
136 commands <- ports.FocusWindow{ID: w.ID}
137
138 commands <- ports.ForwardKey{ID: w.ID, Key: ports.KeyEvent{Keycode: 30, Pressed: true, Time: 5*time.Second + 123456789}}
139 got := log.wait(t, c, 3)
140 want := []string{"ts 0 5 123456789", "subscribed key 5123", "plain key 5123"}
141 if !slices.Equal(got, want) {
142 t.Fatalf("log %v, want %v", got, want)
143 }
144
145 // Seconds past 32 bits go to sec_hi.
146 big := time.Duration(1<<32+7) * time.Second
147 commands <- ports.ForwardKey{ID: w.ID, Key: ports.KeyEvent{Keycode: 30, Time: big}}
148 ms := uint64(big / time.Millisecond)
149 got = log.wait(t, c, 6)
150 want = append(want, "ts 1 7 0", fmt.Sprint("subscribed key ", uint32(ms)), fmt.Sprint("plain key ", uint32(ms)))
151 if !slices.Equal(got, want) {
152 t.Fatalf("log %v, want %v", got, want)
153 }
154
155 // A destroyed timestamps object sends nothing more.
156 requestProtocol(t, c, ts, inputtimestamps.ZwpInputTimestampsV1RequestDestroy)
157 if err := c.Roundtrip(); err != nil {
158 t.Fatal(err)
159 }
160 commands <- ports.ForwardKey{ID: w.ID, Key: ports.KeyEvent{Keycode: 31, Pressed: true, Time: 9 * time.Second}}
161 got = log.wait(t, c, 8)
162 want = append(want, "subscribed key 9000", "plain key 9000")
163 if !slices.Equal(got, want) {
164 t.Fatalf("log %v, want %v", got, want)
165 }
166}
167
168func TestInputTimestampsPointer(t *testing.T) {
169 s, events, commands, dir := keyboardServer(t)
170 c := protocolClient(t, s, dir)
171 log := &stampRecorder{}
172 seat := bindVersion(t, c, "wl_seat", 8)
173 registerProtocol(t, c, seat)
174 pointer := c.AllocateID()
175 pp := &stampedPointer{log: log}
176 pp.SetID(pointer)
177 registerWireProxy(c, pp)
178 requestProtocol(t, c, seat, wayland.SeatRequestGetPointer, pointer)
179 manager := bindProtocol(t, c, "zwp_input_timestamps_manager_v1")
180 ts := stampSubscription(t, c, log, manager, inputtimestamps.ZwpInputTimestampsManagerV1RequestGetPointerTimestamps, pointer)
181 w := toplevelMapper(t, c, events)()
182 commands <- ports.PointerFocus{ID: w.ID, X: 0, Y: 0}
183
184 commands <- ports.PointerMotionTo{ID: w.ID, X: 0, Y: 0, Time: 2*time.Second + time.Millisecond}
185 commands <- ports.PointerButtonTo{ID: w.ID, Button: 0x110, Pressed: true, Time: 3 * time.Second}
186 commands <- ports.PointerAxisTo{ID: w.ID, Axis: ports.PointerAxis{Source: ports.AxisFinger, Vertical: ports.ScrollAxis{Set: true, Value: 5}, Time: 4 * time.Second}}
187 commands <- ports.PointerAxisTo{ID: w.ID, Axis: ports.PointerAxis{Source: ports.AxisFinger, Horizontal: ports.ScrollAxis{Set: true, Stop: true}, Time: 5 * time.Second}}
188 got := log.wait(t, c, 8)
189 want := []string{
190 "ts 0 2 1000000", "motion 2001",
191 "ts 0 3 0", "button 3000",
192 "ts 0 4 0", "axis 4000",
193 "ts 0 5 0", "axis_stop 5000",
194 }
195 if !slices.Equal(got, want) {
196 t.Fatalf("log %v, want %v", got, want)
197 }
198
199 // The stamped input path allocates nothing, with a live subscriber.
200 var allocs float64
201 s.display.Do(func() {
202 allocs = testing.AllocsPerRun(100, func() {
203 s.applyInput(ports.PointerMotionTo{ID: w.ID, Time: 6 * time.Second})
204 })
205 })
206 if allocs != 0 {
207 t.Fatalf("stamped motion allocates %v", allocs)
208 }
209
210 // Releasing the pointer makes the object inert and drops its entry;
211 // destroying it afterwards is still fine.
212 requestProtocol(t, c, pointer, wayland.PointerRequestRelease)
213 if err := c.Roundtrip(); err != nil {
214 t.Fatal(err)
215 }
216 if n := stampEntries(s); n != 0 {
217 t.Fatalf("%d stamp entries after the pointer died", n)
218 }
219 requestProtocol(t, c, ts, inputtimestamps.ZwpInputTimestampsV1RequestDestroy)
220 if err := c.Roundtrip(); err != nil {
221 t.Fatal(err)
222 }
223}
224
225// Destroying the timestamps object before its device drops the entry too.
226func TestInputTimestampsDestroyBeforeDevice(t *testing.T) {
227 s, _, _, dir := keyboardServer(t)
228 c := protocolClient(t, s, dir)
229 log := &stampRecorder{}
230 seat := bindProtocol(t, c, "wl_seat")
231 registerProtocol(t, c, seat)
232 pointer := c.AllocateID()
233 registerProtocol(t, c, pointer)
234 requestProtocol(t, c, seat, wayland.SeatRequestGetPointer, pointer)
235 manager := bindProtocol(t, c, "zwp_input_timestamps_manager_v1")
236 ts := stampSubscription(t, c, log, manager, inputtimestamps.ZwpInputTimestampsManagerV1RequestGetPointerTimestamps, pointer)
237 if err := c.Roundtrip(); err != nil {
238 t.Fatal(err)
239 }
240 if n := stampEntries(s); n != 1 {
241 t.Fatalf("%d stamp entries, want 1", n)
242 }
243 requestProtocol(t, c, ts, inputtimestamps.ZwpInputTimestampsV1RequestDestroy)
244 if err := c.Roundtrip(); err != nil {
245 t.Fatal(err)
246 }
247 if n := stampEntries(s); n != 0 {
248 t.Fatalf("%d stamp entries after destroy", n)
249 }
250}
251
252// stampEntries counts the devices with subscriptions, on the display goroutine.
253func stampEntries(s *Server) int {
254 var n int
255 s.display.Do(func() { n = len(s.seat.keyStamps) + len(s.seat.pointerStamps) })
256 return n
257}