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/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// A format the renderer did not list fails; a plane past the end of its
61// 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	// NV12 is not in the list: failed, not a protocol error.
97	_, result := create('N'|'V'<<8|'1'<<16|'2'<<24, 16)
98	if op := <-result; op != uint16(linuxdmabuf.ZwpLinuxBufferParamsV1EventFailed) {
99		t.Fatalf("event %d", op)
100	}
101	// Supported: created (import errors surface in the renderer).
102	_, result = create(linearARGB.Format, 16)
103	if op := <-result; op != uint16(linuxdmabuf.ZwpLinuxBufferParamsV1EventCreated) {
104		t.Fatalf("event %d", op)
105	}
106	// Taller than the file: out of bounds.
107	big := c.AllocateID()
108	requestProtocol(t, c, dm, linuxdmabuf.ZwpLinuxDmabufV1RequestCreateParams, big)
109	if err := wireRequest(c, big, uint16(linuxdmabuf.ZwpLinuxBufferParamsV1RequestAdd), []int{fd}, uint32(0), uint32(0), uint32(256), uint32(0), uint32(0)); err != nil {
110		t.Fatal(err)
111	}
112	requestProtocol(t, c, big, linuxdmabuf.ZwpLinuxBufferParamsV1RequestCreate, int32(64), int32(17), linearARGB.Format, uint32(0))
113	expectProtocolError(t, c, big, uint32(linuxdmabuf.ZwpLinuxBufferParamsV1ErrorOutOfBounds))
114}
115
116// A reused params object is a protocol error.
117func TestDMABufParamsReuse(t *testing.T) {
118	s, _, _, dir := dmabufServer(t, ports.DMABufSupport{Formats: []ports.DMABufFormat{linearARGB}})
119	c := protocolClient(t, s, dir)
120	g, _ := c.Registry().FindGlobal("zwp_linux_dmabuf_v1")
121	dm, err := bindWireID(c, g.Name, g.Interface, dmabufVersion)
122	if err != nil {
123		t.Fatal(err)
124	}
125	fd, err := unix.MemfdCreate("fake-dmabuf", 0)
126	if err != nil {
127		t.Fatal(err)
128	}
129	defer unix.Close(fd)
130	if err := unix.Ftruncate(fd, 256*16); err != nil {
131		t.Fatal(err)
132	}
133	p := c.AllocateID()
134	requestProtocol(t, c, dm, linuxdmabuf.ZwpLinuxDmabufV1RequestCreateParams, p)
135	proxy := &paramsProxy{result: make(chan uint16, 2)}
136	proxy.SetID(p)
137	registerWireProxy(c, proxy)
138	if err := wireRequest(c, p, uint16(linuxdmabuf.ZwpLinuxBufferParamsV1RequestAdd), []int{fd}, uint32(0), uint32(0), uint32(256), uint32(0), uint32(0)); err != nil {
139		t.Fatal(err)
140	}
141	requestProtocol(t, c, p, linuxdmabuf.ZwpLinuxBufferParamsV1RequestCreate, int32(64), int32(16), linearARGB.Format, uint32(0))
142	if err := c.Roundtrip(); err != nil {
143		t.Fatal(err)
144	}
145	requestProtocol(t, c, p, linuxdmabuf.ZwpLinuxBufferParamsV1RequestCreate, int32(64), int32(16), linearARGB.Format, uint32(0))
146	expectProtocolError(t, c, p, uint32(linuxdmabuf.ZwpLinuxBufferParamsV1ErrorAlreadyUsed))
147}
148
149// weston-simple-dmabuf-egl renders on the GPU and sends dmabufs: its
150// contents reach the renderer as DMABuf, never as pixels.
151func TestWestonDMABuf(t *testing.T) {
152	tool, err := exec.LookPath("weston-simple-dmabuf-egl")
153	if err != nil {
154		t.Skip("weston-simple-dmabuf-egl not installed")
155	}
156	// Linear buffers: every GPU allocates them, and the client picks from
157	// what it is offered.
158	xrgb := ports.DMABufFormat{Format: fourccXRGB}
159	s, events, contents, dir := dmabufServer(t, ports.DMABufSupport{Formats: []ports.DMABufFormat{linearARGB, xrgb}})
160	_, _, output := lifecycleClient(t, tool, s, dir, nil)
161	w := mapped(t, events, 5*time.Second)
162	deadline := time.After(5 * time.Second)
163	for {
164		select {
165		case c := <-contents:
166			if c.ID != w.ID || c.DMABuf == nil || c.SHM != nil || len(c.DMABuf.Planes) == 0 || c.DMABuf.Planes[0].File == nil {
167				t.Fatalf("content %+v", c)
168			}
169			return
170		case <-deadline:
171			t.Fatalf("no dmabuf content: %s", output.String())
172		}
173	}
174}
175
176// feedbackProxy records the tranches of each feedback round (done).
177type feedbackProxy struct {
178	wlturbo.BaseProxy
179	rounds chan []tranche
180	cur    []tranche
181	open   tranche
182}
183
184type tranche struct {
185	flags   uint32
186	indices []uint16
187}
188
189func (p *feedbackProxy) Dispatch(e *wlturbo.Event) {
190	switch uint32(e.Opcode) {
191	case linuxdmabuf.ZwpLinuxDmabufFeedbackV1EventFormatTable:
192		if fd := e.Fd(); fd != 0 {
193			unix.Close(int(fd))
194		}
195	case linuxdmabuf.ZwpLinuxDmabufFeedbackV1EventTrancheFlags:
196		p.open.flags = e.Uint32()
197	case linuxdmabuf.ZwpLinuxDmabufFeedbackV1EventTrancheFormats:
198		a := e.Array()
199		for i := 0; i+1 < len(a); i += 2 {
200			p.open.indices = append(p.open.indices, uint16(a[i])|uint16(a[i+1])<<8)
201		}
202	case linuxdmabuf.ZwpLinuxDmabufFeedbackV1EventTrancheDone:
203		p.cur, p.open = append(p.cur, p.open), tranche{}
204	case linuxdmabuf.ZwpLinuxDmabufFeedbackV1EventDone:
205		p.rounds <- p.cur
206		p.cur = nil
207	}
208}
209
210// A fullscreen surface on an output with scanout formats gets a scanout
211// tranche first; leaving fullscreen sends the renderer tranche alone.
212func TestDMABufScanoutTranche(t *testing.T) {
213	tiled := ports.DMABufFormat{Format: linearARGB.Format, Modifier: 0x0200000000000001}
214	sup := ports.DMABufSupport{Device: 1, Formats: []ports.DMABufFormat{linearARGB, tiled}}
215	dir := t.TempDir()
216	events := make(chan ports.ClientEvent, 16)
217	commands := make(chan ports.ClientCommand, 16)
218	formats := make(chan ports.OutputFormats, 1)
219	s, err := New(Options{RuntimeDir: dir, Outputs: testOutputs, DMABuf: sup}, Channels{Events: events, Commands: commands, OutputFormats: formats}, logging.For(context.Background(), "wayland"))
220	if err != nil {
221		t.Fatal(err)
222	}
223	ctx, cancel := context.WithCancel(context.Background())
224	done := make(chan error, 1)
225	go func() { done <- s.Run(ctx) }()
226	t.Cleanup(func() { cancel(); <-done })
227	c := protocolClient(t, s, dir)
228	w, surf, xdg := surfaceMapper(t, c, events)()
229	// Our configures must not block the mapper's one-slot proxy.
230	registerProtocol(t, c, xdg)
231	dm := bindVersion(t, c, "zwp_linux_dmabuf_v1", dmabufVersion)
232	fb := c.AllocateID()
233	proxy := &feedbackProxy{rounds: make(chan []tranche, 8)}
234	proxy.SetID(fb)
235	registerWireProxy(c, proxy)
236	requestProtocol(t, c, dm, linuxdmabuf.ZwpLinuxDmabufV1RequestGetSurfaceFeedback, fb, surf)
237	round := func(what string) []tranche {
238		t.Helper()
239		deadline := time.After(2 * time.Second)
240		for {
241			if err := c.Roundtrip(); err != nil {
242				t.Fatal(err)
243			}
244			select {
245			case r := <-proxy.rounds:
246				return r
247			case <-deadline:
248				t.Fatalf("no feedback: %s", what)
249			default:
250				time.Sleep(5 * time.Millisecond)
251			}
252		}
253	}
254	scanout := uint32(linuxdmabuf.ZwpLinuxDmabufFeedbackV1TrancheFlagsScanout)
255	if r := round("initial"); len(r) != 1 || r[0].flags != 0 || len(r[0].indices) != 2 {
256		t.Fatalf("tiled window: %+v", r)
257	}
258	formats <- ports.OutputFormats{Output: "HEADLESS-1", Device: 7, Formats: []ports.DMABufFormat{tiled}}
259	commands <- ports.ConfigureWindow{ID: w.ID, Width: 1920, Height: 1080, Fullscreen: true, Output: "HEADLESS-1", Visible: true}
260	r := round("fullscreen")
261	for len(r) == 1 { // the formats may land after the configure
262		r = round("fullscreen with formats")
263	}
264	if len(r) != 2 || r[0].flags != scanout || len(r[0].indices) != 1 || r[0].indices[0] != 1 || r[1].flags != 0 {
265		t.Fatalf("fullscreen: %+v", r)
266	}
267	// Hidden, the window loses its scanout tranche; shown, it gets it back.
268	commands <- ports.ConfigureWindow{ID: w.ID, Width: 1920, Height: 1080, Fullscreen: true, Output: "HEADLESS-1"}
269	if r := round("hidden"); len(r) != 1 {
270		t.Fatalf("hidden fullscreen: %+v", r)
271	}
272	commands <- ports.ConfigureWindow{ID: w.ID, Width: 1920, Height: 1080, Fullscreen: true, Output: "HEADLESS-1", Visible: true}
273	if r := round("shown again"); len(r) != 2 || r[0].flags != scanout {
274		t.Fatalf("shown fullscreen: %+v", r)
275	}
276	// Fullscreen on another output: its formats (none) apply at once.
277	commands <- ports.ConfigureWindow{ID: w.ID, Width: 1920, Height: 1080, Fullscreen: true, Output: "OTHER-1", Visible: true}
278	if r := round("other output"); len(r) != 1 {
279		t.Fatalf("moved output: %+v", r)
280	}
281	commands <- ports.ConfigureWindow{ID: w.ID, Width: 800, Height: 600, Output: "HEADLESS-1", Visible: true}
282	if r := round("tiled again"); len(r) != 1 || r[0].flags != 0 {
283		t.Fatalf("left fullscreen: %+v", r)
284	}
285	// A destroyed feedback gets nothing more (a protocol error would
286	// kill the connection) and leaves the server's list.
287	requestProtocol(t, c, fb, linuxdmabuf.ZwpLinuxDmabufFeedbackV1RequestDestroy)
288	commands <- ports.ConfigureWindow{ID: w.ID, Width: 1920, Height: 1080, Fullscreen: true, Output: "HEADLESS-1", Visible: true}
289	formats <- ports.OutputFormats{Output: "HEADLESS-1", Device: 7}
290	if err := c.Roundtrip(); err != nil {
291		t.Fatal(err)
292	}
293	var n int
294	s.display.Do(func() { n = len(s.dmabuf.feedbacks) })
295	if n != 0 {
296		t.Fatalf("%d feedbacks kept", n)
297	}
298}
299
300func TestInvisibleScanoutOffer(t *testing.T) {
301	w := &window{hasLast: true, last: ports.ConfigureWindow{Fullscreen: true, Output: "DP-2", Visible: true}}
302	surf := &surface{xdg: &xdgSurface{window: w}}
303	s := &Server{}
304	g := &dmabufGlobal{server: s, scanout: map[string]ports.OutputFormats{"DP-2": {Output: "DP-2", Formats: []ports.DMABufFormat{linearARGB}}}}
305	if _, ok := g.scanoutFor(surf); !ok {
306		t.Fatal("no visible scanout")
307	}
308	w.last.Visible = false
309	if _, ok := g.scanoutFor(surf); ok {
310		t.Fatal("invisible scanout offered")
311	}
312	w.last.Visible = true
313	if _, ok := g.scanoutFor(surf); !ok {
314		t.Fatal("scanout not restored")
315	}
316}
317
318// A popup's feedback follows its toplevel: resent with it, and on its
319// output's format changes.
320func TestPopupFeedbackFollowsToplevel(t *testing.T) {
321	topWin := &window{hasLast: true, last: ports.ConfigureWindow{Fullscreen: true, Output: "DP-2", Visible: true}}
322	top := &surface{xdg: &xdgSurface{window: topWin}}
323	topWin.xdg = top.xdg
324	top.xdg.surface = top
325	popWin := &window{}
326	popWin.popup = &popup{w: popWin, parent: topWin}
327	pop := &surface{xdg: &xdgSurface{window: popWin}}
328	popWin.xdg = pop.xdg
329	pop.xdg.surface = pop
330	if toplevelRoot(pop) != top {
331		t.Fatal("popup does not resolve to its toplevel")
332	}
333	g := &dmabufGlobal{server: &Server{}, scanout: map[string]ports.OutputFormats{"DP-2": {Output: "DP-2", Formats: []ports.DMABufFormat{linearARGB}}}}
334	if _, ok := g.scanoutFor(pop); !ok {
335		t.Fatal("popup of a visible fullscreen window has no offer")
336	}
337	topWin.last.Visible = false
338	if _, ok := g.scanoutFor(pop); ok {
339		t.Fatal("popup of an invisible window keeps its offer")
340	}
341}

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