1package vulkan
2
3import (
4 "context"
5 "image"
6 "image/color"
7 "os"
8 "slices"
9 "testing"
10 "time"
11
12 "github.com/bnema/neferwl/internal/adapters/syncfile"
13 "github.com/bnema/neferwl/internal/ports"
14)
15
16// near reports whether two colors are within tol per channel.
17func near(a, b color.RGBA, tol int) bool {
18 d := func(x, y uint8) bool { v := int(x) - int(y); return v >= -tol && v <= tol }
19 return d(a.R, b.R) && d(a.G, b.G) && d(a.B, b.B)
20}
21
22// Scaled wl_shm pixels filter linearly: halfway between a black and a
23// white texel is grey, not either one (nearest neighbour).
24func TestRendererScaledSHMFiltersLinearly(t *testing.T) {
25 r, err := New(64, 16)
26 if err != nil {
27 t.Skipf("Vulkan unavailable: %v", err)
28 }
29 defer r.Close()
30 // 2×1 buffer: black, white; drawn 64×16, texel centres at x=16, x=48.
31 px := []byte{0, 0, 0, 255, 255, 255, 255, 255}
32 c := shmContent(t, 2, 1, 8, px)
33 c.LogicalW, c.LogicalH = 64, 16
34 scene := ports.Scene{Background: "#ff0000", Windows: []ports.SceneWindow{{ID: 1, Rect: ports.Rect{W: 64, H: 16}}}}
35 if err := render(r, scene, map[ports.WindowID]ports.SurfaceContent{1: *c}); err != nil {
36 t.Fatal(err)
37 }
38 got := readPixels(t, r).RGBAAt(32, 8)
39 if !near(got, color.RGBA{128, 128, 128, 255}, 8) {
40 t.Fatalf("midpoint %v, want grey", got)
41 }
42 if got := readPixels(t, r).RGBAAt(2, 8); !near(got, color.RGBA{0, 0, 0, 255}, 2) {
43 t.Fatalf("left edge %v, want black (clamped)", got)
44 }
45}
46
47func TestRendererSourceCrop(t *testing.T) {
48 r, err := New(16, 8)
49 if err != nil {
50 t.Skipf("Vulkan unavailable: %v", err)
51 }
52 defer r.Close()
53 px := []byte{0, 0, 255, 255, 0, 255, 0, 255, 255, 0, 0, 255, 255, 255, 255, 255}
54 c := shmContent(t, 4, 1, 16, px)
55 c.LogicalW, c.LogicalH = 16, 8
56 c.Source = [4]float32{1, 0, 2, 1}
57 scene := ports.Scene{Windows: []ports.SceneWindow{{ID: 1, Rect: ports.Rect{W: 16, H: 8}}}}
58 if err := render(r, scene, map[ports.WindowID]ports.SurfaceContent{1: *c}); err != nil {
59 t.Fatal(err)
60 }
61 for _, tc := range []struct {
62 x int
63 want color.RGBA
64 }{{0, color.RGBA{0, 255, 0, 255}}, {15, color.RGBA{0, 0, 255, 255}}} {
65 if got := readPixels(t, r).RGBAAt(tc.x, 4); !near(got, tc.want, 5) {
66 t.Errorf("crop x=%d: %v want %v", tc.x, got, tc.want)
67 }
68 }
69}
70
71// Scaled dmabufs are sampled with the linear sampler.
72func TestRendererScaledDMABufFiltersLinearly(t *testing.T) {
73 r, err := New(128, 4)
74 if err != nil {
75 t.Skipf("Vulkan unavailable: %v", err)
76 }
77 defer r.Close()
78 linear := ports.DMABufFormat{Format: fourcc('A', 'R', '2', '4'), Modifier: 0}
79 if !slices.Contains(r.DMABuf().Formats, linear) {
80 t.Skipf("linear ARGB8888 not importable")
81 }
82 // 64×1: left half black, right half white, drawn twice as wide.
83 f := udmabuf(t, 64, 1, func(x, _ int) [4]byte {
84 if x < 32 {
85 return [4]byte{0, 0, 0, 255}
86 }
87 return [4]byte{255, 255, 255, 255}
88 })
89 buf := &ports.DMABuf{ID: 3, Width: 64, Height: 1, Format: linear.Format, Planes: []ports.DMABufPlane{{File: f, Stride: 256}}}
90 scene := ports.Scene{Background: "#ff0000", Windows: []ports.SceneWindow{{ID: 1, Rect: ports.Rect{W: 128, H: 4}}}}
91 c := ports.SurfaceContent{ID: 1, Width: 64, Height: 1, LogicalW: 128, LogicalH: 4, DMABuf: buf}
92 if err := render(r, scene, map[ports.WindowID]ports.SurfaceContent{1: c}); err != nil {
93 t.Fatal(err)
94 }
95 // Target x=64 (centre 64.5) maps to buffer 32.25: between texel 31
96 // (black, centre 31.5) and 32 (white, centre 32.5), 75% white.
97 if got := readPixels(t, r).RGBAAt(64, 2); !near(got, color.RGBA{191, 191, 191, 255}, 10) {
98 t.Fatalf("boundary %v, want a linear mix", got)
99 }
100}
101
102// Render returns a sync file that becomes readable when the GPU is done.
103func TestRendererReturnsDoneFence(t *testing.T) {
104 r, err := New(32, 32)
105 if err != nil {
106 t.Skipf("Vulkan unavailable: %v", err)
107 }
108 defer r.Close()
109 if !r.syncFD {
110 t.Skip("device cannot export sync files")
111 }
112 done, err := r.Render(ports.Scene{Background: "#102030"}, nil)
113 if err != nil {
114 t.Fatal(err)
115 }
116 if done == nil {
117 t.Fatal("no done fence")
118 }
119 defer done.Close()
120 ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
121 defer cancel()
122 if err := syncfile.Wait(ctx, done); err != nil {
123 t.Fatal(err)
124 }
125}
126
127// An unchanged wl_shm window is not copied again; a new content is.
128func TestRendererSHMCopiedOncePerContent(t *testing.T) {
129 r, err := New(32, 32)
130 if err != nil {
131 t.Skipf("Vulkan unavailable: %v", err)
132 }
133 defer r.Close()
134 c := solidContent(t, 16, 16, color.RGBA{1, 2, 3, 255})
135 c.ID, c.Seq, c.Version = 1, 1, 1
136 scene := ports.Scene{Background: "#000000", Windows: []ports.SceneWindow{{ID: 1, Rect: ports.Rect{W: 16, H: 16}}}}
137 contents := map[ports.WindowID]ports.SurfaceContent{1: c}
138 render := func() {
139 t.Helper()
140 done, err := r.Render(scene, contents)
141 if done != nil {
142 done.Close()
143 }
144 if err != nil {
145 t.Fatal(err)
146 }
147 }
148 render()
149 if r.copied != 16*16*4 {
150 t.Fatalf("first frame copied %d", r.copied)
151 }
152 for range 3 {
153 render()
154 }
155 if r.copied != 16*16*4 {
156 t.Fatalf("unchanged content copied again: %d", r.copied)
157 }
158 c.Seq, c.Version = 2, 2
159 contents[1] = c
160 render()
161 if r.copied != 2*16*16*4 {
162 t.Fatalf("new content copied %d", r.copied)
163 }
164}
165
166// A surface fully hidden by an opaque subsurface above it is not copied:
167// only the subsurface's bytes are, and the frame shows it.
168func TestRendererSkipsOccludedSurface(t *testing.T) {
169 r, err := New(32, 32)
170 if err != nil {
171 t.Skipf("Vulkan unavailable: %v", err)
172 }
173 defer r.Close()
174 green := color.RGBA{0, 255, 0, 255}
175 root := solidContent(t, 16, 16, color.RGBA{255, 0, 0, 255})
176 root.ID, root.Seq, root.Surface, root.Version = 1, 1, 1, 1
177 game := solidContent(t, 16, 16, green)
178 game.Surface, game.Version = 2, 1
179 game.Opaque = true
180 root.Children = []ports.Subsurface{{SurfaceContent: game}}
181 scene := ports.Scene{Background: "#000000", Windows: []ports.SceneWindow{{ID: 1, Rect: ports.Rect{W: 16, H: 16}}}}
182 if err := render(r, scene, map[ports.WindowID]ports.SurfaceContent{1: root}); err != nil {
183 t.Fatal(err)
184 }
185 if r.copied != 16*16*4 {
186 t.Fatalf("copied %d bytes, want only the subsurface's %d", r.copied, 16*16*4)
187 }
188 if got := readPixels(t, r).At(8, 8); got != green {
189 t.Fatalf("At(8,8)=%v want %v", got, green)
190 }
191 // A smaller opaque child leaves part of the root visible: it is copied.
192 small := solidContent(t, 8, 8, green)
193 small.Surface, small.Version = 3, 1
194 small.Opaque = true
195 root.Seq = 2
196 root.Children = []ports.Subsurface{{SurfaceContent: small}}
197 r.copied = 0
198 if err := render(r, scene, map[ports.WindowID]ports.SurfaceContent{1: root}); err != nil {
199 t.Fatal(err)
200 }
201 if r.copied != 16*16*4+8*8*4 {
202 t.Fatalf("partly hidden root: copied %d", r.copied)
203 }
204}
205
206// A covering child whose buffer will not draw does not hide the root:
207// an unimportable dmabuf still shows the surface below it, not a hole.
208func TestRendererKeepsSurfaceBelowFailedCover(t *testing.T) {
209 r, err := New(32, 32)
210 if err != nil {
211 t.Skipf("Vulkan unavailable: %v", err)
212 }
213 defer r.Close()
214 red := color.RGBA{255, 0, 0, 255}
215 root := solidContent(t, 16, 16, red)
216 root.ID, root.Seq = 1, 1
217 bad := &ports.DMABuf{ID: 9, Width: 16, Height: 16, Format: 0xdeadbeef}
218 root.Children = []ports.Subsurface{{SurfaceContent: ports.SurfaceContent{Width: 16, Height: 16, Opaque: true, DMABuf: bad}}}
219 scene := ports.Scene{Background: "#000000", Windows: []ports.SceneWindow{{ID: 1, Rect: ports.Rect{W: 16, H: 16}}}}
220 if err := render(r, scene, map[ports.WindowID]ports.SurfaceContent{1: root}); err != nil {
221 t.Fatal(err)
222 }
223 if r.copied != 16*16*4 {
224 t.Fatalf("copied %d bytes, want the root's %d", r.copied, 16*16*4)
225 }
226 if got := readPixels(t, r).At(8, 8); got != red {
227 t.Fatalf("At(8,8)=%v want %v", got, red)
228 }
229}
230
231// render draws a frame and closes its fence.
232func render(r *Renderer, s ports.Scene, contents map[ports.WindowID]ports.SurfaceContent) error {
233 done, err := r.Render(s, contents)
234 if done != nil {
235 done.Close()
236 }
237 return err
238}
239
240// A wl_shm window's GPU buffer is rewritten only after every frame that
241// read it completed: contents 1, 2, 3 back to back alternate two buffers,
242// and the third copy waits for the frame that drew the first (the slot
243// ring and shmCopyFor's waitFrame both guarantee it).
244func TestRendererSHMDoubleBufferWaitsForReaders(t *testing.T) {
245 r, err := New(32, 32)
246 if err != nil {
247 t.Skipf("Vulkan unavailable: %v", err)
248 }
249 defer r.Close()
250 if !r.syncFD {
251 t.Skip("frames are finished on return without sync files")
252 }
253 scene := ports.Scene{Background: "#000000", Windows: []ports.SceneWindow{{ID: 1, Rect: ports.Rect{W: 16, H: 16}}}}
254 var readers []uint64
255 for seq := uint64(1); seq <= 3; seq++ {
256 c := solidContent(t, 16, 16, color.RGBA{uint8(seq * 60), 0, 0, 255})
257 c.ID, c.Seq, c.Version = 1, seq, seq
258 if seq == 3 {
259 // The buffer about to be reused was last read by frame 1.
260 if r.completed >= readers[0] {
261 t.Fatalf("frame %d already known done before the reuse", readers[0])
262 }
263 }
264 if err := render(r, scene, map[ports.WindowID]ports.SurfaceContent{1: c}); err != nil {
265 t.Fatal(err)
266 }
267 readers = append(readers, r.frame)
268 if seq == 3 && r.completed < readers[0] {
269 t.Fatalf("buffer of frame %d rewritten with completed=%d", readers[0], r.completed)
270 }
271 }
272 if got := readPixels(t, r).RGBAAt(4, 4); got != (color.RGBA{180, 0, 0, 255}) {
273 t.Fatalf("pixel %v", got)
274 }
275}
276
277// A content's explicit acquire fence becomes a wait semaphore; a file
278// that is not a sync file is refused (no semaphore) without failing the
279// frame, and the caller's file stays open.
280func TestRendererImportsAcquireFence(t *testing.T) {
281 r, err := New(8, 8)
282 if err != nil {
283 t.Skipf("Vulkan unavailable: %v", err)
284 }
285 defer r.Close()
286 done, err := r.Render(ports.Scene{Background: "#000000"}, nil)
287 if err != nil || done == nil {
288 t.Skipf("no sync file: %v", err)
289 }
290 defer done.Close()
291 sem := r.importFence(done)
292 if sem == 0 {
293 t.Fatal("sync file not imported")
294 }
295 r.dd.DestroySemaphore(r.device, sem, nil)
296 if _, err := done.Stat(); err != nil {
297 t.Fatal("caller's fence closed")
298 }
299 f, w, _ := os.Pipe()
300 defer f.Close()
301 defer w.Close()
302 if sem := r.importFence(f); sem != 0 {
303 t.Fatal("pipe imported as a fence")
304 }
305}
306
307// readPixels reads the frame back, failing the test when Pixels cannot.
308func readPixels(t *testing.T, r *Renderer) *image.RGBA {
309 t.Helper()
310 img := r.Pixels()
311 if img == nil {
312 t.Fatal("Pixels: no free capture slot or copy failed")
313 }
314 return img
315}