feat/hints a471fa4

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

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 26

Tags 10

↑↓ moveenter openesc close