main f2d166b

targets_test.go RawHistoryFolder

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}

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 26

Tags 10

↑↓ moveenter openesc close