1//go:build linux
2
3package client_test
4
5// Group A client bindings (presentation timing, surface render hints,
6// activation, pointer warp, colour and DRM lease). Every test drives a real
7// Display over a socketpair and checks the bytes the client writes and the
8// bytes it accepts, not just that a method exists.
9
10import (
11 "bytes"
12 "encoding/binary"
13 "errors"
14 "net"
15 "os"
16 "testing"
17
18 "github.com/bnema/go-wayland-bindings/client/alphamodifier"
19 "github.com/bnema/go-wayland-bindings/client/colormanagement"
20 "github.com/bnema/go-wayland-bindings/client/colorrepresentation"
21 "github.com/bnema/go-wayland-bindings/client/committiming"
22 "github.com/bnema/go-wayland-bindings/client/contenttype"
23 "github.com/bnema/go-wayland-bindings/client/drmlease"
24 "github.com/bnema/go-wayland-bindings/client/fifo"
25 "github.com/bnema/go-wayland-bindings/client/pointerwarp"
26 presentation "github.com/bnema/go-wayland-bindings/client/presentationtime"
27 "github.com/bnema/go-wayland-bindings/client/tearingcontrol"
28 "github.com/bnema/go-wayland-bindings/client/wayland"
29 "github.com/bnema/go-wayland-bindings/client/xdgactivation"
30 "github.com/bnema/wlturbo"
31 "golang.org/x/sys/unix"
32)
33
34// rxEnv is the shared wire environment plus one object of each core kind the
35// group A protocols refer to.
36type rxEnv struct {
37 *wireEnv
38 surface *wayland.Surface
39 output *wayland.Output
40 seat *wayland.Seat
41 pointer *wayland.Pointer
42}
43
44func rxNew(t *testing.T) *rxEnv {
45 t.Helper()
46 s := newWireEnv(t)
47 return &rxEnv{wireEnv: s, surface: s.surface(), output: s.output(), seat: s.seat(), pointer: s.pointer()}
48}
49
50// rxWant reads one request and requires its exact object, opcode and
51// 32-bit word arguments.
52func rxWant(t *testing.T, p *net.UnixConn, id uint32, op uint16, want ...uint32) {
53 t.Helper()
54 wantRequestWords(t, p, id, op, want...)
55}
56
57// rxWantRaw reads one request and requires its exact object, opcode and body.
58func rxWantRaw(t *testing.T, p *net.UnixConn, id uint32, op uint16, want []byte) {
59 t.Helper()
60 gid, gop, body := request(t, p)
61 if gid != id || gop != op || !bytes.Equal(body, want) {
62 t.Fatalf("request object=%d op=%d body=%x, want object=%d op=%d body=%x", gid, gop, body, id, op, want)
63 }
64}
65
66// rxRebind binds a second object to an interface that is already bound: the
67// earlier announcement is withdrawn first so BindNegotiated finds the new one.
68func rxRebind(e *rxEnv, oldName, name uint32, iface string, announced, supported uint32, proxy wlturbo.Proxy) {
69 e.t.Helper()
70 e.event(e.d.Registry().ID(), 1, wordBytes(oldName)...)
71 e.bind(name, iface, announced, supported, proxy)
72}
73
74func rxOpenFDs(t *testing.T) int {
75 t.Helper()
76 ents, err := os.ReadDir("/proc/self/fd")
77 noerr(t, err)
78 return len(ents)
79}
80
81func rxTempFile(t *testing.T, content string) *os.File {
82 t.Helper()
83 f, err := os.CreateTemp(t.TempDir(), "rx")
84 noerr(t, err)
85 _, err = f.WriteString(content)
86 noerr(t, err)
87 t.Cleanup(func() { f.Close() })
88 return f
89}
90
91// rxReadFD reads want bytes at offset 0 of a descriptor and closes it.
92func rxReadFD(t *testing.T, fd int, want string) {
93 t.Helper()
94 buf := make([]byte, len(want))
95 n, err := unix.Pread(fd, buf, 0)
96 noerr(t, err)
97 if string(buf[:n]) != want {
98 t.Fatalf("fd content %q, want %q", buf[:n], want)
99 }
100 noerr(t, unix.Close(fd))
101}
102
103func TestPresentationFeedbackDestructorEvents(t *testing.T) {
104 e := rxNew(t)
105 pres := presentation.NewWpPresentation(e.d.Context())
106 bind(t, e.d, e.p, 1, presentation.WpPresentationInterface, 2, 2, pres)
107 if pres.Version() != 2 {
108 t.Fatalf("version %d", pres.Version())
109 }
110 var clock uint32
111 pres.OnClockId(func(id uint32) { clock = id })
112 e.eventFD(pres.ID(), 0, wordBytes(1)) // CLOCK_MONOTONIC
113 if clock != 1 {
114 t.Fatalf("clock %d", clock)
115 }
116
117 fb1, err := pres.Feedback(e.surface)
118 noerr(t, err)
119 rxWant(t, e.p, pres.ID(), 1, e.surface.ID(), fb1.ID())
120 fb2, err := pres.Feedback(e.surface)
121 noerr(t, err)
122 rxWant(t, e.p, pres.ID(), 1, e.surface.ID(), fb2.ID())
123 if fb1.Version() != 2 || fb1.ID() == fb2.ID() {
124 t.Fatalf("children id=%d/%d version=%d", fb1.ID(), fb2.ID(), fb1.Version())
125 }
126
127 // Destroying the manager must not affect feedback it already created.
128 noerr(t, pres.Destroy())
129 rxWant(t, e.p, pres.ID(), 0)
130 if err := pres.Destroy(); err == nil {
131 t.Fatal("second manager destroy accepted")
132 }
133
134 type presented struct{ hi, lo, nsec, refresh, seqHi, seqLo, flags uint32 }
135 var outputs []uint32
136 var got []presented
137 fb1.OnSyncOutput(func(id uint32) { outputs = append(outputs, id) })
138 fb1.OnPresented(func(hi, lo, nsec, refresh, sh, sl, flags uint32) {
139 got = append(got, presented{hi, lo, nsec, refresh, sh, sl, flags})
140 })
141 discarded := 0
142 fb2.OnDiscarded(func() { discarded++ })
143 fb2.OnPresented(func(uint32, uint32, uint32, uint32, uint32, uint32, uint32) { t.Fatal("discarded feedback presented") })
144
145 e.eventFD(fb1.ID(), 0, wordBytes(e.output.ID()))
146 if len(outputs) != 1 || outputs[0] != e.output.ID() {
147 t.Fatalf("sync_output %v", outputs)
148 }
149 // A presented event missing its last argument is malformed and must not
150 // destroy the object or reach the handler.
151 send(t, e.p, payload(fb1.ID(), 1, wordBytes(0, 1, 2, 3, 4, 5)...))
152 if err := e.d.Dispatch(); !errors.Is(err, wlturbo.ErrMalformedFrame) {
153 t.Fatalf("short presented = %v, want ErrMalformedFrame", err)
154 }
155 if len(got) != 0 {
156 t.Fatal("handler ran for a malformed event")
157 }
158 flags := uint32(presentation.KIND_VSYNC | presentation.KIND_HW_CLOCK | presentation.KIND_ZERO_COPY)
159 e.eventFD(fb1.ID(), 1, wordBytes(0, 0x12345678, 999999999, 16666666, 1, 2, flags))
160 want := presented{0, 0x12345678, 999999999, 16666666, 1, 2, 0xb}
161 if len(got) != 1 || got[0] != want {
162 t.Fatalf("presented %+v, want %+v", got, want)
163 }
164
165 e.eventFD(fb2.ID(), 2, nil)
166 if discarded != 1 {
167 t.Fatalf("discarded %d", discarded)
168 }
169
170 // Both terminal events are destructors: the objects are gone, so a
171 // further event for either is an unknown-object protocol error.
172 send(t, e.p, msg(fb1.ID(), 2))
173 if err := e.d.Dispatch(); !errors.Is(err, wlturbo.ErrUnknownObject) {
174 t.Fatalf("event after presented = %v, want ErrUnknownObject", err)
175 }
176 send(t, e.p, msg(fb2.ID(), 2))
177 if err := e.d.Dispatch(); !errors.Is(err, wlturbo.ErrUnknownObject) {
178 t.Fatalf("event after discarded = %v, want ErrUnknownObject", err)
179 }
180}
181
182func TestPresentationVersionNegotiation(t *testing.T) {
183 e := rxNew(t)
184 old := presentation.NewWpPresentation(e.d.Context())
185 bind(t, e.d, e.p, 1, presentation.WpPresentationInterface, 1, 2, old)
186 if old.Version() != 1 {
187 t.Fatalf("server v1: bound at %d", old.Version())
188 }
189 fb, err := old.Feedback(e.surface)
190 noerr(t, err)
191 if fb.Version() != 1 {
192 t.Fatalf("child version %d, want the parent's 1", fb.Version())
193 }
194 rxWant(t, e.p, old.ID(), 1, e.surface.ID(), fb.ID())
195
196 newer := presentation.NewWpPresentation(e.d.Context())
197 rxRebind(e, 1, 2, presentation.WpPresentationInterface, 5, 2, newer)
198 if newer.Version() != 2 {
199 t.Fatalf("server v5: bound at %d, want the supported 2", newer.Version())
200 }
201 absent := presentation.NewWpPresentation(e.d.Context())
202 if _, err := e.d.Registry().BindNegotiated(presentation.WpPresentationInterface+"_x", 2, absent); !errors.Is(err, wlturbo.ErrGlobalNotFound) {
203 t.Fatalf("absent global = %v", err)
204 }
205 requireNoWrite(t, e.p)
206}
207
208func TestColorManagementVersionRejection(t *testing.T) {
209 e := rxNew(t)
210 cm := colormanagement.NewWpColorManager(e.d.Context())
211 bind(t, e.d, e.p, 1, colormanagement.WpColorManagerInterface, 1, 3, cm)
212 if cm.Version() != 1 {
213 t.Fatalf("bound at %d", cm.Version())
214 }
215 // Version 1 requests still work...
216 surf, err := cm.GetSurface(e.surface)
217 noerr(t, err)
218 rxWant(t, e.p, cm.ID(), 2, surf.ID(), e.surface.ID())
219 // ...but the version 2 and 3 additions are refused locally, without a
220 // byte reaching the compositor and without consuming an object ID.
221 ref := colormanagement.NewWpImageDescriptionReference(e.d.Context())
222 ref.SetID(e.d.AllocateID())
223 e.d.Context().Register(ref)
224 next := e.d.AllocateID()
225 if _, err := cm.GetImageDescription(ref); !errors.Is(err, wlturbo.ErrVersionTooLow) {
226 t.Fatalf("get_image_description on v1 = %v", err)
227 }
228 if _, err := cm.CreateWindowsBt2100(); !errors.Is(err, wlturbo.ErrVersionTooLow) {
229 t.Fatalf("create_windows_bt2100 on v1 = %v", err)
230 }
231 requireNoWrite(t, e.p)
232 if after := e.d.AllocateID(); after != next+1 {
233 t.Fatalf("rejected requests allocated ids: %d then %d", next, after)
234 }
235
236 // Announced v2 admits get_image_description but still not the v3 request.
237 cm2 := colormanagement.NewWpColorManager(e.d.Context())
238 rxRebind(e, 1, 2, colormanagement.WpColorManagerInterface, 2, 3, cm2)
239 desc, err := cm2.GetImageDescription(ref)
240 noerr(t, err)
241 rxWant(t, e.p, cm2.ID(), 7, desc.ID(), ref.ID())
242 if _, err := cm2.CreateWindowsBt2100(); !errors.Is(err, wlturbo.ErrVersionTooLow) {
243 t.Fatalf("create_windows_bt2100 on v2 = %v", err)
244 }
245 requireNoWrite(t, e.p)
246
247 cm3 := colormanagement.NewWpColorManager(e.d.Context())
248 rxRebind(e, 2, 3, colormanagement.WpColorManagerInterface, 3, 3, cm3)
249 bt, err := cm3.CreateWindowsBt2100()
250 noerr(t, err)
251 rxWant(t, e.p, cm3.ID(), 8, bt.ID())
252}
253
254func TestColorManagementParametricWire(t *testing.T) {
255 e := rxNew(t)
256 cm := colormanagement.NewWpColorManager(e.d.Context())
257 bind(t, e.d, e.p, 1, colormanagement.WpColorManagerInterface, 3, 3, cm)
258
259 var seq []string
260 cm.OnSupportedIntent(func(v uint32) {
261 if v != colormanagement.RENDER_INTENT_PERCEPTUAL {
262 t.Fatal(v)
263 }
264 seq = append(seq, "intent")
265 })
266 cm.OnSupportedFeature(func(v uint32) {
267 if v != colormanagement.FEATURE_PARAMETRIC {
268 t.Fatal(v)
269 }
270 seq = append(seq, "feature")
271 })
272 cm.OnSupportedTfNamed(func(v uint32) {
273 if v != colormanagement.TRANSFER_FUNCTION_ST2084_PQ {
274 t.Fatal(v)
275 }
276 seq = append(seq, "tf")
277 })
278 cm.OnSupportedPrimariesNamed(func(v uint32) {
279 if v != colormanagement.PRIMARIES_SRGB {
280 t.Fatal(v)
281 }
282 seq = append(seq, "primaries")
283 })
284 cm.OnDone(func() { seq = append(seq, "done") })
285 e.eventFD(cm.ID(), 0, wordBytes(colormanagement.RENDER_INTENT_PERCEPTUAL))
286 e.eventFD(cm.ID(), 1, wordBytes(colormanagement.FEATURE_PARAMETRIC))
287 e.eventFD(cm.ID(), 2, wordBytes(colormanagement.TRANSFER_FUNCTION_ST2084_PQ))
288 e.eventFD(cm.ID(), 3, wordBytes(colormanagement.PRIMARIES_SRGB))
289 e.eventFD(cm.ID(), 4, nil)
290 if len(seq) != 5 || seq[0] != "intent" || seq[4] != "done" {
291 t.Fatalf("manager events %v", seq)
292 }
293
294 out, err := cm.GetOutput(e.output)
295 noerr(t, err)
296 rxWant(t, e.p, cm.ID(), 1, out.ID(), e.output.ID())
297 changed := 0
298 out.OnImageDescriptionChanged(func() { changed++ })
299 e.eventFD(out.ID(), 0, nil)
300 if changed != 1 {
301 t.Fatal(changed)
302 }
303 outDesc, err := out.GetImageDescription()
304 noerr(t, err)
305 rxWant(t, e.p, out.ID(), 1, outDesc.ID())
306
307 creator, err := cm.CreateParametricCreator()
308 noerr(t, err)
309 rxWant(t, e.p, cm.ID(), 5, creator.ID())
310 noerr(t, creator.SetTfNamed(colormanagement.TRANSFER_FUNCTION_ST2084_PQ))
311 rxWant(t, e.p, creator.ID(), 1, colormanagement.TRANSFER_FUNCTION_ST2084_PQ)
312 noerr(t, creator.SetTfPower(22000))
313 rxWant(t, e.p, creator.ID(), 2, 22000)
314 noerr(t, creator.SetPrimariesNamed(colormanagement.PRIMARIES_SRGB))
315 rxWant(t, e.p, creator.ID(), 3, colormanagement.PRIMARIES_SRGB)
316 // Chromaticities are signed: negative values must survive as two's
317 // complement words.
318 noerr(t, creator.SetPrimaries(640000, 330000, 300000, 600000, 150000, 60000, -312700, 329000))
319 negWhiteX := int32(-312700)
320 rxWant(t, e.p, creator.ID(), 4, 640000, 330000, 300000, 600000, 150000, 60000, uint32(negWhiteX), 329000)
321 noerr(t, creator.SetLuminances(2, 10000, 203))
322 rxWant(t, e.p, creator.ID(), 5, 2, 10000, 203)
323 noerr(t, creator.SetMasteringDisplayPrimaries(1, -2, 3, -4, 5, -6, 7, -8))
324 rxWant(t, e.p, creator.ID(), 6, 1, 0xfffffffe, 3, 0xfffffffc, 5, 0xfffffffa, 7, 0xfffffff8)
325 noerr(t, creator.SetMasteringLuminance(50, 1000))
326 rxWant(t, e.p, creator.ID(), 7, 50, 1000)
327 noerr(t, creator.SetMaxCll(1000))
328 rxWant(t, e.p, creator.ID(), 8, 1000)
329 noerr(t, creator.SetMaxFall(400))
330 rxWant(t, e.p, creator.ID(), 9, 400)
331
332 desc, err := creator.Create()
333 noerr(t, err)
334 rxWant(t, e.p, creator.ID(), 0, desc.ID())
335 // create is a destructor: the creator cannot be used again.
336 if err := creator.SetMaxCll(1); err == nil {
337 t.Fatal("request on a consumed creator accepted")
338 }
339 requireNoWrite(t, e.p)
340
341 var ready [][2]uint32
342 desc.OnReady2(func(hi, lo uint32) { ready = append(ready, [2]uint32{hi, lo}) })
343 e.eventFD(desc.ID(), 2, wordBytes(0xdeadbeef, 7))
344 if len(ready) != 1 || ready[0] != [2]uint32{0xdeadbeef, 7} {
345 t.Fatalf("ready2 %v", ready)
346 }
347
348 cs, err := cm.GetSurface(e.surface)
349 noerr(t, err)
350 rxWant(t, e.p, cm.ID(), 2, cs.ID(), e.surface.ID())
351 noerr(t, cs.SetImageDescription(desc, colormanagement.RENDER_INTENT_PERCEPTUAL))
352 rxWant(t, e.p, cs.ID(), 1, desc.ID(), colormanagement.RENDER_INTENT_PERCEPTUAL)
353 noerr(t, cs.UnsetImageDescription())
354 rxWant(t, e.p, cs.ID(), 2)
355 // A nil description travels as object 0.
356 noerr(t, cs.SetImageDescription(nil, 0))
357 rxWant(t, e.p, cs.ID(), 1, 0, 0)
358
359 // A refused creation arrives as failed(cause, msg).
360 scrgb, err := cm.CreateWindowsScrgb()
361 noerr(t, err)
362 rxWant(t, e.p, cm.ID(), 6, scrgb.ID())
363 var cause uint32
364 var why string
365 scrgb.OnFailed(func(c uint32, m string) { cause, why = c, m })
366 e.eventFD(scrgb.ID(), 0, cat(wordBytes(colormanagement.CAUSE_UNSUPPORTED), str("not supported")))
367 if cause != colormanagement.CAUSE_UNSUPPORTED || why != "not supported" {
368 t.Fatalf("failed %d %q", cause, why)
369 }
370
371 fbk, err := cm.GetSurfaceFeedback(e.surface)
372 noerr(t, err)
373 rxWant(t, e.p, cm.ID(), 3, fbk.ID(), e.surface.ID())
374 var pref [2]uint32
375 fbk.OnPreferredChanged2(func(hi, lo uint32) { pref = [2]uint32{hi, lo} })
376 e.eventFD(fbk.ID(), 1, wordBytes(1, 2))
377 if pref != [2]uint32{1, 2} {
378 t.Fatalf("preferred_changed2 %v", pref)
379 }
380 pd, err := fbk.GetPreferredParametric()
381 noerr(t, err)
382 rxWant(t, e.p, fbk.ID(), 2, pd.ID())
383
384 noerr(t, desc.Destroy())
385 rxWant(t, e.p, desc.ID(), 0)
386 noerr(t, cs.Destroy())
387 rxWant(t, e.p, cs.ID(), 0)
388 noerr(t, cm.Destroy())
389 rxWant(t, e.p, cm.ID(), 0)
390}
391
392func TestColorManagementICCDescriptorOwnership(t *testing.T) {
393 e := rxNew(t)
394 cm := colormanagement.NewWpColorManager(e.d.Context())
395 bind(t, e.d, e.p, 1, colormanagement.WpColorManagerInterface, 3, 3, cm)
396
397 icc, err := cm.CreateIccCreator()
398 noerr(t, err)
399 rxWant(t, e.p, cm.ID(), 4, icc.ID())
400
401 // Request direction: the descriptor is attached as SCM_RIGHTS, the body
402 // carries only offset and length, and the client closes its copy.
403 profile := rxTempFile(t, "ICC-PROFILE-BYTES")
404 fd, err := unix.Dup(int(profile.Fd()))
405 noerr(t, err)
406 noerr(t, icc.SetIccFile(fd, 4, 13))
407 requireFDClosed(t, fd)
408 id, op, body, fds := recvRequestFDs(t, e.p)
409 if id != icc.ID() || op != 1 || len(body) != 8 ||
410 binary.NativeEndian.Uint32(body) != 4 || binary.NativeEndian.Uint32(body[4:]) != 13 || len(fds) != 1 {
411 t.Fatalf("set_icc_file id=%d op=%d body=%x fds=%v", id, op, body, fds)
412 }
413 rxReadFD(t, fds[0], "ICC-PROFILE-BYTES")
414
415 // A failed send must leave the descriptor with the caller.
416 fd2, err := unix.Dup(int(profile.Fd()))
417 noerr(t, err)
418 created, err := icc.Create()
419 noerr(t, err)
420 rxWant(t, e.p, icc.ID(), 0, created.ID())
421 if err := icc.SetIccFile(fd2, 0, 1); err == nil {
422 t.Fatal("set_icc_file on a consumed creator accepted")
423 }
424 if _, err := unix.FcntlInt(uintptr(fd2), unix.F_GETFD, 0); err != nil {
425 t.Fatalf("descriptor of a rejected request was closed: %v", err)
426 }
427 noerr(t, unix.Close(fd2))
428 requireNoWrite(t, e.p)
429
430 // Event direction: image_description_info.icc_file carries an owned fd.
431 info, err := created.GetInformation()
432 noerr(t, err)
433 rxWant(t, e.p, created.ID(), 1, info.ID())
434
435 // The first handler is offered the descriptor and must Take it during
436 // dispatch; anything left is closed when dispatch returns. Later handlers
437 // only ever see nil.
438 var first, second *wlturbo.OwnedFD
439 var taken int
440 var size uint32
441 src2 := rxTempFile(t, "ICC-FROM-COMPOSITOR")
442 before := rxOpenFDs(t)
443 info.OnIccFile(func(f *wlturbo.OwnedFD, n uint32) {
444 first, size = f, n
445 var err error
446 taken, err = f.Take()
447 noerr(t, err)
448 })
449 info.OnIccFile(func(f *wlturbo.OwnedFD, n uint32) { second = f })
450 var prim [8]int32
451 info.OnPrimaries(func(a, b, c, d, e2, f, g, h int32) { prim = [8]int32{a, b, c, d, e2, f, g, h} })
452 doneCalls := 0
453 info.OnDone(func() { doneCalls++ })
454
455 e.eventFD(info.ID(), 1, wordBytes(19), int(src2.Fd()))
456 if first == nil || second != nil || size != 19 {
457 t.Fatalf("icc_file first=%v second=%v size=%d", first, second, size)
458 }
459 // The taken descriptor survives dispatch and is now the caller's.
460 if now := rxOpenFDs(t); now != before+1 {
461 t.Fatalf("open fds %d -> %d, want +1", before, now)
462 }
463 rxReadFD(t, taken, "ICC-FROM-COMPOSITOR")
464 if now := rxOpenFDs(t); now != before {
465 t.Fatalf("open fds %d after close, want %d", now, before)
466 }
467 if _, err := first.Take(); err == nil {
468 t.Fatal("descriptor taken twice")
469 }
470
471 e.eventFD(info.ID(), 2, wordBytes(1, 0xfffffffe, 3, 4, 5, 6, 7, 8))
472 if prim != [8]int32{1, -2, 3, 4, 5, 6, 7, 8} {
473 t.Fatalf("primaries %v", prim)
474 }
475 e.eventFD(info.ID(), 0, nil)
476 if doneCalls != 1 {
477 t.Fatal(doneCalls)
478 }
479 send(t, e.p, msg(info.ID(), 0))
480 if err := e.d.Dispatch(); !errors.Is(err, wlturbo.ErrUnknownObject) {
481 t.Fatalf("event after done = %v", err)
482 }
483
484 // A descriptor no handler takes is closed when dispatch returns.
485 info2, err := created.GetInformation()
486 noerr(t, err)
487 rxWant(t, e.p, created.ID(), 1, info2.ID())
488 info2.OnIccFile(func(f *wlturbo.OwnedFD, n uint32) {})
489 before = rxOpenFDs(t)
490 e.eventFD(info2.ID(), 1, wordBytes(19), int(src2.Fd()))
491 if now := rxOpenFDs(t); now != before {
492 t.Fatalf("unclaimed descriptor leaked: %d -> %d", before, now)
493 }
494 // An icc_file event with no descriptor is a protocol error.
495 send(t, e.p, payload(info2.ID(), 1, wordBytes(19)...))
496 if err := e.d.Dispatch(); !errors.Is(err, wlturbo.ErrMalformedFrame) {
497 t.Fatalf("icc_file without fd = %v", err)
498 }
499}
500
501func TestDRMLeaseDescriptorsAndLifecycle(t *testing.T) {
502 e := rxNew(t)
503 dev := drmlease.NewWpDrmLeaseDevice(e.d.Context())
504 bind(t, e.d, e.p, 1, drmlease.WpDrmLeaseDeviceInterface, 1, 1, dev)
505
506 var seq []string
507 var drm, drm2 *wlturbo.OwnedFD
508 var drmFD int
509 dev.OnDrmFd(func(f *wlturbo.OwnedFD) {
510 drm = f
511 var err error
512 drmFD, err = f.Take()
513 noerr(t, err)
514 seq = append(seq, "drm_fd")
515 })
516 dev.OnDrmFd(func(f *wlturbo.OwnedFD) { drm2 = f })
517 var conn *drmlease.WpDrmLeaseConnector
518 dev.OnConnector(func(c *drmlease.WpDrmLeaseConnector) { conn = c; seq = append(seq, "connector") })
519 dev.OnDone(func() { seq = append(seq, "done") })
520
521 node := rxTempFile(t, "DRM-NODE")
522 before := rxOpenFDs(t)
523 e.eventFD(dev.ID(), 0, nil, int(node.Fd()))
524 if drm == nil || drm2 != nil {
525 t.Fatalf("drm_fd owners: first=%v second=%v", drm, drm2)
526 }
527 if now := rxOpenFDs(t); now != before+1 {
528 t.Fatalf("taken drm_fd not live: %d -> %d", before, now)
529 }
530 rxReadFD(t, drmFD, "DRM-NODE")
531 if now := rxOpenFDs(t); now != before {
532 t.Fatalf("drm_fd leaked: %d -> %d", before, now)
533 }
534
535 // new_id event: the child is created at the server-chosen ID and
536 // inherits the device version.
537 e.eventFD(dev.ID(), 1, wordBytes(serverID))
538 if conn == nil || conn.ID() != serverID || conn.Version() != 1 {
539 t.Fatalf("connector %+v", conn)
540 }
541 var name, desc string
542 var connID uint32
543 withdrawn := 0
544 conn.OnName(func(s string) { name = s })
545 conn.OnDescription(func(s string) { desc = s })
546 conn.OnConnectorId(func(v uint32) { connID = v })
547 conn.OnDone(func() { seq = append(seq, "conn_done") })
548 conn.OnWithdrawn(func() { withdrawn++ })
549 e.eventFD(conn.ID(), 0, str("HDMI-A-1"))
550 e.eventFD(conn.ID(), 1, str("Acme 4K"))
551 e.eventFD(conn.ID(), 2, wordBytes(77))
552 e.eventFD(conn.ID(), 3, nil)
553 e.eventFD(dev.ID(), 2, nil)
554 if name != "HDMI-A-1" || desc != "Acme 4K" || connID != 77 || len(seq) != 4 || seq[3] != "done" {
555 t.Fatalf("connector %q %q %d seq %v", name, desc, connID, seq)
556 }
557
558 req, err := dev.CreateLeaseRequest()
559 noerr(t, err)
560 rxWant(t, e.p, dev.ID(), 0, req.ID())
561 noerr(t, req.RequestConnector(conn))
562 rxWant(t, e.p, req.ID(), 0, conn.ID())
563 lease, err := req.Submit()
564 noerr(t, err)
565 rxWant(t, e.p, req.ID(), 1, lease.ID())
566 // submit is a destructor: the request is spent.
567 if err := req.RequestConnector(conn); err == nil {
568 t.Fatal("request after submit accepted")
569 }
570 if _, err := req.Submit(); err == nil {
571 t.Fatal("second submit accepted")
572 }
573 requireNoWrite(t, e.p)
574
575 var leased *wlturbo.OwnedFD
576 var leasedFD int
577 finished := 0
578 lease.OnLeaseFd(func(f *wlturbo.OwnedFD) {
579 leased = f
580 var err error
581 leasedFD, err = f.Take()
582 noerr(t, err)
583 })
584 lease.OnFinished(func() { finished++ })
585 lfile := rxTempFile(t, "LEASED")
586 e.eventFD(lease.ID(), 0, nil, int(lfile.Fd()))
587 if leased == nil {
588 t.Fatal("lease_fd not delivered")
589 }
590 rxReadFD(t, leasedFD, "LEASED")
591 e.eventFD(lease.ID(), 1, nil)
592 if finished != 1 {
593 t.Fatal(finished)
594 }
595 noerr(t, lease.Destroy())
596 rxWant(t, e.p, lease.ID(), 0)
597
598 // A denied lease sends finished with no descriptor at all.
599 req2, err := dev.CreateLeaseRequest()
600 noerr(t, err)
601 rxWant(t, e.p, dev.ID(), 0, req2.ID())
602 noerr(t, req2.RequestConnector(conn))
603 rxWant(t, e.p, req2.ID(), 0, conn.ID())
604 denied, err := req2.Submit()
605 noerr(t, err)
606 rxWant(t, e.p, req2.ID(), 1, denied.ID())
607 deniedFD := 0
608 deniedDone := 0
609 denied.OnLeaseFd(func(*wlturbo.OwnedFD) { deniedFD++ })
610 denied.OnFinished(func() { deniedDone++ })
611 e.eventFD(denied.ID(), 1, nil)
612 if deniedFD != 0 || deniedDone != 1 {
613 t.Fatalf("denied lease fd=%d finished=%d", deniedFD, deniedDone)
614 }
615
616 // An unclaimed lease descriptor is closed by the dispatcher.
617 req3, err := dev.CreateLeaseRequest()
618 noerr(t, err)
619 rxWant(t, e.p, dev.ID(), 0, req3.ID())
620 noerr(t, req3.RequestConnector(conn))
621 rxWant(t, e.p, req3.ID(), 0, conn.ID())
622 l3, err := req3.Submit()
623 noerr(t, err)
624 rxWant(t, e.p, req3.ID(), 1, l3.ID())
625 l3.OnLeaseFd(func(*wlturbo.OwnedFD) {})
626 before = rxOpenFDs(t)
627 e.eventFD(l3.ID(), 0, nil, int(lfile.Fd()))
628 if now := rxOpenFDs(t); now != before {
629 t.Fatalf("unclaimed lease fd leaked: %d -> %d", before, now)
630 }
631 // lease_fd without SCM_RIGHTS is malformed.
632 send(t, e.p, msg(l3.ID(), 0))
633 if err := e.d.Dispatch(); !errors.Is(err, wlturbo.ErrMalformedFrame) {
634 t.Fatalf("lease_fd without fd = %v", err)
635 }
636
637 e.eventFD(conn.ID(), 4, nil)
638 if withdrawn != 1 {
639 t.Fatal(withdrawn)
640 }
641 noerr(t, conn.Destroy())
642 rxWant(t, e.p, conn.ID(), 0)
643
644 // release is not a destructor: the device stays alive, and keeps
645 // receiving events, until the compositor answers with released.
646 noerr(t, dev.Release())
647 rxWant(t, e.p, dev.ID(), 1)
648 e.eventFD(dev.ID(), 2, nil)
649 released := 0
650 dev.OnReleased(func() { released++ })
651 e.eventFD(dev.ID(), 3, nil)
652 if released != 1 {
653 t.Fatal(released)
654 }
655 if _, err := dev.CreateLeaseRequest(); err == nil {
656 t.Fatal("request on a released device accepted")
657 }
658 requireNoWrite(t, e.p)
659 send(t, e.p, msg(dev.ID(), 2))
660 if err := e.d.Dispatch(); !errors.Is(err, wlturbo.ErrUnknownObject) {
661 t.Fatalf("event after released = %v", err)
662 }
663}
664
665func TestDRMLeaseDeviceWithoutDescriptorIsMalformed(t *testing.T) {
666 e := rxNew(t)
667 dev := drmlease.NewWpDrmLeaseDevice(e.d.Context())
668 bind(t, e.d, e.p, 1, drmlease.WpDrmLeaseDeviceInterface, 1, 1, dev)
669 called := false
670 dev.OnDrmFd(func(*wlturbo.OwnedFD) { called = true })
671 send(t, e.p, msg(dev.ID(), 0))
672 if err := e.d.Dispatch(); !errors.Is(err, wlturbo.ErrMalformedFrame) {
673 t.Fatalf("drm_fd without fd = %v", err)
674 }
675 if called {
676 t.Fatal("handler ran without a descriptor")
677 }
678}
679
680func TestSurfaceHintProtocolsWire(t *testing.T) {
681 e := rxNew(t)
682
683 // alpha modifier
684 am := alphamodifier.NewWpAlphaModifier(e.d.Context())
685 bind(t, e.d, e.p, 1, alphamodifier.WpAlphaModifierInterface, 1, 1, am)
686 as, err := am.GetSurface(e.surface)
687 noerr(t, err)
688 rxWant(t, e.p, am.ID(), 1, as.ID(), e.surface.ID())
689 noerr(t, as.SetMultiplier(0xffffffff))
690 rxWant(t, e.p, as.ID(), 1, 0xffffffff)
691 noerr(t, as.SetMultiplier(0x80000000))
692 rxWant(t, e.p, as.ID(), 1, 0x80000000)
693 // Destroying the manager leaves its children usable.
694 noerr(t, am.Destroy())
695 rxWant(t, e.p, am.ID(), 0)
696 noerr(t, as.SetMultiplier(0))
697 rxWant(t, e.p, as.ID(), 1, 0)
698 noerr(t, as.Destroy())
699 rxWant(t, e.p, as.ID(), 0)
700 if err := as.SetMultiplier(1); err == nil {
701 t.Fatal("request after destroy accepted")
702 }
703 requireNoWrite(t, e.p)
704
705 // commit timing
706 ct := committiming.NewWpCommitTimingManager(e.d.Context())
707 bind(t, e.d, e.p, 2, committiming.WpCommitTimingManagerInterface, 1, 1, ct)
708 timer, err := ct.GetTimer(e.surface)
709 noerr(t, err)
710 rxWant(t, e.p, ct.ID(), 1, timer.ID(), e.surface.ID())
711 noerr(t, timer.SetTimestamp(1, 0xfffffffe, 999999999))
712 rxWant(t, e.p, timer.ID(), 0, 1, 0xfffffffe, 999999999)
713 noerr(t, timer.Destroy())
714 rxWant(t, e.p, timer.ID(), 1)
715 noerr(t, ct.Destroy())
716 rxWant(t, e.p, ct.ID(), 0)
717
718 // fifo: argument-less requests are bare 8-byte headers
719 ff := fifo.NewWpFifoManager(e.d.Context())
720 bind(t, e.d, e.p, 3, fifo.WpFifoManagerInterface, 1, 1, ff)
721 f, err := ff.GetFifo(e.surface)
722 noerr(t, err)
723 rxWant(t, e.p, ff.ID(), 1, f.ID(), e.surface.ID())
724 noerr(t, f.SetBarrier())
725 rxWant(t, e.p, f.ID(), 0)
726 noerr(t, f.WaitBarrier())
727 rxWant(t, e.p, f.ID(), 1)
728 noerr(t, f.Destroy())
729 rxWant(t, e.p, f.ID(), 2)
730 noerr(t, ff.Destroy())
731 rxWant(t, e.p, ff.ID(), 0)
732
733 // tearing control
734 tc := tearingcontrol.NewWpTearingControlManager(e.d.Context())
735 bind(t, e.d, e.p, 4, tearingcontrol.WpTearingControlManagerInterface, 1, 1, tc)
736 tear, err := tc.GetTearingControl(e.surface)
737 noerr(t, err)
738 rxWant(t, e.p, tc.ID(), 1, tear.ID(), e.surface.ID())
739 noerr(t, tear.SetPresentationHint(tearingcontrol.PRESENTATION_HINT_ASYNC))
740 rxWant(t, e.p, tear.ID(), 0, 1)
741 noerr(t, tear.SetPresentationHint(tearingcontrol.PRESENTATION_HINT_VSYNC))
742 rxWant(t, e.p, tear.ID(), 0, 0)
743 noerr(t, tear.Destroy())
744 rxWant(t, e.p, tear.ID(), 1)
745 noerr(t, tc.Destroy())
746 rxWant(t, e.p, tc.ID(), 0)
747}
748
749func TestContentTypeSurfaceHintWire(t *testing.T) {
750 e := rxNew(t)
751 mgr := contenttype.NewWpContentTypeManager(e.d.Context())
752 bind(t, e.d, e.p, 1, contenttype.WpContentTypeManagerInterface, 1, 1, mgr)
753 ct, err := mgr.GetSurfaceContentType(e.surface)
754 noerr(t, err)
755 rxWant(t, e.p, mgr.ID(), 1, ct.ID(), e.surface.ID())
756 for _, typ := range []uint32{contenttype.TYPE_GAME, contenttype.TYPE_VIDEO, contenttype.TYPE_PHOTO, contenttype.TYPE_NONE} {
757 noerr(t, ct.SetContentType(typ))
758 rxWant(t, e.p, ct.ID(), 1, typ)
759 }
760 // The manager may go first; its child keeps working.
761 noerr(t, mgr.Destroy())
762 rxWant(t, e.p, mgr.ID(), 0)
763 noerr(t, ct.SetContentType(contenttype.TYPE_GAME))
764 rxWant(t, e.p, ct.ID(), 1, 3)
765 noerr(t, ct.Destroy())
766 rxWant(t, e.p, ct.ID(), 0)
767 if err := ct.SetContentType(1); err == nil {
768 t.Fatal("request after destroy accepted")
769 }
770 requireNoWrite(t, e.p)
771 // The interface has no events: any event is an unknown opcode.
772 ct2 := contenttype.NewWpContentType(e.d.Context())
773 ct2.SetID(e.d.AllocateID())
774 e.d.Context().Register(ct2)
775 send(t, e.p, msg(ct2.ID(), 0))
776 if err := e.d.Dispatch(); !errors.Is(err, wlturbo.ErrUnknownOpcode) {
777 t.Fatalf("event on eventless interface = %v", err)
778 }
779}
780
781func TestXdgActivationTokenWireAndDoneEvent(t *testing.T) {
782 e := rxNew(t)
783 act := xdgactivation.NewXdgActivation(e.d.Context())
784 bind(t, e.d, e.p, 1, xdgactivation.XdgActivationInterface, 1, 1, act)
785 tok, err := act.GetActivationToken()
786 noerr(t, err)
787 rxWant(t, e.p, act.ID(), 1, tok.ID())
788 noerr(t, tok.SetSerial(0x01020304, e.seat))
789 rxWant(t, e.p, tok.ID(), 0, 0x01020304, e.seat.ID())
790 noerr(t, tok.SetAppId("org.example.App"))
791 rxWantRaw(t, e.p, tok.ID(), 1, str("org.example.App"))
792 noerr(t, tok.SetSurface(e.surface))
793 rxWant(t, e.p, tok.ID(), 2, e.surface.ID())
794 noerr(t, tok.Commit())
795 rxWant(t, e.p, tok.ID(), 3)
796
797 var got string
798 tok.OnDone(func(s string) { got = s })
799 e.eventFD(tok.ID(), 0, str("activation-token-é"))
800 if got != "activation-token-é" {
801 t.Fatalf("done token %q", got)
802 }
803 // A string missing its NUL terminator is rejected before any handler.
804 got = ""
805 send(t, e.p, payload(tok.ID(), 0, arr([]byte("abc"))...))
806 if err := e.d.Dispatch(); !errors.Is(err, wlturbo.ErrMalformedFrame) || got != "" {
807 t.Fatalf("unterminated string = %v handler=%q", err, got)
808 }
809
810 noerr(t, act.Activate("activation-token-é", e.surface))
811 rxWantRaw(t, e.p, act.ID(), 2, cat(str("activation-token-é"), wordBytes(e.surface.ID())))
812 noerr(t, tok.Destroy())
813 rxWant(t, e.p, tok.ID(), 4)
814 noerr(t, act.Destroy())
815 rxWant(t, e.p, act.ID(), 0)
816}
817
818func TestPointerWarpFixedWire(t *testing.T) {
819 e := rxNew(t)
820 pw := pointerwarp.NewWpPointerWarp(e.d.Context())
821 bind(t, e.d, e.p, 1, pointerwarp.WpPointerWarpInterface, 1, 1, pw)
822 noerr(t, pw.WarpPointer(e.surface, e.pointer, wlturbo.NewFixed(10.5), wlturbo.NewFixed(-3.25), 0xabcdef01))
823 rxWant(t, e.p, pw.ID(), 1, e.surface.ID(), e.pointer.ID(), 2688, uint32(0xfffffcc0), 0xabcdef01)
824 noerr(t, pw.Destroy())
825 rxWant(t, e.p, pw.ID(), 0)
826 if err := pw.WarpPointer(e.surface, e.pointer, 0, 0, 1); err == nil {
827 t.Fatal("warp after destroy accepted")
828 }
829 requireNoWrite(t, e.p)
830}
831
832func TestColorRepresentationWireAndCapabilityEvents(t *testing.T) {
833 e := rxNew(t)
834 mgr := colorrepresentation.NewWpColorRepresentationManager(e.d.Context())
835 bind(t, e.d, e.p, 1, colorrepresentation.WpColorRepresentationManagerInterface, 1, 1, mgr)
836
837 var alphas []uint32
838 var combos [][2]uint32
839 done := 0
840 mgr.OnSupportedAlphaMode(func(v uint32) { alphas = append(alphas, v) })
841 mgr.OnSupportedCoefficientsAndRanges(func(c, r uint32) { combos = append(combos, [2]uint32{c, r}) })
842 mgr.OnDone(func() { done++ })
843 e.eventFD(mgr.ID(), 0, wordBytes(colorrepresentation.ALPHA_MODE_STRAIGHT))
844 e.eventFD(mgr.ID(), 1, wordBytes(colorrepresentation.COEFFICIENTS_BT709, colorrepresentation.RANGE_LIMITED))
845 e.eventFD(mgr.ID(), 1, wordBytes(colorrepresentation.COEFFICIENTS_ICTCP, colorrepresentation.RANGE_FULL))
846 e.eventFD(mgr.ID(), 2, nil)
847 if len(alphas) != 1 || alphas[0] != 2 || len(combos) != 2 || combos[0] != [2]uint32{2, 2} || combos[1] != [2]uint32{8, 1} || done != 1 {
848 t.Fatalf("capabilities %v %v %d", alphas, combos, done)
849 }
850
851 s, err := mgr.GetSurface(e.surface)
852 noerr(t, err)
853 rxWant(t, e.p, mgr.ID(), 1, s.ID(), e.surface.ID())
854 noerr(t, s.SetAlphaMode(colorrepresentation.ALPHA_MODE_PREMULTIPLIED_OPTICAL))
855 rxWant(t, e.p, s.ID(), 1, 1)
856 noerr(t, s.SetCoefficientsAndRange(colorrepresentation.COEFFICIENTS_BT2020, colorrepresentation.RANGE_LIMITED))
857 rxWant(t, e.p, s.ID(), 2, 6, 2)
858 noerr(t, s.SetChromaLocation(colorrepresentation.CHROMA_LOCATION_TYPE_5))
859 rxWant(t, e.p, s.ID(), 3, 6)
860 noerr(t, s.Destroy())
861 rxWant(t, e.p, s.ID(), 0)
862 noerr(t, mgr.Destroy())
863 rxWant(t, e.p, mgr.ID(), 0)
864}