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}