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}