fix(drm): turn the overlay colour pipeline off when its 3D LUT size differs
- commit
- 1ec6f714112190b1c65e4318f0de94d8fd5f748b
- author
- brice · · 2026-10-23 05:36 UTC
- parent
- bd4cecf
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
2 files · +45 -2
M internal/adapters/drm/colorop.go +19 -2
| @@ -266,6 +266,7 @@ func (o *Output) setupColor() { | |||
| 266 | 266 | } | |
| 267 | 267 | o.ctmBlob = blob | |
| 268 | 268 | o.colorMult = s3132(float64(o.hdr.settings.SDRBrightness) / 80) | |
| 269 | + | o.alignLUT3DSizes() | |
| 269 | 270 | if n := o.lut3dSize(); n != 0 { | |
| 270 | 271 | if blob, err = o.k.createBlob(lut3dIdentityBytes(n)); err != nil { | |
| 271 | 272 | o.log.Warn().Err(err).Str("connector", o.conn.name).Uint32("size", n).Msg("identity 3D LUT blob; plane colour pipelines off") | |
| @@ -278,8 +279,24 @@ func (o *Output) setupColor() { | |||
| 278 | 279 | Uint32("lut3d_size", o.lut3dSize()).Msg("plane colour objects") | |
| 279 | 280 | } | |
| 280 | 281 | ||
| 281 | - | // lut3dSize is the dimension of the 3D LUT the planes' pipelines have (both | |
| 282 | - | // planes of an output share the same hardware block), 0 without one. | |
| 282 | + | // alignLUT3DSizes drops the overlay's pipeline when its 3D LUT size differs | |
| 283 | + | // from the primary's: the output has one identity LUT blob, sized for the | |
| 284 | + | // primary. SDR content on the overlay is then composed. | |
| 285 | + | func (o *Output) alignLUT3DSizes() { | |
| 286 | + | p, ov := o.primary.pipeline, (*colorPipeline)(nil) | |
| 287 | + | if o.overlay != nil { | |
| 288 | + | ov = o.overlay.pipeline | |
| 289 | + | } | |
| 290 | + | if p == nil || ov == nil || p.lut3dSize == ov.lut3dSize { | |
| 291 | + | return | |
| 292 | + | } | |
| 293 | + | o.log.Warn().Str("connector", o.conn.name).Uint32("primary", p.lut3dSize).Uint32("overlay", ov.lut3dSize). | |
| 294 | + | Msg("3D LUT sizes differ; overlay colour pipeline off") | |
| 295 | + | o.overlay.pipeline = nil | |
| 296 | + | } | |
| 297 | + | ||
| 298 | + | // lut3dSize is the dimension of the 3D LUT the planes' pipelines have, 0 | |
| 299 | + | // without one. alignLUT3DSizes makes it the same for both planes. | |
| 283 | 300 | func (o *Output) lut3dSize() uint32 { | |
| 284 | 301 | for _, p := range [...]*plane{o.primary, o.overlay} { | |
| 285 | 302 | if p != nil && p.pipeline != nil && p.pipeline.lut3d.op != 0 { | |
| @@ -266,6 +266,7 @@ func (o *Output) setupColor() { | |||||
| 266 | } | 266 | } | ||
| 267 | o.ctmBlob = blob | 267 | o.ctmBlob = blob | ||
| 268 | o.colorMult = s3132(float64(o.hdr.settings.SDRBrightness) / 80) | 268 | o.colorMult = s3132(float64(o.hdr.settings.SDRBrightness) / 80) | ||
| 269 | + | o.alignLUT3DSizes() | |||
| 269 | if n := o.lut3dSize(); n != 0 { | 270 | if n := o.lut3dSize(); n != 0 { | ||
| 270 | if blob, err = o.k.createBlob(lut3dIdentityBytes(n)); err != nil { | 271 | 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.log.Warn().Err(err).Str("connector", o.conn.name).Uint32("size", n).Msg("identity 3D LUT blob; plane colour pipelines off") | ||
| @@ -278,8 +279,24 @@ func (o *Output) setupColor() { | |||||
| 278 | Uint32("lut3d_size", o.lut3dSize()).Msg("plane colour objects") | 279 | Uint32("lut3d_size", o.lut3dSize()).Msg("plane colour objects") | ||
| 279 | } | 280 | } | ||
| 280 | 281 | ||||
| 281 | - | // lut3dSize is the dimension of the 3D LUT the planes' pipelines have (both | 282 | + | // alignLUT3DSizes drops the overlay's pipeline when its 3D LUT size differs |
| 282 | - | // planes of an output share the same hardware block), 0 without one. | 283 | + | // from the primary's: the output has one identity LUT blob, sized for the |
| 284 | + | // primary. SDR content on the overlay is then composed. | |||
| 285 | + | func (o *Output) alignLUT3DSizes() { | |||
| 286 | + | p, ov := o.primary.pipeline, (*colorPipeline)(nil) | |||
| 287 | + | if o.overlay != nil { | |||
| 288 | + | ov = o.overlay.pipeline | |||
| 289 | + | } | |||
| 290 | + | if p == nil || ov == nil || p.lut3dSize == ov.lut3dSize { | |||
| 291 | + | return | |||
| 292 | + | } | |||
| 293 | + | o.log.Warn().Str("connector", o.conn.name).Uint32("primary", p.lut3dSize).Uint32("overlay", ov.lut3dSize). | |||
| 294 | + | Msg("3D LUT sizes differ; overlay colour pipeline off") | |||
| 295 | + | o.overlay.pipeline = nil | |||
| 296 | + | } | |||
| 297 | + | ||||
| 298 | + | // lut3dSize is the dimension of the 3D LUT the planes' pipelines have, 0 | |||
| 299 | + | // without one. alignLUT3DSizes makes it the same for both planes. | |||
| 283 | func (o *Output) lut3dSize() uint32 { | 300 | func (o *Output) lut3dSize() uint32 { | ||
| 284 | for _, p := range [...]*plane{o.primary, o.overlay} { | 301 | for _, p := range [...]*plane{o.primary, o.overlay} { | ||
| 285 | if p != nil && p.pipeline != nil && p.pipeline.lut3d.op != 0 { | 302 | if p != nil && p.pipeline != nil && p.pipeline.lut3d.op != 0 { | ||
M internal/adapters/drm/colorop_test.go +26 -0
| @@ -884,6 +884,32 @@ func TestSetupColor(t *testing.T) { | |||
| 884 | 884 | t.Fatalf("blobs %d %d", o.ctmBlob, o.lut3dBlob) | |
| 885 | 885 | } | |
| 886 | 886 | }) | |
| 887 | + | t.Run("an overlay 3D LUT of another size turns the overlay pipeline off", func(t *testing.T) { | |
| 888 | + | o, k, _ := testOutput(t) | |
| 889 | + | props := maps0(planeProps) | |
| 890 | + | props["zpos"] = pZpos | |
| 891 | + | o.primary.props = props | |
| 892 | + | colorPlane(k, o.primary, pColorPipe, opBase, amdgpuChain) | |
| 893 | + | ov := &plane{id: tOverlay, typ: planeOverlay, props: props, zposValue: 2, formats: []ports.DMABufFormat{{Format: fourccXRGB}}} | |
| 894 | + | other := slices.Clone(amdgpuChain) | |
| 895 | + | other[5] = opSpec{typ: "3D LUT", size: 9} | |
| 896 | + | colorPlane(k, ov, pColorPipeOverlay, opBase+20, other) | |
| 897 | + | o.stray = []*plane{ov} | |
| 898 | + | o.pickOverlay() | |
| 899 | + | o.hdr.on = true | |
| 900 | + | o.readColor() | |
| 901 | + | if o.overlay == nil || o.overlay.pipeline == nil || o.primary.pipeline == nil { | |
| 902 | + | t.Fatal("pipelines not matched") | |
| 903 | + | } | |
| 904 | + | o.hdr.settings = HDRSettings{Enabled: true, SDRBrightness: 203} | |
| 905 | + | o.hdr.cap.Capable = true | |
| 906 | + | k.EXPECT().createBlob(mock.MatchedBy(func(b []byte) bool { return slices.Equal(b, ctmBytes(bt709ToBT2020)) })).Return(uint32(61), nil).Once() | |
| 907 | + | k.EXPECT().createBlob(mock.MatchedBy(func(b []byte) bool { return slices.Equal(b, lut3dIdentityBytes(17)) })).Return(uint32(62), nil).Once() | |
| 908 | + | o.setupColor() | |
| 909 | + | if o.primary.pipeline == nil || o.overlay.pipeline != nil || o.lut3dBlob != 62 { | |
| 910 | + | t.Fatalf("primary %v overlay %v lut %d", o.primary.pipeline != nil, o.overlay.pipeline != nil, o.lut3dBlob) | |
| 911 | + | } | |
| 912 | + | }) | |
| 887 | 913 | t.Run("3D LUT blob failure turns the pipelines off", func(t *testing.T) { | |
| 888 | 914 | o, k, _ := colorOutput(t, true, nil) | |
| 889 | 915 | o.ctmBlob, o.lut3dBlob = 0, 0 | |
| @@ -884,6 +884,32 @@ func TestSetupColor(t *testing.T) { | |||||
| 884 | t.Fatalf("blobs %d %d", o.ctmBlob, o.lut3dBlob) | 884 | t.Fatalf("blobs %d %d", o.ctmBlob, o.lut3dBlob) | ||
| 885 | } | 885 | } | ||
| 886 | }) | 886 | }) | ||
| 887 | + | t.Run("an overlay 3D LUT of another size turns the overlay pipeline off", func(t *testing.T) { | |||
| 888 | + | o, k, _ := testOutput(t) | |||
| 889 | + | props := maps0(planeProps) | |||
| 890 | + | props["zpos"] = pZpos | |||
| 891 | + | o.primary.props = props | |||
| 892 | + | colorPlane(k, o.primary, pColorPipe, opBase, amdgpuChain) | |||
| 893 | + | ov := &plane{id: tOverlay, typ: planeOverlay, props: props, zposValue: 2, formats: []ports.DMABufFormat{{Format: fourccXRGB}}} | |||
| 894 | + | other := slices.Clone(amdgpuChain) | |||
| 895 | + | other[5] = opSpec{typ: "3D LUT", size: 9} | |||
| 896 | + | colorPlane(k, ov, pColorPipeOverlay, opBase+20, other) | |||
| 897 | + | o.stray = []*plane{ov} | |||
| 898 | + | o.pickOverlay() | |||
| 899 | + | o.hdr.on = true | |||
| 900 | + | o.readColor() | |||
| 901 | + | if o.overlay == nil || o.overlay.pipeline == nil || o.primary.pipeline == nil { | |||
| 902 | + | t.Fatal("pipelines not matched") | |||
| 903 | + | } | |||
| 904 | + | o.hdr.settings = HDRSettings{Enabled: true, SDRBrightness: 203} | |||
| 905 | + | o.hdr.cap.Capable = true | |||
| 906 | + | k.EXPECT().createBlob(mock.MatchedBy(func(b []byte) bool { return slices.Equal(b, ctmBytes(bt709ToBT2020)) })).Return(uint32(61), nil).Once() | |||
| 907 | + | k.EXPECT().createBlob(mock.MatchedBy(func(b []byte) bool { return slices.Equal(b, lut3dIdentityBytes(17)) })).Return(uint32(62), nil).Once() | |||
| 908 | + | o.setupColor() | |||
| 909 | + | if o.primary.pipeline == nil || o.overlay.pipeline != nil || o.lut3dBlob != 62 { | |||
| 910 | + | t.Fatalf("primary %v overlay %v lut %d", o.primary.pipeline != nil, o.overlay.pipeline != nil, o.lut3dBlob) | |||
| 911 | + | } | |||
| 912 | + | }) | |||
| 887 | t.Run("3D LUT blob failure turns the pipelines off", func(t *testing.T) { | 913 | t.Run("3D LUT blob failure turns the pipelines off", func(t *testing.T) { | ||
| 888 | o, k, _ := colorOutput(t, true, nil) | 914 | o, k, _ := colorOutput(t, true, nil) | ||
| 889 | o.ctmBlob, o.lut3dBlob = 0, 0 | 915 | o.ctmBlob, o.lut3dBlob = 0, 0 | ||