1package vulkan
2
3import (
4 "image"
5 "image/color"
6 "slices"
7 "testing"
8 "time"
9
10 "github.com/bnema/neferwl/internal/ports"
11)
12
13// opaqueRegionContent is a 2x-scale shm root of w×h logical pixels under one
14// child of the same size whose opaque region stops a pixel short of its right
15// and bottom edges (Firefox's page over its shm root).
16func opaqueRegionContent(t testing.TB, w, h int) ports.SurfaceContent {
17 root := solidContent(t, 2*w, 2*h, color.RGBA{R: 255, A: 255})
18 root.ID, root.Surface, root.Seq, root.Version = 1, 1, 1, 1
19 root.LogicalW, root.LogicalH = w, h
20 child := solidContent(t, w, h, color.RGBA{G: 255, A: 255})
21 child.Surface, child.Version = 2, 1
22 child.LogicalW, child.LogicalH = w, h
23 child.OpaqueRect = ports.Rect{W: w - 1, H: h - 1}
24 root.Children = []ports.Subsurface{{SurfaceContent: child}}
25 return root
26}
27
28// drawRects are the rects of the content draws (not the window's fills).
29func drawRects(ds []draw) []image.Rectangle {
30 var out []image.Rectangle
31 for _, d := range ds {
32 if d.pc.misc[0] == modeSolid {
33 continue
34 }
35 out = append(out, image.Rect(int(d.pc.rect[0]), int(d.pc.rect[1]), int(d.pc.rect[2]), int(d.pc.rect[3])))
36 }
37 return out
38}
39
40// A child whose opaque region leaves a thin edge out hides the root under the
41// region only: the root draws the two strips around it, and the child draws
42// over its own whole rect.
43func TestSceneOpaqueRegionClipsRoot(t *testing.T) {
44 r, err := New(64, 48)
45 if err != nil {
46 t.Skipf("Vulkan unavailable: %v", err)
47 }
48 defer r.Close()
49 scene := ports.Scene{Seq: 1, Scale: 1, Background: "#000000", Windows: []ports.SceneWindow{{ID: 1, Rect: ports.Rect{W: 20, H: 12}}}}
50 contents := map[ports.WindowID]ports.SurfaceContent{1: opaqueRegionContent(t, 20, 12)}
51 ds := r.draws(scene, contents, newDamage(r.target(), scene, image.Rect(0, 0, 64, 48)))
52 want := []image.Rectangle{
53 // The root: the strip under the last row, then the one right of the
54 // region; then the child over its whole rect.
55 image.Rect(0, 11, 20, 12), image.Rect(19, 0, 20, 11),
56 image.Rect(0, 0, 20, 12),
57 }
58 if got := drawRects(ds); !sameRects(got, want) {
59 t.Fatalf("draws %v, want %v", got, want)
60 }
61}
62
63func sameRects(a, b []image.Rectangle) bool {
64 if len(a) != len(b) {
65 return false
66 }
67 for i := range a {
68 if a[i] != b[i] {
69 return false
70 }
71 }
72 return true
73}
74
75// A wholly opaque child still skips the root.
76func TestSceneOpaqueChildSkipsRoot(t *testing.T) {
77 r, err := New(64, 48)
78 if err != nil {
79 t.Skipf("Vulkan unavailable: %v", err)
80 }
81 defer r.Close()
82 root := opaqueRegionContent(t, 20, 12)
83 root.Children[0].Opaque, root.Children[0].OpaqueRect = true, ports.Rect{}
84 scene := ports.Scene{Seq: 1, Scale: 1, Background: "#000000", Windows: []ports.SceneWindow{{ID: 1, Rect: ports.Rect{W: 20, H: 12}}}}
85 ds := r.draws(scene, map[ports.WindowID]ports.SurfaceContent{1: root}, newDamage(r.target(), scene, image.Rect(0, 0, 64, 48)))
86 if got, want := drawRects(ds), []image.Rectangle{image.Rect(0, 0, 20, 12)}; !sameRects(got, want) {
87 t.Fatalf("draws %v, want %v", got, want)
88 }
89}
90
91// The strips show the root exactly as it draws alone: the child is
92// transparent on its last row and column, outside its opaque region, and the
93// root's pixels show through there, mapped as without the clipping. Inside
94// the region the child shows.
95func TestRendererOpaqueRegionStripsKeepRoot(t *testing.T) {
96 r, err := New(64, 48)
97 if err != nil {
98 t.Skipf("Vulkan unavailable: %v", err)
99 }
100 defer r.Close()
101 const w, h = 20, 12
102 scene := ports.Scene{Scale: 1, Background: "#000000", Windows: []ports.SceneWindow{{ID: 1, Rect: ports.Rect{W: w, H: h}}}}
103 root := opaqueRegionContent(t, w, h)
104 // A 2x root with a gradient, to see a wrong strip mapping; a child
105 // that is green but for a transparent last row and column.
106 px := make([]byte, 2*w*2*h*4)
107 for y := range 2 * h {
108 for x := range 2 * w {
109 copy(px[(y*2*w+x)*4:], []byte{0, uint8(y * 10), uint8(x * 6), 255})
110 }
111 }
112 root.SHM = shmContent(t, 2*w, 2*h, 2*w*4, px).SHM
113 cp := make([]byte, w*h*4)
114 for y := range h {
115 for x := range w {
116 if x < w-1 && y < h-1 {
117 copy(cp[(y*w+x)*4:], []byte{0, 255, 0, 255})
118 }
119 }
120 }
121 root.Children[0].SHM = shmContent(t, w, h, w*4, cp).SHM
122 if err := render(r, scene, map[ports.WindowID]ports.SurfaceContent{1: root}); err != nil {
123 t.Fatal(err)
124 }
125 with := readPixels(t, r)
126 alone := root
127 alone.Children = nil
128 alone.Seq, alone.Version = 2, 2
129 if err := render(r, scene, map[ports.WindowID]ports.SurfaceContent{1: alone}); err != nil {
130 t.Fatal(err)
131 }
132 without := readPixels(t, r)
133 green := color.RGBA{G: 255, A: 255}
134 for y := range h {
135 for x := range w {
136 want := without.At(x, y)
137 if x < w-1 && y < h-1 {
138 want = green
139 }
140 if got := with.At(x, y); got != want {
141 t.Fatalf("At(%d,%d)=%v want %v", x, y, got, want)
142 }
143 }
144 }
145}
146
147// opaqueCover maps the opaque rect onto the physical rect of the surface,
148// rounding inward, clipped to what shows.
149func TestOpaqueCover(t *testing.T) {
150 c := ports.SurfaceContent{LogicalW: 984, LogicalH: 1231, OpaqueRect: ports.Rect{W: 983, H: 1230}}
151 for _, tc := range []struct {
152 name string
153 full, dst image.Rectangle
154 rect ports.Rect
155 want image.Rectangle
156 }{
157 {"scale 1.3", image.Rect(0, 0, 1279, 1600), image.Rect(0, 0, 1279, 1600), c.OpaqueRect, image.Rect(0, 0, 1277, 1598)},
158 {"offset", image.Rect(10, 20, 1289, 1620), image.Rect(10, 20, 1289, 1620), c.OpaqueRect, image.Rect(10, 20, 1287, 1618)},
159 {"clipped", image.Rect(0, 0, 1279, 1600), image.Rect(0, 0, 100, 100), c.OpaqueRect, image.Rect(0, 0, 100, 100)},
160 {"inner rect", image.Rect(0, 0, 984, 1231), image.Rect(0, 0, 984, 1231), ports.Rect{X: 4, Y: 8, W: 100, H: 200}, image.Rect(4, 8, 104, 208)},
161 {"outside", image.Rect(0, 0, 984, 1231), image.Rect(0, 0, 3, 3), ports.Rect{X: 4, Y: 8, W: 100, H: 200}, image.Rectangle{}},
162 } {
163 t.Run(tc.name, func(t *testing.T) {
164 c.OpaqueRect = tc.rect
165 got := opaqueCover(&c, tc.full, tc.dst)
166 if got != tc.want {
167 t.Fatalf("opaqueCover = %v, want %v", got, tc.want)
168 }
169 })
170 }
171}
172
173// Building the draws of a window with a partly opaque child allocates
174// nothing.
175func TestSceneOpaqueRegionAllocations(t *testing.T) {
176 r, err := New(64, 48)
177 if err != nil {
178 t.Skipf("Vulkan unavailable: %v", err)
179 }
180 defer r.Close()
181 scene := ports.Scene{Seq: 1, Scale: 1.5, Background: "#000000", Windows: []ports.SceneWindow{{ID: 1, Rect: ports.Rect{W: 20, H: 12}}}}
182 contents := map[ports.WindowID]ports.SurfaceContent{1: opaqueRegionContent(t, 20, 12)}
183 bounds := image.Rect(0, 0, 64, 48)
184 frame := func() { _ = r.draws(scene, contents, r.frameDamage(r.target(), scene, bounds)) }
185 frame()
186 if allocs := testing.AllocsPerRun(50, frame); allocs != 0 {
187 t.Errorf("partly opaque child draws: %.1f allocs/frame, want 0", allocs)
188 }
189}
190
191// BenchmarkComposeOpaqueRegionChild composes a Firefox-like window moved every
192// frame as a workspace swipe does, at 2560x1600 and scale 1.3: a 2x shm root
193// of 984x1231 logical pixels under a dmabuf page of 1279x1600 pixels shown
194// at 984x1231 (the viewport destination). "none" is a page with no opaque
195// region (the root draws in full), "short" one with a region a logical pixel
196// short of its right and bottom edges, "whole" one that covers it. The GPU is
197// waited for after every frame. Select it with NEFERWL_VK_DEVICE.
198func BenchmarkComposeOpaqueRegionChild(b *testing.B) {
199 const w, h, lw, lh = 2560, 1600, 984, 1231
200 for _, tc := range []struct {
201 name string
202 opaque bool
203 rect ports.Rect
204 }{
205 {"none", false, ports.Rect{}},
206 {"short", false, ports.Rect{W: lw - 1, H: lh - 1}},
207 {"whole", true, ports.Rect{}},
208 } {
209 b.Run(tc.name, func(b *testing.B) {
210 r, err := New(w, h)
211 if err != nil {
212 b.Skipf("Vulkan unavailable: %v", err)
213 }
214 defer r.Close()
215 linear := ports.DMABufFormat{Format: fourcc('A', 'R', '2', '4'), Modifier: 0}
216 if !slices.Contains(r.DMABuf().Formats, linear) {
217 b.Skipf("linear ARGB8888 not importable")
218 }
219 const pw, ph = 1279, 1600
220 f := udmabuf(b, 1280, ph, func(x, y int) [4]byte { return [4]byte{byte(x), byte(y), 200, 255} })
221 page := ports.SurfaceContent{Width: pw, Height: ph, LogicalW: lw, LogicalH: lh, Surface: 2, Version: 1,
222 Opaque: tc.opaque, OpaqueRect: tc.rect,
223 DMABuf: &ports.DMABuf{ID: 9, Width: pw, Height: ph, Format: linear.Format, Planes: []ports.DMABufPlane{{File: f, Stride: 5120}}}}
224 root := solidContent(b, 2*lw, 2*lh, color.RGBA{R: 240, G: 240, B: 240, A: 255})
225 root.ID, root.Surface, root.Seq, root.Version = 1, 1, 1, 1
226 root.LogicalW, root.LogicalH = lw, lh
227 root.Opaque = true
228 root.Children = []ports.Subsurface{{SurfaceContent: page}}
229 contents := map[ports.WindowID]ports.SurfaceContent{1: root}
230 frame := func(i int) {
231 x := i % 64
232 scene := ports.Scene{Seq: uint64(i + 1), Scale: 1.3, Background: "#101010", Windows: []ports.SceneWindow{{ID: 1, Rect: ports.Rect{X: x, W: lw, H: lh}}}}
233 if err := render(r, scene, contents); err != nil {
234 b.Fatal(err)
235 }
236 if err := r.waitFrame(r.submitted); err != nil {
237 b.Fatal(err)
238 }
239 }
240 for i := range 8 {
241 frame(i)
242 }
243 b.ResetTimer()
244 start := time.Now()
245 for i := range b.N {
246 frame(i + 8)
247 }
248 b.ReportMetric(float64(time.Since(start).Microseconds())/1000/float64(b.N), "ms/frame")
249 })
250 }
251}
252
253// crossingCovers is a root of w×h pixels under n opaque 10×10 children
254// staggered over it, so that each cover crosses the root and the others.
255func crossingCovers(t testing.TB, w, h, n int) ports.SurfaceContent {
256 root := solidContent(t, w, h, color.RGBA{R: 255, A: 255})
257 root.ID, root.Surface, root.Seq, root.Version = 1, 1, 1, 1
258 for i := range n {
259 child := solidContent(t, 10, 10, color.RGBA{G: 255, A: 255})
260 child.Surface, child.Version, child.Opaque = uint64(2+i), 1, true
261 root.Children = append(root.Children, ports.Subsurface{SurfaceContent: child, X: (i * 7) % (w - 10), Y: (i * 5) % (h - 10)})
262 }
263 return root
264}
265
266// A client chooses how many opaque subsurfaces cross its root, and each one
267// cuts the root quad into strips: the split stops at maxSplitCovers covers
268// or maxSplitStrips draws, and the root then draws whole (overdraw only, the
269// covers above it still hide the rest).
270func TestSceneSplitIsBounded(t *testing.T) {
271 r, err := New(64, 48)
272 if err != nil {
273 t.Skipf("Vulkan unavailable: %v", err)
274 }
275 defer r.Close()
276 const w, h = 60, 40
277 scene := ports.Scene{Seq: 1, Scale: 1, Background: "#000000", Windows: []ports.SceneWindow{{ID: 1, Rect: ports.Rect{W: w, H: h}}}}
278 full := image.Rect(0, 0, w, h)
279 for _, n := range []int{1, maxSplitCovers, maxSplitCovers + 1, 40} {
280 contents := map[ports.WindowID]ports.SurfaceContent{1: crossingCovers(t, w, h, n)}
281 ds := r.draws(scene, contents, newDamage(r.target(), scene, image.Rect(0, 0, 64, 48)))
282 rects := drawRects(ds)
283 root := len(rects) - n // the children draw once each, after the root
284 if root < 1 || root > maxSplitStrips {
285 t.Errorf("%d covers: %d root draws, want 1..%d", n, root, maxSplitStrips)
286 }
287 if n > maxSplitCovers && (root != 1 || rects[0] != full) {
288 t.Errorf("%d covers: root draws %v, want the whole quad %v", n, rects[:root], full)
289 }
290 if n == 1 && root < 2 {
291 t.Errorf("one cover: %d root draws, want the quad split around it", root)
292 }
293 bounds := image.Rect(0, 0, 64, 48)
294 frame := func() { _ = r.draws(scene, contents, r.frameDamage(r.target(), scene, bounds)) }
295 frame()
296 if allocs := testing.AllocsPerRun(20, frame); allocs != 0 {
297 t.Errorf("%d covers: %.1f allocs/frame, want 0", n, allocs)
298 }
299 }
300}
301
302// With the split given up on, the covers above the whole root quad still
303// show: the window's pixels are the children's inside them and the root's
304// elsewhere.
305func TestRendererSplitBoundKeepsRoot(t *testing.T) {
306 r, err := New(64, 48)
307 if err != nil {
308 t.Skipf("Vulkan unavailable: %v", err)
309 }
310 defer r.Close()
311 const w, h, n = 60, 40, 40
312 scene := ports.Scene{Scale: 1, Background: "#000000", Windows: []ports.SceneWindow{{ID: 1, Rect: ports.Rect{W: w, H: h}}}}
313 root := crossingCovers(t, w, h, n)
314 if err := render(r, scene, map[ports.WindowID]ports.SurfaceContent{1: root}); err != nil {
315 t.Fatal(err)
316 }
317 got := readPixels(t, r)
318 red, green := color.RGBA{R: 255, A: 255}, color.RGBA{G: 255, A: 255}
319 for y := range h {
320 for x := range w {
321 want := red
322 for _, c := range root.Children {
323 if image.Pt(x, y).In(image.Rect(c.X, c.Y, c.X+10, c.Y+10)) {
324 want = green
325 }
326 }
327 if g := got.At(x, y); g != want {
328 t.Fatalf("At(%d,%d)=%v want %v", x, y, g, want)
329 }
330 }
331 }
332}