v0.2.0 13c6a90

screencopy_test.go RawHistoryFolder

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

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 23

Tags 10

↑↓ moveenter openesc close