feat/data-dnd e05cb15

targets_test.go RawHistoryFolder

1package vulkan
2
3import (
4	"image/color"
5	"math"
6	"slices"
7	"testing"
8
9	"github.com/bnema/neferwl/internal/ports"
10	vk "github.com/bnema/purego-vulkan/vulkan"
11)
12
13// Frames drawn into exported targets reach their dmabufs: another renderer
14// importing a target (as KMS scans it out) sees the frame, and readback
15// reads the target drawn last. The viewer imports one-plane buffers only,
16// so the targets are single-plane here; TestExportTargetsPlanes covers the
17// multi-plane (DCC) ones.
18func TestExportTargets(t *testing.T) {
19	r, err := New(64, 32)
20	if err != nil {
21		t.Skipf("Vulkan unavailable: %v", err)
22	}
23	defer r.Close()
24	bufs, err := r.ExportTargets(2, nil, true)
25	if err != nil {
26		t.Skipf("no exportable targets: %v", err)
27	}
28	defer func() {
29		for _, b := range bufs {
30			for _, p := range b.Planes {
31				p.File.Close()
32			}
33		}
34	}()
35	if len(bufs) != 2 || bufs[0].Width != 64 || bufs[0].Format != fourccXRGB || bufs[0].Planes[0].Stride < 64*4 {
36		t.Fatalf("targets %+v", bufs)
37	}
38	colors := []string{"#ff0000", "#0000ff"}
39	for i, c := range colors {
40		r.UseTarget(i)
41		if err := render(r, ports.Scene{Background: c}, nil); err != nil {
42			t.Fatal(err)
43		}
44	}
45	if got := readPixels(t, r).At(3, 3); got != (color.RGBA{0, 0, 255, 255}) {
46		t.Fatalf("readback of the last target %v", got)
47	}
48	// Import target 0 as a client buffer, like the display would.
49	viewer, err := New(64, 32)
50	if err != nil {
51		t.Fatal(err)
52	}
53	defer viewer.Close()
54	b := bufs[0]
55	b.ID = 1
56	importable := false
57	for _, f := range viewer.DMABuf().Formats {
58		importable = importable || f.Format == b.Format && f.Modifier == b.Modifier
59	}
60	if !importable {
61		t.Skipf("modifier %#x not importable", b.Modifier)
62	}
63	scene := ports.Scene{Background: "#000000", Windows: []ports.SceneWindow{{ID: 1, Rect: ports.Rect{W: 64, H: 32}}}}
64	contents := map[ports.WindowID]ports.SurfaceContent{1: {ID: 1, Width: 64, Height: 32, Opaque: true, DMABuf: &b}}
65	if err := render(viewer, scene, contents); err != nil {
66		t.Fatal(err)
67	}
68	if got := readPixels(t, viewer).At(10, 10); got != (color.RGBA{255, 0, 0, 255}) {
69		t.Fatalf("target 0 as seen by the display %v", got)
70	}
71}
72
73// The exported HDR image carries PQ while Pixels remains the composed SDR
74// image. A test-only transfer-src readback checks the actual shader output.
75func TestHDRExportTarget(t *testing.T) {
76	r, err := New(32, 16)
77	if err != nil {
78		t.Skipf("Vulkan unavailable: %v", err)
79	}
80	defer r.Close()
81	r.SetHDR(203)
82	// GPU tests and headless --screenshot-raw (SetHDRReadback); never DRM.
83	r.SetHDRReadback(true)
84	if r.physical == 0 {
85		t.Skip("no exportable GPU")
86	}
87	r.hdrMods = r.probeModifiers(r.physical, vk.FormatA2r10g10b10UnormPack32)
88	if len(r.hdrMods) == 0 {
89		t.Skip("no HDR modifier supporting transfer-src")
90	}
91	bufs, err := r.ExportTargets(1, r.hdrMods, false)
92	if err != nil {
93		t.Skipf("no HDR-exportable target: %v", err)
94	}
95	defer bufs[0].Planes[0].File.Close()
96	if bufs[0].Format != fourccXR30 {
97		t.Fatalf("format: %#x", bufs[0].Format)
98	}
99
100	// A real second frame with an unchanged scene must retain the internal
101	// image and use a partial-damage load from its actual prior layout.
102	scene := ports.Scene{Seq: 1, Background: "#ffffff"}
103	for _, tc := range []struct {
104		bg  string
105		rgb [3]float64
106	}{
107		{"#ffffff", [3]float64{1, 1, 1}},
108		{"#ff0000", [3]float64{1, 0, 0}},
109		{"#0000ff", [3]float64{0, 0, 1}},
110		{"#808080", [3]float64{128.0 / 255, 128.0 / 255, 128.0 / 255}},
111	} {
112		scene.Seq++
113		scene.Background = tc.bg
114		if err := render(r, scene, nil); err != nil {
115			t.Fatal(err)
116		}
117		checkHDRPixel(t, r, tc.rgb)
118	}
119	before := r.redrawn
120	if err := render(r, scene, nil); err != nil {
121		t.Fatal(err)
122	}
123	if r.redrawn-before >= r.width*r.height {
124		t.Fatalf("unchanged HDR frame redrew %d pixels", r.redrawn-before)
125	}
126	checkHDRPixel(t, r, [3]float64{128.0 / 255, 128.0 / 255, 128.0 / 255})
127	if r.hdrOwn.layout != vk.ImageLayoutTransferSrcOptimal {
128		t.Fatalf("internal layout after HDR: %v", r.hdrOwn.layout)
129	}
130	if got := readPixels(t, r).RGBAAt(0, 0); got.R != 128 || got.G != 128 || got.B != 128 {
131		t.Fatalf("SDR readback: %v", got)
132	}
133}
134
135// A virtual (headless) HDR output has no display to list modifiers: the
136// export accepts every exportable one. A DRM output (no SetVirtualOutput)
137// still refuses an unspecified list.
138func TestVirtualHDRExportsWithoutDisplayList(t *testing.T) {
139	r, err := New(64, 16)
140	if err != nil {
141		t.Skipf("Vulkan unavailable: %v", err)
142	}
143	defer r.Close()
144	if r.physical == 0 || len(r.hdrMods) == 0 {
145		t.Skip("no exportable HDR GPU")
146	}
147	r.SetHDR(203)
148	if bufs, err := r.ExportTargets(1, nil, false); err == nil {
149		bufs[0].Planes[0].File.Close()
150		t.Fatal("HDR export without a display list succeeded on a non-virtual output")
151	}
152	r.SetVirtualOutput(true)
153	bufs, err := r.ExportTargets(1, nil, false)
154	if err != nil {
155		t.Fatalf("virtual HDR export: %v", err)
156	}
157	defer bufs[0].Planes[0].File.Close()
158	if bufs[0].Format != fourccXR30 {
159		t.Fatalf("format: %#x", bufs[0].Format)
160	}
161}
162
163// Test-only target readback: production HDR images do not have transfer-src.
164func checkHDRPixel(t *testing.T, r *Renderer, rgb [3]float64) {
165	t.Helper()
166	v := pqTargetWords(t, r)[0]
167	red, green, blue := hdrPixel(rgb[0], rgb[1], rgb[2], 203)
168	for i, want := range []float64{blue, green, red} {
169		got := float64(v>>uint(i*10)&1023) / 1023
170		if math.Abs(got-want) > 0.005 {
171			t.Fatalf("rgb %v channel %d: PQ %.5f want %.5f", rgb, i, got, want)
172		}
173	}
174}
175
176// Targets may use DCC modifiers: one to four memory planes are kept and
177// their counts recorded; singlePlane drops the multi-plane ones.
178func TestFilterModifiersKeepsMultiPlane(t *testing.T) {
179	need := vk.FormatFeatureFlags(vk.FormatFeatureColorAttachmentBit)
180	mods := []vk.DrmFormatModifierPropertiesEXT{
181		{DrmFormatModifier: 0, DrmFormatModifierPlaneCount: 1, DrmFormatModifierTilingFeatures: need},
182		{DrmFormatModifier: 10, DrmFormatModifierPlaneCount: 3, DrmFormatModifierTilingFeatures: need},
183		{DrmFormatModifier: 11, DrmFormatModifierPlaneCount: 4, DrmFormatModifierTilingFeatures: need},
184		{DrmFormatModifier: 12, DrmFormatModifierPlaneCount: 5, DrmFormatModifierTilingFeatures: need},
185		{DrmFormatModifier: 13, DrmFormatModifierPlaneCount: 0, DrmFormatModifierTilingFeatures: need},
186		{DrmFormatModifier: 14, DrmFormatModifierPlaneCount: 2},
187		{DrmFormatModifier: 15, DrmFormatModifierPlaneCount: 2, DrmFormatModifierTilingFeatures: need},
188	}
189	got, planes := filterModifiers(mods, need, func(m uint64) bool { return m != 15 })
190	if !slices.Equal(got, []uint64{0, 10, 11}) {
191		t.Fatalf("kept %v", got)
192	}
193	if planes[0] != 1 || planes[10] != 3 || planes[11] != 4 || len(planes) != 3 {
194		t.Fatalf("planes %v", planes)
195	}
196	r := &Renderer{renderMods: got, modPlanes: planes}
197	if all := r.exportModifiers(nil, false); !slices.Equal(all, []uint64{0, 10, 11}) {
198		t.Fatalf("multi-plane export %v", all)
199	}
200	if one := r.exportModifiers(nil, true); !slices.Equal(one, []uint64{0}) {
201		t.Fatalf("single-plane export %v", one)
202	}
203	if one := r.exportModifiers([]uint64{10, 11}, true); len(one) != 0 {
204		t.Fatalf("single-plane with only DCC offered: %v", one)
205	}
206	if two := r.exportModifiers([]uint64{10, 0}, false); !slices.Equal(two, []uint64{0, 10}) {
207		t.Fatalf("display intersection %v", two)
208	}
209}
210
211// Every exported target carries one file per memory plane of its modifier
212// (DCC modifiers have two to four), each with a layout, and drawing into
213// each target works: the frames read back through the producing renderer.
214// A multi-plane image cannot be read back through a viewer import, which
215// only imports one-plane buffers.
216func TestExportTargetsPlanes(t *testing.T) {
217	r, err := New(64, 32)
218	if err != nil {
219		t.Skipf("Vulkan unavailable: %v", err)
220	}
221	defer r.Close()
222	closeAll := func(bufs []ports.DMABuf) {
223		for _, b := range bufs {
224			for _, p := range b.Planes {
225				p.File.Close()
226			}
227		}
228	}
229	// Single-plane export never returns a multi-plane modifier.
230	one, err := r.ExportTargets(1, nil, true)
231	if err != nil {
232		t.Skipf("no single-plane targets: %v", err)
233	}
234	if len(one[0].Planes) != 1 || r.modPlanes[one[0].Modifier] > 1 {
235		t.Fatalf("single-plane export has %d planes (modifier %#x)", len(one[0].Planes), one[0].Modifier)
236	}
237	closeAll(one)
238	multi := false
239	for _, m := range r.renderMods {
240		multi = multi || r.modPlanes[m] > 1
241	}
242	if !multi {
243		t.Skip("device has no multi-plane target modifier")
244	}
245	bufs, err := r.ExportTargets(2, nil, false)
246	if err != nil {
247		t.Skipf("no exportable targets: %v", err)
248	}
249	defer closeAll(bufs)
250	for _, b := range bufs {
251		want := max(r.modPlanes[b.Modifier], 1)
252		if uint32(len(b.Planes)) != want {
253			t.Fatalf("modifier %#x: %d plane files, want %d", b.Modifier, len(b.Planes), want)
254		}
255		seen := map[int]bool{}
256		for i, p := range b.Planes {
257			if p.File == nil || p.Stride == 0 {
258				t.Fatalf("modifier %#x plane %d: %+v", b.Modifier, i, p)
259			}
260			// The planes share one memory object: planes 1.. lie after plane 0.
261			if i > 0 && p.Offset == 0 {
262				t.Fatalf("modifier %#x plane %d at offset 0", b.Modifier, i)
263			}
264			if seen[int(p.File.Fd())] {
265				t.Fatalf("modifier %#x: plane %d shares a file", b.Modifier, i)
266			}
267			seen[int(p.File.Fd())] = true
268		}
269		t.Logf("modifier %#x planes %d", b.Modifier, len(b.Planes))
270	}
271	for i, tc := range []struct {
272		bg   string
273		want color.RGBA
274	}{{"#ff0000", color.RGBA{255, 0, 0, 255}}, {"#0000ff", color.RGBA{0, 0, 255, 255}}} {
275		r.UseTarget(i)
276		if err := render(r, ports.Scene{Background: tc.bg}, nil); err != nil {
277			t.Fatal(err)
278		}
279		if got := readPixels(t, r).At(3, 3); got != tc.want {
280			t.Fatalf("target %d (modifier %#x): read back %v, want %v", i, bufs[i].Modifier, got, tc.want)
281		}
282	}
283}
284
285// A target exported for a display modifier list carries one of its
286// modifiers, whichever they are: KMS refuses a framebuffer whose modifier
287// the plane does not advertise, so the intersection must hold on the real
288// driver (RADV also exports DCC variants displays do not list).
289func TestExportTargetsStayInTheDisplayList(t *testing.T) {
290	r, err := New(64, 32)
291	if err != nil {
292		t.Skipf("Vulkan unavailable: %v", err)
293	}
294	defer r.Close()
295	if len(r.renderMods) == 0 {
296		t.Skip("device exports no render modifier")
297	}
298	for _, m := range r.renderMods {
299		bufs, err := r.ExportTargets(1, []uint64{m}, false)
300		if err != nil {
301			t.Fatalf("modifier %#x is exportable but export failed: %v", m, err)
302		}
303		got := bufs[0].Modifier
304		for _, p := range bufs[0].Planes {
305			p.File.Close()
306		}
307		if got != m {
308			t.Fatalf("display lists only %#x, target has modifier %#x", m, got)
309		}
310	}
311	// A list with no exportable modifier is an error, not a guess.
312	if bufs, err := r.ExportTargets(1, []uint64{0x1234567}, false); err == nil {
313		for _, p := range bufs[0].Planes {
314			p.File.Close()
315		}
316		t.Fatalf("export with an unexportable display list gave modifier %#x", bufs[0].Modifier)
317	}
318}
319
320// The AMD DCC retile modifiers cost a compute pass per frame: they are never
321// targets, whatever the display lists. Plain DCC (pipe-aligned or not), the
322// non-DCC tiled modifier and non-AMD modifiers with bit 14 set stay.
323func TestFilterModifiersDropsDCCRetile(t *testing.T) {
324	const (
325		retile       = 0x200000010567b03  // AMD DCC, DCC_RETILE (laptop RADV)
326		pipeAligned  = 0x20000000056bb03  // AMD DCC, pipe-aligned
327		unaligned    = 0x200000000563b03  // AMD DCC, not pipe-aligned
328		tiled        = 0x200000010401b03  // AMD tiled, no DCC
329		foreignRetil = 0x0100000000004003 // vendor 1, bit 14 set: not AMD's
330	)
331	need := vk.FormatFeatureFlags(vk.FormatFeatureColorAttachmentBit)
332	var mods []vk.DrmFormatModifierPropertiesEXT
333	for _, m := range []uint64{retile, pipeAligned, unaligned, tiled, foreignRetil, 0} {
334		planes := uint32(1)
335		if m == retile || m == pipeAligned || m == unaligned {
336			planes = 3
337		}
338		mods = append(mods, vk.DrmFormatModifierPropertiesEXT{DrmFormatModifier: m, DrmFormatModifierPlaneCount: planes, DrmFormatModifierTilingFeatures: need})
339	}
340	got, planes := filterModifiers(mods, need, func(uint64) bool { return true })
341	if want := []uint64{pipeAligned, unaligned, tiled, foreignRetil, 0}; !slices.Equal(got, want) {
342		t.Fatalf("kept %#x, want %#x", got, want)
343	}
344	if _, ok := planes[retile]; ok {
345		t.Fatalf("retile modifier has a plane count: %v", planes)
346	}
347	r := &Renderer{renderMods: got, modPlanes: planes}
348	if out := r.exportModifiers([]uint64{retile, tiled}, false); !slices.Equal(out, []uint64{tiled}) {
349		t.Fatalf("display listing the retile modifier: %#x", out)
350	}
351	if out := r.exportModifiers([]uint64{retile}, false); len(out) != 0 {
352		t.Fatalf("display listing only the retile modifier: %#x", out)
353	}
354}
355
356// The device's exportable modifiers hold no AMD DCC retile one, whatever
357// RADV lists (it lists them on GPUs with several render backends).
358func TestProbedModifiersHaveNoDCCRetile(t *testing.T) {
359	r, err := New(64, 32)
360	if err != nil {
361		t.Skipf("Vulkan unavailable: %v", err)
362	}
363	defer r.Close()
364	for _, m := range append(slices.Clone(r.renderMods), r.hdrMods...) {
365		if retilesDCC(m) {
366			t.Fatalf("target modifier %#x retiles DCC", m)
367		}
368	}
369}

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 25

Tags 10

↑↓ moveenter openesc close