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 Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· 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) { | |||
| 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 | 118 | // showImages validates the images with a TEST_ONLY modeset, retries with | |
| 116 | 119 | // single-plane driver images, then falls back to linear images when KMS | |
| 117 | 120 | // refuses both. | |
| @@ -126,6 +129,7 @@ func TestShowImagesTestsBeforeModeset(t *testing.T) { | |||
| 126 | 129 | w.Close() | |
| 127 | 130 | b.Planes = append(b.Planes, ports.DMABufPlane{File: f}) | |
| 128 | 131 | } | |
| 132 | + | b.Modifier = tiledMod | |
| 129 | 133 | return b | |
| 130 | 134 | } | |
| 131 | 135 | linear := func() ports.DMABuf { | |
| @@ -133,9 +137,14 @@ func TestShowImagesTestsBeforeModeset(t *testing.T) { | |||
| 133 | 137 | w.Close() | |
| 134 | 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 | 145 | r.EXPECT().SetHDR(float64(0)).Return().Maybe() | |
| 137 | 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 | 148 | r.EXPECT().ExportTargets(2, []uint64{0}, false).Return([]ports.DMABuf{linear(), linear()}, nil).Once() | |
| 140 | 149 | r.EXPECT().UseTarget(mock.Anything).Return() | |
| 141 | 150 | r.EXPECT().Render(mock.Anything, mock.Anything).Return(nil, nil) | |
| @@ -168,7 +177,7 @@ func TestShowImagesRetriesSinglePlaneBeforeLinear(t *testing.T) { | |||
| 168 | 177 | o.cursor = nil | |
| 169 | 178 | var files []*os.File | |
| 170 | 179 | buf := func(planes int) ports.DMABuf { | |
| 171 | - | b := ports.DMABuf{} | |
| 180 | + | b := ports.DMABuf{Modifier: tiledMod} | |
| 172 | 181 | for range planes { | |
| 173 | 182 | f, w, _ := os.Pipe() | |
| 174 | 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 | 340 | // Refused driver images that already had one plane are not retried as | |
| 273 | 341 | // single-plane images: the next export is linear. | |
| 274 | 342 | func TestShowImagesSkipsSinglePlaneWhenDriverImagesHadOnePlane(t *testing.T) { | |
| 275 | 343 | o, k, commits := testOutput(t, unix.EINVAL) | |
| 276 | 344 | o.cursor = nil | |
| 277 | 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 | 351 | f, w, _ := os.Pipe() | |
| 279 | 352 | w.Close() | |
| 280 | 353 | return ports.DMABuf{Planes: []ports.DMABufPlane{{File: f}}} | |
| @@ -282,7 +355,7 @@ func TestShowImagesSkipsSinglePlaneWhenDriverImagesHadOnePlane(t *testing.T) { | |||
| 282 | 355 | r := portsmocks.NewMockRenderer(t) | |
| 283 | 356 | r.EXPECT().SetHDR(float64(0)).Return().Maybe() | |
| 284 | 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 | 359 | r.EXPECT().UseTarget(mock.Anything).Return() | |
| 287 | 360 | r.EXPECT().Render(mock.Anything, mock.Anything).Return(nil, nil) | |
| 288 | 361 | 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 with | 118 | // showImages validates the images with a TEST_ONLY modeset, retries with | ||
| 116 | // single-plane driver images, then falls back to linear images when KMS | 119 | // 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 b | 133 | 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 = nil | 177 | o.cursor = nil | ||
| 169 | var files []*os.File | 178 | 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 as | 340 | // 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 = nil | 344 | 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) { | |||
| 73 | 73 | } | |
| 74 | 74 | ||
| 75 | 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 | 77 | o.cursor = nil | |
| 78 | 78 | o.hdr.cap = hdrCapability{Capable: true} | |
| 79 | 79 | o.hdr.settings = HDRSettings{Enabled: true, SDRBrightness: 203} | |
| @@ -88,7 +88,7 @@ func TestHDRTestCommitFallbackReexportsSDR(t *testing.T) { | |||
| 88 | 88 | var calls []string | |
| 89 | 89 | r.EXPECT().SetHDR(float64(203)).Run(func(float64) { calls = append(calls, "hdr") }).Return().Once() | |
| 90 | 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 | 92 | r.EXPECT().ExportTargets(0, []uint64(nil), false).Run(func(int, []uint64, bool) { calls = append(calls, "drop") }).Return(nil, nil).Once() | |
| 93 | 93 | r.EXPECT().ExportTargets(2, []uint64(nil), false).Return([]ports.DMABuf{buf(), buf()}, nil).Once() | |
| 94 | 94 | r.EXPECT().UseTarget(mock.Anything).Return() | |
| @@ -108,16 +108,16 @@ func TestHDRTestCommitFallbackReexportsSDR(t *testing.T) { | |||
| 108 | 108 | if o.hdr.on || !o.hdr.failed { | |
| 109 | 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 | 114 | t.Fatalf("commits: %d", len(*commits)) | |
| 113 | 115 | } | |
| 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) | |
| 118 | 118 | } | |
| 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) | |
| 121 | 121 | } | |
| 122 | 122 | if v, _ := (*commits)[0].req.value(o.conn.id, 100); v != 321 { | |
| 123 | 123 | 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 = nil | 77 | 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 []string | 88 | 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 | |||
| 101 | 101 | o.freeImages() | |
| 102 | 102 | 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 | 110 | o.log.Warn().Err(err).Str("connector", o.conn.name).Int("kind", int(got)).Msg("modeset refused the output images") | |
| 105 | 111 | o.freeImages() | |
| 106 | 112 | kind, cause = got+1, err | |
| @@ -125,14 +131,12 @@ func (o *Output) setupImages(r ports.Renderer, kind imageKind, cause error) (ima | |||
| 125 | 131 | // for the image format: DRM core refuses (ADDFB2: EINVAL) a | |
| 126 | 132 | // framebuffer whose format and modifier no plane advertises, and | |
| 127 | 133 | // 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()) | |
| 136 | 140 | if o.hdr.on { | |
| 137 | 141 | if len(mods) == 0 { | |
| 138 | 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 | 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 | 174 | // primaryModifiers lists the modifiers the primary plane accepts for | |
| 163 | 175 | // format, in IN_FORMATS order. | |
| 164 | 176 | func (o *Output) primaryModifiers(format uint32) []uint64 { | |
| @@ -177,17 +189,13 @@ func (o *Output) exportImages(r ports.Renderer, mods []uint64, singlePlane bool) | |||
| 177 | 189 | if err != nil { | |
| 178 | 190 | return err | |
| 179 | 191 | } | |
| 180 | - | o.planes = 0 | |
| 192 | + | o.planes, o.imageMod = 0, 0 | |
| 181 | 193 | for i := range bufs { | |
| 182 | 194 | if len(bufs[i].Planes) == 0 || len(bufs[i].Planes) > maxFBPlanes { | |
| 183 | 195 | err = errors.Join(err, fmt.Errorf("output image has %d planes", len(bufs[i].Planes))) | |
| 184 | 196 | } | |
| 185 | 197 | 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()) | |
| 191 | 199 | } | |
| 192 | 200 | // Every plane owns its file; the framebuffer keeps its own | |
| 193 | 201 | // reference to the buffer. | |
| @@ -218,7 +226,7 @@ func (o *Output) exportImages(r ports.Renderer, mods []uint64, singlePlane bool) | |||
| 218 | 226 | _, _ = r.ExportTargets(0, nil, false) | |
| 219 | 227 | return err | |
| 220 | 228 | } | |
| 221 | - | o.planes = len(bufs[0].Planes) | |
| 229 | + | o.planes, o.imageMod = len(bufs[0].Planes), bufs[0].Modifier | |
| 222 | 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 | 231 | return nil | |
| 224 | 232 | } | |
| @@ -101,6 +101,12 @@ func (o *Output) showImageKind(r ports.Renderer, kind imageKind, cause error) er | |||||
| 101 | o.freeImages() | 101 | o.freeImages() | ||
| 102 | return err | 102 | 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, err | 112 | 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) a | 131 | // for the image format: DRM core refuses (ADDFB2: EINVAL) a | ||
| 126 | // framebuffer whose format and modifier no plane advertises, and | 132 | // framebuffer whose format and modifier no plane advertises, and | ||
| 127 | // the renderer's own preference may be such a one (RADV's | 133 | // the renderer's own preference may be such a one (RADV's | ||
| 128 | - | // pipe-aligned DCC, which amdgpu does not list for scanout). SDR | 134 | + | // pipe-aligned DCC, which amdgpu does not list for scanout). A |
| 129 | - | // with no modifier listed for the format leaves the choice to the | 135 | + | // 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 = fourccXR30 | 139 | + | 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 for | 174 | // 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 err | 190 | return err | ||
| 179 | } | 191 | } | ||
| 180 | - | o.planes = 0 | 192 | + | 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 own | 200 | // 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 err | 227 | 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 nil | 231 | return nil | ||
| 224 | } | 232 | } | ||
M internal/adapters/drm/output.go +2 -0
| @@ -183,6 +183,8 @@ type Output struct { | |||
| 183 | 183 | kind imageKind | |
| 184 | 184 | // planes is the memory plane count of the last exported images. | |
| 185 | 185 | planes int | |
| 186 | + | // imageMod is the modifier of the exported images (0: linear). | |
| 187 | + | imageMod uint64 | |
| 186 | 188 | // formats receives the direct scanout formats after each modeset; | |
| 187 | 189 | // sampled and device are what they are built from (Want). | |
| 188 | 190 | formats chan<- ports.OutputFormats | |
| @@ -183,6 +183,8 @@ type Output struct { | |||||
| 183 | kind imageKind | 183 | 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 int | 185 | 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.OutputFormats | 190 | formats chan<- ports.OutputFormats | ||