1package drm
2
3import (
4 "encoding/binary"
5 "fmt"
6 "math"
7 "slices"
8 "strings"
9
10 "github.com/bnema/neferwl/internal/ports"
11)
12
13// Plane colour pipelines (DRM_CLIENT_CAP_PLANE_COLOR_PIPELINE).
14//
15// On an HDR10 output the composed image is PQ-encoded BT.2020. A plain sRGB
16// client buffer can still reach the screen without composition when the
17// plane's colour pipeline does what the Vulkan HDR path does to it:
18//
19// sRGB EOTF -> x SDRBrightness/80 -> BT.709 to BT.2020 -> PQ inverse EOTF
20//
21// (PQ 125: 1.0 is 80 nits). The kernel exposes pipelines as chains of colorop
22// objects. The matcher below is driver-agnostic: it looks for these four
23// operations in this order and needs every other operation to be bypassable.
24
25// colorMode is what a plane's colour pipeline does to the buffer it shows.
26type colorMode uint8
27
28const (
29 // colorBypass is the plane's COLOR_PIPELINE "Bypass": pixels go as they are.
30 colorBypass colorMode = iota
31 // colorSDRToPQ turns sRGB pixels into the output's PQ BT.2020 signal.
32 colorSDRToPQ
33)
34
35// colorUse is a plane's colour mode with the format of the buffer it
36// applies to (refusals are cached per format).
37type colorUse struct {
38 mode colorMode
39 format uint32
40}
41
42// Plane and colorop property names and the enum names the matcher looks for.
43// Enum values are never hardcoded: they come from the property.
44const (
45 propColorPipeline = "COLOR_PIPELINE"
46
47 colorTypeCurve = "1D Curve"
48 colorTypeMultiplier = "Multiplier"
49 colorTypeMatrix = "3x4 Matrix"
50 curveSRGBEOTF = "sRGB EOTF"
51 curvePQInverseEOTF = "PQ 125 Inverse EOTF"
52)
53
54// colorop is one operation of a pipeline as read from KMS: property IDs by
55// role (0: the operation has none) and the enum of its curve.
56type colorop struct {
57 id uint32
58 typ string
59 bypass, curveProp, multiplier, data uint32
60 curves map[string]uint64
61}
62
63// colorStage is an operation the output drives: its object, BYPASS and the
64// property that configures it (CURVE_1D_TYPE, MULTIPLIER or DATA).
65type colorStage struct {
66 op, bypass, prop uint32
67 // curve is the CURVE_1D_TYPE enum value of a curve stage.
68 curve uint64
69}
70
71// colorPipeline is the plane's pipeline for SDR content on an HDR output.
72type colorPipeline struct {
73 // id is the COLOR_PIPELINE value: the pipeline's first colorop.
74 id uint64
75 // srgb, mult, ctm and pq are the four stages in pipeline order; others
76 // are every other operation, which must stay bypassed.
77 srgb, mult, ctm, pq colorStage
78 others []colorStage
79}
80
81// is reports whether the operation is the wanted type, with the wanted curve.
82func (o colorop) is(typ, curve string) bool {
83 if o.typ != typ {
84 return false
85 }
86 switch typ {
87 case colorTypeCurve:
88 _, ok := o.curves[curve]
89 return o.curveProp != 0 && ok
90 case colorTypeMultiplier:
91 return o.multiplier != 0
92 case colorTypeMatrix:
93 return o.data != 0
94 }
95 return false
96}
97
98// matchColorPipeline looks in ops, the pipeline starting at colorop id first,
99// for a 1D curve with sRGB EOTF, a multiplier, a 3x4 matrix and a 1D curve
100// with PQ 125 inverse EOTF, in this order. Any other operation must be
101// bypassable. The reason says why a pipeline does not match.
102func matchColorPipeline(first uint64, ops []colorop) (*colorPipeline, string) {
103 want := [4]struct{ typ, curve string }{
104 {colorTypeCurve, curveSRGBEOTF}, {colorTypeMultiplier, ""}, {colorTypeMatrix, ""}, {colorTypeCurve, curvePQInverseEOTF},
105 }
106 cp := &colorPipeline{id: first}
107 stages := [4]*colorStage{&cp.srgb, &cp.mult, &cp.ctm, &cp.pq}
108 n := 0
109 for _, op := range ops {
110 if n < len(want) && op.is(want[n].typ, want[n].curve) {
111 s := stages[n]
112 s.op, s.bypass = op.id, op.bypass
113 switch n {
114 case 0:
115 s.prop, s.curve = op.curveProp, op.curves[curveSRGBEOTF]
116 case 1:
117 s.prop = op.multiplier
118 case 2:
119 s.prop = op.data
120 case 3:
121 s.prop, s.curve = op.curveProp, op.curves[curvePQInverseEOTF]
122 }
123 n++
124 continue
125 }
126 if op.bypass == 0 {
127 return nil, fmt.Sprintf("colorop %d (%s) cannot be bypassed", op.id, op.typ)
128 }
129 cp.others = append(cp.others, colorStage{op: op.id, bypass: op.bypass})
130 }
131 if n < len(want) {
132 return nil, fmt.Sprintf("no %s -> %s -> %s -> %s chain", want[0].curve, want[1].typ, want[2].typ, want[3].curve)
133 }
134 return cp, ""
135}
136
137// maxColorops bounds a pipeline walk: a longer chain is a cycle or garbage.
138const maxColorops = 32
139
140// walkColorops reads the pipeline starting at colorop first, following NEXT.
141func walkColorops(k kms, first uint32) ([]colorop, error) {
142 var ops []colorop
143 for id := first; id != 0; {
144 if len(ops) >= maxColorops {
145 return nil, fmt.Errorf("colorop chain longer than %d", maxColorops)
146 }
147 props, err := k.objProps(id, objColorop)
148 if err != nil {
149 return nil, fmt.Errorf("colorop %d: %w", id, err)
150 }
151 op := colorop{id: id, bypass: uint32(props["BYPASS"][0]), curveProp: uint32(props["CURVE_1D_TYPE"][0]), multiplier: uint32(props["MULTIPLIER"][0]), data: uint32(props["DATA"][0])}
152 if t := props["TYPE"]; t[0] != 0 {
153 types, err := k.propEnums(uint32(t[0]))
154 if err != nil {
155 return nil, fmt.Errorf("colorop %d type: %w", id, err)
156 }
157 for name, v := range types {
158 if v == t[1] {
159 op.typ = name
160 }
161 }
162 }
163 if op.curveProp != 0 {
164 if op.curves, err = k.propEnums(op.curveProp); err != nil {
165 return nil, fmt.Errorf("colorop %d curves: %w", id, err)
166 }
167 }
168 ops = append(ops, op)
169 id = uint32(props["NEXT"][1])
170 }
171 return ops, nil
172}
173
174// readColorPipeline returns the pipeline of p that does SDR to PQ, or why
175// it has none. Call it once per plane (read at output creation).
176func readColorPipeline(k kms, p *plane) (*colorPipeline, string) {
177 prop := p.props[propColorPipeline]
178 if prop == 0 {
179 return nil, "no " + propColorPipeline + " property"
180 }
181 enums, err := k.propEnums(prop)
182 if err != nil {
183 return nil, err.Error()
184 }
185 // Name order is not stable: try the pipelines by colorop ID.
186 var firsts []uint64
187 for _, v := range enums {
188 if v != 0 { // 0 is Bypass
189 firsts = append(firsts, v)
190 }
191 }
192 slices.Sort(firsts)
193 var why []string
194 for _, first := range firsts {
195 ops, err := walkColorops(k, uint32(first))
196 if err == nil {
197 var cp *colorPipeline
198 var reason string
199 if cp, reason = matchColorPipeline(first, ops); cp != nil {
200 return cp, ""
201 }
202 err = fmt.Errorf("%s", reason)
203 }
204 why = append(why, fmt.Sprintf("pipeline %d: %v", first, err))
205 }
206 if len(why) == 0 {
207 return nil, "only Bypass"
208 }
209 return nil, strings.Join(why, "; ")
210}
211
212// bt709ToBT2020 converts linear BT.709 to BT.2020 primaries, row-major. It is
213// the mat3 of vulkan/shaders/hdr.frag, which GLSL writes column by column.
214var bt709ToBT2020 = [3][3]float64{
215 {0.627404, 0.329283, 0.043313},
216 {0.069097, 0.919540, 0.011362},
217 {0.016391, 0.088013, 0.895595},
218}
219
220// s3132 encodes v as the S31.32 sign-magnitude fixed point of KMS colorops
221// (not two's complement): bit 63 is the sign, the rest the magnitude.
222func s3132(v float64) uint64 {
223 m := uint64(math.Round(math.Abs(v) * (1 << 32)))
224 if m > 1<<63-1 {
225 m = 1<<63 - 1
226 }
227 if v < 0 {
228 m |= 1 << 63
229 }
230 return m
231}
232
233// ctmBytes is the DATA blob of a 3x4 matrix colorop (struct
234// drm_color_ctm_3x4): 12 S31.32 values, row-major; the fourth column, an
235// offset, is zero.
236func ctmBytes(m [3][3]float64) []byte {
237 b := make([]byte, 0, 12*8)
238 for _, row := range m {
239 for _, v := range row {
240 b = binary.LittleEndian.AppendUint64(b, s3132(v))
241 }
242 b = binary.LittleEndian.AppendUint64(b, 0)
243 }
244 return b
245}
246
247// planeColor decides how plane (with pipeline, nil: none) shows a client
248// buffer of format whose content is c, on an output in HDR10 mode when hdrOn.
249// opaque is the surface's Opaque. A buffer already in the output's encoding
250// goes as it is; sRGB goes through the pipeline; anything else gets the reason
251// it is composed.
252func planeColor(c ports.SurfaceColor, format uint32, opaque, hdrOn bool, pipeline *colorPipeline) (colorMode, string) {
253 switch {
254 case isYUVFormat(format):
255 // With the colour pipeline cap on, the kernel rejects the plane's
256 // COLOR_ENCODING and COLOR_RANGE: a YUV buffer cannot be scanned
257 // out whatever the output, and is composed.
258 return colorBypass, "yuv"
259 case !hdrOn:
260 // An SDR connector must never show raw PQ values.
261 if c.IsPQ2020() {
262 return colorBypass, "sdr_pq_content"
263 }
264 return colorBypass, ""
265 case c.IsPQ2020():
266 if !isTenBit(format) {
267 return colorBypass, "hdr_format"
268 }
269 return colorBypass, ""
270 case pipeline == nil || !opaque || !isSDRColor(c):
271 // The kernel applies the pipeline to premultiplied pixels: exact
272 // only without alpha.
273 return colorBypass, "hdr_sdr_content"
274 }
275 return colorSDRToPQ, ""
276}
277
278// isSDRColor reports whether the surface is sRGB, set or unspecified: what the
279// Vulkan path decodes with decodeSRGB. Extended linear and PQ are other encodings.
280func isSDRColor(c ports.SurfaceColor) bool {
281 if c.IsPQ2020() || c.IsExtendedLinear() {
282 return false
283 }
284 return (c.TF == 0 || c.TF == ports.ColorTFSRGB) && (c.Primaries == 0 || c.Primaries == ports.ColorPrimariesSRGB)
285}