detached 99e15b4

fix(drm): enable the identity 3D LUT with the PQ shaper of the plane colour pipeline

On amdgpu the 'PQ 125 Inverse EOTF' curve after the 3x4 matrix is the
shaper of the MPC MCM shaper + 3D LUT block. The kernel programs the
shaper RAM but the block only applies it with the 3D LUT on (gamescope
and the driver comment '3D LUT requires shaper' both treat them as a
pair): with the 3D LUT bypassed, linear BT.2020 values left the plane
and were read as PQ codes, hence the saturated and washed-out image of
an SDR buffer scanned out on an HDR10 output.

The matcher now records a sized 3D LUT that follows the PQ curve; the
output makes an identity LUT blob at setup and enables that colorop with
the four stages. Pipelines without a 3D LUT are driven as before, and
the PQ Bypass path for HDR buffers is unchanged.
commit
99e15b4a25c022f86b5de7d986ffe476f4034bf2
author
brice · · 2026-10-10 20:29 UTC
committer
brice · · 2026-10-23 05:24 UTC
parent
14b3345
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
7 files · +190 -27
M internal/adapters/drm/amdgpu_rules_test.go +1 -1
@@ -494,7 +494,7 @@ func TestColorPipelineSequenceAcceptedByAmdgpuRules(t *testing.T) {
494494 k.EXPECT().rmFB(mock.Anything).Return(nil).Maybe()
495495 enableTestHDR(o)
496496 o.hdr.blob = 55
497- m.live[55], m.live[61] = true, true
497+ m.live[55], m.live[61], m.live[lut3dBlob] = true, true, true
498498 o.modeBlob = 99
499499 if err := o.modeset(); err != nil {
500500 t.Fatalf("modeset: %v", err)
@@ -494,7 +494,7 @@ func TestColorPipelineSequenceAcceptedByAmdgpuRules(t *testing.T) {
494 k.EXPECT().rmFB(mock.Anything).Return(nil).Maybe()494 k.EXPECT().rmFB(mock.Anything).Return(nil).Maybe()
495 enableTestHDR(o)495 enableTestHDR(o)
496 o.hdr.blob = 55496 o.hdr.blob = 55
497- m.live[55], m.live[61] = true, true497+ m.live[55], m.live[61], m.live[lut3dBlob] = true, true, true
498 o.modeBlob = 99498 o.modeBlob = 99
499 if err := o.modeset(); err != nil {499 if err := o.modeset(); err != nil {
500 t.Fatalf("modeset: %v", err)500 t.Fatalf("modeset: %v", err)
M internal/adapters/drm/colorop.go +40 -11
@@ -23,9 +23,10 @@ func (p *plane) colorStale(mode colorMode) bool {
2323
2424 // colorProps puts mode on p: Bypass selects the plane's Bypass pipeline;
2525 // colorSDRToPQ selects the matched one with its four operations enabled and
26-// every other one bypassed. mult is the S31.32 multiplier and ctm the matrix
27-// blob.
28-func (p *plane) colorProps(req *atomicReq, mode colorMode, mult uint64, ctm uint32) {
26+// every other one bypassed. mult is the S31.32 multiplier, ctm the matrix
27+// blob and lut3d the identity 3D LUT blob (0: none), enabled with the PQ
28+// curve when the pipeline has a 3D LUT.
29+func (p *plane) colorProps(req *atomicReq, mode colorMode, mult uint64, ctm, lut3d uint32) {
2930 if !p.hasColor() {
3031 return
3132 }
@@ -39,6 +40,14 @@ func (p *plane) colorProps(req *atomicReq, mode colorMode, mult uint64, ctm uint
3940 for _, s := range cp.others {
4041 req.set(s.op, s.bypass, 1)
4142 }
43+ if cp.lut3d.op != 0 {
44+ if lut3d != 0 {
45+ req.set(cp.lut3d.op, cp.lut3d.bypass, 0)
46+ req.set(cp.lut3d.op, cp.lut3d.prop, uint64(lut3d))
47+ } else {
48+ req.set(cp.lut3d.op, cp.lut3d.bypass, 1)
49+ }
50+ }
4251 for _, s := range [...]struct {
4352 stage colorStage
4453 val uint64
@@ -83,7 +92,7 @@ func (o *Output) cursorShown() bool { return o.cursor != nil && !o.protected &&
8392 // primaryColorProps puts mode on the primary plane if it applies another one.
8493 func (o *Output) primaryColorProps(req *atomicReq, mode colorMode) {
8594 if o.primary.colorStale(mode) {
86- o.primary.colorProps(req, mode, o.colorMult, o.ctmBlob)
95+ o.primary.colorProps(req, mode, o.colorMult, o.ctmBlob, o.lut3dBlob)
8796 }
8897 }
8998
@@ -94,8 +103,8 @@ func (o *Output) primaryColorProps(req *atomicReq, mode colorMode) {
94103 // compositor never sets them, and a ramp left by another DRM master would
95104 // apply on top of the PQ output.
96105 func (o *Output) forceBypass(req *atomicReq) {
97- o.primary.colorProps(req, colorBypass, 0, 0)
98- o.overlay.colorProps(req, colorBypass, 0, 0)
106+ o.primary.colorProps(req, colorBypass, 0, 0, 0)
107+ o.overlay.colorProps(req, colorBypass, 0, 0, 0)
99108 for _, name := range [...]string{"DEGAMMA_LUT", "CTM", "GAMMA_LUT"} {
100109 req.set(o.crtc, o.crtcProps[name], 0)
101110 }
@@ -104,7 +113,7 @@ func (o *Output) forceBypass(req *atomicReq) {
104113 // overlayColorProps puts mode on the overlay plane if it applies another one.
105114 func (o *Output) overlayColorProps(req *atomicReq, mode colorMode) {
106115 if o.overlay.colorStale(mode) {
107- o.overlay.colorProps(req, mode, o.colorMult, o.ctmBlob)
116+ o.overlay.colorProps(req, mode, o.colorMult, o.ctmBlob, o.lut3dBlob)
108117 }
109118 }
110119
@@ -220,7 +229,7 @@ func (o *Output) colorAllowed(p *plane, format uint32, test func(req *atomicReq)
220229 req := &o.probeReq
221230 req.reset()
222231 test(req)
223- p.colorProps(req, colorSDRToPQ, o.colorMult, o.ctmBlob)
232+ p.colorProps(req, colorSDRToPQ, o.colorMult, o.ctmBlob, o.lut3dBlob)
224233 if cursor {
225234 o.cursor.props(req, o.crtc, o.cursor.desired())
226235 }
@@ -236,9 +245,10 @@ func (o *Output) colorAllowed(p *plane, format uint32, test func(req *atomicReq)
236245 return err == nil
237246 }
238247
239-// setupColor makes the output's colour objects: the CTM blob and the
240-// multiplier, when HDR can be on and a plane has a matched pipeline. A
241-// failure drops the pipelines: SDR content is composed as before.
248+// setupColor makes the output's colour objects: the CTM blob, the
249+// multiplier and the identity 3D LUT blob of a pipeline that has a 3D LUT,
250+// when HDR can be on and a plane has a matched pipeline. A failure drops the
251+// pipelines: SDR content is composed as before.
242252 func (o *Output) setupColor() {
243253 if !o.hdr.wanted() {
244254 // The pipelines serve HDR outputs only: SDR stays on Bypass.
@@ -256,6 +266,25 @@ func (o *Output) setupColor() {
256266 }
257267 o.ctmBlob = blob
258268 o.colorMult = s3132(float64(o.hdr.settings.SDRBrightness) / 80)
269+ if n := o.lut3dSize(); n != 0 {
270+ if blob, err = o.k.createBlob(lut3dIdentityBytes(n)); err != nil {
271+ o.log.Warn().Err(err).Str("connector", o.conn.name).Uint32("size", n).Msg("identity 3D LUT blob; plane colour pipelines off")
272+ o.dropPipelines()
273+ return
274+ }
275+ o.lut3dBlob = blob
276+ }
277+}
278+
279+// lut3dSize is the dimension of the 3D LUT the planes' pipelines have (both
280+// planes of an output share the same hardware block), 0 without one.
281+func (o *Output) lut3dSize() uint32 {
282+ for _, p := range [...]*plane{o.primary, o.overlay} {
283+ if p != nil && p.pipeline != nil && p.pipeline.lut3d.op != 0 {
284+ return p.pipeline.lut3dSize
285+ }
286+ }
287+ return 0
259288 }
260289
261290 // dropPipelines leaves the output's planes without a pipeline: SDR content
@@ -23,9 +23,10 @@ func (p *plane) colorStale(mode colorMode) bool {
23 23
24 // colorProps puts mode on p: Bypass selects the plane's Bypass pipeline;24 // colorProps puts mode on p: Bypass selects the plane's Bypass pipeline;
25 // colorSDRToPQ selects the matched one with its four operations enabled and25 // colorSDRToPQ selects the matched one with its four operations enabled and
26-// every other one bypassed. mult is the S31.32 multiplier and ctm the matrix26+// every other one bypassed. mult is the S31.32 multiplier, ctm the matrix
27-// blob.27+// blob and lut3d the identity 3D LUT blob (0: none), enabled with the PQ
28-func (p *plane) colorProps(req *atomicReq, mode colorMode, mult uint64, ctm uint32) {28+// curve when the pipeline has a 3D LUT.
29+func (p *plane) colorProps(req *atomicReq, mode colorMode, mult uint64, ctm, lut3d uint32) {
29 if !p.hasColor() {30 if !p.hasColor() {
30 return31 return
31 }32 }
@@ -39,6 +40,14 @@ func (p *plane) colorProps(req *atomicReq, mode colorMode, mult uint64, ctm uint
39 for _, s := range cp.others {40 for _, s := range cp.others {
40 req.set(s.op, s.bypass, 1)41 req.set(s.op, s.bypass, 1)
41 }42 }
43+ if cp.lut3d.op != 0 {
44+ if lut3d != 0 {
45+ req.set(cp.lut3d.op, cp.lut3d.bypass, 0)
46+ req.set(cp.lut3d.op, cp.lut3d.prop, uint64(lut3d))
47+ } else {
48+ req.set(cp.lut3d.op, cp.lut3d.bypass, 1)
49+ }
50+ }
42 for _, s := range [...]struct {51 for _, s := range [...]struct {
43 stage colorStage52 stage colorStage
44 val uint6453 val uint64
@@ -83,7 +92,7 @@ func (o *Output) cursorShown() bool { return o.cursor != nil && !o.protected &&
83 // primaryColorProps puts mode on the primary plane if it applies another one.92 // primaryColorProps puts mode on the primary plane if it applies another one.
84 func (o *Output) primaryColorProps(req *atomicReq, mode colorMode) {93 func (o *Output) primaryColorProps(req *atomicReq, mode colorMode) {
85 if o.primary.colorStale(mode) {94 if o.primary.colorStale(mode) {
86- o.primary.colorProps(req, mode, o.colorMult, o.ctmBlob)95+ o.primary.colorProps(req, mode, o.colorMult, o.ctmBlob, o.lut3dBlob)
87 }96 }
88 }97 }
89 98
@@ -94,8 +103,8 @@ func (o *Output) primaryColorProps(req *atomicReq, mode colorMode) {
94 // compositor never sets them, and a ramp left by another DRM master would103 // compositor never sets them, and a ramp left by another DRM master would
95 // apply on top of the PQ output.104 // apply on top of the PQ output.
96 func (o *Output) forceBypass(req *atomicReq) {105 func (o *Output) forceBypass(req *atomicReq) {
97- o.primary.colorProps(req, colorBypass, 0, 0)106+ o.primary.colorProps(req, colorBypass, 0, 0, 0)
98- o.overlay.colorProps(req, colorBypass, 0, 0)107+ o.overlay.colorProps(req, colorBypass, 0, 0, 0)
99 for _, name := range [...]string{"DEGAMMA_LUT", "CTM", "GAMMA_LUT"} {108 for _, name := range [...]string{"DEGAMMA_LUT", "CTM", "GAMMA_LUT"} {
100 req.set(o.crtc, o.crtcProps[name], 0)109 req.set(o.crtc, o.crtcProps[name], 0)
101 }110 }
@@ -104,7 +113,7 @@ func (o *Output) forceBypass(req *atomicReq) {
104 // overlayColorProps puts mode on the overlay plane if it applies another one.113 // overlayColorProps puts mode on the overlay plane if it applies another one.
105 func (o *Output) overlayColorProps(req *atomicReq, mode colorMode) {114 func (o *Output) overlayColorProps(req *atomicReq, mode colorMode) {
106 if o.overlay.colorStale(mode) {115 if o.overlay.colorStale(mode) {
107- o.overlay.colorProps(req, mode, o.colorMult, o.ctmBlob)116+ o.overlay.colorProps(req, mode, o.colorMult, o.ctmBlob, o.lut3dBlob)
108 }117 }
109 }118 }
110 119
@@ -220,7 +229,7 @@ func (o *Output) colorAllowed(p *plane, format uint32, test func(req *atomicReq)
220 req := &o.probeReq229 req := &o.probeReq
221 req.reset()230 req.reset()
222 test(req)231 test(req)
223- p.colorProps(req, colorSDRToPQ, o.colorMult, o.ctmBlob)232+ p.colorProps(req, colorSDRToPQ, o.colorMult, o.ctmBlob, o.lut3dBlob)
224 if cursor {233 if cursor {
225 o.cursor.props(req, o.crtc, o.cursor.desired())234 o.cursor.props(req, o.crtc, o.cursor.desired())
226 }235 }
@@ -236,9 +245,10 @@ func (o *Output) colorAllowed(p *plane, format uint32, test func(req *atomicReq)
236 return err == nil245 return err == nil
237 }246 }
238 247
239-// setupColor makes the output's colour objects: the CTM blob and the248+// setupColor makes the output's colour objects: the CTM blob, the
240-// multiplier, when HDR can be on and a plane has a matched pipeline. A249+// multiplier and the identity 3D LUT blob of a pipeline that has a 3D LUT,
241-// failure drops the pipelines: SDR content is composed as before.250+// when HDR can be on and a plane has a matched pipeline. A failure drops the
251+// pipelines: SDR content is composed as before.
242 func (o *Output) setupColor() {252 func (o *Output) setupColor() {
243 if !o.hdr.wanted() {253 if !o.hdr.wanted() {
244 // The pipelines serve HDR outputs only: SDR stays on Bypass.254 // The pipelines serve HDR outputs only: SDR stays on Bypass.
@@ -256,6 +266,25 @@ func (o *Output) setupColor() {
256 }266 }
257 o.ctmBlob = blob267 o.ctmBlob = blob
258 o.colorMult = s3132(float64(o.hdr.settings.SDRBrightness) / 80)268 o.colorMult = s3132(float64(o.hdr.settings.SDRBrightness) / 80)
269+ if n := o.lut3dSize(); n != 0 {
270+ if blob, err = o.k.createBlob(lut3dIdentityBytes(n)); err != nil {
271+ o.log.Warn().Err(err).Str("connector", o.conn.name).Uint32("size", n).Msg("identity 3D LUT blob; plane colour pipelines off")
272+ o.dropPipelines()
273+ return
274+ }
275+ o.lut3dBlob = blob
276+ }
277+}
278+
279+// lut3dSize is the dimension of the 3D LUT the planes' pipelines have (both
280+// planes of an output share the same hardware block), 0 without one.
281+func (o *Output) lut3dSize() uint32 {
282+ for _, p := range [...]*plane{o.primary, o.overlay} {
283+ if p != nil && p.pipeline != nil && p.pipeline.lut3d.op != 0 {
284+ return p.pipeline.lut3dSize
285+ }
286+ }
287+ return 0
259 }288 }
260 289
261 // dropPipelines leaves the output's planes without a pipeline: SDR content290 // dropPipelines leaves the output's planes without a pipeline: SDR content
M internal/adapters/drm/colorop_match.go +34 -1
@@ -47,6 +47,7 @@ const (
4747 colorTypeCurve = "1D Curve"
4848 colorTypeMultiplier = "Multiplier"
4949 colorTypeMatrix = "3x4 Matrix"
50+ colorTypeLUT3D = "3D LUT"
5051 curveSRGBEOTF = "sRGB EOTF"
5152 curvePQInverseEOTF = "PQ 125 Inverse EOTF"
5253 )
@@ -58,6 +59,8 @@ type colorop struct {
5859 typ string
5960 bypass, curveProp, multiplier, data uint32
6061 curves map[string]uint64
62+ // size is the SIZE property's value: the dimension of a LUT.
63+ size uint32
6164 }
6265
6366 // colorStage is an operation the output drives: its object, BYPASS and the
@@ -76,6 +79,12 @@ type colorPipeline struct {
7679 // are every other operation, which must stay bypassed.
7780 srgb, mult, ctm, pq colorStage
7881 others []colorStage
82+ // lut3d is the 3D LUT that follows the PQ curve, when the pipeline has
83+ // one (op 0: none), and lut3dSize its dimension. On amdgpu the PQ curve
84+ // is the shaper of the MCM shaper + 3D LUT block: the hardware only
85+ // applies it with the 3D LUT on, so the LUT is loaded with identity.
86+ lut3d colorStage
87+ lut3dSize uint32
7988 }
8089
8190 // is reports whether the operation is the wanted type, with the wanted curve.
@@ -126,6 +135,10 @@ func matchColorPipeline(first uint64, ops []colorop) (*colorPipeline, string) {
126135 if op.bypass == 0 {
127136 return nil, fmt.Sprintf("colorop %d (%s) cannot be bypassed", op.id, op.typ)
128137 }
138+ if n == len(want) && cp.lut3d.op == 0 && op.typ == colorTypeLUT3D && op.data != 0 && op.size > 1 {
139+ cp.lut3d, cp.lut3dSize = colorStage{op: op.id, bypass: op.bypass, prop: op.data}, op.size
140+ continue
141+ }
129142 cp.others = append(cp.others, colorStage{op: op.id, bypass: op.bypass})
130143 }
131144 if n < len(want) {
@@ -148,7 +161,7 @@ func walkColorops(k kms, first uint32) ([]colorop, error) {
148161 if err != nil {
149162 return nil, fmt.Errorf("colorop %d: %w", id, err)
150163 }
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])}
164+ 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]), size: uint32(props["SIZE"][1])}
152165 if t := props["TYPE"]; t[0] != 0 {
153166 types, err := k.propEnums(uint32(t[0]))
154167 if err != nil {
@@ -244,6 +257,26 @@ func ctmBytes(m [3][3]float64) []byte {
244257 return b
245258 }
246259
260+// lut3dIdentityBytes is the DATA blob of a 3D LUT colorop of dimension n
261+// that leaves pixels as they are: n³ drm_color_lut32 entries (u32 red,
262+// green, blue, reserved), red varying fastest, each equal to its own
263+// coordinate on the full 32-bit scale.
264+func lut3dIdentityBytes(n uint32) []byte {
265+ b := make([]byte, 0, int(n*n*n)*16)
266+ step := func(i uint32) uint32 { return uint32(uint64(i) * math.MaxUint32 / uint64(n-1)) }
267+ for bl := range n {
268+ for g := range n {
269+ for r := range n {
270+ b = binary.LittleEndian.AppendUint32(b, step(r))
271+ b = binary.LittleEndian.AppendUint32(b, step(g))
272+ b = binary.LittleEndian.AppendUint32(b, step(bl))
273+ b = binary.LittleEndian.AppendUint32(b, 0)
274+ }
275+ }
276+ }
277+ return b
278+}
279+
247280 // planeColor decides how plane (with pipeline, nil: none) shows a client
248281 // buffer of format whose content is c, on an output in HDR10 mode when hdrOn.
249282 // opaque is the surface's Opaque. A buffer already in the output's encoding
@@ -47,6 +47,7 @@ const (
47 colorTypeCurve = "1D Curve"47 colorTypeCurve = "1D Curve"
48 colorTypeMultiplier = "Multiplier"48 colorTypeMultiplier = "Multiplier"
49 colorTypeMatrix = "3x4 Matrix"49 colorTypeMatrix = "3x4 Matrix"
50+ colorTypeLUT3D = "3D LUT"
50 curveSRGBEOTF = "sRGB EOTF"51 curveSRGBEOTF = "sRGB EOTF"
51 curvePQInverseEOTF = "PQ 125 Inverse EOTF"52 curvePQInverseEOTF = "PQ 125 Inverse EOTF"
52 )53 )
@@ -58,6 +59,8 @@ type colorop struct {
58 typ string59 typ string
59 bypass, curveProp, multiplier, data uint3260 bypass, curveProp, multiplier, data uint32
60 curves map[string]uint6461 curves map[string]uint64
62+ // size is the SIZE property's value: the dimension of a LUT.
63+ size uint32
61 }64 }
62 65
63 // colorStage is an operation the output drives: its object, BYPASS and the66 // colorStage is an operation the output drives: its object, BYPASS and the
@@ -76,6 +79,12 @@ type colorPipeline struct {
76 // are every other operation, which must stay bypassed.79 // are every other operation, which must stay bypassed.
77 srgb, mult, ctm, pq colorStage80 srgb, mult, ctm, pq colorStage
78 others []colorStage81 others []colorStage
82+ // lut3d is the 3D LUT that follows the PQ curve, when the pipeline has
83+ // one (op 0: none), and lut3dSize its dimension. On amdgpu the PQ curve
84+ // is the shaper of the MCM shaper + 3D LUT block: the hardware only
85+ // applies it with the 3D LUT on, so the LUT is loaded with identity.
86+ lut3d colorStage
87+ lut3dSize uint32
79 }88 }
80 89
81 // is reports whether the operation is the wanted type, with the wanted curve.90 // is reports whether the operation is the wanted type, with the wanted curve.
@@ -126,6 +135,10 @@ func matchColorPipeline(first uint64, ops []colorop) (*colorPipeline, string) {
126 if op.bypass == 0 {135 if op.bypass == 0 {
127 return nil, fmt.Sprintf("colorop %d (%s) cannot be bypassed", op.id, op.typ)136 return nil, fmt.Sprintf("colorop %d (%s) cannot be bypassed", op.id, op.typ)
128 }137 }
138+ if n == len(want) && cp.lut3d.op == 0 && op.typ == colorTypeLUT3D && op.data != 0 && op.size > 1 {
139+ cp.lut3d, cp.lut3dSize = colorStage{op: op.id, bypass: op.bypass, prop: op.data}, op.size
140+ continue
141+ }
129 cp.others = append(cp.others, colorStage{op: op.id, bypass: op.bypass})142 cp.others = append(cp.others, colorStage{op: op.id, bypass: op.bypass})
130 }143 }
131 if n < len(want) {144 if n < len(want) {
@@ -148,7 +161,7 @@ func walkColorops(k kms, first uint32) ([]colorop, error) {
148 if err != nil {161 if err != nil {
149 return nil, fmt.Errorf("colorop %d: %w", id, err)162 return nil, fmt.Errorf("colorop %d: %w", id, err)
150 }163 }
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])}164+ 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]), size: uint32(props["SIZE"][1])}
152 if t := props["TYPE"]; t[0] != 0 {165 if t := props["TYPE"]; t[0] != 0 {
153 types, err := k.propEnums(uint32(t[0]))166 types, err := k.propEnums(uint32(t[0]))
154 if err != nil {167 if err != nil {
@@ -244,6 +257,26 @@ func ctmBytes(m [3][3]float64) []byte {
244 return b257 return b
245 }258 }
246 259
260+// lut3dIdentityBytes is the DATA blob of a 3D LUT colorop of dimension n
261+// that leaves pixels as they are: n³ drm_color_lut32 entries (u32 red,
262+// green, blue, reserved), red varying fastest, each equal to its own
263+// coordinate on the full 32-bit scale.
264+func lut3dIdentityBytes(n uint32) []byte {
265+ b := make([]byte, 0, int(n*n*n)*16)
266+ step := func(i uint32) uint32 { return uint32(uint64(i) * math.MaxUint32 / uint64(n-1)) }
267+ for bl := range n {
268+ for g := range n {
269+ for r := range n {
270+ b = binary.LittleEndian.AppendUint32(b, step(r))
271+ b = binary.LittleEndian.AppendUint32(b, step(g))
272+ b = binary.LittleEndian.AppendUint32(b, step(bl))
273+ b = binary.LittleEndian.AppendUint32(b, 0)
274+ }
275+ }
276+ }
277+ return b
278+}
279+
247 // planeColor decides how plane (with pipeline, nil: none) shows a client280 // 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.281 // 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 encoding282 // opaque is the surface's Opaque. A buffer already in the output's encoding
M internal/adapters/drm/colorop_test.go +106 -11
@@ -42,6 +42,8 @@ type opSpec struct {
4242 typ string
4343 curves map[string]uint64
4444 noBypass bool
45+ // size is the SIZE property of a LUT (0: no such property).
46+ size uint32
4547 }
4648
4749 func curveOp(curves map[string]uint64) opSpec { return opSpec{typ: "1D Curve", curves: curves} }
@@ -50,10 +52,12 @@ var (
5052 opSRGB, opPQ = curveOp(firstCurve), curveOp(lastCurve)
5153 opMult = opSpec{typ: "Multiplier"}
5254 opCTM = opSpec{typ: "3x4 Matrix"}
53- opLUT1D = opSpec{typ: "1D LUT"}
54- opLUT3D = opSpec{typ: "3D LUT"}
55+ opLUT1D = opSpec{typ: "1D LUT", size: 4096}
56+ opLUT3D = opSpec{typ: "3D LUT", size: 17}
5557 // amdgpuChain is the pipeline read on kernel 7.2 (DCN3+).
5658 amdgpuChain = []opSpec{opSRGB, opMult, opCTM, opPQ, opLUT1D, opLUT3D, curveOp(map[string]uint64{"sRGB EOTF": 5}), opLUT1D}
59+ // lut3dBlob is the identity 3D LUT blob ID of the test output.
60+ lut3dBlob uint32 = 62
5761 )
5862
5963 // colorPlane gives p a COLOR_PIPELINE property (id pipeProp) offering the
@@ -89,6 +93,9 @@ func colorPlane(k *mockkms, p *plane, pipeProp, base uint32, chains ...[]opSpec)
8993 case "3x4 Matrix", "1D LUT", "3D LUT":
9094 props["DATA"] = [2]uint64{uint64(dataProp(id)), 0}
9195 }
96+ if op.size != 0 {
97+ props["SIZE"] = [2]uint64{6000 + uint64(id), uint64(op.size)}
98+ }
9299 k.EXPECT().objProps(id, uint32(objColorop)).Return(props, nil).Maybe()
93100 id++
94101 }
@@ -114,6 +121,8 @@ func TestMatchColorPipeline(t *testing.T) {
114121 }{
115122 {"amdgpu", [][]opSpec{amdgpuChain}, 0, ""},
116123 {"minimal", [][]opSpec{{opSRGB, opMult, opCTM, opPQ}}, 0, ""},
124+ {"3D LUT without size", [][]opSpec{{opSRGB, opMult, opCTM, opPQ, {typ: "3D LUT"}}}, 0, ""},
125+ {"3D LUT before PQ", [][]opSpec{{opSRGB, opMult, opCTM, opLUT3D, opPQ}}, 0, ""},
117126 {"bypassable op before the chain", [][]opSpec{{opLUT1D, opSRGB, opMult, opCTM, opPQ}}, 0, ""},
118127 {"second pipeline matches", [][]opSpec{{opLUT1D}, amdgpuChain}, 1, ""},
119128 {"no pipeline", nil, -1, "only Bypass"},
@@ -164,8 +173,27 @@ func TestMatchColorPipeline(t *testing.T) {
164173 cp.pq != (colorStage{op: pq, bypass: bypassProp(pq), prop: curveProp(pq), curve: 8}) {
165174 t.Fatalf("stages %+v", cp)
166175 }
167- if len(cp.others) != len(chain)-4 {
168- t.Fatalf("%d other ops, want %d", len(cp.others), len(chain)-4)
176+ // A sized 3D LUT after the PQ curve is the LUT stage; any
177+ // other operation is bypassed.
178+ lut := 0
179+ for i := int(pq-base) + 1; i < len(chain); i++ {
180+ if chain[i].typ == "3D LUT" && chain[i].size > 1 && lut == 0 {
181+ lut = i
182+ }
183+ }
184+ if lut == 0 {
185+ if cp.lut3d.op != 0 {
186+ t.Fatalf("3D LUT stage %+v without a usable 3D LUT", cp.lut3d)
187+ }
188+ } else if id := base + uint32(lut); cp.lut3d != (colorStage{op: id, bypass: bypassProp(id), prop: dataProp(id)}) || cp.lut3dSize != chain[lut].size {
189+ t.Fatalf("3D LUT stage %+v size %d, want op %d size %d", cp.lut3d, cp.lut3dSize, id, chain[lut].size)
190+ }
191+ want := len(chain) - 4
192+ if lut != 0 {
193+ want--
194+ }
195+ if len(cp.others) != want {
196+ t.Fatalf("%d other ops, want %d", len(cp.others), want)
169197 }
170198 })
171199 }
@@ -273,6 +301,41 @@ func TestCTMBlobMatchesHDRShader(t *testing.T) {
273301 }
274302 }
275303
304+// The identity 3D LUT is laid out as the kernel reads it (red fastest)
305+// and maps every grid point to itself on the 32-bit scale.
306+func TestLUT3DIdentityBytes(t *testing.T) {
307+ const n = 17
308+ b := lut3dIdentityBytes(n)
309+ if len(b) != n*n*n*16 {
310+ t.Fatalf("%d bytes, want %d", len(b), n*n*n*16)
311+ }
312+ at := func(r, g, bl int) [4]uint32 {
313+ i := (r + n*(g+n*bl)) * 16
314+ return [4]uint32{binary.LittleEndian.Uint32(b[i:]), binary.LittleEndian.Uint32(b[i+4:]), binary.LittleEndian.Uint32(b[i+8:]), binary.LittleEndian.Uint32(b[i+12:])}
315+ }
316+ if v := at(0, 0, 0); v != [4]uint32{} {
317+ t.Errorf("black %v", v)
318+ }
319+ if v := at(n-1, n-1, n-1); v != [4]uint32{math.MaxUint32, math.MaxUint32, math.MaxUint32, 0} {
320+ t.Errorf("white %v", v)
321+ }
322+ if v := at(n-1, 0, 0); v != [4]uint32{math.MaxUint32, 0, 0, 0} {
323+ t.Errorf("red %v", v)
324+ }
325+ if v := at(0, 0, n-1); v != [4]uint32{0, 0, math.MaxUint32, 0} {
326+ t.Errorf("blue %v", v)
327+ }
328+ half := uint32(8 * uint64(math.MaxUint32) / 16)
329+ if v := at(8, 8, 8); v != [4]uint32{half, half, half, 0} {
330+ t.Errorf("mid grey %v, want %d", v, half)
331+ }
332+ for r := 1; r < n; r++ {
333+ if at(r, 3, 5)[0] <= at(r-1, 3, 5)[0] {
334+ t.Fatalf("red not increasing at %d", r)
335+ }
336+ }
337+}
338+
276339 func TestPlaneColor(t *testing.T) {
277340 pq := ports.SurfaceColor{TF: ports.ColorTFPQ, Primaries: ports.ColorPrimariesBT2020}
278341 srgb := ports.SurfaceColor{TF: ports.ColorTFSRGB, Primaries: ports.ColorPrimariesSRGB}
@@ -340,7 +403,7 @@ func colorOutput(t *testing.T, overlay bool, rule func(*atomicReq, uint32) error
340403 }
341404 o.hdr.on = true
342405 o.readColor()
343- o.ctmBlob, o.colorMult = 61, s3132(203.0/80)
406+ o.ctmBlob, o.lut3dBlob, o.colorMult = 61, lut3dBlob, s3132(203.0/80)
344407 if o.primary.pipeline == nil || overlay && o.overlay.pipeline == nil {
345408 t.Fatal("pipeline not matched")
346409 }
@@ -391,11 +454,16 @@ func TestColorCommitTransitions(t *testing.T) {
391454 {cp.mult.op, cp.mult.bypass, 0}, {cp.mult.op, cp.mult.prop, s3132(203.0 / 80)},
392455 {cp.ctm.op, cp.ctm.bypass, 0}, {cp.ctm.op, cp.ctm.prop, 61},
393456 {cp.pq.op, cp.pq.bypass, 0}, {cp.pq.op, cp.pq.prop, cp.pq.curve},
457+ // The 3D LUT after the PQ shaper is on, with identity.
458+ {cp.lut3d.op, cp.lut3d.bypass, 0}, {cp.lut3d.op, cp.lut3d.prop, uint64(lut3dBlob)},
394459 } {
395460 if v, ok := first.req.value(s.op, s.prop); !ok || v != s.want {
396461 t.Errorf("colorop %d prop %d = %d %v, want %d", s.op, s.prop, v, ok, s.want)
397462 }
398463 }
464+ if cp.lut3d.op == 0 {
465+ t.Fatal("the amdgpu chain has no 3D LUT stage")
466+ }
399467 for _, other := range cp.others {
400468 if v, ok := first.req.value(other.op, other.bypass); !ok || v != 1 {
401469 t.Errorf("other colorop %d BYPASS %d %v, want 1", other.op, v, ok)
@@ -492,11 +560,12 @@ func TestColorBypassOnComposedProtectedOffCloseAndModeset(t *testing.T) {
492560 o, k, commits := colorOutput(t, true, nil)
493561 k.EXPECT().rmFB(mock.Anything).Return(nil)
494562 k.EXPECT().destroyBlob(uint32(61)).Return(nil).Once()
563+ k.EXPECT().destroyBlob(lut3dBlob).Return(nil).Once()
495564 // Unknown state: Close still writes Bypass.
496565 o.Close()
497566 bypassed(t, (*commits)[0])
498- if o.ctmBlob != 0 {
499- t.Fatal("CTM blob kept")
567+ if o.ctmBlob != 0 || o.lut3dBlob != 0 {
568+ t.Fatal("colour blobs kept")
500569 }
501570 })
502571 t.Run("modeset", func(t *testing.T) {
@@ -789,15 +858,41 @@ func TestColorOverlayInHDR(t *testing.T) {
789858 }
790859
791860 func TestSetupColor(t *testing.T) {
792- t.Run("HDR makes the matrix blob and the multiplier", func(t *testing.T) {
861+ t.Run("HDR makes the matrix blob, the multiplier and the identity 3D LUT", func(t *testing.T) {
793862 o, k, _ := colorOutput(t, false, nil)
794- o.ctmBlob, o.colorMult = 0, 0
863+ o.ctmBlob, o.lut3dBlob, o.colorMult = 0, 0, 0
795864 o.hdr.settings = HDRSettings{Enabled: true, SDRBrightness: 160}
796865 o.hdr.cap.Capable = true
797866 k.EXPECT().createBlob(mock.MatchedBy(func(b []byte) bool { return slices.Equal(b, ctmBytes(bt709ToBT2020)) })).Return(uint32(61), nil).Once()
867+ k.EXPECT().createBlob(mock.MatchedBy(func(b []byte) bool { return slices.Equal(b, lut3dIdentityBytes(17)) })).Return(uint32(62), nil).Once()
868+ o.setupColor()
869+ if o.ctmBlob != 61 || o.lut3dBlob != 62 || o.colorMult != 2<<32 {
870+ t.Fatalf("blobs %d %d multiplier %#x", o.ctmBlob, o.lut3dBlob, o.colorMult)
871+ }
872+ })
873+ t.Run("a pipeline without 3D LUT makes no LUT blob", func(t *testing.T) {
874+ o, k, _ := testOutput(t)
875+ colorPlane(k, o.primary, pColorPipe, opBase, []opSpec{opSRGB, opMult, opCTM, opPQ})
876+ o.hdr.on = true
877+ o.readColor()
878+ o.hdr.settings = HDRSettings{Enabled: true, SDRBrightness: 160}
879+ o.hdr.cap.Capable = true
880+ k.EXPECT().createBlob(mock.Anything).Return(uint32(61), nil).Once()
881+ o.setupColor()
882+ if o.ctmBlob != 61 || o.lut3dBlob != 0 {
883+ t.Fatalf("blobs %d %d", o.ctmBlob, o.lut3dBlob)
884+ }
885+ })
886+ t.Run("3D LUT blob failure turns the pipelines off", func(t *testing.T) {
887+ o, k, _ := colorOutput(t, true, nil)
888+ o.ctmBlob, o.lut3dBlob = 0, 0
889+ o.hdr.settings = HDRSettings{Enabled: true, SDRBrightness: 203}
890+ o.hdr.cap.Capable = true
891+ k.EXPECT().createBlob(mock.Anything).Return(uint32(61), nil).Once()
892+ k.EXPECT().createBlob(mock.Anything).Return(uint32(0), unix.ENOMEM).Once()
798893 o.setupColor()
799- if o.ctmBlob != 61 || o.colorMult != 2<<32 {
800- t.Fatalf("blob %d multiplier %#x", o.ctmBlob, o.colorMult)
894+ if o.primary.pipeline != nil || o.overlay.pipeline != nil || o.lut3dBlob != 0 {
895+ t.Fatal("pipelines kept without their 3D LUT blob")
801896 }
802897 })
803898 t.Run("blob failure turns the pipelines off", func(t *testing.T) {
@@ -42,6 +42,8 @@ type opSpec struct {
42 typ string42 typ string
43 curves map[string]uint6443 curves map[string]uint64
44 noBypass bool44 noBypass bool
45+ // size is the SIZE property of a LUT (0: no such property).
46+ size uint32
45 }47 }
46 48
47 func curveOp(curves map[string]uint64) opSpec { return opSpec{typ: "1D Curve", curves: curves} }49 func curveOp(curves map[string]uint64) opSpec { return opSpec{typ: "1D Curve", curves: curves} }
@@ -50,10 +52,12 @@ var (
50 opSRGB, opPQ = curveOp(firstCurve), curveOp(lastCurve)52 opSRGB, opPQ = curveOp(firstCurve), curveOp(lastCurve)
51 opMult = opSpec{typ: "Multiplier"}53 opMult = opSpec{typ: "Multiplier"}
52 opCTM = opSpec{typ: "3x4 Matrix"}54 opCTM = opSpec{typ: "3x4 Matrix"}
53- opLUT1D = opSpec{typ: "1D LUT"}55+ opLUT1D = opSpec{typ: "1D LUT", size: 4096}
54- opLUT3D = opSpec{typ: "3D LUT"}56+ opLUT3D = opSpec{typ: "3D LUT", size: 17}
55 // amdgpuChain is the pipeline read on kernel 7.2 (DCN3+).57 // 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}58 amdgpuChain = []opSpec{opSRGB, opMult, opCTM, opPQ, opLUT1D, opLUT3D, curveOp(map[string]uint64{"sRGB EOTF": 5}), opLUT1D}
59+ // lut3dBlob is the identity 3D LUT blob ID of the test output.
60+ lut3dBlob uint32 = 62
57 )61 )
58 62
59 // colorPlane gives p a COLOR_PIPELINE property (id pipeProp) offering the63 // colorPlane gives p a COLOR_PIPELINE property (id pipeProp) offering the
@@ -89,6 +93,9 @@ func colorPlane(k *mockkms, p *plane, pipeProp, base uint32, chains ...[]opSpec)
89 case "3x4 Matrix", "1D LUT", "3D LUT":93 case "3x4 Matrix", "1D LUT", "3D LUT":
90 props["DATA"] = [2]uint64{uint64(dataProp(id)), 0}94 props["DATA"] = [2]uint64{uint64(dataProp(id)), 0}
91 }95 }
96+ if op.size != 0 {
97+ props["SIZE"] = [2]uint64{6000 + uint64(id), uint64(op.size)}
98+ }
92 k.EXPECT().objProps(id, uint32(objColorop)).Return(props, nil).Maybe()99 k.EXPECT().objProps(id, uint32(objColorop)).Return(props, nil).Maybe()
93 id++100 id++
94 }101 }
@@ -114,6 +121,8 @@ func TestMatchColorPipeline(t *testing.T) {
114 }{121 }{
115 {"amdgpu", [][]opSpec{amdgpuChain}, 0, ""},122 {"amdgpu", [][]opSpec{amdgpuChain}, 0, ""},
116 {"minimal", [][]opSpec{{opSRGB, opMult, opCTM, opPQ}}, 0, ""},123 {"minimal", [][]opSpec{{opSRGB, opMult, opCTM, opPQ}}, 0, ""},
124+ {"3D LUT without size", [][]opSpec{{opSRGB, opMult, opCTM, opPQ, {typ: "3D LUT"}}}, 0, ""},
125+ {"3D LUT before PQ", [][]opSpec{{opSRGB, opMult, opCTM, opLUT3D, opPQ}}, 0, ""},
117 {"bypassable op before the chain", [][]opSpec{{opLUT1D, opSRGB, opMult, opCTM, opPQ}}, 0, ""},126 {"bypassable op before the chain", [][]opSpec{{opLUT1D, opSRGB, opMult, opCTM, opPQ}}, 0, ""},
118 {"second pipeline matches", [][]opSpec{{opLUT1D}, amdgpuChain}, 1, ""},127 {"second pipeline matches", [][]opSpec{{opLUT1D}, amdgpuChain}, 1, ""},
119 {"no pipeline", nil, -1, "only Bypass"},128 {"no pipeline", nil, -1, "only Bypass"},
@@ -164,8 +173,27 @@ func TestMatchColorPipeline(t *testing.T) {
164 cp.pq != (colorStage{op: pq, bypass: bypassProp(pq), prop: curveProp(pq), curve: 8}) {173 cp.pq != (colorStage{op: pq, bypass: bypassProp(pq), prop: curveProp(pq), curve: 8}) {
165 t.Fatalf("stages %+v", cp)174 t.Fatalf("stages %+v", cp)
166 }175 }
167- if len(cp.others) != len(chain)-4 {176+ // A sized 3D LUT after the PQ curve is the LUT stage; any
168- t.Fatalf("%d other ops, want %d", len(cp.others), len(chain)-4)177+ // other operation is bypassed.
178+ lut := 0
179+ for i := int(pq-base) + 1; i < len(chain); i++ {
180+ if chain[i].typ == "3D LUT" && chain[i].size > 1 && lut == 0 {
181+ lut = i
182+ }
183+ }
184+ if lut == 0 {
185+ if cp.lut3d.op != 0 {
186+ t.Fatalf("3D LUT stage %+v without a usable 3D LUT", cp.lut3d)
187+ }
188+ } else if id := base + uint32(lut); cp.lut3d != (colorStage{op: id, bypass: bypassProp(id), prop: dataProp(id)}) || cp.lut3dSize != chain[lut].size {
189+ t.Fatalf("3D LUT stage %+v size %d, want op %d size %d", cp.lut3d, cp.lut3dSize, id, chain[lut].size)
190+ }
191+ want := len(chain) - 4
192+ if lut != 0 {
193+ want--
194+ }
195+ if len(cp.others) != want {
196+ t.Fatalf("%d other ops, want %d", len(cp.others), want)
169 }197 }
170 })198 })
171 }199 }
@@ -273,6 +301,41 @@ func TestCTMBlobMatchesHDRShader(t *testing.T) {
273 }301 }
274 }302 }
275 303
304+// The identity 3D LUT is laid out as the kernel reads it (red fastest)
305+// and maps every grid point to itself on the 32-bit scale.
306+func TestLUT3DIdentityBytes(t *testing.T) {
307+ const n = 17
308+ b := lut3dIdentityBytes(n)
309+ if len(b) != n*n*n*16 {
310+ t.Fatalf("%d bytes, want %d", len(b), n*n*n*16)
311+ }
312+ at := func(r, g, bl int) [4]uint32 {
313+ i := (r + n*(g+n*bl)) * 16
314+ return [4]uint32{binary.LittleEndian.Uint32(b[i:]), binary.LittleEndian.Uint32(b[i+4:]), binary.LittleEndian.Uint32(b[i+8:]), binary.LittleEndian.Uint32(b[i+12:])}
315+ }
316+ if v := at(0, 0, 0); v != [4]uint32{} {
317+ t.Errorf("black %v", v)
318+ }
319+ if v := at(n-1, n-1, n-1); v != [4]uint32{math.MaxUint32, math.MaxUint32, math.MaxUint32, 0} {
320+ t.Errorf("white %v", v)
321+ }
322+ if v := at(n-1, 0, 0); v != [4]uint32{math.MaxUint32, 0, 0, 0} {
323+ t.Errorf("red %v", v)
324+ }
325+ if v := at(0, 0, n-1); v != [4]uint32{0, 0, math.MaxUint32, 0} {
326+ t.Errorf("blue %v", v)
327+ }
328+ half := uint32(8 * uint64(math.MaxUint32) / 16)
329+ if v := at(8, 8, 8); v != [4]uint32{half, half, half, 0} {
330+ t.Errorf("mid grey %v, want %d", v, half)
331+ }
332+ for r := 1; r < n; r++ {
333+ if at(r, 3, 5)[0] <= at(r-1, 3, 5)[0] {
334+ t.Fatalf("red not increasing at %d", r)
335+ }
336+ }
337+}
338+
276 func TestPlaneColor(t *testing.T) {339 func TestPlaneColor(t *testing.T) {
277 pq := ports.SurfaceColor{TF: ports.ColorTFPQ, Primaries: ports.ColorPrimariesBT2020}340 pq := ports.SurfaceColor{TF: ports.ColorTFPQ, Primaries: ports.ColorPrimariesBT2020}
278 srgb := ports.SurfaceColor{TF: ports.ColorTFSRGB, Primaries: ports.ColorPrimariesSRGB}341 srgb := ports.SurfaceColor{TF: ports.ColorTFSRGB, Primaries: ports.ColorPrimariesSRGB}
@@ -340,7 +403,7 @@ func colorOutput(t *testing.T, overlay bool, rule func(*atomicReq, uint32) error
340 }403 }
341 o.hdr.on = true404 o.hdr.on = true
342 o.readColor()405 o.readColor()
343- o.ctmBlob, o.colorMult = 61, s3132(203.0/80)406+ o.ctmBlob, o.lut3dBlob, o.colorMult = 61, lut3dBlob, s3132(203.0/80)
344 if o.primary.pipeline == nil || overlay && o.overlay.pipeline == nil {407 if o.primary.pipeline == nil || overlay && o.overlay.pipeline == nil {
345 t.Fatal("pipeline not matched")408 t.Fatal("pipeline not matched")
346 }409 }
@@ -391,11 +454,16 @@ func TestColorCommitTransitions(t *testing.T) {
391 {cp.mult.op, cp.mult.bypass, 0}, {cp.mult.op, cp.mult.prop, s3132(203.0 / 80)},454 {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},455 {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},456 {cp.pq.op, cp.pq.bypass, 0}, {cp.pq.op, cp.pq.prop, cp.pq.curve},
457+ // The 3D LUT after the PQ shaper is on, with identity.
458+ {cp.lut3d.op, cp.lut3d.bypass, 0}, {cp.lut3d.op, cp.lut3d.prop, uint64(lut3dBlob)},
394 } {459 } {
395 if v, ok := first.req.value(s.op, s.prop); !ok || v != s.want {460 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)461 t.Errorf("colorop %d prop %d = %d %v, want %d", s.op, s.prop, v, ok, s.want)
397 }462 }
398 }463 }
464+ if cp.lut3d.op == 0 {
465+ t.Fatal("the amdgpu chain has no 3D LUT stage")
466+ }
399 for _, other := range cp.others {467 for _, other := range cp.others {
400 if v, ok := first.req.value(other.op, other.bypass); !ok || v != 1 {468 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)469 t.Errorf("other colorop %d BYPASS %d %v, want 1", other.op, v, ok)
@@ -492,11 +560,12 @@ func TestColorBypassOnComposedProtectedOffCloseAndModeset(t *testing.T) {
492 o, k, commits := colorOutput(t, true, nil)560 o, k, commits := colorOutput(t, true, nil)
493 k.EXPECT().rmFB(mock.Anything).Return(nil)561 k.EXPECT().rmFB(mock.Anything).Return(nil)
494 k.EXPECT().destroyBlob(uint32(61)).Return(nil).Once()562 k.EXPECT().destroyBlob(uint32(61)).Return(nil).Once()
563+ k.EXPECT().destroyBlob(lut3dBlob).Return(nil).Once()
495 // Unknown state: Close still writes Bypass.564 // Unknown state: Close still writes Bypass.
496 o.Close()565 o.Close()
497 bypassed(t, (*commits)[0])566 bypassed(t, (*commits)[0])
498- if o.ctmBlob != 0 {567+ if o.ctmBlob != 0 || o.lut3dBlob != 0 {
499- t.Fatal("CTM blob kept")568+ t.Fatal("colour blobs kept")
500 }569 }
501 })570 })
502 t.Run("modeset", func(t *testing.T) {571 t.Run("modeset", func(t *testing.T) {
@@ -789,15 +858,41 @@ func TestColorOverlayInHDR(t *testing.T) {
789 }858 }
790 859
791 func TestSetupColor(t *testing.T) {860 func TestSetupColor(t *testing.T) {
792- t.Run("HDR makes the matrix blob and the multiplier", func(t *testing.T) {861+ t.Run("HDR makes the matrix blob, the multiplier and the identity 3D LUT", func(t *testing.T) {
793 o, k, _ := colorOutput(t, false, nil)862 o, k, _ := colorOutput(t, false, nil)
794- o.ctmBlob, o.colorMult = 0, 0863+ o.ctmBlob, o.lut3dBlob, o.colorMult = 0, 0, 0
795 o.hdr.settings = HDRSettings{Enabled: true, SDRBrightness: 160}864 o.hdr.settings = HDRSettings{Enabled: true, SDRBrightness: 160}
796 o.hdr.cap.Capable = true865 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()866 k.EXPECT().createBlob(mock.MatchedBy(func(b []byte) bool { return slices.Equal(b, ctmBytes(bt709ToBT2020)) })).Return(uint32(61), nil).Once()
867+ k.EXPECT().createBlob(mock.MatchedBy(func(b []byte) bool { return slices.Equal(b, lut3dIdentityBytes(17)) })).Return(uint32(62), nil).Once()
868+ o.setupColor()
869+ if o.ctmBlob != 61 || o.lut3dBlob != 62 || o.colorMult != 2<<32 {
870+ t.Fatalf("blobs %d %d multiplier %#x", o.ctmBlob, o.lut3dBlob, o.colorMult)
871+ }
872+ })
873+ t.Run("a pipeline without 3D LUT makes no LUT blob", func(t *testing.T) {
874+ o, k, _ := testOutput(t)
875+ colorPlane(k, o.primary, pColorPipe, opBase, []opSpec{opSRGB, opMult, opCTM, opPQ})
876+ o.hdr.on = true
877+ o.readColor()
878+ o.hdr.settings = HDRSettings{Enabled: true, SDRBrightness: 160}
879+ o.hdr.cap.Capable = true
880+ k.EXPECT().createBlob(mock.Anything).Return(uint32(61), nil).Once()
881+ o.setupColor()
882+ if o.ctmBlob != 61 || o.lut3dBlob != 0 {
883+ t.Fatalf("blobs %d %d", o.ctmBlob, o.lut3dBlob)
884+ }
885+ })
886+ t.Run("3D LUT blob failure turns the pipelines off", func(t *testing.T) {
887+ o, k, _ := colorOutput(t, true, nil)
888+ o.ctmBlob, o.lut3dBlob = 0, 0
889+ o.hdr.settings = HDRSettings{Enabled: true, SDRBrightness: 203}
890+ o.hdr.cap.Capable = true
891+ k.EXPECT().createBlob(mock.Anything).Return(uint32(61), nil).Once()
892+ k.EXPECT().createBlob(mock.Anything).Return(uint32(0), unix.ENOMEM).Once()
798 o.setupColor()893 o.setupColor()
799- if o.ctmBlob != 61 || o.colorMult != 2<<32 {894+ if o.primary.pipeline != nil || o.overlay.pipeline != nil || o.lut3dBlob != 0 {
800- t.Fatalf("blob %d multiplier %#x", o.ctmBlob, o.colorMult)895+ t.Fatal("pipelines kept without their 3D LUT blob")
801 }896 }
802 })897 })
803 t.Run("blob failure turns the pipelines off", func(t *testing.T) {898 t.Run("blob failure turns the pipelines off", func(t *testing.T) {
M internal/adapters/drm/output.go +6 -0
@@ -77,7 +77,9 @@ type Output struct {
7777 // ctmBlob is the 3x4 matrix blob of the planes' colour pipelines
7878 // (BT.709 to BT.2020), made at setup and destroyed by Close (0: none);
7979 // colorMult is the S31.32 multiplier, SDR white over 80 nits.
80+ // lut3dBlob is the identity 3D LUT blob (0: the pipelines have no 3D LUT).
8081 ctmBlob uint32
82+ lut3dBlob uint32
8183 colorMult uint64
8284 // seatDisable carries a request to leave the planes on Bypass before
8385 // the seat is disabled (PrepareSeatDisable); Run answers it.
@@ -1089,6 +1091,10 @@ func (o *Output) Close() {
10891091 _ = o.k.destroyBlob(o.ctmBlob)
10901092 o.ctmBlob = 0
10911093 }
1094+ if o.lut3dBlob != 0 {
1095+ _ = o.k.destroyBlob(o.lut3dBlob)
1096+ o.lut3dBlob = 0
1097+ }
10921098 if o.cursor != nil {
10931099 o.cursor.free()
10941100 }
@@ -77,7 +77,9 @@ type Output struct {
77 // ctmBlob is the 3x4 matrix blob of the planes' colour pipelines77 // ctmBlob is the 3x4 matrix blob of the planes' colour pipelines
78 // (BT.709 to BT.2020), made at setup and destroyed by Close (0: none);78 // (BT.709 to BT.2020), made at setup and destroyed by Close (0: none);
79 // colorMult is the S31.32 multiplier, SDR white over 80 nits.79 // colorMult is the S31.32 multiplier, SDR white over 80 nits.
80+ // lut3dBlob is the identity 3D LUT blob (0: the pipelines have no 3D LUT).
80 ctmBlob uint3281 ctmBlob uint32
82+ lut3dBlob uint32
81 colorMult uint6483 colorMult uint64
82 // seatDisable carries a request to leave the planes on Bypass before84 // seatDisable carries a request to leave the planes on Bypass before
83 // the seat is disabled (PrepareSeatDisable); Run answers it.85 // the seat is disabled (PrepareSeatDisable); Run answers it.
@@ -1089,6 +1091,10 @@ func (o *Output) Close() {
1089 _ = o.k.destroyBlob(o.ctmBlob)1091 _ = o.k.destroyBlob(o.ctmBlob)
1090 o.ctmBlob = 01092 o.ctmBlob = 0
1091 }1093 }
1094+ if o.lut3dBlob != 0 {
1095+ _ = o.k.destroyBlob(o.lut3dBlob)
1096+ o.lut3dBlob = 0
1097+ }
1092 if o.cursor != nil {1098 if o.cursor != nil {
1093 o.cursor.free()1099 o.cursor.free()
1094 }1100 }
M internal/adapters/drm/overlay.go +2 -2
@@ -304,9 +304,9 @@ func (o *Output) probeOverlay(fb uint32, ov overlayWin, mode colorMode, cur curs
304304 req := &o.probeReq // no frame request is alive: the frame commit follows
305305 req.reset()
306306 o.primaryProps(req, fb)
307- o.primary.colorProps(req, colorBypass, 0, 0)
307+ o.primary.colorProps(req, colorBypass, 0, 0, 0)
308308 o.overlayProps(req, ov)
309- o.overlay.colorProps(req, mode, o.colorMult, o.ctmBlob)
309+ o.overlay.colorProps(req, mode, o.colorMult, o.ctmBlob, o.lut3dBlob)
310310 if o.cursor != nil {
311311 o.cursor.props(req, o.crtc, cur)
312312 }
@@ -304,9 +304,9 @@ func (o *Output) probeOverlay(fb uint32, ov overlayWin, mode colorMode, cur curs
304 req := &o.probeReq // no frame request is alive: the frame commit follows304 req := &o.probeReq // no frame request is alive: the frame commit follows
305 req.reset()305 req.reset()
306 o.primaryProps(req, fb)306 o.primaryProps(req, fb)
307- o.primary.colorProps(req, colorBypass, 0, 0)307+ o.primary.colorProps(req, colorBypass, 0, 0, 0)
308 o.overlayProps(req, ov)308 o.overlayProps(req, ov)
309- o.overlay.colorProps(req, mode, o.colorMult, o.ctmBlob)309+ o.overlay.colorProps(req, mode, o.colorMult, o.ctmBlob, o.lut3dBlob)
310 if o.cursor != nil {310 if o.cursor != nil {
311 o.cursor.props(req, o.crtc, cur)311 o.cursor.props(req, o.crtc, cur)
312 }312 }
M internal/adapters/drm/scanout.go +1 -1
@@ -269,7 +269,7 @@ func (o *Output) scanoutFrame(scene ports.Scene, surfaces map[ports.WindowID]por
269269 // Entering scanout: how the buffer will be shown on the plane.
270270 o.renderLog.Info().Str("connector", o.conn.name).Uint32("format", c.DMABuf.Format).Uint64("modifier", c.DMABuf.Modifier).
271271 Bool("opaque", c.Opaque).Uint8("tf", c.Color.TF).Uint8("primaries", c.Color.Primaries).
272- Bool("hdr_on", o.hdr.on).Bool("pipeline_matched", o.primary.pipeline != nil).Bool("sdr_to_pq", mode == colorSDRToPQ).
272+ Bool("hdr_on", o.hdr.on).Bool("pipeline_matched", o.primary.pipeline != nil).Bool("sdr_to_pq", mode == colorSDRToPQ).Bool("lut3d_identity", o.lut3dBlob != 0).
273273 Msg("scanout colour")
274274 }
275275 o.setScanoutReason(reason)
@@ -269,7 +269,7 @@ func (o *Output) scanoutFrame(scene ports.Scene, surfaces map[ports.WindowID]por
269 // Entering scanout: how the buffer will be shown on the plane.269 // Entering scanout: how the buffer will be shown on the plane.
270 o.renderLog.Info().Str("connector", o.conn.name).Uint32("format", c.DMABuf.Format).Uint64("modifier", c.DMABuf.Modifier).270 o.renderLog.Info().Str("connector", o.conn.name).Uint32("format", c.DMABuf.Format).Uint64("modifier", c.DMABuf.Modifier).
271 Bool("opaque", c.Opaque).Uint8("tf", c.Color.TF).Uint8("primaries", c.Color.Primaries).271 Bool("opaque", c.Opaque).Uint8("tf", c.Color.TF).Uint8("primaries", c.Color.Primaries).
272- Bool("hdr_on", o.hdr.on).Bool("pipeline_matched", o.primary.pipeline != nil).Bool("sdr_to_pq", mode == colorSDRToPQ).272+ Bool("hdr_on", o.hdr.on).Bool("pipeline_matched", o.primary.pipeline != nil).Bool("sdr_to_pq", mode == colorSDRToPQ).Bool("lut3d_identity", o.lut3dBlob != 0).
273 Msg("scanout colour")273 Msg("scanout colour")
274 }274 }
275 o.setScanoutReason(reason)275 o.setScanoutReason(reason)

Keyboard shortcuts

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

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

Branches 23

Tags 10

↑↓ moveenter openesc close