1package vulkan
2
3import (
4 "errors"
5 "image/color"
6 "math"
7 "os"
8 "slices"
9 "testing"
10
11 "github.com/bnema/neferwl/internal/ports"
12 vk "github.com/bnema/purego-vulkan/vulkan"
13)
14
15// Frames drawn into exported targets reach their dmabufs: another renderer
16// importing a target (as KMS scans it out) sees the frame, and readback
17// reads the target drawn last. The viewer imports one-plane buffers only,
18// so the targets are single-plane here; TestExportTargetsPlanes covers the
19// multi-plane (DCC) ones.
20func TestExportTargets(t *testing.T) {
21 r, err := New(64, 32)
22 if err != nil {
23 t.Skipf("Vulkan unavailable: %v", err)
24 }
25 defer r.Close()
26 bufs, err := r.ExportTargets(2, nil, true)
27 if err != nil {
28 t.Skipf("no exportable targets: %v", err)
29 }
30 defer func() {
31 for _, b := range bufs {
32 for _, p := range b.Planes {
33 p.File.Close()
34 }
35 }
36 }()
37 if len(bufs) != 2 || bufs[0].Width != 64 || bufs[0].Format != fourccXRGB || bufs[0].Planes[0].Stride < 64*4 {
38 t.Fatalf("targets %+v", bufs)
39 }
40 colors := []string{"#ff0000", "#0000ff"}
41 for i, c := range colors {
42 r.UseTarget(i)
43 if err := render(r, ports.Scene{Background: c}, nil); err != nil {
44 t.Fatal(err)
45 }
46 }
47 if got := readPixels(t, r).At(3, 3); got != (color.RGBA{0, 0, 255, 255}) {
48 t.Fatalf("readback of the last target %v", got)
49 }
50 // Import target 0 as a client buffer, like the display would.
51 viewer, err := New(64, 32)
52 if err != nil {
53 t.Fatal(err)
54 }
55 defer viewer.Close()
56 b := bufs[0]
57 b.ID = 1
58 importable := false
59 for _, f := range viewer.DMABuf().Formats {
60 importable = importable || f.Format == b.Format && f.Modifier == b.Modifier
61 }
62 if !importable {
63 t.Skipf("modifier %#x not importable", b.Modifier)
64 }
65 scene := ports.Scene{Background: "#000000", Windows: []ports.SceneWindow{{ID: 1, Rect: ports.Rect{W: 64, H: 32}}}}
66 contents := map[ports.WindowID]ports.SurfaceContent{1: {ID: 1, Width: 64, Height: 32, Opaque: true, DMABuf: &b}}
67 if err := render(viewer, scene, contents); err != nil {
68 t.Fatal(err)
69 }
70 if got := readPixels(t, viewer).At(10, 10); got != (color.RGBA{255, 0, 0, 255}) {
71 t.Fatalf("target 0 as seen by the display %v", got)
72 }
73}
74
75// The exported HDR image carries PQ while Pixels remains the composed SDR
76// image. A test-only transfer-src readback checks the actual shader output.
77func TestHDRExportTarget(t *testing.T) {
78 r, err := New(32, 16)
79 if err != nil {
80 t.Skipf("Vulkan unavailable: %v", err)
81 }
82 defer r.Close()
83 r.SetHDR(203)
84 // GPU tests and headless --screenshot-raw (SetHDRReadback); never DRM.
85 r.SetHDRReadback(true)
86 if r.physical == 0 {
87 t.Skip("no exportable GPU")
88 }
89 r.hdrMods = r.probeModifiers(r.physical, vk.FormatA2r10g10b10UnormPack32)
90 if len(r.hdrMods) == 0 {
91 t.Skip("no HDR modifier supporting transfer-src")
92 }
93 bufs, err := r.ExportTargets(1, r.hdrMods, false)
94 if err != nil {
95 t.Skipf("no HDR-exportable target: %v", err)
96 }
97 defer bufs[0].Planes[0].File.Close()
98 if bufs[0].Format != fourccXR30 {
99 t.Fatalf("format: %#x", bufs[0].Format)
100 }
101
102 // A real second frame with an unchanged scene must retain the internal
103 // image and use a partial-damage load from its actual prior layout.
104 scene := ports.Scene{Seq: 1, Background: "#ffffff"}
105 for _, tc := range []struct {
106 bg string
107 rgb [3]float64
108 }{
109 {"#ffffff", [3]float64{1, 1, 1}},
110 {"#ff0000", [3]float64{1, 0, 0}},
111 {"#0000ff", [3]float64{0, 0, 1}},
112 {"#808080", [3]float64{128.0 / 255, 128.0 / 255, 128.0 / 255}},
113 } {
114 scene.Seq++
115 scene.Background = tc.bg
116 if err := render(r, scene, nil); err != nil {
117 t.Fatal(err)
118 }
119 checkHDRPixel(t, r, tc.rgb)
120 }
121 before := r.redrawn
122 if err := render(r, scene, nil); err != nil {
123 t.Fatal(err)
124 }
125 if r.redrawn-before >= r.width*r.height {
126 t.Fatalf("unchanged HDR frame redrew %d pixels", r.redrawn-before)
127 }
128 checkHDRPixel(t, r, [3]float64{128.0 / 255, 128.0 / 255, 128.0 / 255})
129 if r.hdrOwn.layout != vk.ImageLayoutTransferSrcOptimal {
130 t.Fatalf("internal layout after HDR: %v", r.hdrOwn.layout)
131 }
132 if got := readPixels(t, r).RGBAAt(0, 0); got.R != 128 || got.G != 128 || got.B != 128 {
133 t.Fatalf("SDR readback: %v", got)
134 }
135}
136
137// A virtual (headless) HDR output has no display to list modifiers: the
138// export accepts every exportable one. A DRM output (no SetVirtualOutput)
139// still refuses an unspecified list.
140func TestVirtualHDRExportsWithoutDisplayList(t *testing.T) {
141 r, err := New(64, 16)
142 if err != nil {
143 t.Skipf("Vulkan unavailable: %v", err)
144 }
145 defer r.Close()
146 if r.physical == 0 || len(r.hdrMods) == 0 {
147 t.Skip("no exportable HDR GPU")
148 }
149 r.SetHDR(203)
150 if bufs, err := r.ExportTargets(1, nil, false); err == nil {
151 bufs[0].Planes[0].File.Close()
152 t.Fatal("HDR export without a display list succeeded on a non-virtual output")
153 }
154 r.SetVirtualOutput(true)
155 bufs, err := r.ExportTargets(1, nil, false)
156 if err != nil {
157 t.Fatalf("virtual HDR export: %v", err)
158 }
159 defer bufs[0].Planes[0].File.Close()
160 if bufs[0].Format != fourccXR30 {
161 t.Fatalf("format: %#x", bufs[0].Format)
162 }
163}
164
165// Test-only target readback: production HDR images do not have transfer-src.
166func checkHDRPixel(t *testing.T, r *Renderer, rgb [3]float64) {
167 t.Helper()
168 v := pqTargetWords(t, r)[0]
169 red, green, blue := hdrPixel(rgb[0], rgb[1], rgb[2], 203)
170 for i, want := range []float64{blue, green, red} {
171 got := float64(v>>uint(i*10)&1023) / 1023
172 if math.Abs(got-want) > 0.005 {
173 t.Fatalf("rgb %v channel %d: PQ %.5f want %.5f", rgb, i, got, want)
174 }
175 }
176}
177
178// Targets may use DCC modifiers: one to four memory planes are kept and
179// their counts recorded; singlePlane drops the multi-plane ones.
180func TestFilterModifiersKeepsMultiPlane(t *testing.T) {
181 need := vk.FormatFeatureFlags(vk.FormatFeatureColorAttachmentBit)
182 mods := []vk.DrmFormatModifierPropertiesEXT{
183 {DrmFormatModifier: 0, DrmFormatModifierPlaneCount: 1, DrmFormatModifierTilingFeatures: need},
184 {DrmFormatModifier: 10, DrmFormatModifierPlaneCount: 3, DrmFormatModifierTilingFeatures: need},
185 {DrmFormatModifier: 11, DrmFormatModifierPlaneCount: 4, DrmFormatModifierTilingFeatures: need},
186 {DrmFormatModifier: 12, DrmFormatModifierPlaneCount: 5, DrmFormatModifierTilingFeatures: need},
187 {DrmFormatModifier: 13, DrmFormatModifierPlaneCount: 0, DrmFormatModifierTilingFeatures: need},
188 {DrmFormatModifier: 14, DrmFormatModifierPlaneCount: 2},
189 {DrmFormatModifier: 15, DrmFormatModifierPlaneCount: 2, DrmFormatModifierTilingFeatures: need},
190 }
191 got, planes := filterModifiers(mods, need, func(m uint64) bool { return m != 15 })
192 if !slices.Equal(got, []uint64{0, 10, 11}) {
193 t.Fatalf("kept %v", got)
194 }
195 if planes[0] != 1 || planes[10] != 3 || planes[11] != 4 || len(planes) != 3 {
196 t.Fatalf("planes %v", planes)
197 }
198 r := &Renderer{renderMods: got, modPlanes: planes}
199 if all := r.exportModifiers(nil, false); !slices.Equal(all, []uint64{0, 10, 11}) {
200 t.Fatalf("multi-plane export %v", all)
201 }
202 if one := r.exportModifiers(nil, true); !slices.Equal(one, []uint64{0}) {
203 t.Fatalf("single-plane export %v", one)
204 }
205 if one := r.exportModifiers([]uint64{10, 11}, true); len(one) != 0 {
206 t.Fatalf("single-plane with only DCC offered: %v", one)
207 }
208 if two := r.exportModifiers([]uint64{10, 0}, false); !slices.Equal(two, []uint64{0, 10}) {
209 t.Fatalf("display intersection %v", two)
210 }
211}
212
213// Every exported target carries one file per memory plane of its modifier
214// (DCC modifiers have two to four), each with a layout, and drawing into
215// each target works: the frames read back through the producing renderer.
216// A multi-plane image cannot be read back through a viewer import, which
217// only imports one-plane buffers.
218func TestExportTargetsPlanes(t *testing.T) {
219 r, err := New(64, 32)
220 if err != nil {
221 t.Skipf("Vulkan unavailable: %v", err)
222 }
223 defer r.Close()
224 closeAll := func(bufs []ports.DMABuf) {
225 for _, b := range bufs {
226 for _, p := range b.Planes {
227 p.File.Close()
228 }
229 }
230 }
231 // Single-plane export never returns a multi-plane modifier.
232 one, err := r.ExportTargets(1, nil, true)
233 if err != nil {
234 t.Skipf("no single-plane targets: %v", err)
235 }
236 if len(one[0].Planes) != 1 || r.modPlanes[one[0].Modifier] > 1 {
237 t.Fatalf("single-plane export has %d planes (modifier %#x)", len(one[0].Planes), one[0].Modifier)
238 }
239 closeAll(one)
240 multi := false
241 for _, m := range r.renderMods {
242 multi = multi || r.modPlanes[m] > 1
243 }
244 if !multi {
245 t.Skip("device has no multi-plane target modifier")
246 }
247 bufs, err := r.ExportTargets(2, nil, false)
248 if err != nil {
249 t.Skipf("no exportable targets: %v", err)
250 }
251 defer closeAll(bufs)
252 for _, b := range bufs {
253 want := max(r.modPlanes[b.Modifier], 1)
254 if uint32(len(b.Planes)) != want {
255 t.Fatalf("modifier %#x: %d plane files, want %d", b.Modifier, len(b.Planes), want)
256 }
257 seen := map[int]bool{}
258 for i, p := range b.Planes {
259 if p.File == nil || p.Stride == 0 {
260 t.Fatalf("modifier %#x plane %d: %+v", b.Modifier, i, p)
261 }
262 // The planes share one memory object: planes 1.. lie after plane 0.
263 if i > 0 && p.Offset == 0 {
264 t.Fatalf("modifier %#x plane %d at offset 0", b.Modifier, i)
265 }
266 if seen[int(p.File.Fd())] {
267 t.Fatalf("modifier %#x: plane %d shares a file", b.Modifier, i)
268 }
269 seen[int(p.File.Fd())] = true
270 }
271 t.Logf("modifier %#x planes %d", b.Modifier, len(b.Planes))
272 }
273 for i, tc := range []struct {
274 bg string
275 want color.RGBA
276 }{{"#ff0000", color.RGBA{255, 0, 0, 255}}, {"#0000ff", color.RGBA{0, 0, 255, 255}}} {
277 r.UseTarget(i)
278 if err := render(r, ports.Scene{Background: tc.bg}, nil); err != nil {
279 t.Fatal(err)
280 }
281 if got := readPixels(t, r).At(3, 3); got != tc.want {
282 t.Fatalf("target %d (modifier %#x): read back %v, want %v", i, bufs[i].Modifier, got, tc.want)
283 }
284 }
285}
286
287// A target exported for a display modifier list carries one of its
288// modifiers, whichever they are: KMS refuses a framebuffer whose modifier
289// the plane does not advertise, so the intersection must hold on the real
290// driver (RADV also exports DCC variants displays do not list).
291func TestExportTargetsStayInTheDisplayList(t *testing.T) {
292 r, err := New(64, 32)
293 if err != nil {
294 t.Skipf("Vulkan unavailable: %v", err)
295 }
296 defer r.Close()
297 if len(r.renderMods) == 0 {
298 t.Skip("device exports no render modifier")
299 }
300 for _, m := range r.renderMods {
301 bufs, err := r.ExportTargets(1, []uint64{m}, false)
302 if err != nil {
303 t.Fatalf("modifier %#x is exportable but export failed: %v", m, err)
304 }
305 got := bufs[0].Modifier
306 for _, p := range bufs[0].Planes {
307 p.File.Close()
308 }
309 if got != m {
310 t.Fatalf("display lists only %#x, target has modifier %#x", m, got)
311 }
312 }
313 // A list with no exportable modifier is an error, not a guess.
314 if bufs, err := r.ExportTargets(1, []uint64{0x1234567}, false); err == nil {
315 for _, p := range bufs[0].Planes {
316 p.File.Close()
317 }
318 t.Fatalf("export with an unexportable display list gave modifier %#x", bufs[0].Modifier)
319 }
320}
321
322// The AMD DCC retile modifiers cost a compute pass per frame: they are never
323// targets, whatever the display lists. Plain DCC (pipe-aligned or not), the
324// non-DCC tiled modifier and non-AMD modifiers with bit 14 set stay.
325func TestFilterModifiersDropsDCCRetile(t *testing.T) {
326 const (
327 retile = 0x200000010567b03 // AMD DCC, DCC_RETILE (laptop RADV)
328 pipeAligned = 0x20000000056bb03 // AMD DCC, pipe-aligned
329 unaligned = 0x200000000563b03 // AMD DCC, not pipe-aligned
330 tiled = 0x200000010401b03 // AMD tiled, no DCC
331 foreignRetil = 0x0100000000004003 // vendor 1, bit 14 set: not AMD's
332 )
333 need := vk.FormatFeatureFlags(vk.FormatFeatureColorAttachmentBit)
334 var mods []vk.DrmFormatModifierPropertiesEXT
335 for _, m := range []uint64{retile, pipeAligned, unaligned, tiled, foreignRetil, 0} {
336 planes := uint32(1)
337 if m == retile || m == pipeAligned || m == unaligned {
338 planes = 3
339 }
340 mods = append(mods, vk.DrmFormatModifierPropertiesEXT{DrmFormatModifier: m, DrmFormatModifierPlaneCount: planes, DrmFormatModifierTilingFeatures: need})
341 }
342 got, planes := filterModifiers(mods, need, func(uint64) bool { return true })
343 if want := []uint64{pipeAligned, unaligned, tiled, foreignRetil, 0}; !slices.Equal(got, want) {
344 t.Fatalf("kept %#x, want %#x", got, want)
345 }
346 if _, ok := planes[retile]; ok {
347 t.Fatalf("retile modifier has a plane count: %v", planes)
348 }
349 r := &Renderer{renderMods: got, modPlanes: planes}
350 if out := r.exportModifiers([]uint64{retile, tiled}, false); !slices.Equal(out, []uint64{tiled}) {
351 t.Fatalf("display listing the retile modifier: %#x", out)
352 }
353 if out := r.exportModifiers([]uint64{retile}, false); len(out) != 0 {
354 t.Fatalf("display listing only the retile modifier: %#x", out)
355 }
356}
357
358// The device's exportable modifiers hold no AMD DCC retile one, whatever
359// RADV lists (it lists them on GPUs with several render backends).
360func TestProbedModifiersHaveNoDCCRetile(t *testing.T) {
361 r, err := New(64, 32)
362 if err != nil {
363 t.Skipf("Vulkan unavailable: %v", err)
364 }
365 defer r.Close()
366 for _, m := range append(slices.Clone(r.renderMods), r.hdrMods...) {
367 if retilesDCC(m) {
368 t.Fatalf("target modifier %#x retiles DCC", m)
369 }
370 }
371}
372
373// pipeFile is a file for plane tests: a distinct inode per call.
374func pipeFile(t *testing.T) *os.File {
375 t.Helper()
376 f, w, err := os.Pipe()
377 if err != nil {
378 t.Fatal(err)
379 }
380 w.Close()
381 t.Cleanup(func() { f.Close() })
382 return f
383}
384
385// Planes must be in one dmabuf: the same file or duplicates of it are, two
386// files are not. No device needed.
387func TestSameDMABuf(t *testing.T) {
388 one, other := pipeFile(t), pipeFile(t)
389 for _, tc := range []struct {
390 name string
391 planes []ports.DMABufPlane
392 ok bool
393 }{
394 {"one plane", []ports.DMABufPlane{{File: one}}, true},
395 {"same file twice", []ports.DMABufPlane{{File: one}, {File: one}}, true},
396 {"two files", []ports.DMABufPlane{{File: one}, {File: other}}, false},
397 {"third plane elsewhere", []ports.DMABufPlane{{File: one}, {File: one}, {File: other}}, false},
398 {"no file", []ports.DMABufPlane{{File: one}, {}}, false},
399 } {
400 err := sameDMABuf(tc.planes)
401 if (err == nil) != tc.ok {
402 t.Errorf("%s: %v", tc.name, err)
403 }
404 if err != nil && tc.name != "no file" && !errors.Is(err, errImageDMABuf) {
405 t.Errorf("%s: %v is not errImageDMABuf", tc.name, err)
406 }
407 }
408}
409
410// TargetModifiers keeps the modifiers the display lists and drops
411// multi-plane ones for singlePlane.
412func TestTargetModifiersFiltering(t *testing.T) {
413 r, err := New(64, 32)
414 if err != nil {
415 t.Skipf("Vulkan unavailable: %v", err)
416 }
417 defer r.Close()
418 mods := r.TargetModifiers(nil, false)
419 if len(mods) == 0 {
420 t.Skip("no importable target modifier")
421 }
422 if got := r.TargetModifiers([]uint64{0x1234567}, false); len(got) != 0 {
423 t.Errorf("modifiers outside the display list: %#x", got)
424 }
425 if got := r.TargetModifiers(mods[:1], false); len(got) != 1 || got[0] != mods[0] {
426 t.Errorf("display list %#x gave %#x", mods[:1], got)
427 }
428 for _, m := range r.TargetModifiers(nil, true) {
429 if r.modPlanes[m] > 1 {
430 t.Errorf("single-plane list holds multi-plane modifier %#x", m)
431 }
432 }
433 // The answer is probed once and kept.
434 if len(r.importMods) == 0 {
435 t.Error("import modifiers not cached")
436 }
437 again := r.TargetModifiers(nil, false)
438 if !slices.Equal(mods, again) {
439 t.Errorf("cached list differs: %#x vs %#x", mods, again)
440 }
441}
442
443// ImportTargets refuses an image that does not fit the renderer before
444// asking the driver, says why, and leaves no targets: frames go back to the
445// renderer's own image.
446func TestImportTargetsRefusesMismatchedImages(t *testing.T) {
447 r, err := New(64, 32)
448 if err != nil {
449 t.Skipf("Vulkan unavailable: %v", err)
450 }
451 defer r.Close()
452 var single uint64
453 found := false
454 for _, m := range r.TargetModifiers(nil, true) {
455 single, found = m, true
456 break
457 }
458 if !found {
459 t.Skip("no importable one-plane target modifier")
460 }
461 good := func() ports.DMABuf {
462 return ports.DMABuf{Width: 64, Height: 32, Format: fourccXRGB, Modifier: single, Planes: []ports.DMABufPlane{{File: pipeFile(t)}}}
463 }
464 for _, tc := range []struct {
465 name string
466 mutate func(*ports.DMABuf)
467 want error
468 }{
469 {"width", func(b *ports.DMABuf) { b.Width = 65 }, errImageGeometry},
470 {"height", func(b *ports.DMABuf) { b.Height = 31 }, errImageGeometry},
471 {"format", func(b *ports.DMABuf) { b.Format = fourccXR30 }, errImageGeometry},
472 {"unknown modifier", func(b *ports.DMABuf) { b.Modifier = 0x1234567 }, errImageModifier},
473 {"no planes", func(b *ports.DMABuf) { b.Planes = nil }, errImagePlanes},
474 {"too many planes", func(b *ports.DMABuf) {
475 b.Planes = append(b.Planes, ports.DMABufPlane{File: pipeFile(t)})
476 }, errImagePlanes},
477 } {
478 t.Run(tc.name, func(t *testing.T) {
479 b := good()
480 tc.mutate(&b)
481 err := r.ImportTargets([]ports.DMABuf{b})
482 if !errors.Is(err, tc.want) {
483 t.Fatalf("error %v, want %v", err, tc.want)
484 }
485 if len(r.targets) != 0 {
486 t.Fatalf("%d targets left", len(r.targets))
487 }
488 })
489 }
490 t.Run("second image refused drops the first", func(t *testing.T) {
491 // A real dmabuf for the first image: the renderer's own export.
492 exported, err := r.ExportTargets(1, []uint64{single}, true)
493 if err != nil {
494 t.Skipf("no exportable target to reuse: %v", err)
495 }
496 defer closeBufs(exported)
497 bad := good()
498 bad.Width = 1
499 err = r.ImportTargets([]ports.DMABuf{exported[0], bad})
500 if !errors.Is(err, errImageGeometry) {
501 t.Fatalf("error %v, want the second image's geometry refusal", err)
502 }
503 if len(r.targets) != 0 {
504 t.Fatalf("%d targets left", len(r.targets))
505 }
506 })
507 t.Run("dropping", func(t *testing.T) {
508 if err := r.ImportTargets(nil); err != nil || len(r.targets) != 0 {
509 t.Fatalf("%v, %d targets", err, len(r.targets))
510 }
511 })
512 t.Run("planes in different dmabufs", func(t *testing.T) {
513 multi := uint64(0)
514 for _, m := range r.TargetModifiers(nil, false) {
515 if r.modPlanes[m] > 1 {
516 multi = m
517 break
518 }
519 }
520 if multi == 0 {
521 t.Skip("no multi-plane importable modifier on this device: the check is covered by TestSameDMABuf")
522 }
523 b := good()
524 b.Modifier = multi
525 b.Planes = nil
526 for range r.modPlanes[multi] {
527 b.Planes = append(b.Planes, ports.DMABufPlane{File: pipeFile(t)})
528 }
529 if err := r.ImportTargets([]ports.DMABuf{b}); !errors.Is(err, errImageDMABuf) {
530 t.Fatalf("error %v", err)
531 }
532 })
533}
534
535func closeBufs(bufs []ports.DMABuf) {
536 for _, b := range bufs {
537 for _, p := range b.Planes {
538 p.File.Close()
539 }
540 }
541}