1package wayland
2
3import (
4 "context"
5 "encoding/binary"
6 "testing"
7 "time"
8
9 "github.com/bnema/go-wayland-bindings/server/presentationtime"
10 "github.com/bnema/go-wayland-bindings/server/wayland"
11 "github.com/bnema/neferwl/internal/logging"
12 "github.com/bnema/neferwl/internal/ports"
13 "github.com/bnema/wlturbo"
14 "golang.org/x/sys/unix"
15)
16
17// presentedEvent is one feedback answer: presented (with its fields) or
18// discarded.
19type presentedEvent struct {
20 discarded bool
21 when time.Duration
22 refresh uint32
23 seq uint64
24 flags uint32
25 syncOutputs int
26}
27
28type feedbackEvents struct {
29 wlturbo.BaseProxy
30 out chan presentedEvent
31 sync int
32}
33
34func (p *feedbackEvents) Dispatch(e *wlturbo.Event) {
35 switch uint32(e.Opcode) {
36 case presentationtime.WpPresentationFeedbackEventSyncOutput:
37 p.sync++
38 case presentationtime.WpPresentationFeedbackEventPresented:
39 d := e.Data()
40 u := func(i int) uint32 { return binary.NativeEndian.Uint32(d[i*4:]) }
41 sec := uint64(u(0))<<32 | uint64(u(1))
42 p.out <- presentedEvent{when: time.Duration(sec)*time.Second + time.Duration(u(2)), refresh: u(3), seq: uint64(u(4))<<32 | uint64(u(5)), flags: u(6), syncOutputs: p.sync}
43 case presentationtime.WpPresentationFeedbackEventDiscarded:
44 p.out <- presentedEvent{discarded: true}
45 }
46}
47
48// presentServer is a server whose flips the test sends.
49func presentServer(t *testing.T) (*Server, chan ports.ClientEvent, chan ports.ClientCommand, chan ports.SurfaceContent, chan ports.OutputPresented, string) {
50 t.Helper()
51 dir := t.TempDir()
52 events := make(chan ports.ClientEvent, 16)
53 commands := make(chan ports.ClientCommand, 16)
54 contents := make(chan ports.SurfaceContent, 64)
55 presented := make(chan ports.OutputPresented, 16)
56 s, err := New(Options{RuntimeDir: dir, Outputs: testOutputs}, Channels{Events: events, Commands: commands, Contents: contents, Presented: presented}, logging.For(context.Background(), "wayland"))
57 if err != nil {
58 t.Fatal(err)
59 }
60 ctx, cancel := context.WithCancel(context.Background())
61 done := make(chan error, 1)
62 go func() { done <- s.Run(ctx) }()
63 t.Cleanup(func() { cancel(); <-done })
64 return s, events, commands, contents, presented, dir
65}
66
67func TestPresentationFeedback(t *testing.T) {
68 s, events, commands, contents, presented, dir := presentServer(t)
69 c := protocolClient(t, s, dir)
70 w, surf, xdg := surfaceMapper(t, c, events)()
71 registerProtocol(t, c, xdg)
72 commands <- ports.ConfigureWindow{ID: w.ID, Width: 100, Height: 100, Output: "HEADLESS-1", Visible: true}
73 for deadline := time.Now().Add(2 * time.Second); ; {
74 var out string
75 s.display.Do(func() { out = s.frameOutput(s.windows[w.ID].xdg.surface) })
76 if out != "" {
77 break
78 }
79 if time.Now().After(deadline) {
80 t.Fatal("configure not applied")
81 }
82 time.Sleep(2 * time.Millisecond)
83 }
84 registerProtocol(t, c, bindProtocol(t, c, "wl_output"))
85 pres := bindProtocol(t, c, "wp_presentation")
86 registerProtocol(t, c, pres)
87 // Drain contents so seqs are known.
88 lastSeq := func() uint64 {
89 t.Helper()
90 var seq uint64
91 deadline := time.After(2 * time.Second)
92 for {
93 select {
94 case got := <-contents:
95 if got.ID == w.ID {
96 seq = got.Seq
97 }
98 case <-time.After(50 * time.Millisecond):
99 if seq != 0 {
100 return seq
101 }
102 case <-deadline:
103 t.Fatal("no content")
104 }
105 }
106 }
107 feedback := func() *feedbackEvents {
108 t.Helper()
109 fb := c.AllocateID()
110 p := &feedbackEvents{out: make(chan presentedEvent, 4)}
111 p.SetID(fb)
112 registerWireProxy(c, p)
113 requestProtocol(t, c, pres, presentationtime.WpPresentationRequestFeedback, surf, fb)
114 return p
115 }
116 answer := func(p *feedbackEvents, what string) presentedEvent {
117 t.Helper()
118 deadline := time.Now().Add(2 * time.Second)
119 for time.Now().Before(deadline) {
120 if err := c.Roundtrip(); err != nil {
121 t.Fatal(err)
122 }
123 select {
124 case ev := <-p.out:
125 return ev
126 case <-time.After(5 * time.Millisecond):
127 }
128 }
129 t.Fatalf("no answer: %s", what)
130 return presentedEvent{}
131 }
132 // A fresh 1×1 buffer per commit: every commit is a new content.
133 shm := bindProtocol(t, c, "wl_shm")
134 registerProtocol(t, c, shm)
135 fd, err := unix.MemfdCreate("present", 0)
136 if err != nil {
137 t.Fatal(err)
138 }
139 defer unix.Close(fd)
140 if err := unix.Ftruncate(fd, 4); err != nil {
141 t.Fatal(err)
142 }
143 pool := c.AllocateID()
144 registerProtocol(t, c, pool)
145 if err := wireRequest(c, shm, uint16(wayland.ShmRequestCreatePool), []int{fd}, pool, int32(4)); err != nil {
146 t.Fatal(err)
147 }
148 commit := func() {
149 buf := c.AllocateID()
150 registerProtocol(t, c, buf)
151 requestProtocol(t, c, pool, wayland.ShmPoolRequestCreateBuffer, buf, int32(0), int32(1), int32(1), int32(4), uint32(0))
152 requestProtocol(t, c, surf, wayland.SurfaceRequestAttach, buf, int32(0), int32(0))
153 requestProtocol(t, c, surf, wayland.SurfaceRequestCommit)
154 if err := c.Roundtrip(); err != nil {
155 t.Fatal(err)
156 }
157 }
158 flip := func(when time.Duration, seq uint64, shows uint64, zero ports.WindowID) {
159 presented <- ports.OutputPresented{Output: "HEADLESS-1", Flip: &ports.FlipInfo{When: when, Seq: seq, Refresh: time.Second / 60, ZeroCopy: zero, HardwareClock: true, Shows: map[ports.WindowID]uint64{w.ID: shows}}}
160 }
161 // With no flip, hiding must discard a waiting feedback immediately.
162 hidden := feedback()
163 commit()
164 commands <- ports.ConfigureWindow{ID: w.ID, Width: 100, Height: 100, Output: "HEADLESS-1", Visible: false}
165 if ev := answer(hidden, "hidden without flip"); !ev.discarded {
166 t.Fatalf("hidden feedback: %+v", ev)
167 }
168 commands <- ports.ConfigureWindow{ID: w.ID, Width: 100, Height: 100, Output: "HEADLESS-1", Visible: true}
169 // Wait for the resume before committing the next feedback: the command
170 // goroutine applies it in its own display round trip, which an empty Do
171 // here may overtake.
172 for deadline := time.Now().Add(2 * time.Second); ; {
173 var visible bool
174 s.display.Do(func() { visible = s.windows[w.ID].last.Visible })
175 if visible {
176 break
177 }
178 if time.Now().After(deadline) {
179 t.Fatal("resume not applied")
180 }
181 time.Sleep(2 * time.Millisecond)
182 }
183 // Presented with the flip's time, counter and flags.
184 a := feedback()
185 commit()
186 seqA := lastSeq()
187 flip(5*time.Second+7, 42, seqA, w.ID)
188 ev := answer(a, "presented")
189 if ev.discarded || ev.when != 5*time.Second+7 || ev.seq != 42 || ev.refresh != uint32(time.Second/60) || ev.syncOutputs != 1 {
190 t.Fatalf("presented %+v", ev)
191 }
192 want := uint32(presentationtime.WpPresentationFeedbackKindVsync | presentationtime.WpPresentationFeedbackKindHwClock | presentationtime.WpPresentationFeedbackKindHwCompletion | presentationtime.WpPresentationFeedbackKindZeroCopy)
193 if ev.flags != want {
194 t.Fatalf("flags %#x want %#x", ev.flags, want)
195 }
196 // Two flips in one drain: two presented events, each with its time.
197 b := feedback()
198 commit()
199 seqB := lastSeq()
200 d := feedback()
201 commit()
202 seqD := lastSeq()
203 flip(6*time.Second, 43, seqB, w.ID+1) // another window on the overlay
204 flip(7*time.Second, 44, seqD, 0)
205 if ev := answer(b, "first of two"); ev.when != 6*time.Second || ev.seq != 43 || ev.flags&uint32(presentationtime.WpPresentationFeedbackKindZeroCopy) != 0 {
206 t.Fatalf("first %+v", ev)
207 }
208 if ev := answer(d, "second of two"); ev.when != 7*time.Second || ev.seq != 44 {
209 t.Fatalf("second %+v", ev)
210 }
211 // Replaced before shown: discarded when the flip shows the newer one.
212 old := feedback()
213 commit()
214 lastSeq()
215 commit()
216 seqNew := lastSeq()
217 flip(8*time.Second, 45, seqNew, 0)
218 if ev := answer(old, "replaced"); !ev.discarded {
219 t.Fatalf("replaced: %+v", ev)
220 }
221 // A subsurface's feedback follows its root: presented with the root's
222 // content that the flip shows.
223 comp := bindProtocol(t, c, "wl_compositor")
224 sub := bindProtocol(t, c, "wl_subcompositor")
225 child, subsurf := c.AllocateID(), c.AllocateID()
226 requestProtocol(t, c, comp, wayland.CompositorRequestCreateSurface, child)
227 registerProtocol(t, c, child)
228 requestProtocol(t, c, sub, wayland.SubcompositorRequestGetSubsurface, subsurf, child, surf)
229 registerProtocol(t, c, subsurf)
230 cfb := c.AllocateID()
231 cp := &feedbackEvents{out: make(chan presentedEvent, 1)}
232 cp.SetID(cfb)
233 registerWireProxy(c, cp)
234 requestProtocol(t, c, pres, presentationtime.WpPresentationRequestFeedback, child, cfb)
235 requestProtocol(t, c, child, wayland.SurfaceRequestCommit)
236 commit() // the parent applies the subsurface
237 seqC := lastSeq()
238 flip(8500*time.Millisecond, 50, seqC, 0)
239 if ev := answer(cp, "subsurface"); ev.discarded || ev.seq != 50 {
240 t.Fatalf("subsurface %+v", ev)
241 }
242 // Surface destroyed: discarded.
243 gone := feedback()
244 commit()
245 lastSeq()
246 requestProtocol(t, c, surf, wayland.SurfaceRequestDestroy)
247 flip(9*time.Second, 46, 0, 0)
248 if ev := answer(gone, "destroyed"); !ev.discarded {
249 t.Fatalf("destroyed: %+v", ev)
250 }
251}
252
253// A hidden window (no output) gets discarded at once.
254func TestPresentationHiddenDiscarded(t *testing.T) {
255 s, events, _, _, _, dir := presentServer(t)
256 c := protocolClient(t, s, dir)
257 _, surf, _ := surfaceMapper(t, c, events)()
258 pres := bindProtocol(t, c, "wp_presentation")
259 registerProtocol(t, c, pres)
260 fb := c.AllocateID()
261 p := &feedbackEvents{out: make(chan presentedEvent, 1)}
262 p.SetID(fb)
263 registerWireProxy(c, p)
264 requestProtocol(t, c, pres, presentationtime.WpPresentationRequestFeedback, surf, fb)
265 requestProtocol(t, c, surf, wayland.SurfaceRequestCommit)
266 if err := c.Roundtrip(); err != nil {
267 t.Fatal(err)
268 }
269 if err := c.Roundtrip(); err != nil {
270 t.Fatal(err)
271 }
272 select {
273 case ev := <-p.out:
274 if !ev.discarded {
275 t.Fatalf("%+v", ev)
276 }
277 default:
278 t.Fatal("not discarded")
279 }
280}
281
282// Commit timing: a commit targeted two refreshes ahead of the last flip
283// is not applied on the next one.
284func TestNextRefreshFollowsFlips(t *testing.T) {
285 s := &Server{flips: map[string]ports.FlipInfo{}}
286 s.outputs = []*output{{place: ports.OutputPlacement{Info: ports.OutputInfo{Name: "A", RefreshMilli: 60000}}}}
287 now := time.Now()
288 mono := monotonic(now)
289 period := time.Second / 60
290 s.flips["A"] = ports.FlipInfo{When: mono - period/4, Refresh: period}
291 next := s.nextRefresh("A", now)
292 if d := next.Sub(now); d < period*3/4-time.Millisecond || d > period*3/4+time.Millisecond {
293 t.Fatalf("next refresh in %v, want %v", d, period*3/4)
294 }
295}
296
297// Software flips (headless) answer feedback without flags and do not pace
298// frame callbacks.
299func TestSoftwareFlipsDoNotPace(t *testing.T) {
300 if paces(ports.OutputPresented{Flip: &ports.FlipInfo{}}) {
301 t.Fatal("software flip paces callbacks")
302 }
303 if !paces(ports.OutputPresented{Flip: &ports.FlipInfo{HardwareClock: true}}) {
304 t.Fatal("display flip does not pace")
305 }
306}