main f2d166b

commit_test.go RawHistoryFolder

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

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 25

Tags 10

↑↓ moveenter openesc close