main f2d166b

scanout_test.go RawHistoryFolder

1package drm
2
3import (
4	"os"
5	"testing"
6	"time"
7
8	"github.com/bnema/neferwl/internal/ports"
9	"github.com/stretchr/testify/mock"
10)
11
12func TestScanoutCandidate(t *testing.T) {
13	full := ports.SceneWindow{ID: 1, Rect: ports.Rect{W: 100, H: 50}, Fullscreen: true}
14	scene := ports.Scene{OutputWidth: 100, OutputHeight: 50, Windows: []ports.SceneWindow{full}}
15	buf := &ports.DMABuf{ID: 9, Width: 200, Height: 100, Format: fourccARGB, Planes: []ports.DMABufPlane{{}}}
16	game := ports.SurfaceContent{ID: 1, Width: 200, Height: 100, LogicalW: 100, LogicalH: 50, DMABuf: buf}
17	for _, tc := range []struct {
18		name   string
19		scene  func(s *ports.Scene)
20		cont   func(c *ports.SurfaceContent)
21		reason string
22	}{
23		{"scanout", nil, nil, ""},
24		{"hidden others ignored", func(s *ports.Scene) {
25			s.Windows = append(s.Windows, ports.SceneWindow{ID: 2, Hidden: true})
26		}, nil, ""},
27		{"no window", func(s *ports.Scene) { s.Windows = nil }, nil, "no_fullscreen"},
28		{"rotated output", func(s *ports.Scene) { s.Transform = 1 }, nil, "output_transform"},
29		{"dimmed", func(s *ports.Scene) { s.Windows[0].Dim = 0.5 }, nil, "dim"},
30		{"rounded", func(s *ports.Scene) { s.Windows[0].Radius = 10 }, nil, "window_fade"},
31		{"window fade", func(s *ports.Scene) { s.Windows[0].Fade = 0.5 }, nil, "window_fade"},
32		{"window preview", func(s *ports.Scene) { s.Windows[0].Preview = 0.9 }, nil, "window_fade"},
33		{"window zoom", func(s *ports.Scene) { s.Windows[0].Zoom = 0.9 }, nil, "window_fade"},
34		{"faded", nil, func(c *ports.SurfaceContent) { c.Fade = 0.5 }, "fade"},
35		{"tiled", func(s *ports.Scene) { s.Windows[0].Fullscreen = false }, nil, "other_windows"},
36		{"float above", func(s *ports.Scene) {
37			s.Windows = append(s.Windows, ports.SceneWindow{ID: 2, Rect: ports.Rect{W: 10, H: 10}})
38		}, nil, "other_windows"},
39		{"not covering", func(s *ports.Scene) { s.Windows[0].Rect.W = 90 }, nil, "other_windows"},
40		{"overlay layer", func(s *ports.Scene) {
41			s.Layers = []ports.SceneLayer{{ID: 3, Layer: ports.LayerOverlay, Rect: ports.Rect{W: 10, H: 10}}}
42		}, nil, "overlay_surface"},
43		{"top layer", func(s *ports.Scene) {
44			s.Layers = []ports.SceneLayer{{ID: 3, Layer: ports.LayerTop, Rect: ports.Rect{W: 10, H: 10}}}
45		}, nil, "overlay_surface"},
46		{"background layer", func(s *ports.Scene) {
47			s.Layers = []ports.SceneLayer{{ID: 3, Layer: ports.LayerBackground, Rect: ports.Rect{W: 10, H: 10}}}
48		}, nil, ""},
49		{"shm", nil, func(c *ports.SurfaceContent) { c.DMABuf, c.SHM = nil, &ports.SHMBuffer{} }, "not_dmabuf"},
50		{"subsurface", nil, func(c *ports.SurfaceContent) { c.Children = []ports.Subsurface{{}} }, "subsurfaces"},
51		{"buffer transform", nil, func(c *ports.SurfaceContent) { c.Transform = 2 }, "buffer_transform"},
52		{"scaled", nil, func(c *ports.SurfaceContent) { c.Width = 100 }, ""},
53		{"cropped", nil, func(c *ports.SurfaceContent) { c.Geometry = ports.Rect{X: 5, W: 90, H: 50} }, "geometry_crop"},
54		{"client ignores scale", nil, func(c *ports.SurfaceContent) { c.LogicalW = 200 }, "letterbox"},
55		{"whole geometry", nil, func(c *ports.SurfaceContent) { c.Geometry = ports.Rect{W: 100, H: 50} }, ""},
56		{"wine client subsurface", nil, wineTree(nil), ""},
57		{"client subsurface translucent", nil, wineTree(func(ch *ports.Subsurface) { ch.Opaque = false }), "subsurface_translucent"},
58		{"client subsurface shm", nil, wineTree(func(ch *ports.Subsurface) { ch.DMABuf, ch.SHM = nil, &ports.SHMBuffer{} }), "not_dmabuf"},
59		{"client subsurface offset", nil, wineTree(func(ch *ports.Subsurface) { ch.X = 4 }), "subsurface_offset"},
60		{"client subsurface below root", nil, wineTree(func(ch *ports.Subsurface) { ch.Below = true }), "subsurface_below"},
61		{"client subsurface cropped", nil, wineTree(func(ch *ports.Subsurface) { ch.Source = [4]float32{0, 0, 100, 50} }), ""},
62		{"client subsurface full source", nil, wineTree(func(ch *ports.Subsurface) { ch.Source = [4]float32{0, 0, 200, 100} }), ""},
63		{"root cropped", nil, func(c *ports.SurfaceContent) { c.Source = [4]float32{10, 0, 190, 100} }, ""},
64		{"client subsurface scaled", nil, wineTree(func(ch *ports.Subsurface) { ch.Width = 100 }), ""},
65		{"client subsurface small", nil, wineTree(func(ch *ports.Subsurface) { ch.LogicalW = 90 }), "letterbox"},
66		{"two subsurfaces", nil, func(c *ports.SurfaceContent) {
67			wineTree(nil)(c)
68			c.Children = append(c.Children, c.Children[0])
69		}, "subsurfaces"},
70	} {
71		s := scene
72		s.Windows = append([]ports.SceneWindow(nil), scene.Windows...)
73		c := game
74		if tc.scene != nil {
75			tc.scene(&s)
76		}
77		if tc.cont != nil {
78			tc.cont(&c)
79		}
80		if _, reason := scanoutCandidate(s, map[ports.WindowID]ports.SurfaceContent{1: c}, 200, 100, nil); reason != tc.reason {
81			t.Errorf("%s: reason %q, want %q", tc.name, reason, tc.reason)
82		}
83	}
84}
85
86// wineTree turns the test content into Wine's layout: the root is a wl_shm
87// frame and the game is one opaque GPU subsurface over the client area.
88func wineTree(edit func(ch *ports.Subsurface)) func(c *ports.SurfaceContent) {
89	return func(c *ports.SurfaceContent) {
90		ch := ports.Subsurface{SurfaceContent: *c}
91		ch.Opaque = true
92		if edit != nil {
93			edit(&ch)
94		}
95		c.DMABuf, c.SHM = nil, &ports.SHMBuffer{}
96		c.Children = []ports.Subsurface{ch}
97	}
98}
99
100// Wine's game subsurface is scanned out in place of the window: the
101// content keeps the window's ID and Seq and takes the subsurface buffer.
102func TestScanoutCandidateClientSubsurface(t *testing.T) {
103	scene := ports.Scene{OutputWidth: 100, OutputHeight: 50, Windows: []ports.SceneWindow{{ID: 1, Rect: ports.Rect{W: 100, H: 50}, Fullscreen: true}}}
104	game := &ports.DMABuf{ID: 9, Width: 200, Height: 100, Format: fourccXRGB, Planes: []ports.DMABufPlane{{}}}
105	acquire := &os.File{}
106	root := ports.SurfaceContent{ID: 1, Seq: 7, Width: 200, Height: 100, LogicalW: 100, LogicalH: 50, SHM: &ports.SHMBuffer{},
107		Geometry: ports.Rect{W: 100, H: 50},
108		Children: []ports.Subsurface{{SurfaceContent: ports.SurfaceContent{Seq: 3, Width: 200, Height: 100, LogicalW: 100, LogicalH: 50, Opaque: true, DMABuf: game, Async: true, Acquire: acquire}}}}
109	c, reason := scanoutCandidate(scene, map[ports.WindowID]ports.SurfaceContent{1: root}, 200, 100, nil)
110	if reason != "" || c.DMABuf != game || c.ID != 1 || c.Seq != 7 || !c.Async || c.Acquire != acquire || c.Geometry != (ports.Rect{}) {
111		t.Fatalf("candidate %+v, reason %q", c, reason)
112	}
113
114	// A single-pixel root is hidden by the opaque dmabuf over it, like an shm
115	// one; alone it is not a dmabuf.
116	solidRoot := root
117	solidRoot.SHM, solidRoot.Solid = nil, &ports.SolidColor{A: 1}
118	c, reason = scanoutCandidate(scene, map[ports.WindowID]ports.SurfaceContent{1: solidRoot}, 200, 100, nil)
119	if reason != "" || c.DMABuf != game || c.Solid != nil || c.ID != 1 || c.Seq != 7 {
120		t.Fatalf("solid root with dmabuf child: candidate %+v, reason %q", c, reason)
121	}
122	solidRoot.Children = nil
123	if _, reason = scanoutCandidate(scene, map[ports.WindowID]ports.SurfaceContent{1: solidRoot}, 200, 100, nil); reason != "not_dmabuf" {
124		t.Fatalf("solid root alone: reason %q, want not_dmabuf", reason)
125	}
126}
127
128// VRR follows a visible fullscreen window covering the output, even
129// when other windows or layers force composition.
130func TestFullscreenShown(t *testing.T) {
131	full := ports.SceneWindow{ID: 1, Rect: ports.Rect{W: 100, H: 50}, Fullscreen: true}
132	for _, tc := range []struct {
133		name string
134		wins []ports.SceneWindow
135		want bool
136	}{
137		{"fullscreen", []ports.SceneWindow{full}, true},
138		{"with dialog above", []ports.SceneWindow{full, {ID: 2, Rect: ports.Rect{W: 10, H: 10}, Floating: true}}, true},
139		{"hidden", []ports.SceneWindow{{ID: 1, Rect: full.Rect, Fullscreen: true, Hidden: true}}, false},
140		{"off screen", []ports.SceneWindow{{ID: 1, Rect: ports.Rect{X: 100, W: 100, H: 50}, Fullscreen: true}}, false},
141		{"tiled covering", []ports.SceneWindow{{ID: 1, Rect: full.Rect}}, false},
142		{"none", nil, false},
143	} {
144		s := ports.Scene{OutputWidth: 100, OutputHeight: 50, Windows: tc.wins}
145		if got := fullscreenShown(&s); got != tc.want {
146			t.Errorf("%s: %v, want %v", tc.name, got, tc.want)
147		}
148	}
149}
150
151// fullscreenShown runs on every composed frame of a VRR output.
152func TestFullscreenShownAllocations(t *testing.T) {
153	s := tiledFullscreenScene()
154	if allocs := testing.AllocsPerRun(100, func() { fullscreenShown(&s) }); allocs != 0 {
155		t.Errorf("fullscreenShown: %.1f allocs, want 0", allocs)
156	}
157}
158
159func BenchmarkFullscreenShown(b *testing.B) {
160	s := tiledFullscreenScene()
161	b.ReportAllocs()
162	for b.Loop() {
163		fullscreenShown(&s)
164	}
165}
166
167// tiledFullscreenScene is 64 tiled windows with the fullscreen one last
168// (worst case for the walk).
169func tiledFullscreenScene() ports.Scene {
170	s := ports.Scene{OutputWidth: 3840, OutputHeight: 2160}
171	for i := range 64 {
172		s.Windows = append(s.Windows, ports.SceneWindow{ID: ports.WindowID(i + 1), Rect: ports.Rect{X: i * 60, W: 60, H: 2160}})
173	}
174	s.Windows = append(s.Windows, ports.SceneWindow{ID: 99, Rect: ports.Rect{W: 3840, H: 2160}, Fullscreen: true})
175	return s
176}
177
178// A buffer scans out when the primary plane lists its format (or the
179// opaque variant) with its modifier; the tranche offers only formats the
180// renderer also composes.
181func TestScanoutFormatFollowsInFormats(t *testing.T) {
182	const tiled = 0x0200000000000001
183	o, k, _ := testOutput(t)
184	o.primary.formats = []ports.DMABufFormat{{Format: fourccXRGB, Modifier: tiled}, {Format: fourccXRGB, Modifier: 0}, {Format: fourccXR30, Modifier: tiled}}
185	nv12 := uint32('N' | 'V'<<8 | '1'<<16 | '2'<<24)
186	for _, tc := range []struct {
187		name   string
188		format uint32
189		mod    uint64
190		as     uint32
191		ok     bool
192	}{
193		{"argb as xrgb", fourccARGB, tiled, fourccXRGB, true},
194		{"xrgb linear", fourccXRGB, 0, fourccXRGB, true},
195		{"10 bit alpha as opaque", fourccAR30, tiled, fourccXR30, true},
196		{"modifier not listed", fourccXRGB, 0x0200000000000002, 0, false},
197		{"format not listed", nv12, tiled, 0, false},
198	} {
199		got, ok := o.scanoutFormat(&ports.DMABuf{Format: tc.format, Modifier: tc.mod})
200		if ok != tc.ok || ok && got != tc.as {
201			t.Errorf("%s: %#x %v", tc.name, got, ok)
202		}
203	}
204	sampled := []ports.DMABufFormat{{Format: fourccARGB, Modifier: tiled}, {Format: fourccARGB, Modifier: 0x0200000000000002}, {Format: nv12, Modifier: tiled}}
205	if got := o.scanoutFormats(sampled); len(got) != 1 || got[0] != sampled[0] {
206		t.Fatalf("tranche %v", got)
207	}
208	// A buffer not in IN_FORMATS is refused before any import.
209	o.clientFBs = map[uint64]*clientFB{}
210	if fb, reason := o.scanoutFB(&ports.DMABuf{ID: 4, Format: nv12, Modifier: tiled}, time.Now()); fb != 0 || reason != "format" {
211		t.Fatalf("fb %d reason %q", fb, reason)
212	}
213	// A listed one is imported with every plane (addFB builds ADDFB2).
214	k.EXPECT().addFB(mock.Anything, uint32(fourccXRGB)).Return(88, nil).Once()
215	if fb, reason := o.scanoutFB(&ports.DMABuf{ID: 5, Format: fourccARGB, Modifier: tiled, Planes: []ports.DMABufPlane{{}, {}}}, time.Now()); fb != 88 || reason != "" {
216		t.Fatalf("fb %d reason %q", fb, reason)
217	}
218}
219
220// Each modeset reports the formats clients may allocate for scanout;
221// with direct scanout off the list is empty.
222func TestModesetSendsScanoutFormats(t *testing.T) {
223	const tiled = 0x0200000000000001
224	for _, on := range []bool{true, false} {
225		o, k, _ := testOutput(t)
226		o.cursor = nil
227		o.scanout, o.device = on, 9
228		o.primary.formats = []ports.DMABufFormat{{Format: fourccXRGB, Modifier: tiled}, {Format: fourccNV12, Modifier: tiled}, {Format: fourccP010, Modifier: tiled}}
229		o.sampled = []ports.DMABufFormat{{Format: fourccARGB, Modifier: tiled}, {Format: fourccARGB, Modifier: 0}, {Format: fourccNV12, Modifier: tiled}, {Format: fourccP010, Modifier: tiled}}
230		ch := make(chan ports.OutputFormats, 1)
231		o.formats = ch
232		k.EXPECT().createBlob(mock.Anything).Return(99, nil).Once()
233		if err := o.modeset(); err != nil {
234			t.Fatal(err)
235		}
236		f := <-ch
237		if f.Output != "DP-1" || f.Device != 9 {
238			t.Fatalf("report %+v", f)
239		}
240		if on != (len(f.Formats) == 1 && f.Formats[0] == o.sampled[0]) || !on && len(f.Formats) != 0 {
241			t.Fatalf("scanout=%v formats %v", on, f.Formats)
242		}
243	}
244}
245
246func TestHDRScanoutDecision(t *testing.T) {
247	o, k, _ := testOutput(t)
248	o.hdr.on = true
249	o.primary.formats = []ports.DMABufFormat{{Format: fourccXR30}}
250	scene := ports.Scene{OutputWidth: 200, OutputHeight: 100, Windows: []ports.SceneWindow{{ID: 1, Fullscreen: true, Rect: ports.Rect{W: 200, H: 100}}}}
251	c := ports.SurfaceContent{ID: 1, Width: 200, Height: 100, LogicalW: 200, LogicalH: 100, DMABuf: &ports.DMABuf{ID: 11, Format: fourccXR30}}
252	check := func(want string) {
253		t.Helper()
254		fb, _, _ := o.scanoutFrame(scene, map[ports.WindowID]ports.SurfaceContent{1: c})
255		if o.reason != want || (fb != 0) != (want == "") {
256			t.Fatalf("fb %d reason %q, want %q", fb, o.reason, want)
257		}
258	}
259	check("hdr_sdr_content")
260	c.Color = ports.SurfaceColor{TF: ports.ColorTFPQ, Primaries: ports.ColorPrimariesBT2020}
261	c.DMABuf.Format = fourccXRGB
262	check("hdr_format")
263	c.DMABuf.Format = fourccP010
264	check("yuv")
265	c.DMABuf.Format = fourccXR30
266	k.EXPECT().addFB(mock.Anything, uint32(fourccXR30)).Return(uint32(77), nil).Once()
267	check("")
268	o.hdr.on = false
269	check("sdr_pq_content")
270}
271
272func TestModesetReportsConfirmedHDR(t *testing.T) {
273	o, k, _ := testOutput(t)
274	o.cursor = nil
275	o.hdr.on = true
276	o.hdr.cap = hdrCapability{MaxLuminance: 1000, MaxFrameAverage: 400, MinLuminance: 0.005}
277	o.primary.formats = []ports.DMABufFormat{{Format: fourccXR30}, {Format: fourccXRGB}}
278	o.sampled = []ports.DMABufFormat{{Format: fourccXR30}, {Format: fourccXRGB}}
279	ch := make(chan ports.OutputFormats, 2)
280	o.formats = ch
281	k.EXPECT().createBlob(mock.Anything).Return(99, nil).Twice()
282	k.EXPECT().destroyBlob(uint32(99)).Return(nil).Once()
283	if err := o.modeset(); err != nil {
284		t.Fatal(err)
285	}
286	if f := <-ch; f.HDR == nil || f.HDR.MaxLuminance != 1000 || f.HDR.MaxFrameAverage != 400 || f.HDR.MinLuminance != 0.005 || len(f.Formats) != 1 || f.Formats[0].Format != fourccXR30 {
287		t.Fatalf("HDR report: %+v", f)
288	}
289	o.hdr.on = false // fallback to SDR
290	if err := o.modeset(); err != nil {
291		t.Fatal(err)
292	}
293	if f := <-ch; f.HDR != nil || len(f.Formats) != 1 || f.Formats[0].Format != fourccXRGB {
294		t.Fatalf("SDR fallback report: %+v", f)
295	}
296}
297
298// Power off withdraws HDR and scanout formats: clients must not target an
299// inactive output.
300func TestPowerOffWithdrawsHDRAndFormats(t *testing.T) {
301	o, _, _ := testOutput(t)
302	o.cursor = nil
303	o.hdr.on, o.scanout = true, true
304	o.hdr.cap = hdrCapability{MaxLuminance: 1000}
305	o.primary.formats = []ports.DMABufFormat{{Format: fourccXR30}}
306	o.sampled = []ports.DMABufFormat{{Format: fourccXR30}}
307	ch := make(chan ports.OutputFormats, 1)
308	o.formats = ch
309	if err := o.powerOff(); err != nil {
310		t.Fatal(err)
311	}
312	select {
313	case f := <-ch:
314		if f.HDR != nil || len(f.Formats) != 0 {
315			t.Fatalf("power-off report: %+v", f)
316		}
317	default:
318		t.Fatal("no report after power off")
319	}
320}
321
322// A plane without IN_FORMATS (driver without modifiers) scans out its
323// GETPLANE formats, linear.
324func TestPlaneWithoutInFormatsIsLinear(t *testing.T) {
325	k := newMockkms(t)
326	k.EXPECT().planes().Return([]planeRes{{id: 1, possible: 1, formats: []uint32{fourccXRGB}}}, nil)
327	k.EXPECT().objProps(uint32(1), uint32(objPlane)).Return(map[string][2]uint64{"type": {20, planePrimary}}, nil)
328	ps, err := readPlanes(k, 0)
329	if err != nil || len(ps) != 1 {
330		t.Fatalf("%v %v", ps, err)
331	}
332	o := &Output{primary: ps[0]}
333	if f, ok := o.scanoutFormat(&ports.DMABuf{Format: fourccARGB}); !ok || f != fourccXRGB {
334		t.Fatalf("linear argb: %#x %v", f, ok)
335	}
336	if _, ok := o.scanoutFormat(&ports.DMABuf{Format: fourccARGB, Modifier: 0x0200000000000001}); ok {
337		t.Fatal("tiled accepted without IN_FORMATS")
338	}
339}
340
341// A flip shows only the windows its scene draws: presentation feedback of
342// a window on another workspace, hidden or scrolled off the output is
343// discarded, not presented.
344func TestShownByScene(t *testing.T) {
345	s := ports.Scene{OutputWidth: 10, OutputHeight: 10, Windows: []ports.SceneWindow{{ID: 1, Rect: ports.Rect{X: 5, W: 10, H: 10}}, {ID: 2, Hidden: true}, {ID: 3, Rect: ports.Rect{X: 10, W: 10, H: 10}}}, Layers: []ports.SceneLayer{{ID: 5}}}
346	got := (&Output{}).shownBy(s, map[ports.WindowID]uint64{1: 3, 2: 4, 3: 8, 5: 6, 9: 7})
347	if len(got) != 2 || got[1] != 3 || got[5] != 6 {
348		t.Fatalf("shows %v", got)
349	}
350}
351
352// The decision is callable without Run and keeps the zero-allocation
353// composition and cached direct-scanout paths cheap.
354func TestFrameDecisionAllocations(t *testing.T) {
355	o, _, _ := testOutput(t)
356	o.scanout = true
357	s := ports.Scene{OutputWidth: 200, OutputHeight: 100, Windows: []ports.SceneWindow{{ID: 1, Fullscreen: true, Rect: ports.Rect{W: 200, H: 100}}}}
358	c := ports.SurfaceContent{ID: 1, Width: 200, Height: 100, LogicalW: 200, LogicalH: 100, DMABuf: &ports.DMABuf{ID: 17, Format: fourccXRGB}}
359	surfaces := map[ports.WindowID]ports.SurfaceContent{1: c}
360	o.clientFBs[17] = &clientFB{fbID: 77}
361	o.primary.formats = []ports.DMABufFormat{{Format: fourccXRGB}}
362	for _, tc := range []struct {
363		name       string
364		capture    bool
365		wantFB     uint32
366		wantReason string
367	}{
368		{"direct", false, 77, ""}, {"capture", true, 0, "capture"},
369	} {
370		t.Run(tc.name, func(t *testing.T) {
371			d := o.decideFrame(s, surfaces, tc.capture)
372			if d.fb != tc.wantFB || o.reason != tc.wantReason {
373				t.Fatalf("decision: fb=%d reason=%q", d.fb, o.reason)
374			}
375			if tc.capture && o.overlayReason != "capture" {
376				t.Fatalf("capture overlay reason %q", o.overlayReason)
377			}
378			if n := testing.AllocsPerRun(100, func() { o.decideFrame(s, surfaces, tc.capture) }); n != 0 {
379				t.Fatalf("decision: %.1f allocs", n)
380			}
381		})
382	}
383	o.primary.formats = nil
384	d := o.decideFrame(s, surfaces, false)
385	if d.fb != 0 || o.overlayReason != "no_plane" || len(d.composed.Windows) != 1 {
386		t.Fatalf("composition: %+v", d)
387	}
388	if n := testing.AllocsPerRun(100, func() { o.decideFrame(s, surfaces, false) }); n != 0 {
389		t.Fatalf("composition decision: %.1f allocs", n)
390	}
391
392	// Overlay active: the scene composed without the overlaid window reuses
393	// the output's window slice.
394	t.Run("overlay", func(t *testing.T) {
395		o, _, _ := overlayOutput(t)
396		o.overlay.formats = []ports.DMABufFormat{{Format: fourccXRGB}}
397		s, surfaces := overlayScene()
398		o.clientFBs[9] = &clientFB{fbID: 88}
399		d := o.decideFrame(s, surfaces, false)
400		if d.fb != 0 || d.overlay.fb != 88 || o.overlayReason != "" || len(d.composed.Windows) != 1 || d.composed.Windows[0].ID != 1 {
401			t.Fatalf("overlay decision: %+v reason %q", d, o.overlayReason)
402		}
403		if n := testing.AllocsPerRun(100, func() { o.decideFrame(s, surfaces, false) }); n != 0 {
404			t.Fatalf("overlay decision: %.1f allocs", n)
405		}
406		// The slice is reused: the previous scene's Windows is overwritten
407		// by the next call, and the input scene is never modified.
408		first := d.composed.Windows
409		d2 := o.decideFrame(s, surfaces, false)
410		if &first[0] != &d2.composed.Windows[0] || len(s.Windows) != 2 || s.Windows[0].ID != 1 || s.Windows[1].ID != 2 {
411			t.Fatal("rest windows not reused or the input scene changed")
412		}
413	})
414}
415
416func TestFrameDecisionOverlayFallback(t *testing.T) {
417	o, k, _ := overlayOutput(t)
418	o.overlay.formats = []ports.DMABufFormat{{Format: fourccXRGB}}
419	s, surfaces := overlayScene()
420	k.EXPECT().addFB(mock.Anything, uint32(fourccXRGB)).Return(88, nil).Once()
421	d := o.decideFrame(s, surfaces, false)
422	if d.fb != 0 || d.overlay.fb != 88 || o.reason != "other_windows" || len(d.composed.Windows) != 1 {
423		t.Fatalf("overlay decision: %+v", d)
424	}
425	d.overlay.close()
426	o.clientFBs[9].overlayFailed = "overlay_refused"
427	d = o.decideFrame(s, surfaces, false)
428	if d.overlay.fb != 0 || o.overlayReason != "overlay_refused" || len(d.composed.Windows) != 2 {
429		t.Fatalf("fallback decision: %+v", d)
430	}
431}

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