v0.4.0 b6be3a8

colorop_match.go RawHistoryFolder

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}

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