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 Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· 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) { | |||
| 494 | 494 | k.EXPECT().rmFB(mock.Anything).Return(nil).Maybe() | |
| 495 | 495 | enableTestHDR(o) | |
| 496 | 496 | 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 | |
| 498 | 498 | o.modeBlob = 99 | |
| 499 | 499 | if err := o.modeset(); err != nil { | |
| 500 | 500 | 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 = 55 | 496 | 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 |
| 498 | o.modeBlob = 99 | 498 | 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 { | |||
| 23 | 23 | ||
| 24 | 24 | // colorProps puts mode on p: Bypass selects the plane's Bypass pipeline; | |
| 25 | 25 | // 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) { | |
| 29 | 30 | if !p.hasColor() { | |
| 30 | 31 | return | |
| 31 | 32 | } | |
| @@ -39,6 +40,14 @@ func (p *plane) colorProps(req *atomicReq, mode colorMode, mult uint64, ctm uint | |||
| 39 | 40 | for _, s := range cp.others { | |
| 40 | 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 | 51 | for _, s := range [...]struct { | |
| 43 | 52 | stage colorStage | |
| 44 | 53 | val uint64 | |
| @@ -83,7 +92,7 @@ func (o *Output) cursorShown() bool { return o.cursor != nil && !o.protected && | |||
| 83 | 92 | // primaryColorProps puts mode on the primary plane if it applies another one. | |
| 84 | 93 | func (o *Output) primaryColorProps(req *atomicReq, mode colorMode) { | |
| 85 | 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 | 103 | // compositor never sets them, and a ramp left by another DRM master would | |
| 95 | 104 | // apply on top of the PQ output. | |
| 96 | 105 | 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) | |
| 99 | 108 | for _, name := range [...]string{"DEGAMMA_LUT", "CTM", "GAMMA_LUT"} { | |
| 100 | 109 | req.set(o.crtc, o.crtcProps[name], 0) | |
| 101 | 110 | } | |
| @@ -104,7 +113,7 @@ func (o *Output) forceBypass(req *atomicReq) { | |||
| 104 | 113 | // overlayColorProps puts mode on the overlay plane if it applies another one. | |
| 105 | 114 | func (o *Output) overlayColorProps(req *atomicReq, mode colorMode) { | |
| 106 | 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 | 229 | req := &o.probeReq | |
| 221 | 230 | req.reset() | |
| 222 | 231 | test(req) | |
| 223 | - | p.colorProps(req, colorSDRToPQ, o.colorMult, o.ctmBlob) | |
| 232 | + | p.colorProps(req, colorSDRToPQ, o.colorMult, o.ctmBlob, o.lut3dBlob) | |
| 224 | 233 | if cursor { | |
| 225 | 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 | 245 | return err == nil | |
| 237 | 246 | } | |
| 238 | 247 | ||
| 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. | |
| 242 | 252 | func (o *Output) setupColor() { | |
| 243 | 253 | if !o.hdr.wanted() { | |
| 244 | 254 | // The pipelines serve HDR outputs only: SDR stays on Bypass. | |
| @@ -256,6 +266,25 @@ func (o *Output) setupColor() { | |||
| 256 | 266 | } | |
| 257 | 267 | o.ctmBlob = blob | |
| 258 | 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 | 290 | // 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 and | 25 | // 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 | 26 | + | // 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 | return | 31 | 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 colorStage | 52 | stage colorStage | ||
| 44 | val uint64 | 53 | 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 would | 103 | // 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.probeReq | 229 | 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 == nil | 245 | return err == nil | ||
| 237 | } | 246 | } | ||
| 238 | 247 | ||||
| 239 | - | // setupColor makes the output's colour objects: the CTM blob and the | 248 | + | // 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. A | 249 | + | // 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 = blob | 267 | 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 content | 290 | // 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 ( | |||
| 47 | 47 | colorTypeCurve = "1D Curve" | |
| 48 | 48 | colorTypeMultiplier = "Multiplier" | |
| 49 | 49 | colorTypeMatrix = "3x4 Matrix" | |
| 50 | + | colorTypeLUT3D = "3D LUT" | |
| 50 | 51 | curveSRGBEOTF = "sRGB EOTF" | |
| 51 | 52 | curvePQInverseEOTF = "PQ 125 Inverse EOTF" | |
| 52 | 53 | ) | |
| @@ -58,6 +59,8 @@ type colorop struct { | |||
| 58 | 59 | typ string | |
| 59 | 60 | bypass, curveProp, multiplier, data uint32 | |
| 60 | 61 | 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 | 66 | // colorStage is an operation the output drives: its object, BYPASS and the | |
| @@ -76,6 +79,12 @@ type colorPipeline struct { | |||
| 76 | 79 | // are every other operation, which must stay bypassed. | |
| 77 | 80 | srgb, mult, ctm, pq colorStage | |
| 78 | 81 | 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 | 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 | 135 | if op.bypass == 0 { | |
| 127 | 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 | 142 | cp.others = append(cp.others, colorStage{op: op.id, bypass: op.bypass}) | |
| 130 | 143 | } | |
| 131 | 144 | if n < len(want) { | |
| @@ -148,7 +161,7 @@ func walkColorops(k kms, first uint32) ([]colorop, error) { | |||
| 148 | 161 | if err != nil { | |
| 149 | 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 | 165 | if t := props["TYPE"]; t[0] != 0 { | |
| 153 | 166 | types, err := k.propEnums(uint32(t[0])) | |
| 154 | 167 | if err != nil { | |
| @@ -244,6 +257,26 @@ func ctmBytes(m [3][3]float64) []byte { | |||
| 244 | 257 | 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 | 280 | // planeColor decides how plane (with pipeline, nil: none) shows a client | |
| 248 | 281 | // buffer of format whose content is c, on an output in HDR10 mode when hdrOn. | |
| 249 | 282 | // 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 string | 59 | typ string | ||
| 59 | bypass, curveProp, multiplier, data uint32 | 60 | bypass, curveProp, multiplier, data uint32 | ||
| 60 | curves map[string]uint64 | 61 | 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 the | 66 | // 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 colorStage | 80 | srgb, mult, ctm, pq colorStage | ||
| 78 | others []colorStage | 81 | 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 b | 257 | 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 client | 280 | // 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 encoding | 282 | // 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 { | |||
| 42 | 42 | typ string | |
| 43 | 43 | curves map[string]uint64 | |
| 44 | 44 | noBypass bool | |
| 45 | + | // size is the SIZE property of a LUT (0: no such property). | |
| 46 | + | size uint32 | |
| 45 | 47 | } | |
| 46 | 48 | ||
| 47 | 49 | func curveOp(curves map[string]uint64) opSpec { return opSpec{typ: "1D Curve", curves: curves} } | |
| @@ -50,10 +52,12 @@ var ( | |||
| 50 | 52 | opSRGB, opPQ = curveOp(firstCurve), curveOp(lastCurve) | |
| 51 | 53 | opMult = opSpec{typ: "Multiplier"} | |
| 52 | 54 | 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} | |
| 55 | 57 | // amdgpuChain is the pipeline read on kernel 7.2 (DCN3+). | |
| 56 | 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 | 63 | // 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 | 93 | case "3x4 Matrix", "1D LUT", "3D LUT": | |
| 90 | 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 | 99 | k.EXPECT().objProps(id, uint32(objColorop)).Return(props, nil).Maybe() | |
| 93 | 100 | id++ | |
| 94 | 101 | } | |
| @@ -114,6 +121,8 @@ func TestMatchColorPipeline(t *testing.T) { | |||
| 114 | 121 | }{ | |
| 115 | 122 | {"amdgpu", [][]opSpec{amdgpuChain}, 0, ""}, | |
| 116 | 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 | 126 | {"bypassable op before the chain", [][]opSpec{{opLUT1D, opSRGB, opMult, opCTM, opPQ}}, 0, ""}, | |
| 118 | 127 | {"second pipeline matches", [][]opSpec{{opLUT1D}, amdgpuChain}, 1, ""}, | |
| 119 | 128 | {"no pipeline", nil, -1, "only Bypass"}, | |
| @@ -164,8 +173,27 @@ func TestMatchColorPipeline(t *testing.T) { | |||
| 164 | 173 | cp.pq != (colorStage{op: pq, bypass: bypassProp(pq), prop: curveProp(pq), curve: 8}) { | |
| 165 | 174 | t.Fatalf("stages %+v", cp) | |
| 166 | 175 | } | |
| 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) | |
| 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 | 339 | func TestPlaneColor(t *testing.T) { | |
| 277 | 340 | pq := ports.SurfaceColor{TF: ports.ColorTFPQ, Primaries: ports.ColorPrimariesBT2020} | |
| 278 | 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 | 404 | o.hdr.on = true | |
| 342 | 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 | 407 | if o.primary.pipeline == nil || overlay && o.overlay.pipeline == nil { | |
| 345 | 408 | t.Fatal("pipeline not matched") | |
| 346 | 409 | } | |
| @@ -391,11 +454,16 @@ func TestColorCommitTransitions(t *testing.T) { | |||
| 391 | 454 | {cp.mult.op, cp.mult.bypass, 0}, {cp.mult.op, cp.mult.prop, s3132(203.0 / 80)}, | |
| 392 | 455 | {cp.ctm.op, cp.ctm.bypass, 0}, {cp.ctm.op, cp.ctm.prop, 61}, | |
| 393 | 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 | 460 | if v, ok := first.req.value(s.op, s.prop); !ok || v != s.want { | |
| 396 | 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 | 467 | for _, other := range cp.others { | |
| 400 | 468 | if v, ok := first.req.value(other.op, other.bypass); !ok || v != 1 { | |
| 401 | 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 | 560 | o, k, commits := colorOutput(t, true, nil) | |
| 493 | 561 | k.EXPECT().rmFB(mock.Anything).Return(nil) | |
| 494 | 562 | k.EXPECT().destroyBlob(uint32(61)).Return(nil).Once() | |
| 563 | + | k.EXPECT().destroyBlob(lut3dBlob).Return(nil).Once() | |
| 495 | 564 | // Unknown state: Close still writes Bypass. | |
| 496 | 565 | o.Close() | |
| 497 | 566 | 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") | |
| 500 | 569 | } | |
| 501 | 570 | }) | |
| 502 | 571 | t.Run("modeset", func(t *testing.T) { | |
| @@ -789,15 +858,41 @@ func TestColorOverlayInHDR(t *testing.T) { | |||
| 789 | 858 | } | |
| 790 | 859 | ||
| 791 | 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 | 862 | o, k, _ := colorOutput(t, false, nil) | |
| 794 | - | o.ctmBlob, o.colorMult = 0, 0 | |
| 863 | + | o.ctmBlob, o.lut3dBlob, o.colorMult = 0, 0, 0 | |
| 795 | 864 | o.hdr.settings = HDRSettings{Enabled: true, SDRBrightness: 160} | |
| 796 | 865 | o.hdr.cap.Capable = true | |
| 797 | 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 | 893 | 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") | |
| 801 | 896 | } | |
| 802 | 897 | }) | |
| 803 | 898 | t.Run("blob failure turns the pipelines off", func(t *testing.T) { | |
| @@ -42,6 +42,8 @@ type opSpec struct { | |||||
| 42 | typ string | 42 | typ string | ||
| 43 | curves map[string]uint64 | 43 | curves map[string]uint64 | ||
| 44 | noBypass bool | 44 | 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 the | 63 | // 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 = true | 404 | 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, 0 | 863 | + | 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 = true | 865 | 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 { | |||
| 77 | 77 | // ctmBlob is the 3x4 matrix blob of the planes' colour pipelines | |
| 78 | 78 | // (BT.709 to BT.2020), made at setup and destroyed by Close (0: none); | |
| 79 | 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 | 81 | ctmBlob uint32 | |
| 82 | + | lut3dBlob uint32 | |
| 81 | 83 | colorMult uint64 | |
| 82 | 84 | // seatDisable carries a request to leave the planes on Bypass before | |
| 83 | 85 | // the seat is disabled (PrepareSeatDisable); Run answers it. | |
| @@ -1089,6 +1091,10 @@ func (o *Output) Close() { | |||
| 1089 | 1091 | _ = o.k.destroyBlob(o.ctmBlob) | |
| 1090 | 1092 | o.ctmBlob = 0 | |
| 1091 | 1093 | } | |
| 1094 | + | if o.lut3dBlob != 0 { | |
| 1095 | + | _ = o.k.destroyBlob(o.lut3dBlob) | |
| 1096 | + | o.lut3dBlob = 0 | |
| 1097 | + | } | |
| 1092 | 1098 | if o.cursor != nil { | |
| 1093 | 1099 | o.cursor.free() | |
| 1094 | 1100 | } | |
| @@ -77,7 +77,9 @@ type Output struct { | |||||
| 77 | // ctmBlob is the 3x4 matrix blob of the planes' colour pipelines | 77 | // 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 uint32 | 81 | ctmBlob uint32 | ||
| 82 | + | lut3dBlob uint32 | |||
| 81 | colorMult uint64 | 83 | colorMult uint64 | ||
| 82 | // seatDisable carries a request to leave the planes on Bypass before | 84 | // 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 = 0 | 1092 | 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 | |||
| 304 | 304 | req := &o.probeReq // no frame request is alive: the frame commit follows | |
| 305 | 305 | req.reset() | |
| 306 | 306 | o.primaryProps(req, fb) | |
| 307 | - | o.primary.colorProps(req, colorBypass, 0, 0) | |
| 307 | + | o.primary.colorProps(req, colorBypass, 0, 0, 0) | |
| 308 | 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 | 310 | if o.cursor != nil { | |
| 311 | 311 | o.cursor.props(req, o.crtc, cur) | |
| 312 | 312 | } | |
| @@ -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 follows | 304 | 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 | |||
| 269 | 269 | // Entering scanout: how the buffer will be shown on the plane. | |
| 270 | 270 | o.renderLog.Info().Str("connector", o.conn.name).Uint32("format", c.DMABuf.Format).Uint64("modifier", c.DMABuf.Modifier). | |
| 271 | 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 | 273 | Msg("scanout colour") | |
| 274 | 274 | } | |
| 275 | 275 | 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) | ||