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