1package drm
2
3import (
4 "context"
5 "errors"
6 "os"
7 "testing"
8 "testing/synctest"
9 "time"
10
11 portsmocks "github.com/bnema/neferwl/internal/mocks/ports"
12 "github.com/bnema/neferwl/internal/ports"
13 "github.com/stretchr/testify/mock"
14 "golang.org/x/sys/unix"
15)
16
17// An event queued before a modeset (or for an earlier output on the CRTC)
18// must not complete the commit that follows.
19func TestStaleEventDoesNotCompleteNewCommit(t *testing.T) {
20 o, _, commits := testOutput(t)
21 if err := o.commitFrame(70, nil, false, false, pendingFrame{}); err != nil {
22 t.Fatal(err)
23 }
24 old := eventOf((*commits)[0])
25 o.frame.endPending() // a modeset dropped the commit; its event is queued
26 if err := o.commitFrame(71, nil, false, false, pendingFrame{}); err != nil {
27 t.Fatal(err)
28 }
29 if o.completed(old, nil) || !o.frame.pendingCommit() {
30 t.Fatal("stale event completed the new commit")
31 }
32 if !o.completed(eventOf((*commits)[1]), nil) || o.frame.pendingCommit() {
33 t.Fatal("own event did not complete the commit")
34 }
35}
36
37// After EBUSY no commit of ours is known: the next event of the CRTC ends
38// the wait, then the frame commits.
39func TestBusyFrameCommitsAfterEvent(t *testing.T) {
40 o, _, commits := testOutput(t, unix.EBUSY)
41 enabled := true
42 if err := o.commitFrame(70, nil, false, false, pendingFrame{}); !o.commitFailed(err, &enabled) {
43 t.Fatalf("EBUSY not handled: %v", err)
44 }
45 if !o.completed(flipEvent{crtc: tCrtc, user: 12345 << userKindBits}, nil) || o.frame.pendingCommit() {
46 t.Fatal("EBUSY wait not ended by the CRTC's event")
47 }
48 if err := o.commitFrame(70, nil, false, false, pendingFrame{}); err != nil || len(*commits) != 2 || !o.frame.pendingCommit() {
49 t.Fatalf("frame after EBUSY: %v, %d commits", err, len(*commits))
50 }
51}
52
53// A cursor image change writes only an image the plane neither shows nor
54// is about to show; with both busy it waits for the event.
55func TestCursorImageNeverOverwritesScreen(t *testing.T) {
56 o, _, _ := testOutput(t)
57 c := o.cursor
58 imgs := newMockcursorImages(t)
59 c.imgs = imgs
60 var written []int
61 imgs.EXPECT().write(mock.Anything, mock.Anything, 1, 1, float64(0)).RunAndReturn(func(i int, _ []byte, _, _ int, _ float64) error {
62 written = append(written, i)
63 return nil
64 })
65 px := []byte{1, 2, 3, 4}
66 // Image 0 on screen: two changes before a commit both go to image 1.
67 c.screen, c.cur, c.image = 90, 0, true
68 for range 2 {
69 if err := c.setImage(px, 1, 1, 0, 0, 0); err != nil {
70 t.Fatal(err)
71 }
72 }
73 if len(written) != 2 || written[0] != 1 || written[1] != 1 {
74 t.Fatalf("written %v", written)
75 }
76 // Image 1 in flight, image 0 on screen: nothing is free.
77 c.committed(cursorState{on: true, fb: 91})
78 if err := c.setImage(px, 1, 1, 0, 0, 0); err != nil || len(written) != 2 || c.later == nil {
79 t.Fatalf("wrote a busy image: %v %v", err, written)
80 }
81 // The flip frees image 0.
82 c.landed()
83 if changed, err := c.flushLater(0); !changed || err != nil || len(written) != 3 || written[2] != 0 || c.cur != 0 {
84 t.Fatalf("after flip: %v %v %v cur %d", changed, err, written, c.cur)
85 }
86}
87
88// KMS refusing the cursor plane turns the hardware cursor off and the
89// frame commits without it; the output keeps running.
90func TestCursorRefusedFrameCommitsWithout(t *testing.T) {
91 o, _, commits := testOutput(t, unix.EINVAL, unix.EINVAL)
92 o.cursor.image = true
93 o.cursor.Move(5, 5)
94 if err := o.commitFrame(70, nil, false, false, pendingFrame{}); err != nil {
95 t.Fatal(err)
96 }
97 if !o.cursor.off || len(*commits) != 3 || (*commits)[1].flags != atomicTestOnly {
98 t.Fatalf("off=%v commits %d", o.cursor.off, len(*commits))
99 }
100 if v, _ := (*commits)[2].req.value(tCursor, pFB); v != 0 {
101 t.Fatalf("retry shows cursor fb %d", v)
102 }
103}
104
105// A refused cursor is not blamed on the client buffer.
106func TestCursorRefusedDoesNotBlameScanoutBuffer(t *testing.T) {
107 o, _, _ := testOutput(t, unix.EINVAL, unix.EINVAL)
108 o.cursor.image = true
109 o.cursor.Move(5, 5)
110 o.clientFBs[9] = &clientFB{fbID: 80}
111 direct, err := o.commitScanout(80, ports.SurfaceContent{DMABuf: &ports.DMABuf{ID: 9}}, pendingFrame{})
112 if err != nil || !direct || o.clientFBs[9].failed != "" {
113 t.Fatalf("direct=%v err=%v failed=%q", direct, err, o.clientFBs[9].failed)
114 }
115}
116
117// tiledMod stands for a non-linear driver modifier in the image tests.
118const tiledMod = 0x200000000401b03
119
120// showImages validates the images with a TEST_ONLY modeset, retries with
121// single-plane driver images, then falls back to linear images when KMS
122// refuses both.
123func TestShowImagesTestsBeforeModeset(t *testing.T) {
124 o, k, commits := testOutput(t, unix.EINVAL, unix.EINVAL)
125 o.cursor = nil
126 r := portsmocks.NewMockRenderer(t)
127 buf := func() ports.DMABuf {
128 var b ports.DMABuf
129 for range 2 {
130 f, w, _ := os.Pipe()
131 w.Close()
132 b.Planes = append(b.Planes, ports.DMABufPlane{File: f})
133 }
134 b.Modifier = tiledMod
135 return b
136 }
137 linear := func() ports.DMABuf {
138 f, w, _ := os.Pipe()
139 w.Close()
140 return ports.DMABuf{Planes: []ports.DMABufPlane{{File: f}}}
141 }
142 tiled1 := func() ports.DMABuf {
143 b := linear()
144 b.Modifier = tiledMod
145 return b
146 }
147 r.EXPECT().SetHDR(float64(0)).Return().Maybe()
148 r.EXPECT().ExportTargets(2, []uint64(nil), false).Return([]ports.DMABuf{buf(), buf()}, nil).Once()
149 r.EXPECT().ExportTargets(2, []uint64(nil), true).Return([]ports.DMABuf{tiled1(), tiled1()}, nil).Once()
150 r.EXPECT().ExportTargets(2, []uint64{0}, false).Return([]ports.DMABuf{linear(), linear()}, nil).Once()
151 r.EXPECT().UseTarget(mock.Anything).Return()
152 r.EXPECT().Render(mock.Anything, mock.Anything).Return(nil, nil)
153 k.EXPECT().addFB(mock.Anything, uint32(fourccXRGB)).Return(70, nil)
154 k.EXPECT().rmFB(mock.Anything).Return(nil)
155 k.EXPECT().createBlob(mock.Anything).Return(99, nil)
156 k.EXPECT().destroyBlob(mock.Anything).Return(nil)
157 if err := o.showImages(r, imagesDriver, nil); err != nil {
158 t.Fatal(err)
159 }
160 want := []uint32{atomicTestOnly | atomicAllowModes, atomicTestOnly | atomicAllowModes, atomicTestOnly | atomicAllowModes, atomicAllowModes}
161 if len(*commits) != len(want) {
162 t.Fatalf("%d commits", len(*commits))
163 }
164 for i, f := range want {
165 if (*commits)[i].flags != f {
166 t.Fatalf("commit %d flags %#x want %#x", i, (*commits)[i].flags, f)
167 }
168 }
169 if o.kind != imagesLinear {
170 t.Fatalf("kind %d", o.kind)
171 }
172}
173
174// A refused multi-plane (DCC) driver image is retried with single-plane
175// driver images before linear; every plane file of every buffer is closed
176// once the framebuffers exist.
177func TestShowImagesRetriesSinglePlaneBeforeLinear(t *testing.T) {
178 o, k, _ := testOutput(t, unix.EINVAL)
179 o.cursor = nil
180 var files []*os.File
181 buf := func(planes int) ports.DMABuf {
182 b := ports.DMABuf{Modifier: tiledMod}
183 for range planes {
184 f, w, _ := os.Pipe()
185 w.Close()
186 files = append(files, f)
187 b.Planes = append(b.Planes, ports.DMABufPlane{File: f})
188 }
189 return b
190 }
191 r := portsmocks.NewMockRenderer(t)
192 r.EXPECT().SetHDR(float64(0)).Return().Maybe()
193 r.EXPECT().ExportTargets(2, []uint64(nil), false).Return([]ports.DMABuf{buf(3), buf(3)}, nil).Once()
194 r.EXPECT().ExportTargets(2, []uint64(nil), true).Return([]ports.DMABuf{buf(1), buf(1)}, nil).Once()
195 r.EXPECT().UseTarget(mock.Anything).Return()
196 r.EXPECT().Render(mock.Anything, mock.Anything).Return(nil, nil)
197 k.EXPECT().addFB(mock.Anything, uint32(fourccXRGB)).Return(70, nil)
198 k.EXPECT().rmFB(mock.Anything).Return(nil)
199 k.EXPECT().createBlob(mock.Anything).Return(99, nil)
200 k.EXPECT().destroyBlob(mock.Anything).Return(nil)
201 if err := o.showImages(r, imagesDriver, nil); err != nil {
202 t.Fatal(err)
203 }
204 if o.kind != imagesSinglePlane {
205 t.Fatalf("kind %d", o.kind)
206 }
207 if len(files) != 8 {
208 t.Fatalf("%d plane files", len(files))
209 }
210 for i, f := range files {
211 if err := f.Close(); !errors.Is(err, os.ErrClosed) {
212 t.Fatalf("plane file %d left open (close: %v)", i, err)
213 }
214 }
215}
216
217// SDR images are asked for the modifiers the primary plane lists for
218// XRGB8888, in IN_FORMATS order, never the renderer's own choice: RADV's
219// preferred DCC variant (pipe-aligned) is not one amdgpu lists, and KMS
220// refuses a framebuffer with a modifier the plane does not advertise
221// (ADDFB2: EINVAL). Other formats' modifiers are not offered.
222func TestShowImagesOffersThePrimaryPlanesXRGBModifiers(t *testing.T) {
223 const dccListed, tiled, other = 0x200000000563b03, 0x200000000401b03, 0x200000000401903
224 o, k, _ := testOutput(t)
225 o.cursor = nil
226 o.primary.formats = []ports.DMABufFormat{
227 {Format: fourccXRGB, Modifier: dccListed}, {Format: fourccXR30, Modifier: other},
228 {Format: fourccXRGB, Modifier: tiled}, {Format: fourccXRGB, Modifier: 0},
229 }
230 buf := func() ports.DMABuf {
231 f, w, _ := os.Pipe()
232 w.Close()
233 return ports.DMABuf{Planes: []ports.DMABufPlane{{File: f}}}
234 }
235 r := portsmocks.NewMockRenderer(t)
236 r.EXPECT().SetHDR(float64(0)).Return().Maybe()
237 r.EXPECT().ExportTargets(2, []uint64{dccListed, tiled, 0}, false).Return([]ports.DMABuf{buf(), buf()}, nil).Once()
238 r.EXPECT().UseTarget(mock.Anything).Return()
239 r.EXPECT().Render(mock.Anything, mock.Anything).Return(nil, nil)
240 k.EXPECT().addFB(mock.Anything, uint32(fourccXRGB)).Return(70, nil)
241 k.EXPECT().rmFB(mock.Anything).Return(nil).Maybe()
242 k.EXPECT().createBlob(mock.Anything).Return(99, nil)
243 k.EXPECT().destroyBlob(mock.Anything).Return(nil)
244 if err := o.showImages(r, imagesDriver, nil); err != nil {
245 t.Fatal(err)
246 }
247 if o.kind != imagesDriver {
248 t.Fatalf("kind %d", o.kind)
249 }
250}
251
252// When the renderer has no modifier in common with the plane, the images
253// fall back to single-plane ones with the same list, then to linear.
254func TestShowImagesNoCommonModifierFallsBackWithThePlanesList(t *testing.T) {
255 const listed = 0x200000000563b03
256 o, k, _ := testOutput(t)
257 o.cursor = nil
258 o.primary.formats = []ports.DMABufFormat{{Format: fourccXRGB, Modifier: listed}, {Format: fourccXRGB, Modifier: 0}}
259 buf := func() ports.DMABuf {
260 f, w, _ := os.Pipe()
261 w.Close()
262 return ports.DMABuf{Planes: []ports.DMABufPlane{{File: f}}}
263 }
264 r := portsmocks.NewMockRenderer(t)
265 r.EXPECT().SetHDR(float64(0)).Return().Maybe()
266 none := errors.New("no XRGB8888 modifier both the device and the display accept")
267 r.EXPECT().ExportTargets(2, []uint64{listed, 0}, false).Return(nil, none).Once()
268 r.EXPECT().ExportTargets(2, []uint64{listed, 0}, true).Return([]ports.DMABuf{buf(), buf()}, nil).Once()
269 r.EXPECT().UseTarget(mock.Anything).Return()
270 r.EXPECT().Render(mock.Anything, mock.Anything).Return(nil, nil)
271 k.EXPECT().addFB(mock.Anything, uint32(fourccXRGB)).Return(70, nil)
272 k.EXPECT().rmFB(mock.Anything).Return(nil).Maybe()
273 k.EXPECT().createBlob(mock.Anything).Return(99, nil)
274 k.EXPECT().destroyBlob(mock.Anything).Return(nil)
275 if err := o.showImages(r, imagesDriver, nil); err != nil {
276 t.Fatal(err)
277 }
278 if o.kind != imagesSinglePlane {
279 t.Fatalf("kind %d", o.kind)
280 }
281}
282
283// A plane without IN_FORMATS lists every format with modifier 0 only
284// (readPlanes): the images are linear straight away, with no driver or
285// single-plane attempt KMS would refuse.
286func TestShowImagesLinearOnlyPlaneExportsLinearFirst(t *testing.T) {
287 o, k, commits := testOutput(t)
288 o.fbs = [2]uint32{} // the first setup of a run: no image yet
289 o.cursor = nil
290 o.primary.formats = []ports.DMABufFormat{{Format: fourccXRGB, Modifier: 0}}
291 r := portsmocks.NewMockRenderer(t)
292 r.EXPECT().SetHDR(float64(0)).Return().Maybe()
293 // No ExportTargets(..., true) and no second call: the mock fails on them.
294 r.EXPECT().ExportTargets(2, []uint64{0}, false).RunAndReturn(func(int, []uint64, bool) ([]ports.DMABuf, error) {
295 return []ports.DMABuf{linearBuf(), linearBuf()}, nil
296 }).Once()
297 r.EXPECT().UseTarget(mock.Anything).Return()
298 r.EXPECT().Render(mock.Anything, mock.Anything).Return(nil, nil)
299 k.EXPECT().addFB(mock.Anything, uint32(fourccXRGB)).Return(70, nil)
300 k.EXPECT().createBlob(mock.Anything).Return(99, nil)
301 k.EXPECT().destroyBlob(mock.Anything).Return(nil)
302 if err := o.showImages(r, imagesDriver, nil); err != nil {
303 t.Fatal(err)
304 }
305 if o.kind != imagesDriver || o.imageMod != 0 || len(*commits) != 2 {
306 t.Fatalf("kind %d, modifier %#x, %d commits", o.kind, o.imageMod, len(*commits))
307 }
308}
309
310// KMS refusing images that are already linear ends the attempt with that
311// error: the single-plane and linear kinds would export the same images.
312func TestShowImagesRefusedLinearImagesAreNotExportedAgain(t *testing.T) {
313 o, k, commits := testOutput(t, unix.EINVAL)
314 o.cursor = nil
315 o.primary.formats = []ports.DMABufFormat{{Format: fourccXRGB, Modifier: 0}}
316 r := portsmocks.NewMockRenderer(t)
317 r.EXPECT().SetHDR(float64(0)).Return().Maybe()
318 r.EXPECT().ExportTargets(2, []uint64{0}, false).RunAndReturn(func(int, []uint64, bool) ([]ports.DMABuf, error) {
319 return []ports.DMABuf{linearBuf(), linearBuf()}, nil
320 }).Once()
321 r.EXPECT().UseTarget(mock.Anything).Return()
322 r.EXPECT().Render(mock.Anything, mock.Anything).Return(nil, nil)
323 k.EXPECT().addFB(mock.Anything, uint32(fourccXRGB)).Return(70, nil)
324 k.EXPECT().rmFB(mock.Anything).Return(nil)
325 k.EXPECT().createBlob(mock.Anything).Return(99, nil)
326 k.EXPECT().destroyBlob(mock.Anything).Return(nil)
327 err := o.showImages(r, imagesDriver, nil)
328 if !errors.Is(err, unix.EINVAL) {
329 t.Fatalf("error %v, want the KMS refusal", err)
330 }
331 if len(*commits) != 1 || o.fbs != [2]uint32{} {
332 t.Fatalf("%d commits, fbs %v", len(*commits), o.fbs)
333 }
334}
335
336// linearBuf is a one-plane linear dmabuf over a closed pipe.
337func linearBuf() ports.DMABuf {
338 f, w, _ := os.Pipe()
339 w.Close()
340 return ports.DMABuf{Planes: []ports.DMABufPlane{{File: f}}}
341}
342
343// Refused driver images that already had one plane are not retried as
344// single-plane images: the next export is linear.
345func TestShowImagesSkipsSinglePlaneWhenDriverImagesHadOnePlane(t *testing.T) {
346 o, k, commits := testOutput(t, unix.EINVAL)
347 o.cursor = nil
348 buf := func() ports.DMABuf {
349 f, w, _ := os.Pipe()
350 w.Close()
351 return ports.DMABuf{Modifier: tiledMod, Planes: []ports.DMABufPlane{{File: f}}}
352 }
353 linear := func() ports.DMABuf {
354 f, w, _ := os.Pipe()
355 w.Close()
356 return ports.DMABuf{Planes: []ports.DMABufPlane{{File: f}}}
357 }
358 r := portsmocks.NewMockRenderer(t)
359 r.EXPECT().SetHDR(float64(0)).Return().Maybe()
360 r.EXPECT().ExportTargets(2, []uint64(nil), false).Return([]ports.DMABuf{buf(), buf()}, nil).Once()
361 r.EXPECT().ExportTargets(2, []uint64{0}, false).Return([]ports.DMABuf{linear(), linear()}, nil).Once()
362 r.EXPECT().UseTarget(mock.Anything).Return()
363 r.EXPECT().Render(mock.Anything, mock.Anything).Return(nil, nil)
364 k.EXPECT().addFB(mock.Anything, uint32(fourccXRGB)).Return(70, nil)
365 k.EXPECT().rmFB(mock.Anything).Return(nil)
366 k.EXPECT().createBlob(mock.Anything).Return(99, nil)
367 k.EXPECT().destroyBlob(mock.Anything).Return(nil)
368 if err := o.showImages(r, imagesDriver, nil); err != nil {
369 t.Fatal(err)
370 }
371 if o.kind != imagesLinear || len(*commits) != 3 {
372 t.Fatalf("kind %d, %d commits", o.kind, len(*commits))
373 }
374}
375
376// Enabling an output whose images were not made yet (started switched
377// away) tests them and modesets once.
378func TestEnableAfterStartAwayModesetsOnce(t *testing.T) {
379 synctest.Test(t, enableAfterStartAwayModesetsOnce)
380}
381
382func enableAfterStartAwayModesetsOnce(t *testing.T) {
383 o, k, commits, commitMu := testOutputMu(t)
384 o.cursor, o.tearing, o.fbs = nil, false, [2]uint32{}
385 r := portsmocks.NewMockRenderer(t)
386 buf := func() ports.DMABuf {
387 f, w, _ := os.Pipe()
388 w.Close()
389 return ports.DMABuf{Planes: []ports.DMABufPlane{{File: f}}}
390 }
391 r.EXPECT().SetHDR(float64(0)).Return().Maybe()
392 r.EXPECT().ExportTargets(2, mock.Anything, false).Return([]ports.DMABuf{buf(), buf()}, nil).Once()
393 r.EXPECT().UseTarget(mock.Anything).Return()
394 r.EXPECT().Render(mock.Anything, mock.Anything).Return(nil, nil)
395 r.EXPECT().Close().Return().Once()
396 k.EXPECT().addFB(mock.Anything, uint32(fourccXRGB)).Return(70, nil).Twice()
397 k.EXPECT().createBlob(mock.Anything).Return(99, nil)
398 k.EXPECT().destroyBlob(mock.Anything).Return(nil)
399 k.EXPECT().rmFB(mock.Anything).Return(nil).Maybe()
400 active := make(chan bool, 1)
401 active <- false
402 ctx, cancel := context.WithCancel(context.Background())
403 done := make(chan error, 1)
404 go func() {
405 done <- o.Run(ctx, func(int, int) (ports.Renderer, error) { return r, nil }, nil, active, nil, nil, nil, nil, discardSink(t), nil, nil)
406 }()
407 count := func(flags uint32) int {
408 commitMu.Lock()
409 defer commitMu.Unlock()
410 n := 0
411 for _, c := range *commits {
412 if c.flags == flags {
413 n++
414 }
415 }
416 return n
417 }
418 synctest.Wait()
419 if n := count(atomicAllowModes); n != 0 {
420 t.Fatalf("%d modesets while away", n)
421 }
422 active <- true
423 waitFor(t, func() bool { return count(atomicAllowModes) == 1 })
424 cancel()
425 if err := <-done; err != nil {
426 t.Fatal(err)
427 }
428 if n := count(atomicTestOnly | atomicAllowModes); n != 1 {
429 t.Fatalf("%d test modesets", n)
430 }
431}
432
433// Close without a mode to restore turns the CRTC off.
434func TestCloseWithoutSavedModeDisablesCrtc(t *testing.T) {
435 o, k, commits := testOutput(t)
436 k.EXPECT().rmFB(mock.Anything).Return(nil)
437 o.Close()
438 c := (*commits)[0]
439 for _, p := range []struct{ obj, prop uint32 }{{tCrtc, pActive}, {tCrtc, pMode}, {tPrimary, pFB}, {tConn, pConnCrtc}} {
440 if v, ok := c.req.value(p.obj, p.prop); !ok || v != 0 {
441 t.Fatalf("object %d prop %d = %d %v", p.obj, p.prop, v, ok)
442 }
443 }
444}
445
446// Close restores the saved mode and gives the connector its CRTC back.
447func TestCloseRestoresSavedMode(t *testing.T) {
448 o, k, commits := testOutput(t)
449 o.saved = modeCrtc{crtcID: tCrtc, fbID: 5, modeValid: 1, mode: modeInfo{HDisplay: 200, VDisplay: 100}}
450 k.EXPECT().createBlob(mock.Anything).Return(98, nil).Once()
451 k.EXPECT().destroyBlob(uint32(98)).Return(nil).Once()
452 k.EXPECT().rmFB(mock.Anything).Return(nil)
453 o.Close()
454 c := (*commits)[0]
455 if v, _ := c.req.value(tConn, pConnCrtc); v != tCrtc {
456 t.Fatalf("connector crtc %d", v)
457 }
458 if v, _ := c.req.value(tPrimary, pFB); v != 5 {
459 t.Fatalf("primary fb %d", v)
460 }
461}
462
463// With the cursor off no image is loaded (no warning per change).
464func TestCursorOffSkipsLoads(t *testing.T) {
465 o, _, _ := testOutput(t)
466 o.cursor.off = true
467 loaded := false
468 o.setCursor(func(ports.CursorChange, float64, ports.BufferTransform, int) (ports.CursorImage, error) {
469 loaded = true
470 return ports.CursorImage{}, nil
471 }, ports.CursorChange{}, 1, 0)
472 if loaded || o.cursor.desired().on {
473 t.Fatal("cursor used while off")
474 }
475}
476
477// Content read while only a cursor commit is in flight is reported when
478// that commit's event arrives, not held until a later frame.
479func TestRunReportsSeenAfterCursorCommit(t *testing.T) {
480 synctest.Test(t, runReportsSeenAfterCursorCommit)
481}
482
483func runReportsSeenAfterCursorCommit(t *testing.T) {
484 o, k, commits, commitMu := testOutputMu(t)
485 o.tearing, o.cursor.fbs = false, [2]uint32{}
486 flips := make(chan flipEvent, 1)
487 o.flipped = flips
488 r := portsmocks.NewMockRenderer(t)
489 buf := func() ports.DMABuf {
490 f, w, _ := os.Pipe()
491 w.Close()
492 return ports.DMABuf{Planes: []ports.DMABufPlane{{File: f}}}
493 }
494 r.EXPECT().CursorBuffers(64).Return([2]ports.DMABuf{buf(), buf()}, nil).Once()
495 r.EXPECT().WriteCursor(mock.Anything, mock.Anything, 1, 1).Return(nil)
496 r.EXPECT().SetHDR(float64(0)).Return().Maybe()
497 r.EXPECT().ExportTargets(2, mock.Anything, false).Return([]ports.DMABuf{buf(), buf()}, nil).Once()
498 r.EXPECT().UseTarget(mock.Anything).Return()
499 r.EXPECT().Render(mock.Anything, mock.Anything).Return(nil, nil)
500 r.EXPECT().Close().Return().Once()
501 k.EXPECT().createDumb(64, 64).Return(dumbBuffer{}, unix.ENOSYS).Once() // renderer images
502 k.EXPECT().addFB(mock.Anything, uint32(fourccARGB)).Return(90, nil).Once()
503 k.EXPECT().addFB(mock.Anything, uint32(fourccARGB)).Return(91, nil).Once()
504 k.EXPECT().addFB(mock.Anything, uint32(fourccXRGB)).Return(70, nil).Twice()
505 k.EXPECT().createBlob(mock.Anything).Return(99, nil)
506 k.EXPECT().destroyBlob(mock.Anything).Return(nil).Maybe()
507 k.EXPECT().rmFB(mock.Anything).Return(nil).Maybe()
508 load := func(ports.CursorChange, float64, ports.BufferTransform, int) (ports.CursorImage, error) {
509 return ports.CursorImage{Pixels: []byte{1, 2, 3, 4}, W: 1, H: 1}, nil
510 }
511 last := func(kind uint64) (commitRec, bool) {
512 commitMu.Lock()
513 defer commitMu.Unlock()
514 for i := len(*commits) - 1; i >= 0; i-- {
515 if c := (*commits)[i]; c.user&3 == kind {
516 return c, true
517 }
518 }
519 return commitRec{}, false
520 }
521 scenes := make(chan ports.Scene, 1)
522 contents := newContentFeed(t, 1)
523 sink, presented := presentedSink(t, 8)
524 ctx, cancel := context.WithCancel(context.Background())
525 done := make(chan error, 1)
526 go func() {
527 done <- o.Run(ctx, func(int, int) (ports.Renderer, error) { return r, nil }, load, make(chan bool), scenes, contents.in(), contents.ret, nil, sink, nil, nil)
528 }()
529 scenes <- ports.Scene{OutputWidth: 200, OutputHeight: 100, Scale: 1}
530 var frame commitRec
531 waitFor(t, func() bool { var ok bool; frame, ok = last(userFrame); return ok })
532 flips <- eventOf(frame)
533 <-presented // the frame's flip
534 o.cursor.Move(3, 3)
535 var cur commitRec
536 waitFor(t, func() bool { var ok bool; cur, ok = last(userState); return ok })
537 // A window this output does not show commits while the cursor flies.
538 contents.send(ports.SurfaceContent{ID: 7, Seq: 2, SHM: &ports.SHMBuffer{}})
539 synctest.Wait()
540 flips <- eventOf(cur)
541 select {
542 case p := <-presented:
543 if p.Seen[7] != 2 {
544 t.Fatalf("report %+v", p)
545 }
546 case <-time.After(time.Second):
547 t.Fatal("Seen not reported after the cursor commit")
548 }
549 cancel()
550 if err := <-done; err != nil {
551 t.Fatal(err)
552 }
553}
554
555// Frame fences are closed on every path: flipped, EBUSY (re-rendered
556// after the event) and lost master (re-rendered after the modeset). The
557// fd count is unchanged after many frames.
558func TestRunClosesFrameFences(t *testing.T) {
559 fds := func() int {
560 ents, err := os.ReadDir("/proc/self/fd")
561 if err != nil {
562 t.Skip(err)
563 }
564 return len(ents)
565 }
566 const rounds = 30
567 // Commit results: modeset test and modeset, then per round
568 // EBUSY + flip, or lost master + modeset + flip, or a plain flip.
569 errs := []error{nil, nil}
570 for i := range rounds {
571 switch i % 3 {
572 case 0:
573 errs = append(errs, unix.EBUSY, nil)
574 case 1:
575 errs = append(errs, unix.EACCES, nil, nil)
576 default:
577 errs = append(errs, nil)
578 }
579 }
580 o, k, commits, commitMu := testOutputMu(t, errs...)
581 o.cursor, o.tearing, o.fbs = nil, false, [2]uint32{}
582 flips := make(chan flipEvent, 1)
583 o.flipped = flips
584 r := portsmocks.NewMockRenderer(t)
585 buf := func() ports.DMABuf {
586 f, w, _ := os.Pipe()
587 w.Close()
588 return ports.DMABuf{Planes: []ports.DMABufPlane{{File: f}}}
589 }
590 r.EXPECT().SetHDR(float64(0)).Return().Maybe()
591 r.EXPECT().ExportTargets(2, mock.Anything, false).Return([]ports.DMABuf{buf(), buf()}, nil).Once()
592 r.EXPECT().UseTarget(mock.Anything).Return()
593 r.EXPECT().Render(mock.Anything, mock.Anything).RunAndReturn(func(ports.Scene, map[ports.WindowID]ports.SurfaceContent) (*os.File, error) {
594 f, w, _ := os.Pipe()
595 _, _ = w.Write([]byte{1})
596 w.Close()
597 return f, nil
598 })
599 r.EXPECT().Close().Return().Once()
600 k.EXPECT().addFB(mock.Anything, uint32(fourccXRGB)).Return(70, nil).Twice()
601 k.EXPECT().createBlob(mock.Anything).Return(99, nil)
602 k.EXPECT().destroyBlob(mock.Anything).Return(nil)
603 k.EXPECT().rmFB(mock.Anything).Return(nil).Maybe()
604 before := fds()
605 active := make(chan bool, 1)
606 scenes := make(chan ports.Scene, 1)
607 sink, _ := presentedSink(t, 4*rounds)
608 ctx, cancel := context.WithCancel(context.Background())
609 done := make(chan error, 1)
610 go func() {
611 done <- o.Run(ctx, func(int, int) (ports.Renderer, error) { return r, nil }, nil, active, scenes, nil, nil, nil, sink, nil, nil)
612 }()
613 count := func() int {
614 commitMu.Lock()
615 defer commitMu.Unlock()
616 return len(*commits)
617 }
618 last := func() commitRec {
619 commitMu.Lock()
620 defer commitMu.Unlock()
621 return (*commits)[len(*commits)-1]
622 }
623 waitFor(t, func() bool { return count() == 2 })
624 for i := range rounds {
625 n := count()
626 scenes <- ports.Scene{Seq: uint64(i)}
627 switch i % 3 {
628 case 0:
629 waitFor(t, func() bool { return count() == n+1 })
630 flips <- flipEvent{crtc: tCrtc} // ends the EBUSY wait
631 waitFor(t, func() bool { return count() == n+2 })
632 case 1:
633 waitFor(t, func() bool { return count() == n+1 })
634 active <- true
635 waitFor(t, func() bool { return count() == n+3 })
636 default:
637 waitFor(t, func() bool { return count() == n+1 })
638 }
639 flips <- eventOf(last())
640 }
641 cancel()
642 if err := <-done; err != nil {
643 t.Fatal(err)
644 }
645 if after := fds(); after > before {
646 t.Fatalf("fds %d -> %d", before, after)
647 }
648}