1package drm
2
3import (
4 "context"
5 "errors"
6 "image"
7 "os"
8 "sync"
9 "sync/atomic"
10 "testing"
11 "time"
12
13 "github.com/bnema/neferwl/internal/adapters/presented"
14 portsmocks "github.com/bnema/neferwl/internal/mocks/ports"
15 "github.com/bnema/neferwl/internal/ports"
16 "github.com/bnema/zerowrap"
17 "github.com/stretchr/testify/mock"
18 "golang.org/x/sys/unix"
19)
20
21// Object and property IDs of the test output.
22const (
23 tCrtc, tConn, tPrimary, tCursor, tOverlay = 30, 40, 50, 51, 52
24 pFB, pCrtcID, pFence, pMode, pActive = 1, 2, 3, 4, 5
25 pVRR, pConnCrtc = 6, 7
26)
27
28var planeProps = map[string]uint32{"FB_ID": pFB, "CRTC_ID": pCrtcID, "IN_FENCE_FD": pFence, "SRC_X": 10, "SRC_Y": 11, "SRC_W": 12, "SRC_H": 13, "CRTC_X": 14, "CRTC_Y": 15, "CRTC_W": 16, "CRTC_H": 17}
29
30// commitRec is one recorded atomic commit.
31type commitRec struct {
32 req atomicReq
33 flags uint32
34 user uint64
35 at time.Time // when the commit was made
36}
37
38// testOutput returns an output on a mocked kms that records commits;
39// commit returns the next error of errs (nil when empty).
40func testOutput(t *testing.T, errs ...error) (*Output, *mockkms, *[]commitRec) {
41 o, k, commits, _ := testOutputMu(t, errs...)
42 return o, k, commits
43}
44
45// testOutputMu is testOutput with the mutex that guards the recorded
46// commits (Run commits on its goroutine).
47func testOutputMu(t *testing.T, errs ...error) (*Output, *mockkms, *[]commitRec, *sync.Mutex) {
48 return testOutputRules(t, nil, errs...)
49}
50
51// testOutputRules is testOutputMu with an optional driver rule: a non-nil
52// rule sees every commit (under the commit mutex) and may refuse it.
53func testOutputRules(t *testing.T, rule func(*atomicReq, uint32) error, errs ...error) (*Output, *mockkms, *[]commitRec, *sync.Mutex) {
54 k := newMockkms(t)
55 var commits []commitRec
56 commitMu := &sync.Mutex{}
57 k.EXPECT().commit(mock.Anything, mock.Anything, mock.Anything).RunAndReturn(func(r *atomicReq, flags uint32, user uint64) error {
58 commitMu.Lock()
59 defer commitMu.Unlock()
60 c := commitRec{flags: flags, user: user, at: time.Now()}
61 c.req.objs = append([]uint32(nil), r.objs...)
62 for _, ps := range r.props {
63 c.req.props = append(c.req.props, append([]propValue(nil), ps...))
64 }
65 commits = append(commits, c)
66 if rule != nil {
67 if err := rule(r, flags); err != nil {
68 return err
69 }
70 }
71 if len(errs) > 0 {
72 err := errs[0]
73 errs = errs[1:]
74 return err
75 }
76 return nil
77 }).Maybe()
78 primary := &plane{id: tPrimary, typ: planePrimary, props: planeProps}
79 cur := newCursor(&plane{id: tCursor, typ: planeCursor, props: planeProps}, 64)
80 cur.fbs = [2]uint32{90, 91}
81 o := &Output{k: k, frame: frameLifecycle{serials: &atomic.Uint64{}}, crtc: tCrtc, conn: connector{id: tConn, name: "DP-1"}, mode: modeInfo{HDisplay: 200, VDisplay: 100, VRefresh: 60}, log: zerowrap.Default(),
82 crtcProps: map[string]uint32{"MODE_ID": pMode, "ACTIVE": pActive, "VRR_ENABLED": pVRR}, connCrtc: pConnCrtc, vrrProp: pVRR,
83 primary: primary, cursor: cur, fbs: [2]uint32{70, 71}, clientFBs: map[uint64]*clientFB{}, scanout: true, tearing: true, asyncFence: true, planeRect: fullPlaneRect(200, 100)}
84 return o, k, &commits, commitMu
85}
86
87// eventOf is the flip event of commit c.
88func eventOf(c commitRec) flipEvent { return flipEvent{crtc: tCrtc, user: c.user} }
89
90func objCount(c commitRec) int { return len(c.req.objs) }
91
92func TestFrameCommitCarriesPrimaryFenceCursorAndVRR(t *testing.T) {
93 o, _, commits := testOutput(t)
94 o.cursor.image = true
95 o.cursor.Move(12, 34)
96 fence, w, err := os.Pipe()
97 if err != nil {
98 t.Fatal(err)
99 }
100 defer fence.Close()
101 defer w.Close()
102 if err := o.commitFrame(70, fence, false, true, pendingFrame{}); err != nil {
103 t.Fatal(err)
104 }
105 c := (*commits)[0]
106 if c.flags != atomicNonblock|flipEventFlag || c.user&3 != userFrame || c.user>>userKindBits != o.frame.pendingSerial {
107 t.Fatalf("flags %#x user %d", c.flags, c.user)
108 }
109 if v, _ := c.req.value(tPrimary, pFB); v != 70 {
110 t.Fatalf("primary fb %d", v)
111 }
112 if v, ok := c.req.value(tPrimary, pFence); !ok || v != uint64(fence.Fd()) {
113 t.Fatalf("fence %d %v", v, ok)
114 }
115 if v, _ := c.req.value(tCursor, 14); v != 12 {
116 t.Fatalf("cursor x %d", v)
117 }
118 if v, _ := c.req.value(tCrtc, pVRR); v != 1 {
119 t.Fatalf("vrr %d", v)
120 }
121 if !o.frame.pendingCommit() || !o.vrrOn || o.cursor.applied.x != 12 {
122 t.Fatalf("state pending=%v vrr=%v cursor=%+v", o.frame.pendingCommit(), o.vrrOn, o.cursor.applied)
123 }
124}
125
126func TestAsyncCommitHasOnlyPrimaryFBAndFence(t *testing.T) {
127 o, _, commits := testOutput(t)
128 fence, w, _ := os.Pipe()
129 defer fence.Close()
130 defer w.Close()
131 if err := o.commitFrame(70, fence, true, false, pendingFrame{}); err != nil {
132 t.Fatal(err)
133 }
134 c := (*commits)[0]
135 if c.flags&flipAsyncFlag == 0 {
136 t.Fatal("not async")
137 }
138 if objCount(c) != 1 || c.req.objs[0] != tPrimary || len(c.req.props[0]) != 2 {
139 t.Fatalf("async commit %+v", c.req)
140 }
141}
142
143func TestAsyncFallsBackToSyncWhenCursorMoves(t *testing.T) {
144 o, _, commits := testOutput(t)
145 o.cursor.image = true
146 o.cursor.Move(5, 5)
147 if err := o.commitFrame(70, nil, true, false, pendingFrame{}); err != nil {
148 t.Fatal(err)
149 }
150 if (*commits)[0].flags&flipAsyncFlag != 0 {
151 t.Fatal("async commit moved the cursor")
152 }
153}
154
155func TestAsyncWithoutFenceSupportStaysSync(t *testing.T) {
156 o, _, commits := testOutput(t)
157 o.asyncFence = false
158 fence, w, _ := os.Pipe()
159 defer fence.Close()
160 defer w.Close()
161 if err := o.commitFrame(70, fence, true, false, pendingFrame{}); err != nil {
162 t.Fatal(err)
163 }
164 if (*commits)[0].flags&flipAsyncFlag != 0 {
165 t.Fatal("async commit with a fence the driver refuses")
166 }
167}
168
169func TestScanoutAsyncRefusedRetriesSync(t *testing.T) {
170 o, _, commits := testOutput(t, unix.EINVAL)
171 o.shown, o.vrrOn = 1, true
172 buf := &ports.DMABuf{ID: 9}
173 o.clientFBs[9] = &clientFB{fbID: 80}
174 ok, err := o.commitScanout(80, ports.SurfaceContent{DMABuf: buf, Async: true}, pendingFrame{})
175 if !ok || err != nil {
176 t.Fatalf("ok=%v err=%v", ok, err)
177 }
178 if len(*commits) != 2 || (*commits)[0].flags&flipAsyncFlag == 0 || (*commits)[1].flags&flipAsyncFlag != 0 {
179 t.Fatalf("commits %+v", *commits)
180 }
181 if !o.clientFBs[9].noAsync || o.queued != 9 {
182 t.Fatalf("noAsync=%v queued=%d", o.clientFBs[9].noAsync, o.queued)
183 }
184}
185
186func TestCursorCommitWaitsForEvent(t *testing.T) {
187 o, _, commits := testOutput(t)
188 o.cursor.image = true
189 o.cursor.Move(1, 1)
190 if err := o.commitState(false); err != nil {
191 t.Fatal(err)
192 }
193 c := (*commits)[0]
194 if c.flags != atomicNonblock|flipEventFlag || c.user&3 != userState || !o.frame.pendingCommit() {
195 t.Fatalf("cursor commit flags %#x user %d pending %v", c.flags, c.user, o.frame.pendingCommit())
196 }
197 // Its event is not a frame: nothing is reported.
198 seen := map[ports.WindowID]uint64{}
199 o.completed(eventOf(c), seen)
200 if o.frame.pendingCommit() || o.reports.Len() != 0 || o.flips != 0 {
201 t.Fatalf("pending=%v unsent=%d flips=%d", o.frame.pendingCommit(), o.reports.Len(), o.flips)
202 }
203 // Nothing changed: no commit.
204 if err := o.commitState(false); err != nil || len(*commits) != 1 {
205 t.Fatalf("idle commit: %v, %d commits", err, len(*commits))
206 }
207}
208
209// Under VRR with a game shown, a cursor move rides on the next frame: a
210// cursor-only commit would hold the panel for its slowest refresh. A game
211// that stops drawing still gets the cursor after cursorMinInterval.
212func TestVRRGameCursorWaitsForFrame(t *testing.T) {
213 o, _, commits := testOutput(t)
214 o.cursor.image = true
215 o.vrrOn, o.vrrGame = true, true
216 o.lastFrame = time.Now()
217 o.cursor.Move(5, 5)
218 if err := o.commitState(o.stateVRR()); err != nil {
219 t.Fatal(err)
220 }
221 if len(*commits) != 0 || !o.cursorHeld || o.frame.pendingCommit() {
222 t.Fatalf("cursor committed alone: %d commits held=%v", len(*commits), o.cursorHeld)
223 }
224 // The next frame carries the cursor.
225 if err := o.commitFrame(70, nil, false, true, pendingFrame{}); err != nil {
226 t.Fatal(err)
227 }
228 if v, _ := (*commits)[0].req.value(tCursor, 14); v != 5 || o.cursorHeld {
229 t.Fatalf("frame cursor x %d held=%v", v, o.cursorHeld)
230 }
231 o.completed(eventOf((*commits)[0]), map[ports.WindowID]uint64{})
232 // The game stalls: the cursor commits alone after the interval.
233 o.lastFrame = time.Now().Add(-cursorMinInterval)
234 o.cursor.Move(8, 8)
235 if err := o.commitState(o.stateVRR()); err != nil {
236 t.Fatal(err)
237 }
238 if len(*commits) != 2 || o.cursorHeld {
239 t.Fatalf("stalled game: %d commits held=%v", len(*commits), o.cursorHeld)
240 }
241 if v, _ := (*commits)[1].req.value(tCursor, 14); v != 8 {
242 t.Fatalf("cursor x %d", v)
243 }
244}
245
246// A held move reversed before the next frame holds nothing: the cursor
247// timer is not armed again.
248func TestHeldCursorMovedBackClearsHold(t *testing.T) {
249 o, _, commits := testOutput(t)
250 o.cursor.image = true
251 o.vrrOn, o.vrrGame, o.lastFrame = true, true, time.Now()
252 o.cursor.Move(0, 0)
253 o.cursor.applied = o.cursor.desired() // on screen at (0, 0)
254 o.cursor.Move(5, 5)
255 if err := o.commitState(o.stateVRR()); err != nil || !o.cursorHeld {
256 t.Fatalf("err %v held=%v", err, o.cursorHeld)
257 }
258 o.cursor.Move(0, 0)
259 if err := o.commitState(o.stateVRR()); err != nil || o.cursorHeld || len(*commits) != 0 {
260 t.Fatalf("err %v held=%v commits %d", err, o.cursorHeld, len(*commits))
261 }
262}
263
264// Without a game, the cursor keeps its own commits.
265func TestCursorCommitsAloneWithoutGame(t *testing.T) {
266 o, _, commits := testOutput(t)
267 o.cursor.image = true
268 o.vrrOn, o.lastFrame = true, time.Now()
269 o.composedSince = time.Now()
270 o.cursor.Move(5, 5)
271 if err := o.commitState(o.stateVRR()); err != nil || len(*commits) != 1 || o.cursorHeld {
272 t.Fatalf("err %v, %d commits held=%v", err, len(*commits), o.cursorHeld)
273 }
274}
275
276func TestBusyCommitStaysPending(t *testing.T) {
277 o, _, _ := testOutput(t, unix.EBUSY)
278 err := o.commitFrame(70, nil, false, false, pendingFrame{})
279 enabled := true
280 if !o.commitFailed(err, &enabled) || !o.frame.pendingCommit() || !enabled {
281 t.Fatalf("EBUSY: pending=%v enabled=%v", o.frame.pendingCommit(), enabled)
282 }
283}
284
285func TestFlipReportsTimestampAndShownFrame(t *testing.T) {
286 o, _, commits := testOutput(t)
287 o.vrrOn = false
288 o.mode.Clock, o.mode.HTotal, o.mode.VTotal = 0, 0, 0
289 if err := o.commitFrame(70, nil, false, false, pendingFrame{shows: map[ports.WindowID]uint64{1: 4}}); err != nil {
290 t.Fatal(err)
291 }
292 o.completed(flipEvent{user: (*commits)[0].user, when: 5 * time.Second, seq: 11}, map[ports.WindowID]uint64{1: 4})
293 if o.reports.Len() != 1 {
294 t.Fatalf("reports %d", o.reports.Len())
295 }
296 f := o.reports.Unsent()[0].Flip
297 if f == nil || f.When != 5*time.Second || f.Seq != 11 || f.Refresh != time.Second/60 || f.Shows[1] != 4 || !f.HardwareClock {
298 t.Fatalf("flip %+v", f)
299 }
300}
301
302// Unchanged reports share an immutable snapshot; a later content update
303// must not change an earlier report, even after it has been sent.
304func TestReportSeenAllocations(t *testing.T) {
305 o, _, _ := testOutput(t)
306 seen := map[ports.WindowID]uint64{1: 1, 2: 2}
307 o.report(nil, seen)
308 // Keep the queue bounded without measuring the queue's growth.
309 if allocs := testing.AllocsPerRun(100, func() { o.report(nil, seen) }); allocs != 0 {
310 t.Errorf("unchanged report: %.1f allocs, want 0", allocs)
311 } else {
312 t.Logf("unchanged report: %.1f allocs", allocs)
313 }
314 previous := o.reports.Unsent()[0].Seen
315 seen[1] = 3
316 o.report(nil, seen)
317 if previous[1] != 1 || o.reports.Unsent()[0].Seen[1] != 3 {
318 t.Fatalf("snapshot mutated: old %v new %v", previous, o.reports.Unsent()[0].Seen)
319 }
320}
321
322func TestShownBySnapshotAllocations(t *testing.T) {
323 o := &Output{}
324 s := ports.Scene{OutputWidth: 10, OutputHeight: 10, Windows: []ports.SceneWindow{{ID: 1, Rect: ports.Rect{W: 10, H: 10}}}}
325 seen := map[ports.WindowID]uint64{1: 1, 2: 2}
326 previous := o.shownBy(s, seen)
327 if allocs := testing.AllocsPerRun(100, func() { o.shownBy(s, seen) }); allocs != 0 {
328 t.Errorf("unchanged shows: %.1f allocs, want 0", allocs)
329 }
330 seen[1] = 3
331 current := o.shownBy(s, seen)
332 if previous[1] != 1 || current[1] != 3 {
333 t.Fatalf("snapshot mutated: old %v new %v", previous, current)
334 }
335 direct := o.directShownBy(1, 3)
336 if allocs := testing.AllocsPerRun(100, func() { o.directShownBy(1, 3) }); allocs != 0 {
337 t.Errorf("unchanged direct shows: %.1f allocs, want 0", allocs)
338 }
339 o.directShownBy(1, 4)
340 if direct[1] != 3 {
341 t.Fatalf("direct snapshot mutated: %v", direct)
342 }
343}
344
345func TestReportsKeepEveryFlip(t *testing.T) {
346 o, _, _ := testOutput(t)
347 seen := map[ports.WindowID]uint64{1: 1}
348 o.report(&ports.FlipInfo{Seq: 1}, seen)
349 o.report(nil, seen)
350 o.report(&ports.FlipInfo{Seq: 2}, seen)
351 o.report(nil, map[ports.WindowID]uint64{1: 3})
352 if o.reports.Len() != 2 || o.reports.Unsent()[0].Flip.Seq != 1 || o.reports.Unsent()[1].Flip.Seq != 2 || o.reports.Unsent()[1].Seen[1] != 3 {
353 t.Fatalf("unsent %+v", o.reports.Unsent())
354 }
355 for i := range presented.MaxUnsent + 3 {
356 o.report(&ports.FlipInfo{Seq: uint64(10 + i), Shows: map[ports.WindowID]uint64{}}, seen)
357 }
358 if o.reports.Len() != presented.MaxUnsent || o.reports.Unsent()[0].Flip.Merged == 0 {
359 t.Fatalf("cap: %d reports, first merged %d", o.reports.Len(), o.reports.Unsent()[0].Flip.Merged)
360 }
361}
362
363func TestModesetDisablesCursorAndStrayPlanes(t *testing.T) {
364 o, k, commits := testOutput(t)
365 stray := &plane{id: tOverlay, typ: planeOverlay, props: planeProps}
366 o.stray = []*plane{stray}
367 k.EXPECT().createBlob(mock.Anything).Return(99, nil).Once()
368 k.EXPECT().objProps(uint32(tOverlay), uint32(objPlane)).Return(map[string][2]uint64{"CRTC_ID": {pCrtcID, tCrtc}}, nil).Once()
369 if err := o.modeset(); err != nil {
370 t.Fatal(err)
371 }
372 c := (*commits)[0]
373 if c.flags != atomicAllowModes {
374 t.Fatalf("flags %#x", c.flags)
375 }
376 if v, _ := c.req.value(tCrtc, pMode); v != 99 {
377 t.Fatalf("mode blob %d", v)
378 }
379 for _, p := range []uint32{tCursor, tOverlay} {
380 if v, ok := c.req.value(p, pFB); !ok || v != 0 {
381 t.Fatalf("plane %d fb %d %v", p, v, ok)
382 }
383 }
384 if v, _ := c.req.value(tConn, pConnCrtc); v != tCrtc {
385 t.Fatalf("connector crtc %d", v)
386 }
387 // The next modeset replaces the blob.
388 k.EXPECT().createBlob(mock.Anything).Return(100, nil).Once()
389 k.EXPECT().objProps(uint32(tOverlay), uint32(objPlane)).Return(map[string][2]uint64{}, nil).Once()
390 k.EXPECT().destroyBlob(uint32(99)).Return(nil).Once()
391 if err := o.modeset(); err != nil {
392 t.Fatal(err)
393 }
394 if _, ok := (*commits)[1].req.value(tOverlay, pFB); ok {
395 t.Fatal("plane on another CRTC turned off")
396 }
397}
398
399func TestAsyncProbeAfterModeset(t *testing.T) {
400 o, _, commits := testOutput(t, unix.EINVAL)
401 o.asyncFence = true
402 r := portsmocks.NewMockRenderer(t)
403 fence, w, _ := os.Pipe()
404 defer w.Close()
405 r.EXPECT().UseTarget(mock.Anything).Return().Once()
406 r.EXPECT().Render(mock.Anything, mock.Anything).Return(fence, nil).Once()
407 o.probeAsync(r)
408 c := (*commits)[0]
409 if c.flags != atomicTestOnly|flipAsyncFlag {
410 t.Fatalf("flags %#x", c.flags)
411 }
412 if _, ok := c.req.value(tPrimary, pFence); !ok {
413 t.Fatal("probe without a fence")
414 }
415 if o.asyncFence {
416 t.Fatal("refused probe kept asyncFence")
417 }
418 if _, err := fence.Stat(); err == nil {
419 t.Fatal("probe fence not closed")
420 }
421 o.probeAsync(r) // once only
422}
423
424func TestVRRToggleOnlyInsideFrames(t *testing.T) {
425 o, _, commits := testOutput(t)
426 o.vrrOn = true
427 o.composedSince = time.Now().Add(-time.Second)
428 if o.wantVRR(false) {
429 t.Fatal("VRR kept after vrrHold of composition")
430 }
431 o.composedSince = time.Now()
432 if !o.wantVRR(false) {
433 t.Fatal("VRR dropped before vrrHold")
434 }
435 if err := o.commitState(false); err != nil {
436 t.Fatal(err)
437 }
438 if v, ok := (*commits)[0].req.value(tCrtc, pVRR); !ok || v != 0 || (*commits)[0].flags&flipEventFlag == 0 {
439 t.Fatalf("vrr off commit %+v", (*commits)[0])
440 }
441}
442
443// A composed fullscreen frame keeps VRR and restarts the hold; state
444// commits (cursor, vrrOff timer) keep it too until the fullscreen ends.
445func TestVRRKeptWhileFullscreenComposed(t *testing.T) {
446 o, _, _ := testOutput(t)
447 o.vrrOn = true
448 o.composedSince = time.Now().Add(-time.Second)
449 if !o.wantVRR(true) || !o.composedSince.IsZero() {
450 t.Fatal("composed fullscreen dropped VRR")
451 }
452 // Cursor-only state commit between frames.
453 if !o.stateVRR() {
454 t.Fatal("state commit dropped VRR during fullscreen")
455 }
456 // Fullscreen ends, a tiled window stays on the overlay plane: the
457 // hold starts, then VRR goes off.
458 o.shown = 9
459 if !o.wantVRR(false) || o.composedSince.IsZero() {
460 t.Fatal("hold not started on exit")
461 }
462 o.composedSince = time.Now().Add(-time.Second)
463 if o.stateVRR() {
464 t.Fatal("VRR kept after the hold (overlay buffer taken for a game)")
465 }
466}
467
468// A fullscreen game on a workspace that is left keeps VRR only for the
469// hold: its hidden window no longer counts as a game frame, and its
470// commits do not draw frames.
471func TestVRRHiddenGameOnlyHolds(t *testing.T) {
472 o, _, _ := testOutput(t)
473 game := ports.SceneWindow{ID: 1, Rect: ports.Rect{W: 100, H: 50}, Fullscreen: true}
474 shown := ports.Scene{OutputWidth: 100, OutputHeight: 50, Windows: []ports.SceneWindow{game}}
475 o.vrrOn = true
476 if !o.wantVRR(fullscreenShown(&shown)) {
477 t.Fatal("VRR off while the game is shown")
478 }
479 game.Hidden, game.Rect = true, ports.Rect{}
480 left := ports.Scene{OutputWidth: 100, OutputHeight: 50, Windows: []ports.SceneWindow{game, {ID: 2, Rect: ports.Rect{W: 100, H: 50}}}}
481 if fullscreenShown(&left) || left.Shows(1) {
482 t.Fatal("hidden game still drives frames")
483 }
484 if !o.wantVRR(fullscreenShown(&left)) {
485 t.Fatal("VRR dropped before the hold")
486 }
487 o.composedSince = time.Now().Add(-vrrHold - time.Millisecond)
488 if o.stateVRR() {
489 t.Fatal("VRR kept after the hold for a hidden game")
490 }
491}
492
493// A VRR refusal on a composed frame with an overlay retries with the
494// overlay: the composed image left its window out.
495func TestVRRRefusedKeepsOverlay(t *testing.T) {
496 o, k, commits := overlayOutput(t, unix.EINVAL)
497 k.EXPECT().addFB(mock.Anything, uint32(fourccXRGB)).Return(88, nil).Once()
498 o.overlay.formats = []ports.DMABufFormat{{Format: fourccXRGB}}
499 s, c := overlayScene()
500 ov, _ := o.overlayFrame(s, c)
501 if ov.fb == 0 {
502 t.Fatal("no overlay")
503 }
504 if err := o.commitWith(70, nil, false, true, pendingFrame{queued: ov.buf}, ov); err != nil {
505 t.Fatal(err)
506 }
507 if len(*commits) != 2 || o.vrrProp != 0 {
508 t.Fatalf("%d commits, vrrProp %d", len(*commits), o.vrrProp)
509 }
510 if v, _ := (*commits)[1].req.value(tOverlay, pFB); v != 88 || o.overlayOn != ov.buf {
511 t.Fatalf("retry overlay fb %d, overlayOn %d", v, o.overlayOn)
512 }
513}
514
515func TestReadPlanes(t *testing.T) {
516 k := newMockkms(t)
517 k.EXPECT().planes().Return([]planeRes{{id: 1, possible: 1}, {id: 2, possible: 2}, {id: 3, possible: 3}}, nil)
518 k.EXPECT().objProps(uint32(1), uint32(objPlane)).Return(map[string][2]uint64{"type": {20, planePrimary}, "FB_ID": {21, 0}, "IN_FORMATS": {22, 5}}, nil)
519 k.EXPECT().objProps(uint32(3), uint32(objPlane)).Return(map[string][2]uint64{"type": {20, planeCursor}}, nil)
520 k.EXPECT().getBlob(uint32(5)).Return(nil, nil)
521 ps, err := readPlanes(k, 0)
522 if err != nil || len(ps) != 2 || ps[0].typ != planePrimary || ps[0].prop("FB_ID") != 21 || ps[1].typ != planeCursor {
523 t.Fatalf("planes %+v %v", ps, err)
524 }
525}
526
527func TestVRRRefusedIsDisabled(t *testing.T) {
528 o, _, commits := testOutput(t, unix.EINVAL)
529 if err := o.commitFrame(70, nil, false, true, pendingFrame{}); err != nil {
530 t.Fatal(err)
531 }
532 if len(*commits) != 2 || o.vrrProp != 0 || !o.frame.pendingCommit() {
533 t.Fatalf("%d commits, vrrProp %d", len(*commits), o.vrrProp)
534 }
535 if v, _ := (*commits)[1].req.value(tCrtc, pVRR); v != 0 {
536 t.Fatal("retry kept VRR on")
537 }
538}
539
540func TestBufferRefusedIsNotBlamedOnVRR(t *testing.T) {
541 o, _, _ := testOutput(t, unix.EINVAL, unix.EINVAL)
542 o.clientFBs[9] = &clientFB{fbID: 80}
543 ok, err := o.commitScanout(80, ports.SurfaceContent{DMABuf: &ports.DMABuf{ID: 9}}, pendingFrame{})
544 if ok || err != nil || o.vrrProp == 0 || o.clientFBs[9].failed != "flip_refused" {
545 t.Fatalf("ok=%v err=%v vrrProp=%d failed=%q", ok, err, o.vrrProp, o.clientFBs[9].failed)
546 }
547}
548
549// Run: while a frame is in flight its buffers may still be read by the
550// GPU, so a newer content is reported only after the flip event; every
551// frame fence is closed.
552func TestRunReportsSeenAfterFlip(t *testing.T) {
553 o, k, commits, commitMu := testOutputMu(t)
554 o.cursor, o.tearing = nil, false
555 flips := make(chan flipEvent, 1)
556 o.flipped = flips
557 r := portsmocks.NewMockRenderer(t)
558 var mu sync.Mutex
559 var fences []*os.File
560 pipeBuf := func() ports.DMABuf {
561 f, w, _ := os.Pipe()
562 w.Close()
563 return ports.DMABuf{Planes: []ports.DMABufPlane{{File: f}}}
564 }
565 r.EXPECT().SetHDR(float64(0)).Return().Maybe()
566 r.EXPECT().ExportTargets(2, mock.Anything).Return([]ports.DMABuf{pipeBuf(), pipeBuf()}, nil).Once()
567 r.EXPECT().UseTarget(mock.Anything).Return()
568 r.EXPECT().Render(mock.Anything, mock.Anything).RunAndReturn(func(ports.Scene, map[ports.WindowID]ports.SurfaceContent) (*os.File, error) {
569 f, w, _ := os.Pipe()
570 _, _ = w.Write([]byte{1}) // readable: signalled
571 w.Close()
572 mu.Lock()
573 fences = append(fences, f)
574 mu.Unlock()
575 return f, nil
576 })
577 r.EXPECT().Close().Return().Once()
578 k.EXPECT().addFB(mock.Anything, uint32(fourccXRGB)).Return(70, nil).Twice()
579 k.EXPECT().createBlob(mock.Anything).Return(99, nil)
580 k.EXPECT().destroyBlob(mock.Anything).Return(nil).Maybe()
581 k.EXPECT().rmFB(mock.Anything).Return(nil).Maybe()
582 frameCommits := func() int {
583 commitMu.Lock()
584 defer commitMu.Unlock()
585 n := 0
586 for _, c := range *commits {
587 if c.user&3 == userFrame {
588 n++
589 }
590 }
591 return n
592 }
593 scenes := make(chan ports.Scene, 1)
594 contents := make(chan ports.SurfaceContent, 1)
595 presented := make(chan ports.OutputPresented, 8)
596 ctx, cancel := context.WithCancel(context.Background())
597 done := make(chan error, 1)
598 go func() {
599 done <- o.Run(ctx, func(int, int) (ports.Renderer, error) { return r, nil }, nil, make(chan bool), scenes, contents, nil, presented, nil, nil)
600 }()
601 scenes <- ports.Scene{OutputWidth: 200, OutputHeight: 100, Windows: []ports.SceneWindow{{ID: 1, Rect: ports.Rect{W: 10, H: 10}}}}
602 waitFor(t, func() bool { return frameCommits() == 1 })
603 // Frame 1 is in flight: content 3 replaces what it may be reading.
604 contents <- ports.SurfaceContent{ID: 1, Seq: 3, SHM: &ports.SHMBuffer{}}
605 select {
606 case p := <-presented:
607 t.Fatalf("report while a frame is in flight: %+v", p)
608 case <-time.After(30 * time.Millisecond):
609 }
610 commitMu.Lock()
611 last := (*commits)[len(*commits)-1]
612 commitMu.Unlock()
613 flips <- flipEvent{crtc: tCrtc, user: last.user, when: time.Second, seq: 1}
614 select {
615 case p := <-presented:
616 if p.Flip == nil || p.Flip.When != time.Second || p.Seen[1] != 3 {
617 t.Fatalf("report %+v", p)
618 }
619 case <-time.After(2 * time.Second):
620 t.Fatal("no report after the flip")
621 }
622 cancel()
623 if err := <-done; err != nil {
624 t.Fatal(err)
625 }
626 mu.Lock()
627 defer mu.Unlock()
628 if len(fences) < 3 {
629 t.Fatalf("%d renders", len(fences))
630 }
631 for _, f := range fences {
632 if _, err := f.Stat(); err == nil {
633 t.Fatal("frame fence left open")
634 }
635 }
636}
637
638// Run: a fullscreen window composed under an overlay layer keeps VRR on in
639// the frame commit; a tiled scene turns it off only after vrrHold.
640func TestRunComposedFullscreenKeepsVRR(t *testing.T) {
641 o, k, commits, commitMu := testOutputMu(t)
642 o.cursor, o.tearing = nil, false
643 flips := make(chan flipEvent, 1)
644 o.flipped = flips
645 r := portsmocks.NewMockRenderer(t)
646 pipeBuf := func() ports.DMABuf {
647 f, w, _ := os.Pipe()
648 w.Close()
649 return ports.DMABuf{Planes: []ports.DMABufPlane{{File: f}}}
650 }
651 r.EXPECT().SetHDR(float64(0)).Return().Maybe()
652 r.EXPECT().ExportTargets(2, mock.Anything).Return([]ports.DMABuf{pipeBuf(), pipeBuf()}, nil).Once()
653 r.EXPECT().UseTarget(mock.Anything).Return()
654 r.EXPECT().Render(mock.Anything, mock.Anything).Return(nil, nil)
655 r.EXPECT().Close().Return().Once()
656 k.EXPECT().addFB(mock.Anything, uint32(fourccXRGB)).Return(70, nil).Twice()
657 k.EXPECT().createBlob(mock.Anything).Return(99, nil)
658 k.EXPECT().destroyBlob(mock.Anything).Return(nil).Maybe()
659 k.EXPECT().rmFB(mock.Anything).Return(nil).Maybe()
660 lastFrame := func() (commitRec, int) {
661 commitMu.Lock()
662 defer commitMu.Unlock()
663 n, last := 0, commitRec{}
664 for _, c := range *commits {
665 if c.user&3 == userFrame {
666 n, last = n+1, c
667 }
668 }
669 return last, n
670 }
671 scenes := make(chan ports.Scene, 1)
672 ctx, cancel := context.WithCancel(context.Background())
673 done := make(chan error, 1)
674 go func() {
675 done <- o.Run(ctx, func(int, int) (ports.Renderer, error) { return r, nil }, nil, make(chan bool), scenes, nil, nil, make(chan ports.OutputPresented, 8), nil, nil)
676 }()
677 scenes <- ports.Scene{OutputWidth: 200, OutputHeight: 100,
678 Windows: []ports.SceneWindow{{ID: 1, Rect: ports.Rect{W: 200, H: 100}, Fullscreen: true}},
679 Layers: []ports.SceneLayer{{ID: 5, Layer: ports.LayerOverlay, Rect: ports.Rect{W: 10, H: 10}}}}
680 waitFor(t, func() bool { _, n := lastFrame(); return n == 1 })
681 last, _ := lastFrame()
682 if v, ok := last.req.value(tCrtc, pVRR); !ok || v != 1 {
683 t.Fatalf("composed fullscreen frame VRR %d %v", v, ok)
684 }
685 flips <- flipEvent{crtc: tCrtc, user: last.user, when: time.Second, seq: 1}
686 scenes <- ports.Scene{OutputWidth: 200, OutputHeight: 100, Windows: []ports.SceneWindow{{ID: 1, Rect: ports.Rect{W: 100, H: 100}}}}
687 waitFor(t, func() bool { _, n := lastFrame(); return n == 2 })
688 last, _ = lastFrame()
689 if v, _ := last.req.value(tCrtc, pVRR); v != 1 {
690 t.Fatal("VRR dropped before vrrHold")
691 }
692 flips <- flipEvent{crtc: tCrtc, user: last.user, when: 2 * time.Second, seq: 2}
693 // The vrrOff timer turns VRR off with a state commit after the hold.
694 waitFor(t, func() bool {
695 commitMu.Lock()
696 defer commitMu.Unlock()
697 c := (*commits)[len(*commits)-1]
698 v, ok := c.req.value(tCrtc, pVRR)
699 return c.user&3 == userState && ok && v == 0
700 })
701 cancel()
702 if err := <-done; err != nil {
703 t.Fatal(err)
704 }
705}
706
707func waitFor(t *testing.T, cond func() bool) {
708 t.Helper()
709 deadline := time.Now().Add(2 * time.Second)
710 for !cond() {
711 if time.Now().After(deadline) {
712 t.Fatal("timed out")
713 }
714 time.Sleep(2 * time.Millisecond)
715 }
716}
717
718// The stats tick lets the renderer free what the output no longer draws,
719// even while nothing renders; a failed trim stops the output.
720func TestRunTrimsRendererOnStatsTick(t *testing.T) {
721 o, k, _ := testOutput(t)
722 o.cursor, o.tearing, o.fbs = nil, false, [2]uint32{}
723 ticks := make(chan time.Time)
724 clk := portsmocks.NewMockClock(t)
725 tk := portsmocks.NewMockTicker(t)
726 clk.EXPECT().NewTicker(10 * time.Second).Return(tk).Once()
727 tk.EXPECT().C().Return(ticks)
728 tk.EXPECT().Stop().Return().Once()
729 now := time.Unix(1000, 0)
730 clk.EXPECT().Now().Return(now)
731 o.clock = clk
732 r := portsmocks.NewMockRenderer(t)
733 r.EXPECT().SetHDR(float64(0)).Return().Maybe()
734 r.EXPECT().Close().Return().Once()
735 trimmed := make(chan struct{}, 2)
736 failure := os.ErrInvalid
737 calls := 0 // Run's goroutine only
738 r.EXPECT().Trim(now).RunAndReturn(func(time.Time) error {
739 trimmed <- struct{}{}
740 if calls++; calls == 2 {
741 return failure
742 }
743 return nil
744 }).Twice()
745 k.EXPECT().rmFB(mock.Anything).Return(nil).Maybe()
746 k.EXPECT().destroyBlob(mock.Anything).Return(nil).Maybe()
747 // Switched away from the start: no target, no frame.
748 active := make(chan bool, 1)
749 active <- false
750 done := make(chan error, 1)
751 go func() {
752 done <- o.Run(context.Background(), func(int, int) (ports.Renderer, error) { return r, nil }, nil, active, nil, nil, nil, make(chan ports.OutputPresented, 1), nil, nil)
753 }()
754 ticks <- now
755 <-trimmed
756 ticks <- now
757 if err := <-done; !errors.Is(err, failure) {
758 t.Fatalf("run: %v", err)
759 }
760}
761
762// A render failure must complete captures queued before the first frame.
763func TestCaptureRenderFailureClosesPendingRequest(t *testing.T) {
764 o, k, _ := testOutput(t)
765 o.cursor, o.tearing = nil, false
766 r := portsmocks.NewMockRenderer(t)
767 boom := errors.New("render failed")
768 r.EXPECT().SetHDR(float64(0)).Return().Once()
769 r.EXPECT().UseTarget(mock.Anything).Return()
770 makeBuf := func() ports.DMABuf {
771 f, err := os.CreateTemp(t.TempDir(), "target")
772 if err != nil {
773 t.Fatal(err)
774 }
775 t.Cleanup(func() { _ = f.Close() })
776 return ports.DMABuf{Planes: []ports.DMABufPlane{{File: f}}}
777 }
778 r.EXPECT().ExportTargets(2, mock.Anything).Return([]ports.DMABuf{makeBuf(), makeBuf()}, nil).Once()
779 r.EXPECT().Render(ports.Scene{Background: "#000000"}, mock.Anything).Return(nil, nil).Twice()
780 r.EXPECT().Render(mock.MatchedBy(func(s ports.Scene) bool { return s.Background != "#000000" }), mock.Anything).Return(nil, boom).Once()
781 r.EXPECT().Close().Return().Once()
782 k.EXPECT().addFB(mock.Anything, uint32(fourccXRGB)).Return(70, nil).Twice()
783 k.EXPECT().createBlob(mock.Anything).Return(99, nil)
784 k.EXPECT().destroyBlob(mock.Anything).Return(nil).Maybe()
785 k.EXPECT().rmFB(mock.Anything).Return(nil).Maybe()
786 ctx, cancel := context.WithCancel(context.Background())
787 defer cancel()
788 scenes := make(chan ports.Scene, 1)
789 requests := make(chan ports.CaptureRequest)
790 replies := make(chan ports.CaptureDone, 1)
791 f, err := os.CreateTemp(t.TempDir(), "capture")
792 if err != nil {
793 t.Fatal(err)
794 }
795 done := make(chan error, 1)
796 go func() {
797 done <- o.Run(ctx, func(int, int) (ports.Renderer, error) { return r, nil }, nil, nil, scenes, nil, nil, nil, requests, replies)
798 }()
799 // The unbuffered handoff proves Run owns the request before any scene.
800 select {
801 case requests <- ports.CaptureRequest{ID: 9, Dst: ports.SHMBuffer{File: f}}:
802 case <-time.After(2 * time.Second):
803 t.Fatal("output did not accept capture")
804 }
805 scenes <- ports.Scene{Seq: 1, Scale: 1}
806 select {
807 case err := <-done:
808 if !errors.Is(err, boom) {
809 t.Fatalf("run error %v", err)
810 }
811 case <-time.After(2 * time.Second):
812 t.Fatal("output did not stop after render failure")
813 }
814 select {
815 case result := <-replies:
816 if result.ID != 9 || result.Err == nil {
817 t.Fatalf("capture result %+v", result)
818 }
819 default:
820 t.Fatal("render failure lost capture completion")
821 }
822 if _, err := f.Stat(); err == nil {
823 t.Fatal("render failure leaked capture descriptor")
824 }
825}
826
827// A request must render even when the output already has an unchanged scene.
828func TestCaptureForcesDRMComposition(t *testing.T) {
829 o, k, commits, commitMu := testOutputMu(t)
830 o.cursor, o.tearing = nil, false
831 flips := make(chan flipEvent, 2)
832 o.flipped = flips
833 r := &portsmocks.MockRenderer{}
834 makeBuf := func() ports.DMABuf {
835 f, w, _ := os.Pipe()
836 w.Close()
837 return ports.DMABuf{Planes: []ports.DMABufPlane{{File: f}}}
838 }
839 r.EXPECT().SetHDR(float64(0)).Return().Maybe() // SDR output
840 r.EXPECT().ExportTargets(2, mock.Anything).Return([]ports.DMABuf{makeBuf(), makeBuf()}, nil).Once()
841 r.EXPECT().UseTarget(mock.Anything).Return()
842 rendered := make(chan ports.Scene, 3)
843 r.EXPECT().Render(mock.Anything, mock.Anything).RunAndReturn(func(s ports.Scene, _ map[ports.WindowID]ports.SurfaceContent) (*os.File, error) {
844 rendered <- s
845 return nil, nil
846 })
847 frame := portsmocks.NewMockCaptureFrame(t)
848 frame.EXPECT().Done().Return(nil)
849 frame.EXPECT().Read(image.Rect(0, 0, 2, 2), mock.MatchedBy(func(p []byte) bool { return len(p) >= 16 }), 8).RunAndReturn(func(_ image.Rectangle, dst []byte, _ int) error { copy(dst, []byte{3, 4, 5, 255}); return nil }).Once()
850 r.EXPECT().BeginCapture().Return(frame, nil).Once()
851 r.EXPECT().EndCapture(frame).Return().Once()
852 r.EXPECT().Close().Return().Once()
853 k.EXPECT().addFB(mock.Anything, uint32(fourccXRGB)).Return(70, nil).Twice()
854 k.EXPECT().createBlob(mock.Anything).Return(99, nil)
855 k.EXPECT().destroyBlob(mock.Anything).Return(nil).Maybe()
856 k.EXPECT().rmFB(mock.Anything).Return(nil).Maybe()
857 scenes := make(chan ports.Scene, 1)
858 requests := make(chan ports.CaptureRequest, 1)
859 replies := make(chan ports.CaptureDone, 1)
860 ctx, cancel := context.WithCancel(context.Background())
861 done := make(chan error, 1)
862 go func() {
863 done <- o.Run(ctx, func(int, int) (ports.Renderer, error) { return r, nil }, nil, nil, scenes, nil, nil, make(chan ports.OutputPresented, 8), requests, replies)
864 }()
865 scenes <- ports.Scene{OutputWidth: 200, OutputHeight: 100, Background: "#000000"}
866 select {
867 case <-rendered:
868 case <-time.After(2 * time.Second):
869 t.Fatal("no initial render")
870 }
871 waitFor(t, func() bool { commitMu.Lock(); defer commitMu.Unlock(); return len(*commits) > 0 })
872 commitMu.Lock()
873 last := (*commits)[len(*commits)-1]
874 commitMu.Unlock()
875 flips <- flipEvent{crtc: tCrtc, user: last.user, when: time.Second, seq: 1}
876 f, err := os.CreateTemp(t.TempDir(), "capture")
877 if err != nil {
878 t.Fatal(err)
879 }
880 if err := f.Truncate(16); err != nil {
881 t.Fatal(err)
882 }
883 requests <- ports.CaptureRequest{ID: 4, Region: image.Rect(0, 0, 2, 2), Width: 2, Height: 2, Stride: 8, Dst: ports.SHMBuffer{File: f}}
884 select {
885 case result := <-replies:
886 if result.Err != nil {
887 t.Fatal(result.Err)
888 }
889 case <-time.After(2 * time.Second):
890 t.Fatal("capture not replied")
891 }
892 select {
893 case <-rendered:
894 case <-time.After(2 * time.Second):
895 t.Fatal("capture did not render")
896 }
897 frame.Calls = nil // Testify must not inspect unmapped slice during expectation cleanup.
898 if _, err := f.Stat(); err == nil {
899 t.Fatal("fd not closed")
900 }
901 cancel()
902 if err := <-done; err != nil {
903 t.Fatal(err)
904 }
905}
906
907func TestRunPanicReportsReadinessError(t *testing.T) {
908 o, _, _ := testOutput(t)
909 o.ready = make(chan error, 1)
910 defer func() {
911 if recover() == nil {
912 t.Fatal("panic not propagated")
913 }
914 select {
915 case err := <-o.Ready():
916 if err == nil {
917 t.Fatal("panic reported as ready")
918 }
919 default:
920 t.Fatal("missing readiness")
921 }
922 }()
923 o.Run(context.Background(), func(int, int) (ports.Renderer, error) { panic("renderer panic") }, nil, nil, nil, nil, nil, nil, nil, nil)
924}