main f2d166b

amdgpu_rules_test.go RawHistoryFolder

1package drm
2
3import (
4	"context"
5	"errors"
6	"sync"
7	"sync/atomic"
8	"testing"
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// Connector HDR property IDs and values of the amdgpu rule tests.
18const (
19	pHDRMeta, pColorspace, pMaxBPC = 20, 21, 22
20	colorspaceBT2020               = 9
21)
22
23// Blob lifetime is part of the model: attach feeds createBlob/destroyBlob
24// mocks into the live set.
25type amdgpuModel struct {
26	t    *testing.T
27	mu   sync.Mutex
28	live map[uint32]bool
29	next uint32
30	rule func(*atomicReq, uint32) error
31}
32
33// newAmdgpuModel starts from a lit desktop whose mode blob 99 is live.
34func newAmdgpuModel(t *testing.T) *amdgpuModel {
35	t.Helper()
36	m := &amdgpuModel{t: t, live: map[uint32]bool{99: true}, next: 100}
37	m.rule = amdgpuRuleWith(t, m.isLive)
38	return m
39}
40
41func (m *amdgpuModel) isLive(blob uint32) bool {
42	m.mu.Lock()
43	defer m.mu.Unlock()
44	return m.live[blob]
45}
46
47// attach makes k create ids from 100 up and destroy only live blobs. With
48// strict ids, each is expected to be destroyed exactly once and no other
49// destroy is allowed; without, any live blob may be destroyed.
50func (m *amdgpuModel) attach(k *mockkms, strict ...uint32) {
51	k.EXPECT().createBlob(mock.Anything).RunAndReturn(func([]byte) (uint32, error) {
52		m.mu.Lock()
53		defer m.mu.Unlock()
54		id := m.next
55		m.next++
56		m.live[id] = true
57		return id, nil
58	}).Maybe()
59	destroy := func(id uint32) error {
60		m.mu.Lock()
61		defer m.mu.Unlock()
62		if !m.live[id] {
63			m.t.Errorf("amdgpu: destroyBlob(%d) of an unknown or destroyed blob", id)
64			return unix.ENOENT
65		}
66		delete(m.live, id)
67		return nil
68	}
69	if len(strict) == 0 {
70		k.EXPECT().destroyBlob(mock.Anything).RunAndReturn(destroy).Maybe()
71	}
72	for _, id := range strict {
73		k.EXPECT().destroyBlob(id).RunAndReturn(destroy).Once()
74	}
75}
76
77// amdgpuRule returns a commit rule encoding the atomic_check constraints of
78// amdgpu_dm that real hardware applied to the protected requests (EINVAL on
79// kernel 7.2, RX/DP+HDMI). The rule tracks the committed state, starting from
80// a lit desktop: CRTC enabled and active, primary/cursor/overlay/stray
81// planes attached, connector routed.
82//
83//   - a CRTC with MODE_ID set (enabled) needs its primary plane attached,
84//     even with ACTIVE=0 (drm_atomic_helper_check_crtc_primary_plane);
85//   - MODE_ID, connector CRTC_ID and ACTIVE agree: an active or routed CRTC
86//     is enabled, an enabled one is routed;
87//   - a plane's FB_ID and CRTC_ID are both set or both clear, and only on an
88//     enabled CRTC;
89//   - the active primary plane covers the whole mode (no partial src/dst);
90//   - HDR_OUTPUT_METADATA needs the BT.2020 Colorspace;
91//   - MODE_ID must name a live blob, and a change of MODE_ID, ACTIVE or the
92//     connector CRTC_ID is a modeset that needs the ALLOW_MODESET flag;
93//   - a plane COLOR_PIPELINE cannot be selected together with the CRTC
94//     DEGAMMA_LUT (amdgpu_dm_plane.c);
95//   - an async commit changes nothing but a plane's FB_ID and IN_FENCE_FD
96//     (drm_atomic_set_property: a changed CRTC, connector, colorop or other
97//     plane property is EINVAL; writing the current value is allowed).
98//
99// amdgpuRule has no blob model: any MODE_ID is live.
100func amdgpuRule(t *testing.T) func(*atomicReq, uint32) error {
101	t.Helper()
102	return amdgpuRuleWith(t, func(uint32) bool { return true })
103}
104
105func amdgpuRuleWith(t *testing.T, live func(uint32) bool) func(*atomicReq, uint32) error {
106	t.Helper()
107	state := map[[2]uint32]uint64{
108		{tCrtc, pMode}: 99, {tCrtc, pActive}: 1, {tConn, pConnCrtc}: tCrtc,
109	}
110	for _, p := range []uint32{tPrimary, tCursor, tOverlay, 53} {
111		state[[2]uint32{p, pFB}], state[[2]uint32{p, pCrtcID}] = 70, tCrtc
112	}
113	// The lit desktop's primary plane covers the whole mode: a commit that
114	// leaves its geometry alone (an async flip) keeps it.
115	for _, c := range [...]struct {
116		prop uint32
117		val  uint64
118	}{{12, 200 << 16}, {13, 100 << 16}, {16, 200}, {17, 100}} {
119		state[[2]uint32{tPrimary, c.prop}] = c.val
120	}
121	return func(req *atomicReq, flags uint32) error {
122		next := make(map[[2]uint32]uint64, len(state))
123		for k, v := range state {
124			next[k] = v
125		}
126		for i, obj := range req.objs {
127			for _, p := range req.props[i] {
128				next[[2]uint32{obj, p.prop}] = p.val
129			}
130		}
131		get := func(obj, prop uint32) uint64 { return next[[2]uint32{obj, prop}] }
132		if flags&flipAsyncFlag != 0 {
133			for i, obj := range req.objs {
134				for _, p := range req.props[i] {
135					isPlane := obj == tPrimary || obj == tCursor || obj == tOverlay || obj == 53
136					if p.val != state[[2]uint32{obj, p.prop}] && !(isPlane && (p.prop == pFB || p.prop == pFence)) {
137						t.Logf("amdgpu: async commit changes object %d property %d", obj, p.prop)
138						return unix.EINVAL
139					}
140				}
141			}
142		}
143		for _, p := range []uint32{tPrimary, tOverlay} {
144			if get(tCrtc, pDegamma) != 0 && (get(p, pColorPipe) != 0 || get(p, pColorPipeOverlay) != 0) {
145				t.Logf("amdgpu: COLOR_PIPELINE on plane %d with a CRTC DEGAMMA_LUT", p)
146				return unix.EINVAL
147			}
148		}
149		if mode := get(tCrtc, pMode); mode != 0 && !live(uint32(mode)) {
150			t.Logf("amdgpu: MODE_ID %d names an unknown or destroyed blob", mode)
151			return unix.EINVAL
152		}
153		for _, k := range [...][2]uint32{{tCrtc, pMode}, {tCrtc, pActive}, {tConn, pConnCrtc}} {
154			if next[k] != state[k] && flags&atomicAllowModes == 0 {
155				t.Logf("amdgpu: modeset change of %v without ALLOW_MODESET", k)
156				return unix.EINVAL
157			}
158		}
159		enabled, active := get(tCrtc, pMode) != 0, get(tCrtc, pActive) == 1
160		if active && !enabled || (get(tConn, pConnCrtc) != 0) != enabled {
161			t.Logf("amdgpu: ACTIVE/MODE_ID/connector routing disagree: %v", next)
162			return unix.EINVAL
163		}
164		for _, p := range []uint32{tPrimary, tCursor, tOverlay, 53} {
165			fb, crtc := get(p, pFB), get(p, pCrtcID)
166			if (fb != 0) != (crtc != 0) || crtc != 0 && (!enabled || crtc != tCrtc) {
167				t.Logf("amdgpu: plane %d fb=%d crtc=%d enabled=%v", p, fb, crtc, enabled)
168				return unix.EINVAL
169			}
170		}
171		if enabled && get(tPrimary, pFB) == 0 {
172			t.Log("amdgpu: enabled CRTC without primary plane")
173			return unix.EINVAL
174		}
175		if active {
176			for _, c := range [...]struct {
177				prop uint32
178				want uint64
179			}{{10, 0}, {11, 0}, {12, 200 << 16}, {13, 100 << 16}, {14, 0}, {15, 0}, {16, 200}, {17, 100}} {
180				if get(tPrimary, c.prop) != c.want {
181					t.Logf("amdgpu: primary prop %d = %d, want full-screen %d", c.prop, get(tPrimary, c.prop), c.want)
182					return unix.EINVAL
183				}
184			}
185		}
186		if get(tConn, pHDRMeta) != 0 && get(tConn, pColorspace) != colorspaceBT2020 {
187			t.Log("amdgpu: HDR metadata without BT.2020 colorspace")
188			return unix.EINVAL
189		}
190		if flags&atomicTestOnly == 0 {
191			state = next
192		}
193		return nil
194	}
195}
196
197func enableTestHDR(o *Output) {
198	o.hdr.on, o.hdr.blob = true, 55
199	o.hdr.props = connectorHDRProps{Metadata: pHDRMeta, Colorspace: pColorspace, MaxBPC: pMaxBPC, BT2020Value: colorspaceBT2020, MaxBPCValue: 8, HasDefault: true}
200}
201
202func TestAmdgpuRuleRejectsWhatHardwareRejected(t *testing.T) {
203	for name, build := range map[string]func(*atomicReq){
204		// The shape that failed on hardware: inactive, still enabled, no planes.
205		"enabled CRTC without primary": func(r *atomicReq) {
206			r.set(tCrtc, pActive, 0)
207			for _, p := range []uint32{tPrimary, tCursor, tOverlay, 53} {
208				r.set(p, pFB, 0)
209				r.set(p, pCrtcID, 0)
210			}
211		},
212		"plane on disabled CRTC": func(r *atomicReq) {
213			r.set(tCrtc, pActive, 0)
214			r.set(tCrtc, pMode, 0)
215			r.set(tConn, pConnCrtc, 0)
216		},
217		"connector routed to disabled CRTC": func(r *atomicReq) {
218			r.set(tCrtc, pActive, 0)
219			r.set(tCrtc, pMode, 0)
220			for _, p := range []uint32{tPrimary, tCursor, tOverlay, 53} {
221				r.set(p, pFB, 0)
222				r.set(p, pCrtcID, 0)
223			}
224		},
225		"cursor with CRTC but no FB": func(r *atomicReq) { r.set(tCursor, pFB, 0) },
226		"partial primary":            func(r *atomicReq) { r.set(tPrimary, 16, 100) },
227		"HDR metadata without colorspace": func(r *atomicReq) {
228			r.set(tConn, pHDRMeta, 55)
229		},
230		"COLOR_PIPELINE with DEGAMMA_LUT": func(r *atomicReq) {
231			r.set(tCrtc, pDegamma, 77)
232			r.set(tPrimary, pColorPipe, opBase)
233		},
234	} {
235		t.Run(name, func(t *testing.T) {
236			req := &atomicReq{}
237			build(req)
238			if err := amdgpuRule(t)(req, atomicAllowModes); err == nil {
239				t.Fatal("rule accepted a request amdgpu refuses")
240			}
241		})
242	}
243}
244
245// Every protected disable/activation request must pass the amdgpu rules, with
246// HDR on and off, and leave the state dark then black-only.
247func TestProtectedRequestsAcceptedByAmdgpuRules(t *testing.T) {
248	for _, hdr := range []bool{false, true} {
249		name := map[bool]string{false: "sdr", true: "hdr"}[hdr]
250		t.Run(name, func(t *testing.T) {
251			m := newAmdgpuModel(t)
252			o, k, commits, _ := testOutputRules(t, m.rule)
253			m.attach(k, 99)
254			if hdr {
255				enableTestHDR(o)
256			}
257			o.modeBlob = 99
258			o.overlay = &plane{id: tOverlay, props: planeProps}
259			o.stray = []*plane{o.overlay, {id: 53, props: planeProps}}
260			k.EXPECT().objProps(uint32(53), uint32(objPlane)).Return(map[string][2]uint64{"CRTC_ID": {pCrtcID, tCrtc}}, nil)
261			r := portsmocks.NewMockRenderer(t)
262			r.EXPECT().UseTarget(0).Return().Once()
263			r.EXPECT().Render(mock.Anything, mock.Anything).Return(nil, nil).Once()
264			if err := o.protectedModeset(context.Background(), r); err != nil {
265				t.Fatalf("amdgpu refused the protected transaction: %v", err)
266			}
267			if len(*commits) != 2 {
268				t.Fatalf("commits %d", len(*commits))
269			}
270			act := (*commits)[1]
271			if fb, _ := act.req.value(tPrimary, pFB); fb != 71 && fb != 70 {
272				t.Fatalf("activated fb %d", fb)
273			}
274			if crtc, ok := act.req.value(tCursor, pCrtcID); !ok || crtc != 0 {
275				t.Fatalf("cursor not explicitly detached: %d %v", crtc, ok)
276			}
277			if fb, ok := act.req.value(tCursor, pFB); !ok || fb != 0 {
278				t.Fatalf("cursor fb not explicitly detached: %d %v", fb, ok)
279			}
280			if o.off || !o.protected {
281				t.Fatalf("off=%v protected=%v", o.off, o.protected)
282			}
283			// The desktop mode blob is destroyed once by the disable; the
284			// activation's blob is the only live one and o.modeBlob owns it.
285			if o.modeBlob != 100 || !m.isLive(100) || m.isLive(99) {
286				t.Fatalf("modeBlob %d live100=%v live99=%v", o.modeBlob, m.isLive(100), m.isLive(99))
287			}
288		})
289	}
290}
291
292func TestAmdgpuRuleRejectsModesetWithoutAllowModes(t *testing.T) {
293	for name, build := range map[string]func(*atomicReq){
294		"MODE_ID":           func(r *atomicReq) { r.set(tCrtc, pMode, 99) },
295		"ACTIVE":            func(r *atomicReq) { r.set(tCrtc, pActive, 0) },
296		"connector CRTC_ID": func(r *atomicReq) { r.set(tConn, pConnCrtc, 0) },
297	} {
298		t.Run(name, func(t *testing.T) {
299			m := newAmdgpuModel(t)
300			m.live[98] = true
301			req := &atomicReq{}
302			build(req)
303			if name == "MODE_ID" {
304				req.set(tCrtc, pMode, 98)
305			}
306			if err := m.rule(req, 0); err == nil {
307				t.Fatal("rule accepted a modeset change without ALLOW_MODESET")
308			}
309		})
310	}
311}
312
313// The state a lit desktop starts from is valid: the rule only refuses an
314// unknown MODE_ID once it models blobs.
315func TestAmdgpuRuleRejectsUnknownModeBlob(t *testing.T) {
316	req := &atomicReq{}
317	req.set(tCrtc, pMode, 123)
318	if err := newAmdgpuModel(t).rule(req, atomicAllowModes); err == nil {
319		t.Fatal("rule accepted an unknown MODE_ID")
320	}
321}
322
323func TestAmdgpuRuleRejectsDestroyedModeBlob(t *testing.T) {
324	m := newAmdgpuModel(t)
325	req := &atomicReq{}
326	req.set(tCrtc, pMode, 99)
327	for _, c := range [...]struct{ prop, val uint32 }{{12, 200 << 16}, {13, 100 << 16}, {16, 200}, {17, 100}} {
328		req.set(tPrimary, c.prop, uint64(c.val))
329	}
330	if err := m.rule(req, atomicAllowModes); err != nil {
331		t.Fatalf("live blob rejected: %v", err)
332	}
333	m.mu.Lock()
334	delete(m.live, 99)
335	m.mu.Unlock()
336	if err := m.rule(req, atomicAllowModes); err == nil {
337		t.Fatal("rule accepted a destroyed MODE_ID blob")
338	}
339}
340
341func TestProtectedResumeAcceptedByAmdgpuRules(t *testing.T) {
342	m := newAmdgpuModel(t)
343	o, k, commits, _ := testOutputRules(t, m.rule)
344	m.attach(k)
345	o.modeBlob = 99
346	o.protected = true
347	o.overlay = &plane{id: tOverlay, props: planeProps}
348	o.stray = []*plane{o.overlay, {id: 53, props: planeProps}}
349	k.EXPECT().objProps(uint32(53), uint32(objPlane)).Return(map[string][2]uint64{"CRTC_ID": {pCrtcID, tCrtc}}, nil)
350	if err := o.modeset(); err != nil { // resume while protected: dark, not stale
351		t.Fatalf("amdgpu refused protected inactive modeset: %v", err)
352	}
353	if !o.off || len(*commits) != 1 {
354		t.Fatalf("off=%v commits=%d", o.off, len(*commits))
355	}
356	if v, ok := (*commits)[0].req.value(tCrtc, pMode); !ok || v != 0 {
357		t.Fatal("protected disable left the CRTC enabled")
358	}
359}
360
361// The legacy request is refused by the rule; the output stays registered and
362// protected, retries with bounded backoff and never reports proof.
363func TestProtectedRefusalKeepsOutputProtectedAndBacksOff(t *testing.T) {
364	o, k, commits, _ := testOutputRules(t, func(r *atomicReq, flags uint32) error {
365		if v, ok := r.value(tCrtc, pActive); ok && v == 1 {
366			return unix.EINVAL
367		}
368		return nil
369	})
370	k.EXPECT().createBlob(mock.Anything).Return(100, nil)
371	k.EXPECT().destroyBlob(mock.Anything).Return(nil).Maybe()
372	r := portsmocks.NewMockRenderer(t)
373	r.EXPECT().UseTarget(0).Return().Once()
374	r.EXPECT().Render(mock.Anything, mock.Anything).Return(nil, nil).Once()
375	err := o.protectedModeset(context.Background(), r)
376	if err == nil || !retryableProtected(err) || !errorsIsEINVAL(err) {
377		t.Fatalf("refusal result %v", err)
378	}
379	if !o.protected || !o.off || o.back != 0 || len(*commits) != 2 {
380		t.Fatalf("protected=%v off=%v back=%d commits=%d", o.protected, o.off, o.back, len(*commits))
381	}
382	var enabled = true
383	if !o.commitFailed(err, &enabled) || !enabled || !o.protectBackoffActive() {
384		t.Fatal("refusal not retryable with backoff")
385	}
386	d := time.Duration(0)
387	for range 10 {
388		d = nextProtectBackoff(d)
389	}
390	if d != protectRetryMax || nextProtectBackoff(0) != protectRetryMin {
391		t.Fatalf("backoff not bounded: %v", d)
392	}
393}
394
395func errorsIsEINVAL(err error) bool { return err != nil && errors.Is(err, unix.EINVAL) }
396
397// Run: activation refused twice with EINVAL. The output keeps running, stays
398// dark, and only the later accepted black activation produces proof.
399func TestRunProtectedActivationRetriesWithoutStoppingOrEarlyProof(t *testing.T) {
400	var activations, refusals atomic.Int32
401	o, k, _, _ := testOutputRules(t, func(r *atomicReq, flags uint32) error {
402		if v, ok := r.value(tCrtc, pActive); ok && v == 1 && flags&atomicTestOnly == 0 {
403			activations.Add(1)
404			if refusals.Add(1) <= 2 {
405				return unix.EINVAL
406			}
407		}
408		return nil
409	})
410	o.fbs = [2]uint32{} // the first setup of a run: no image yet
411	o.cursor = nil
412	var state atomic.Uint64
413	state.Store(3)
414	securityGate(t, o, &state)
415	events := make(chan ports.SecurityBackendEvent, 32)
416	o.SecurityEvents = events
417	r := portsmocks.NewMockRenderer(t)
418	buf := func() ports.DMABuf {
419		f, w := protectionFence(t, false)
420		w.Close()
421		return ports.DMABuf{Planes: []ports.DMABufPlane{{File: f}}}
422	}
423	r.EXPECT().SetHDR(float64(0)).Return().Maybe()
424	r.EXPECT().ExportTargets(2, mock.Anything, false).Return([]ports.DMABuf{buf(), buf()}, nil).Once()
425	r.EXPECT().UseTarget(mock.Anything).Return()
426	r.EXPECT().Render(mock.Anything, mock.Anything).Return(nil, nil)
427	r.EXPECT().Close().Return().Once()
428	k.EXPECT().addFB(mock.Anything, uint32(fourccXRGB)).Return(70, nil)
429	k.EXPECT().createBlob(mock.Anything).Return(99, nil)
430	k.EXPECT().destroyBlob(mock.Anything).Return(nil).Maybe()
431	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
432	defer cancel()
433	done := make(chan error, 1)
434	go func() {
435		done <- o.Run(ctx, func(int, int) (ports.Renderer, error) { return r, nil }, nil, nil, nil, nil, nil, nil, discardSink(t), nil, nil)
436	}()
437	for {
438		select {
439		case ev := <-events:
440			if _, ok := ev.(ports.SecurityOutputProof); !ok {
441				continue
442			}
443			if n := activations.Load(); n < 3 {
444				t.Fatalf("proof after %d activation attempts; refused ones are no evidence", n)
445			}
446			cancel()
447			if err := <-done; err != nil {
448				t.Fatalf("output stopped: %v", err)
449			}
450			return
451		case err := <-done:
452			t.Fatalf("output stopped on refused protected commit: %v", err)
453		case <-ctx.Done():
454			t.Fatal("no proof after retries")
455		}
456	}
457}
458
459// Async commits may change FB_ID and IN_FENCE_FD only; writing a value a
460// property already has is allowed.
461func TestAmdgpuRuleAsyncChangesOnlyFB(t *testing.T) {
462	for _, tc := range []struct {
463		name  string
464		build func(*atomicReq)
465		ok    bool
466	}{
467		{"FB_ID and fence", func(r *atomicReq) { r.set(tPrimary, pFB, 71); r.set(tPrimary, pFence, 5) }, true},
468		{"the current value of another property", func(r *atomicReq) { r.set(tPrimary, pFB, 71); r.set(tPrimary, pCrtcID, tCrtc) }, true},
469		{"COLOR_PIPELINE changes", func(r *atomicReq) { r.set(tPrimary, pFB, 71); r.set(tPrimary, pColorPipe, opBase) }, false},
470		{"colorop BYPASS changes", func(r *atomicReq) { r.set(tPrimary, pFB, 71); r.set(opBase, bypassProp(opBase), 1) }, false},
471		{"VRR changes", func(r *atomicReq) { r.set(tPrimary, pFB, 71); r.set(tCrtc, pVRR, 1) }, false},
472		{"cursor moves", func(r *atomicReq) { r.set(tPrimary, pFB, 71); r.set(tCursor, 14, 9) }, false},
473	} {
474		t.Run(tc.name, func(t *testing.T) {
475			req := &atomicReq{}
476			tc.build(req)
477			err := amdgpuRule(t)(req, flipAsyncFlag)
478			if (err == nil) != tc.ok {
479				t.Fatalf("async rule error %v, want ok=%v", err, tc.ok)
480			}
481		})
482	}
483}
484
485// The colour pipeline sequence the output drives on an HDR output meets the
486// amdgpu rules: modeset on Bypass, SDR scanout selecting the pipeline in a
487// synchronous commit, async flips of it carrying FB_ID alone, a composed
488// frame back on Bypass, and Close.
489func TestColorPipelineSequenceAcceptedByAmdgpuRules(t *testing.T) {
490	m := newAmdgpuModel(t)
491	o, k, commits := colorOutput(t, true, m.rule)
492	m.attach(k)
493	k.EXPECT().objProps(uint32(tOverlay), uint32(objPlane)).Return(map[string][2]uint64{"CRTC_ID": {pCrtcID, tCrtc}}, nil).Maybe()
494	k.EXPECT().rmFB(mock.Anything).Return(nil).Maybe()
495	enableTestHDR(o)
496	o.hdr.blob = 55
497	m.live[55], m.live[61] = true, true
498	o.modeBlob = 99
499	if err := o.modeset(); err != nil {
500		t.Fatalf("modeset: %v", err)
501	}
502	c := sdrContent(o)
503	c.Async = true
504	o.shown = 9
505	flip := func(mode colorMode) {
506		t.Helper()
507		o.frame.endPending()
508		o.vrrOn = true // keep VRR out of the way: it is its own commit field
509		if ok, err := o.commitScanoutRect(80, c, pendingFrame{}, fullPlaneRect(200, 100), mode); !ok || err != nil {
510			t.Fatalf("scanout %d: ok=%v err=%v", mode, ok, err)
511		}
512	}
513	flip(colorSDRToPQ)
514	flip(colorSDRToPQ)
515	flip(colorSDRToPQ)
516	if n := len(*commits); (*commits)[n-1].flags&flipAsyncFlag == 0 {
517		t.Fatal("steady-state flip with the pipeline was not async")
518	}
519	flip(colorBypass)
520	o.frame.endPending()
521	if err := o.commitFrame(70, nil, false, false, pendingFrame{}); err != nil {
522		t.Fatalf("composed: %v", err)
523	}
524	o.Close()
525}
526
527// The pipeline is never selected next to a CRTC DEGAMMA_LUT: the modeset
528// state this compositor commits has none.
529func TestColorPipelineNeverSetsDegamma(t *testing.T) {
530	m := newAmdgpuModel(t)
531	o, k, commits := colorOutput(t, false, m.rule)
532	m.attach(k)
533	k.EXPECT().objProps(mock.Anything, mock.Anything).Return(nil, nil).Maybe()
534	c := sdrContent(o)
535	if ok, err := o.commitScanoutRect(80, c, pendingFrame{}, fullPlaneRect(200, 100), colorSDRToPQ); !ok || err != nil {
536		t.Fatalf("ok=%v err=%v", ok, err)
537	}
538	for _, rec := range *commits {
539		if _, ok := rec.req.value(tCrtc, pDegamma); ok {
540			t.Fatal("commit touches DEGAMMA_LUT")
541		}
542	}
543}

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