detached 1ec6f71

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 VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · 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() {
266266 }
267267 o.ctmBlob = blob
268268 o.colorMult = s3132(float64(o.hdr.settings.SDRBrightness) / 80)
269+ o.alignLUT3DSizes()
269270 if n := o.lut3dSize(); n != 0 {
270271 if blob, err = o.k.createBlob(lut3dIdentityBytes(n)); err != nil {
271272 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() {
278279 Uint32("lut3d_size", o.lut3dSize()).Msg("plane colour objects")
279280 }
280281
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.
283300 func (o *Output) lut3dSize() uint32 {
284301 for _, p := range [...]*plane{o.primary, o.overlay} {
285302 if p != nil && p.pipeline != nil && p.pipeline.lut3d.op != 0 {
@@ -266,6 +266,7 @@ func (o *Output) setupColor() {
266 }266 }
267 o.ctmBlob = blob267 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 (both282+// 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) {
884884 t.Fatalf("blobs %d %d", o.ctmBlob, o.lut3dBlob)
885885 }
886886 })
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+ })
887913 t.Run("3D LUT blob failure turns the pipelines off", func(t *testing.T) {
888914 o, k, _ := colorOutput(t, true, nil)
889915 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, 0915 o.ctmBlob, o.lut3dBlob = 0, 0

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