main f2d166b

colorop_test.go RawHistoryFolder

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

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