1package drm
2
3import (
4 "context"
5 "os"
6 "testing"
7 "time"
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
15// An event queued before a modeset (or for an earlier output on the CRTC)
16// must not complete the commit that follows.
17func TestStaleEventDoesNotCompleteNewCommit(t *testing.T) {
18 o, _, commits := testOutput(t)
19 if err := o.commitFrame(70, nil, false, false, pendingFrame{}); err != nil {
20 t.Fatal(err)
21 }
22 old := eventOf((*commits)[0])
23 o.pending = false // a modeset dropped the commit; its event is queued
24 if err := o.commitFrame(71, nil, false, false, pendingFrame{}); err != nil {
25 t.Fatal(err)
26 }
27 if o.completed(old, nil) || !o.pending {
28 t.Fatal("stale event completed the new commit")
29 }
30 if !o.completed(eventOf((*commits)[1]), nil) || o.pending {
31 t.Fatal("own event did not complete the commit")
32 }
33}
34
35// After EBUSY no commit of ours is known: the next event of the CRTC ends
36// the wait, then the frame commits.
37func TestBusyFrameCommitsAfterEvent(t *testing.T) {
38 o, _, commits := testOutput(t, unix.EBUSY)
39 enabled := true
40 if err := o.commitFrame(70, nil, false, false, pendingFrame{}); !o.commitFailed(err, &enabled) {
41 t.Fatalf("EBUSY not handled: %v", err)
42 }
43 if !o.completed(flipEvent{crtc: tCrtc, user: 12345 << userKindBits}, nil) || o.pending {
44 t.Fatal("EBUSY wait not ended by the CRTC's event")
45 }
46 if err := o.commitFrame(70, nil, false, false, pendingFrame{}); err != nil || len(*commits) != 2 || !o.pending {
47 t.Fatalf("frame after EBUSY: %v, %d commits", err, len(*commits))
48 }
49}
50
51// A cursor image change writes only an image the plane neither shows nor
52// is about to show; with both busy it waits for the event.
53func TestCursorImageNeverOverwritesScreen(t *testing.T) {
54 o, _, _ := testOutput(t)
55 c := o.cursor
56 r := portsmocks.NewMockRenderer(t)
57 var written []int
58 r.EXPECT().WriteCursor(mock.Anything, mock.Anything, 1, 1).RunAndReturn(func(i int, _ []byte, _, _ int) error {
59 written = append(written, i)
60 return nil
61 })
62 px := []byte{1, 2, 3, 4}
63 // Image 0 on screen: two changes before a commit both go to image 1.
64 c.screen, c.cur, c.image = 90, 0, true
65 for range 2 {
66 if err := c.setImage(r, px, 1, 1, 0, 0); err != nil {
67 t.Fatal(err)
68 }
69 }
70 if len(written) != 2 || written[0] != 1 || written[1] != 1 {
71 t.Fatalf("written %v", written)
72 }
73 // Image 1 in flight, image 0 on screen: nothing is free.
74 c.committed(cursorState{on: true, fb: 91})
75 if err := c.setImage(r, px, 1, 1, 0, 0); err != nil || len(written) != 2 || c.later == nil {
76 t.Fatalf("wrote a busy image: %v %v", err, written)
77 }
78 // The flip frees image 0.
79 c.landed()
80 if changed, err := c.flushLater(r); !changed || err != nil || len(written) != 3 || written[2] != 0 || c.cur != 0 {
81 t.Fatalf("after flip: %v %v %v cur %d", changed, err, written, c.cur)
82 }
83}
84
85// KMS refusing the cursor plane turns the hardware cursor off and the
86// frame commits without it; the output keeps running.
87func TestCursorRefusedFrameCommitsWithout(t *testing.T) {
88 o, _, commits := testOutput(t, unix.EINVAL, unix.EINVAL)
89 o.cursor.image = true
90 o.cursor.Move(5, 5)
91 if err := o.commitFrame(70, nil, false, false, pendingFrame{}); err != nil {
92 t.Fatal(err)
93 }
94 if !o.cursor.off || len(*commits) != 3 || (*commits)[1].flags != atomicTestOnly {
95 t.Fatalf("off=%v commits %d", o.cursor.off, len(*commits))
96 }
97 if v, _ := (*commits)[2].req.value(tCursor, pFB); v != 0 {
98 t.Fatalf("retry shows cursor fb %d", v)
99 }
100}
101
102// A refused cursor is not blamed on the client buffer.
103func TestCursorRefusedDoesNotBlameScanoutBuffer(t *testing.T) {
104 o, _, _ := testOutput(t, unix.EINVAL, unix.EINVAL)
105 o.cursor.image = true
106 o.cursor.Move(5, 5)
107 o.clientFBs[9] = &clientFB{fbID: 80}
108 direct, err := o.commitScanout(80, ports.SurfaceContent{DMABuf: &ports.DMABuf{ID: 9}}, pendingFrame{})
109 if err != nil || !direct || o.clientFBs[9].failed != "" {
110 t.Fatalf("direct=%v err=%v failed=%q", direct, err, o.clientFBs[9].failed)
111 }
112}
113
114// showImages validates the images with a TEST_ONLY modeset and falls back
115// to linear images when KMS refuses the driver's.
116func TestShowImagesTestsBeforeModeset(t *testing.T) {
117 o, k, commits := testOutput(t, unix.EINVAL)
118 o.cursor = nil
119 r := portsmocks.NewMockRenderer(t)
120 buf := func() ports.DMABuf {
121 f, w, _ := os.Pipe()
122 w.Close()
123 return ports.DMABuf{Planes: []ports.DMABufPlane{{File: f}}}
124 }
125 r.EXPECT().ExportTargets(2, []uint64(nil)).Return([]ports.DMABuf{buf(), buf()}, nil).Once()
126 r.EXPECT().ExportTargets(2, []uint64{0}).Return([]ports.DMABuf{buf(), buf()}, nil).Once()
127 r.EXPECT().UseTarget(mock.Anything).Return()
128 r.EXPECT().Render(mock.Anything, mock.Anything).Return(nil, nil)
129 k.EXPECT().addFB(mock.Anything, uint32(fourccXRGB)).Return(70, nil)
130 k.EXPECT().rmFB(mock.Anything).Return(nil)
131 k.EXPECT().createBlob(mock.Anything).Return(99, nil)
132 k.EXPECT().destroyBlob(mock.Anything).Return(nil)
133 if err := o.showImages(r, imagesDriver, nil); err != nil {
134 t.Fatal(err)
135 }
136 want := []uint32{atomicTestOnly | atomicAllowModes, atomicTestOnly | atomicAllowModes, atomicAllowModes}
137 if len(*commits) != len(want) {
138 t.Fatalf("%d commits", len(*commits))
139 }
140 for i, f := range want {
141 if (*commits)[i].flags != f {
142 t.Fatalf("commit %d flags %#x want %#x", i, (*commits)[i].flags, f)
143 }
144 }
145 if o.kind != imagesLinear {
146 t.Fatalf("kind %d", o.kind)
147 }
148}
149
150// Enabling an output whose images were not made yet (started switched
151// away) tests them and modesets once.
152func TestEnableAfterStartAwayModesetsOnce(t *testing.T) {
153 o, k, commits, commitMu := testOutputMu(t)
154 o.cursor, o.tearing, o.fbs = nil, false, [2]uint32{}
155 r := portsmocks.NewMockRenderer(t)
156 buf := func() ports.DMABuf {
157 f, w, _ := os.Pipe()
158 w.Close()
159 return ports.DMABuf{Planes: []ports.DMABufPlane{{File: f}}}
160 }
161 r.EXPECT().ExportTargets(2, mock.Anything).Return([]ports.DMABuf{buf(), buf()}, nil).Once()
162 r.EXPECT().UseTarget(mock.Anything).Return()
163 r.EXPECT().Render(mock.Anything, mock.Anything).Return(nil, nil)
164 r.EXPECT().Close().Return().Once()
165 k.EXPECT().addFB(mock.Anything, uint32(fourccXRGB)).Return(70, nil).Twice()
166 k.EXPECT().createBlob(mock.Anything).Return(99, nil)
167 k.EXPECT().destroyBlob(mock.Anything).Return(nil)
168 k.EXPECT().rmFB(mock.Anything).Return(nil).Maybe()
169 active := make(chan bool, 1)
170 active <- false
171 ctx, cancel := context.WithCancel(context.Background())
172 done := make(chan error, 1)
173 go func() {
174 done <- o.Run(ctx, func(int, int) (ports.Renderer, error) { return r, nil }, nil, active, nil, nil, nil, make(chan ports.OutputPresented, 1))
175 }()
176 count := func(flags uint32) int {
177 commitMu.Lock()
178 defer commitMu.Unlock()
179 n := 0
180 for _, c := range *commits {
181 if c.flags == flags {
182 n++
183 }
184 }
185 return n
186 }
187 time.Sleep(20 * time.Millisecond)
188 if n := count(atomicAllowModes); n != 0 {
189 t.Fatalf("%d modesets while away", n)
190 }
191 active <- true
192 waitFor(t, func() bool { return count(atomicAllowModes) == 1 })
193 cancel()
194 if err := <-done; err != nil {
195 t.Fatal(err)
196 }
197 if n := count(atomicTestOnly | atomicAllowModes); n != 1 {
198 t.Fatalf("%d test modesets", n)
199 }
200}
201
202// Close without a mode to restore turns the CRTC off.
203func TestCloseWithoutSavedModeDisablesCrtc(t *testing.T) {
204 o, k, commits := testOutput(t)
205 k.EXPECT().rmFB(mock.Anything).Return(nil)
206 o.Close()
207 c := (*commits)[0]
208 for _, p := range []struct{ obj, prop uint32 }{{tCrtc, pActive}, {tCrtc, pMode}, {tPrimary, pFB}, {tConn, pConnCrtc}} {
209 if v, ok := c.req.value(p.obj, p.prop); !ok || v != 0 {
210 t.Fatalf("object %d prop %d = %d %v", p.obj, p.prop, v, ok)
211 }
212 }
213}
214
215// Close restores the saved mode and gives the connector its CRTC back.
216func TestCloseRestoresSavedMode(t *testing.T) {
217 o, k, commits := testOutput(t)
218 o.saved = modeCrtc{crtcID: tCrtc, fbID: 5, modeValid: 1, mode: modeInfo{HDisplay: 200, VDisplay: 100}}
219 k.EXPECT().createBlob(mock.Anything).Return(98, nil).Once()
220 k.EXPECT().destroyBlob(uint32(98)).Return(nil).Once()
221 k.EXPECT().rmFB(mock.Anything).Return(nil)
222 o.Close()
223 c := (*commits)[0]
224 if v, _ := c.req.value(tConn, pConnCrtc); v != tCrtc {
225 t.Fatalf("connector crtc %d", v)
226 }
227 if v, _ := c.req.value(tPrimary, pFB); v != 5 {
228 t.Fatalf("primary fb %d", v)
229 }
230}
231
232// With the cursor off no image is loaded (no warning per change).
233func TestCursorOffSkipsLoads(t *testing.T) {
234 o, _, _ := testOutput(t)
235 o.cursor.off = true
236 loaded := false
237 o.setCursor(nil, func(ports.CursorChange, float64, int) (ports.CursorImage, error) {
238 loaded = true
239 return ports.CursorImage{}, nil
240 }, ports.CursorChange{}, 1)
241 if loaded || o.cursor.desired().on {
242 t.Fatal("cursor used while off")
243 }
244}
245
246// Content read while only a cursor commit is in flight is reported when
247// that commit's event arrives, not held until a later frame.
248func TestRunReportsSeenAfterCursorCommit(t *testing.T) {
249 o, k, commits, commitMu := testOutputMu(t)
250 o.tearing, o.cursor.fbs = false, [2]uint32{}
251 flips := make(chan flipEvent, 1)
252 o.flipped = flips
253 r := portsmocks.NewMockRenderer(t)
254 buf := func() ports.DMABuf {
255 f, w, _ := os.Pipe()
256 w.Close()
257 return ports.DMABuf{Planes: []ports.DMABufPlane{{File: f}}}
258 }
259 r.EXPECT().CursorBuffers(64).Return([2]ports.DMABuf{buf(), buf()}, nil).Once()
260 r.EXPECT().WriteCursor(mock.Anything, mock.Anything, 1, 1).Return(nil)
261 r.EXPECT().ExportTargets(2, mock.Anything).Return([]ports.DMABuf{buf(), buf()}, nil).Once()
262 r.EXPECT().UseTarget(mock.Anything).Return()
263 r.EXPECT().Render(mock.Anything, mock.Anything).Return(nil, nil)
264 r.EXPECT().Close().Return().Once()
265 k.EXPECT().addFB(mock.Anything, uint32(fourccARGB)).Return(90, nil).Once()
266 k.EXPECT().addFB(mock.Anything, uint32(fourccARGB)).Return(91, nil).Once()
267 k.EXPECT().addFB(mock.Anything, uint32(fourccXRGB)).Return(70, nil).Twice()
268 k.EXPECT().createBlob(mock.Anything).Return(99, nil)
269 k.EXPECT().destroyBlob(mock.Anything).Return(nil).Maybe()
270 k.EXPECT().rmFB(mock.Anything).Return(nil).Maybe()
271 load := func(ports.CursorChange, float64, int) (ports.CursorImage, error) {
272 return ports.CursorImage{Pixels: []byte{1, 2, 3, 4}, W: 1, H: 1}, nil
273 }
274 last := func(kind uint64) (commitRec, bool) {
275 commitMu.Lock()
276 defer commitMu.Unlock()
277 for i := len(*commits) - 1; i >= 0; i-- {
278 if c := (*commits)[i]; c.user&3 == kind {
279 return c, true
280 }
281 }
282 return commitRec{}, false
283 }
284 scenes := make(chan ports.Scene, 1)
285 contents := make(chan ports.SurfaceContent, 1)
286 presented := make(chan ports.OutputPresented, 8)
287 ctx, cancel := context.WithCancel(context.Background())
288 done := make(chan error, 1)
289 go func() {
290 done <- o.Run(ctx, func(int, int) (ports.Renderer, error) { return r, nil }, load, make(chan bool), scenes, contents, nil, presented)
291 }()
292 scenes <- ports.Scene{OutputWidth: 200, OutputHeight: 100, Scale: 1}
293 var frame commitRec
294 waitFor(t, func() bool { var ok bool; frame, ok = last(userFrame); return ok })
295 flips <- eventOf(frame)
296 <-presented // the frame's flip
297 o.cursor.Move(3, 3)
298 var cur commitRec
299 waitFor(t, func() bool { var ok bool; cur, ok = last(userState); return ok })
300 // A window this output does not show commits while the cursor flies.
301 contents <- ports.SurfaceContent{ID: 7, Seq: 2, SHM: &ports.SHMBuffer{}}
302 time.Sleep(20 * time.Millisecond)
303 flips <- eventOf(cur)
304 select {
305 case p := <-presented:
306 if p.Seen[7] != 2 {
307 t.Fatalf("report %+v", p)
308 }
309 case <-time.After(time.Second):
310 t.Fatal("Seen not reported after the cursor commit")
311 }
312 cancel()
313 if err := <-done; err != nil {
314 t.Fatal(err)
315 }
316}
317
318// Frame fences are closed on every path: flipped, EBUSY (re-rendered
319// after the event) and lost master (re-rendered after the modeset). The
320// fd count is unchanged after many frames.
321func TestRunClosesFrameFences(t *testing.T) {
322 fds := func() int {
323 ents, err := os.ReadDir("/proc/self/fd")
324 if err != nil {
325 t.Skip(err)
326 }
327 return len(ents)
328 }
329 const rounds = 30
330 // Commit results: modeset test and modeset, then per round
331 // EBUSY + flip, or lost master + modeset + flip, or a plain flip.
332 errs := []error{nil, nil}
333 for i := range rounds {
334 switch i % 3 {
335 case 0:
336 errs = append(errs, unix.EBUSY, nil)
337 case 1:
338 errs = append(errs, unix.EACCES, nil, nil)
339 default:
340 errs = append(errs, nil)
341 }
342 }
343 o, k, commits, commitMu := testOutputMu(t, errs...)
344 o.cursor, o.tearing, o.fbs = nil, false, [2]uint32{}
345 flips := make(chan flipEvent, 1)
346 o.flipped = flips
347 r := portsmocks.NewMockRenderer(t)
348 buf := func() ports.DMABuf {
349 f, w, _ := os.Pipe()
350 w.Close()
351 return ports.DMABuf{Planes: []ports.DMABufPlane{{File: f}}}
352 }
353 r.EXPECT().ExportTargets(2, mock.Anything).Return([]ports.DMABuf{buf(), buf()}, nil).Once()
354 r.EXPECT().UseTarget(mock.Anything).Return()
355 r.EXPECT().Render(mock.Anything, mock.Anything).RunAndReturn(func(ports.Scene, map[ports.WindowID]ports.SurfaceContent) (*os.File, error) {
356 f, w, _ := os.Pipe()
357 _, _ = w.Write([]byte{1})
358 w.Close()
359 return f, nil
360 })
361 r.EXPECT().Close().Return().Once()
362 k.EXPECT().addFB(mock.Anything, uint32(fourccXRGB)).Return(70, nil).Twice()
363 k.EXPECT().createBlob(mock.Anything).Return(99, nil)
364 k.EXPECT().destroyBlob(mock.Anything).Return(nil)
365 k.EXPECT().rmFB(mock.Anything).Return(nil).Maybe()
366 before := fds()
367 active := make(chan bool, 1)
368 scenes := make(chan ports.Scene, 1)
369 presented := make(chan ports.OutputPresented, 4*rounds)
370 ctx, cancel := context.WithCancel(context.Background())
371 done := make(chan error, 1)
372 go func() {
373 done <- o.Run(ctx, func(int, int) (ports.Renderer, error) { return r, nil }, nil, active, scenes, nil, nil, presented)
374 }()
375 count := func() int {
376 commitMu.Lock()
377 defer commitMu.Unlock()
378 return len(*commits)
379 }
380 last := func() commitRec {
381 commitMu.Lock()
382 defer commitMu.Unlock()
383 return (*commits)[len(*commits)-1]
384 }
385 waitFor(t, func() bool { return count() == 2 })
386 for i := range rounds {
387 n := count()
388 scenes <- ports.Scene{Seq: uint64(i)}
389 switch i % 3 {
390 case 0:
391 waitFor(t, func() bool { return count() == n+1 })
392 flips <- flipEvent{crtc: tCrtc} // ends the EBUSY wait
393 waitFor(t, func() bool { return count() == n+2 })
394 case 1:
395 waitFor(t, func() bool { return count() == n+1 })
396 active <- true
397 waitFor(t, func() bool { return count() == n+3 })
398 default:
399 waitFor(t, func() bool { return count() == n+1 })
400 }
401 flips <- eventOf(last())
402 }
403 cancel()
404 if err := <-done; err != nil {
405 t.Fatal(err)
406 }
407 if after := fds(); after > before {
408 t.Fatalf("fds %d -> %d", before, after)
409 }
410}