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 := ¶msProxy{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 := ¶msProxy{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 := ¶msProxy{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 := ¶msProxy{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}