1package wayland
2
3import (
4 "context"
5 "encoding/binary"
6 "testing"
7 "time"
8
9 "github.com/bnema/neferwl/internal/logging"
10 "github.com/bnema/neferwl/internal/ports"
11 "github.com/bnema/purego-libwayland/protocol/presentationtime"
12 "github.com/bnema/purego-libwayland/protocol/wayland"
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"}
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 c.Context().Register(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 := c.SendRequestWithFDs(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 // Presented with the flip's time, counter and flags.
162 a := feedback()
163 commit()
164 seqA := lastSeq()
165 flip(5*time.Second+7, 42, seqA, w.ID)
166 ev := answer(a, "presented")
167 if ev.discarded || ev.when != 5*time.Second+7 || ev.seq != 42 || ev.refresh != uint32(time.Second/60) || ev.syncOutputs != 1 {
168 t.Fatalf("presented %+v", ev)
169 }
170 want := uint32(presentationtime.WpPresentationFeedbackKindVsync | presentationtime.WpPresentationFeedbackKindHwClock | presentationtime.WpPresentationFeedbackKindHwCompletion | presentationtime.WpPresentationFeedbackKindZeroCopy)
171 if ev.flags != want {
172 t.Fatalf("flags %#x want %#x", ev.flags, want)
173 }
174 // Two flips in one drain: two presented events, each with its time.
175 b := feedback()
176 commit()
177 seqB := lastSeq()
178 d := feedback()
179 commit()
180 seqD := lastSeq()
181 flip(6*time.Second, 43, seqB, w.ID+1) // another window on the overlay
182 flip(7*time.Second, 44, seqD, 0)
183 if ev := answer(b, "first of two"); ev.when != 6*time.Second || ev.seq != 43 || ev.flags&uint32(presentationtime.WpPresentationFeedbackKindZeroCopy) != 0 {
184 t.Fatalf("first %+v", ev)
185 }
186 if ev := answer(d, "second of two"); ev.when != 7*time.Second || ev.seq != 44 {
187 t.Fatalf("second %+v", ev)
188 }
189 // Replaced before shown: discarded when the flip shows the newer one.
190 old := feedback()
191 commit()
192 lastSeq()
193 commit()
194 seqNew := lastSeq()
195 flip(8*time.Second, 45, seqNew, 0)
196 if ev := answer(old, "replaced"); !ev.discarded {
197 t.Fatalf("replaced: %+v", ev)
198 }
199 // A subsurface's feedback follows its root: presented with the root's
200 // content that the flip shows.
201 comp := bindProtocol(t, c, "wl_compositor")
202 sub := bindProtocol(t, c, "wl_subcompositor")
203 child, subsurf := c.AllocateID(), c.AllocateID()
204 requestProtocol(t, c, comp, wayland.CompositorRequestCreateSurface, child)
205 registerProtocol(t, c, child)
206 requestProtocol(t, c, sub, wayland.SubcompositorRequestGetSubsurface, subsurf, child, surf)
207 registerProtocol(t, c, subsurf)
208 cfb := c.AllocateID()
209 cp := &feedbackEvents{out: make(chan presentedEvent, 1)}
210 cp.SetID(cfb)
211 c.Context().Register(cp)
212 requestProtocol(t, c, pres, presentationtime.WpPresentationRequestFeedback, child, cfb)
213 requestProtocol(t, c, child, wayland.SurfaceRequestCommit)
214 commit() // the parent applies the subsurface
215 seqC := lastSeq()
216 flip(8500*time.Millisecond, 50, seqC, 0)
217 if ev := answer(cp, "subsurface"); ev.discarded || ev.seq != 50 {
218 t.Fatalf("subsurface %+v", ev)
219 }
220 // Surface destroyed: discarded.
221 gone := feedback()
222 commit()
223 lastSeq()
224 requestProtocol(t, c, surf, wayland.SurfaceRequestDestroy)
225 flip(9*time.Second, 46, 0, 0)
226 if ev := answer(gone, "destroyed"); !ev.discarded {
227 t.Fatalf("destroyed: %+v", ev)
228 }
229}
230
231// A hidden window (no output) gets discarded at once.
232func TestPresentationHiddenDiscarded(t *testing.T) {
233 s, events, _, _, _, dir := presentServer(t)
234 c := protocolClient(t, s, dir)
235 _, surf, _ := surfaceMapper(t, c, events)()
236 pres := bindProtocol(t, c, "wp_presentation")
237 registerProtocol(t, c, pres)
238 fb := c.AllocateID()
239 p := &feedbackEvents{out: make(chan presentedEvent, 1)}
240 p.SetID(fb)
241 c.Context().Register(p)
242 requestProtocol(t, c, pres, presentationtime.WpPresentationRequestFeedback, surf, fb)
243 requestProtocol(t, c, surf, wayland.SurfaceRequestCommit)
244 if err := c.Roundtrip(); err != nil {
245 t.Fatal(err)
246 }
247 if err := c.Roundtrip(); err != nil {
248 t.Fatal(err)
249 }
250 select {
251 case ev := <-p.out:
252 if !ev.discarded {
253 t.Fatalf("%+v", ev)
254 }
255 default:
256 t.Fatal("not discarded")
257 }
258}
259
260// Commit timing: a commit targeted two refreshes ahead of the last flip
261// is not applied on the next one.
262func TestNextRefreshFollowsFlips(t *testing.T) {
263 s := &Server{flips: map[string]ports.FlipInfo{}}
264 s.outputs = []*output{{place: ports.OutputPlacement{Info: ports.OutputInfo{Name: "A", RefreshMilli: 60000}}}}
265 now := time.Now()
266 mono := monotonic(now)
267 period := time.Second / 60
268 s.flips["A"] = ports.FlipInfo{When: mono - period/4, Refresh: period}
269 next := s.nextRefresh("A", now)
270 if d := next.Sub(now); d < period*3/4-time.Millisecond || d > period*3/4+time.Millisecond {
271 t.Fatalf("next refresh in %v, want %v", d, period*3/4)
272 }
273}
274
275// Software flips (headless) answer feedback without flags and do not pace
276// frame callbacks.
277func TestSoftwareFlipsDoNotPace(t *testing.T) {
278 if paces(ports.OutputPresented{Flip: &ports.FlipInfo{}}) {
279 t.Fatal("software flip paces callbacks")
280 }
281 if !paces(ports.OutputPresented{Flip: &ports.FlipInfo{HardwareClock: true}}) {
282 t.Fatal("display flip does not pace")
283 }
284}