1package drm
2
3import (
4 "bytes"
5 "context"
6 "errors"
7 "image"
8 "maps"
9 "os"
10 "strings"
11 "sync"
12 "sync/atomic"
13 "testing"
14 "testing/synctest"
15 "time"
16
17 portsmocks "github.com/bnema/neferwl/internal/mocks/ports"
18 "github.com/bnema/neferwl/internal/ports"
19 "github.com/bnema/zerowrap"
20 "github.com/stretchr/testify/mock"
21 "golang.org/x/sys/unix"
22)
23
24// Object and property IDs of the test output.
25const (
26 tCrtc, tConn, tPrimary, tCursor, tOverlay = 30, 40, 50, 51, 52
27 pFB, pCrtcID, pFence, pMode, pActive = 1, 2, 3, 4, 5
28 pVRR, pConnCrtc = 6, 7
29)
30
31var 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}
32
33// commitRec is one recorded atomic commit.
34type commitRec struct {
35 req atomicReq
36 flags uint32
37 user uint64
38 at time.Time // when the commit was made
39}
40
41// testOutput returns an output on a mocked kms that records commits;
42// commit returns the next error of errs (nil when empty).
43func testOutput(t *testing.T, errs ...error) (*Output, *mockkms, *[]commitRec) {
44 o, k, commits, _ := testOutputMu(t, errs...)
45 return o, k, commits
46}
47
48// testOutputMu is testOutput with the mutex that guards the recorded
49// commits (Run commits on its goroutine).
50func testOutputMu(t *testing.T, errs ...error) (*Output, *mockkms, *[]commitRec, *sync.Mutex) {
51 return testOutputRules(t, nil, errs...)
52}
53
54// testOutputRules is testOutputMu with an optional driver rule: a non-nil
55// rule sees every commit (under the commit mutex) and may refuse it.
56func testOutputRules(t *testing.T, rule func(*atomicReq, uint32) error, errs ...error) (*Output, *mockkms, *[]commitRec, *sync.Mutex) {
57 k := newMockkms(t)
58 var commits []commitRec
59 commitMu := &sync.Mutex{}
60 k.EXPECT().commit(mock.Anything, mock.Anything, mock.Anything).RunAndReturn(func(r *atomicReq, flags uint32, user uint64) error {
61 commitMu.Lock()
62 defer commitMu.Unlock()
63 c := commitRec{flags: flags, user: user, at: time.Now()}
64 c.req.objs = append([]uint32(nil), r.objs...)
65 for _, ps := range r.props {
66 c.req.props = append(c.req.props, append([]propValue(nil), ps...))
67 }
68 commits = append(commits, c)
69 if rule != nil {
70 if err := rule(r, flags); err != nil {
71 return err
72 }
73 }
74 if len(errs) > 0 {
75 err := errs[0]
76 errs = errs[1:]
77 return err
78 }
79 return nil
80 }).Maybe()
81 primary := &plane{id: tPrimary, typ: planePrimary, props: planeProps}
82 cur := newCursor(&plane{id: tCursor, typ: planeCursor, props: planeProps}, 64)
83 cur.fbs = [2]uint32{90, 91}
84 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(), renderLog: zerowrap.Default(),
85 crtcProps: map[string]uint32{"MODE_ID": pMode, "ACTIVE": pActive, "VRR_ENABLED": pVRR}, connCrtc: pConnCrtc, vrrProp: pVRR,
86 primary: primary, cursor: cur, fbs: [2]uint32{70, 71}, clientFBs: map[uint64]*clientFB{}, scanout: true, tearing: true, asyncFence: true, planeRect: fullPlaneRect(200, 100), seatDisable: make(chan chan struct{})}
87 return o, k, &commits, commitMu
88}
89
90// eventOf is the flip event of commit c.
91func eventOf(c commitRec) flipEvent { return flipEvent{crtc: tCrtc, user: c.user} }
92
93func objCount(c commitRec) int { return len(c.req.objs) }
94
95func TestFrameCommitCarriesPrimaryFenceCursorAndVRR(t *testing.T) {
96 o, _, commits := testOutput(t)
97 o.cursor.image = true
98 o.cursor.Move(12, 34)
99 fence, w, err := os.Pipe()
100 if err != nil {
101 t.Fatal(err)
102 }
103 defer fence.Close()
104 defer w.Close()
105 if err := o.commitFrame(70, fence, false, true, pendingFrame{}); err != nil {
106 t.Fatal(err)
107 }
108 c := (*commits)[0]
109 if c.flags != atomicNonblock|flipEventFlag || c.user&3 != userFrame || c.user>>userKindBits != o.frame.pendingSerial {
110 t.Fatalf("flags %#x user %d", c.flags, c.user)
111 }
112 if v, _ := c.req.value(tPrimary, pFB); v != 70 {
113 t.Fatalf("primary fb %d", v)
114 }
115 if v, ok := c.req.value(tPrimary, pFence); !ok || v != uint64(fence.Fd()) {
116 t.Fatalf("fence %d %v", v, ok)
117 }
118 if v, _ := c.req.value(tCursor, 14); v != 12 {
119 t.Fatalf("cursor x %d", v)
120 }
121 if v, _ := c.req.value(tCrtc, pVRR); v != 1 {
122 t.Fatalf("vrr %d", v)
123 }
124 if !o.frame.pendingCommit() || !o.vrrOn || o.cursor.applied.x != 12 {
125 t.Fatalf("state pending=%v vrr=%v cursor=%+v", o.frame.pendingCommit(), o.vrrOn, o.cursor.applied)
126 }
127}
128
129func TestAsyncCommitHasOnlyPrimaryFBAndFence(t *testing.T) {
130 o, _, commits := testOutput(t)
131 fence, w, _ := os.Pipe()
132 defer fence.Close()
133 defer w.Close()
134 if err := o.commitFrame(70, fence, true, false, pendingFrame{}); err != nil {
135 t.Fatal(err)
136 }
137 c := (*commits)[0]
138 if c.flags&flipAsyncFlag == 0 {
139 t.Fatal("not async")
140 }
141 if objCount(c) != 1 || c.req.objs[0] != tPrimary || len(c.req.props[0]) != 2 {
142 t.Fatalf("async commit %+v", c.req)
143 }
144}
145
146func TestAsyncFallsBackToSyncWhenCursorMoves(t *testing.T) {
147 o, _, commits := testOutput(t)
148 o.cursor.image = true
149 o.cursor.Move(5, 5)
150 if err := o.commitFrame(70, nil, true, false, pendingFrame{}); err != nil {
151 t.Fatal(err)
152 }
153 if (*commits)[0].flags&flipAsyncFlag != 0 {
154 t.Fatal("async commit moved the cursor")
155 }
156}
157
158func TestAsyncWithoutFenceSupportStaysSync(t *testing.T) {
159 o, _, commits := testOutput(t)
160 o.asyncFence = false
161 fence, w, _ := os.Pipe()
162 defer fence.Close()
163 defer w.Close()
164 if err := o.commitFrame(70, fence, true, false, pendingFrame{}); err != nil {
165 t.Fatal(err)
166 }
167 if (*commits)[0].flags&flipAsyncFlag != 0 {
168 t.Fatal("async commit with a fence the driver refuses")
169 }
170}
171
172func TestScanoutAsyncRefusedRetriesSync(t *testing.T) {
173 o, _, commits := testOutput(t, unix.EINVAL)
174 o.shown, o.vrrOn = 1, true
175 buf := &ports.DMABuf{ID: 9}
176 o.clientFBs[9] = &clientFB{fbID: 80}
177 ok, err := o.commitScanout(80, ports.SurfaceContent{DMABuf: buf, Async: true}, pendingFrame{})
178 if !ok || err != nil {
179 t.Fatalf("ok=%v err=%v", ok, err)
180 }
181 if len(*commits) != 2 || (*commits)[0].flags&flipAsyncFlag == 0 || (*commits)[1].flags&flipAsyncFlag != 0 {
182 t.Fatalf("commits %+v", *commits)
183 }
184 if !o.clientFBs[9].noAsync || o.queued != 9 {
185 t.Fatalf("noAsync=%v queued=%d", o.clientFBs[9].noAsync, o.queued)
186 }
187}
188
189func TestCursorCommitWaitsForEvent(t *testing.T) {
190 o, _, commits := testOutput(t)
191 o.cursor.image = true
192 o.cursor.Move(1, 1)
193 if err := o.commitState(false); err != nil {
194 t.Fatal(err)
195 }
196 c := (*commits)[0]
197 if c.flags != atomicNonblock|flipEventFlag || c.user&3 != userState || !o.frame.pendingCommit() {
198 t.Fatalf("cursor commit flags %#x user %d pending %v", c.flags, c.user, o.frame.pendingCommit())
199 }
200 // Its event is not a frame: nothing is reported.
201 seen := map[ports.WindowID]uint64{}
202 o.completed(eventOf(c), seen)
203 if o.frame.pendingCommit() || o.reports.Len() != 0 || o.flips != 0 {
204 t.Fatalf("pending=%v unsent=%d flips=%d", o.frame.pendingCommit(), o.reports.Len(), o.flips)
205 }
206 // Nothing changed: no commit.
207 if err := o.commitState(false); err != nil || len(*commits) != 1 {
208 t.Fatalf("idle commit: %v, %d commits", err, len(*commits))
209 }
210}
211
212// Under VRR with a game shown, a cursor move rides on the next frame: a
213// cursor-only commit would hold the panel for its slowest refresh. A game
214// that stops drawing still gets the cursor after cursorMinInterval.
215func TestVRRGameCursorWaitsForFrame(t *testing.T) {
216 o, _, commits := testOutput(t)
217 o.cursor.image = true
218 o.vrrOn, o.vrrGame = true, true
219 o.lastFrame = time.Now()
220 o.cursor.Move(5, 5)
221 if err := o.commitState(o.stateVRR()); err != nil {
222 t.Fatal(err)
223 }
224 if len(*commits) != 0 || !o.cursorHeld || o.frame.pendingCommit() {
225 t.Fatalf("cursor committed alone: %d commits held=%v", len(*commits), o.cursorHeld)
226 }
227 // The next frame carries the cursor.
228 if err := o.commitFrame(70, nil, false, true, pendingFrame{}); err != nil {
229 t.Fatal(err)
230 }
231 if v, _ := (*commits)[0].req.value(tCursor, 14); v != 5 || o.cursorHeld {
232 t.Fatalf("frame cursor x %d held=%v", v, o.cursorHeld)
233 }
234 o.completed(eventOf((*commits)[0]), map[ports.WindowID]uint64{})
235 // The game stalls: the cursor commits alone after the interval.
236 o.lastFrame = time.Now().Add(-cursorMinInterval)
237 o.cursor.Move(8, 8)
238 if err := o.commitState(o.stateVRR()); err != nil {
239 t.Fatal(err)
240 }
241 if len(*commits) != 2 || o.cursorHeld {
242 t.Fatalf("stalled game: %d commits held=%v", len(*commits), o.cursorHeld)
243 }
244 if v, _ := (*commits)[1].req.value(tCursor, 14); v != 8 {
245 t.Fatalf("cursor x %d", v)
246 }
247}
248
249// A held move reversed before the next frame holds nothing: the cursor
250// timer is not armed again.
251func TestHeldCursorMovedBackClearsHold(t *testing.T) {
252 o, _, commits := testOutput(t)
253 o.cursor.image = true
254 o.vrrOn, o.vrrGame, o.lastFrame = true, true, time.Now()
255 o.cursor.Move(0, 0)
256 o.cursor.applied = o.cursor.desired() // on screen at (0, 0)
257 o.cursor.Move(5, 5)
258 if err := o.commitState(o.stateVRR()); err != nil || !o.cursorHeld {
259 t.Fatalf("err %v held=%v", err, o.cursorHeld)
260 }
261 o.cursor.Move(0, 0)
262 if err := o.commitState(o.stateVRR()); err != nil || o.cursorHeld || len(*commits) != 0 {
263 t.Fatalf("err %v held=%v commits %d", err, o.cursorHeld, len(*commits))
264 }
265}
266
267// Without a game, the cursor keeps its own commits.
268func TestCursorCommitsAloneWithoutGame(t *testing.T) {
269 o, _, commits := testOutput(t)
270 o.cursor.image = true
271 o.vrrOn, o.lastFrame = true, time.Now()
272 o.composedSince = time.Now()
273 o.cursor.Move(5, 5)
274 if err := o.commitState(o.stateVRR()); err != nil || len(*commits) != 1 || o.cursorHeld {
275 t.Fatalf("err %v, %d commits held=%v", err, len(*commits), o.cursorHeld)
276 }
277}
278
279func TestBusyCommitStaysPending(t *testing.T) {
280 o, _, _ := testOutput(t, unix.EBUSY)
281 err := o.commitFrame(70, nil, false, false, pendingFrame{})
282 enabled := true
283 if !o.commitFailed(err, &enabled) || !o.frame.pendingCommit() || !enabled {
284 t.Fatalf("EBUSY: pending=%v enabled=%v", o.frame.pendingCommit(), enabled)
285 }
286}
287
288func TestFlipReportsTimestampAndShownFrame(t *testing.T) {
289 o, _, commits := testOutput(t)
290 o.vrrOn = false
291 o.mode.Clock, o.mode.HTotal, o.mode.VTotal = 0, 0, 0
292 if err := o.commitFrame(70, nil, false, false, pendingFrame{shows: map[ports.WindowID]uint64{1: 4}}); err != nil {
293 t.Fatal(err)
294 }
295 o.completed(flipEvent{user: (*commits)[0].user, when: 5 * time.Second, seq: 11}, map[ports.WindowID]uint64{1: 4})
296 if o.reports.Len() != 1 {
297 t.Fatalf("reports %d", o.reports.Len())
298 }
299 f := o.reports.Unsent()[0].Flip
300 if f == nil || f.When != 5*time.Second || f.Seq != 11 || f.Refresh != time.Second/60 || f.Shows[1] != 4 || !f.HardwareClock {
301 t.Fatalf("flip %+v", f)
302 }
303}
304
305// Unchanged reports share an immutable snapshot; a later content update
306// must not change an earlier report, even after it has been sent.
307func TestReportSeenAllocations(t *testing.T) {
308 o, _, _ := testOutput(t)
309 seen := map[ports.WindowID]uint64{1: 1, 2: 2}
310 o.report(nil, seen)
311 // Keep the queue bounded without measuring the queue's growth.
312 if allocs := testing.AllocsPerRun(100, func() { o.report(nil, seen) }); allocs != 0 {
313 t.Errorf("unchanged report: %.1f allocs, want 0", allocs)
314 } else {
315 t.Logf("unchanged report: %.1f allocs", allocs)
316 }
317 previous := o.reports.Unsent()[0].Seen
318 seen[1] = 3
319 o.report(nil, seen)
320 if previous[1] != 1 || o.reports.Unsent()[0].Seen[1] != 3 {
321 t.Fatalf("snapshot mutated: old %v new %v", previous, o.reports.Unsent()[0].Seen)
322 }
323}
324
325func TestShownBySnapshotAllocations(t *testing.T) {
326 o := &Output{}
327 s := ports.Scene{OutputWidth: 10, OutputHeight: 10, Windows: []ports.SceneWindow{{ID: 1, Rect: ports.Rect{W: 10, H: 10}}}}
328 seen := map[ports.WindowID]uint64{1: 1, 2: 2}
329 previous := o.shownBy(s, seen)
330 if allocs := testing.AllocsPerRun(100, func() { o.shownBy(s, seen) }); allocs != 0 {
331 t.Errorf("unchanged shows: %.1f allocs, want 0", allocs)
332 }
333 seen[1] = 3
334 current := o.shownBy(s, seen)
335 if previous[1] != 1 || current[1] != 3 {
336 t.Fatalf("snapshot mutated: old %v new %v", previous, current)
337 }
338 direct := o.directShownBy(1, 3)
339 if allocs := testing.AllocsPerRun(100, func() { o.directShownBy(1, 3) }); allocs != 0 {
340 t.Errorf("unchanged direct shows: %.1f allocs, want 0", allocs)
341 }
342 o.directShownBy(1, 4)
343 if direct[1] != 3 {
344 t.Fatalf("direct snapshot mutated: %v", direct)
345 }
346}
347
348func TestReportsKeepEveryFlip(t *testing.T) {
349 o, _, _ := testOutput(t)
350 seen := map[ports.WindowID]uint64{1: 1}
351 o.report(&ports.FlipInfo{Seq: 1}, seen)
352 o.report(nil, seen)
353 o.report(&ports.FlipInfo{Seq: 2}, seen)
354 o.report(nil, map[ports.WindowID]uint64{1: 3})
355 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 {
356 t.Fatalf("unsent %+v", o.reports.Unsent())
357 }
358 for i := range 20 {
359 o.report(&ports.FlipInfo{Seq: uint64(10 + i), Shows: map[ports.WindowID]uint64{}}, seen)
360 }
361 // The wayland sink alone merges flips: the output queue keeps them all.
362 if o.reports.Len() != 22 || o.reports.Unsent()[2].Flip.Merged != 0 {
363 t.Fatalf("%d reports, merged %d", o.reports.Len(), o.reports.Unsent()[2].Flip.Merged)
364 }
365}
366
367func TestModesetDisablesCursorAndStrayPlanes(t *testing.T) {
368 o, k, commits := testOutput(t)
369 stray := &plane{id: tOverlay, typ: planeOverlay, props: planeProps}
370 o.stray = []*plane{stray}
371 k.EXPECT().createBlob(mock.Anything).Return(99, nil).Once()
372 k.EXPECT().objProps(uint32(tOverlay), uint32(objPlane)).Return(map[string][2]uint64{"CRTC_ID": {pCrtcID, tCrtc}}, nil).Once()
373 if err := o.modeset(); err != nil {
374 t.Fatal(err)
375 }
376 c := (*commits)[0]
377 if c.flags != atomicAllowModes {
378 t.Fatalf("flags %#x", c.flags)
379 }
380 if v, _ := c.req.value(tCrtc, pMode); v != 99 {
381 t.Fatalf("mode blob %d", v)
382 }
383 for _, p := range []uint32{tCursor, tOverlay} {
384 if v, ok := c.req.value(p, pFB); !ok || v != 0 {
385 t.Fatalf("plane %d fb %d %v", p, v, ok)
386 }
387 }
388 if v, _ := c.req.value(tConn, pConnCrtc); v != tCrtc {
389 t.Fatalf("connector crtc %d", v)
390 }
391 // The next modeset replaces the blob.
392 k.EXPECT().createBlob(mock.Anything).Return(100, nil).Once()
393 k.EXPECT().objProps(uint32(tOverlay), uint32(objPlane)).Return(map[string][2]uint64{}, nil).Once()
394 k.EXPECT().destroyBlob(uint32(99)).Return(nil).Once()
395 if err := o.modeset(); err != nil {
396 t.Fatal(err)
397 }
398 if _, ok := (*commits)[1].req.value(tOverlay, pFB); ok {
399 t.Fatal("plane on another CRTC turned off")
400 }
401}
402
403func TestAsyncProbeAfterModeset(t *testing.T) {
404 o, _, commits := testOutput(t, unix.EINVAL)
405 o.asyncFence = true
406 r := portsmocks.NewMockRenderer(t)
407 fence, w, _ := os.Pipe()
408 defer w.Close()
409 r.EXPECT().UseTarget(mock.Anything).Return().Once()
410 r.EXPECT().Render(mock.Anything, mock.Anything).Return(fence, nil).Once()
411 o.probeAsync(r)
412 c := (*commits)[0]
413 if c.flags != atomicTestOnly|flipAsyncFlag {
414 t.Fatalf("flags %#x", c.flags)
415 }
416 if _, ok := c.req.value(tPrimary, pFence); !ok {
417 t.Fatal("probe without a fence")
418 }
419 if o.asyncFence {
420 t.Fatal("refused probe kept asyncFence")
421 }
422 if _, err := fence.Stat(); err == nil {
423 t.Fatal("probe fence not closed")
424 }
425 o.probeAsync(r) // once only
426}
427
428func TestVRRToggleOnlyInsideFrames(t *testing.T) {
429 o, _, commits := testOutput(t)
430 o.vrrOn = true
431 o.composedSince = time.Now().Add(-time.Second)
432 if o.wantVRR(false) {
433 t.Fatal("VRR kept after vrrHold of composition")
434 }
435 o.composedSince = time.Now()
436 if !o.wantVRR(false) {
437 t.Fatal("VRR dropped before vrrHold")
438 }
439 if err := o.commitState(false); err != nil {
440 t.Fatal(err)
441 }
442 if v, ok := (*commits)[0].req.value(tCrtc, pVRR); !ok || v != 0 || (*commits)[0].flags&flipEventFlag == 0 {
443 t.Fatalf("vrr off commit %+v", (*commits)[0])
444 }
445}
446
447// A composed fullscreen frame keeps VRR and restarts the hold; state
448// commits (cursor, vrrOff timer) keep it too until the fullscreen ends.
449func TestVRRKeptWhileFullscreenComposed(t *testing.T) {
450 o, _, _ := testOutput(t)
451 o.vrrOn = true
452 o.composedSince = time.Now().Add(-time.Second)
453 if !o.wantVRR(true) || !o.composedSince.IsZero() {
454 t.Fatal("composed fullscreen dropped VRR")
455 }
456 // Cursor-only state commit between frames.
457 if !o.stateVRR() {
458 t.Fatal("state commit dropped VRR during fullscreen")
459 }
460 // Fullscreen ends, a tiled window stays on the overlay plane: the
461 // hold starts, then VRR goes off.
462 o.shown = 9
463 if !o.wantVRR(false) || o.composedSince.IsZero() {
464 t.Fatal("hold not started on exit")
465 }
466 o.composedSince = time.Now().Add(-time.Second)
467 if o.stateVRR() {
468 t.Fatal("VRR kept after the hold (overlay buffer taken for a game)")
469 }
470}
471
472// A fullscreen game on a workspace that is left keeps VRR only for the
473// hold: its hidden window no longer counts as a game frame, and its
474// commits do not draw frames.
475func TestVRRHiddenGameOnlyHolds(t *testing.T) {
476 o, _, _ := testOutput(t)
477 game := ports.SceneWindow{ID: 1, Rect: ports.Rect{W: 100, H: 50}, Fullscreen: true}
478 shown := ports.Scene{OutputWidth: 100, OutputHeight: 50, Windows: []ports.SceneWindow{game}}
479 o.vrrOn = true
480 if !o.wantVRR(fullscreenShown(&shown)) {
481 t.Fatal("VRR off while the game is shown")
482 }
483 game.Hidden, game.Rect = true, ports.Rect{}
484 left := ports.Scene{OutputWidth: 100, OutputHeight: 50, Windows: []ports.SceneWindow{game, {ID: 2, Rect: ports.Rect{W: 100, H: 50}}}}
485 if fullscreenShown(&left) || left.Shows(1) {
486 t.Fatal("hidden game still drives frames")
487 }
488 if !o.wantVRR(fullscreenShown(&left)) {
489 t.Fatal("VRR dropped before the hold")
490 }
491 o.composedSince = time.Now().Add(-vrrHold - time.Millisecond)
492 if o.stateVRR() {
493 t.Fatal("VRR kept after the hold for a hidden game")
494 }
495}
496
497// A VRR refusal on a composed frame with an overlay retries with the
498// overlay: the composed image left its window out.
499func TestVRRRefusedKeepsOverlay(t *testing.T) {
500 o, k, commits := overlayOutput(t, unix.EINVAL)
501 k.EXPECT().addFB(mock.Anything, uint32(fourccXRGB)).Return(88, nil).Once()
502 o.overlay.formats = []ports.DMABufFormat{{Format: fourccXRGB}}
503 s, c := overlayScene()
504 ov, _ := o.overlayFrame(s, c)
505 if ov.fb == 0 {
506 t.Fatal("no overlay")
507 }
508 if err := o.commitWith(70, nil, false, true, pendingFrame{queued: ov.buf}, ov); err != nil {
509 t.Fatal(err)
510 }
511 if len(*commits) != 2 || o.vrrProp != 0 {
512 t.Fatalf("%d commits, vrrProp %d", len(*commits), o.vrrProp)
513 }
514 if v, _ := (*commits)[1].req.value(tOverlay, pFB); v != 88 || o.overlayOn != ov.buf {
515 t.Fatalf("retry overlay fb %d, overlayOn %d", v, o.overlayOn)
516 }
517}
518
519func TestReadPlanes(t *testing.T) {
520 k := newMockkms(t)
521 k.EXPECT().planes().Return([]planeRes{{id: 1, possible: 1}, {id: 2, possible: 2}, {id: 3, possible: 3}}, nil)
522 k.EXPECT().objProps(uint32(1), uint32(objPlane)).Return(map[string][2]uint64{"type": {20, planePrimary}, "FB_ID": {21, 0}, "IN_FORMATS": {22, 5}}, nil)
523 k.EXPECT().objProps(uint32(3), uint32(objPlane)).Return(map[string][2]uint64{"type": {20, planeCursor}}, nil)
524 k.EXPECT().getBlob(uint32(5)).Return(nil, nil)
525 ps, err := readPlanes(k, 0)
526 if err != nil || len(ps) != 2 || ps[0].typ != planePrimary || ps[0].prop("FB_ID") != 21 || ps[1].typ != planeCursor {
527 t.Fatalf("planes %+v %v", ps, err)
528 }
529}
530
531func TestVRRRefusedIsDisabled(t *testing.T) {
532 o, _, commits := testOutput(t, unix.EINVAL)
533 if err := o.commitFrame(70, nil, false, true, pendingFrame{}); err != nil {
534 t.Fatal(err)
535 }
536 if len(*commits) != 2 || o.vrrProp != 0 || !o.frame.pendingCommit() {
537 t.Fatalf("%d commits, vrrProp %d", len(*commits), o.vrrProp)
538 }
539 if v, _ := (*commits)[1].req.value(tCrtc, pVRR); v != 0 {
540 t.Fatal("retry kept VRR on")
541 }
542}
543
544func TestBufferRefusedIsNotBlamedOnVRR(t *testing.T) {
545 o, _, _ := testOutput(t, unix.EINVAL, unix.EINVAL)
546 o.clientFBs[9] = &clientFB{fbID: 80}
547 ok, err := o.commitScanout(80, ports.SurfaceContent{DMABuf: &ports.DMABuf{ID: 9}}, pendingFrame{})
548 if ok || err != nil || o.vrrProp == 0 || o.clientFBs[9].failed != "flip_refused" {
549 t.Fatalf("ok=%v err=%v vrrProp=%d failed=%q", ok, err, o.vrrProp, o.clientFBs[9].failed)
550 }
551}
552
553// Run: while a frame is in flight its buffers may still be read by the
554// GPU, so a newer content is reported only after the flip event; every
555// frame fence is closed.
556func TestRunReportsSeenAfterFlip(t *testing.T) { synctest.Test(t, runReportsSeenAfterFlip) }
557
558func runReportsSeenAfterFlip(t *testing.T) {
559 o, k, commits, commitMu := testOutputMu(t)
560 o.cursor, o.tearing = nil, false
561 flips := make(chan flipEvent, 1)
562 o.flipped = flips
563 r := portsmocks.NewMockRenderer(t)
564 var mu sync.Mutex
565 var fences []*os.File
566 pipeBuf := func() ports.DMABuf {
567 f, w, _ := os.Pipe()
568 w.Close()
569 return ports.DMABuf{Planes: []ports.DMABufPlane{{File: f}}}
570 }
571 r.EXPECT().SetHDR(float64(0)).Return().Maybe()
572 r.EXPECT().ExportTargets(2, mock.Anything, false).Return([]ports.DMABuf{pipeBuf(), pipeBuf()}, nil).Once()
573 r.EXPECT().UseTarget(mock.Anything).Return()
574 r.EXPECT().Render(mock.Anything, mock.Anything).RunAndReturn(func(ports.Scene, map[ports.WindowID]ports.SurfaceContent) (*os.File, error) {
575 f, w, _ := os.Pipe()
576 _, _ = w.Write([]byte{1}) // readable: signalled
577 w.Close()
578 mu.Lock()
579 fences = append(fences, f)
580 mu.Unlock()
581 return f, nil
582 })
583 r.EXPECT().Close().Return().Once()
584 k.EXPECT().addFB(mock.Anything, uint32(fourccXRGB)).Return(70, nil).Twice()
585 k.EXPECT().createBlob(mock.Anything).Return(99, nil)
586 k.EXPECT().destroyBlob(mock.Anything).Return(nil).Maybe()
587 k.EXPECT().rmFB(mock.Anything).Return(nil).Maybe()
588 frameCommits := func() int {
589 commitMu.Lock()
590 defer commitMu.Unlock()
591 n := 0
592 for _, c := range *commits {
593 if c.user&3 == userFrame {
594 n++
595 }
596 }
597 return n
598 }
599 scenes := make(chan ports.Scene, 1)
600 contents := newContentFeed(t, 1)
601 sink, presented := presentedSink(t, 8)
602 ctx, cancel := context.WithCancel(context.Background())
603 done := make(chan error, 1)
604 go func() {
605 done <- o.Run(ctx, func(int, int) (ports.Renderer, error) { return r, nil }, nil, make(chan bool), scenes, contents.in(), contents.ret, nil, sink, nil, nil)
606 }()
607 scenes <- ports.Scene{OutputWidth: 200, OutputHeight: 100, Windows: []ports.SceneWindow{{ID: 1, Rect: ports.Rect{W: 10, H: 10}}}}
608 waitFor(t, func() bool { return frameCommits() == 1 })
609 // Frame 1 is in flight: content 3 replaces what it may be reading.
610 // One batch carries two windows: both are applied and the batch goes
611 // back to wayland (reset) at once, whatever the frame in flight reads.
612 contents.send(ports.SurfaceContent{ID: 1, Seq: 3, SHM: &ports.SHMBuffer{}}, ports.SurfaceContent{ID: 2, Seq: 4, SHM: &ports.SHMBuffer{}})
613 synctest.Wait()
614 select {
615 case <-contents.returned:
616 default:
617 t.Fatal("batch not returned")
618 }
619 select {
620 case p := <-presented:
621 t.Fatalf("report while a frame is in flight: %+v", p)
622 default:
623 }
624 commitMu.Lock()
625 last := (*commits)[len(*commits)-1]
626 commitMu.Unlock()
627 flips <- flipEvent{crtc: tCrtc, user: last.user, when: time.Second, seq: 1}
628 select {
629 case p := <-presented:
630 if p.Flip == nil || p.Flip.When != time.Second || p.Seen[1] != 3 || p.Seen[2] != 4 {
631 t.Fatalf("report %+v", p)
632 }
633 case <-time.After(2 * time.Second):
634 t.Fatal("no report after the flip")
635 }
636 cancel()
637 if err := <-done; err != nil {
638 t.Fatal(err)
639 }
640 mu.Lock()
641 defer mu.Unlock()
642 if len(fences) < 3 {
643 t.Fatalf("%d renders", len(fences))
644 }
645 for _, f := range fences {
646 if _, err := f.Stat(); err == nil {
647 t.Fatal("frame fence left open")
648 }
649 }
650}
651
652// Run: a fullscreen window composed under an overlay layer keeps VRR on in
653// the frame commit; a tiled scene turns it off only after vrrHold.
654func TestRunComposedFullscreenKeepsVRR(t *testing.T) {
655 o, k, commits, commitMu := testOutputMu(t)
656 o.cursor, o.tearing = nil, false
657 flips := make(chan flipEvent, 1)
658 o.flipped = flips
659 r := portsmocks.NewMockRenderer(t)
660 pipeBuf := func() ports.DMABuf {
661 f, w, _ := os.Pipe()
662 w.Close()
663 return ports.DMABuf{Planes: []ports.DMABufPlane{{File: f}}}
664 }
665 r.EXPECT().SetHDR(float64(0)).Return().Maybe()
666 r.EXPECT().ExportTargets(2, mock.Anything, false).Return([]ports.DMABuf{pipeBuf(), pipeBuf()}, nil).Once()
667 r.EXPECT().UseTarget(mock.Anything).Return()
668 r.EXPECT().Render(mock.Anything, mock.Anything).Return(nil, nil)
669 r.EXPECT().Close().Return().Once()
670 k.EXPECT().addFB(mock.Anything, uint32(fourccXRGB)).Return(70, nil).Twice()
671 k.EXPECT().createBlob(mock.Anything).Return(99, nil)
672 k.EXPECT().destroyBlob(mock.Anything).Return(nil).Maybe()
673 k.EXPECT().rmFB(mock.Anything).Return(nil).Maybe()
674 lastFrame := func() (commitRec, int) {
675 commitMu.Lock()
676 defer commitMu.Unlock()
677 n, last := 0, commitRec{}
678 for _, c := range *commits {
679 if c.user&3 == userFrame {
680 n, last = n+1, c
681 }
682 }
683 return last, n
684 }
685 scenes := make(chan ports.Scene, 1)
686 ctx, cancel := context.WithCancel(context.Background())
687 done := make(chan error, 1)
688 go func() {
689 done <- o.Run(ctx, func(int, int) (ports.Renderer, error) { return r, nil }, nil, make(chan bool), scenes, nil, nil, nil, discardSink(t), nil, nil)
690 }()
691 scenes <- ports.Scene{OutputWidth: 200, OutputHeight: 100,
692 Windows: []ports.SceneWindow{{ID: 1, Rect: ports.Rect{W: 200, H: 100}, Fullscreen: true}},
693 Layers: []ports.SceneLayer{{ID: 5, Layer: ports.LayerOverlay, Rect: ports.Rect{W: 10, H: 10}}}}
694 waitFor(t, func() bool { _, n := lastFrame(); return n == 1 })
695 last, _ := lastFrame()
696 if v, ok := last.req.value(tCrtc, pVRR); !ok || v != 1 {
697 t.Fatalf("composed fullscreen frame VRR %d %v", v, ok)
698 }
699 flips <- flipEvent{crtc: tCrtc, user: last.user, when: time.Second, seq: 1}
700 scenes <- ports.Scene{OutputWidth: 200, OutputHeight: 100, Windows: []ports.SceneWindow{{ID: 1, Rect: ports.Rect{W: 100, H: 100}}}}
701 waitFor(t, func() bool { _, n := lastFrame(); return n == 2 })
702 last, _ = lastFrame()
703 if v, _ := last.req.value(tCrtc, pVRR); v != 1 {
704 t.Fatal("VRR dropped before vrrHold")
705 }
706 flips <- flipEvent{crtc: tCrtc, user: last.user, when: 2 * time.Second, seq: 2}
707 // The vrrOff timer turns VRR off with a state commit after the hold.
708 waitFor(t, func() bool {
709 commitMu.Lock()
710 defer commitMu.Unlock()
711 c := (*commits)[len(*commits)-1]
712 v, ok := c.req.value(tCrtc, pVRR)
713 return c.user&3 == userState && ok && v == 0
714 })
715 cancel()
716 if err := <-done; err != nil {
717 t.Fatal(err)
718 }
719}
720
721func waitFor(t *testing.T, cond func() bool) {
722 t.Helper()
723 deadline := time.Now().Add(2 * time.Second)
724 for !cond() {
725 if time.Now().After(deadline) {
726 t.Fatal("timed out")
727 }
728 time.Sleep(2 * time.Millisecond)
729 }
730}
731
732// The stats tick lets the renderer free what the output no longer draws,
733// even while nothing renders; a failed trim stops the output.
734func TestRunTrimsRendererOnStatsTick(t *testing.T) {
735 o, k, _ := testOutput(t)
736 o.cursor, o.tearing, o.fbs = nil, false, [2]uint32{}
737 ticks := make(chan time.Time)
738 clk := portsmocks.NewMockClock(t)
739 tk := portsmocks.NewMockTicker(t)
740 clk.EXPECT().NewTicker(10 * time.Second).Return(tk).Once()
741 tk.EXPECT().C().Return(ticks)
742 tk.EXPECT().Stop().Return().Once()
743 now := time.Unix(1000, 0)
744 clk.EXPECT().Now().Return(now)
745 o.clock = clk
746 r := portsmocks.NewMockRenderer(t)
747 r.EXPECT().SetHDR(float64(0)).Return().Maybe()
748 r.EXPECT().Close().Return().Once()
749 r.EXPECT().TakeRedrawn().Return(1234).Once()
750 var statsLog bytes.Buffer // written by Run's goroutine, read after it ends
751 o.log = zerowrap.New(zerowrap.Config{Level: "info", Format: "json", Output: &statsLog})
752 o.flipStats = flipStats{missedVblanks: 3, maxInterval: 50 * time.Millisecond, maxFlipToRead: 25 * time.Millisecond, lateFences: 2}
753 trimmed := make(chan struct{}, 2)
754 failure := os.ErrInvalid
755 calls := 0 // Run's goroutine only
756 r.EXPECT().Trim(now).RunAndReturn(func(time.Time) error {
757 trimmed <- struct{}{}
758 if calls++; calls == 2 {
759 return failure
760 }
761 return nil
762 }).Twice()
763 k.EXPECT().rmFB(mock.Anything).Return(nil).Maybe()
764 k.EXPECT().destroyBlob(mock.Anything).Return(nil).Maybe()
765 // Switched away from the start: no target, no frame.
766 active := make(chan bool, 1)
767 active <- false
768 done := make(chan error, 1)
769 go func() {
770 done <- o.Run(context.Background(), func(int, int) (ports.Renderer, error) { return r, nil }, nil, active, nil, nil, nil, nil, discardSink(t), nil, nil)
771 }()
772 ticks <- now
773 <-trimmed
774 ticks <- now
775 if err := <-done; !errors.Is(err, failure) {
776 t.Fatalf("run: %v", err)
777 }
778 // Reported once, then reset. late_fences needs flip tracing.
779 for _, want := range []string{`"message":"stats"`, `"missed_vblanks":3`, `"max_flip_interval_ms":50`, `"max_flip_to_read_ms":25`, `"redrawn_pixels":1234`} {
780 if !strings.Contains(statsLog.String(), want) {
781 t.Fatalf("stats entry lacks %s: %s", want, statsLog.String())
782 }
783 }
784 if strings.Contains(statsLog.String(), "late_fences") || o.flipStats != (flipStats{}) {
785 t.Fatalf("late fences untraced or stats not reset: %s %+v", statsLog.String(), o.flipStats)
786 }
787}
788
789// A render failure must complete captures queued before the first frame.
790func TestCaptureRenderFailureClosesPendingRequest(t *testing.T) {
791 o, k, _ := testOutput(t)
792 o.cursor, o.tearing = nil, false
793 r := portsmocks.NewMockRenderer(t)
794 boom := errors.New("render failed")
795 r.EXPECT().SetHDR(float64(0)).Return().Once()
796 r.EXPECT().UseTarget(mock.Anything).Return()
797 makeBuf := func() ports.DMABuf {
798 f, err := os.CreateTemp(t.TempDir(), "target")
799 if err != nil {
800 t.Fatal(err)
801 }
802 t.Cleanup(func() { _ = f.Close() })
803 return ports.DMABuf{Planes: []ports.DMABufPlane{{File: f}}}
804 }
805 r.EXPECT().ExportTargets(2, mock.Anything, false).Return([]ports.DMABuf{makeBuf(), makeBuf()}, nil).Once()
806 r.EXPECT().Render(ports.Scene{Background: "#000000"}, mock.Anything).Return(nil, nil).Twice()
807 r.EXPECT().Render(mock.MatchedBy(func(s ports.Scene) bool { return s.Background != "#000000" }), mock.Anything).Return(nil, boom).Once()
808 r.EXPECT().Close().Return().Once()
809 k.EXPECT().addFB(mock.Anything, uint32(fourccXRGB)).Return(70, nil).Twice()
810 k.EXPECT().createBlob(mock.Anything).Return(99, nil)
811 k.EXPECT().destroyBlob(mock.Anything).Return(nil).Maybe()
812 k.EXPECT().rmFB(mock.Anything).Return(nil).Maybe()
813 ctx, cancel := context.WithCancel(context.Background())
814 defer cancel()
815 scenes := make(chan ports.Scene, 1)
816 requests := make(chan ports.CaptureRequest)
817 replies := make(chan ports.CaptureDone, 1)
818 f, err := os.CreateTemp(t.TempDir(), "capture")
819 if err != nil {
820 t.Fatal(err)
821 }
822 done := make(chan error, 1)
823 go func() {
824 done <- o.Run(ctx, func(int, int) (ports.Renderer, error) { return r, nil }, nil, nil, scenes, nil, nil, nil, nil, requests, replies)
825 }()
826 // The unbuffered handoff proves Run owns the request before any scene.
827 select {
828 case requests <- ports.CaptureRequest{ID: 9, Dst: ports.SHMBuffer{File: f}}:
829 case <-time.After(2 * time.Second):
830 t.Fatal("output did not accept capture")
831 }
832 scenes <- ports.Scene{Seq: 1, Scale: 1}
833 select {
834 case err := <-done:
835 if !errors.Is(err, boom) {
836 t.Fatalf("run error %v", err)
837 }
838 case <-time.After(2 * time.Second):
839 t.Fatal("output did not stop after render failure")
840 }
841 select {
842 case result := <-replies:
843 if result.ID != 9 || result.Err == nil {
844 t.Fatalf("capture result %+v", result)
845 }
846 default:
847 t.Fatal("render failure lost capture completion")
848 }
849 if _, err := f.Stat(); err == nil {
850 t.Fatal("render failure leaked capture descriptor")
851 }
852}
853
854// A request must render even when the output already has an unchanged scene.
855func TestCaptureForcesDRMComposition(t *testing.T) {
856 o, k, commits, commitMu := testOutputMu(t)
857 o.cursor, o.tearing = nil, false
858 flips := make(chan flipEvent, 2)
859 o.flipped = flips
860 r := &portsmocks.MockRenderer{}
861 makeBuf := func() ports.DMABuf {
862 f, w, _ := os.Pipe()
863 w.Close()
864 return ports.DMABuf{Planes: []ports.DMABufPlane{{File: f}}}
865 }
866 r.EXPECT().SetHDR(float64(0)).Return().Maybe() // SDR output
867 r.EXPECT().ExportTargets(2, mock.Anything, false).Return([]ports.DMABuf{makeBuf(), makeBuf()}, nil).Once()
868 r.EXPECT().UseTarget(mock.Anything).Return()
869 rendered := make(chan ports.Scene, 3)
870 r.EXPECT().Render(mock.Anything, mock.Anything).RunAndReturn(func(s ports.Scene, _ map[ports.WindowID]ports.SurfaceContent) (*os.File, error) {
871 rendered <- s
872 return nil, nil
873 })
874 frame := portsmocks.NewMockCaptureFrame(t)
875 frame.EXPECT().Done().Return(nil)
876 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()
877 r.EXPECT().BeginCapture().Return(frame, nil).Once()
878 r.EXPECT().EndCapture(frame).Return().Once()
879 r.EXPECT().Close().Return().Once()
880 k.EXPECT().addFB(mock.Anything, uint32(fourccXRGB)).Return(70, nil).Twice()
881 k.EXPECT().createBlob(mock.Anything).Return(99, nil)
882 k.EXPECT().destroyBlob(mock.Anything).Return(nil).Maybe()
883 k.EXPECT().rmFB(mock.Anything).Return(nil).Maybe()
884 scenes := make(chan ports.Scene, 1)
885 requests := make(chan ports.CaptureRequest, 1)
886 replies := make(chan ports.CaptureDone, 1)
887 ctx, cancel := context.WithCancel(context.Background())
888 done := make(chan error, 1)
889 go func() {
890 done <- o.Run(ctx, func(int, int) (ports.Renderer, error) { return r, nil }, nil, nil, scenes, nil, nil, nil, discardSink(t), requests, replies)
891 }()
892 scenes <- ports.Scene{OutputWidth: 200, OutputHeight: 100, Background: "#000000"}
893 select {
894 case <-rendered:
895 case <-time.After(2 * time.Second):
896 t.Fatal("no initial render")
897 }
898 waitFor(t, func() bool { commitMu.Lock(); defer commitMu.Unlock(); return len(*commits) > 0 })
899 commitMu.Lock()
900 last := (*commits)[len(*commits)-1]
901 commitMu.Unlock()
902 flips <- flipEvent{crtc: tCrtc, user: last.user, when: time.Second, seq: 1}
903 f, err := os.CreateTemp(t.TempDir(), "capture")
904 if err != nil {
905 t.Fatal(err)
906 }
907 if err := f.Truncate(16); err != nil {
908 t.Fatal(err)
909 }
910 requests <- ports.CaptureRequest{ID: 4, Region: image.Rect(0, 0, 2, 2), Width: 2, Height: 2, Stride: 8, Dst: ports.SHMBuffer{File: f}}
911 select {
912 case result := <-replies:
913 if result.Err != nil {
914 t.Fatal(result.Err)
915 }
916 case <-time.After(2 * time.Second):
917 t.Fatal("capture not replied")
918 }
919 select {
920 case <-rendered:
921 case <-time.After(2 * time.Second):
922 t.Fatal("capture did not render")
923 }
924 frame.Calls = nil // Testify must not inspect unmapped slice during expectation cleanup.
925 if _, err := f.Stat(); err == nil {
926 t.Fatal("fd not closed")
927 }
928 cancel()
929 if err := <-done; err != nil {
930 t.Fatal(err)
931 }
932}
933
934func TestRunPanicReportsReadinessError(t *testing.T) {
935 o, _, _ := testOutput(t)
936 o.ready = make(chan error, 1)
937 defer func() {
938 if recover() == nil {
939 t.Fatal("panic not propagated")
940 }
941 select {
942 case err := <-o.Ready():
943 if err == nil {
944 t.Fatal("panic reported as ready")
945 }
946 default:
947 t.Fatal("missing readiness")
948 }
949 }()
950 o.Run(context.Background(), func(int, int) (ports.Renderer, error) { panic("renderer panic") }, nil, nil, nil, nil, nil, nil, nil, nil, nil)
951}
952
953// stampRun starts Run on an output with unbuffered scene and content
954// channels: a send completes once Run took the message, and the next send
955// once it went round its loop again, which is the sync point for what the
956// loop did with the first.
957type stampRun struct {
958 o *Output
959 scenes chan ports.Scene
960 contents *contentFeed
961 flips chan flipEvent
962 wake chan ports.SecurityState
963 state *atomic.Uint64
964 frameCommit func() int
965 lastCommit func() commitRec
966 stop func()
967}
968
969func startStampRun(t *testing.T) *stampRun {
970 o, k, commits, commitMu := testOutputMu(t)
971 o.cursor, o.tearing = nil, false
972 sr := &stampRun{o: o, scenes: make(chan ports.Scene), contents: newContentFeed(t, 0), flips: make(chan flipEvent, 1), wake: make(chan ports.SecurityState, 1), state: &atomic.Uint64{}}
973 o.flipped = sr.flips
974 securityGate(t, o, sr.state)
975 o.SecurityChanges = sr.wake
976 r := portsmocks.NewMockRenderer(t)
977 pipeBuf := func() ports.DMABuf {
978 f, w, _ := os.Pipe()
979 w.Close()
980 return ports.DMABuf{Planes: []ports.DMABufPlane{{File: f}}}
981 }
982 r.EXPECT().SetHDR(float64(0)).Return().Maybe()
983 r.EXPECT().ExportTargets(2, mock.Anything, false).Return([]ports.DMABuf{pipeBuf(), pipeBuf()}, nil).Once()
984 r.EXPECT().UseTarget(mock.Anything).Return()
985 r.EXPECT().Render(mock.Anything, mock.Anything).Return(nil, nil)
986 r.EXPECT().Close().Return().Once()
987 k.EXPECT().addFB(mock.Anything, uint32(fourccXRGB)).Return(70, nil).Twice()
988 k.EXPECT().createBlob(mock.Anything).Return(99, nil)
989 k.EXPECT().destroyBlob(mock.Anything).Return(nil).Maybe()
990 k.EXPECT().rmFB(mock.Anything).Return(nil).Maybe()
991 sr.frameCommit = func() int {
992 commitMu.Lock()
993 defer commitMu.Unlock()
994 n := 0
995 for _, c := range *commits {
996 if c.user&3 == userFrame {
997 n++
998 }
999 }
1000 return n
1001 }
1002 sr.lastCommit = func() commitRec {
1003 commitMu.Lock()
1004 defer commitMu.Unlock()
1005 return (*commits)[len(*commits)-1]
1006 }
1007 ctx, cancel := context.WithCancel(context.Background())
1008 done := make(chan error, 1)
1009 go func() {
1010 done <- o.Run(ctx, func(int, int) (ports.Renderer, error) { return r, nil }, nil, make(chan bool), sr.scenes, sr.contents.in(), sr.contents.ret, nil, discardSink(t), nil, nil)
1011 }()
1012 // stop ends Run: its state is ours to read afterwards.
1013 sr.stop = func() {
1014 cancel()
1015 if err := <-done; err != nil {
1016 t.Fatal(err)
1017 }
1018 }
1019 return sr
1020}
1021
1022// Run: a frame wanted while the previous one is in flight is stamped when
1023// it becomes dirty, and the commit that follows takes the stamp.
1024func TestRunStampsWhenFrameWasWanted(t *testing.T) {
1025 sr := startStampRun(t)
1026 scene := ports.Scene{OutputWidth: 200, OutputHeight: 100, Windows: []ports.SceneWindow{{ID: 1, Rect: ports.Rect{W: 10, H: 10}}}}
1027 sr.scenes <- scene
1028 waitFor(t, func() bool { return sr.frameCommit() == 1 })
1029 // Frame 1 is in flight: a new scene arrives and waits.
1030 last := sr.lastCommit()
1031 scene.Seq = 2
1032 before := monotonic()
1033 sr.scenes <- scene
1034 sr.contents.send(ports.SurfaceContent{ID: 1, Seq: 1, SHM: &ports.SHMBuffer{}}) // Run went round its loop
1035 flipSent := monotonic()
1036 sr.flips <- flipEvent{crtc: tCrtc, user: last.user, when: time.Second, seq: 1}
1037 waitFor(t, func() bool { return sr.frameCommit() == 2 })
1038 sr.stop()
1039 if sr.o.wantedAt != 0 {
1040 t.Fatalf("stamp left after the commit: %s", sr.o.wantedAt)
1041 }
1042 f := sr.o.frame.pendingFrame
1043 if !f.frame || f.wantedAt < before || f.wantedAt >= flipSent {
1044 t.Fatalf("second frame (%v) wanted at %s, scene sent at %s, flip sent at %s", f.frame, f.wantedAt, before, flipSent)
1045 }
1046}
1047
1048// A change of security state drops the scene, and with it the stamp of
1049// the frame it wanted.
1050func TestRunSecurityResetClearsWantedAt(t *testing.T) {
1051 sr := startStampRun(t)
1052 scene := ports.Scene{OutputWidth: 200, OutputHeight: 100, Windows: []ports.SceneWindow{{ID: 1, Rect: ports.Rect{W: 10, H: 10}}}}
1053 sr.scenes <- scene
1054 waitFor(t, func() bool { return sr.frameCommit() == 1 })
1055 scene.Seq = 2
1056 sr.scenes <- scene
1057 sr.contents.send(ports.SurfaceContent{ID: 1, Seq: 1, SHM: &ports.SHMBuffer{}}) // stamped by now
1058 // A new security generation, not protected.
1059 sr.state.Store(2)
1060 sr.wake <- ports.SecurityState{}
1061 sr.contents.send(ports.SurfaceContent{ID: 1, Seq: 2, SHM: &ports.SHMBuffer{}}) // Run went round and reset
1062 sr.stop()
1063 if sr.o.wantedAt != 0 {
1064 t.Fatalf("stamp kept across the security reset: %s", sr.o.wantedAt)
1065 }
1066}
1067
1068// A scene whose Seq the output already holds draws nothing new: no frame,
1069// until a scene with a new Seq arrives.
1070func TestOutputSameSeqRendersOnce(t *testing.T) {
1071 sr := startStampRun(t)
1072 scene := ports.Scene{Seq: 5, OutputWidth: 200, OutputHeight: 100, Windows: []ports.SceneWindow{{ID: 1, Rect: ports.Rect{W: 10, H: 10}}}}
1073 sr.scenes <- scene
1074 waitFor(t, func() bool { return sr.frameCommit() == 1 })
1075 sr.flips <- flipEvent{crtc: tCrtc, user: sr.lastCommit().user, when: time.Second, seq: 1}
1076 sr.scenes <- scene
1077 // Unshown content only sends Run round its loop: the flip and the held
1078 // scene are handled by the time the second one is taken.
1079 sr.contents.send(ports.SurfaceContent{ID: 99, Seq: 1, SHM: &ports.SHMBuffer{}})
1080 sr.contents.send(ports.SurfaceContent{ID: 99, Seq: 2, SHM: &ports.SHMBuffer{}})
1081 if n := sr.frameCommit(); n != 1 {
1082 t.Fatalf("%d frames after a scene with the held Seq", n)
1083 }
1084 scene.Seq = 6
1085 sr.scenes <- scene
1086 waitFor(t, func() bool { return sr.frameCommit() == 2 })
1087 sr.stop()
1088}
1089
1090// Run: the empty content of a window that unmapped while the scene still
1091// draws it (its close animation) keeps the last content for the renderer,
1092// without its Acquire fence; a scene that no longer lists the window drops
1093// it.
1094func TestRunKeepsLastContentOfLeavingWindow(t *testing.T) {
1095 o, k, _ := testOutput(t)
1096 o.cursor, o.tearing = nil, false
1097 r := portsmocks.NewMockRenderer(t)
1098 pipeBuf := func() ports.DMABuf {
1099 f, w, _ := os.Pipe()
1100 w.Close()
1101 return ports.DMABuf{Planes: []ports.DMABufPlane{{File: f}}}
1102 }
1103 var mu sync.Mutex
1104 var last map[ports.WindowID]ports.SurfaceContent
1105 var lastSeq uint64
1106 r.EXPECT().SetHDR(float64(0)).Return().Maybe()
1107 r.EXPECT().ExportTargets(2, mock.Anything, false).Return([]ports.DMABuf{pipeBuf(), pipeBuf()}, nil).Once()
1108 r.EXPECT().UseTarget(mock.Anything).Return()
1109 r.EXPECT().Render(mock.Anything, mock.Anything).RunAndReturn(func(s ports.Scene, c map[ports.WindowID]ports.SurfaceContent) (*os.File, error) {
1110 mu.Lock()
1111 last, lastSeq = maps.Clone(c), s.Seq
1112 mu.Unlock()
1113 f, w, _ := os.Pipe()
1114 _, _ = w.Write([]byte{1})
1115 w.Close()
1116 return f, nil
1117 })
1118 r.EXPECT().Close().Return().Once()
1119 k.EXPECT().addFB(mock.Anything, uint32(fourccXRGB)).Return(70, nil).Twice()
1120 k.EXPECT().createBlob(mock.Anything).Return(99, nil)
1121 k.EXPECT().destroyBlob(mock.Anything).Return(nil).Maybe()
1122 k.EXPECT().rmFB(mock.Anything).Return(nil).Maybe()
1123 // drawn waits for a frame of scene seq and returns what it drew for
1124 // window 1.
1125 drawn := func(seq uint64, cond func(ports.SurfaceContent, bool) bool) (ports.SurfaceContent, bool) {
1126 t.Helper()
1127 var c ports.SurfaceContent
1128 var ok bool
1129 waitFor(t, func() bool {
1130 mu.Lock()
1131 defer mu.Unlock()
1132 c, ok = last[1]
1133 return lastSeq == seq && cond(c, ok)
1134 })
1135 return c, ok
1136 }
1137 scenes := make(chan ports.Scene, 1)
1138 contents := newContentFeed(t, 1)
1139 ctx, cancel := context.WithCancel(context.Background())
1140 done := make(chan error, 1)
1141 go func() {
1142 done <- o.Run(ctx, func(int, int) (ports.Renderer, error) { return r, nil }, nil, make(chan bool), scenes, contents.in(), contents.ret, nil, discardSink(t), nil, nil)
1143 }()
1144 acq, acqW, _ := os.Pipe()
1145 defer acq.Close()
1146 defer acqW.Close()
1147 win := ports.SceneWindow{ID: 1, Rect: ports.Rect{W: 10, H: 10}}
1148 scenes <- ports.Scene{Seq: 1, OutputWidth: 200, OutputHeight: 100, Windows: []ports.SceneWindow{win}}
1149 full := ports.SurfaceContent{ID: 1, Seq: 1, Width: 10, Height: 10, DMABuf: &ports.DMABuf{ID: 5, Planes: []ports.DMABufPlane{{}}}, Acquire: acq}
1150 contents.send(full)
1151 drawn(1, func(c ports.SurfaceContent, ok bool) bool { return ok && c.Acquire == acq })
1152 // The window unmaps: its empty content arrives before the scene that
1153 // draws it leaving (fading). The last content stays, fence dropped.
1154 contents.send(ports.SurfaceContent{ID: 1, Seq: 2})
1155 kept, _ := drawn(1, func(c ports.SurfaceContent, ok bool) bool { return !ok || c.Acquire == nil })
1156 if kept.DMABuf != full.DMABuf || kept.Seq != 1 {
1157 t.Fatalf("frame after the empty content drew %+v, want the last content", kept)
1158 }
1159 // A leaving frame: still drawn from the kept content.
1160 win.Fade = 0.5
1161 scenes <- ports.Scene{Seq: 2, OutputWidth: 200, OutputHeight: 100, Windows: []ports.SceneWindow{win}}
1162 if c, ok := drawn(2, func(ports.SurfaceContent, bool) bool { return true }); !ok || c.DMABuf != full.DMABuf {
1163 t.Fatalf("the leaving frame drew %+v (ok %t), want the kept content", c, ok)
1164 }
1165 // The scene no longer lists it: gone.
1166 scenes <- ports.Scene{Seq: 3, OutputWidth: 200, OutputHeight: 100}
1167 if c, ok := drawn(3, func(ports.SurfaceContent, bool) bool { return true }); ok {
1168 t.Fatalf("the kept content %+v survived a scene without the window", c)
1169 }
1170 cancel()
1171 if err := <-done; err != nil {
1172 t.Fatal(err)
1173 }
1174}