v0.5.0 ccce658

commit_test.go RawHistoryFolder

1package drm
2
3import (
4	"context"
5	"errors"
6	"os"
7	"testing"
8	"time"
9
10	portsmocks "github.com/bnema/neferwl/internal/mocks/ports"
11	"github.com/bnema/neferwl/internal/ports"
12	"github.com/stretchr/testify/mock"
13	"golang.org/x/sys/unix"
14)
15
16// An event queued before a modeset (or for an earlier output on the CRTC)
17// must not complete the commit that follows.
18func TestStaleEventDoesNotCompleteNewCommit(t *testing.T) {
19	o, _, commits := testOutput(t)
20	if err := o.commitFrame(70, nil, false, false, pendingFrame{}); err != nil {
21		t.Fatal(err)
22	}
23	old := eventOf((*commits)[0])
24	o.frame.endPending() // a modeset dropped the commit; its event is queued
25	if err := o.commitFrame(71, nil, false, false, pendingFrame{}); err != nil {
26		t.Fatal(err)
27	}
28	if o.completed(old, nil) || !o.frame.pendingCommit() {
29		t.Fatal("stale event completed the new commit")
30	}
31	if !o.completed(eventOf((*commits)[1]), nil) || o.frame.pendingCommit() {
32		t.Fatal("own event did not complete the commit")
33	}
34}
35
36// After EBUSY no commit of ours is known: the next event of the CRTC ends
37// the wait, then the frame commits.
38func TestBusyFrameCommitsAfterEvent(t *testing.T) {
39	o, _, commits := testOutput(t, unix.EBUSY)
40	enabled := true
41	if err := o.commitFrame(70, nil, false, false, pendingFrame{}); !o.commitFailed(err, &enabled) {
42		t.Fatalf("EBUSY not handled: %v", err)
43	}
44	if !o.completed(flipEvent{crtc: tCrtc, user: 12345 << userKindBits}, nil) || o.frame.pendingCommit() {
45		t.Fatal("EBUSY wait not ended by the CRTC's event")
46	}
47	if err := o.commitFrame(70, nil, false, false, pendingFrame{}); err != nil || len(*commits) != 2 || !o.frame.pendingCommit() {
48		t.Fatalf("frame after EBUSY: %v, %d commits", err, len(*commits))
49	}
50}
51
52// A cursor image change writes only an image the plane neither shows nor
53// is about to show; with both busy it waits for the event.
54func TestCursorImageNeverOverwritesScreen(t *testing.T) {
55	o, _, _ := testOutput(t)
56	c := o.cursor
57	r := portsmocks.NewMockRenderer(t)
58	var written []int
59	r.EXPECT().WriteCursor(mock.Anything, mock.Anything, 1, 1).RunAndReturn(func(i int, _ []byte, _, _ int) error {
60		written = append(written, i)
61		return nil
62	})
63	px := []byte{1, 2, 3, 4}
64	// Image 0 on screen: two changes before a commit both go to image 1.
65	c.screen, c.cur, c.image = 90, 0, true
66	for range 2 {
67		if err := c.setImage(r, px, 1, 1, 0, 0); err != nil {
68			t.Fatal(err)
69		}
70	}
71	if len(written) != 2 || written[0] != 1 || written[1] != 1 {
72		t.Fatalf("written %v", written)
73	}
74	// Image 1 in flight, image 0 on screen: nothing is free.
75	c.committed(cursorState{on: true, fb: 91})
76	if err := c.setImage(r, px, 1, 1, 0, 0); err != nil || len(written) != 2 || c.later == nil {
77		t.Fatalf("wrote a busy image: %v %v", err, written)
78	}
79	// The flip frees image 0.
80	c.landed()
81	if changed, err := c.flushLater(r); !changed || err != nil || len(written) != 3 || written[2] != 0 || c.cur != 0 {
82		t.Fatalf("after flip: %v %v %v cur %d", changed, err, written, c.cur)
83	}
84}
85
86// KMS refusing the cursor plane turns the hardware cursor off and the
87// frame commits without it; the output keeps running.
88func TestCursorRefusedFrameCommitsWithout(t *testing.T) {
89	o, _, commits := testOutput(t, unix.EINVAL, unix.EINVAL)
90	o.cursor.image = true
91	o.cursor.Move(5, 5)
92	if err := o.commitFrame(70, nil, false, false, pendingFrame{}); err != nil {
93		t.Fatal(err)
94	}
95	if !o.cursor.off || len(*commits) != 3 || (*commits)[1].flags != atomicTestOnly {
96		t.Fatalf("off=%v commits %d", o.cursor.off, len(*commits))
97	}
98	if v, _ := (*commits)[2].req.value(tCursor, pFB); v != 0 {
99		t.Fatalf("retry shows cursor fb %d", v)
100	}
101}
102
103// A refused cursor is not blamed on the client buffer.
104func TestCursorRefusedDoesNotBlameScanoutBuffer(t *testing.T) {
105	o, _, _ := testOutput(t, unix.EINVAL, unix.EINVAL)
106	o.cursor.image = true
107	o.cursor.Move(5, 5)
108	o.clientFBs[9] = &clientFB{fbID: 80}
109	direct, err := o.commitScanout(80, ports.SurfaceContent{DMABuf: &ports.DMABuf{ID: 9}}, pendingFrame{})
110	if err != nil || !direct || o.clientFBs[9].failed != "" {
111		t.Fatalf("direct=%v err=%v failed=%q", direct, err, o.clientFBs[9].failed)
112	}
113}
114
115// tiledMod stands for a non-linear driver modifier in the image tests.
116const tiledMod = 0x200000000401b03
117
118// showImages validates the images with a TEST_ONLY modeset, retries with
119// single-plane driver images, then falls back to linear images when KMS
120// refuses both.
121func TestShowImagesTestsBeforeModeset(t *testing.T) {
122	o, k, commits := testOutput(t, unix.EINVAL, unix.EINVAL)
123	o.cursor = nil
124	r := portsmocks.NewMockRenderer(t)
125	buf := func() ports.DMABuf {
126		var b ports.DMABuf
127		for range 2 {
128			f, w, _ := os.Pipe()
129			w.Close()
130			b.Planes = append(b.Planes, ports.DMABufPlane{File: f})
131		}
132		b.Modifier = tiledMod
133		return b
134	}
135	linear := func() ports.DMABuf {
136		f, w, _ := os.Pipe()
137		w.Close()
138		return ports.DMABuf{Planes: []ports.DMABufPlane{{File: f}}}
139	}
140	tiled1 := func() ports.DMABuf {
141		b := linear()
142		b.Modifier = tiledMod
143		return b
144	}
145	r.EXPECT().SetHDR(float64(0)).Return().Maybe()
146	r.EXPECT().ExportTargets(2, []uint64(nil), false).Return([]ports.DMABuf{buf(), buf()}, nil).Once()
147	r.EXPECT().ExportTargets(2, []uint64(nil), true).Return([]ports.DMABuf{tiled1(), tiled1()}, nil).Once()
148	r.EXPECT().ExportTargets(2, []uint64{0}, false).Return([]ports.DMABuf{linear(), linear()}, nil).Once()
149	r.EXPECT().UseTarget(mock.Anything).Return()
150	r.EXPECT().Render(mock.Anything, mock.Anything).Return(nil, nil)
151	k.EXPECT().addFB(mock.Anything, uint32(fourccXRGB)).Return(70, nil)
152	k.EXPECT().rmFB(mock.Anything).Return(nil)
153	k.EXPECT().createBlob(mock.Anything).Return(99, nil)
154	k.EXPECT().destroyBlob(mock.Anything).Return(nil)
155	if err := o.showImages(r, imagesDriver, nil); err != nil {
156		t.Fatal(err)
157	}
158	want := []uint32{atomicTestOnly | atomicAllowModes, atomicTestOnly | atomicAllowModes, atomicTestOnly | atomicAllowModes, atomicAllowModes}
159	if len(*commits) != len(want) {
160		t.Fatalf("%d commits", len(*commits))
161	}
162	for i, f := range want {
163		if (*commits)[i].flags != f {
164			t.Fatalf("commit %d flags %#x want %#x", i, (*commits)[i].flags, f)
165		}
166	}
167	if o.kind != imagesLinear {
168		t.Fatalf("kind %d", o.kind)
169	}
170}
171
172// A refused multi-plane (DCC) driver image is retried with single-plane
173// driver images before linear; every plane file of every buffer is closed
174// once the framebuffers exist.
175func TestShowImagesRetriesSinglePlaneBeforeLinear(t *testing.T) {
176	o, k, _ := testOutput(t, unix.EINVAL)
177	o.cursor = nil
178	var files []*os.File
179	buf := func(planes int) ports.DMABuf {
180		b := ports.DMABuf{Modifier: tiledMod}
181		for range planes {
182			f, w, _ := os.Pipe()
183			w.Close()
184			files = append(files, f)
185			b.Planes = append(b.Planes, ports.DMABufPlane{File: f})
186		}
187		return b
188	}
189	r := portsmocks.NewMockRenderer(t)
190	r.EXPECT().SetHDR(float64(0)).Return().Maybe()
191	r.EXPECT().ExportTargets(2, []uint64(nil), false).Return([]ports.DMABuf{buf(3), buf(3)}, nil).Once()
192	r.EXPECT().ExportTargets(2, []uint64(nil), true).Return([]ports.DMABuf{buf(1), buf(1)}, nil).Once()
193	r.EXPECT().UseTarget(mock.Anything).Return()
194	r.EXPECT().Render(mock.Anything, mock.Anything).Return(nil, nil)
195	k.EXPECT().addFB(mock.Anything, uint32(fourccXRGB)).Return(70, nil)
196	k.EXPECT().rmFB(mock.Anything).Return(nil)
197	k.EXPECT().createBlob(mock.Anything).Return(99, nil)
198	k.EXPECT().destroyBlob(mock.Anything).Return(nil)
199	if err := o.showImages(r, imagesDriver, nil); err != nil {
200		t.Fatal(err)
201	}
202	if o.kind != imagesSinglePlane {
203		t.Fatalf("kind %d", o.kind)
204	}
205	if len(files) != 8 {
206		t.Fatalf("%d plane files", len(files))
207	}
208	for i, f := range files {
209		if err := f.Close(); !errors.Is(err, os.ErrClosed) {
210			t.Fatalf("plane file %d left open (close: %v)", i, err)
211		}
212	}
213}
214
215// SDR images are asked for the modifiers the primary plane lists for
216// XRGB8888, in IN_FORMATS order, never the renderer's own choice: RADV's
217// preferred DCC variant (pipe-aligned) is not one amdgpu lists, and KMS
218// refuses a framebuffer with a modifier the plane does not advertise
219// (ADDFB2: EINVAL). Other formats' modifiers are not offered.
220func TestShowImagesOffersThePrimaryPlanesXRGBModifiers(t *testing.T) {
221	const dccListed, tiled, other = 0x200000000563b03, 0x200000000401b03, 0x200000000401903
222	o, k, _ := testOutput(t)
223	o.cursor = nil
224	o.primary.formats = []ports.DMABufFormat{
225		{Format: fourccXRGB, Modifier: dccListed}, {Format: fourccXR30, Modifier: other},
226		{Format: fourccXRGB, Modifier: tiled}, {Format: fourccXRGB, Modifier: 0},
227	}
228	buf := func() ports.DMABuf {
229		f, w, _ := os.Pipe()
230		w.Close()
231		return ports.DMABuf{Planes: []ports.DMABufPlane{{File: f}}}
232	}
233	r := portsmocks.NewMockRenderer(t)
234	r.EXPECT().SetHDR(float64(0)).Return().Maybe()
235	r.EXPECT().ExportTargets(2, []uint64{dccListed, tiled, 0}, false).Return([]ports.DMABuf{buf(), buf()}, nil).Once()
236	r.EXPECT().UseTarget(mock.Anything).Return()
237	r.EXPECT().Render(mock.Anything, mock.Anything).Return(nil, nil)
238	k.EXPECT().addFB(mock.Anything, uint32(fourccXRGB)).Return(70, nil)
239	k.EXPECT().rmFB(mock.Anything).Return(nil).Maybe()
240	k.EXPECT().createBlob(mock.Anything).Return(99, nil)
241	k.EXPECT().destroyBlob(mock.Anything).Return(nil)
242	if err := o.showImages(r, imagesDriver, nil); err != nil {
243		t.Fatal(err)
244	}
245	if o.kind != imagesDriver {
246		t.Fatalf("kind %d", o.kind)
247	}
248}
249
250// When the renderer has no modifier in common with the plane, the images
251// fall back to single-plane ones with the same list, then to linear.
252func TestShowImagesNoCommonModifierFallsBackWithThePlanesList(t *testing.T) {
253	const listed = 0x200000000563b03
254	o, k, _ := testOutput(t)
255	o.cursor = nil
256	o.primary.formats = []ports.DMABufFormat{{Format: fourccXRGB, Modifier: listed}, {Format: fourccXRGB, Modifier: 0}}
257	buf := func() ports.DMABuf {
258		f, w, _ := os.Pipe()
259		w.Close()
260		return ports.DMABuf{Planes: []ports.DMABufPlane{{File: f}}}
261	}
262	r := portsmocks.NewMockRenderer(t)
263	r.EXPECT().SetHDR(float64(0)).Return().Maybe()
264	none := errors.New("no XRGB8888 modifier both the device and the display accept")
265	r.EXPECT().ExportTargets(2, []uint64{listed, 0}, false).Return(nil, none).Once()
266	r.EXPECT().ExportTargets(2, []uint64{listed, 0}, true).Return([]ports.DMABuf{buf(), buf()}, nil).Once()
267	r.EXPECT().UseTarget(mock.Anything).Return()
268	r.EXPECT().Render(mock.Anything, mock.Anything).Return(nil, nil)
269	k.EXPECT().addFB(mock.Anything, uint32(fourccXRGB)).Return(70, nil)
270	k.EXPECT().rmFB(mock.Anything).Return(nil).Maybe()
271	k.EXPECT().createBlob(mock.Anything).Return(99, nil)
272	k.EXPECT().destroyBlob(mock.Anything).Return(nil)
273	if err := o.showImages(r, imagesDriver, nil); err != nil {
274		t.Fatal(err)
275	}
276	if o.kind != imagesSinglePlane {
277		t.Fatalf("kind %d", o.kind)
278	}
279}
280
281// A plane without IN_FORMATS lists every format with modifier 0 only
282// (readPlanes): the images are linear straight away, with no driver or
283// single-plane attempt KMS would refuse.
284func TestShowImagesLinearOnlyPlaneExportsLinearFirst(t *testing.T) {
285	o, k, commits := testOutput(t)
286	o.cursor = nil
287	o.primary.formats = []ports.DMABufFormat{{Format: fourccXRGB, Modifier: 0}}
288	r := portsmocks.NewMockRenderer(t)
289	r.EXPECT().SetHDR(float64(0)).Return().Maybe()
290	// No ExportTargets(..., true) and no second call: the mock fails on them.
291	r.EXPECT().ExportTargets(2, []uint64{0}, false).RunAndReturn(func(int, []uint64, bool) ([]ports.DMABuf, error) {
292		return []ports.DMABuf{linearBuf(), linearBuf()}, nil
293	}).Once()
294	r.EXPECT().UseTarget(mock.Anything).Return()
295	r.EXPECT().Render(mock.Anything, mock.Anything).Return(nil, nil)
296	k.EXPECT().addFB(mock.Anything, uint32(fourccXRGB)).Return(70, nil)
297	k.EXPECT().createBlob(mock.Anything).Return(99, nil)
298	k.EXPECT().destroyBlob(mock.Anything).Return(nil)
299	if err := o.showImages(r, imagesDriver, nil); err != nil {
300		t.Fatal(err)
301	}
302	if o.kind != imagesDriver || o.imageMod != 0 || len(*commits) != 2 {
303		t.Fatalf("kind %d, modifier %#x, %d commits", o.kind, o.imageMod, len(*commits))
304	}
305}
306
307// KMS refusing images that are already linear ends the attempt with that
308// error: the single-plane and linear kinds would export the same images.
309func TestShowImagesRefusedLinearImagesAreNotExportedAgain(t *testing.T) {
310	o, k, commits := testOutput(t, unix.EINVAL)
311	o.cursor = nil
312	o.primary.formats = []ports.DMABufFormat{{Format: fourccXRGB, Modifier: 0}}
313	r := portsmocks.NewMockRenderer(t)
314	r.EXPECT().SetHDR(float64(0)).Return().Maybe()
315	r.EXPECT().ExportTargets(2, []uint64{0}, false).RunAndReturn(func(int, []uint64, bool) ([]ports.DMABuf, error) {
316		return []ports.DMABuf{linearBuf(), linearBuf()}, nil
317	}).Once()
318	r.EXPECT().UseTarget(mock.Anything).Return()
319	r.EXPECT().Render(mock.Anything, mock.Anything).Return(nil, nil)
320	k.EXPECT().addFB(mock.Anything, uint32(fourccXRGB)).Return(70, nil)
321	k.EXPECT().rmFB(mock.Anything).Return(nil)
322	k.EXPECT().createBlob(mock.Anything).Return(99, nil)
323	k.EXPECT().destroyBlob(mock.Anything).Return(nil)
324	err := o.showImages(r, imagesDriver, nil)
325	if !errors.Is(err, unix.EINVAL) {
326		t.Fatalf("error %v, want the KMS refusal", err)
327	}
328	if len(*commits) != 1 || o.fbs != [2]uint32{} {
329		t.Fatalf("%d commits, fbs %v", len(*commits), o.fbs)
330	}
331}
332
333// linearBuf is a one-plane linear dmabuf over a closed pipe.
334func linearBuf() ports.DMABuf {
335	f, w, _ := os.Pipe()
336	w.Close()
337	return ports.DMABuf{Planes: []ports.DMABufPlane{{File: f}}}
338}
339
340// Refused driver images that already had one plane are not retried as
341// single-plane images: the next export is linear.
342func TestShowImagesSkipsSinglePlaneWhenDriverImagesHadOnePlane(t *testing.T) {
343	o, k, commits := testOutput(t, unix.EINVAL)
344	o.cursor = nil
345	buf := func() ports.DMABuf {
346		f, w, _ := os.Pipe()
347		w.Close()
348		return ports.DMABuf{Modifier: tiledMod, Planes: []ports.DMABufPlane{{File: f}}}
349	}
350	linear := func() ports.DMABuf {
351		f, w, _ := os.Pipe()
352		w.Close()
353		return ports.DMABuf{Planes: []ports.DMABufPlane{{File: f}}}
354	}
355	r := portsmocks.NewMockRenderer(t)
356	r.EXPECT().SetHDR(float64(0)).Return().Maybe()
357	r.EXPECT().ExportTargets(2, []uint64(nil), false).Return([]ports.DMABuf{buf(), buf()}, nil).Once()
358	r.EXPECT().ExportTargets(2, []uint64{0}, false).Return([]ports.DMABuf{linear(), linear()}, nil).Once()
359	r.EXPECT().UseTarget(mock.Anything).Return()
360	r.EXPECT().Render(mock.Anything, mock.Anything).Return(nil, nil)
361	k.EXPECT().addFB(mock.Anything, uint32(fourccXRGB)).Return(70, nil)
362	k.EXPECT().rmFB(mock.Anything).Return(nil)
363	k.EXPECT().createBlob(mock.Anything).Return(99, nil)
364	k.EXPECT().destroyBlob(mock.Anything).Return(nil)
365	if err := o.showImages(r, imagesDriver, nil); err != nil {
366		t.Fatal(err)
367	}
368	if o.kind != imagesLinear || len(*commits) != 3 {
369		t.Fatalf("kind %d, %d commits", o.kind, len(*commits))
370	}
371}
372
373// Enabling an output whose images were not made yet (started switched
374// away) tests them and modesets once.
375func TestEnableAfterStartAwayModesetsOnce(t *testing.T) {
376	o, k, commits, commitMu := testOutputMu(t)
377	o.cursor, o.tearing, o.fbs = nil, false, [2]uint32{}
378	r := portsmocks.NewMockRenderer(t)
379	buf := func() ports.DMABuf {
380		f, w, _ := os.Pipe()
381		w.Close()
382		return ports.DMABuf{Planes: []ports.DMABufPlane{{File: f}}}
383	}
384	r.EXPECT().SetHDR(float64(0)).Return().Maybe()
385	r.EXPECT().ExportTargets(2, mock.Anything, false).Return([]ports.DMABuf{buf(), buf()}, nil).Once()
386	r.EXPECT().UseTarget(mock.Anything).Return()
387	r.EXPECT().Render(mock.Anything, mock.Anything).Return(nil, nil)
388	r.EXPECT().Close().Return().Once()
389	k.EXPECT().addFB(mock.Anything, uint32(fourccXRGB)).Return(70, nil).Twice()
390	k.EXPECT().createBlob(mock.Anything).Return(99, nil)
391	k.EXPECT().destroyBlob(mock.Anything).Return(nil)
392	k.EXPECT().rmFB(mock.Anything).Return(nil).Maybe()
393	active := make(chan bool, 1)
394	active <- false
395	ctx, cancel := context.WithCancel(context.Background())
396	done := make(chan error, 1)
397	go func() {
398		done <- o.Run(ctx, func(int, int) (ports.Renderer, error) { return r, nil }, nil, active, nil, nil, nil, make(chan ports.OutputPresented, 1), nil, nil)
399	}()
400	count := func(flags uint32) int {
401		commitMu.Lock()
402		defer commitMu.Unlock()
403		n := 0
404		for _, c := range *commits {
405			if c.flags == flags {
406				n++
407			}
408		}
409		return n
410	}
411	time.Sleep(20 * time.Millisecond)
412	if n := count(atomicAllowModes); n != 0 {
413		t.Fatalf("%d modesets while away", n)
414	}
415	active <- true
416	waitFor(t, func() bool { return count(atomicAllowModes) == 1 })
417	cancel()
418	if err := <-done; err != nil {
419		t.Fatal(err)
420	}
421	if n := count(atomicTestOnly | atomicAllowModes); n != 1 {
422		t.Fatalf("%d test modesets", n)
423	}
424}
425
426// Close without a mode to restore turns the CRTC off.
427func TestCloseWithoutSavedModeDisablesCrtc(t *testing.T) {
428	o, k, commits := testOutput(t)
429	k.EXPECT().rmFB(mock.Anything).Return(nil)
430	o.Close()
431	c := (*commits)[0]
432	for _, p := range []struct{ obj, prop uint32 }{{tCrtc, pActive}, {tCrtc, pMode}, {tPrimary, pFB}, {tConn, pConnCrtc}} {
433		if v, ok := c.req.value(p.obj, p.prop); !ok || v != 0 {
434			t.Fatalf("object %d prop %d = %d %v", p.obj, p.prop, v, ok)
435		}
436	}
437}
438
439// Close restores the saved mode and gives the connector its CRTC back.
440func TestCloseRestoresSavedMode(t *testing.T) {
441	o, k, commits := testOutput(t)
442	o.saved = modeCrtc{crtcID: tCrtc, fbID: 5, modeValid: 1, mode: modeInfo{HDisplay: 200, VDisplay: 100}}
443	k.EXPECT().createBlob(mock.Anything).Return(98, nil).Once()
444	k.EXPECT().destroyBlob(uint32(98)).Return(nil).Once()
445	k.EXPECT().rmFB(mock.Anything).Return(nil)
446	o.Close()
447	c := (*commits)[0]
448	if v, _ := c.req.value(tConn, pConnCrtc); v != tCrtc {
449		t.Fatalf("connector crtc %d", v)
450	}
451	if v, _ := c.req.value(tPrimary, pFB); v != 5 {
452		t.Fatalf("primary fb %d", v)
453	}
454}
455
456// With the cursor off no image is loaded (no warning per change).
457func TestCursorOffSkipsLoads(t *testing.T) {
458	o, _, _ := testOutput(t)
459	o.cursor.off = true
460	loaded := false
461	o.setCursor(nil, func(ports.CursorChange, float64, ports.BufferTransform, int) (ports.CursorImage, error) {
462		loaded = true
463		return ports.CursorImage{}, nil
464	}, ports.CursorChange{}, 1, 0)
465	if loaded || o.cursor.desired().on {
466		t.Fatal("cursor used while off")
467	}
468}
469
470// Content read while only a cursor commit is in flight is reported when
471// that commit's event arrives, not held until a later frame.
472func TestRunReportsSeenAfterCursorCommit(t *testing.T) {
473	o, k, commits, commitMu := testOutputMu(t)
474	o.tearing, o.cursor.fbs = false, [2]uint32{}
475	flips := make(chan flipEvent, 1)
476	o.flipped = flips
477	r := portsmocks.NewMockRenderer(t)
478	buf := func() ports.DMABuf {
479		f, w, _ := os.Pipe()
480		w.Close()
481		return ports.DMABuf{Planes: []ports.DMABufPlane{{File: f}}}
482	}
483	r.EXPECT().CursorBuffers(64).Return([2]ports.DMABuf{buf(), buf()}, nil).Once()
484	r.EXPECT().WriteCursor(mock.Anything, mock.Anything, 1, 1).Return(nil)
485	r.EXPECT().SetHDR(float64(0)).Return().Maybe()
486	r.EXPECT().ExportTargets(2, mock.Anything, false).Return([]ports.DMABuf{buf(), buf()}, nil).Once()
487	r.EXPECT().UseTarget(mock.Anything).Return()
488	r.EXPECT().Render(mock.Anything, mock.Anything).Return(nil, nil)
489	r.EXPECT().Close().Return().Once()
490	k.EXPECT().addFB(mock.Anything, uint32(fourccARGB)).Return(90, nil).Once()
491	k.EXPECT().addFB(mock.Anything, uint32(fourccARGB)).Return(91, nil).Once()
492	k.EXPECT().addFB(mock.Anything, uint32(fourccXRGB)).Return(70, nil).Twice()
493	k.EXPECT().createBlob(mock.Anything).Return(99, nil)
494	k.EXPECT().destroyBlob(mock.Anything).Return(nil).Maybe()
495	k.EXPECT().rmFB(mock.Anything).Return(nil).Maybe()
496	load := func(ports.CursorChange, float64, ports.BufferTransform, int) (ports.CursorImage, error) {
497		return ports.CursorImage{Pixels: []byte{1, 2, 3, 4}, W: 1, H: 1}, nil
498	}
499	last := func(kind uint64) (commitRec, bool) {
500		commitMu.Lock()
501		defer commitMu.Unlock()
502		for i := len(*commits) - 1; i >= 0; i-- {
503			if c := (*commits)[i]; c.user&3 == kind {
504				return c, true
505			}
506		}
507		return commitRec{}, false
508	}
509	scenes := make(chan ports.Scene, 1)
510	contents := make(chan ports.SurfaceContent, 1)
511	presented := make(chan ports.OutputPresented, 8)
512	ctx, cancel := context.WithCancel(context.Background())
513	done := make(chan error, 1)
514	go func() {
515		done <- o.Run(ctx, func(int, int) (ports.Renderer, error) { return r, nil }, load, make(chan bool), scenes, contents, nil, presented, nil, nil)
516	}()
517	scenes <- ports.Scene{OutputWidth: 200, OutputHeight: 100, Scale: 1}
518	var frame commitRec
519	waitFor(t, func() bool { var ok bool; frame, ok = last(userFrame); return ok })
520	flips <- eventOf(frame)
521	<-presented // the frame's flip
522	o.cursor.Move(3, 3)
523	var cur commitRec
524	waitFor(t, func() bool { var ok bool; cur, ok = last(userState); return ok })
525	// A window this output does not show commits while the cursor flies.
526	contents <- ports.SurfaceContent{ID: 7, Seq: 2, SHM: &ports.SHMBuffer{}}
527	time.Sleep(20 * time.Millisecond)
528	flips <- eventOf(cur)
529	select {
530	case p := <-presented:
531		if p.Seen[7] != 2 {
532			t.Fatalf("report %+v", p)
533		}
534	case <-time.After(time.Second):
535		t.Fatal("Seen not reported after the cursor commit")
536	}
537	cancel()
538	if err := <-done; err != nil {
539		t.Fatal(err)
540	}
541}
542
543// Frame fences are closed on every path: flipped, EBUSY (re-rendered
544// after the event) and lost master (re-rendered after the modeset). The
545// fd count is unchanged after many frames.
546func TestRunClosesFrameFences(t *testing.T) {
547	fds := func() int {
548		ents, err := os.ReadDir("/proc/self/fd")
549		if err != nil {
550			t.Skip(err)
551		}
552		return len(ents)
553	}
554	const rounds = 30
555	// Commit results: modeset test and modeset, then per round
556	// EBUSY + flip, or lost master + modeset + flip, or a plain flip.
557	errs := []error{nil, nil}
558	for i := range rounds {
559		switch i % 3 {
560		case 0:
561			errs = append(errs, unix.EBUSY, nil)
562		case 1:
563			errs = append(errs, unix.EACCES, nil, nil)
564		default:
565			errs = append(errs, nil)
566		}
567	}
568	o, k, commits, commitMu := testOutputMu(t, errs...)
569	o.cursor, o.tearing, o.fbs = nil, false, [2]uint32{}
570	flips := make(chan flipEvent, 1)
571	o.flipped = flips
572	r := portsmocks.NewMockRenderer(t)
573	buf := func() ports.DMABuf {
574		f, w, _ := os.Pipe()
575		w.Close()
576		return ports.DMABuf{Planes: []ports.DMABufPlane{{File: f}}}
577	}
578	r.EXPECT().SetHDR(float64(0)).Return().Maybe()
579	r.EXPECT().ExportTargets(2, mock.Anything, false).Return([]ports.DMABuf{buf(), buf()}, nil).Once()
580	r.EXPECT().UseTarget(mock.Anything).Return()
581	r.EXPECT().Render(mock.Anything, mock.Anything).RunAndReturn(func(ports.Scene, map[ports.WindowID]ports.SurfaceContent) (*os.File, error) {
582		f, w, _ := os.Pipe()
583		_, _ = w.Write([]byte{1})
584		w.Close()
585		return f, nil
586	})
587	r.EXPECT().Close().Return().Once()
588	k.EXPECT().addFB(mock.Anything, uint32(fourccXRGB)).Return(70, nil).Twice()
589	k.EXPECT().createBlob(mock.Anything).Return(99, nil)
590	k.EXPECT().destroyBlob(mock.Anything).Return(nil)
591	k.EXPECT().rmFB(mock.Anything).Return(nil).Maybe()
592	before := fds()
593	active := make(chan bool, 1)
594	scenes := make(chan ports.Scene, 1)
595	presented := make(chan ports.OutputPresented, 4*rounds)
596	ctx, cancel := context.WithCancel(context.Background())
597	done := make(chan error, 1)
598	go func() {
599		done <- o.Run(ctx, func(int, int) (ports.Renderer, error) { return r, nil }, nil, active, scenes, nil, nil, presented, nil, nil)
600	}()
601	count := func() int {
602		commitMu.Lock()
603		defer commitMu.Unlock()
604		return len(*commits)
605	}
606	last := func() commitRec {
607		commitMu.Lock()
608		defer commitMu.Unlock()
609		return (*commits)[len(*commits)-1]
610	}
611	waitFor(t, func() bool { return count() == 2 })
612	for i := range rounds {
613		n := count()
614		scenes <- ports.Scene{Seq: uint64(i)}
615		switch i % 3 {
616		case 0:
617			waitFor(t, func() bool { return count() == n+1 })
618			flips <- flipEvent{crtc: tCrtc} // ends the EBUSY wait
619			waitFor(t, func() bool { return count() == n+2 })
620		case 1:
621			waitFor(t, func() bool { return count() == n+1 })
622			active <- true
623			waitFor(t, func() bool { return count() == n+3 })
624		default:
625			waitFor(t, func() bool { return count() == n+1 })
626		}
627		flips <- eventOf(last())
628	}
629	cancel()
630	if err := <-done; err != nil {
631		t.Fatal(err)
632	}
633	if after := fds(); after > before {
634		t.Fatalf("fds %d -> %d", before, after)
635	}
636}

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