v0.4.0 b6be3a8

dmabuf_test.go RawHistoryFolder

1package wayland
2
3import (
4	"context"
5	"os/exec"
6	"testing"
7	"time"
8
9	"github.com/bnema/go-wayland-bindings/server/linuxdmabuf"
10	"github.com/bnema/neferwl/internal/adapters/logging"
11	"github.com/bnema/neferwl/internal/ports"
12	"github.com/bnema/wlturbo"
13	"golang.org/x/sys/unix"
14)
15
16var linearARGB = ports.DMABufFormat{Format: 'A' | 'R'<<8 | '2'<<16 | '4'<<24}
17
18func dmabufServer(t *testing.T, sup ports.DMABufSupport) (*Server, chan ports.ClientEvent, chan ports.SurfaceContent, string) {
19	t.Helper()
20	dir := t.TempDir()
21	events := make(chan ports.ClientEvent, 16)
22	contents := make(chan ports.SurfaceContent, 16)
23	s, err := New(Options{RuntimeDir: dir, Outputs: testOutputs, DMABuf: sup}, Channels{Events: events, Contents: contents}, logging.For(context.Background(), "wayland"))
24	if err != nil {
25		t.Fatal(err)
26	}
27	ctx, cancel := context.WithCancel(context.Background())
28	done := make(chan error, 1)
29	go func() { done <- s.Run(ctx) }()
30	t.Cleanup(func() {
31		cancel()
32		select {
33		case err := <-done:
34			if err != nil {
35				t.Errorf("Run: %v", err)
36			}
37		case <-time.After(2 * time.Second):
38			t.Error("Run hung")
39		}
40	})
41	return s, events, contents, dir
42}
43
44// Without renderer formats there is no linux-dmabuf global.
45func TestNoDMABufWithoutFormats(t *testing.T) {
46	s, _, _, dir := dmabufServer(t, ports.DMABufSupport{})
47	c := protocolClient(t, s, dir)
48	if _, ok := c.Registry().FindGlobal("zwp_linux_dmabuf_v1"); ok {
49		t.Fatal("zwp_linux_dmabuf_v1 advertised")
50	}
51}
52
53type paramsProxy struct {
54	wlturbo.BaseProxy
55	result chan uint16
56}
57
58func (p *paramsProxy) Dispatch(e *wlturbo.Event) { p.result <- e.Opcode }
59
60// An unadvertised format is a protocol error from v4 and `failed` before; a
61// plane past the end of its file is a protocol error.
62func TestDMABufParams(t *testing.T) {
63	s, _, _, dir := dmabufServer(t, ports.DMABufSupport{Formats: []ports.DMABufFormat{linearARGB}})
64	c := protocolClient(t, s, dir)
65	g, ok := c.Registry().FindGlobal("zwp_linux_dmabuf_v1")
66	if !ok || g.Version != dmabufVersion {
67		t.Fatalf("global %+v", g)
68	}
69	dm, err := bindWireID(c, g.Name, g.Interface, dmabufVersion)
70	if err != nil {
71		t.Fatal(err)
72	}
73	fd, err := unix.MemfdCreate("fake-dmabuf", 0)
74	if err != nil {
75		t.Fatal(err)
76	}
77	defer unix.Close(fd)
78	if err := unix.Ftruncate(fd, 256*16); err != nil {
79		t.Fatal(err)
80	}
81	create := func(format uint32, height int32) (uint32, chan uint16) {
82		p := c.AllocateID()
83		requestProtocol(t, c, dm, linuxdmabuf.ZwpLinuxDmabufV1RequestCreateParams, p)
84		proxy := &paramsProxy{result: make(chan uint16, 2)}
85		proxy.SetID(p)
86		registerWireProxy(c, proxy)
87		if err := wireRequest(c, p, uint16(linuxdmabuf.ZwpLinuxBufferParamsV1RequestAdd), []int{fd}, uint32(0), uint32(0), uint32(256), uint32(0), uint32(0)); err != nil {
88			t.Fatal(err)
89		}
90		requestProtocol(t, c, p, linuxdmabuf.ZwpLinuxBufferParamsV1RequestCreate, int32(64), height, format, uint32(0))
91		if err := c.Roundtrip(); err != nil {
92			t.Fatal(err)
93		}
94		return p, proxy.result
95	}
96	// Supported: created (import errors surface in the renderer).
97	_, result := create(linearARGB.Format, 16)
98	if op := <-result; op != uint16(linuxdmabuf.ZwpLinuxBufferParamsV1EventCreated) {
99		t.Fatalf("event %d", op)
100	}
101	// Taller than the file: out of bounds.
102	big := c.AllocateID()
103	requestProtocol(t, c, dm, linuxdmabuf.ZwpLinuxDmabufV1RequestCreateParams, big)
104	if err := wireRequest(c, big, uint16(linuxdmabuf.ZwpLinuxBufferParamsV1RequestAdd), []int{fd}, uint32(0), uint32(0), uint32(256), uint32(0), uint32(0)); err != nil {
105		t.Fatal(err)
106	}
107	requestProtocol(t, c, big, linuxdmabuf.ZwpLinuxBufferParamsV1RequestCreate, int32(64), int32(17), linearARGB.Format, uint32(0))
108	expectProtocolError(t, c, big, uint32(linuxdmabuf.ZwpLinuxBufferParamsV1ErrorOutOfBounds))
109	// Each case below needs its own connection: a protocol error kills it.
110	t.Run("unadvertised format v6", func(t *testing.T) {
111		c, p, _ := createNV12(t, dmabufVersion, false, 0)
112		expectProtocolError(t, c, p, uint32(linuxdmabuf.ZwpLinuxBufferParamsV1ErrorInvalidFormat))
113	})
114	t.Run("unadvertised format v6 immed", func(t *testing.T) {
115		c, p, _ := createNV12(t, dmabufVersion, true, 0)
116		expectProtocolError(t, c, p, uint32(linuxdmabuf.ZwpLinuxBufferParamsV1ErrorInvalidFormat))
117	})
118	// Flags are checked first: a y-inverted buffer fails, whatever its format.
119	t.Run("flags before format v6", func(t *testing.T) {
120		c, _, proxy := createNV12(t, dmabufVersion, false, 1)
121		if err := c.Roundtrip(); err != nil {
122			t.Fatal(err)
123		}
124		if op := <-proxy.result; op != uint16(linuxdmabuf.ZwpLinuxBufferParamsV1EventFailed) {
125			t.Fatalf("event %d", op)
126		}
127	})
128	// Before v4 there is no feedback to follow: the client gets `failed`,
129	// or invalid_wl_buffer for an immediate create.
130	t.Run("unadvertised format v3 immed", func(t *testing.T) {
131		c, p, _ := createNV12(t, 3, true, 0)
132		expectProtocolError(t, c, p, uint32(linuxdmabuf.ZwpLinuxBufferParamsV1ErrorInvalidWlBuffer))
133	})
134	t.Run("unadvertised format v3", func(t *testing.T) {
135		c, _, proxy := createNV12(t, 3, false, 0)
136		if err := c.Roundtrip(); err != nil {
137			t.Fatal(err)
138		}
139		if op := <-proxy.result; op != uint16(linuxdmabuf.ZwpLinuxBufferParamsV1EventFailed) {
140			t.Fatalf("event %d", op)
141		}
142	})
143}
144
145// createNV12 sends a one-plane create (or create_immed) of NV12, which the
146// server does not list, on a fresh connection bound at the given version.
147func createNV12(t *testing.T, version uint32, immed bool, flags uint32) (*wlturbo.Display, uint32, *paramsProxy) {
148	t.Helper()
149	s, _, _, dir := dmabufServer(t, ports.DMABufSupport{Formats: []ports.DMABufFormat{linearARGB}})
150	c := protocolClient(t, s, dir)
151	dm := bindVersion(t, c, "zwp_linux_dmabuf_v1", version)
152	fd, err := unix.MemfdCreate("fake-dmabuf", 0)
153	if err != nil {
154		t.Fatal(err)
155	}
156	t.Cleanup(func() { unix.Close(fd) })
157	if err := unix.Ftruncate(fd, 256*16); err != nil {
158		t.Fatal(err)
159	}
160	p := c.AllocateID()
161	requestProtocol(t, c, dm, linuxdmabuf.ZwpLinuxDmabufV1RequestCreateParams, p)
162	proxy := &paramsProxy{result: make(chan uint16, 2)}
163	proxy.SetID(p)
164	registerWireProxy(c, proxy)
165	if err := wireRequest(c, p, uint16(linuxdmabuf.ZwpLinuxBufferParamsV1RequestAdd), []int{fd}, uint32(0), uint32(0), uint32(256), uint32(0), uint32(0)); err != nil {
166		t.Fatal(err)
167	}
168	if immed {
169		requestProtocol(t, c, p, linuxdmabuf.ZwpLinuxBufferParamsV1RequestCreateImmed, c.AllocateID(), int32(64), int32(16), uint32(fourccNV12), flags)
170	} else {
171		requestProtocol(t, c, p, linuxdmabuf.ZwpLinuxBufferParamsV1RequestCreate, int32(64), int32(16), uint32(fourccNV12), flags)
172	}
173	return c, p, proxy
174}
175
176// set_sampling_device takes an 8-byte dev_t, before the params are used.
177func TestDMABufSamplingDevice(t *testing.T) {
178	s, _, _, dir := dmabufServer(t, ports.DMABufSupport{Formats: []ports.DMABufFormat{linearARGB}})
179	fd, err := unix.MemfdCreate("fake-dmabuf", 0)
180	if err != nil {
181		t.Fatal(err)
182	}
183	defer unix.Close(fd)
184	if err := unix.Ftruncate(fd, 256*16); err != nil {
185		t.Fatal(err)
186	}
187	newParams := func(c *wlturbo.Display, dm uint32) (uint32, *paramsProxy) {
188		p := c.AllocateID()
189		requestProtocol(t, c, dm, linuxdmabuf.ZwpLinuxDmabufV1RequestCreateParams, p)
190		proxy := &paramsProxy{result: make(chan uint16, 2)}
191		proxy.SetID(p)
192		registerWireProxy(c, proxy)
193		return p, proxy
194	}
195	setDevice := func(c *wlturbo.Display, p uint32, dev []byte) {
196		t.Helper()
197		if err := wireRequest(c, p, uint16(linuxdmabuf.ZwpLinuxBufferParamsV1RequestSetSamplingDevice), nil, dev); err != nil {
198			t.Fatal(err)
199		}
200	}
201	t.Run("accepted", func(t *testing.T) {
202		c := protocolClient(t, s, dir)
203		dm := bindVersion(t, c, "zwp_linux_dmabuf_v1", dmabufVersion)
204		p, proxy := newParams(c, dm)
205		setDevice(c, p, devBytes(42))
206		if err := wireRequest(c, p, uint16(linuxdmabuf.ZwpLinuxBufferParamsV1RequestAdd), []int{fd}, uint32(0), uint32(0), uint32(256), uint32(0), uint32(0)); err != nil {
207			t.Fatal(err)
208		}
209		requestProtocol(t, c, p, linuxdmabuf.ZwpLinuxBufferParamsV1RequestCreate, int32(64), int32(16), linearARGB.Format, uint32(0))
210		if err := c.Roundtrip(); err != nil {
211			t.Fatal(err)
212		}
213		if op := <-proxy.result; op != uint16(linuxdmabuf.ZwpLinuxBufferParamsV1EventCreated) {
214			t.Fatalf("event %d", op)
215		}
216	})
217	t.Run("bad size", func(t *testing.T) {
218		c := protocolClient(t, s, dir)
219		dm := bindVersion(t, c, "zwp_linux_dmabuf_v1", dmabufVersion)
220		p, _ := newParams(c, dm)
221		setDevice(c, p, make([]byte, 4))
222		expectProtocolError(t, c, p, uint32(linuxdmabuf.ZwpLinuxBufferParamsV1ErrorInvalidDevTSize))
223	})
224	t.Run("after create", func(t *testing.T) {
225		c := protocolClient(t, s, dir)
226		dm := bindVersion(t, c, "zwp_linux_dmabuf_v1", dmabufVersion)
227		p, _ := newParams(c, dm)
228		if err := wireRequest(c, p, uint16(linuxdmabuf.ZwpLinuxBufferParamsV1RequestAdd), []int{fd}, uint32(0), uint32(0), uint32(256), uint32(0), uint32(0)); err != nil {
229			t.Fatal(err)
230		}
231		requestProtocol(t, c, p, linuxdmabuf.ZwpLinuxBufferParamsV1RequestCreate, int32(64), int32(16), linearARGB.Format, uint32(0))
232		setDevice(c, p, devBytes(42))
233		expectProtocolError(t, c, p, uint32(linuxdmabuf.ZwpLinuxBufferParamsV1ErrorAlreadyUsed))
234	})
235}
236
237// A reused params object is a protocol error.
238func TestDMABufParamsReuse(t *testing.T) {
239	s, _, _, dir := dmabufServer(t, ports.DMABufSupport{Formats: []ports.DMABufFormat{linearARGB}})
240	c := protocolClient(t, s, dir)
241	g, _ := c.Registry().FindGlobal("zwp_linux_dmabuf_v1")
242	dm, err := bindWireID(c, g.Name, g.Interface, dmabufVersion)
243	if err != nil {
244		t.Fatal(err)
245	}
246	fd, err := unix.MemfdCreate("fake-dmabuf", 0)
247	if err != nil {
248		t.Fatal(err)
249	}
250	defer unix.Close(fd)
251	if err := unix.Ftruncate(fd, 256*16); err != nil {
252		t.Fatal(err)
253	}
254	p := c.AllocateID()
255	requestProtocol(t, c, dm, linuxdmabuf.ZwpLinuxDmabufV1RequestCreateParams, p)
256	proxy := &paramsProxy{result: make(chan uint16, 2)}
257	proxy.SetID(p)
258	registerWireProxy(c, proxy)
259	if err := wireRequest(c, p, uint16(linuxdmabuf.ZwpLinuxBufferParamsV1RequestAdd), []int{fd}, uint32(0), uint32(0), uint32(256), uint32(0), uint32(0)); err != nil {
260		t.Fatal(err)
261	}
262	requestProtocol(t, c, p, linuxdmabuf.ZwpLinuxBufferParamsV1RequestCreate, int32(64), int32(16), linearARGB.Format, uint32(0))
263	if err := c.Roundtrip(); err != nil {
264		t.Fatal(err)
265	}
266	requestProtocol(t, c, p, linuxdmabuf.ZwpLinuxBufferParamsV1RequestCreate, int32(64), int32(16), linearARGB.Format, uint32(0))
267	expectProtocolError(t, c, p, uint32(linuxdmabuf.ZwpLinuxBufferParamsV1ErrorAlreadyUsed))
268}
269
270// weston-simple-dmabuf-egl renders on the GPU and sends dmabufs: its
271// contents reach the renderer as DMABuf, never as pixels.
272func TestWestonDMABuf(t *testing.T) {
273	tool, err := exec.LookPath("weston-simple-dmabuf-egl")
274	if err != nil {
275		t.Skip("weston-simple-dmabuf-egl not installed")
276	}
277	// Linear buffers: every GPU allocates them, and the client picks from
278	// what it is offered.
279	xrgb := ports.DMABufFormat{Format: fourccXRGB}
280	s, events, contents, dir := dmabufServer(t, ports.DMABufSupport{Formats: []ports.DMABufFormat{linearARGB, xrgb}})
281	_, _, output := lifecycleClient(t, tool, s, dir, nil)
282	w := mapped(t, events, 5*time.Second)
283	deadline := time.After(5 * time.Second)
284	for {
285		select {
286		case c := <-contents:
287			if c.ID != w.ID || c.DMABuf == nil || c.SHM != nil || len(c.DMABuf.Planes) == 0 || c.DMABuf.Planes[0].File == nil {
288				t.Fatalf("content %+v", c)
289			}
290			return
291		case <-deadline:
292			t.Fatalf("no dmabuf content: %s", output.String())
293		}
294	}
295}
296
297// feedbackProxy records each feedback round (up to done).
298type feedbackProxy struct {
299	wlturbo.BaseProxy
300	rounds chan round
301	cur    round
302	open   tranche
303}
304
305// round is what one feedback round carried.
306type round struct {
307	tranches    []tranche
308	tables      int
309	mainDevices int
310}
311
312type tranche struct {
313	flags   uint32
314	indices []uint16
315}
316
317func (p *feedbackProxy) Dispatch(e *wlturbo.Event) {
318	switch uint32(e.Opcode) {
319	case linuxdmabuf.ZwpLinuxDmabufFeedbackV1EventFormatTable:
320		p.cur.tables++
321		if fd := e.Fd(); fd != 0 {
322			unix.Close(int(fd))
323		}
324	case linuxdmabuf.ZwpLinuxDmabufFeedbackV1EventMainDevice:
325		p.cur.mainDevices++
326	case linuxdmabuf.ZwpLinuxDmabufFeedbackV1EventTrancheFlags:
327		p.open.flags = e.Uint32()
328	case linuxdmabuf.ZwpLinuxDmabufFeedbackV1EventTrancheFormats:
329		a := e.Array()
330		for i := 0; i+1 < len(a); i += 2 {
331			p.open.indices = append(p.open.indices, uint16(a[i])|uint16(a[i+1])<<8)
332		}
333	case linuxdmabuf.ZwpLinuxDmabufFeedbackV1EventTrancheDone:
334		p.cur.tranches, p.open = append(p.cur.tranches, p.open), tranche{}
335	case linuxdmabuf.ZwpLinuxDmabufFeedbackV1EventDone:
336		p.rounds <- p.cur
337		p.cur = round{}
338	}
339}
340
341// scanoutHarness is a server with commands and output formats channels, a
342// mapped window, and a surface feedback bound at the given version.
343type scanoutHarness struct {
344	t        *testing.T
345	c        *wlturbo.Display
346	s        *Server
347	w        ports.WindowMapped
348	fb       uint32
349	proxy    *feedbackProxy
350	commands chan ports.ClientCommand
351	formats  chan ports.OutputFormats
352}
353
354func newScanoutHarness(t *testing.T, sup ports.DMABufSupport, version uint32) *scanoutHarness {
355	t.Helper()
356	dir := t.TempDir()
357	events := make(chan ports.ClientEvent, 16)
358	h := &scanoutHarness{t: t, commands: make(chan ports.ClientCommand, 16), formats: make(chan ports.OutputFormats, 1)}
359	s, err := New(Options{RuntimeDir: dir, Outputs: testOutputs, DMABuf: sup}, Channels{Events: events, Commands: h.commands, OutputFormats: h.formats}, logging.For(context.Background(), "wayland"))
360	if err != nil {
361		t.Fatal(err)
362	}
363	h.s = s
364	ctx, cancel := context.WithCancel(context.Background())
365	done := make(chan error, 1)
366	go func() { done <- s.Run(ctx) }()
367	t.Cleanup(func() { cancel(); <-done })
368	h.c = protocolClient(t, s, dir)
369	w, surf, xdg := surfaceMapper(t, h.c, events)()
370	h.w = w
371	// Our configures must not block the mapper's one-slot proxy.
372	registerProtocol(t, h.c, xdg)
373	dm := bindVersion(t, h.c, "zwp_linux_dmabuf_v1", version)
374	h.fb = h.c.AllocateID()
375	h.proxy = &feedbackProxy{rounds: make(chan round, 8)}
376	h.proxy.SetID(h.fb)
377	registerWireProxy(h.c, h.proxy)
378	requestProtocol(t, h.c, dm, linuxdmabuf.ZwpLinuxDmabufV1RequestGetSurfaceFeedback, h.fb, surf)
379	return h
380}
381
382// next waits for the next feedback round.
383func (h *scanoutHarness) next(what string) round {
384	h.t.Helper()
385	deadline := time.After(2 * time.Second)
386	for {
387		if err := h.c.Roundtrip(); err != nil {
388			h.t.Fatal(err)
389		}
390		select {
391		case r := <-h.proxy.rounds:
392			return r
393		case <-deadline:
394			h.t.Fatalf("no feedback: %s", what)
395		default:
396			time.Sleep(5 * time.Millisecond)
397		}
398	}
399}
400
401// A fullscreen surface on an output with scanout formats gets a scanout
402// tranche first; leaving fullscreen sends the renderer tranche alone. From
403// v6 the table comes once, main_device never, and the renderer tranche is
404// flagged for sampling.
405func TestDMABufScanoutTranche(t *testing.T) {
406	tiled := ports.DMABufFormat{Format: linearARGB.Format, Modifier: 0x0200000000000001}
407	sup := ports.DMABufSupport{Device: 1, Formats: []ports.DMABufFormat{linearARGB, tiled}}
408	h := newScanoutHarness(t, sup, dmabufVersion)
409	c, s, w, fb, commands, formats := h.c, h.s, h.w, h.fb, h.commands, h.formats
410	scanout := uint32(linuxdmabuf.ZwpLinuxDmabufFeedbackV1TrancheFlagsScanout)
411	sampling := uint32(linuxdmabuf.ZwpLinuxDmabufFeedbackV1TrancheFlagsSampling)
412	if r := h.next("initial"); len(r.tranches) != 1 || r.tranches[0].flags != sampling || len(r.tranches[0].indices) != 2 || r.tables != 1 || r.mainDevices != 0 {
413		t.Fatalf("tiled window: %+v", r)
414	}
415	formats <- ports.OutputFormats{Output: "HEADLESS-1", Device: 7, Formats: []ports.DMABufFormat{tiled}}
416	commands <- ports.ConfigureWindow{ID: w.ID, Width: 1920, Height: 1080, Fullscreen: true, Output: "HEADLESS-1", Visible: true}
417	r := h.next("fullscreen")
418	for len(r.tranches) == 1 { // the formats may land after the configure
419		if r.tables != 0 || r.mainDevices != 0 {
420			t.Fatalf("resent table or main device: %+v", r)
421		}
422		r = h.next("fullscreen with formats")
423	}
424	if len(r.tranches) != 2 || r.tranches[0].flags != scanout || len(r.tranches[0].indices) != 1 || r.tranches[0].indices[0] != 1 || r.tranches[1].flags != sampling || r.tables != 0 || r.mainDevices != 0 {
425		t.Fatalf("fullscreen: %+v", r)
426	}
427	// Hidden, the window loses its scanout tranche; shown, it gets it back.
428	commands <- ports.ConfigureWindow{ID: w.ID, Width: 1920, Height: 1080, Fullscreen: true, Output: "HEADLESS-1"}
429	if r := h.next("hidden"); len(r.tranches) != 1 || r.tables != 0 {
430		t.Fatalf("hidden fullscreen: %+v", r)
431	}
432	commands <- ports.ConfigureWindow{ID: w.ID, Width: 1920, Height: 1080, Fullscreen: true, Output: "HEADLESS-1", Visible: true}
433	if r := h.next("shown again"); len(r.tranches) != 2 || r.tranches[0].flags != scanout || r.tables != 0 {
434		t.Fatalf("shown fullscreen: %+v", r)
435	}
436	// Fullscreen on another output: its formats (none) apply at once.
437	commands <- ports.ConfigureWindow{ID: w.ID, Width: 1920, Height: 1080, Fullscreen: true, Output: "OTHER-1", Visible: true}
438	if r := h.next("other output"); len(r.tranches) != 1 || r.tables != 0 {
439		t.Fatalf("moved output: %+v", r)
440	}
441	commands <- ports.ConfigureWindow{ID: w.ID, Width: 800, Height: 600, Output: "HEADLESS-1", Visible: true}
442	if r := h.next("tiled again"); len(r.tranches) != 1 || r.tranches[0].flags != sampling || r.tables != 0 {
443		t.Fatalf("left fullscreen: %+v", r)
444	}
445	// A destroyed feedback gets nothing more (a protocol error would
446	// kill the connection) and leaves the server's list.
447	requestProtocol(t, c, fb, linuxdmabuf.ZwpLinuxDmabufFeedbackV1RequestDestroy)
448	commands <- ports.ConfigureWindow{ID: w.ID, Width: 1920, Height: 1080, Fullscreen: true, Output: "HEADLESS-1", Visible: true}
449	formats <- ports.OutputFormats{Output: "HEADLESS-1", Device: 7}
450	if err := c.Roundtrip(); err != nil {
451		t.Fatal(err)
452	}
453	var n int
454	s.display.Do(func() { n = len(s.dmabuf.feedbacks) })
455	if n != 0 {
456		t.Fatalf("%d feedbacks kept", n)
457	}
458}
459
460// A v4 client keeps the whole feedback on every round: table, main device,
461// and an unflagged renderer tranche.
462func TestDMABufFeedbackV4(t *testing.T) {
463	tiled := ports.DMABufFormat{Format: linearARGB.Format, Modifier: 0x0200000000000001}
464	sup := ports.DMABufSupport{Device: 1, Formats: []ports.DMABufFormat{linearARGB, tiled}}
465	h := newScanoutHarness(t, sup, 4)
466	check := func(what string, r round, tranches int) {
467		t.Helper()
468		if r.tables != 1 || r.mainDevices != 1 || len(r.tranches) != tranches || r.tranches[len(r.tranches)-1].flags != 0 {
469			t.Fatalf("%s: %+v", what, r)
470		}
471	}
472	check("initial", h.next("initial"), 1)
473	h.formats <- ports.OutputFormats{Output: "HEADLESS-1", Device: 7, Formats: []ports.DMABufFormat{tiled}}
474	h.commands <- ports.ConfigureWindow{ID: h.w.ID, Width: 1920, Height: 1080, Fullscreen: true, Output: "HEADLESS-1", Visible: true}
475	r := h.next("fullscreen")
476	for len(r.tranches) == 1 { // the formats may land after the configure
477		check("fullscreen before formats", r, 1)
478		r = h.next("fullscreen with formats")
479	}
480	check("fullscreen", r, 2)
481	if r.tranches[0].flags != uint32(linuxdmabuf.ZwpLinuxDmabufFeedbackV1TrancheFlagsScanout) {
482		t.Fatalf("scanout tranche: %+v", r)
483	}
484}
485
486// testDMABufGlobal is a global with one scanout offer for f's output.
487func testDMABufGlobal(s *Server, f ports.OutputFormats) *dmabufGlobal {
488	g := &dmabufGlobal{
489		server:  s,
490		support: ports.DMABufSupport{Formats: []ports.DMABufFormat{linearARGB}},
491		index:   map[ports.DMABufFormat]uint16{linearARGB: 0},
492		scanout: map[string]scanoutOffer{},
493	}
494	g.scanout[f.Output] = g.offer(f)
495	return g
496}
497
498func TestFullscreenOutput(t *testing.T) {
499	windowSurface := func(mod func(*window)) *surface {
500		w := &window{hasLast: true, last: ports.ConfigureWindow{Fullscreen: true, Output: "DP-2", Visible: true}}
501		mod(w)
502		return &surface{xdg: &xdgSurface{window: w}}
503	}
504	tests := []struct {
505		name string
506		surf *surface
507		out  string
508		ok   bool
509	}{
510		{"nil", nil, "", false},
511		{"destroyed", &surface{destroyed: true, xdg: &xdgSurface{window: &window{hasLast: true, last: ports.ConfigureWindow{Fullscreen: true, Visible: true}}}}, "", false},
512		{"no window", &surface{xdg: &xdgSurface{}}, "", false},
513		{"not configured", windowSurface(func(w *window) { w.hasLast = false }), "", false},
514		{"not fullscreen", windowSurface(func(w *window) { w.last.Fullscreen = false }), "", false},
515		{"invisible", windowSurface(func(w *window) { w.last.Visible = false }), "", false},
516		{"visible fullscreen", windowSurface(func(*window) {}), "DP-2", true},
517	}
518	g := testDMABufGlobal(&Server{}, ports.OutputFormats{Output: "DP-2", Formats: []ports.DMABufFormat{linearARGB}})
519	for _, tc := range tests {
520		t.Run(tc.name, func(t *testing.T) {
521			if out, ok := g.fullscreenOutput(tc.surf); out != tc.out || ok != tc.ok {
522				t.Fatalf("got (%q, %v), want (%q, %v)", out, ok, tc.out, tc.ok)
523			}
524		})
525	}
526}
527
528func TestInvisibleScanoutOffer(t *testing.T) {
529	w := &window{hasLast: true, last: ports.ConfigureWindow{Fullscreen: true, Output: "DP-2", Visible: true}}
530	surf := &surface{xdg: &xdgSurface{window: w}}
531	s := &Server{}
532	g := testDMABufGlobal(s, ports.OutputFormats{Output: "DP-2", Formats: []ports.DMABufFormat{linearARGB}})
533	if _, ok := g.scanoutFor(surf); !ok {
534		t.Fatal("no visible scanout")
535	}
536	w.last.Visible = false
537	if _, ok := g.scanoutFor(surf); ok {
538		t.Fatal("invisible scanout offered")
539	}
540	w.last.Visible = true
541	if _, ok := g.scanoutFor(surf); !ok {
542		t.Fatal("scanout not restored")
543	}
544}
545
546// A popup's feedback follows its toplevel: resent with it, and on its
547// output's format changes.
548func TestPopupFeedbackFollowsToplevel(t *testing.T) {
549	topWin := &window{hasLast: true, last: ports.ConfigureWindow{Fullscreen: true, Output: "DP-2", Visible: true}}
550	top := &surface{xdg: &xdgSurface{window: topWin}}
551	topWin.xdg = top.xdg
552	top.xdg.surface = top
553	popWin := &window{}
554	popWin.popup = &popup{w: popWin, parent: topWin}
555	pop := &surface{xdg: &xdgSurface{window: popWin}}
556	popWin.xdg = pop.xdg
557	pop.xdg.surface = pop
558	if toplevelRoot(pop) != top {
559		t.Fatal("popup does not resolve to its toplevel")
560	}
561	g := testDMABufGlobal(&Server{}, ports.OutputFormats{Output: "DP-2", Formats: []ports.DMABufFormat{linearARGB}})
562	if _, ok := g.scanoutFor(pop); !ok {
563		t.Fatal("popup of a visible fullscreen window has no offer")
564	}
565	topWin.last.Visible = false
566	if _, ok := g.scanoutFor(pop); ok {
567		t.Fatal("popup of an invisible window keeps its offer")
568	}
569}

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 25

Tags 10

↑↓ moveenter openesc close