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