detached be5748f

fix(drm): compare 3D LUT sizes only when both colour pipelines have one

commit
be5748f60fbbd9f56e5c75855b385b9027dba458
author
brice · · 2026-10-23 05:38 UTC
parent
40705c4
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
checks
1/1
2 files · +43 -28
M internal/adapters/drm/colorop.go +5 -4
@@ -279,15 +279,16 @@ func (o *Output) setupColor() {
279279 Uint32("lut3d_size", o.lut3dSize()).Msg("plane colour objects")
280280 }
281281
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.
282+// alignLUT3DSizes drops the overlay's pipeline when both planes have a 3D
283+// LUT of different sizes: the output has one identity LUT blob, sized for
284+// the primary. A pipeline without a 3D LUT ignores the blob. SDR content on
285+// the overlay is then composed.
285286 func (o *Output) alignLUT3DSizes() {
286287 p, ov := o.primary.pipeline, (*colorPipeline)(nil)
287288 if o.overlay != nil {
288289 ov = o.overlay.pipeline
289290 }
290- if p == nil || ov == nil || p.lut3dSize == ov.lut3dSize {
291+ if p == nil || ov == nil || p.lut3d.op == 0 || ov.lut3d.op == 0 || p.lut3dSize == ov.lut3dSize {
291292 return
292293 }
293294 o.log.Warn().Str("connector", o.conn.name).Uint32("primary", p.lut3dSize).Uint32("overlay", ov.lut3dSize).
@@ -279,15 +279,16 @@ func (o *Output) setupColor() {
279 Uint32("lut3d_size", o.lut3dSize()).Msg("plane colour objects")279 Uint32("lut3d_size", o.lut3dSize()).Msg("plane colour objects")
280 }280 }
281 281
282-// alignLUT3DSizes drops the overlay's pipeline when its 3D LUT size differs282+// alignLUT3DSizes drops the overlay's pipeline when both planes have a 3D
283-// from the primary's: the output has one identity LUT blob, sized for the283+// LUT of different sizes: the output has one identity LUT blob, sized for
284-// primary. SDR content on the overlay is then composed.284+// the primary. A pipeline without a 3D LUT ignores the blob. SDR content on
285+// the overlay is then composed.
285 func (o *Output) alignLUT3DSizes() {286 func (o *Output) alignLUT3DSizes() {
286 p, ov := o.primary.pipeline, (*colorPipeline)(nil)287 p, ov := o.primary.pipeline, (*colorPipeline)(nil)
287 if o.overlay != nil {288 if o.overlay != nil {
288 ov = o.overlay.pipeline289 ov = o.overlay.pipeline
289 }290 }
290- if p == nil || ov == nil || p.lut3dSize == ov.lut3dSize {291+ if p == nil || ov == nil || p.lut3d.op == 0 || ov.lut3d.op == 0 || p.lut3dSize == ov.lut3dSize {
291 return292 return
292 }293 }
293 o.log.Warn().Str("connector", o.conn.name).Uint32("primary", p.lut3dSize).Uint32("overlay", ov.lut3dSize).294 o.log.Warn().Str("connector", o.conn.name).Uint32("primary", p.lut3dSize).Uint32("overlay", ov.lut3dSize).
M internal/adapters/drm/colorop_test.go +38 -24
@@ -884,30 +884,44 @@ 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)
887+ t.Run("3D LUT sizes of the primary and overlay pipelines", func(t *testing.T) {
888+ noLUT := []opSpec{opSRGB, opMult, opCTM, opPQ}
889+ small := slices.Clone(amdgpuChain)
890+ small[5] = opSpec{typ: "3D LUT", size: 9}
891+ for _, tc := range []struct {
892+ name string
893+ primary, overlay []opSpec
894+ lut int // size of the identity LUT blob, 0: none
895+ overlayKept bool
896+ }{
897+ {"different sizes drop the overlay pipeline", amdgpuChain, small, 17, false},
898+ {"a primary without 3D LUT keeps both", noLUT, amdgpuChain, 17, true},
899+ {"an overlay without 3D LUT keeps both", amdgpuChain, noLUT, 17, true},
900+ } {
901+ t.Run(tc.name, func(t *testing.T) {
902+ o, k, _ := testOutput(t)
903+ props := maps0(planeProps)
904+ props["zpos"] = pZpos
905+ o.primary.props = props
906+ colorPlane(k, o.primary, pColorPipe, opBase, tc.primary)
907+ ov := &plane{id: tOverlay, typ: planeOverlay, props: props, zposValue: 2, formats: []ports.DMABufFormat{{Format: fourccXRGB}}}
908+ colorPlane(k, ov, pColorPipeOverlay, opBase+20, tc.overlay)
909+ o.stray = []*plane{ov}
910+ o.pickOverlay()
911+ o.hdr.on = true
912+ o.readColor()
913+ if o.overlay == nil || o.overlay.pipeline == nil || o.primary.pipeline == nil {
914+ t.Fatal("pipelines not matched")
915+ }
916+ o.hdr.settings = HDRSettings{Enabled: true, SDRBrightness: 203}
917+ o.hdr.cap.Capable = true
918+ k.EXPECT().createBlob(mock.MatchedBy(func(b []byte) bool { return slices.Equal(b, ctmBytes(bt709ToBT2020)) })).Return(uint32(61), nil).Once()
919+ k.EXPECT().createBlob(mock.MatchedBy(func(b []byte) bool { return slices.Equal(b, lut3dIdentityBytes(uint32(tc.lut))) })).Return(uint32(62), nil).Once()
920+ o.setupColor()
921+ if o.primary.pipeline == nil || (o.overlay.pipeline != nil) != tc.overlayKept || o.lut3dBlob != 62 {
922+ t.Fatalf("primary %v overlay %v lut %d", o.primary.pipeline != nil, o.overlay.pipeline != nil, o.lut3dBlob)
923+ }
924+ })
911925 }
912926 })
913927 t.Run("3D LUT blob failure turns the pipelines off", func(t *testing.T) {
@@ -884,30 +884,44 @@ 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) {887+ t.Run("3D LUT sizes of the primary and overlay pipelines", func(t *testing.T) {
888- o, k, _ := testOutput(t)888+ noLUT := []opSpec{opSRGB, opMult, opCTM, opPQ}
889- props := maps0(planeProps)889+ small := slices.Clone(amdgpuChain)
890- props["zpos"] = pZpos890+ small[5] = opSpec{typ: "3D LUT", size: 9}
891- o.primary.props = props891+ for _, tc := range []struct {
892- colorPlane(k, o.primary, pColorPipe, opBase, amdgpuChain)892+ name string
893- ov := &plane{id: tOverlay, typ: planeOverlay, props: props, zposValue: 2, formats: []ports.DMABufFormat{{Format: fourccXRGB}}}893+ primary, overlay []opSpec
894- other := slices.Clone(amdgpuChain)894+ lut int // size of the identity LUT blob, 0: none
895- other[5] = opSpec{typ: "3D LUT", size: 9}895+ overlayKept bool
896- colorPlane(k, ov, pColorPipeOverlay, opBase+20, other)896+ }{
897- o.stray = []*plane{ov}897+ {"different sizes drop the overlay pipeline", amdgpuChain, small, 17, false},
898- o.pickOverlay()898+ {"a primary without 3D LUT keeps both", noLUT, amdgpuChain, 17, true},
899- o.hdr.on = true899+ {"an overlay without 3D LUT keeps both", amdgpuChain, noLUT, 17, true},
900- o.readColor()900+ } {
901- if o.overlay == nil || o.overlay.pipeline == nil || o.primary.pipeline == nil {901+ t.Run(tc.name, func(t *testing.T) {
902- t.Fatal("pipelines not matched")902+ o, k, _ := testOutput(t)
903- }903+ props := maps0(planeProps)
904- o.hdr.settings = HDRSettings{Enabled: true, SDRBrightness: 203}904+ props["zpos"] = pZpos
905- o.hdr.cap.Capable = true905+ o.primary.props = props
906- k.EXPECT().createBlob(mock.MatchedBy(func(b []byte) bool { return slices.Equal(b, ctmBytes(bt709ToBT2020)) })).Return(uint32(61), nil).Once()906+ colorPlane(k, o.primary, pColorPipe, opBase, tc.primary)
907- k.EXPECT().createBlob(mock.MatchedBy(func(b []byte) bool { return slices.Equal(b, lut3dIdentityBytes(17)) })).Return(uint32(62), nil).Once()907+ ov := &plane{id: tOverlay, typ: planeOverlay, props: props, zposValue: 2, formats: []ports.DMABufFormat{{Format: fourccXRGB}}}
908- o.setupColor()908+ colorPlane(k, ov, pColorPipeOverlay, opBase+20, tc.overlay)
909- if o.primary.pipeline == nil || o.overlay.pipeline != nil || o.lut3dBlob != 62 {909+ o.stray = []*plane{ov}
910- t.Fatalf("primary %v overlay %v lut %d", o.primary.pipeline != nil, o.overlay.pipeline != nil, o.lut3dBlob)910+ o.pickOverlay()
911+ o.hdr.on = true
912+ o.readColor()
913+ if o.overlay == nil || o.overlay.pipeline == nil || o.primary.pipeline == nil {
914+ t.Fatal("pipelines not matched")
915+ }
916+ o.hdr.settings = HDRSettings{Enabled: true, SDRBrightness: 203}
917+ o.hdr.cap.Capable = true
918+ k.EXPECT().createBlob(mock.MatchedBy(func(b []byte) bool { return slices.Equal(b, ctmBytes(bt709ToBT2020)) })).Return(uint32(61), nil).Once()
919+ k.EXPECT().createBlob(mock.MatchedBy(func(b []byte) bool { return slices.Equal(b, lut3dIdentityBytes(uint32(tc.lut))) })).Return(uint32(62), nil).Once()
920+ o.setupColor()
921+ if o.primary.pipeline == nil || (o.overlay.pipeline != nil) != tc.overlayKept || o.lut3dBlob != 62 {
922+ t.Fatalf("primary %v overlay %v lut %d", o.primary.pipeline != nil, o.overlay.pipeline != nil, o.lut3dBlob)
923+ }
924+ })
911 }925 }
912 })926 })
913 t.Run("3D LUT blob failure turns the pipelines off", func(t *testing.T) {927 t.Run("3D LUT blob failure turns the pipelines off", func(t *testing.T) {

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