1package vulkan
2
3import (
4 "image"
5 "image/color"
6 "os"
7 "testing"
8
9 "github.com/bnema/neferwl/internal/ports"
10 "golang.org/x/sys/unix"
11)
12
13func TestRendererClear(t *testing.T) {
14 r, err := New(64, 48)
15 if err != nil {
16 t.Skipf("Vulkan unavailable: %v", err)
17 }
18 defer r.Close()
19 if err := r.Clear([3]uint8{0x10, 0x20, 0x30}); err != nil {
20 t.Fatal(err)
21 }
22 pixels := readPixels(t, r)
23 want := color.RGBA{0x10, 0x20, 0x30, 255}
24 for _, p := range [][2]int{{0, 0}, {63, 47}} {
25 if got := pixels.At(p[0], p[1]); got != want {
26 t.Errorf("pixel %v = %v, want %v", p, got, want)
27 }
28 }
29}
30
31func TestParseColor(t *testing.T) {
32 for _, tc := range []struct {
33 s string
34 want [3]uint8
35 }{{"#102030", [3]uint8{16, 32, 48}}, {"#FFFFFF", [3]uint8{255, 255, 255}}, {"bad", [3]uint8{}}, {"#12gg34", [3]uint8{}}, {"102030", [3]uint8{}}} {
36 if got := parseColor(tc.s); got != tc.want {
37 t.Errorf("parseColor(%q)=%v want %v", tc.s, got, tc.want)
38 }
39 }
40}
41
42func TestRendererRender(t *testing.T) {
43 r, err := New(64, 48)
44 if err != nil {
45 t.Skipf("Vulkan unavailable: %v", err)
46 }
47 defer r.Close()
48 bg := color.RGBA{16, 32, 48, 255}
49 wc, c2, fc := color.RGBA{200, 10, 10, 255}, color.RGBA{10, 200, 10, 255}, color.RGBA{10, 10, 200, 255}
50 contents := map[ports.WindowID]ports.SurfaceContent{
51 1: solidContent(t, 64, 48, wc), 2: solidContent(t, 64, 48, c2), 3: solidContent(t, 64, 48, fc),
52 }
53 check := func(s ports.Scene, expected map[image.Point]color.RGBA) {
54 t.Helper()
55 if err := render(r, s, contents); err != nil {
56 t.Fatal(err)
57 }
58 for p, want := range expected {
59 if got := readPixels(t, r).At(p.X, p.Y); got != want {
60 t.Errorf("pixel %v=%v want %v", p, got, want)
61 }
62 }
63 }
64 // The client sits inside its inset sides; separators are drawn over
65 // the windows in order, with the border colors.
66 win := ports.SceneWindow{ID: 1, Rect: ports.Rect{X: 8, Y: 8, W: 16, H: 16}, Inset: ports.SideAll}
67 gray, lit := color.RGBA{0x31, 0x32, 0x44, 255}, color.RGBA{0xb4, 0xbe, 0xfe, 255}
68 s := ports.Scene{Background: "#102030", Border: ports.Border{Width: 2, Active: "#b4befe", Inactive: "#313244"}, Windows: []ports.SceneWindow{win}}
69 check(s, map[image.Point]color.RGBA{{0, 0}: bg, {9, 9}: bg, {10, 10}: wc, {16, 16}: wc, {21, 21}: wc, {22, 22}: bg, {24, 24}: bg})
70 s.Separators = []ports.Separator{
71 {Rect: ports.Rect{X: 8, Y: 8, W: 16, H: 2}},
72 {Rect: ports.Rect{X: 8, Y: 8, W: 8, H: 2}, Active: true},
73 }
74 check(s, map[image.Point]color.RGBA{{9, 9}: lit, {15, 9}: lit, {16, 9}: gray, {23, 9}: gray, {10, 10}: wc})
75 s.Border.Inactive = ""
76 check(s, map[image.Point]color.RGBA{{9, 9}: lit, {16, 9}: bg})
77 // A float covers the tile lines; its own border is drawn with it.
78 s.Windows = []ports.SceneWindow{win, {ID: 3, Rect: ports.Rect{X: 12, Y: 4, W: 8, H: 8}, Floating: true, Inset: ports.SideAll}}
79 s.Separators = []ports.Separator{
80 {Rect: ports.Rect{X: 8, Y: 8, W: 16, H: 2}, Active: true},
81 {Rect: ports.Rect{X: 12, Y: 4, W: 8, H: 1}, Active: true, Window: 3},
82 }
83 check(s, map[image.Point]color.RGBA{{9, 9}: lit, {14, 9}: fc, {14, 4}: lit})
84 s.Windows = []ports.SceneWindow{win}
85 s.Separators, s.Border.Inactive = nil, "#313244"
86 s.Windows[0].Fullscreen = true
87 check(s, map[image.Point]color.RGBA{{9, 9}: wc})
88 s.Windows[0].Fullscreen = false
89 s.Windows[0] = ports.SceneWindow{ID: 2, Rect: ports.Rect{X: -10, Y: 0, W: 20, H: 10}}
90 check(s, map[image.Point]color.RGBA{{5, 5}: c2, {15, 5}: bg})
91 s.Windows[0].Hidden = true
92 check(s, map[image.Point]color.RGBA{{5, 5}: bg})
93}
94
95// A faded window blends with the background (wp_alpha_modifier_v1).
96func TestRendererFade(t *testing.T) {
97 r, err := New(64, 48)
98 if err != nil {
99 t.Skipf("Vulkan unavailable: %v", err)
100 }
101 defer r.Close()
102 c := solidContent(t, 64, 48, color.RGBA{200, 0, 0, 255})
103 c.Fade = 0.5
104 c.Opaque = false
105 s := ports.Scene{Background: "#000000", Windows: []ports.SceneWindow{{ID: 1, Rect: ports.Rect{W: 64, H: 48}}}}
106 if err := render(r, s, map[ports.WindowID]ports.SurfaceContent{1: c}); err != nil {
107 t.Fatal(err)
108 }
109 if got := readPixels(t, r).At(10, 10).(color.RGBA); got.R < 99 || got.R > 101 || got.G != 0 || got.B != 0 {
110 t.Fatalf("faded pixel %v", got)
111 }
112}
113
114// A pulsing window lifts toward white: black the most, white not at all.
115func TestRendererPulse(t *testing.T) {
116 r, err := New(64, 48)
117 if err != nil {
118 t.Skipf("Vulkan unavailable: %v", err)
119 }
120 defer r.Close()
121 for i, tc := range []struct {
122 in, want uint8
123 }{{0, 26}, {64, 83}, {200, 206}, {255, 255}} {
124 // One window per case: the shm copy is cached per window.
125 id := ports.WindowID(i + 1)
126 c := solidContent(t, 64, 48, color.RGBA{tc.in, tc.in, tc.in, 255})
127 s := ports.Scene{Background: "#000000", Windows: []ports.SceneWindow{{ID: id, Rect: ports.Rect{W: 64, H: 48}, FocusEffect: 0.1}}}
128 if err := render(r, s, map[ports.WindowID]ports.SurfaceContent{id: c}); err != nil {
129 t.Fatal(err)
130 }
131 got := readPixels(t, r).At(10, 10).(color.RGBA)
132 if d := int(got.R) - int(tc.want); d < -1 || d > 1 || got.R != got.G || got.G != got.B {
133 t.Errorf("pulse of %d = %v, want %d", tc.in, got, tc.want)
134 }
135 }
136}
137
138func TestRendererContents(t *testing.T) {
139 r, err := New(64, 48)
140 if err != nil {
141 t.Skipf("Vulkan unavailable: %v", err)
142 }
143 defer r.Close()
144 bg := color.RGBA{16, 32, 48, 255}
145 scene := ports.Scene{Background: "#102030", Windows: []ports.SceneWindow{{ID: 1, Rect: ports.Rect{X: 8, Y: 8, W: 20, H: 10}}}}
146 pixels := make([]byte, 4*2*4)
147 for i := 0; i < len(pixels); i += 4 {
148 copy(pixels[i:i+4], []byte{0, 0, 255, 255})
149 }
150 render := func(c *ports.SurfaceContent) {
151 t.Helper()
152 var contents map[ports.WindowID]ports.SurfaceContent
153 if c != nil {
154 contents = map[ports.WindowID]ports.SurfaceContent{1: *c}
155 }
156 if err := render(r, scene, contents); err != nil {
157 t.Fatal(err)
158 }
159 }
160 check := func(x, y int, want color.RGBA) {
161 t.Helper()
162 if got := readPixels(t, r).At(x, y); got != want {
163 t.Errorf("At(%d,%d)=%v want %v", x, y, got, want)
164 }
165 }
166 render(shmContent(t, 4, 2, 16, pixels))
167 check(8, 8, color.RGBA{255, 0, 0, 255})
168 check(11, 9, color.RGBA{255, 0, 0, 255})
169 check(12, 8, bg)
170 check(0, 0, bg)
171 scene.Windows[0].Rect = ports.Rect{X: -2, Y: 0, W: 10, H: 10}
172 padded := make([]byte, 4*20)
173 copy(padded[8:12], []byte{7, 11, 23, 255})
174 render(shmContent(t, 4, 4, 20, padded))
175 check(0, 0, color.RGBA{23, 11, 7, 255})
176 // A window without a buffer yet shows the background, not a color.
177 render(nil)
178 check(0, 0, bg)
179}
180
181func TestRendererUploadOrderAndOpaque(t *testing.T) {
182 r, err := New(32, 32)
183 if err != nil {
184 t.Skipf("Vulkan unavailable: %v", err)
185 }
186 defer r.Close()
187 scene := ports.Scene{Background: "#102030", Windows: []ports.SceneWindow{
188 {ID: 1, Rect: ports.Rect{X: 2, Y: 2, W: 16, H: 16}},
189 {ID: 2, Rect: ports.Rect{X: 8, Y: 8, W: 16, H: 16}},
190 }}
191 top := color.RGBA{10, 200, 10, 255}
192 upper := map[ports.WindowID]ports.SurfaceContent{1: solidContent(t, 16, 16, color.RGBA{200, 10, 10, 255}), 2: solidContent(t, 16, 16, top)}
193 for id, c := range upper {
194 c.Version = 1 // the opaque buffer below is another commit
195 upper[id] = c
196 }
197 if err := render(r, scene, upper); err != nil {
198 t.Fatal(err)
199 }
200 if got, want := readPixels(t, r).RGBAAt(10, 10), top; got != want {
201 t.Errorf("overlap = %v, want %v", got, want)
202 }
203 scene.Border = ports.Border{Width: 4, Active: "#ffffff"}
204 scene.Windows = []ports.SceneWindow{{ID: 1, Rect: ports.Rect{X: 2, Y: 2, W: 16, H: 16}, Focused: true, Inset: ports.SideAll}}
205 scene.Separators = []ports.Separator{{Rect: ports.Rect{X: 2, Y: 2, W: 16, H: 4}, Active: true}}
206 px := make([]byte, 16*16*4)
207 for i := 0; i < len(px); i += 4 {
208 copy(px[i:i+4], []byte{3, 5, 7, 0})
209 }
210 opaque := shmContent(t, 16, 16, 64, px)
211 opaque.Opaque = true
212 contents := map[ports.WindowID]ports.SurfaceContent{1: *opaque}
213 if err := render(r, scene, contents); err != nil {
214 t.Fatal(err)
215 }
216 for _, tc := range []struct {
217 x, y int
218 want color.RGBA
219 }{{3, 3, color.RGBA{255, 255, 255, 255}}, {10, 10, color.RGBA{7, 5, 3, 255}}} {
220 if got := readPixels(t, r).RGBAAt(tc.x, tc.y); got != tc.want {
221 t.Errorf("pixel (%d,%d) = %v, want %v", tc.x, tc.y, got, tc.want)
222 }
223 }
224}
225
226func TestRendererLayers(t *testing.T) {
227 r, err := New(64, 48)
228 if err != nil {
229 t.Skipf("Vulkan unavailable: %v", err)
230 }
231 defer r.Close()
232 red := make([]byte, 64*8*4)
233 for i := 0; i < len(red); i += 4 {
234 copy(red[i:i+4], []byte{0, 0, 255, 0})
235 }
236 layer := shmContent(t, 64, 8, 64*4, red)
237 layer.ID, layer.Opaque = 2, true
238 contents := map[ports.WindowID]ports.SurfaceContent{2: *layer}
239 scene := ports.Scene{Background: "#102030", Windows: []ports.SceneWindow{{ID: 1, Rect: ports.Rect{W: 64, H: 48}}}, Layers: []ports.SceneLayer{{ID: 2, Layer: ports.LayerTop, Rect: ports.Rect{W: 64, H: 8}}}}
240 window := color.RGBA{16, 32, 48, 255} // no buffer yet: background
241 check := func(want color.RGBA) {
242 t.Helper()
243 if err := render(r, scene, contents); err != nil {
244 t.Fatal(err)
245 }
246 if got := readPixels(t, r).RGBAAt(5, 2); got != want {
247 t.Errorf("pixel = %v, want %v", got, want)
248 }
249 }
250 check(color.RGBA{255, 0, 0, 255})
251 scene.Layers[0].Layer = ports.LayerBottom
252 check(window)
253 // Core leaves hidden layers out of the scene; the renderer draws what
254 // it gets, over a fullscreen window too.
255 scene.Windows[0].Fullscreen = true
256 scene.Layers[0].Layer = ports.LayerOverlay
257 check(color.RGBA{255, 0, 0, 255})
258 scene.Windows[0].Hidden = true
259 scene.Layers[0].Layer = ports.LayerBottom
260 check(color.RGBA{255, 0, 0, 255})
261 delete(contents, 2)
262 check(color.RGBA{16, 32, 48, 255})
263}
264
265// solid returns a w×h B8G8R8A8 buffer of one color.
266func solid(w, h int, c [3]uint8) []byte {
267 p := make([]byte, w*h*4)
268 for i := 0; i < len(p); i += 4 {
269 p[i], p[i+1], p[i+2], p[i+3] = c[2], c[1], c[0], 255
270 }
271 return p
272}
273
274func TestRendererScale(t *testing.T) {
275 r, err := New(64, 48)
276 if err != nil {
277 t.Skipf("Vulkan unavailable: %v", err)
278 }
279 defer r.Close()
280 red := [3]uint8{200, 0, 0}
281 // Scale 2: a 10x10 logical window at (4,4) covers physical (8,8)-(28,28);
282 // its 20x20 buffer (fractional client) is copied 1:1.
283 s := ports.Scene{Scale: 2, Background: "#000000", Windows: []ports.SceneWindow{{ID: 1, Rect: ports.Rect{X: 4, Y: 4, W: 10, H: 10}}}}
284 scaled := shmContent(t, 20, 20, 80, solid(20, 20, red))
285 scaled.ID, scaled.LogicalW, scaled.LogicalH = 1, 10, 10
286 content := map[ports.WindowID]ports.SurfaceContent{1: *scaled}
287 if err := render(r, s, content); err != nil {
288 t.Fatal(err)
289 }
290 px := readPixels(t, r)
291 want := color.RGBA{200, 0, 0, 255}
292 for _, p := range []image.Point{{8, 8}, {27, 27}} {
293 if got := px.At(p.X, p.Y); got != want {
294 t.Errorf("%v = %v", p, got)
295 }
296 }
297 for _, p := range []image.Point{{7, 7}, {28, 28}} {
298 if got := px.At(p.X, p.Y); got != (color.RGBA{0, 0, 0, 255}) {
299 t.Errorf("outside %v = %v", p, got)
300 }
301 }
302 // Scale 1.5 with an integer-scale client (buffer scale 2): the 20x20
303 // buffer is 10x10 logical and downscaled to 15x15 physical at (6,6).
304 s.Scale = 1.5
305 if err := render(r, s, content); err != nil {
306 t.Fatal(err)
307 }
308 px = readPixels(t, r)
309 for _, p := range []image.Point{{6, 6}, {20, 20}} {
310 if got := px.At(p.X, p.Y); got != want {
311 t.Errorf("1.5 %v = %v", p, got)
312 }
313 }
314 if got := px.At(21, 21); got != (color.RGBA{0, 0, 0, 255}) {
315 t.Errorf("1.5 outside = %v", got)
316 }
317 // A buffer 1px wider than its slot is clipped, not resampled.
318 odd := shmContent(t, 16, 15, 64, solid(16, 15, red))
319 odd.ID, odd.LogicalW, odd.LogicalH = 1, 10, 10
320 content[1] = *odd
321 if err := render(r, s, content); err != nil {
322 t.Fatal(err)
323 }
324 if got := readPixels(t, r).At(21, 10); got != (color.RGBA{0, 0, 0, 255}) {
325 t.Errorf("clipped column = %v", got)
326 }
327}
328
329// An overview preview draws the whole buffer shrunk into its rect, not
330// the buffer's corner.
331func TestRendererPreview(t *testing.T) {
332 r, err := New(64, 48)
333 if err != nil {
334 t.Skipf("Vulkan unavailable: %v", err)
335 }
336 defer r.Close()
337 // 40x20 buffer: left half red, right half blue, drawn at half size.
338 px := make([]byte, 40*20*4)
339 for i := 0; i < len(px); i += 4 {
340 if (i/4)%40 < 20 {
341 px[i+2] = 200
342 } else {
343 px[i] = 200
344 }
345 px[i+3] = 255
346 }
347 c := shmContent(t, 40, 20, 160, px)
348 c.ID = 1
349 s := ports.Scene{Scale: 1, Background: "#000000", Windows: []ports.SceneWindow{{ID: 1, Rect: ports.Rect{X: 4, Y: 4, W: 20, H: 10}, Preview: 0.5}}}
350 if err := render(r, s, map[ports.WindowID]ports.SurfaceContent{1: *c}); err != nil {
351 t.Fatal(err)
352 }
353 out := readPixels(t, r)
354 for p, want := range map[image.Point]color.RGBA{
355 {5, 5}: {200, 0, 0, 255},
356 {22, 12}: {0, 0, 200, 255},
357 {25, 5}: {0, 0, 0, 255},
358 {5, 15}: {0, 0, 0, 255},
359 } {
360 if got := out.At(p.X, p.Y); got != want {
361 t.Errorf("%v = %v, want %v", p, got, want)
362 }
363 }
364}
365
366// A buffer shrunk under half size averages its texels: one-pixel white
367// and black columns shrunk 4× come out an even gray, not a beat pattern.
368func TestRendererPreviewSmooth(t *testing.T) {
369 r, err := New(64, 48)
370 if err != nil {
371 t.Skipf("Vulkan unavailable: %v", err)
372 }
373 defer r.Close()
374 px := make([]byte, 80*40*4)
375 for i := 0; i < len(px); i += 4 {
376 if (i/4)%2 == 0 {
377 px[i], px[i+1], px[i+2] = 255, 255, 255
378 }
379 px[i+3] = 255
380 }
381 c := shmContent(t, 80, 40, 320, px)
382 c.ID = 1
383 s := ports.Scene{Scale: 1, Background: "#000000", Windows: []ports.SceneWindow{{ID: 1, Rect: ports.Rect{W: 20, H: 10}, Preview: 0.25}}}
384 if err := render(r, s, map[ports.WindowID]ports.SurfaceContent{1: *c}); err != nil {
385 t.Fatal(err)
386 }
387 out := readPixels(t, r)
388 for x := 1; x < 19; x++ {
389 if v := out.RGBAAt(x, 5).R; v < 100 || v > 155 {
390 t.Fatalf("x=%d: %d, want mid gray", x, v)
391 }
392 }
393}
394
395// solidContent is a w×h B8G8R8A8 buffer of one color.
396func solidContent(t testing.TB, w, h int, c color.RGBA) ports.SurfaceContent {
397 px := make([]byte, w*h*4)
398 for i := 0; i < len(px); i += 4 {
399 px[i], px[i+1], px[i+2], px[i+3] = c.B, c.G, c.R, 255
400 }
401 return *shmContent(t, w, h, w*4, px)
402}
403
404// shmContent puts pixels in a memfd pool, like a wl_shm client.
405func shmContent(t testing.TB, w, h, stride int, pixels []byte) *ports.SurfaceContent {
406 t.Helper()
407 fd, err := unix.MemfdCreate("shm-test", unix.MFD_CLOEXEC)
408 if err != nil {
409 t.Fatal(err)
410 }
411 f := os.NewFile(uintptr(fd), "shm-test")
412 t.Cleanup(func() { f.Close() })
413 if _, err := f.Write(pixels); err != nil {
414 t.Fatal(err)
415 }
416 shmPools++
417 return &ports.SurfaceContent{Width: w, Height: h, SHM: &ports.SHMBuffer{Pool: shmPools, File: f, Stride: stride}}
418}
419
420// shmPools numbers test pools: a renderer maps each pool ID once.
421var shmPools uint64
422
423// The window geometry lands on the window rect and the client shadow
424// around it is clipped; subsurfaces draw from the root origin, above or
425// below it.
426func TestRendererGeometryAndSubsurfaces(t *testing.T) {
427 r, err := New(64, 48)
428 if err != nil {
429 t.Skipf("Vulkan unavailable: %v", err)
430 }
431 defer r.Close()
432 red, green, blue := color.RGBA{255, 0, 0, 255}, color.RGBA{0, 255, 0, 255}, color.RGBA{0, 0, 255, 255}
433 bg := color.RGBA{16, 32, 48, 255}
434 scene := ports.Scene{Background: "#102030", Windows: []ports.SceneWindow{{ID: 1, Rect: ports.Rect{X: 10, Y: 10, W: 20, H: 10}}}}
435 // A 30×20 surface whose window is the 20×10 at (5, 5): a 5px shadow.
436 root := solidContent(t, 30, 20, red)
437 root.Surface, root.Version = 1, 1
438 root.Geometry = ports.Rect{X: 5, Y: 5, W: 20, H: 10}
439 // Green covers the window's top-left 4×4 corner; blue sits below the
440 // root, hidden by it.
441 below := solidContent(t, 30, 20, blue)
442 below.Surface, below.Version = 2, 1
443 above := solidContent(t, 4, 4, green)
444 above.Surface, above.Version = 3, 1
445 root.Children = []ports.Subsurface{
446 {X: 3, Y: 3, Below: true, SurfaceContent: below},
447 {X: 5, Y: 5, SurfaceContent: above},
448 }
449 if err := render(r, scene, map[ports.WindowID]ports.SurfaceContent{1: root}); err != nil {
450 t.Fatal(err)
451 }
452 px := readPixels(t, r)
453 for _, c := range []struct {
454 x, y int
455 want color.RGBA
456 }{
457 {10, 10, green}, {13, 13, green}, {14, 14, red}, {29, 19, red},
458 // The shadow and the child reaching past it are clipped.
459 {9, 10, bg}, {30, 19, bg}, {29, 20, bg},
460 } {
461 if got := px.At(c.x, c.y); got != c.want {
462 t.Errorf("At(%d,%d)=%v want %v", c.x, c.y, got, c.want)
463 }
464 }
465 // A root with no buffer of its own still shows its children.
466 only := ports.SurfaceContent{Children: []ports.Subsurface{{SurfaceContent: solidContent(t, 20, 10, blue)}}}
467 if err := render(r, scene, map[ports.WindowID]ports.SurfaceContent{1: only}); err != nil {
468 t.Fatal(err)
469 }
470 if got := readPixels(t, r).At(20, 15); got != blue {
471 t.Errorf("child only: %v", got)
472 }
473}
474
475// A tall fill covers every row through the last one.
476func TestRendererTallFill(t *testing.T) {
477 h := 135
478 r, err := New(8, h)
479 if err != nil {
480 t.Skipf("Vulkan unavailable: %v", err)
481 }
482 defer r.Close()
483 scene := ports.Scene{Background: "#102030", Windows: []ports.SceneWindow{{ID: 1, Rect: ports.Rect{X: 1, Y: 0, W: 6, H: h}}}}
484 wc := color.RGBA{200, 10, 10, 255}
485 if err := render(r, scene, map[ports.WindowID]ports.SurfaceContent{1: solidContent(t, 6, h, wc)}); err != nil {
486 t.Fatal(err)
487 }
488 bg := color.RGBA{16, 32, 48, 255}
489 px := readPixels(t, r)
490 for y := 0; y < h; y++ {
491 if got := px.At(1, y); got != wc {
492 t.Fatalf("At(1,%d)=%v want %v", y, got, wc)
493 }
494 if got := px.At(6, y); got != wc {
495 t.Fatalf("At(6,%d)=%v want %v", y, got, wc)
496 }
497 if got := px.At(7, y); got != bg {
498 t.Fatalf("At(7,%d)=%v want background", y, got)
499 }
500 }
501}
502
503// fill returns a w×h premultiplied B8G8R8A8 buffer of one pixel.
504func fill(w, h int, bgra [4]byte) []byte {
505 p := make([]byte, w*h*4)
506 for i := 0; i < len(p); i += 4 {
507 copy(p[i:i+4], bgra[:])
508 }
509 return p
510}
511
512// Translucent buffers blend over what is below them, in paint order: a
513// bottom layer (one draw), an opaque window copied over it, then two
514// overlapping overlays sharing one draw. A transparent pixel changes nothing.
515func TestRendererAlphaBlend(t *testing.T) {
516 r, err := New(64, 48)
517 if err != nil {
518 t.Skipf("Vulkan unavailable: %v", err)
519 }
520 defer r.Close()
521 halfRed, halfBlue := [4]byte{0, 0, 128, 128}, [4]byte{128, 0, 0, 128}
522 partly := fill(16, 8, halfRed)
523 for y := range 8 {
524 copy(partly[y*64+48:y*64+64], make([]byte, 16)) // last 4 pixels: transparent
525 }
526 white := shmContent(t, 32, 48, 32*4, solid(32, 48, [3]uint8{255, 255, 255}))
527 white.Opaque = true
528 contents := map[ports.WindowID]ports.SurfaceContent{
529 1: *white,
530 2: *shmContent(t, 64, 48, 64*4, fill(64, 48, halfRed)),
531 3: *shmContent(t, 16, 8, 16*4, partly),
532 4: *shmContent(t, 16, 8, 16*4, fill(16, 8, halfBlue)),
533 }
534 scene := ports.Scene{Background: "#000000",
535 Windows: []ports.SceneWindow{{ID: 1, Rect: ports.Rect{W: 32, H: 48}}},
536 Layers: []ports.SceneLayer{
537 {ID: 2, Layer: ports.LayerBottom, Rect: ports.Rect{W: 64, H: 48}},
538 {ID: 3, Layer: ports.LayerOverlay, Rect: ports.Rect{W: 16, H: 8}},
539 {ID: 4, Layer: ports.LayerOverlay, Rect: ports.Rect{X: 8, W: 16, H: 8}},
540 }}
541 if err := render(r, scene, contents); err != nil {
542 t.Fatal(err)
543 }
544 px := readPixels(t, r)
545 near := func(a, b uint8) bool { d := int(a) - int(b); return d >= -1 && d <= 1 }
546 for _, tc := range []struct {
547 name string
548 x, y int
549 r, g, b uint8
550 }{
551 {"bottom layer over background", 40, 20, 128, 0, 0},
552 {"opaque window over bottom layer", 20, 20, 255, 255, 255},
553 {"red overlay over window", 4, 4, 255, 127, 127},
554 {"blue over red over window", 10, 4, 127, 63, 191},
555 {"blue over window", 20, 4, 127, 127, 255},
556 {"transparent red pixels: blue over window only", 14, 4, 127, 127, 255},
557 } {
558 got := px.RGBAAt(tc.x, tc.y)
559 if !near(got.R, tc.r) || !near(got.G, tc.g) || !near(got.B, tc.b) {
560 t.Errorf("%s: pixel (%d,%d) = %v, want ~{%d %d %d}", tc.name, tc.x, tc.y, got, tc.r, tc.g, tc.b)
561 }
562 }
563}
564
565// A window kept after its client closed (a closing window fading out) still
566// draws from the GPU copy of its content: the pool file is closed and its
567// mapping gone, and the copy is found without reading the pool.
568func TestRendererSHMKeptWindowDrawsFromCopy(t *testing.T) {
569 r, err := New(32, 32)
570 if err != nil {
571 t.Skipf("Vulkan unavailable: %v", err)
572 }
573 defer r.Close()
574 scene := ports.Scene{Scale: 1, Background: "#000000", Windows: []ports.SceneWindow{{ID: 1, Rect: ports.Rect{W: 16, H: 16}}}}
575 c := solidContent(t, 16, 16, color.RGBA{R: 255, A: 255})
576 c.ID, c.Surface, c.Seq, c.Version = 1, 1, 1, 1
577 if err := render(r, scene, map[ports.WindowID]ports.SurfaceContent{1: c}); err != nil {
578 t.Fatal(err)
579 }
580 // Wayland closed the pool; the mapping was dropped meanwhile.
581 if err := c.SHM.File.Close(); err != nil {
582 t.Fatal(err)
583 }
584 r.dropPools()
585 for pool, m := range r.pools {
586 _ = unix.Munmap(m.data)
587 delete(r.pools, pool)
588 }
589 // The window moves (a fade-out), so it draws again.
590 scene.Seq = 2
591 scene.Windows[0].Rect.X = 4
592 if err := render(r, scene, map[ports.WindowID]ports.SurfaceContent{1: c}); err != nil {
593 t.Fatal(err)
594 }
595 got := readPixels(t, r)
596 if g, want := got.At(18, 8), (color.RGBA{R: 255, A: 255}); g != want {
597 t.Fatalf("kept window pixel %v, want %v", g, want)
598 }
599 if g, want := got.At(1, 8), (color.RGBA{A: 255}); g != want {
600 t.Fatalf("background pixel %v, want %v", g, want)
601 }
602}