1package drm
2
3import (
4 "bytes"
5 "errors"
6 "os"
7 "testing"
8
9 portsmocks "github.com/bnema/neferwl/internal/mocks/ports"
10 "github.com/bnema/neferwl/internal/ports"
11 "github.com/stretchr/testify/mock"
12 "golang.org/x/sys/unix"
13)
14
15const tSide = 16
16
17// dumbOK expects two dumb buffers with handles 10 and 11, mappings (with
18// pitch bytes per row) returned in mem, and framebuffers 90 and 91.
19func dumbOK(k *mockkms, pitch int) (mem [2][]byte) {
20 for i := range mem {
21 mem[i] = make([]byte, pitch*tSide)
22 h := uint32(10 + i)
23 b := dumbBuffer{handle: h, pitch: uint32(pitch), size: uint64(len(mem[i]))}
24 k.EXPECT().createDumb(tSide, tSide).Return(b, nil).Once()
25 k.EXPECT().mapDumb(b).Return(mem[i], nil).Once()
26 k.EXPECT().addFBHandle(b, tSide, tSide, uint32(fourccARGB)).Return(90+uint32(i), nil).Once()
27 }
28 return mem
29}
30
31func dumbFree(k *mockkms) {
32 for i := range 2 {
33 k.EXPECT().rmFB(90 + uint32(i)).Return(nil).Once()
34 k.EXPECT().destroyDumb(10 + uint32(i)).Return(nil).Once()
35 }
36 k.EXPECT().unmapDumb(mock.Anything).Return(nil).Twice()
37}
38
39func rendererBufs(t *testing.T) [2]ports.DMABuf {
40 var b [2]ports.DMABuf
41 for i := range b {
42 f, w, err := os.Pipe()
43 if err != nil {
44 t.Fatal(err)
45 }
46 t.Cleanup(func() { _ = f.Close(); _ = w.Close() })
47 if err := w.Close(); err != nil {
48 t.Fatal(err)
49 }
50 b[i] = ports.DMABuf{Planes: []ports.DMABufPlane{{File: f}}}
51 }
52 return b
53}
54
55func rendererOK(t *testing.T, k *mockkms, r *portsmocks.MockRenderer) {
56 b := rendererBufs(t)
57 r.EXPECT().CursorBuffers(tSide).Return(b, nil).Once()
58 t.Cleanup(func() { assertPlanesClosed(t, b) })
59 k.EXPECT().addFB(mock.Anything, uint32(fourccARGB)).Return(80, nil).Once()
60 k.EXPECT().addFB(mock.Anything, uint32(fourccARGB)).Return(81, nil).Once()
61}
62
63// Dumb buffers come first: the renderer allocates nothing.
64func TestCursorSetupUsesDumbBuffers(t *testing.T) {
65 k, r := newMockkms(t), portsmocks.NewMockRenderer(t)
66 dumbOK(k, tSide*4)
67 c := newCursor(&plane{}, tSide)
68 dumbErr, err := c.setup(k, r)
69 if dumbErr != nil || err != nil {
70 t.Fatalf("setup: %v %v", dumbErr, err)
71 }
72 if c.alloc != allocDumb || c.fbs != [2]uint32{90, 91} {
73 t.Fatalf("alloc %q fbs %v", c.alloc, c.fbs)
74 }
75 dumbFree(k)
76 c.free()
77 if c.imgs != nil || c.fbs != [2]uint32{} {
78 t.Fatal("images kept after free")
79 }
80}
81
82// A dumb buffer that cannot be made (or mapped, or wrapped) falls back to
83// the renderer's images, and what was made is released.
84func TestCursorSetupFallsBackToRenderer(t *testing.T) {
85 for name, fail := range map[string]func(k *mockkms){
86 "create": func(k *mockkms) {
87 k.EXPECT().createDumb(tSide, tSide).Return(dumbBuffer{}, unix.ENOSYS).Once()
88 },
89 "map": func(k *mockkms) {
90 b := dumbBuffer{handle: 10, pitch: tSide * 4, size: tSide * tSide * 4}
91 k.EXPECT().createDumb(tSide, tSide).Return(b, nil).Once()
92 k.EXPECT().mapDumb(b).Return(nil, unix.ENOMEM).Once()
93 k.EXPECT().destroyDumb(uint32(10)).Return(nil).Once()
94 },
95 "framebuffer": func(k *mockkms) {
96 b := dumbBuffer{handle: 10, pitch: tSide * 4, size: tSide * tSide * 4}
97 mem := make([]byte, b.size)
98 k.EXPECT().createDumb(tSide, tSide).Return(b, nil).Once()
99 k.EXPECT().mapDumb(b).Return(mem, nil).Once()
100 k.EXPECT().addFBHandle(b, tSide, tSide, uint32(fourccARGB)).Return(0, unix.EINVAL).Once()
101 k.EXPECT().unmapDumb(mem).Return(nil).Once()
102 k.EXPECT().destroyDumb(uint32(10)).Return(nil).Once()
103 },
104 "second buffer": func(k *mockkms) {
105 b := dumbBuffer{handle: 10, pitch: tSide * 4, size: tSide * tSide * 4}
106 mem := make([]byte, b.size)
107 k.EXPECT().createDumb(tSide, tSide).Return(b, nil).Once()
108 k.EXPECT().mapDumb(b).Return(mem, nil).Once()
109 k.EXPECT().addFBHandle(b, tSide, tSide, uint32(fourccARGB)).Return(90, nil).Once()
110 k.EXPECT().createDumb(tSide, tSide).Return(dumbBuffer{}, unix.EMFILE).Once()
111 k.EXPECT().rmFB(uint32(90)).Return(nil).Once()
112 k.EXPECT().unmapDumb(mem).Return(nil).Once()
113 k.EXPECT().destroyDumb(uint32(10)).Return(nil).Once()
114 },
115 } {
116 t.Run(name, func(t *testing.T) {
117 k, r := newMockkms(t), portsmocks.NewMockRenderer(t)
118 fail(k)
119 rendererOK(t, k, r)
120 c := newCursor(&plane{}, tSide)
121 dumbErr, err := c.setup(k, r)
122 if dumbErr == nil || err != nil {
123 t.Fatalf("setup: %v %v", dumbErr, err)
124 }
125 if c.alloc != allocVulkan || c.fbs != [2]uint32{80, 81} {
126 t.Fatalf("alloc %q fbs %v", c.alloc, c.fbs)
127 }
128 })
129 }
130}
131
132// Without either allocator there are no images.
133func TestCursorSetupFailsWithoutAllocators(t *testing.T) {
134 k, r := newMockkms(t), portsmocks.NewMockRenderer(t)
135 k.EXPECT().createDumb(tSide, tSide).Return(dumbBuffer{}, unix.ENOSYS).Once()
136 r.EXPECT().CursorBuffers(tSide).Return([2]ports.DMABuf{}, errors.New("no export")).Once()
137 c := newCursor(&plane{}, tSide)
138 dumbErr, err := c.setup(k, r)
139 if dumbErr == nil || err == nil || c.imgs != nil || c.fbs != [2]uint32{} {
140 t.Fatalf("setup: %v %v imgs %v fbs %v", dumbErr, err, c.imgs, c.fbs)
141 }
142 if _, err := c.useFallback(0); err != nil {
143 t.Fatal(err)
144 }
145}
146
147// A renderer image whose framebuffer fails is released.
148func TestRendererCursorReleasesOnFailure(t *testing.T) {
149 k, r := newMockkms(t), portsmocks.NewMockRenderer(t)
150 bufs := rendererBufs(t)
151 r.EXPECT().CursorBuffers(tSide).Return(bufs, nil).Once()
152 k.EXPECT().addFB(mock.Anything, uint32(fourccARGB)).Return(80, nil).Once()
153 k.EXPECT().addFB(mock.Anything, uint32(fourccARGB)).Return(0, unix.EINVAL).Once()
154 k.EXPECT().rmFB(uint32(80)).Return(nil).Once()
155 if _, err := newRendererCursor(k, r, tSide); err == nil {
156 t.Fatal("framebuffer failure not reported")
157 }
158 assertPlanesClosed(t, bufs)
159}
160
161func assertPlanesClosed(t *testing.T, bufs [2]ports.DMABuf) {
162 t.Helper()
163 for i, b := range bufs {
164 for j, p := range b.Planes {
165 if _, err := p.File.Stat(); !errors.Is(err, os.ErrClosed) {
166 t.Errorf("buffer %d plane %d: Stat = %v, want os.ErrClosed", i, j, err)
167 }
168 }
169 }
170}
171
172// cursorOutput is a test output whose cursor plane has size tSide.
173func cursorOutput(t *testing.T, errs ...error) (*Output, *mockkms, *portsmocks.MockRenderer, *[]commitRec) {
174 o, k, commits := testOutput(t, errs...)
175 o.cursor = newCursor(&plane{id: tCursor, typ: planeCursor, props: planeProps}, tSide)
176 return o, k, portsmocks.NewMockRenderer(t), commits
177}
178
179// The plane refuses the dumb images: the renderer's are tried (TEST_ONLY),
180// the image on the cursor is rewritten into them, and writes go there.
181func TestCursorDumbRefusedUsesRendererImages(t *testing.T) {
182 o, k, r, commits := cursorOutput(t, unix.EINVAL)
183 dumbOK(k, tSide*4)
184 if _, err := o.cursor.setup(k, r); err != nil {
185 t.Fatal(err)
186 }
187 px := []byte{1, 2, 3, 4}
188 if err := o.cursor.setImage(px, 1, 1, 2, 3, 0); err != nil {
189 t.Fatal(err)
190 }
191 dumbFree(k)
192 rendererOK(t, k, r)
193 var written []int
194 r.EXPECT().WriteCursor(mock.Anything, px, 1, 1).RunAndReturn(func(i int, _ []byte, _, _ int) error {
195 written = append(written, i)
196 return nil
197 })
198 o.testCursor()
199 c := o.cursor
200 if c.off || c.alloc != allocVulkan || c.fbs != [2]uint32{80, 81} || !c.image || c.hotX != 2 || c.hotY != 3 {
201 t.Fatalf("off %v alloc %q fbs %v image %v hot %d,%d", c.off, c.alloc, c.fbs, c.image, c.hotX, c.hotY)
202 }
203 if len(written) != 1 || len(*commits) != 2 {
204 t.Fatalf("written %v, %d TEST_ONLY commits", written, len(*commits))
205 }
206 if v, _ := (*commits)[1].req.value(tCursor, pFB); v != 80 || (*commits)[1].flags != atomicTestOnly {
207 t.Fatalf("second test shows fb %d flags %#x", v, (*commits)[1].flags)
208 }
209 // Further changes are written by the renderer, not into the freed buffers.
210 if err := c.setImage(px, 1, 1, 0, 0, 0); err != nil || len(written) != 2 {
211 t.Fatalf("write after fallback: %v %v", err, written)
212 }
213}
214
215// Both kinds refused: the software cursor takes over.
216func TestCursorBothRefusedGoesOff(t *testing.T) {
217 o, k, r, commits := cursorOutput(t, unix.EINVAL, unix.EINVAL)
218 dumbOK(k, tSide*4)
219 if _, err := o.cursor.setup(k, r); err != nil {
220 t.Fatal(err)
221 }
222 dumbFree(k)
223 rendererOK(t, k, r)
224 if !o.testCursor() {
225 t.Fatal("no change reported")
226 }
227 if !o.cursor.off || o.cursor.alloc != allocVulkan || len(*commits) != 2 {
228 t.Fatalf("off %v alloc %q commits %d", o.cursor.off, o.cursor.alloc, len(*commits))
229 }
230 if o.cursor.desired().on {
231 t.Fatal("cursor shown while off")
232 }
233 // The renderer's images are released with the output.
234 k.EXPECT().rmFB(uint32(80)).Return(nil).Once()
235 k.EXPECT().rmFB(uint32(81)).Return(nil).Once()
236 o.cursor.free()
237}
238
239// Dumb images the plane takes stay, with no second test.
240func TestCursorDumbAcceptedStays(t *testing.T) {
241 for _, tc := range []struct {
242 name string
243 err error
244 }{{"accepted", nil}, {"non-refusal", unix.EIO}} {
245 t.Run(tc.name, func(t *testing.T) {
246 o, k, r, commits := cursorOutput(t, tc.err)
247 dumbOK(k, tSide*4)
248 if _, err := o.cursor.setup(k, r); err != nil {
249 t.Fatal(err)
250 }
251 if o.testCursor() || o.cursor.off || o.cursor.alloc != allocDumb || len(*commits) != 1 {
252 t.Fatalf("off %v alloc %q commits %d", o.cursor.off, o.cursor.alloc, len(*commits))
253 }
254 dumbFree(k)
255 o.cursor.free()
256 })
257 }
258}
259
260// Renderer images refused while no dumb ones exist: off, no retry.
261func TestCursorRendererRefusedGoesOff(t *testing.T) {
262 o, k, r, commits := cursorOutput(t, unix.EINVAL)
263 k.EXPECT().createDumb(tSide, tSide).Return(dumbBuffer{}, unix.ENOSYS).Once()
264 rendererOK(t, k, r)
265 if _, err := o.cursor.setup(k, r); err != nil {
266 t.Fatal(err)
267 }
268 if !o.testCursor() || !o.cursor.off || len(*commits) != 1 {
269 t.Fatalf("off %v commits %d", o.cursor.off, len(*commits))
270 }
271}
272
273// Writes go to the images that own the cursor, and the output's HDR white
274// is passed along.
275func TestCursorWritesGoToCurrentImages(t *testing.T) {
276 o, _, _, _ := cursorOutput(t)
277 imgs := newMockcursorImages(t)
278 imgs.EXPECT().fb(0).Return(90).Once()
279 imgs.EXPECT().fb(1).Return(91).Once()
280 o.cursor.install(imgs, allocDumb)
281 px := []byte{1, 2, 3, 4}
282 imgs.EXPECT().write(0, px, 1, 1, float64(0)).Return(nil).Once()
283 if err := o.cursor.setImage(px, 1, 1, 0, 0, o.cursorNits()); err != nil {
284 t.Fatal(err)
285 }
286 o.hdr.on, o.hdr.settings.SDRBrightness = true, 203
287 if got := o.cursorNits(); got != 203 {
288 t.Fatalf("HDR white %v", got)
289 }
290 o.cursor.screen = 90
291 imgs.EXPECT().write(1, px, 1, 1, float64(203)).Return(errors.New("full")).Once()
292 if err := o.cursor.setImage(px, 1, 1, 0, 0, o.cursorNits()); err == nil {
293 t.Fatal("write error swallowed")
294 }
295}
296
297// Dumb pitch may exceed the row size: rows land at the pitch, the image is
298// cropped to the side and cleared around.
299func TestDumbCursorWritePitchCropClear(t *testing.T) {
300 k := newMockkms(t)
301 const pitch = tSide*4 + 32
302 mem := dumbOK(k, pitch)
303 d, err := newDumbCursor(k, tSide)
304 if err != nil {
305 t.Fatal(err)
306 }
307 defer func() { dumbFree(k); d.close() }()
308 big := make([]byte, 40*40*4)
309 for y := range 40 {
310 for x := range 40 {
311 copy(big[(y*40+x)*4:], []byte{byte(x), byte(y), byte(x + y), 255})
312 }
313 }
314 if err := d.write(1, big, 40, 40, 0); err != nil { // cropped to tSide
315 t.Fatal(err)
316 }
317 last := (tSide - 1) * pitch
318 for _, y := range []int{0, 1, 7, tSide - 1} {
319 for _, x := range []int{0, 3, tSide - 1} {
320 got := mem[1][y*pitch+x*4 : y*pitch+x*4+4]
321 want := big[(y*40+x)*4 : (y*40+x)*4+4]
322 if !bytes.Equal(got, want) {
323 t.Fatalf("pixel %d,%d: %v want %v", x, y, got, want)
324 }
325 }
326 }
327 for y := range tSide {
328 if !bytes.Equal(mem[1][y*pitch+tSide*4:(y+1)*pitch], make([]byte, pitch-tSide*4)) {
329 t.Fatalf("row %d padding written", y)
330 }
331 }
332 if err := d.write(1, []byte{1, 2, 3, 4}, 1, 1, 0); err != nil {
333 t.Fatal(err)
334 }
335 m := mem[1]
336 if !bytes.Equal(m[:4], []byte{1, 2, 3, 4}) || m[4] != 0 || m[pitch] != 0 || m[last+(tSide-1)*4] != 0 {
337 t.Fatal("image not cleared around the new pixels")
338 }
339 // A wide, short image keeps its rows apart.
340 row := []byte{9, 9, 9, 9, 8, 8, 8, 8}
341 if err := d.write(0, append(row, row...), 2, 2, 0); err != nil {
342 t.Fatal(err)
343 }
344 if m := mem[0]; !bytes.Equal(m[:8], row) || !bytes.Equal(m[pitch:pitch+8], row) || m[8] != 0 {
345 t.Fatal("rows of a narrow image")
346 }
347 if err := d.write(0, []byte{1}, 1, 1, 0); err == nil {
348 t.Fatal("short image accepted")
349 }
350 if err := d.write(2, nil, 0, 0, 0); err == nil {
351 t.Fatal("bad index accepted")
352 }
353}
354
355// HDR cursors convert exactly as the renderer did (see hdrcursor's vector).
356func TestDumbCursorWriteHDR(t *testing.T) {
357 k := newMockkms(t)
358 mem := dumbOK(k, tSide*4)
359 d, err := newDumbCursor(k, tSide)
360 if err != nil {
361 t.Fatal(err)
362 }
363 defer func() { dumbFree(k); d.close() }()
364 if err := d.write(0, []byte{255, 255, 255, 255}, 1, 1, 203); err != nil {
365 t.Fatal(err)
366 }
367 if m := mem[0]; m[0] != 148 || m[1] != 148 || m[2] != 148 || m[3] != 255 {
368 t.Fatalf("HDR white: %v", m[:4])
369 }
370 if err := d.write(1, []byte{255, 255, 255, 255}, 1, 1, 0); err != nil || mem[1][0] != 255 {
371 t.Fatalf("SDR white: %v %v", err, mem[1][:4])
372 }
373 if d.cache.Len() != 4 {
374 t.Fatalf("cache %d", d.cache.Len())
375 }
376}
377
378func TestCursorFallbackState(t *testing.T) {
379 for _, mode := range []string{"loaded", "queued", "hidden"} {
380 t.Run(mode, func(t *testing.T) {
381 o, k, r, commits := cursorOutput(t, unix.EINVAL)
382 dumbOK(k, tSide*4)
383 c := o.cursor
384 if _, err := c.setup(k, r); err != nil {
385 t.Fatal(err)
386 }
387 px := []byte{1, 2, 3, 4}
388 if err := c.setImage(px, 1, 1, 2, 3, 0); err != nil {
389 t.Fatal(err)
390 }
391 c.screen = 90
392 c.committed(cursorState{on: true, fb: 91, x: 8, y: 9})
393 want := []byte{1, 2, 3, 4}
394 if mode == "queued" {
395 px = []byte{5, 6, 7, 8}
396 want = bytes.Clone(px)
397 if err := c.setImage(px, 1, 1, 4, 5, 0); err != nil {
398 t.Fatal(err)
399 }
400 if c.later == nil {
401 t.Fatal("image not queued")
402 }
403 }
404 if mode == "hidden" {
405 if err := c.setImage(nil, 0, 0, 0, 0, 0); err != nil {
406 t.Fatal(err)
407 }
408 }
409 px[0] = 99 // Neither the loaded nor queued image may alias caller memory.
410 dumbFree(k)
411 rendererOK(t, k, r)
412 if mode != "hidden" {
413 r.EXPECT().WriteCursor(0, want, 1, 1).Return(nil).Once()
414 }
415 if !o.testCursor() {
416 t.Fatal("swap not reported")
417 }
418 if c.screen != 0 || c.flying || c.flight != (cursorState{}) || c.applied != (cursorState{}) || c.later != nil {
419 t.Fatalf("stale scanout state: screen %d flight %+v flying %v applied %+v", c.screen, c.flight, c.flying, c.applied)
420 }
421 c.landed()
422 if c.screen != 0 {
423 t.Fatal("old pending flight resurrected")
424 }
425 s := c.desired()
426 if mode == "hidden" {
427 if s.on || c.image {
428 t.Fatal("hidden cursor shown")
429 }
430 } else {
431 if !s.on || s.fb != 80 {
432 t.Fatalf("desired %+v", s)
433 }
434 if mode == "queued" && (c.hotX != 4 || c.hotY != 5) {
435 t.Fatal("queued hotspot lost")
436 }
437 }
438 req := &atomicReq{}
439 c.props(req, o.crtc, s)
440 if err := k.commit(req, 0, 0); err != nil {
441 t.Fatal(err)
442 }
443 wantFB := uint64(80)
444 if mode == "hidden" {
445 wantFB = 0
446 }
447 if fb, _ := (*commits)[2].req.value(tCursor, pFB); fb != wantFB {
448 t.Fatalf("next commit fb %d want %d", fb, wantFB)
449 }
450 c.committed(s)
451 c.landed()
452 if c.screen != uint32(wantFB) {
453 t.Fatalf("screen %d", c.screen)
454 }
455 })
456 }
457}
458
459func TestCursorFallbackFailureGoesOff(t *testing.T) {
460 for _, mode := range []string{"allocation", "rewrite"} {
461 t.Run(mode, func(t *testing.T) {
462 o, k, r, commits := cursorOutput(t, unix.EINVAL)
463 dumbOK(k, tSide*4)
464 c := o.cursor
465 if _, err := c.setup(k, r); err != nil {
466 t.Fatal(err)
467 }
468 px := []byte{1, 2, 3, 4}
469 if err := c.setImage(px, 1, 1, 0, 0, 0); err != nil {
470 t.Fatal(err)
471 }
472 if mode == "allocation" {
473 r.EXPECT().CursorBuffers(tSide).Return([2]ports.DMABuf{}, errors.New("allocation failed")).Once()
474 } else {
475 dumbFree(k)
476 rendererOK(t, k, r)
477 r.EXPECT().WriteCursor(0, px, 1, 1).Return(errors.New("rewrite failed")).Once()
478 }
479 if !o.testCursor() || !c.off || c.desired().on || len(*commits) != 1 {
480 t.Fatalf("off %v desired %+v commits %d", c.off, c.desired(), len(*commits))
481 }
482 if mode == "allocation" {
483 dumbFree(k)
484 } else {
485 k.EXPECT().rmFB(uint32(80)).Return(nil).Once()
486 k.EXPECT().rmFB(uint32(81)).Return(nil).Once()
487 }
488 c.free()
489 })
490 }
491}