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