1package wayland
2
3import (
4 "context"
5 "fmt"
6 "image"
7 "reflect"
8 "slices"
9 "strings"
10 "testing"
11 "time"
12
13 source "github.com/bnema/go-wayland-bindings/server/extimagecapturesource"
14 ext "github.com/bnema/go-wayland-bindings/server/extimagecopycapture"
15
16 "github.com/bnema/go-wayland-bindings/server/wayland"
17 wlr "github.com/bnema/go-wayland-bindings/server/wlrscreencopy"
18 "github.com/bnema/neferwl/internal/logging"
19 "github.com/bnema/neferwl/internal/ports"
20 "github.com/bnema/wlturbo"
21 "golang.org/x/sys/unix"
22)
23
24type captureEvents struct {
25 wlturbo.BaseProxy
26 events chan uint16
27}
28
29func (p *captureEvents) Dispatch(e *wlturbo.Event) { p.events <- e.Opcode }
30func TestCaptureRegion(t *testing.T) {
31 o := &output{place: ports.OutputPlacement{Info: ports.OutputInfo{Width: 300, Height: 200}, Scale: 1.5}}
32 for _, tc := range []struct{ in, want image.Rectangle }{
33 {image.Rect(0, 0, 100, 100), image.Rect(0, 0, 150, 150)},
34 {image.Rect(-10, -10, 10, 10), image.Rect(0, 0, 15, 15)},
35 {image.Rect(190, 120, 240, 160), image.Rect(285, 180, 300, 200)},
36 } {
37 if got := captureRegion(o, tc.in); got != tc.want {
38 t.Errorf("%v: %v != %v", tc.in, got, tc.want)
39 }
40 }
41}
42
43// Regions are logical; a rotated output holds them in its target under the
44// transform, and logicalRegion maps them back.
45func TestCaptureRegionTransform(t *testing.T) {
46 o := &output{place: ports.OutputPlacement{Info: ports.OutputInfo{Width: 200, Height: 100}, Scale: 1, Transform: 1, Width: 100, Height: 200}}
47 got := captureRegion(o, image.Rect(0, 0, 10, 20))
48 if want := image.Rect(0, 90, 20, 100); got != want {
49 t.Fatalf("captureRegion %v, want %v", got, want)
50 }
51 if back := logicalRegion(o, got); back != (ports.Rect{X: 0, Y: 0, W: 10, H: 20}) {
52 t.Fatalf("logicalRegion %+v", back)
53 }
54 if full := logicalRegion(o, image.Rect(0, 0, 200, 100)); full != (ports.Rect{}) {
55 t.Fatalf("full target %+v", full)
56 }
57}
58func TestScreencopyProtocol(t *testing.T) {
59 dir := t.TempDir()
60 requests := make(chan ports.CaptureRequest, 1)
61 replies := make(chan ports.CaptureDone, 1)
62 s, err := New(Options{RuntimeDir: dir, CaptureAllow: allowStore(t, "*\n"), Outputs: ports.Layout{{Info: ports.OutputInfo{Name: "HEADLESS-1", Width: 4, Height: 4}, Width: 4, Height: 4, Scale: 1}}}, Channels{Captures: requests, Captured: replies}, logging.For(context.Background(), "wayland"))
63 if err != nil {
64 t.Fatal(err)
65 }
66 ctx, cancel := context.WithCancel(context.Background())
67 done := make(chan error, 1)
68 go func() { done <- s.Run(ctx) }()
69 defer func() {
70 cancel()
71 if err := <-done; err != nil {
72 t.Error(err)
73 }
74 }()
75 c := protocolClient(t, s, dir)
76 var out uint32
77 for name, g := range c.Registry().GetGlobals() {
78 if g.Interface == "wl_output" {
79 out = name
80 }
81 }
82 if out == 0 {
83 t.Fatal("output global missing")
84 }
85 outputID, err := bindWireID(c, out, "wl_output", 4)
86 if err != nil {
87 t.Fatal(err)
88 }
89 registerProtocol(t, c, outputID)
90 manager := bindVersion(t, c, "zwlr_screencopy_manager_v1", 3)
91 frame := c.AllocateID()
92 events := &captureEvents{events: make(chan uint16, 8)}
93 events.SetID(frame)
94 registerWireProxy(c, events)
95 requestProtocol(t, c, manager, wlr.ZwlrScreencopyManagerV1RequestCaptureOutputRegion, frame, int32(0), outputID, int32(1), int32(1), int32(2), int32(2))
96 if err := c.Roundtrip(); err != nil {
97 t.Fatal(err)
98 }
99 if op := <-events.events; op != uint16(wlr.ZwlrScreencopyFrameV1EventBuffer) {
100 t.Fatalf("buffer: %d", op)
101 }
102 if op := <-events.events; op != uint16(wlr.ZwlrScreencopyFrameV1EventBufferDone) {
103 t.Fatalf("done: %d", op)
104 }
105 shm := bindProtocol(t, c, "wl_shm")
106 registerProtocol(t, c, shm)
107 fd, err := unix.MemfdCreate("capture", 0)
108 if err != nil {
109 t.Fatal(err)
110 }
111 defer unix.Close(fd)
112 if err := unix.Ftruncate(fd, 16); err != nil {
113 t.Fatal(err)
114 }
115 pool, buf := c.AllocateID(), c.AllocateID()
116 registerProtocol(t, c, pool)
117 registerProtocol(t, c, buf)
118 if err := wireRequest(c, shm, uint16(wayland.ShmRequestCreatePool), []int{fd}, pool, int32(16)); err != nil {
119 t.Fatal(err)
120 }
121 requestProtocol(t, c, pool, wayland.ShmPoolRequestCreateBuffer, buf, int32(0), int32(2), int32(2), int32(8), uint32(wayland.ShmFormatXrgb8888))
122 requestProtocol(t, c, frame, wlr.ZwlrScreencopyFrameV1RequestCopyWithDamage, buf)
123 if err := c.Roundtrip(); err != nil {
124 t.Fatal(err)
125 }
126 select {
127 case req := <-requests:
128 if req.Region != image.Rect(1, 1, 3, 3) {
129 t.Errorf("region: %v", req.Region)
130 }
131 if _, err := req.Dst.File.WriteAt([]byte{1, 2, 3, 255}, 0); err != nil {
132 t.Fatal(err)
133 }
134 if err := req.Dst.File.Close(); err != nil {
135 t.Fatal(err)
136 }
137 deliverCapture(t, s, replies, ports.CaptureDone{ID: req.ID, Output: req.Output, Time: time.Now()})
138 case <-time.After(2 * time.Second):
139 t.Fatal("no request")
140 }
141 if err := c.Roundtrip(); err != nil {
142 t.Fatal(err)
143 }
144 for _, want := range []uint16{uint16(wlr.ZwlrScreencopyFrameV1EventFlags), uint16(wlr.ZwlrScreencopyFrameV1EventDamage), uint16(wlr.ZwlrScreencopyFrameV1EventReady)} {
145 var got uint16
146 select {
147 case got = <-events.events:
148 case <-time.After(2 * time.Second):
149 t.Fatalf("missing event %d", want)
150 }
151 if got != want {
152 t.Errorf("event %d, want %d", got, want)
153 }
154 }
155 data := make([]byte, 4)
156 if _, err := unix.Pread(fd, data, 0); err != nil {
157 t.Fatal(err)
158 }
159 if data[0] != 1 {
160 t.Fatal(data)
161 }
162}
163
164func captureTestClient(t *testing.T) (*Server, *wlturbo.Display, uint32) {
165 t.Helper()
166 s, _, _, dir := lifecycleServer(t)
167 c := protocolClient(t, s, dir)
168 g, ok := c.Registry().FindGlobal("wl_output")
169 if !ok {
170 t.Fatal("wl_output missing")
171 }
172 out, err := bindWireID(c, g.Name, g.Interface, 4)
173 if err != nil {
174 t.Fatal(err)
175 }
176 registerProtocol(t, c, out)
177 if err := c.Roundtrip(); err != nil {
178 t.Fatal(err)
179 }
180 return s, c, out
181}
182
183func captureTestExt(t *testing.T, c *wlturbo.Display, out uint32) (uint32, uint32, uint32) {
184 t.Helper()
185 sourceManager := bindProtocol(t, c, "ext_output_image_capture_source_manager_v1")
186 manager := bindProtocol(t, c, "ext_image_copy_capture_manager_v1")
187 src, session := c.AllocateID(), c.AllocateID()
188 registerProtocol(t, c, src)
189 requestProtocol(t, c, sourceManager, source.ExtOutputImageCaptureSourceManagerV1RequestCreateSource, src, out)
190 events := &captureEvents{events: make(chan uint16, 16)}
191 events.SetID(session)
192 registerWireProxy(c, events)
193 requestProtocol(t, c, manager, ext.ExtImageCopyCaptureManagerV1RequestCreateSession, session, src, uint32(0))
194 if err := c.Roundtrip(); err != nil {
195 t.Fatal(err)
196 }
197 for _, want := range []uint16{0, 1, 1, 4} {
198 if got := <-events.events; got != want {
199 t.Fatalf("session event %d != %d", got, want)
200 }
201 }
202 return manager, session, src
203}
204
205func TestCaptureProtocolErrors(t *testing.T) {
206 for _, tc := range []struct {
207 name string
208 object string
209 code uint32
210 }{
211 {"wlr already used", "wlr", uint32(wlr.ZwlrScreencopyFrameV1ErrorAlreadyUsed)},
212 {"wlr invalid size", "wlr size", uint32(wlr.ZwlrScreencopyFrameV1ErrorInvalidBuffer)},
213 {"wlr invalid format", "wlr format", uint32(wlr.ZwlrScreencopyFrameV1ErrorInvalidBuffer)},
214 {"ext no buffer", "frame", uint32(ext.ExtImageCopyCaptureFrameV1ErrorNoBuffer)},
215 {"ext already captured", "frame twice", uint32(ext.ExtImageCopyCaptureFrameV1ErrorAlreadyCaptured)},
216 {"ext damage", "damage", uint32(ext.ExtImageCopyCaptureFrameV1ErrorInvalidBufferDamage)},
217 {"ext duplicate frame", "session", uint32(ext.ExtImageCopyCaptureSessionV1ErrorDuplicateFrame)},
218 {"ext invalid option", "manager", uint32(ext.ExtImageCopyCaptureManagerV1ErrorInvalidOption)},
219 {"ext duplicate cursor session", "cursor", uint32(ext.ExtImageCopyCaptureCursorSessionV1ErrorDuplicateSession)},
220 {"ext buffer constraints", "constraints", 0},
221 {"ext attach after capture", "attach after", uint32(ext.ExtImageCopyCaptureFrameV1ErrorAlreadyCaptured)},
222 {"ext damage after capture", "damage after", uint32(ext.ExtImageCopyCaptureFrameV1ErrorAlreadyCaptured)},
223 } {
224 t.Run(tc.name, func(t *testing.T) {
225 _, c, out := captureTestClient(t)
226 if strings.HasPrefix(tc.object, "wlr") {
227 manager := bindVersion(t, c, "zwlr_screencopy_manager_v1", 3)
228 frame := c.AllocateID()
229 registerProtocol(t, c, frame)
230 requestProtocol(t, c, manager, wlr.ZwlrScreencopyManagerV1RequestCaptureOutput, frame, int32(0), out)
231 if err := c.Roundtrip(); err != nil {
232 t.Fatal(err)
233 }
234 shm := bindProtocol(t, c, "wl_shm")
235 fd, err := unix.MemfdCreate("bad-capture", 0)
236 if err != nil {
237 t.Fatal(err)
238 }
239 defer unix.Close(fd)
240 if err := unix.Ftruncate(fd, int64(testOutputs[0].Info.Width*testOutputs[0].Info.Height*4)); err != nil {
241 t.Fatal(err)
242 }
243 pool, buf := c.AllocateID(), c.AllocateID()
244 registerProtocol(t, c, pool)
245 registerProtocol(t, c, buf)
246 registerProtocol(t, c, shm)
247 if err := wireRequest(c, shm, uint16(wayland.ShmRequestCreatePool), []int{fd}, pool, int32(testOutputs[0].Info.Width*testOutputs[0].Info.Height*4)); err != nil {
248 t.Fatal(err)
249 }
250 w := int32(1)
251 format := uint32(wayland.ShmFormatXrgb8888)
252 if tc.object == "wlr format" {
253 format = uint32(wayland.ShmFormatArgb8888)
254 }
255 if tc.object != "wlr size" {
256 w = int32(testOutputs[0].Info.Width)
257 }
258 requestProtocol(t, c, pool, wayland.ShmPoolRequestCreateBuffer, buf, int32(0), w, int32(testOutputs[0].Info.Height), w*4, format)
259 if tc.object == "wlr" {
260 if err := c.Roundtrip(); err != nil {
261 t.Fatal(err)
262 }
263 requestProtocol(t, c, frame, wlr.ZwlrScreencopyFrameV1RequestCopy, buf)
264 if err := c.Roundtrip(); err != nil {
265 t.Fatal(err)
266 }
267 }
268 requestProtocol(t, c, frame, wlr.ZwlrScreencopyFrameV1RequestCopy, buf)
269 expectProtocolError(t, c, frame, tc.code)
270 return
271 }
272 manager, session, src := captureTestExt(t, c, out)
273 switch tc.object {
274 case "manager":
275 requestProtocol(t, c, manager, ext.ExtImageCopyCaptureManagerV1RequestCreateSession, c.AllocateID(), src, uint32(2))
276 expectProtocolError(t, c, manager, tc.code)
277 return
278 case "cursor":
279 seat := bindProtocol(t, c, "wl_seat")
280 pointer := c.AllocateID()
281 registerProtocol(t, c, pointer)
282 requestProtocol(t, c, seat, wayland.SeatRequestGetPointer, pointer)
283 registerProtocol(t, c, seat)
284 cursor := c.AllocateID()
285 registerProtocol(t, c, cursor)
286 requestProtocol(t, c, manager, ext.ExtImageCopyCaptureManagerV1RequestCreatePointerCursorSession, cursor, src, pointer)
287 registerProtocol(t, c, manager)
288 sessionID := c.AllocateID()
289 registerProtocol(t, c, sessionID)
290 requestProtocol(t, c, cursor, ext.ExtImageCopyCaptureCursorSessionV1RequestGetCaptureSession, sessionID)
291 if err := c.Roundtrip(); err != nil {
292 t.Fatal(err)
293 }
294 requestProtocol(t, c, cursor, ext.ExtImageCopyCaptureCursorSessionV1RequestGetCaptureSession, c.AllocateID())
295 expectProtocolError(t, c, cursor, tc.code)
296 return
297 }
298 frame := c.AllocateID()
299 frameEvents := &captureDetails{events: make(chan []uint32, 4)}
300 frameEvents.SetID(frame)
301 registerWireProxy(c, frameEvents)
302 requestProtocol(t, c, session, ext.ExtImageCopyCaptureSessionV1RequestCreateFrame, frame)
303 switch tc.object {
304 case "frame":
305 requestProtocol(t, c, frame, ext.ExtImageCopyCaptureFrameV1RequestCapture)
306 case "constraints", "attach after", "damage after", "frame twice":
307 shm := bindProtocol(t, c, "wl_shm")
308 registerProtocol(t, c, shm)
309 fd, err := unix.MemfdCreate("ext-capture", 0)
310 if err != nil {
311 t.Fatal(err)
312 }
313 defer unix.Close(fd)
314 if err := unix.Ftruncate(fd, 4); err != nil {
315 t.Fatal(err)
316 }
317 pool, buf := c.AllocateID(), c.AllocateID()
318 registerProtocol(t, c, pool)
319 registerProtocol(t, c, buf)
320 if err := wireRequest(c, shm, uint16(wayland.ShmRequestCreatePool), []int{fd}, pool, int32(4)); err != nil {
321 t.Fatal(err)
322 }
323 requestProtocol(t, c, pool, wayland.ShmPoolRequestCreateBuffer, buf, int32(0), int32(1), int32(1), int32(4), uint32(wayland.ShmFormatXrgb8888))
324 requestProtocol(t, c, frame, ext.ExtImageCopyCaptureFrameV1RequestAttachBuffer, buf)
325 requestProtocol(t, c, frame, ext.ExtImageCopyCaptureFrameV1RequestCapture)
326 if tc.object == "constraints" {
327 if err := c.Roundtrip(); err != nil {
328 t.Fatal(err)
329 }
330 if got := <-frameEvents.events; !reflect.DeepEqual(got, []uint32{uint32(ext.ExtImageCopyCaptureFrameV1EventFailed), uint32(ext.ExtImageCopyCaptureFrameV1FailureReasonBufferConstraints)}) {
331 t.Fatalf("constraints: event %v", got)
332 }
333 return
334 }
335 switch tc.object {
336 case "frame twice":
337 requestProtocol(t, c, frame, ext.ExtImageCopyCaptureFrameV1RequestCapture)
338 case "attach after":
339 requestProtocol(t, c, frame, ext.ExtImageCopyCaptureFrameV1RequestAttachBuffer, buf)
340 case "damage after":
341 requestProtocol(t, c, frame, ext.ExtImageCopyCaptureFrameV1RequestDamageBuffer, int32(0), int32(0), int32(1), int32(1))
342 }
343 case "damage":
344 requestProtocol(t, c, frame, ext.ExtImageCopyCaptureFrameV1RequestDamageBuffer, int32(-1), int32(0), int32(1), int32(1))
345 case "session":
346 requestProtocol(t, c, session, ext.ExtImageCopyCaptureSessionV1RequestCreateFrame, c.AllocateID())
347 }
348 object := frame
349 if tc.object == "session" {
350 object = session
351 }
352 expectProtocolError(t, c, object, tc.code)
353 })
354 }
355}
356
357// deliverCapture sends a capture reply and waits until the display
358// goroutine has run it, so a following Roundtrip sees its events.
359func deliverCapture(t *testing.T, s *Server, replies chan<- ports.CaptureDone, done ports.CaptureDone) {
360 t.Helper()
361 replies <- done
362 deadline := time.Now().Add(2 * time.Second)
363 for {
364 pending := false
365 if !s.display.Do(func() { _, pending = s.captureReplies[done.ID] }) {
366 t.Fatal("display stopped")
367 }
368 if !pending {
369 return
370 }
371 if time.Now().After(deadline) {
372 t.Fatal("capture reply not delivered")
373 }
374 time.Sleep(time.Millisecond)
375 }
376}
377
378// applyOutputs sends a layout and waits until the display goroutine has
379// applied it, so a following Roundtrip sees its events. A no-op Do alone can
380// run before the command goroutine has even received the command.
381func applyOutputs(t *testing.T, s *Server, commands chan<- ports.ClientCommand, c ports.SetOutputs) {
382 t.Helper()
383 commands <- c
384 deadline := time.Now().Add(2 * time.Second)
385 for {
386 applied := false
387 if !s.display.Do(func() {
388 applied = len(s.outputs) == len(c.Outputs) && slices.Equal(s.outputPlaces, c.Outputs)
389 }) {
390 t.Fatal("display stopped")
391 }
392 if applied {
393 return
394 }
395 if time.Now().After(deadline) {
396 t.Fatal("outputs not applied")
397 }
398 time.Sleep(time.Millisecond)
399 }
400}
401
402// captureDetails records protocol payloads, not just the event opcodes.
403type captureDetails struct {
404 wlturbo.BaseProxy
405 events chan []uint32
406}
407
408func (p *captureDetails) Dispatch(e *wlturbo.Event) {
409 n := 0
410 switch e.Opcode {
411 case 0:
412 n = 2
413 case 1:
414 n = 1
415 if len(e.Data()) >= 16 {
416 n = 4
417 }
418 case 2:
419 n = 3
420 case 4:
421 if len(e.Data()) >= 4 {
422 n = 1
423 }
424 }
425 values := []uint32{uint32(e.Opcode)}
426 for i := 0; i < n; i++ {
427 values = append(values, e.Uint32())
428 }
429 p.events <- values
430}
431func TestExtCaptureLifecycle(t *testing.T) {
432 dir := t.TempDir()
433 requests := make(chan ports.CaptureRequest, 1)
434 replies := make(chan ports.CaptureDone, 1)
435 commands := make(chan ports.ClientCommand, 4)
436 initial := ports.OutputPlacement{Info: ports.OutputInfo{Name: "HEADLESS-1", Width: 4, Height: 4}, Width: 4, Height: 4, Scale: 1}
437 s, err := New(Options{RuntimeDir: dir, CaptureAllow: allowStore(t, "*\n"), Outputs: ports.Layout{initial}}, Channels{Captures: requests, Captured: replies, Commands: commands}, logging.For(context.Background(), "wayland"))
438 if err != nil {
439 t.Fatal(err)
440 }
441 ctx, cancel := context.WithCancel(context.Background())
442 done := make(chan error, 1)
443 go func() { done <- s.Run(ctx) }()
444 defer func() {
445 cancel()
446 if err := <-done; err != nil {
447 t.Error(err)
448 }
449 }()
450 c := protocolClient(t, s, dir)
451 g, ok := c.Registry().FindGlobal("wl_output")
452 if !ok {
453 t.Fatal("output missing")
454 }
455 out, err := bindWireID(c, g.Name, g.Interface, 4)
456 if err != nil {
457 t.Fatal(err)
458 }
459 registerProtocol(t, c, out)
460 srcMgr := bindProtocol(t, c, "ext_output_image_capture_source_manager_v1")
461 mgr := bindProtocol(t, c, "ext_image_copy_capture_manager_v1")
462 src, session := c.AllocateID(), c.AllocateID()
463 registerProtocol(t, c, src)
464 p := &captureDetails{events: make(chan []uint32, 16)}
465 p.SetID(session)
466 registerWireProxy(c, p)
467 requestProtocol(t, c, srcMgr, source.ExtOutputImageCaptureSourceManagerV1RequestCreateSource, src, out)
468 requestProtocol(t, c, mgr, ext.ExtImageCopyCaptureManagerV1RequestCreateSession, session, src, uint32(0))
469 if err := c.Roundtrip(); err != nil {
470 t.Fatal(err)
471 }
472 for _, want := range [][]uint32{{0, 4, 4}, {1, uint32(wayland.ShmFormatXrgb8888)}, {1, uint32(wayland.ShmFormatArgb8888)}, {4}} {
473 got := <-p.events
474 if !reflect.DeepEqual(got, want) {
475 t.Fatalf("session event %v != %v", got, want)
476 }
477 }
478 shm := bindProtocol(t, c, "wl_shm")
479 registerProtocol(t, c, shm)
480 fd, err := unix.MemfdCreate("ext-capture", 0)
481 if err != nil {
482 t.Fatal(err)
483 }
484 defer unix.Close(fd)
485 // Room for the 4x4 and the resized 5x4 buffers.
486 if err := unix.Ftruncate(fd, 80); err != nil {
487 t.Fatal(err)
488 }
489 pool, buf := c.AllocateID(), c.AllocateID()
490 registerProtocol(t, c, pool)
491 registerProtocol(t, c, buf)
492 if err := wireRequest(c, shm, uint16(wayland.ShmRequestCreatePool), []int{fd}, pool, int32(80)); err != nil {
493 t.Fatal(err)
494 }
495 requestProtocol(t, c, pool, wayland.ShmPoolRequestCreateBuffer, buf, int32(0), int32(4), int32(4), int32(16), uint32(wayland.ShmFormatXrgb8888))
496 frame := c.AllocateID()
497 fp := &captureDetails{events: make(chan []uint32, 16)}
498 fp.SetID(frame)
499 registerWireProxy(c, fp)
500 requestProtocol(t, c, session, ext.ExtImageCopyCaptureSessionV1RequestCreateFrame, frame)
501 requestProtocol(t, c, frame, ext.ExtImageCopyCaptureFrameV1RequestAttachBuffer, buf)
502 requestProtocol(t, c, frame, ext.ExtImageCopyCaptureFrameV1RequestCapture)
503 if err := c.Roundtrip(); err != nil {
504 t.Fatal(err)
505 }
506 var req ports.CaptureRequest
507 select {
508 case req = <-requests:
509 case <-time.After(2 * time.Second):
510 t.Fatal("no capture request")
511 }
512 if req.Width != 4 || req.Height != 4 {
513 t.Fatalf("request %+v", req)
514 }
515 if err := req.Dst.File.Close(); err != nil {
516 t.Fatal(err)
517 }
518 var now unix.Timespec
519 if err := unix.ClockGettime(unix.CLOCK_MONOTONIC, &now); err != nil {
520 t.Fatal(err)
521 }
522 deliverCapture(t, s, replies, ports.CaptureDone{ID: req.ID, Time: time.Unix(now.Sec, now.Nsec)})
523 if err := c.Roundtrip(); err != nil {
524 t.Fatal(err)
525 }
526 for _, want := range [][]uint32{{0, 0, 0}, {1, 0, 0, 4, 4}, {2, 0, uint32(now.Sec), uint32(now.Nsec)}, {3}} {
527 got := <-fp.events
528 if !reflect.DeepEqual(got, want) {
529 t.Fatalf("frame event %v != %v", got, want)
530 }
531 }
532 resized := initial
533 resized.Info.Width = 5
534 resized.Width = 5
535 applyOutputs(t, s, commands, ports.SetOutputs{Outputs: ports.Layout{resized}})
536 if err := c.Roundtrip(); err != nil {
537 t.Fatal(err)
538 }
539 // A resize sends a complete constraint batch, formats included.
540 for _, want := range [][]uint32{{0, 5, 4}, {1, uint32(wayland.ShmFormatXrgb8888)}, {1, uint32(wayland.ShmFormatArgb8888)}, {4}} {
541 got := <-p.events
542 if !reflect.DeepEqual(got, want) {
543 t.Fatalf("resize event %v != %v", got, want)
544 }
545 }
546 // Destroying the session leaves its pending frame alive: it still gets ready.
547 requestProtocol(t, c, frame, ext.ExtImageCopyCaptureFrameV1RequestDestroy) // one live frame per session
548 buf2, frame2 := c.AllocateID(), c.AllocateID()
549 registerProtocol(t, c, buf2)
550 requestProtocol(t, c, pool, wayland.ShmPoolRequestCreateBuffer, buf2, int32(0), int32(5), int32(4), int32(20), uint32(wayland.ShmFormatXrgb8888))
551 fp2 := &captureDetails{events: make(chan []uint32, 16)}
552 fp2.SetID(frame2)
553 registerWireProxy(c, fp2)
554 requestProtocol(t, c, session, ext.ExtImageCopyCaptureSessionV1RequestCreateFrame, frame2)
555 requestProtocol(t, c, frame2, ext.ExtImageCopyCaptureFrameV1RequestAttachBuffer, buf2)
556 requestProtocol(t, c, frame2, ext.ExtImageCopyCaptureFrameV1RequestCapture)
557 if err := c.Roundtrip(); err != nil {
558 t.Fatal(err)
559 }
560 select {
561 case req = <-requests:
562 case <-time.After(2 * time.Second):
563 t.Fatal("no capture request after resize")
564 }
565 if err := req.Dst.File.Close(); err != nil {
566 t.Fatal(err)
567 }
568 requestProtocol(t, c, session, ext.ExtImageCopyCaptureSessionV1RequestDestroy)
569 if err := c.Roundtrip(); err != nil {
570 t.Fatal(err)
571 }
572 deliverCapture(t, s, replies, ports.CaptureDone{ID: req.ID, Time: time.Unix(now.Sec, now.Nsec)})
573 if err := c.Roundtrip(); err != nil {
574 t.Fatal(err)
575 }
576 for _, want := range [][]uint32{{0, 0, 0}, {1, 0, 0, 5, 4}, {2, 0, uint32(now.Sec), uint32(now.Nsec)}, {3}} {
577 select {
578 case got := <-fp2.events:
579 if !reflect.DeepEqual(got, want) {
580 t.Fatalf("frame after session destroy: %v != %v", got, want)
581 }
582 case <-time.After(2 * time.Second):
583 t.Fatalf("frame after session destroy: missing %v", want)
584 }
585 }
586}
587
588// The remaining stop check needs a live session.
589func TestExtCaptureStoppedOnOutputRemoval(t *testing.T) {
590 dir := t.TempDir()
591 commands := make(chan ports.ClientCommand, 4)
592 initial := ports.OutputPlacement{Info: ports.OutputInfo{Name: "HEADLESS-1", Width: 4, Height: 4}, Width: 4, Height: 4, Scale: 1}
593 s, err := New(Options{RuntimeDir: dir, CaptureAllow: allowStore(t, "*\n"), Outputs: ports.Layout{initial}}, Channels{Captures: make(chan ports.CaptureRequest, 1), Captured: make(chan ports.CaptureDone, 1), Commands: commands}, logging.For(context.Background(), "wayland"))
594 if err != nil {
595 t.Fatal(err)
596 }
597 ctx, cancel := context.WithCancel(context.Background())
598 done := make(chan error, 1)
599 go func() { done <- s.Run(ctx) }()
600 defer func() {
601 cancel()
602 if err := <-done; err != nil {
603 t.Error(err)
604 }
605 }()
606 c := protocolClient(t, s, dir)
607 g, ok := c.Registry().FindGlobal("wl_output")
608 if !ok {
609 t.Fatal("output missing")
610 }
611 out, err := bindWireID(c, g.Name, g.Interface, 4)
612 if err != nil {
613 t.Fatal(err)
614 }
615 registerProtocol(t, c, out)
616 srcMgr := bindProtocol(t, c, "ext_output_image_capture_source_manager_v1")
617 mgr := bindProtocol(t, c, "ext_image_copy_capture_manager_v1")
618 src, session := c.AllocateID(), c.AllocateID()
619 registerProtocol(t, c, src)
620 p := &captureDetails{events: make(chan []uint32, 16)}
621 p.SetID(session)
622 registerWireProxy(c, p)
623 requestProtocol(t, c, srcMgr, source.ExtOutputImageCaptureSourceManagerV1RequestCreateSource, src, out)
624 requestProtocol(t, c, mgr, ext.ExtImageCopyCaptureManagerV1RequestCreateSession, session, src, uint32(0))
625 if err := c.Roundtrip(); err != nil {
626 t.Fatal(err)
627 }
628 for range 4 { // initial constraint batch
629 <-p.events
630 }
631 applyOutputs(t, s, commands, ports.SetOutputs{})
632 if err := c.Roundtrip(); err != nil {
633 t.Fatal(err)
634 }
635 if got := <-p.events; !reflect.DeepEqual(got, []uint32{5}) {
636 t.Fatalf("stopped: %v", got)
637 }
638}
639
640func TestWlrOutputGoneAndDestroyedBeforeReply(t *testing.T) {
641 for _, destroy := range []bool{false, true} {
642 t.Run(fmt.Sprint("destroy=", destroy), func(t *testing.T) {
643 dir := t.TempDir()
644 requests := make(chan ports.CaptureRequest, 1)
645 replies := make(chan ports.CaptureDone, 1)
646 commands := make(chan ports.ClientCommand, 2)
647 initial := ports.OutputPlacement{Info: ports.OutputInfo{Name: "HEADLESS-1", Width: 2, Height: 2}, Width: 2, Height: 2, Scale: 1}
648 s, err := New(Options{RuntimeDir: dir, CaptureAllow: allowStore(t, "*\n"), Outputs: ports.Layout{initial}}, Channels{Captures: requests, Captured: replies, Commands: commands}, logging.For(context.Background(), "wayland"))
649 if err != nil {
650 t.Fatal(err)
651 }
652 ctx, cancel := context.WithCancel(context.Background())
653 done := make(chan error, 1)
654 go func() { done <- s.Run(ctx) }()
655 defer func() {
656 cancel()
657 if err := <-done; err != nil {
658 t.Error(err)
659 }
660 }()
661 c := protocolClient(t, s, dir)
662 g, ok := c.Registry().FindGlobal("wl_output")
663 if !ok {
664 t.Fatal("output missing")
665 }
666 out, err := bindWireID(c, g.Name, g.Interface, 4)
667 if err != nil {
668 t.Fatal(err)
669 }
670 registerProtocol(t, c, out)
671 manager := bindVersion(t, c, "zwlr_screencopy_manager_v1", 3)
672 frame := c.AllocateID()
673 events := &captureEvents{events: make(chan uint16, 8)}
674 events.SetID(frame)
675 registerWireProxy(c, events)
676 requestProtocol(t, c, manager, wlr.ZwlrScreencopyManagerV1RequestCaptureOutput, frame, int32(0), out)
677 if err := c.Roundtrip(); err != nil {
678 t.Fatal(err)
679 }
680 if got := <-events.events; got != uint16(wlr.ZwlrScreencopyFrameV1EventBuffer) {
681 t.Fatal(got)
682 }
683 if got := <-events.events; got != uint16(wlr.ZwlrScreencopyFrameV1EventBufferDone) {
684 t.Fatal(got)
685 }
686 if !destroy {
687 applyOutputs(t, s, commands, ports.SetOutputs{})
688 shm, buf, fd := captureSmallBuffer(t, c)
689 defer unix.Close(fd)
690 _ = shm
691 requestProtocol(t, c, frame, wlr.ZwlrScreencopyFrameV1RequestCopy, buf)
692 if err := c.Roundtrip(); err != nil {
693 t.Fatal(err)
694 }
695 if got := <-events.events; got != uint16(wlr.ZwlrScreencopyFrameV1EventFailed) {
696 t.Fatal(got)
697 }
698 return
699 }
700 _, buf, fd := captureSmallBuffer(t, c)
701 defer unix.Close(fd)
702 requestProtocol(t, c, frame, wlr.ZwlrScreencopyFrameV1RequestCopy, buf)
703 if err := c.Roundtrip(); err != nil {
704 t.Fatal(err)
705 }
706 var req ports.CaptureRequest
707 select {
708 case req = <-requests:
709 case <-time.After(2 * time.Second):
710 t.Fatal("no request")
711 }
712 requestProtocol(t, c, frame, wlr.ZwlrScreencopyFrameV1RequestDestroy)
713 if err := c.Roundtrip(); err != nil {
714 t.Fatal(err)
715 }
716 if !s.display.Do(func() {
717 if _, ok := s.captureReplies[req.ID]; ok {
718 t.Error("reply retained after frame destroy")
719 }
720 }) {
721 t.Fatal("display stopped")
722 }
723 if err := req.Dst.File.Close(); err != nil {
724 t.Fatal(err)
725 }
726 if _, err := req.Dst.File.Stat(); err == nil {
727 t.Fatal("output descriptor open")
728 }
729 deliverCapture(t, s, replies, ports.CaptureDone{ID: req.ID})
730 if err := c.Roundtrip(); err != nil {
731 t.Fatal(err)
732 }
733 })
734 }
735}
736
737func captureSmallBuffer(t *testing.T, c *wlturbo.Display) (uint32, uint32, int) {
738 t.Helper()
739 shm := bindProtocol(t, c, "wl_shm")
740 registerProtocol(t, c, shm)
741 fd, err := unix.MemfdCreate("small-capture", 0)
742 if err != nil {
743 t.Fatal(err)
744 }
745 if err := unix.Ftruncate(fd, 16); err != nil {
746 t.Fatal(err)
747 }
748 pool, buf := c.AllocateID(), c.AllocateID()
749 registerProtocol(t, c, pool)
750 registerProtocol(t, c, buf)
751 if err := wireRequest(c, shm, uint16(wayland.ShmRequestCreatePool), []int{fd}, pool, int32(16)); err != nil {
752 t.Fatal(err)
753 }
754 requestProtocol(t, c, pool, wayland.ShmPoolRequestCreateBuffer, buf, int32(0), int32(2), int32(2), int32(8), uint32(wayland.ShmFormatXrgb8888))
755 return shm, buf, fd
756}