detached 03ba80b

refactor(drm): share the image format and stop re-exporting refused linear images

Add Output.imageFormat for setupImages and exportImages. When KMS refuses
images that were already linear, return that error instead of exporting
the identical linear images again for the later kinds. Document that a
plane without IN_FORMATS gets linear images first, and pin both with tests.
commit
03ba80bcfc0be13ec02e666042d1e88c463be801
author
brice · · 2026-10-05 03:49 UTC
committer
brice · · 2026-10-05 03:49 UTC
parent
8982159
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
4 files · +110 -27
M internal/adapters/drm/commit_test.go +76 -3
@@ -112,6 +112,9 @@ func TestCursorRefusedDoesNotBlameScanoutBuffer(t *testing.T) {
112112 }
113113 }
114114
115+// tiledMod stands for a non-linear driver modifier in the image tests.
116+const tiledMod = 0x200000000401b03
117+
115118 // showImages validates the images with a TEST_ONLY modeset, retries with
116119 // single-plane driver images, then falls back to linear images when KMS
117120 // refuses both.
@@ -126,6 +129,7 @@ func TestShowImagesTestsBeforeModeset(t *testing.T) {
126129 w.Close()
127130 b.Planes = append(b.Planes, ports.DMABufPlane{File: f})
128131 }
132+ b.Modifier = tiledMod
129133 return b
130134 }
131135 linear := func() ports.DMABuf {
@@ -133,9 +137,14 @@ func TestShowImagesTestsBeforeModeset(t *testing.T) {
133137 w.Close()
134138 return ports.DMABuf{Planes: []ports.DMABufPlane{{File: f}}}
135139 }
140+ tiled1 := func() ports.DMABuf {
141+ b := linear()
142+ b.Modifier = tiledMod
143+ return b
144+ }
136145 r.EXPECT().SetHDR(float64(0)).Return().Maybe()
137146 r.EXPECT().ExportTargets(2, []uint64(nil), false).Return([]ports.DMABuf{buf(), buf()}, nil).Once()
138- r.EXPECT().ExportTargets(2, []uint64(nil), true).Return([]ports.DMABuf{linear(), linear()}, nil).Once()
147+ r.EXPECT().ExportTargets(2, []uint64(nil), true).Return([]ports.DMABuf{tiled1(), tiled1()}, nil).Once()
139148 r.EXPECT().ExportTargets(2, []uint64{0}, false).Return([]ports.DMABuf{linear(), linear()}, nil).Once()
140149 r.EXPECT().UseTarget(mock.Anything).Return()
141150 r.EXPECT().Render(mock.Anything, mock.Anything).Return(nil, nil)
@@ -168,7 +177,7 @@ func TestShowImagesRetriesSinglePlaneBeforeLinear(t *testing.T) {
168177 o.cursor = nil
169178 var files []*os.File
170179 buf := func(planes int) ports.DMABuf {
171- b := ports.DMABuf{}
180+ b := ports.DMABuf{Modifier: tiledMod}
172181 for range planes {
173182 f, w, _ := os.Pipe()
174183 w.Close()
@@ -269,12 +278,76 @@ func TestShowImagesNoCommonModifierFallsBackWithThePlanesList(t *testing.T) {
269278 }
270279 }
271280
281+// A plane without IN_FORMATS lists every format with modifier 0 only
282+// (readPlanes): the images are linear straight away, with no driver or
283+// single-plane attempt KMS would refuse.
284+func TestShowImagesLinearOnlyPlaneExportsLinearFirst(t *testing.T) {
285+ o, k, commits := testOutput(t)
286+ o.cursor = nil
287+ o.primary.formats = []ports.DMABufFormat{{Format: fourccXRGB, Modifier: 0}}
288+ r := portsmocks.NewMockRenderer(t)
289+ r.EXPECT().SetHDR(float64(0)).Return().Maybe()
290+ // No ExportTargets(..., true) and no second call: the mock fails on them.
291+ r.EXPECT().ExportTargets(2, []uint64{0}, false).RunAndReturn(func(int, []uint64, bool) ([]ports.DMABuf, error) {
292+ return []ports.DMABuf{linearBuf(), linearBuf()}, nil
293+ }).Once()
294+ r.EXPECT().UseTarget(mock.Anything).Return()
295+ r.EXPECT().Render(mock.Anything, mock.Anything).Return(nil, nil)
296+ k.EXPECT().addFB(mock.Anything, uint32(fourccXRGB)).Return(70, nil)
297+ k.EXPECT().createBlob(mock.Anything).Return(99, nil)
298+ k.EXPECT().destroyBlob(mock.Anything).Return(nil)
299+ if err := o.showImages(r, imagesDriver, nil); err != nil {
300+ t.Fatal(err)
301+ }
302+ if o.kind != imagesDriver || o.imageMod != 0 || len(*commits) != 2 {
303+ t.Fatalf("kind %d, modifier %#x, %d commits", o.kind, o.imageMod, len(*commits))
304+ }
305+}
306+
307+// KMS refusing images that are already linear ends the attempt with that
308+// error: the single-plane and linear kinds would export the same images.
309+func TestShowImagesRefusedLinearImagesAreNotExportedAgain(t *testing.T) {
310+ o, k, commits := testOutput(t, unix.EINVAL)
311+ o.cursor = nil
312+ o.primary.formats = []ports.DMABufFormat{{Format: fourccXRGB, Modifier: 0}}
313+ r := portsmocks.NewMockRenderer(t)
314+ r.EXPECT().SetHDR(float64(0)).Return().Maybe()
315+ r.EXPECT().ExportTargets(2, []uint64{0}, false).RunAndReturn(func(int, []uint64, bool) ([]ports.DMABuf, error) {
316+ return []ports.DMABuf{linearBuf(), linearBuf()}, nil
317+ }).Once()
318+ r.EXPECT().UseTarget(mock.Anything).Return()
319+ r.EXPECT().Render(mock.Anything, mock.Anything).Return(nil, nil)
320+ k.EXPECT().addFB(mock.Anything, uint32(fourccXRGB)).Return(70, nil)
321+ k.EXPECT().rmFB(mock.Anything).Return(nil)
322+ k.EXPECT().createBlob(mock.Anything).Return(99, nil)
323+ k.EXPECT().destroyBlob(mock.Anything).Return(nil)
324+ err := o.showImages(r, imagesDriver, nil)
325+ if !errors.Is(err, unix.EINVAL) {
326+ t.Fatalf("error %v, want the KMS refusal", err)
327+ }
328+ if len(*commits) != 1 || o.fbs != [2]uint32{} {
329+ t.Fatalf("%d commits, fbs %v", len(*commits), o.fbs)
330+ }
331+}
332+
333+// linearBuf is a one-plane linear dmabuf over a closed pipe.
334+func linearBuf() ports.DMABuf {
335+ f, w, _ := os.Pipe()
336+ w.Close()
337+ return ports.DMABuf{Planes: []ports.DMABufPlane{{File: f}}}
338+}
339+
272340 // Refused driver images that already had one plane are not retried as
273341 // single-plane images: the next export is linear.
274342 func TestShowImagesSkipsSinglePlaneWhenDriverImagesHadOnePlane(t *testing.T) {
275343 o, k, commits := testOutput(t, unix.EINVAL)
276344 o.cursor = nil
277345 buf := func() ports.DMABuf {
346+ f, w, _ := os.Pipe()
347+ w.Close()
348+ return ports.DMABuf{Modifier: tiledMod, Planes: []ports.DMABufPlane{{File: f}}}
349+ }
350+ linear := func() ports.DMABuf {
278351 f, w, _ := os.Pipe()
279352 w.Close()
280353 return ports.DMABuf{Planes: []ports.DMABufPlane{{File: f}}}
@@ -282,7 +355,7 @@ func TestShowImagesSkipsSinglePlaneWhenDriverImagesHadOnePlane(t *testing.T) {
282355 r := portsmocks.NewMockRenderer(t)
283356 r.EXPECT().SetHDR(float64(0)).Return().Maybe()
284357 r.EXPECT().ExportTargets(2, []uint64(nil), false).Return([]ports.DMABuf{buf(), buf()}, nil).Once()
285- r.EXPECT().ExportTargets(2, []uint64{0}, false).Return([]ports.DMABuf{buf(), buf()}, nil).Once()
358+ r.EXPECT().ExportTargets(2, []uint64{0}, false).Return([]ports.DMABuf{linear(), linear()}, nil).Once()
286359 r.EXPECT().UseTarget(mock.Anything).Return()
287360 r.EXPECT().Render(mock.Anything, mock.Anything).Return(nil, nil)
288361 k.EXPECT().addFB(mock.Anything, uint32(fourccXRGB)).Return(70, nil)
@@ -112,6 +112,9 @@ func TestCursorRefusedDoesNotBlameScanoutBuffer(t *testing.T) {
112 }112 }
113 }113 }
114 114
115+// tiledMod stands for a non-linear driver modifier in the image tests.
116+const tiledMod = 0x200000000401b03
117+
115 // showImages validates the images with a TEST_ONLY modeset, retries with118 // showImages validates the images with a TEST_ONLY modeset, retries with
116 // single-plane driver images, then falls back to linear images when KMS119 // single-plane driver images, then falls back to linear images when KMS
117 // refuses both.120 // refuses both.
@@ -126,6 +129,7 @@ func TestShowImagesTestsBeforeModeset(t *testing.T) {
126 w.Close()129 w.Close()
127 b.Planes = append(b.Planes, ports.DMABufPlane{File: f})130 b.Planes = append(b.Planes, ports.DMABufPlane{File: f})
128 }131 }
132+ b.Modifier = tiledMod
129 return b133 return b
130 }134 }
131 linear := func() ports.DMABuf {135 linear := func() ports.DMABuf {
@@ -133,9 +137,14 @@ func TestShowImagesTestsBeforeModeset(t *testing.T) {
133 w.Close()137 w.Close()
134 return ports.DMABuf{Planes: []ports.DMABufPlane{{File: f}}}138 return ports.DMABuf{Planes: []ports.DMABufPlane{{File: f}}}
135 }139 }
140+ tiled1 := func() ports.DMABuf {
141+ b := linear()
142+ b.Modifier = tiledMod
143+ return b
144+ }
136 r.EXPECT().SetHDR(float64(0)).Return().Maybe()145 r.EXPECT().SetHDR(float64(0)).Return().Maybe()
137 r.EXPECT().ExportTargets(2, []uint64(nil), false).Return([]ports.DMABuf{buf(), buf()}, nil).Once()146 r.EXPECT().ExportTargets(2, []uint64(nil), false).Return([]ports.DMABuf{buf(), buf()}, nil).Once()
138- r.EXPECT().ExportTargets(2, []uint64(nil), true).Return([]ports.DMABuf{linear(), linear()}, nil).Once()147+ r.EXPECT().ExportTargets(2, []uint64(nil), true).Return([]ports.DMABuf{tiled1(), tiled1()}, nil).Once()
139 r.EXPECT().ExportTargets(2, []uint64{0}, false).Return([]ports.DMABuf{linear(), linear()}, nil).Once()148 r.EXPECT().ExportTargets(2, []uint64{0}, false).Return([]ports.DMABuf{linear(), linear()}, nil).Once()
140 r.EXPECT().UseTarget(mock.Anything).Return()149 r.EXPECT().UseTarget(mock.Anything).Return()
141 r.EXPECT().Render(mock.Anything, mock.Anything).Return(nil, nil)150 r.EXPECT().Render(mock.Anything, mock.Anything).Return(nil, nil)
@@ -168,7 +177,7 @@ func TestShowImagesRetriesSinglePlaneBeforeLinear(t *testing.T) {
168 o.cursor = nil177 o.cursor = nil
169 var files []*os.File178 var files []*os.File
170 buf := func(planes int) ports.DMABuf {179 buf := func(planes int) ports.DMABuf {
171- b := ports.DMABuf{}180+ b := ports.DMABuf{Modifier: tiledMod}
172 for range planes {181 for range planes {
173 f, w, _ := os.Pipe()182 f, w, _ := os.Pipe()
174 w.Close()183 w.Close()
@@ -269,12 +278,76 @@ func TestShowImagesNoCommonModifierFallsBackWithThePlanesList(t *testing.T) {
269 }278 }
270 }279 }
271 280
281+// A plane without IN_FORMATS lists every format with modifier 0 only
282+// (readPlanes): the images are linear straight away, with no driver or
283+// single-plane attempt KMS would refuse.
284+func TestShowImagesLinearOnlyPlaneExportsLinearFirst(t *testing.T) {
285+ o, k, commits := testOutput(t)
286+ o.cursor = nil
287+ o.primary.formats = []ports.DMABufFormat{{Format: fourccXRGB, Modifier: 0}}
288+ r := portsmocks.NewMockRenderer(t)
289+ r.EXPECT().SetHDR(float64(0)).Return().Maybe()
290+ // No ExportTargets(..., true) and no second call: the mock fails on them.
291+ r.EXPECT().ExportTargets(2, []uint64{0}, false).RunAndReturn(func(int, []uint64, bool) ([]ports.DMABuf, error) {
292+ return []ports.DMABuf{linearBuf(), linearBuf()}, nil
293+ }).Once()
294+ r.EXPECT().UseTarget(mock.Anything).Return()
295+ r.EXPECT().Render(mock.Anything, mock.Anything).Return(nil, nil)
296+ k.EXPECT().addFB(mock.Anything, uint32(fourccXRGB)).Return(70, nil)
297+ k.EXPECT().createBlob(mock.Anything).Return(99, nil)
298+ k.EXPECT().destroyBlob(mock.Anything).Return(nil)
299+ if err := o.showImages(r, imagesDriver, nil); err != nil {
300+ t.Fatal(err)
301+ }
302+ if o.kind != imagesDriver || o.imageMod != 0 || len(*commits) != 2 {
303+ t.Fatalf("kind %d, modifier %#x, %d commits", o.kind, o.imageMod, len(*commits))
304+ }
305+}
306+
307+// KMS refusing images that are already linear ends the attempt with that
308+// error: the single-plane and linear kinds would export the same images.
309+func TestShowImagesRefusedLinearImagesAreNotExportedAgain(t *testing.T) {
310+ o, k, commits := testOutput(t, unix.EINVAL)
311+ o.cursor = nil
312+ o.primary.formats = []ports.DMABufFormat{{Format: fourccXRGB, Modifier: 0}}
313+ r := portsmocks.NewMockRenderer(t)
314+ r.EXPECT().SetHDR(float64(0)).Return().Maybe()
315+ r.EXPECT().ExportTargets(2, []uint64{0}, false).RunAndReturn(func(int, []uint64, bool) ([]ports.DMABuf, error) {
316+ return []ports.DMABuf{linearBuf(), linearBuf()}, nil
317+ }).Once()
318+ r.EXPECT().UseTarget(mock.Anything).Return()
319+ r.EXPECT().Render(mock.Anything, mock.Anything).Return(nil, nil)
320+ k.EXPECT().addFB(mock.Anything, uint32(fourccXRGB)).Return(70, nil)
321+ k.EXPECT().rmFB(mock.Anything).Return(nil)
322+ k.EXPECT().createBlob(mock.Anything).Return(99, nil)
323+ k.EXPECT().destroyBlob(mock.Anything).Return(nil)
324+ err := o.showImages(r, imagesDriver, nil)
325+ if !errors.Is(err, unix.EINVAL) {
326+ t.Fatalf("error %v, want the KMS refusal", err)
327+ }
328+ if len(*commits) != 1 || o.fbs != [2]uint32{} {
329+ t.Fatalf("%d commits, fbs %v", len(*commits), o.fbs)
330+ }
331+}
332+
333+// linearBuf is a one-plane linear dmabuf over a closed pipe.
334+func linearBuf() ports.DMABuf {
335+ f, w, _ := os.Pipe()
336+ w.Close()
337+ return ports.DMABuf{Planes: []ports.DMABufPlane{{File: f}}}
338+}
339+
272 // Refused driver images that already had one plane are not retried as340 // Refused driver images that already had one plane are not retried as
273 // single-plane images: the next export is linear.341 // single-plane images: the next export is linear.
274 func TestShowImagesSkipsSinglePlaneWhenDriverImagesHadOnePlane(t *testing.T) {342 func TestShowImagesSkipsSinglePlaneWhenDriverImagesHadOnePlane(t *testing.T) {
275 o, k, commits := testOutput(t, unix.EINVAL)343 o, k, commits := testOutput(t, unix.EINVAL)
276 o.cursor = nil344 o.cursor = nil
277 buf := func() ports.DMABuf {345 buf := func() ports.DMABuf {
346+ f, w, _ := os.Pipe()
347+ w.Close()
348+ return ports.DMABuf{Modifier: tiledMod, Planes: []ports.DMABufPlane{{File: f}}}
349+ }
350+ linear := func() ports.DMABuf {
278 f, w, _ := os.Pipe()351 f, w, _ := os.Pipe()
279 w.Close()352 w.Close()
280 return ports.DMABuf{Planes: []ports.DMABufPlane{{File: f}}}353 return ports.DMABuf{Planes: []ports.DMABufPlane{{File: f}}}
@@ -282,7 +355,7 @@ func TestShowImagesSkipsSinglePlaneWhenDriverImagesHadOnePlane(t *testing.T) {
282 r := portsmocks.NewMockRenderer(t)355 r := portsmocks.NewMockRenderer(t)
283 r.EXPECT().SetHDR(float64(0)).Return().Maybe()356 r.EXPECT().SetHDR(float64(0)).Return().Maybe()
284 r.EXPECT().ExportTargets(2, []uint64(nil), false).Return([]ports.DMABuf{buf(), buf()}, nil).Once()357 r.EXPECT().ExportTargets(2, []uint64(nil), false).Return([]ports.DMABuf{buf(), buf()}, nil).Once()
285- r.EXPECT().ExportTargets(2, []uint64{0}, false).Return([]ports.DMABuf{buf(), buf()}, nil).Once()358+ r.EXPECT().ExportTargets(2, []uint64{0}, false).Return([]ports.DMABuf{linear(), linear()}, nil).Once()
286 r.EXPECT().UseTarget(mock.Anything).Return()359 r.EXPECT().UseTarget(mock.Anything).Return()
287 r.EXPECT().Render(mock.Anything, mock.Anything).Return(nil, nil)360 r.EXPECT().Render(mock.Anything, mock.Anything).Return(nil, nil)
288 k.EXPECT().addFB(mock.Anything, uint32(fourccXRGB)).Return(70, nil)361 k.EXPECT().addFB(mock.Anything, uint32(fourccXRGB)).Return(70, nil)
M internal/adapters/drm/hdr_test.go +9 -9
@@ -73,7 +73,7 @@ func TestHDRModesetPropertiesAndPlaneRefusal(t *testing.T) {
7373 }
7474
7575 func TestHDRTestCommitFallbackReexportsSDR(t *testing.T) {
76- o, k, commits := testOutput(t, unix.EINVAL, unix.EINVAL)
76+ o, k, commits := testOutput(t, unix.EINVAL)
7777 o.cursor = nil
7878 o.hdr.cap = hdrCapability{Capable: true}
7979 o.hdr.settings = HDRSettings{Enabled: true, SDRBrightness: 203}
@@ -88,7 +88,7 @@ func TestHDRTestCommitFallbackReexportsSDR(t *testing.T) {
8888 var calls []string
8989 r.EXPECT().SetHDR(float64(203)).Run(func(float64) { calls = append(calls, "hdr") }).Return().Once()
9090 r.EXPECT().SetHDR(float64(0)).Run(func(float64) { calls = append(calls, "sdr") }).Return().Once()
91- r.EXPECT().ExportTargets(2, []uint64{0}, false).Return([]ports.DMABuf{buf(), buf()}, nil).Twice()
91+ r.EXPECT().ExportTargets(2, []uint64{0}, false).Return([]ports.DMABuf{buf(), buf()}, nil).Once()
9292 r.EXPECT().ExportTargets(0, []uint64(nil), false).Run(func(int, []uint64, bool) { calls = append(calls, "drop") }).Return(nil, nil).Once()
9393 r.EXPECT().ExportTargets(2, []uint64(nil), false).Return([]ports.DMABuf{buf(), buf()}, nil).Once()
9494 r.EXPECT().UseTarget(mock.Anything).Return()
@@ -108,16 +108,16 @@ func TestHDRTestCommitFallbackReexportsSDR(t *testing.T) {
108108 if o.hdr.on || !o.hdr.failed {
109109 t.Fatalf("HDR on %t failed %t", o.hdr.on, o.hdr.failed)
110110 }
111- if len(*commits) != 4 {
111+ // The HDR images were already linear: refusing them ends HDR at once,
112+ // there is no second identical linear export.
113+ if len(*commits) != 3 {
112114 t.Fatalf("commits: %d", len(*commits))
113115 }
114- for i := range 2 {
115- if (*commits)[i].flags != atomicTestOnly|atomicAllowModes {
116- t.Fatalf("HDR test %d flags: %#x", i, (*commits)[i].flags)
117- }
116+ if (*commits)[0].flags != atomicTestOnly|atomicAllowModes {
117+ t.Fatalf("HDR test flags: %#x", (*commits)[0].flags)
118118 }
119- if (*commits)[2].flags != atomicTestOnly|atomicAllowModes || (*commits)[3].flags != atomicAllowModes {
120- t.Fatalf("SDR commits: %#x %#x", (*commits)[2].flags, (*commits)[3].flags)
119+ if (*commits)[1].flags != atomicTestOnly|atomicAllowModes || (*commits)[2].flags != atomicAllowModes {
120+ t.Fatalf("SDR commits: %#x %#x", (*commits)[1].flags, (*commits)[2].flags)
121121 }
122122 if v, _ := (*commits)[0].req.value(o.conn.id, 100); v != 321 {
123123 t.Fatalf("HDR test metadata: %d", v)
@@ -73,7 +73,7 @@ func TestHDRModesetPropertiesAndPlaneRefusal(t *testing.T) {
73 }73 }
74 74
75 func TestHDRTestCommitFallbackReexportsSDR(t *testing.T) {75 func TestHDRTestCommitFallbackReexportsSDR(t *testing.T) {
76- o, k, commits := testOutput(t, unix.EINVAL, unix.EINVAL)76+ o, k, commits := testOutput(t, unix.EINVAL)
77 o.cursor = nil77 o.cursor = nil
78 o.hdr.cap = hdrCapability{Capable: true}78 o.hdr.cap = hdrCapability{Capable: true}
79 o.hdr.settings = HDRSettings{Enabled: true, SDRBrightness: 203}79 o.hdr.settings = HDRSettings{Enabled: true, SDRBrightness: 203}
@@ -88,7 +88,7 @@ func TestHDRTestCommitFallbackReexportsSDR(t *testing.T) {
88 var calls []string88 var calls []string
89 r.EXPECT().SetHDR(float64(203)).Run(func(float64) { calls = append(calls, "hdr") }).Return().Once()89 r.EXPECT().SetHDR(float64(203)).Run(func(float64) { calls = append(calls, "hdr") }).Return().Once()
90 r.EXPECT().SetHDR(float64(0)).Run(func(float64) { calls = append(calls, "sdr") }).Return().Once()90 r.EXPECT().SetHDR(float64(0)).Run(func(float64) { calls = append(calls, "sdr") }).Return().Once()
91- r.EXPECT().ExportTargets(2, []uint64{0}, false).Return([]ports.DMABuf{buf(), buf()}, nil).Twice()91+ r.EXPECT().ExportTargets(2, []uint64{0}, false).Return([]ports.DMABuf{buf(), buf()}, nil).Once()
92 r.EXPECT().ExportTargets(0, []uint64(nil), false).Run(func(int, []uint64, bool) { calls = append(calls, "drop") }).Return(nil, nil).Once()92 r.EXPECT().ExportTargets(0, []uint64(nil), false).Run(func(int, []uint64, bool) { calls = append(calls, "drop") }).Return(nil, nil).Once()
93 r.EXPECT().ExportTargets(2, []uint64(nil), false).Return([]ports.DMABuf{buf(), buf()}, nil).Once()93 r.EXPECT().ExportTargets(2, []uint64(nil), false).Return([]ports.DMABuf{buf(), buf()}, nil).Once()
94 r.EXPECT().UseTarget(mock.Anything).Return()94 r.EXPECT().UseTarget(mock.Anything).Return()
@@ -108,16 +108,16 @@ func TestHDRTestCommitFallbackReexportsSDR(t *testing.T) {
108 if o.hdr.on || !o.hdr.failed {108 if o.hdr.on || !o.hdr.failed {
109 t.Fatalf("HDR on %t failed %t", o.hdr.on, o.hdr.failed)109 t.Fatalf("HDR on %t failed %t", o.hdr.on, o.hdr.failed)
110 }110 }
111- if len(*commits) != 4 {111+ // The HDR images were already linear: refusing them ends HDR at once,
112+ // there is no second identical linear export.
113+ if len(*commits) != 3 {
112 t.Fatalf("commits: %d", len(*commits))114 t.Fatalf("commits: %d", len(*commits))
113 }115 }
114- for i := range 2 {116+ if (*commits)[0].flags != atomicTestOnly|atomicAllowModes {
115- if (*commits)[i].flags != atomicTestOnly|atomicAllowModes {117+ t.Fatalf("HDR test flags: %#x", (*commits)[0].flags)
116- t.Fatalf("HDR test %d flags: %#x", i, (*commits)[i].flags)
117- }
118 }118 }
119- if (*commits)[2].flags != atomicTestOnly|atomicAllowModes || (*commits)[3].flags != atomicAllowModes {119+ if (*commits)[1].flags != atomicTestOnly|atomicAllowModes || (*commits)[2].flags != atomicAllowModes {
120- t.Fatalf("SDR commits: %#x %#x", (*commits)[2].flags, (*commits)[3].flags)120+ t.Fatalf("SDR commits: %#x %#x", (*commits)[1].flags, (*commits)[2].flags)
121 }121 }
122 if v, _ := (*commits)[0].req.value(o.conn.id, 100); v != 321 {122 if v, _ := (*commits)[0].req.value(o.conn.id, 100); v != 321 {
123 t.Fatalf("HDR test metadata: %d", v)123 t.Fatalf("HDR test metadata: %d", v)
M internal/adapters/drm/images.go +23 -15
@@ -101,6 +101,12 @@ func (o *Output) showImageKind(r ports.Renderer, kind imageKind, cause error) er
101101 o.freeImages()
102102 return err
103103 }
104+ if o.imageMod == 0 {
105+ // The refused images were already linear: the later kinds
106+ // would export the same ones again.
107+ o.freeImages()
108+ return err
109+ }
104110 o.log.Warn().Err(err).Str("connector", o.conn.name).Int("kind", int(got)).Msg("modeset refused the output images")
105111 o.freeImages()
106112 kind, cause = got+1, err
@@ -125,14 +131,12 @@ func (o *Output) setupImages(r ports.Renderer, kind imageKind, cause error) (ima
125131 // for the image format: DRM core refuses (ADDFB2: EINVAL) a
126132 // framebuffer whose format and modifier no plane advertises, and
127133 // the renderer's own preference may be such a one (RADV's
128- // pipe-aligned DCC, which amdgpu does not list for scanout). SDR
129- // with no modifier listed for the format leaves the choice to the
130- // renderer.
131- format := uint32(fourccXRGB)
132- if o.hdr.on {
133- format = fourccXR30
134- }
135- mods := o.primaryModifiers(format)
134+ // pipe-aligned DCC, which amdgpu does not list for scanout). A
135+ // plane without IN_FORMATS lists every format with modifier 0
136+ // (readPlanes), so it gets linear images straight away rather than
137+ // two refused driver attempts. Only a plane listing no modifier
138+ // at all for the format (SDR) leaves the choice to the renderer.
139+ mods := o.primaryModifiers(o.imageFormat())
136140 if o.hdr.on {
137141 if len(mods) == 0 {
138142 return kind, fmt.Errorf("primary plane has no XRGB2101010 modifiers")
@@ -159,6 +163,14 @@ func (o *Output) setupImages(r ports.Renderer, kind imageKind, cause error) (ima
159163 return imagesLinear, fmt.Errorf("%s: GPU cannot export scanout images (ADR 014): %w", o.conn.name, err)
160164 }
161165
166+// imageFormat is the framebuffer format of the output images.
167+func (o *Output) imageFormat() uint32 {
168+ if o.hdr.on {
169+ return fourccXR30
170+ }
171+ return fourccXRGB
172+}
173+
162174 // primaryModifiers lists the modifiers the primary plane accepts for
163175 // format, in IN_FORMATS order.
164176 func (o *Output) primaryModifiers(format uint32) []uint64 {
@@ -177,17 +189,13 @@ func (o *Output) exportImages(r ports.Renderer, mods []uint64, singlePlane bool)
177189 if err != nil {
178190 return err
179191 }
180- o.planes = 0
192+ o.planes, o.imageMod = 0, 0
181193 for i := range bufs {
182194 if len(bufs[i].Planes) == 0 || len(bufs[i].Planes) > maxFBPlanes {
183195 err = errors.Join(err, fmt.Errorf("output image has %d planes", len(bufs[i].Planes)))
184196 }
185197 if err == nil {
186- format := uint32(fourccXRGB)
187- if o.hdr.on {
188- format = fourccXR30
189- }
190- o.fbs[i], err = o.k.addFB(&bufs[i], format)
198+ o.fbs[i], err = o.k.addFB(&bufs[i], o.imageFormat())
191199 }
192200 // Every plane owns its file; the framebuffer keeps its own
193201 // reference to the buffer.
@@ -218,7 +226,7 @@ func (o *Output) exportImages(r ports.Renderer, mods []uint64, singlePlane bool)
218226 _, _ = r.ExportTargets(0, nil, false)
219227 return err
220228 }
221- o.planes = len(bufs[0].Planes)
229+ o.planes, o.imageMod = len(bufs[0].Planes), bufs[0].Modifier
222230 o.log.Info().Str("connector", o.conn.name).Uint64("modifier", bufs[0].Modifier).Int("planes", len(bufs[0].Planes)).Msg("zero-copy output")
223231 return nil
224232 }
@@ -101,6 +101,12 @@ func (o *Output) showImageKind(r ports.Renderer, kind imageKind, cause error) er
101 o.freeImages()101 o.freeImages()
102 return err102 return err
103 }103 }
104+ if o.imageMod == 0 {
105+ // The refused images were already linear: the later kinds
106+ // would export the same ones again.
107+ o.freeImages()
108+ return err
109+ }
104 o.log.Warn().Err(err).Str("connector", o.conn.name).Int("kind", int(got)).Msg("modeset refused the output images")110 o.log.Warn().Err(err).Str("connector", o.conn.name).Int("kind", int(got)).Msg("modeset refused the output images")
105 o.freeImages()111 o.freeImages()
106 kind, cause = got+1, err112 kind, cause = got+1, err
@@ -125,14 +131,12 @@ func (o *Output) setupImages(r ports.Renderer, kind imageKind, cause error) (ima
125 // for the image format: DRM core refuses (ADDFB2: EINVAL) a131 // for the image format: DRM core refuses (ADDFB2: EINVAL) a
126 // framebuffer whose format and modifier no plane advertises, and132 // framebuffer whose format and modifier no plane advertises, and
127 // the renderer's own preference may be such a one (RADV's133 // the renderer's own preference may be such a one (RADV's
128- // pipe-aligned DCC, which amdgpu does not list for scanout). SDR134+ // pipe-aligned DCC, which amdgpu does not list for scanout). A
129- // with no modifier listed for the format leaves the choice to the135+ // plane without IN_FORMATS lists every format with modifier 0
130- // renderer.136+ // (readPlanes), so it gets linear images straight away rather than
131- format := uint32(fourccXRGB)137+ // two refused driver attempts. Only a plane listing no modifier
132- if o.hdr.on {138+ // at all for the format (SDR) leaves the choice to the renderer.
133- format = fourccXR30139+ mods := o.primaryModifiers(o.imageFormat())
134- }
135- mods := o.primaryModifiers(format)
136 if o.hdr.on {140 if o.hdr.on {
137 if len(mods) == 0 {141 if len(mods) == 0 {
138 return kind, fmt.Errorf("primary plane has no XRGB2101010 modifiers")142 return kind, fmt.Errorf("primary plane has no XRGB2101010 modifiers")
@@ -159,6 +163,14 @@ func (o *Output) setupImages(r ports.Renderer, kind imageKind, cause error) (ima
159 return imagesLinear, fmt.Errorf("%s: GPU cannot export scanout images (ADR 014): %w", o.conn.name, err)163 return imagesLinear, fmt.Errorf("%s: GPU cannot export scanout images (ADR 014): %w", o.conn.name, err)
160 }164 }
161 165
166+// imageFormat is the framebuffer format of the output images.
167+func (o *Output) imageFormat() uint32 {
168+ if o.hdr.on {
169+ return fourccXR30
170+ }
171+ return fourccXRGB
172+}
173+
162 // primaryModifiers lists the modifiers the primary plane accepts for174 // primaryModifiers lists the modifiers the primary plane accepts for
163 // format, in IN_FORMATS order.175 // format, in IN_FORMATS order.
164 func (o *Output) primaryModifiers(format uint32) []uint64 {176 func (o *Output) primaryModifiers(format uint32) []uint64 {
@@ -177,17 +189,13 @@ func (o *Output) exportImages(r ports.Renderer, mods []uint64, singlePlane bool)
177 if err != nil {189 if err != nil {
178 return err190 return err
179 }191 }
180- o.planes = 0192+ o.planes, o.imageMod = 0, 0
181 for i := range bufs {193 for i := range bufs {
182 if len(bufs[i].Planes) == 0 || len(bufs[i].Planes) > maxFBPlanes {194 if len(bufs[i].Planes) == 0 || len(bufs[i].Planes) > maxFBPlanes {
183 err = errors.Join(err, fmt.Errorf("output image has %d planes", len(bufs[i].Planes)))195 err = errors.Join(err, fmt.Errorf("output image has %d planes", len(bufs[i].Planes)))
184 }196 }
185 if err == nil {197 if err == nil {
186- format := uint32(fourccXRGB)198+ o.fbs[i], err = o.k.addFB(&bufs[i], o.imageFormat())
187- if o.hdr.on {
188- format = fourccXR30
189- }
190- o.fbs[i], err = o.k.addFB(&bufs[i], format)
191 }199 }
192 // Every plane owns its file; the framebuffer keeps its own200 // Every plane owns its file; the framebuffer keeps its own
193 // reference to the buffer.201 // reference to the buffer.
@@ -218,7 +226,7 @@ func (o *Output) exportImages(r ports.Renderer, mods []uint64, singlePlane bool)
218 _, _ = r.ExportTargets(0, nil, false)226 _, _ = r.ExportTargets(0, nil, false)
219 return err227 return err
220 }228 }
221- o.planes = len(bufs[0].Planes)229+ o.planes, o.imageMod = len(bufs[0].Planes), bufs[0].Modifier
222 o.log.Info().Str("connector", o.conn.name).Uint64("modifier", bufs[0].Modifier).Int("planes", len(bufs[0].Planes)).Msg("zero-copy output")230 o.log.Info().Str("connector", o.conn.name).Uint64("modifier", bufs[0].Modifier).Int("planes", len(bufs[0].Planes)).Msg("zero-copy output")
223 return nil231 return nil
224 }232 }
M internal/adapters/drm/output.go +2 -0
@@ -183,6 +183,8 @@ type Output struct {
183183 kind imageKind
184184 // planes is the memory plane count of the last exported images.
185185 planes int
186+ // imageMod is the modifier of the exported images (0: linear).
187+ imageMod uint64
186188 // formats receives the direct scanout formats after each modeset;
187189 // sampled and device are what they are built from (Want).
188190 formats chan<- ports.OutputFormats
@@ -183,6 +183,8 @@ type Output struct {
183 kind imageKind183 kind imageKind
184 // planes is the memory plane count of the last exported images.184 // planes is the memory plane count of the last exported images.
185 planes int185 planes int
186+ // imageMod is the modifier of the exported images (0: linear).
187+ imageMod uint64
186 // formats receives the direct scanout formats after each modeset;188 // formats receives the direct scanout formats after each modeset;
187 // sampled and device are what they are built from (Want).189 // sampled and device are what they are built from (Want).
188 formats chan<- ports.OutputFormats190 formats chan<- ports.OutputFormats

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