refactor/kvconf 3462c76

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/adapters/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}

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