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