colorop_test.go RawHistoryFolder

1package drm
2
3import (
4	"encoding/binary"
5	"math"
6	"os"
7	"regexp"
8	"slices"
9	"strconv"
10	"strings"
11	"testing"
12
13	"github.com/bnema/neferwl/internal/ports"
14	"github.com/stretchr/testify/mock"
15	"golang.org/x/sys/unix"
16)
17
18// Plane COLOR_PIPELINE property IDs of the primary and overlay test planes,
19// and the CRTC DEGAMMA_LUT.
20const (
21	pColorPipe, pColorPipeOverlay, pDegamma = 60, 61, 62
22	opBase                                  = 200 // first colorop ID of a plane; a plane's ops use 20 IDs
23)
24
25// Property IDs of a colorop, by role: kernel properties are per object.
26func bypassProp(op uint32) uint32 { return 1000 + op }
27func curveProp(op uint32) uint32  { return 2000 + op }
28func multProp(op uint32) uint32   { return 3000 + op }
29func dataProp(op uint32) uint32   { return 4000 + op }
30func typeProp(op uint32) uint32   { return 5000 + op }
31
32// The enum values are arbitrary: the matcher must read names.
33var (
34	colorTypes = map[string]uint64{"1D Curve": 9, "1D LUT": 4, "3x4 Matrix": 2, "Multiplier": 6, "3D LUT": 1}
35	firstCurve = map[string]uint64{"sRGB EOTF": 5, "PQ 125 EOTF": 6, "BT.2020 Inverse OETF": 3, "Gamma 2.2": 1}
36	lastCurve  = map[string]uint64{"sRGB Inverse EOTF": 0, "PQ 125 Inverse EOTF": 8, "BT.2020 OETF": 2, "Gamma 2.2 Inverse": 7}
37)
38
39// opSpec is a colorop of a test pipeline.
40type opSpec struct {
41	typ      string
42	curves   map[string]uint64
43	noBypass bool
44}
45
46func curveOp(curves map[string]uint64) opSpec { return opSpec{typ: "1D Curve", curves: curves} }
47
48var (
49	opSRGB, opPQ = curveOp(firstCurve), curveOp(lastCurve)
50	opMult       = opSpec{typ: "Multiplier"}
51	opCTM        = opSpec{typ: "3x4 Matrix"}
52	opLUT1D      = opSpec{typ: "1D LUT"}
53	opLUT3D      = opSpec{typ: "3D LUT"}
54	// amdgpuChain is the pipeline read on kernel 7.2 (DCN3+).
55	amdgpuChain = []opSpec{opSRGB, opMult, opCTM, opPQ, opLUT1D, opLUT3D, curveOp(map[string]uint64{"sRGB EOTF": 5}), opLUT1D}
56)
57
58// colorPlane gives p a COLOR_PIPELINE property (id pipeProp) offering the
59// chains, each with ops from base up, and registers the kernel's answers.
60// It returns the first colorop of each chain.
61func colorPlane(k *mockkms, p *plane, pipeProp, base uint32, chains ...[]opSpec) []uint64 {
62	p.props = maps0(p.props)
63	p.props["COLOR_PIPELINE"] = pipeProp
64	enums := map[string]uint64{"Bypass": 0}
65	var firsts []uint64
66	k.EXPECT().propEnums(uint32(typeProp(0))).Return(colorTypes, nil).Maybe()
67	id := base
68	for c, chain := range chains {
69		firsts = append(firsts, uint64(id))
70		enums["Pipeline "+strconv.Itoa(c)] = uint64(id)
71		for i, op := range chain {
72			props := map[string][2]uint64{
73				"TYPE": {uint64(typeProp(0)), colorTypes[op.typ]},
74				"NEXT": {1, 0},
75			}
76			if i+1 < len(chain) {
77				props["NEXT"] = [2]uint64{1, uint64(id + 1)}
78			}
79			if !op.noBypass {
80				props["BYPASS"] = [2]uint64{uint64(bypassProp(id)), 1}
81			}
82			switch op.typ {
83			case "1D Curve":
84				props["CURVE_1D_TYPE"] = [2]uint64{uint64(curveProp(id)), 0}
85				k.EXPECT().propEnums(curveProp(id)).Return(op.curves, nil).Maybe()
86			case "Multiplier":
87				props["MULTIPLIER"] = [2]uint64{uint64(multProp(id)), 0}
88			case "3x4 Matrix", "1D LUT", "3D LUT":
89				props["DATA"] = [2]uint64{uint64(dataProp(id)), 0}
90			}
91			k.EXPECT().objProps(id, uint32(objColorop)).Return(props, nil).Maybe()
92			id++
93		}
94	}
95	k.EXPECT().propEnums(pipeProp).Return(enums, nil).Maybe()
96	return firsts
97}
98
99func maps0(m map[string]uint32) map[string]uint32 {
100	out := make(map[string]uint32, len(m)+1)
101	for k, v := range m {
102		out[k] = v
103	}
104	return out
105}
106
107func TestMatchColorPipeline(t *testing.T) {
108	for _, tc := range []struct {
109		name   string
110		chains [][]opSpec
111		want   int    // index of the chain that matches, -1: none
112		reason string // substring of the reason when none matches
113	}{
114		{"amdgpu", [][]opSpec{amdgpuChain}, 0, ""},
115		{"minimal", [][]opSpec{{opSRGB, opMult, opCTM, opPQ}}, 0, ""},
116		{"bypassable op before the chain", [][]opSpec{{opLUT1D, opSRGB, opMult, opCTM, opPQ}}, 0, ""},
117		{"second pipeline matches", [][]opSpec{{opLUT1D}, amdgpuChain}, 1, ""},
118		{"no pipeline", nil, -1, "only Bypass"},
119		{"no PQ curve", [][]opSpec{{opSRGB, opMult, opCTM, curveOp(map[string]uint64{"sRGB Inverse EOTF": 0})}}, -1, "no sRGB EOTF"},
120		{"no sRGB curve", [][]opSpec{{curveOp(map[string]uint64{"Gamma 2.2": 1}), opMult, opCTM, opPQ}}, -1, "no sRGB EOTF"},
121		{"no multiplier", [][]opSpec{{opSRGB, opCTM, opPQ}}, -1, "no sRGB EOTF"},
122		{"no matrix", [][]opSpec{{opSRGB, opMult, opPQ}}, -1, "no sRGB EOTF"},
123		{"wrong order", [][]opSpec{{opSRGB, opCTM, opMult, opPQ}}, -1, "no sRGB EOTF"},
124		{"PQ before sRGB", [][]opSpec{{opPQ, opMult, opCTM, opSRGB}}, -1, "no sRGB EOTF"},
125		{"op without BYPASS", [][]opSpec{{opSRGB, opMult, opCTM, opPQ, {typ: "1D LUT", noBypass: true}}}, -1, "cannot be bypassed"},
126		{"op without BYPASS first", [][]opSpec{{{typ: "3D LUT", noBypass: true}, opSRGB, opMult, opCTM, opPQ}}, -1, "cannot be bypassed"},
127	} {
128		t.Run(tc.name, func(t *testing.T) {
129			o, k, _ := testOutput(t)
130			firsts := colorPlane(k, o.primary, pColorPipe, opBase, tc.chains...)
131			cp, why := readColorPipeline(k, o.primary)
132			if tc.want < 0 {
133				if cp != nil || !strings.Contains(why, tc.reason) {
134					t.Fatalf("matched %+v, reason %q; want none with %q", cp, why, tc.reason)
135				}
136				return
137			}
138			if cp == nil {
139				t.Fatalf("no match: %s", why)
140			}
141			if cp.id != firsts[tc.want] {
142				t.Fatalf("pipeline id %d, want %d", cp.id, firsts[tc.want])
143			}
144			// The matched ops are the chain's, found by type, with their
145			// own properties and the enum values read from the curves.
146			chain, base := tc.chains[tc.want], uint32(cp.id)
147			find := func(match func(opSpec) bool, from int) uint32 {
148				for i := from; i < len(chain); i++ {
149					if match(chain[i]) {
150						return base + uint32(i)
151					}
152				}
153				t.Fatal("test chain lacks an op")
154				return 0
155			}
156			srgb := find(func(s opSpec) bool { return s.typ == "1D Curve" && s.curves["sRGB EOTF"] == 5 }, 0)
157			mult := find(func(s opSpec) bool { return s.typ == "Multiplier" }, 0)
158			ctm := find(func(s opSpec) bool { return s.typ == "3x4 Matrix" }, 0)
159			pq := find(func(s opSpec) bool { _, ok := s.curves["PQ 125 Inverse EOTF"]; return ok }, 0)
160			if cp.srgb != (colorStage{op: srgb, bypass: bypassProp(srgb), prop: curveProp(srgb), curve: 5}) ||
161				cp.mult != (colorStage{op: mult, bypass: bypassProp(mult), prop: multProp(mult)}) ||
162				cp.ctm != (colorStage{op: ctm, bypass: bypassProp(ctm), prop: dataProp(ctm)}) ||
163				cp.pq != (colorStage{op: pq, bypass: bypassProp(pq), prop: curveProp(pq), curve: 8}) {
164				t.Fatalf("stages %+v", cp)
165			}
166			if len(cp.others) != len(chain)-4 {
167				t.Fatalf("%d other ops, want %d", len(cp.others), len(chain)-4)
168			}
169		})
170	}
171}
172
173func TestReadColorPipelineWithoutProperty(t *testing.T) {
174	o, k, _ := testOutput(t)
175	if cp, why := readColorPipeline(k, o.primary); cp != nil || !strings.Contains(why, "COLOR_PIPELINE") {
176		t.Fatalf("plane without the property: %+v %q", cp, why)
177	}
178}
179
180// A NEXT loop must not hang the read.
181func TestWalkColoropsStopsOnCycle(t *testing.T) {
182	o, k, _ := testOutput(t)
183	k.EXPECT().objProps(uint32(7), uint32(objColorop)).Return(map[string][2]uint64{"NEXT": {1, 7}}, nil)
184	if _, err := walkColorops(k, 7); err == nil {
185		t.Fatal("cyclic chain accepted")
186	}
187	_ = o
188}
189
190func TestS3132(t *testing.T) {
191	for _, tc := range []struct {
192		v    float64
193		want uint64
194	}{
195		{0, 0},
196		{math.Copysign(0, -1), 0},
197		{1, 1 << 32},
198		{2.5, 5 << 31},
199		{0.5, 1 << 31},
200		{-1, 1<<63 | 1<<32},
201		{-0.5, 1<<63 | 1<<31},
202		{203.0 / 80, uint64(math.Round(203.0 / 80 * (1 << 32)))},
203		{1e30, 1<<63 - 1},
204		{-1e30, 1<<63 | (1<<63 - 1)},
205	} {
206		if got := s3132(tc.v); got != tc.want {
207			t.Errorf("s3132(%v) = %#x, want %#x", tc.v, got, tc.want)
208		}
209	}
210}
211
212// fromS3132 decodes sign-magnitude S31.32.
213func fromS3132(v uint64) float64 {
214	f := float64(v&(1<<63-1)) / (1 << 32)
215	if v>>63 != 0 {
216		return -f
217	}
218	return f
219}
220
221// The CTM is the mat3 of hdr.frag, transposed: GLSL lists columns, the blob
222// is row-major with a zero fourth column.
223func TestCTMBlobMatchesHDRShader(t *testing.T) {
224	src, err := os.ReadFile("../vulkan/shaders/hdr.frag")
225	if err != nil {
226		t.Fatal(err)
227	}
228	m := regexp.MustCompile(`mat3\(([^)]*)\)`).FindSubmatch(src)
229	if m == nil {
230		t.Fatal("no mat3 in hdr.frag")
231	}
232	var cols []float64 // column-major, as GLSL reads it
233	for _, f := range strings.Split(string(m[1]), ",") {
234		v, err := strconv.ParseFloat(strings.TrimSpace(f), 64)
235		if err != nil {
236			t.Fatal(err)
237		}
238		cols = append(cols, v)
239	}
240	if len(cols) != 9 {
241		t.Fatalf("mat3 has %d values", len(cols))
242	}
243	blob := ctmBytes(bt709ToBT2020)
244	if len(blob) != 96 {
245		t.Fatalf("blob is %d bytes, struct drm_color_ctm_3x4 is 96", len(blob))
246	}
247	for row := range 3 {
248		for col := range 4 {
249			got := fromS3132(binary.LittleEndian.Uint64(blob[(row*4+col)*8:]))
250			want := 0.0
251			if col < 3 {
252				want = cols[col*3+row] // out[row] = sum over col of M[col][row]*in[col]
253			}
254			if math.Abs(got-want) > 1e-9 {
255				t.Errorf("blob[%d][%d] = %v, want %v", row, col, got, want)
256			}
257		}
258	}
259	// Applied to white, as the shader would: a column vector times mat3.
260	var out [3]float64
261	for row := range 3 {
262		for col := range 3 {
263			out[row] += fromS3132(binary.LittleEndian.Uint64(blob[(row*4+col)*8:]))
264		}
265	}
266	for row, want := range [3]float64{
267		cols[0] + cols[3] + cols[6], cols[1] + cols[4] + cols[7], cols[2] + cols[5] + cols[8],
268	} {
269		if math.Abs(out[row]-want) > 1e-9 {
270			t.Errorf("white channel %d = %v, shader %v", row, out[row], want)
271		}
272	}
273}
274
275func TestPlaneColor(t *testing.T) {
276	pq := ports.SurfaceColor{TF: ports.ColorTFPQ, Primaries: ports.ColorPrimariesBT2020}
277	srgb := ports.SurfaceColor{TF: ports.ColorTFSRGB, Primaries: ports.ColorPrimariesSRGB}
278	ext := ports.SurfaceColor{TF: ports.ColorTFExtendedLinear, Primaries: ports.ColorPrimariesSRGB}
279	gamma := ports.SurfaceColor{TF: 10, Primaries: ports.ColorPrimariesSRGB}
280	p3 := ports.SurfaceColor{TF: ports.ColorTFSRGB, Primaries: 7}
281	pipe := &colorPipeline{}
282	for _, tc := range []struct {
283		name   string
284		c      ports.SurfaceColor
285		format uint32
286		opaque bool
287		hdr    bool
288		pipe   *colorPipeline
289		want   colorMode
290		reason string
291	}{
292		{"SDR output, sRGB", ports.SurfaceColor{}, fourccXRGB, true, false, nil, colorBypass, ""},
293		{"SDR output ignores a pipeline", srgb, fourccXRGB, true, false, pipe, colorBypass, ""},
294		{"SDR output, PQ", pq, fourccXR30, true, false, nil, colorBypass, "sdr_pq_content"},
295		{"SDR output, YUV", ports.SurfaceColor{}, fourccNV12, true, false, nil, colorBypass, "yuv"},
296		{"HDR, PQ 10-bit", pq, fourccXR30, true, true, pipe, colorBypass, ""},
297		{"HDR, PQ 10-bit with alpha", pq, fourccAR30, false, true, nil, colorBypass, ""},
298		{"HDR, PQ 8-bit", pq, fourccXRGB, true, true, pipe, colorBypass, "hdr_format"},
299		{"HDR, PQ YUV", pq, fourccP010, true, true, pipe, colorBypass, "yuv"},
300		{"HDR, unspecified colour", ports.SurfaceColor{}, fourccXRGB, true, true, pipe, colorSDRToPQ, ""},
301		{"HDR, sRGB", srgb, fourccXRGB, true, true, pipe, colorSDRToPQ, ""},
302		{"HDR, sRGB, 10-bit buffer", srgb, fourccXR30, true, true, pipe, colorSDRToPQ, ""},
303		{"HDR, sRGB, no pipeline", srgb, fourccXRGB, true, true, nil, colorBypass, "hdr_sdr_content"},
304		{"HDR, sRGB with alpha", srgb, fourccARGB, false, true, pipe, colorBypass, "hdr_sdr_content"},
305		{"HDR, extended linear", ext, fourccXRGB, true, true, pipe, colorBypass, "hdr_sdr_content"},
306		{"HDR, other transfer function", gamma, fourccXRGB, true, true, pipe, colorBypass, "hdr_sdr_content"},
307		{"HDR, other primaries", p3, fourccXRGB, true, true, pipe, colorBypass, "hdr_sdr_content"},
308		{"HDR, sRGB YUV", srgb, fourccNV12, true, true, pipe, colorBypass, "yuv"},
309	} {
310		t.Run(tc.name, func(t *testing.T) {
311			mode, reason := planeColor(tc.c, tc.format, tc.opaque, tc.hdr, tc.pipe)
312			if mode != tc.want || reason != tc.reason {
313				t.Fatalf("mode %d reason %q, want %d %q", mode, reason, tc.want, tc.reason)
314			}
315		})
316	}
317}
318
319// Colour test outputs.
320
321// colorOutput is an HDR output whose primary (and overlay) plane matches the
322// amdgpu pipeline. rule, when set, is the driver model every commit meets.
323func colorOutput(t *testing.T, overlay bool, rule func(*atomicReq, uint32) error) (*Output, *mockkms, *[]commitRec) {
324	t.Helper()
325	o, k, commits, _ := testOutputRules(t, rule)
326	props := maps0(planeProps)
327	props["zpos"] = pZpos
328	o.primary.props = props
329	o.primary.formats = []ports.DMABufFormat{{Format: fourccXRGB}}
330	colorPlane(k, o.primary, pColorPipe, opBase, amdgpuChain)
331	if overlay {
332		ov := &plane{id: tOverlay, typ: planeOverlay, props: props, zposValue: 2, formats: []ports.DMABufFormat{{Format: fourccXRGB}}}
333		colorPlane(k, ov, pColorPipeOverlay, opBase+20, amdgpuChain)
334		o.stray = []*plane{ov}
335		o.pickOverlay()
336		if o.overlay == nil {
337			t.Fatal("no overlay")
338		}
339	}
340	o.hdr.on = true
341	o.readColor()
342	o.ctmBlob, o.colorMult = 61, s3132(203.0/80)
343	if o.primary.pipeline == nil || overlay && o.overlay.pipeline == nil {
344		t.Fatal("pipeline not matched")
345	}
346	return o, k, commits
347}
348
349// colorOps are the colorop objects a commit touches.
350func colorOps(c commitRec) []uint32 {
351	var out []uint32
352	for _, obj := range c.req.objs {
353		if obj >= opBase {
354			out = append(out, obj)
355		}
356	}
357	return out
358}
359
360func sdrContent(o *Output) ports.SurfaceContent {
361	o.clientFBs[9] = &clientFB{fbID: 80}
362	return ports.SurfaceContent{ID: 1, Width: 200, Height: 100, Opaque: true, DMABuf: &ports.DMABuf{ID: 9, Format: fourccXRGB}}
363}
364
365func TestColorCommitTransitions(t *testing.T) {
366	o, _, commits := colorOutput(t, false, nil)
367	c := sdrContent(o)
368	c.Async = true
369	o.shown = 9
370	cp := o.primary.pipeline
371	last := func() commitRec { return (*commits)[len(*commits)-1] }
372
373	// First SDR scanout writes the pipeline, never async even though the
374	// buffer asks for it: only FB_ID may change in an async flip.
375	if ok, err := o.commitScanoutRect(80, c, pendingFrame{}, fullPlaneRect(200, 100), colorSDRToPQ); !ok || err != nil {
376		t.Fatalf("ok=%v err=%v", ok, err)
377	}
378	first := last()
379	if first.flags&flipAsyncFlag != 0 {
380		t.Fatal("a colour change went async")
381	}
382	if v, ok := first.req.value(tPrimary, pColorPipe); !ok || v != cp.id {
383		t.Fatalf("COLOR_PIPELINE %d %v, want %d", v, ok, cp.id)
384	}
385	for _, s := range []struct {
386		op, prop uint32
387		want     uint64
388	}{
389		{cp.srgb.op, cp.srgb.bypass, 0}, {cp.srgb.op, cp.srgb.prop, cp.srgb.curve},
390		{cp.mult.op, cp.mult.bypass, 0}, {cp.mult.op, cp.mult.prop, s3132(203.0 / 80)},
391		{cp.ctm.op, cp.ctm.bypass, 0}, {cp.ctm.op, cp.ctm.prop, 61},
392		{cp.pq.op, cp.pq.bypass, 0}, {cp.pq.op, cp.pq.prop, cp.pq.curve},
393	} {
394		if v, ok := first.req.value(s.op, s.prop); !ok || v != s.want {
395			t.Errorf("colorop %d prop %d = %d %v, want %d", s.op, s.prop, v, ok, s.want)
396		}
397	}
398	for _, other := range cp.others {
399		if v, ok := first.req.value(other.op, other.bypass); !ok || v != 1 {
400			t.Errorf("other colorop %d BYPASS %d %v, want 1", other.op, v, ok)
401		}
402	}
403	if !o.primary.colorKnown || o.primary.colorApplied != colorSDRToPQ {
404		t.Fatalf("applied %v known %v", o.primary.colorApplied, o.primary.colorKnown)
405	}
406
407	// Same mode again: nothing is written, and the flip may be async.
408	o.frame.endPending()
409	o.vrrOn = true
410	if ok, err := o.commitScanoutRect(80, c, pendingFrame{}, fullPlaneRect(200, 100), colorSDRToPQ); !ok || err != nil {
411		t.Fatalf("ok=%v err=%v", ok, err)
412	}
413	if second := last(); second.flags&flipAsyncFlag == 0 || len(colorOps(second)) != 0 {
414		t.Fatalf("steady state: flags %#x colorops %v", second.flags, colorOps(second))
415	} else if _, ok := second.req.value(tPrimary, pColorPipe); ok {
416		t.Fatal("steady state wrote COLOR_PIPELINE")
417	}
418
419	// A composed frame sets Bypass once.
420	o.frame.endPending()
421	if err := o.commitFrame(70, nil, true, false, pendingFrame{}); err != nil {
422		t.Fatal(err)
423	}
424	composed := last()
425	if composed.flags&flipAsyncFlag != 0 {
426		t.Fatal("the return to Bypass went async")
427	}
428	if v, ok := composed.req.value(tPrimary, pColorPipe); !ok || v != 0 || len(colorOps(composed)) != 0 {
429		t.Fatalf("composed: COLOR_PIPELINE %d %v colorops %v", v, ok, colorOps(composed))
430	}
431	o.frame.endPending()
432	if err := o.commitFrame(70, nil, false, false, pendingFrame{}); err != nil {
433		t.Fatal(err)
434	}
435	again := last()
436	if _, ok := again.req.value(tPrimary, pColorPipe); ok {
437		t.Fatal("Bypass written twice")
438	}
439}
440
441// A plane that applies the pipeline never sees colorop properties in a
442// cursor or VRR commit.
443func TestCommitStateNeverCarriesColorProps(t *testing.T) {
444	o, _, commits := colorOutput(t, false, nil)
445	o.primary.colorKnown, o.primary.colorApplied = false, colorSDRToPQ // stale on purpose
446	o.cursor.image = true
447	o.cursor.Move(3, 4)
448	if err := o.commitState(true); err != nil {
449		t.Fatal(err)
450	}
451	c := (*commits)[0]
452	if _, ok := c.req.value(tPrimary, pColorPipe); ok || len(colorOps(c)) != 0 {
453		t.Fatalf("state commit carries colour: %+v", c.req)
454	}
455	if _, ok := c.req.value(tCursor, 14); !ok {
456		t.Fatal("cursor not moved")
457	}
458}
459
460// Protected frames, power off, Close and every modeset leave Bypass, on the
461// overlay too, whatever was applied or known.
462func TestColorBypassOnComposedProtectedOffCloseAndModeset(t *testing.T) {
463	apply := func(o *Output) {
464		for _, p := range []*plane{o.primary, o.overlay} {
465			p.colorKnown, p.colorApplied = true, colorSDRToPQ
466		}
467	}
468	bypassed := func(t *testing.T, c commitRec) {
469		t.Helper()
470		for _, p := range []struct{ plane, prop uint32 }{{tPrimary, pColorPipe}, {tOverlay, pColorPipeOverlay}} {
471			if v, ok := c.req.value(p.plane, p.prop); !ok || v != 0 {
472				t.Errorf("plane %d COLOR_PIPELINE %d %v, want Bypass", p.plane, v, ok)
473			}
474		}
475		if ops := colorOps(c); len(ops) != 0 {
476			t.Errorf("Bypass commit touches colorops %v", ops)
477		}
478	}
479	t.Run("power off", func(t *testing.T) {
480		o, _, commits := colorOutput(t, true, nil)
481		apply(o)
482		if err := o.powerOff(); err != nil {
483			t.Fatal(err)
484		}
485		bypassed(t, (*commits)[0])
486		if !o.primary.colorKnown || o.primary.colorApplied != colorBypass || o.overlay.colorApplied != colorBypass {
487			t.Fatal("power off did not record Bypass")
488		}
489	})
490	t.Run("close", func(t *testing.T) {
491		o, k, commits := colorOutput(t, true, nil)
492		k.EXPECT().rmFB(mock.Anything).Return(nil)
493		k.EXPECT().destroyBlob(uint32(61)).Return(nil).Once()
494		// Unknown state: Close still writes Bypass.
495		o.Close()
496		bypassed(t, (*commits)[0])
497		if o.ctmBlob != 0 {
498			t.Fatal("CTM blob kept")
499		}
500	})
501	t.Run("modeset", func(t *testing.T) {
502		o, k, commits := colorOutput(t, true, nil)
503		k.EXPECT().createBlob(mock.Anything).Return(99, nil).Once()
504		k.EXPECT().objProps(uint32(tOverlay), uint32(objPlane)).Return(map[string][2]uint64{"CRTC_ID": {pCrtcID, 0}}, nil)
505		apply(o)
506		o.primary.verdicts = []colorVerdict{{format: fourccXRGB}}
507		if err := o.modeset(); err != nil {
508			t.Fatal(err)
509		}
510		bypassed(t, (*commits)[0])
511		if o.primary.colorKnown || o.overlay.colorKnown || len(o.primary.verdicts) != 0 {
512			t.Fatal("a modeset kept the colour state or the refusals")
513		}
514	})
515	t.Run("protected frame", func(t *testing.T) {
516		o, _, commits := colorOutput(t, true, nil)
517		apply(o)
518		o.protected = true
519		if err := o.commitWith(70, nil, false, false, pendingFrame{}, overlayWin{}); err != nil {
520			t.Fatal(err)
521		}
522		c := (*commits)[0]
523		if v, ok := c.req.value(tPrimary, pColorPipe); !ok || v != 0 {
524			t.Fatalf("protected frame: COLOR_PIPELINE %d %v", v, ok)
525		}
526	})
527}
528
529// The overlay's pipeline is driven with the primary on Bypass; a frame
530// without the overlay turns it back to Bypass once.
531func TestColorOverlayTransitions(t *testing.T) {
532	o, _, commits := colorOutput(t, true, nil)
533	o.clientFBs[9] = &clientFB{fbID: 88}
534	ov := overlayWin{id: 2, fb: 88, buf: 9, w: 100, h: 100, rect: ports.Rect{X: 100, W: 100, H: 100}, color: colorUse{mode: colorSDRToPQ, format: fourccXRGB}}
535	if err := o.commitWith(70, nil, false, false, pendingFrame{}, ov); err != nil {
536		t.Fatal(err)
537	}
538	c := (*commits)[0]
539	cp := o.overlay.pipeline
540	if v, ok := c.req.value(tOverlay, pColorPipeOverlay); !ok || v != cp.id {
541		t.Fatalf("overlay COLOR_PIPELINE %d %v, want %d", v, ok, cp.id)
542	}
543	if v, ok := c.req.value(tPrimary, pColorPipe); !ok || v != 0 {
544		t.Fatalf("composed primary COLOR_PIPELINE %d %v, want Bypass", v, ok)
545	}
546	o.frame.endPending()
547	if err := o.commitWith(70, nil, false, false, pendingFrame{}, ov); err != nil {
548		t.Fatal(err)
549	}
550	if again := (*commits)[1]; len(colorOps(again)) != 0 {
551		t.Fatal("overlay colour written twice")
552	} else if _, ok := again.req.value(tOverlay, pColorPipeOverlay); ok {
553		t.Fatal("overlay COLOR_PIPELINE written twice")
554	}
555	o.frame.endPending()
556	if err := o.commitWith(70, nil, false, false, pendingFrame{}, overlayWin{}); err != nil {
557		t.Fatal(err)
558	}
559	if v, ok := (*commits)[2].req.value(tOverlay, pColorPipeOverlay); !ok || v != 0 {
560		t.Fatalf("overlay left without Bypass: %d %v", v, ok)
561	}
562}
563
564// refusePipeline is a driver that refuses any request selecting a pipeline
565// on a plane with EINVAL, TEST_ONLY included.
566func refusePipeline(r *atomicReq, _ uint32) error {
567	for _, obj := range [...]struct{ plane, prop uint32 }{{tPrimary, pColorPipe}, {tOverlay, pColorPipeOverlay}} {
568		if v, ok := r.value(obj.plane, obj.prop); ok && v != 0 {
569			return unix.EINVAL
570		}
571	}
572	return nil
573}
574
575func hdrScanoutScene() (ports.Scene, map[ports.WindowID]ports.SurfaceContent) {
576	s, c := fullscreenScene()
577	return s, c
578}
579
580func countTests(commits []commitRec) int {
581	n := 0
582	for _, c := range commits {
583		if c.flags&atomicTestOnly != 0 {
584			n++
585		}
586	}
587	return n
588}
589
590// SDR scanout on HDR is tested before its first use. A refusal is cached per
591// plane, format and cursor state and is never tested again; a modeset asks
592// the driver again.
593func TestColorRefusalIsTestedOnceAndCached(t *testing.T) {
594	o, k, commits := colorOutput(t, false, refusePipeline)
595	k.EXPECT().addFB(mock.Anything, uint32(fourccXRGB)).Return(uint32(80), nil).Maybe()
596	k.EXPECT().createBlob(mock.Anything).Return(99, nil).Maybe()
597	s, surfaces := hdrScanoutScene()
598	decide := func() string {
599		t.Helper()
600		if fb, _, _ := o.scanoutFrame(s, surfaces); fb != 0 {
601			t.Fatalf("refused pipeline scanned out (fb %d)", fb)
602		}
603		return o.reason
604	}
605	if got := decide(); got != "color_refused" || countTests(*commits) != 1 {
606		t.Fatalf("reason %q after %d tests", got, countTests(*commits))
607	}
608	for range 3 {
609		decide()
610	}
611	if n := countTests(*commits); n != 1 {
612		t.Fatalf("a cached refusal was tested %d times", n)
613	}
614	if cfb := o.clientFBs[9]; cfb.failed != "" {
615		t.Fatalf("buffer marked failed: %q", cfb.failed)
616	}
617	// The cursor shown is another state: asked once more, then cached.
618	o.cursor.image = true
619	o.cursor.Move(1, 1)
620	decide()
621	decide()
622	if n := countTests(*commits); n != 2 {
623		t.Fatalf("%d tests with the cursor shown, want 2", n)
624	}
625	// A modeset asks the driver again.
626	o.forgetColorVerdicts()
627	decide()
628	if n := countTests(*commits); n != 3 {
629		t.Fatalf("a modeset did not ask the driver again: %d tests", n)
630	}
631}
632
633// KMS accepts the colour test and refuses the real commit: the frame is
634// composed, the refusal cached, the buffer not blamed.
635func TestColorRefusalOfCommittedFrame(t *testing.T) {
636	// The first request that selects the pipeline outside TEST_ONLY fails.
637	o, k, commits := colorOutput(t, false, func(r *atomicReq, flags uint32) error {
638		if flags&atomicTestOnly == 0 {
639			return refusePipeline(r, flags)
640		}
641		return nil
642	})
643	k.EXPECT().addFB(mock.Anything, uint32(fourccXRGB)).Return(uint32(80), nil).Maybe()
644	s, surfaces := hdrScanoutScene()
645	d := o.decideFrame(s, surfaces, false)
646	if d.fb != 80 || d.color != colorSDRToPQ {
647		t.Fatalf("decision %+v", d)
648	}
649	ok, err := o.commitScanoutRect(d.fb, d.content, pendingFrame{}, d.rect, d.color)
650	if ok || err != nil || o.reason != "color_refused" {
651		t.Fatalf("ok=%v err=%v reason %q", ok, err, o.reason)
652	}
653	if o.frame.pendingCommit() || o.primary.colorKnown {
654		t.Fatal("a refused commit changed the state")
655	}
656	before := len(*commits)
657	if d := o.decideFrame(s, surfaces, false); d.fb != 0 || o.reason != "color_refused" || len(*commits) != before {
658		t.Fatalf("refusal not cached: fb %d reason %q commits %d->%d", d.fb, o.reason, before, len(*commits))
659	}
660	if cfb := o.clientFBs[9]; cfb.failed != "" || cfb.noAsync {
661		t.Fatalf("buffer blamed: %+v", cfb)
662	}
663}
664
665// A buffer KMS refuses on its own is still blamed on the buffer, not the
666// pipeline.
667func TestColorRefusalOfTheBufferStaysTheBuffers(t *testing.T) {
668	o, k, _ := colorOutput(t, false, func(r *atomicReq, flags uint32) error {
669		// The driver takes the pipeline test, then refuses the buffer
670		// whatever the pipeline: the commit and its Bypass test fail.
671		if v, ok := r.value(tPrimary, pColorPipe); flags&atomicTestOnly == 0 || ok && v == 0 {
672			return unix.EINVAL
673		}
674		return nil
675	})
676	k.EXPECT().addFB(mock.Anything, uint32(fourccXRGB)).Return(uint32(80), nil).Maybe()
677	s, surfaces := hdrScanoutScene()
678	d := o.decideFrame(s, surfaces, false)
679	if ok, err := o.commitScanoutRect(d.fb, d.content, pendingFrame{}, d.rect, d.color); ok || err != nil {
680		t.Fatalf("ok=%v err=%v", ok, err)
681	}
682	if cfb := o.clientFBs[9]; cfb.failed != "flip_refused" || o.reason != "flip_refused" {
683		t.Fatalf("failed %q reason %q", cfb.failed, o.reason)
684	}
685}
686
687// A cursor commit refused while a plane shows the pipeline caches the
688// refusal for the cursor state and has the next frame composed.
689func TestColorCursorConflict(t *testing.T) {
690	o, _, _ := colorOutput(t, false, func(r *atomicReq, flags uint32) error {
691		if v, ok := r.value(tCursor, pFB); ok && v != 0 {
692			return unix.EINVAL
693		}
694		return nil
695	})
696	o.primary.colorKnown, o.primary.colorApplied, o.primary.colorFormat = true, colorSDRToPQ, fourccXRGB
697	o.cursor.image = true
698	o.cursor.Move(5, 5)
699	if err := o.commitState(false); err != errOverlayDropped {
700		t.Fatalf("commitState: %v", err)
701	}
702	if ok, known := o.primary.verdict(fourccXRGB, true); !known || ok {
703		t.Fatal("refusal with the cursor not cached")
704	}
705	if o.cursor.off {
706		t.Fatal("the cursor was blamed")
707	}
708}
709
710// Overlay: allowed in HDR when the colour resolves; a refusal is cached.
711func TestColorOverlayInHDR(t *testing.T) {
712	o, k, commits := colorOutput(t, true, refusePipeline)
713	o.primary.pipeline = nil // only the overlay converts
714	k.EXPECT().addFB(mock.Anything, uint32(fourccXRGB)).Return(uint32(88), nil).Maybe()
715	s, surfaces := overlayScene()
716	// PQ content and SDR content without alpha are both candidates now.
717	ov, rest := o.overlayFrame(s, surfaces)
718	if ov.fb != 88 || ov.color.mode != colorSDRToPQ || len(rest.Windows) != 1 {
719		t.Fatalf("overlay %+v", ov)
720	}
721	if o.testOverlay(70, ov) {
722		t.Fatal("the driver's refusal was taken")
723	}
724	if o.overlayReason != "color_refused" {
725		t.Fatalf("reason %q", o.overlayReason)
726	}
727	if cfb := o.clientFBs[9]; cfb.overlayFailed != "" {
728		t.Fatalf("buffer blamed: %q", cfb.overlayFailed)
729	}
730	tests := countTests(*commits)
731	if ov, rest := o.overlayFrame(s, surfaces); ov.fb != 0 || o.overlayReason != "color_refused" || len(rest.Windows) != 2 || countTests(*commits) != tests {
732		t.Fatalf("not cached: %+v reason %q", ov, o.overlayReason)
733	}
734	// Without a pipeline an SDR window stays composed on HDR.
735	o.overlay.pipeline = nil
736	o.forgetColorVerdicts()
737	if ov, _ := o.overlayFrame(s, surfaces); ov.fb != 0 || o.overlayReason != "no_candidate" {
738		t.Fatalf("no pipeline: %+v reason %q", ov, o.overlayReason)
739	}
740}
741
742func TestSetupColor(t *testing.T) {
743	t.Run("HDR makes the matrix blob and the multiplier", func(t *testing.T) {
744		o, k, _ := colorOutput(t, false, nil)
745		o.ctmBlob, o.colorMult = 0, 0
746		o.hdr.settings = HDRSettings{Enabled: true, SDRBrightness: 160}
747		o.hdr.cap.Capable = true
748		k.EXPECT().createBlob(mock.MatchedBy(func(b []byte) bool { return slices.Equal(b, ctmBytes(bt709ToBT2020)) })).Return(uint32(61), nil).Once()
749		o.setupColor()
750		if o.ctmBlob != 61 || o.colorMult != 2<<32 {
751			t.Fatalf("blob %d multiplier %#x", o.ctmBlob, o.colorMult)
752		}
753	})
754	t.Run("blob failure turns the pipelines off", func(t *testing.T) {
755		o, k, _ := colorOutput(t, true, nil)
756		o.ctmBlob = 0
757		o.hdr.settings = HDRSettings{Enabled: true, SDRBrightness: 203}
758		o.hdr.cap.Capable = true
759		k.EXPECT().createBlob(mock.Anything).Return(uint32(0), unix.ENOMEM).Once()
760		o.setupColor()
761		if o.primary.pipeline != nil || o.overlay.pipeline != nil || o.ctmBlob != 0 {
762			t.Fatal("pipelines kept without their blob")
763		}
764	})
765	t.Run("SDR output keeps Bypass", func(t *testing.T) {
766		o, _, _ := colorOutput(t, false, nil)
767		o.ctmBlob = 0
768		o.hdr.settings.Enabled = false
769		o.setupColor()
770		if o.primary.pipeline != nil || o.ctmBlob != 0 {
771			t.Fatal("SDR output set up a pipeline")
772		}
773	})
774}
775
776// A frame refused because of its pipeline never turns the hardware cursor
777// off: only a refusal the frame has with Bypass does.
778func TestColorRefusalDoesNotDisableTheCursor(t *testing.T) {
779	// The driver refuses the pipeline, and also the cursor whenever it is
780	// shown with the pipeline.
781	rule := func(r *atomicReq, flags uint32) error {
782		if v, ok := r.value(tPrimary, pColorPipe); ok && v != 0 && flags&atomicTestOnly == 0 {
783			return unix.EINVAL
784		}
785		return nil
786	}
787	o, k, _ := colorOutput(t, false, rule)
788	k.EXPECT().addFB(mock.Anything, uint32(fourccXRGB)).Return(uint32(80), nil).Maybe()
789	o.cursor.image = true
790	o.cursor.Move(5, 5)
791	c := sdrContent(o)
792	ok, err := o.commitScanoutRect(80, c, pendingFrame{}, fullPlaneRect(200, 100), colorSDRToPQ)
793	if ok || err != nil || o.reason != "color_refused" {
794		t.Fatalf("ok=%v err=%v reason %q", ok, err, o.reason)
795	}
796	if o.cursor.off {
797		t.Fatal("a pipeline refusal turned the hardware cursor off")
798	}
799	if v, known := o.primary.verdict(fourccXRGB, true); !known || v {
800		t.Fatal("refusal with the cursor not cached")
801	}
802	// A cursor the driver refuses on its own, pipeline or not, is still turned off.
803	o2, k2, _ := colorOutput(t, false, func(r *atomicReq, flags uint32) error {
804		if v, ok := r.value(tCursor, pFB); ok && v != 0 {
805			return unix.EINVAL
806		}
807		return nil
808	})
809	k2.EXPECT().addFB(mock.Anything, uint32(fourccXRGB)).Return(uint32(80), nil).Maybe()
810	o2.cursor.image = true
811	o2.cursor.Move(5, 5)
812	if ok, err := o2.commitScanoutRect(80, sdrContent(o2), pendingFrame{}, fullPlaneRect(200, 100), colorSDRToPQ); !ok || err != nil {
813		t.Fatalf("ok=%v err=%v", ok, err)
814	}
815	if !o2.cursor.off {
816		t.Fatal("a cursor refusal did not turn the cursor off")
817	}
818}
819
820// The colour test differs from the failed commit in colour alone. A frame
821// refused for its content hint is not blamed on the pipeline; the hint is
822// disabled as before. A frame refused for its pipeline, with the hint wanted,
823// is blamed on the pipeline and the hint stays.
824func TestColorRefusalBlamesOnlyColour(t *testing.T) {
825	const pContent = 70
826	setup := func(t *testing.T, rule func(*atomicReq, uint32) error) (*Output, ports.SurfaceContent) {
827		o, k, _ := colorOutput(t, false, rule)
828		k.EXPECT().addFB(mock.Anything, uint32(fourccXRGB)).Return(uint32(80), nil).Maybe()
829		o.contentProp, o.contentValues = pContent, [5]uint64{0, 1, 2, 3, 4}
830		o.contentWanted = 4 // wanted Game, the applied value is still the first
831		return o, sdrContent(o)
832	}
833	hasHint := func(r *atomicReq) bool { v, ok := r.value(tConn, pContent); return ok && v == 4 }
834	usesPipeline := func(r *atomicReq) bool { v, ok := r.value(tPrimary, pColorPipe); return ok && v != 0 }
835	t.Run("the hint is refused, the pipeline is accepted", func(t *testing.T) {
836		o, c := setup(t, func(r *atomicReq, flags uint32) error {
837			if hasHint(r) {
838				return unix.EINVAL
839			}
840			return nil
841		})
842		ok, err := o.commitScanoutRect(80, c, pendingFrame{}, fullPlaneRect(200, 100), colorSDRToPQ)
843		if !ok || err != nil {
844			t.Fatalf("ok=%v err=%v reason %q", ok, err, o.reason)
845		}
846		if o.contentProp != 0 {
847			t.Fatal("the content hint was not disabled")
848		}
849		if _, known := o.primary.verdict(fourccXRGB, false); known || o.reason == "color_refused" {
850			t.Fatal("the pipeline was blamed for the hint")
851		}
852		if !o.primary.colorKnown || o.primary.colorApplied != colorSDRToPQ {
853			t.Fatal("the retried frame did not apply the pipeline")
854		}
855	})
856	t.Run("the pipeline is refused, the hint is accepted", func(t *testing.T) {
857		o, c := setup(t, func(r *atomicReq, flags uint32) error {
858			if usesPipeline(r) && flags&atomicTestOnly == 0 {
859				return unix.EINVAL
860			}
861			return nil
862		})
863		ok, err := o.commitScanoutRect(80, c, pendingFrame{}, fullPlaneRect(200, 100), colorSDRToPQ)
864		if ok || err != nil || o.reason != "color_refused" {
865			t.Fatalf("ok=%v err=%v reason %q", ok, err, o.reason)
866		}
867		if v, known := o.primary.verdict(fourccXRGB, false); !known || v {
868			t.Fatal("the pipeline refusal was not cached")
869		}
870		if o.contentProp != pContent {
871			t.Fatal("the content hint was disabled for the pipeline's refusal")
872		}
873	})
874}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 23

Tags 10

↑↓ moveenter openesc close