v0.5.0 089fa12

render_extensions_test.go RawHistoryFolder

1//go:build linux
2
3package protocol_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/wlturbo"
19	"github.com/bnema/wlturbo/protocol/alphamodifier"
20	"github.com/bnema/wlturbo/protocol/colormanagement"
21	"github.com/bnema/wlturbo/protocol/colorrepresentation"
22	"github.com/bnema/wlturbo/protocol/committiming"
23	"github.com/bnema/wlturbo/protocol/contenttype"
24	"github.com/bnema/wlturbo/protocol/core"
25	"github.com/bnema/wlturbo/protocol/drmlease"
26	"github.com/bnema/wlturbo/protocol/fifo"
27	"github.com/bnema/wlturbo/protocol/pointerwarp"
28	"github.com/bnema/wlturbo/protocol/presentation"
29	"github.com/bnema/wlturbo/protocol/tearingcontrol"
30	"github.com/bnema/wlturbo/protocol/xdgactivation"
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 *core.Surface
39	output  *core.Output
40	seat    *core.Seat
41	pointer *core.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}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 1

Tags 8

↑↓ moveenter openesc close